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 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
27#include "nilorea/n_gui.h"
28
29#ifdef HAVE_CJSON
30#include "cJSON.h"
31#endif
32
33/* =========================================================================
34 * INTERNAL HELPERS
35 * ========================================================================= */
36
38static double _clamp(double v, double lo, double hi) {
39 if (v < lo) return lo;
40 if (v > hi) return hi;
41 return v;
42}
43
47static double _slider_snap_value(double val, double min_val, double max_val, double step) {
48 if (step <= 0.0) step = 1.0;
49 double steps = round((val - min_val) / step);
50 double snapped = min_val + steps * step;
51 if (snapped < min_val) snapped = min_val;
52 if (snapped > max_val) snapped = max_val;
53 return snapped;
54}
55
58N_GUI_TEXT_DIMS n_gui_get_text_dims(ALLEGRO_FONT* font, const char* text) {
59 N_GUI_TEXT_DIMS d = {0, 0, 0, 0};
60 if (font && text) al_get_text_dimensions(font, text, &d.x, &d.y, &d.w, &d.h);
61 return d;
62}
63
65static int _is_focusable_type(int type) {
66 return type == N_GUI_TYPE_TEXTAREA ||
67 type == N_GUI_TYPE_SLIDER ||
68 type == N_GUI_TYPE_CHECKBOX ||
69 type == N_GUI_TYPE_LISTBOX ||
70 type == N_GUI_TYPE_RADIOLIST ||
71 type == N_GUI_TYPE_COMBOBOX ||
72 type == N_GUI_TYPE_SCROLLBAR ||
73 type == N_GUI_TYPE_DROPMENU;
74}
75
78 if (ctx->focused_widget_id < 0) return NULL;
79 list_foreach(wnode, ctx->windows) {
80 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
81 if (!win || !(win->state & N_GUI_WIN_OPEN)) continue;
82 list_foreach(wgn, win->widgets) {
83 const N_GUI_WIDGET* w = (const N_GUI_WIDGET*)wgn->ptr;
84 if (w && w->id == ctx->focused_widget_id) return win;
85 }
86 }
87 return NULL;
88}
89
92static void _normalize_crlf(char* s) {
93 if (!s) return;
94 char* dst = s;
95 for (char* src = s; *src; src++) {
96 if (*src != '\r') *dst++ = *src;
97 }
98 *dst = '\0';
99}
100
105static float _text_w(ALLEGRO_FONT* font, const char* text) {
106 if (font && text) return (float)al_get_text_width(font, text);
107 return 0.0f;
108}
109
111static float _win_tbh(N_GUI_WINDOW* win) {
112 return (win->flags & N_GUI_WIN_FRAMELESS) ? 0.0f : win->titlebar_h;
113}
114
116static int _point_in_rect(float px, float py, float rx, float ry, float rw, float rh) {
117 return (px >= rx && px <= rx + rw && py >= ry && py <= ry + rh);
118}
119
131static float _scrollbar_calc_scroll(float mouse, float track_start, float track_length, float viewport, float content, float thumb_min) {
132 float ratio = viewport / content;
133 if (ratio > 1.0f) ratio = 1.0f;
134 float thumb = ratio * track_length;
135 if (thumb < thumb_min) thumb = thumb_min;
136 float max_scroll = content - viewport;
137 float track_range = track_length - thumb;
138 if (track_range <= 0 || max_scroll <= 0) return 0;
139 float pos_ratio = (mouse - track_start - thumb / 2.0f) / track_range;
140 if (pos_ratio < 0) pos_ratio = 0;
141 if (pos_ratio > 1) pos_ratio = 1;
142 return pos_ratio * max_scroll;
143}
144
154static int _scrollbar_calc_scroll_int(float mouse, float track_start, float track_length, int visible_items, int total_items, float thumb_min) {
155 int max_off = total_items - visible_items;
156 if (max_off <= 0) return 0;
157 float scroll = _scrollbar_calc_scroll(mouse, track_start, track_length,
158 (float)visible_items, (float)total_items, thumb_min);
159 int offset = (int)(scroll + 0.5f);
160 if (offset < 0) offset = 0;
161 if (offset > max_off) offset = max_off;
162 return offset;
163}
164
173static N_GUI_WINDOW* _find_widget_window(N_GUI_CTX* ctx, int wgt_id, float* ox, float* oy) {
174 list_foreach(wnode, ctx->windows) {
175 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
176 if (!(win->state & N_GUI_WIN_OPEN)) continue;
177 list_foreach(wgn, win->widgets) {
178 if (((N_GUI_WIDGET*)wgn->ptr)->id == wgt_id) {
179 *ox = win->x - win->scroll_x;
180 *oy = win->y + _win_tbh(win) - win->scroll_y;
181 return win;
182 }
183 }
184 }
185 *ox = 0;
186 *oy = 0;
187 return NULL;
188}
189
191static void _destroy_widget(void* ptr) {
192 if (!ptr) return;
193 N_GUI_WIDGET* w = (N_GUI_WIDGET*)ptr;
194 if (w->data) {
195 /* free inner allocations for types that have them */
196 if (w->type == N_GUI_TYPE_LISTBOX) {
198 FreeNoLog(ld->items);
199 } else if (w->type == N_GUI_TYPE_RADIOLIST) {
201 FreeNoLog(rd->items);
202 } else if (w->type == N_GUI_TYPE_COMBOBOX) {
204 FreeNoLog(cd->items);
205 } else if (w->type == N_GUI_TYPE_DROPMENU) {
207 FreeNoLog(dd->entries);
208 } else if (w->type == N_GUI_TYPE_TEXTAREA) {
210 FreeNoLog(td->text);
211 }
212 FreeNoLog(w->data);
213 }
214 FreeNoLog(w);
215}
216
218static void _destroy_window(void* ptr) {
219 if (!ptr) return;
220 N_GUI_WINDOW* win = (N_GUI_WINDOW*)ptr;
221 if (win->widgets) {
222 list_destroy(&win->widgets);
223 }
224 FreeNoLog(win);
225}
226
228static LIST_NODE* _find_window_node(N_GUI_CTX* ctx, int window_id) {
229 __n_assert(ctx, return NULL);
230 __n_assert(ctx->windows, return NULL);
231 list_foreach(node, ctx->windows) {
232 const N_GUI_WINDOW* win = (const N_GUI_WINDOW*)node->ptr;
233 if (win && win->id == window_id) return node;
234 }
235 return NULL;
236}
237
240 __n_assert(ctx, return);
241 __n_assert(w, return);
242 char key[32];
243 snprintf(key, sizeof(key), "%d", w->id);
244 ht_put_ptr(ctx->widgets_by_id, key, w, NULL, NULL);
245}
246
248static int _items_grow(N_GUI_LISTITEM** items, const size_t* nb, size_t* cap) {
249 if (*nb >= *cap) {
250 size_t new_cap = (*cap == 0) ? 8 : (*cap) * 2;
251 if (new_cap < *cap) return 0; /* overflow */
252 N_GUI_LISTITEM* tmp = NULL;
253 Malloc(tmp, N_GUI_LISTITEM, new_cap);
254 if (!tmp) return 0;
255 if (*items && *nb > 0) {
256 memcpy(tmp, *items, (*nb) * sizeof(N_GUI_LISTITEM));
257 }
258 FreeNoLog(*items);
259 *items = tmp;
260 *cap = new_cap;
261 }
262 return 1;
263}
264
266static N_GUI_WIDGET* _new_widget(N_GUI_CTX* ctx, int type, float x, float y, float w, float h) {
267 __n_assert(ctx, return NULL);
268 N_GUI_WIDGET* wgt = NULL;
269 Malloc(wgt, N_GUI_WIDGET, 1);
270 __n_assert(wgt, return NULL);
271 wgt->id = ctx->next_widget_id++;
272 wgt->type = type;
273 wgt->x = x;
274 wgt->y = y;
275 wgt->w = w;
276 wgt->h = h;
277 wgt->state = N_GUI_STATE_IDLE;
278 wgt->visible = 1;
279 wgt->enabled = 1;
280 wgt->theme = ctx->default_theme;
281 wgt->font = NULL;
282 wgt->data = NULL;
283 return wgt;
284}
285
286/* =========================================================================
287 * THEME
288 * ========================================================================= */
289
295 N_GUI_THEME t;
296 t.bg_normal = al_map_rgba(50, 50, 60, 230);
297 t.bg_hover = al_map_rgba(70, 70, 85, 240);
298 t.bg_active = al_map_rgba(90, 90, 110, 250);
299 t.border_normal = al_map_rgba(120, 120, 140, 255);
300 t.border_hover = al_map_rgba(160, 160, 180, 255);
301 t.border_active = al_map_rgba(200, 200, 220, 255);
302 t.text_normal = al_map_rgba(220, 220, 220, 255);
303 t.text_hover = al_map_rgba(255, 255, 255, 255);
304 t.text_active = al_map_rgba(255, 255, 255, 255);
305 t.border_thickness = 1.0f;
306 t.corner_rx = 4.0f;
307 t.corner_ry = 4.0f;
308 t.selection_color = al_map_rgba(50, 100, 200, 120);
309 return t;
310}
311
329 ALLEGRO_COLOR bg,
330 ALLEGRO_COLOR bg_hover,
331 ALLEGRO_COLOR bg_active,
332 ALLEGRO_COLOR border,
333 ALLEGRO_COLOR border_hover,
334 ALLEGRO_COLOR border_active,
335 ALLEGRO_COLOR text,
336 ALLEGRO_COLOR text_hover,
337 ALLEGRO_COLOR text_active,
338 float border_thickness,
339 float corner_rx,
340 float corner_ry) {
341 N_GUI_THEME t;
342 t.bg_normal = bg;
343 t.bg_hover = bg_hover;
344 t.bg_active = bg_active;
345 t.border_normal = border;
346 t.border_hover = border_hover;
347 t.border_active = border_active;
348 t.text_normal = text;
349 t.text_hover = text_hover;
350 t.text_active = text_active;
351 t.border_thickness = border_thickness;
352 t.corner_rx = corner_rx;
353 t.corner_ry = corner_ry;
354 t.selection_color = al_map_rgba(50, 100, 200, 120);
355 return t;
356}
357
358/* =========================================================================
359 * STYLE DEFAULTS
360 * ========================================================================= */
361
366 N_GUI_STYLE s;
367
368 /* window chrome */
369 s.titlebar_h = 28.0f;
370 s.min_win_w = 120.0f;
371 s.min_win_h = 60.0f;
372 s.title_padding = 8.0f;
373 s.title_max_w_reserve = 16.0f;
374
375 /* window auto-scrollbar */
376 s.scrollbar_size = 12.0f;
377 s.scrollbar_thumb_min = 16.0f;
380 s.scrollbar_track_color = al_map_rgba(40, 40, 50, 200);
381 s.scrollbar_thumb_color = al_map_rgba(120, 120, 140, 220);
382
383 /* global display scrollbar */
384 s.global_scrollbar_size = 14.0f;
389 s.global_scrollbar_track_color = al_map_rgba(30, 30, 40, 200);
390 s.global_scrollbar_thumb_color = al_map_rgba(100, 100, 120, 220);
391 s.global_scrollbar_thumb_border_color = al_map_rgba(140, 140, 160, 255);
392
393 /* resize grip */
394 s.grip_size = 12.0f;
395 s.grip_line_thickness = 1.0f;
396 s.grip_color = al_map_rgba(160, 160, 180, 200);
397
398 /* slider */
399 s.slider_track_size = 6.0f;
400 s.slider_track_corner_r = 3.0f;
402 s.slider_handle_min_r = 4.0f;
406
407 /* text area */
408 s.textarea_padding = 4.0f;
409 s.textarea_cursor_width = 2.0f;
411
412 /* checkbox */
413 s.checkbox_max_size = 20.0f;
414 s.checkbox_mark_margin = 4.0f;
416 s.checkbox_label_gap = 10.0f;
417 s.checkbox_label_offset = 6.0f;
418
419 /* radio list */
420 s.radio_circle_min_r = 4.0f;
422 s.radio_inner_offset = 3.0f;
423 s.radio_label_gap = 6.0f;
424
425 /* list items */
430 s.item_text_padding = 6.0f;
431 s.item_selection_inset = 1.0f;
432 s.item_height_pad = 4.0f;
433
434 /* dropdown arrow */
435 s.dropdown_arrow_reserve = 16.0f;
437 s.dropdown_arrow_half_h = 3.0f;
438 s.dropdown_arrow_half_w = 5.0f;
440
441 /* label */
442 s.label_padding = 4.0f;
444 s.link_color_normal = al_map_rgba(80, 140, 220, 255);
445 s.link_color_hover = al_map_rgba(120, 180, 255, 255);
446
447 /* scroll step */
448 s.scroll_step = 20.0f;
449 s.global_scroll_step = 30.0f;
450
451 /* combobox auto-width cap */
452 s.combobox_max_dropdown_width = 0.0f; /* 0 = clamp to display width */
453
454 return s;
455}
456
457/* =========================================================================
458 * CONTEXT
459 * ========================================================================= */
460
466N_GUI_CTX* n_gui_new_ctx(ALLEGRO_FONT* default_font) {
467 __n_assert(default_font, return NULL);
468 N_GUI_CTX* ctx = NULL;
469 Malloc(ctx, N_GUI_CTX, 1);
470 __n_assert(ctx, return NULL);
472 if (!ctx->windows) {
473 n_log(LOG_ERR, "n_gui_new_ctx: failed to allocate window list");
474 Free(ctx);
475 return NULL;
476 }
477 ctx->widgets_by_id = new_ht(256);
478 if (!ctx->widgets_by_id) {
479 n_log(LOG_ERR, "n_gui_new_ctx: failed to allocate widget hash table");
480 list_destroy(&ctx->windows);
481 Free(ctx);
482 return NULL;
483 }
484 ctx->next_widget_id = 1;
485 ctx->next_window_id = 1;
486 ctx->default_font = default_font;
488 ctx->focused_widget_id = -1;
489 ctx->mouse_x = 0;
490 ctx->mouse_y = 0;
491 ctx->mouse_b1 = 0;
492 ctx->mouse_b1_prev = 0;
493 ctx->open_combobox_id = -1;
494 ctx->scrollbar_drag_widget_id = -1;
495 ctx->open_dropmenu_id = -1;
496 ctx->display_w = 0;
497 ctx->display_h = 0;
498 ctx->global_scroll_x = 0;
499 ctx->global_scroll_y = 0;
500 ctx->gui_bounds_w = 0;
501 ctx->gui_bounds_h = 0;
502 ctx->dpi_scale = 1.0f;
503 ctx->virtual_w = 0;
504 ctx->virtual_h = 0;
505 ctx->gui_scale = 1.0f;
506 ctx->gui_offset_x = 0;
507 ctx->gui_offset_y = 0;
508 ctx->global_vscroll_drag = 0;
509 ctx->global_hscroll_drag = 0;
510 ctx->style = n_gui_default_style();
511 ctx->display = NULL;
512 ctx->selected_label_id = -1;
514 ctx->ref_display_w = 0.0f;
515 ctx->ref_display_h = 0.0f;
516 return ctx;
517}
518
524 __n_assert(ctx && *ctx, return);
525 if ((*ctx)->windows) {
526 list_destroy(&(*ctx)->windows);
527 }
528 if ((*ctx)->widgets_by_id) {
529 destroy_ht(&(*ctx)->widgets_by_id);
530 }
531 Free((*ctx));
532}
533
534/* =========================================================================
535 * ADAPTIVE RESIZE HELPERS
536 * ========================================================================= */
537
540 float dw = ctx->ref_display_w;
541 float dh = ctx->ref_display_h;
542 if (dw <= 0) dw = ctx->display_w;
543 if (dh <= 0) dh = ctx->display_h;
544 if (dw <= 0 || dh <= 0) return;
545
546 win->norm_x = win->x / dw;
547 win->norm_y = win->y / dh;
548 win->norm_w = win->w / dw;
549 win->norm_h = win->h / dh;
550
551 /* capture widget normalized coords relative to window */
552 if (win->resize_policy == N_GUI_WIN_RESIZE_SCALE && win->w > 0 && win->h > 0) {
553 list_foreach(wnode, win->widgets) {
554 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wnode->ptr;
555 wgt->norm_x = wgt->x / win->w;
556 wgt->norm_y = wgt->y / win->h;
557 wgt->norm_w = wgt->w / win->w;
558 wgt->norm_h = wgt->h / win->h;
559 }
560 }
561}
562
565 if (win->w > 0 && win->h > 0) {
566 wgt->norm_x = wgt->x / win->w;
567 wgt->norm_y = wgt->y / win->h;
568 wgt->norm_w = wgt->w / win->w;
569 wgt->norm_h = wgt->h / win->h;
570 }
571}
572
573/* =========================================================================
574 * WINDOW MANAGEMENT
575 * ========================================================================= */
576
581int n_gui_add_window(N_GUI_CTX* ctx, const char* title, float x, float y, float w, float h) {
582 __n_assert(ctx, return -1);
583 N_GUI_WINDOW* win = NULL;
584 Malloc(win, N_GUI_WINDOW, 1);
585 __n_assert(win, return -1);
586 win->id = ctx->next_window_id++;
587 if (title) {
588 strncpy(win->title, title, N_GUI_ID_MAX - 1);
589 win->title[N_GUI_ID_MAX - 1] = '\0';
590 } else {
591 win->title[0] = '\0';
592 }
593 win->x = x;
594 win->y = y;
595 win->w = w;
596 win->h = h;
597 win->titlebar_h = ctx->style.titlebar_h;
598 win->state = N_GUI_WIN_OPEN;
600 if (!win->widgets) {
601 n_log(LOG_ERR, "n_gui_add_window: failed to allocate widget list");
602 Free(win);
603 return -1;
604 }
605 win->theme = ctx->default_theme;
606 win->font = NULL;
607 win->drag_ox = 0;
608 win->drag_oy = 0;
609 win->min_w = ctx->style.min_win_w;
610 win->min_h = ctx->style.min_win_h;
611 win->flags = 0;
612 win->scroll_x = 0.0f;
613 win->scroll_y = 0.0f;
614 win->content_w = 0.0f;
615 win->content_h = 0.0f;
617 win->z_value = 0;
618 win->autofit_flags = 0;
619 win->autofit_border = 0.0f;
620 win->autofit_origin_x = x;
621 win->autofit_origin_y = y;
623 win->norm_x = 0.0f;
624 win->norm_y = 0.0f;
625 win->norm_w = 0.0f;
626 win->norm_h = 0.0f;
630 }
631 return win->id;
632}
633
638 LIST_NODE* node = _find_window_node(ctx, window_id);
639 if (node) return (N_GUI_WINDOW*)node->ptr;
640 return NULL;
641}
642
646void n_gui_close_window(N_GUI_CTX* ctx, int window_id) {
647 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
648 if (win) win->state &= ~N_GUI_WIN_OPEN;
649}
650
654void n_gui_open_window(N_GUI_CTX* ctx, int window_id) {
655 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
656 if (win) win->state |= N_GUI_WIN_OPEN;
657}
658
662void n_gui_minimize_window(N_GUI_CTX* ctx, int window_id) {
663 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
664 if (win) win->state ^= N_GUI_WIN_MINIMISED;
665}
666
667/* Return the group ordinal for a window's z-order:
668 * 0 = ALWAYS_BEHIND, 1 = FIXED, 2 = NORMAL, 3 = ALWAYS_ON_TOP.
669 * Ordering is ALWAYS_BEHIND -> FIXED -> NORMAL -> ALWAYS_ON_TOP. */
670static int _zorder_group(const N_GUI_WINDOW* w) {
671 if (!w) return 2; /* treat NULL as NORMAL */
672 switch (w->z_order) {
674 return 0;
676 return 1;
678 return 3;
679 default:
680 return 2;
681 }
682}
683
690 if (!ctx || !ctx->windows || ctx->windows->nb_items < 2) return;
691
692 /* Sort by (group, z_value) so that group ordering is always respected:
693 * ALWAYS_BEHIND windows always precede FIXED, which always precede NORMAL,
694 * which always precede ALWAYS_ON_TOP, regardless of z_value magnitude.
695 * Within the FIXED group, windows are ordered by z_value (lower = behind).
696 * Within the same (group, z_value), list order is preserved (stable sort). */
697
698 /* collect into array for stable sort */
699 int n = (int)ctx->windows->nb_items;
700 N_GUI_WINDOW** arr = NULL;
701 Malloc(arr, N_GUI_WINDOW*, (size_t)n);
702 if (!arr) return;
703
704 int idx = 0;
705 list_foreach(node, ctx->windows) {
706 arr[idx++] = (N_GUI_WINDOW*)node->ptr;
707 }
708
709 /* stable insertion sort by (group, z_value) */
710 for (int i = 1; i < n; i++) {
711 N_GUI_WINDOW* tmp = arr[i];
712 int gi = _zorder_group(tmp);
713 int j = i - 1;
714 while (j >= 0) {
715 int gj = _zorder_group(arr[j]);
716 /* z_value secondary sort applies only within the FIXED group (ordinal 1) */
717 if (gj > gi || (gj == gi && gi == 1 && arr[j]->z_value > tmp->z_value)) {
718 arr[j + 1] = arr[j];
719 j--;
720 } else {
721 break;
722 }
723 }
724 arr[j + 1] = tmp;
725 }
726
727 /* rebuild list in sorted order */
728 /* detach all nodes without destroying windows */
729 while (ctx->windows->nb_items > 0) {
730 LIST_NODE* node = ctx->windows->start;
731 node->destroy_func = NULL;
732 remove_list_node_f(ctx->windows, node);
733 }
734 for (int i = 0; i < n; i++) {
735 list_push(ctx->windows, arr[i], _destroy_window);
736 }
737 Free(arr);
738}
739
745void n_gui_raise_window(N_GUI_CTX* ctx, int window_id) {
746 __n_assert(ctx, return);
747 LIST_NODE* node = _find_window_node(ctx, window_id);
748 if (!node) return;
749 N_GUI_WINDOW* win = (N_GUI_WINDOW*)node->ptr;
750 /* only NORMAL windows can be freely raised */
751 if (win->z_order != N_GUI_ZORDER_NORMAL) return;
752 /* remove from list without destroying, then push to end */
753 node->destroy_func = NULL;
754 remove_list_node_f(ctx->windows, node);
757}
758
763void n_gui_lower_window(N_GUI_CTX* ctx, int window_id) {
764 __n_assert(ctx, return);
765 LIST_NODE* node = _find_window_node(ctx, window_id);
766 if (!node) return;
767 N_GUI_WINDOW* win = (N_GUI_WINDOW*)node->ptr;
768 if (win->z_order != N_GUI_ZORDER_NORMAL) return;
769 node->destroy_func = NULL;
770 remove_list_node_f(ctx->windows, node);
773}
774
779void n_gui_window_set_zorder(N_GUI_CTX* ctx, int window_id, int z_mode, int z_value) {
780 __n_assert(ctx, return);
781 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
782 if (!win) return;
783 if (z_mode != N_GUI_ZORDER_NORMAL && z_mode != N_GUI_ZORDER_ALWAYS_ON_TOP &&
784 z_mode != N_GUI_ZORDER_ALWAYS_BEHIND && z_mode != N_GUI_ZORDER_FIXED) {
785 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, or N_GUI_ZORDER_FIXED), falling back to N_GUI_ZORDER_NORMAL", z_mode, window_id);
786 z_mode = N_GUI_ZORDER_NORMAL;
787 }
788 win->z_order = z_mode;
789 win->z_value = z_value;
791}
792
796int n_gui_window_get_zorder(N_GUI_CTX* ctx, int window_id) {
797 __n_assert(ctx, return N_GUI_ZORDER_NORMAL);
798 const N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
799 if (!win) return N_GUI_ZORDER_NORMAL;
800 return win->z_order;
801}
802
806int n_gui_window_get_zvalue(N_GUI_CTX* ctx, int window_id) {
807 __n_assert(ctx, return 0);
808 const N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
809 if (!win) return 0;
810 return win->z_value;
811}
812
817int n_gui_add_window_auto(N_GUI_CTX* ctx, const char* title, float x, float y) {
818 /* start with minimum size, will be expanded by n_gui_window_autosize */
819 return n_gui_add_window(ctx, title, x, y, ctx->style.min_win_w, ctx->style.min_win_h);
820}
821
825void n_gui_toggle_window(N_GUI_CTX* ctx, int window_id) {
826 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
827 if (win) win->state ^= N_GUI_WIN_OPEN;
828}
829
834int n_gui_window_is_open(N_GUI_CTX* ctx, int window_id) {
835 const N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
836 if (win) return (win->state & N_GUI_WIN_OPEN) ? 1 : 0;
837 return 0;
838}
839
843void n_gui_window_set_flags(N_GUI_CTX* ctx, int window_id, int flags) {
844 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
845 if (win) win->flags = flags;
846}
847
852int n_gui_window_get_flags(N_GUI_CTX* ctx, int window_id) {
853 const N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
854 if (win) return win->flags;
855 return 0;
856}
857
862void n_gui_window_autosize(N_GUI_CTX* ctx, int window_id) {
863 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
864 if (!win || !win->widgets) return;
865
866 float max_right = 0.0f;
867 float max_bottom = 0.0f;
868 float pad = 20.0f;
869
870 list_foreach(node, win->widgets) {
871 const N_GUI_WIDGET* wgt = (const N_GUI_WIDGET*)node->ptr;
872 if (!wgt) continue;
873 float r = wgt->x + wgt->w;
874 float b = wgt->y + wgt->h;
875 if (r > max_right) max_right = r;
876 if (b > max_bottom) max_bottom = b;
877 }
878
879 float new_w = max_right + pad;
880 float new_h = max_bottom + pad + _win_tbh(win);
881 if (new_w < win->min_w) new_w = win->min_w;
882 if (new_h < win->min_h) new_h = win->min_h;
883 win->w = new_w;
884 win->h = new_h;
885}
886
891void n_gui_window_set_autofit(N_GUI_CTX* ctx, int window_id, int autofit_flags, float border) {
892 __n_assert(ctx, return);
893 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
894 if (!win) {
895 n_log(LOG_ERR, "n_gui_window_set_autofit: window %d not found", window_id);
896 return;
897 }
898 win->autofit_flags = autofit_flags;
899 win->autofit_border = border;
900 /* capture current position as the insertion point for centering */
901 win->autofit_origin_x = win->x;
902 win->autofit_origin_y = win->y;
903}
904
910void n_gui_window_apply_autofit(N_GUI_CTX* ctx, int window_id) {
911 __n_assert(ctx, return);
912 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
913 if (!win) return;
914 if (win->autofit_flags == 0) return;
915 if (!win->widgets) return;
916
917 /* compute content bounding box from visible widgets */
918 float max_right = 0.0f;
919 float max_bottom = 0.0f;
920 list_foreach(node, win->widgets) {
921 const N_GUI_WIDGET* wgt = (const N_GUI_WIDGET*)node->ptr;
922 if (!wgt || !wgt->visible) continue;
923 float r = wgt->x + wgt->w;
924 float b = wgt->y + wgt->h;
925 if (r > max_right) max_right = r;
926 if (b > max_bottom) max_bottom = b;
927 }
928
929 float border = win->autofit_border;
930 float tbh = _win_tbh(win);
931
932 int center = (win->autofit_flags & N_GUI_AUTOFIT_CENTER) ? 1 : 0;
933
934 /* apply width adjustment */
935 if (win->autofit_flags & N_GUI_AUTOFIT_W) {
936 float needed_w = max_right + border * 2.0f;
937 if (needed_w < win->min_w) needed_w = win->min_w;
938 if (center) {
939 win->x = win->autofit_origin_x - needed_w / 2.0f;
940 } else if (win->autofit_flags & N_GUI_AUTOFIT_EXPAND_LEFT) {
941 win->x -= (needed_w - win->w);
942 }
943 win->w = needed_w;
944 }
945
946 /* apply height adjustment */
947 if (win->autofit_flags & N_GUI_AUTOFIT_H) {
948 float needed_h = max_bottom + border * 2.0f + tbh;
949 if (needed_h < win->min_h) needed_h = win->min_h;
950 if (center) {
951 win->y = win->autofit_origin_y - needed_h / 2.0f;
952 } else if (win->autofit_flags & N_GUI_AUTOFIT_EXPAND_UP) {
953 win->y -= (needed_h - win->h);
954 }
955 win->h = needed_h;
956 }
957
958 /* update content extents for scrollbar calculations */
959 win->content_w = max_right;
960 win->content_h = max_bottom;
961}
962
964static void _window_update_content_size(N_GUI_WINDOW* win, ALLEGRO_FONT* default_font) {
965 if (!win || !win->widgets) return;
966 float max_right = 0.0f;
967 float max_bottom = 0.0f;
968 list_foreach(node, win->widgets) {
969 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)node->ptr;
970 if (!wgt || !wgt->visible) continue;
971 float r = wgt->x + wgt->w;
972 float b = wgt->y + wgt->h;
973 /* for labels in auto-scrollbar windows, use actual text pixel width
974 * so that horizontal scrollbar appears when text overflows */
975 if (wgt->type == N_GUI_TYPE_LABEL && (win->flags & N_GUI_WIN_AUTO_SCROLLBAR)) {
976 const N_GUI_LABEL_DATA* lb = (const N_GUI_LABEL_DATA*)wgt->data;
977 if (lb && lb->text[0] && lb->align != N_GUI_ALIGN_JUSTIFIED) {
978 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
979 if (font) {
980 float tw = _text_w(font, lb->text);
981 float text_r = wgt->x + tw + 8.0f; /* small padding */
982 if (text_r > r) r = text_r;
983 }
984 }
985 }
986 if (r > max_right) max_right = r;
987 if (b > max_bottom) max_bottom = b;
988 }
989 win->content_w = max_right;
990 win->content_h = max_bottom;
991}
992
993/* =========================================================================
994 * WIDGET CREATION
995 * ========================================================================= */
996
1001int 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) {
1002 __n_assert(ctx, return -1);
1003 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1004 __n_assert(win, return -1);
1005
1006 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_BUTTON, x, y, w, h);
1007 __n_assert(wgt, return -1);
1008
1009 N_GUI_BUTTON_DATA* bd = NULL;
1010 Malloc(bd, N_GUI_BUTTON_DATA, 1);
1011 __n_assert(bd, Free(wgt); return -1);
1012 if (label) {
1013 strncpy(bd->label, label, N_GUI_ID_MAX - 1);
1014 bd->label[N_GUI_ID_MAX - 1] = '\0';
1015 }
1016 bd->bitmap = NULL;
1017 bd->bitmap_hover = NULL;
1018 bd->bitmap_active = NULL;
1019 bd->shape = shape;
1020 bd->toggle_mode = 0;
1021 bd->toggled = 0;
1022 bd->keycode = 0;
1023 bd->key_modifiers = 0;
1024 bd->on_click = on_click;
1025 bd->user_data = user_data;
1026 wgt->data = bd;
1027 wgt->norm_x = 0.0f;
1028 wgt->norm_y = 0.0f;
1029 wgt->norm_w = 0.0f;
1030 wgt->norm_h = 0.0f;
1031
1032 list_push(win->widgets, wgt, _destroy_widget);
1034 _register_widget(ctx, wgt);
1035 return wgt->id;
1036}
1037
1042int 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) {
1043 int id = n_gui_add_button(ctx, window_id, label, x, y, w, h, N_GUI_SHAPE_BITMAP, on_click, user_data);
1044 if (id < 0) return -1;
1045 N_GUI_WIDGET* wgt = n_gui_get_widget(ctx, id);
1046 if (wgt && wgt->data) {
1048 bd->bitmap = normal;
1049 bd->bitmap_hover = hover;
1050 bd->bitmap_active = active;
1051 }
1052 return id;
1053}
1054
1070int 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) {
1071 int id = n_gui_add_button(ctx, window_id, label, x, y, w, h, shape, on_click, user_data);
1072 if (id < 0) return -1;
1073 N_GUI_WIDGET* wgt = n_gui_get_widget(ctx, id);
1074 if (wgt && wgt->data) {
1076 bd->toggle_mode = 1;
1077 bd->toggled = initial_state ? 1 : 0;
1078 }
1079 return id;
1080}
1081
1086int n_gui_button_is_toggled(N_GUI_CTX* ctx, int widget_id) {
1087 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1088 if (w && w->type == N_GUI_TYPE_BUTTON && w->data) {
1089 return ((N_GUI_BUTTON_DATA*)w->data)->toggled;
1090 }
1091 return 0;
1092}
1093
1097void n_gui_button_set_toggled(N_GUI_CTX* ctx, int widget_id, int toggled) {
1098 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1099 if (w && w->type == N_GUI_TYPE_BUTTON && w->data) {
1100 ((N_GUI_BUTTON_DATA*)w->data)->toggled = toggled ? 1 : 0;
1101 }
1102}
1103
1110void n_gui_button_set_toggle_mode(N_GUI_CTX* ctx, int widget_id, int toggle_mode) {
1111 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1112 if (w && w->type == N_GUI_TYPE_BUTTON && w->data) {
1114 bd->toggle_mode = toggle_mode ? 1 : 0;
1115 if (!bd->toggle_mode) bd->toggled = 0;
1116 }
1117}
1118
1129void n_gui_button_set_keycode(N_GUI_CTX* ctx, int widget_id, int keycode, int modifiers) {
1130 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1131 if (w && w->type == N_GUI_TYPE_BUTTON && w->data) {
1133 bd->keycode = keycode;
1134 bd->key_modifiers = modifiers & N_GUI_KEY_MOD_MASK;
1135 }
1136}
1137
1139void n_gui_button_set_keycode_focused(N_GUI_CTX* ctx, int widget_id, int keycode, int modifiers, const int* sources, int source_count) {
1140 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1141 if (w && w->type == N_GUI_TYPE_BUTTON && w->data) {
1143 bd->keycode = keycode;
1144 bd->key_modifiers = modifiers & N_GUI_KEY_MOD_MASK;
1145 bd->key_focus_only = 1;
1146 int count = source_count < N_GUI_KEY_SOURCES_MAX ? source_count : N_GUI_KEY_SOURCES_MAX;
1147 for (int i = 0; i < count; i++) {
1148 bd->key_sources[i] = sources[i];
1149 }
1150 if (count < N_GUI_KEY_SOURCES_MAX) {
1151 bd->key_sources[count] = -1;
1152 }
1153 }
1154}
1155
1160int 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) {
1161 __n_assert(ctx, return -1);
1162 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1163 __n_assert(win, return -1);
1164
1165 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_SLIDER, x, y, w, h);
1166 __n_assert(wgt, return -1);
1167
1168 N_GUI_SLIDER_DATA* sd = NULL;
1169 Malloc(sd, N_GUI_SLIDER_DATA, 1);
1170 __n_assert(sd, Free(wgt); return -1);
1171 if (mode == N_GUI_SLIDER_PERCENT) {
1172 sd->min_val = 0.0;
1173 sd->max_val = 100.0;
1174 } else {
1175 sd->min_val = min_val;
1176 sd->max_val = max_val;
1177 }
1178 sd->step = 0.0; /* 0 = no step constraint (treated as continuous, snaps with step=1 only when explicitly set) */
1179 sd->value = _clamp(initial, sd->min_val, sd->max_val);
1180 sd->mode = mode;
1182 sd->on_change = on_change;
1183 sd->user_data = user_data;
1184 wgt->data = sd;
1185 wgt->norm_x = 0.0f;
1186 wgt->norm_y = 0.0f;
1187 wgt->norm_w = 0.0f;
1188 wgt->norm_h = 0.0f;
1189
1190 list_push(win->widgets, wgt, _destroy_widget);
1192 _register_widget(ctx, wgt);
1193 return wgt->id;
1194}
1195
1200int 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) {
1201 int id = n_gui_add_slider(ctx, window_id, x, y, w, h, min_val, max_val, initial, mode, on_change, user_data);
1202 if (id < 0) return -1;
1203 N_GUI_WIDGET* wgt = n_gui_get_widget(ctx, id);
1204 if (wgt && wgt->data) {
1205 ((N_GUI_SLIDER_DATA*)wgt->data)->orientation = N_GUI_SLIDER_V;
1206 }
1207 return id;
1208}
1209
1214int 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) {
1215 __n_assert(ctx, return -1);
1216 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1217 __n_assert(win, return -1);
1218
1219 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_TEXTAREA, x, y, w, h);
1220 __n_assert(wgt, return -1);
1221
1222 N_GUI_TEXTAREA_DATA* td = NULL;
1224 __n_assert(td, Free(wgt); return -1);
1225 memset(td, 0, sizeof(*td));
1226 td->char_limit = (char_limit > 0) ? char_limit : N_GUI_TEXT_MAX - 1;
1227 td->text_alloc = td->char_limit + 1;
1228 Malloc(td->text, char, td->text_alloc);
1229 __n_assert(td->text, Free(td); Free(wgt); return -1);
1230 td->text[0] = '\0';
1231 td->text_len = 0;
1232 td->multiline = multiline;
1233 td->cursor_pos = 0;
1234 td->sel_start = 0;
1235 td->sel_end = 0;
1236 td->scroll_y = 0;
1237 td->scroll_x = 0.0f;
1238 td->cursor_time = 0.0;
1239 td->bg_bitmap = NULL;
1240 td->mask_char = '\0';
1241 td->on_change = on_change;
1242 td->user_data = user_data;
1243 wgt->data = td;
1244 wgt->norm_x = 0.0f;
1245 wgt->norm_y = 0.0f;
1246 wgt->norm_w = 0.0f;
1247 wgt->norm_h = 0.0f;
1248
1249 list_push(win->widgets, wgt, _destroy_widget);
1251 _register_widget(ctx, wgt);
1252 return wgt->id;
1253}
1254
1259int 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) {
1260 __n_assert(ctx, return -1);
1261 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1262 __n_assert(win, return -1);
1263
1264 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_CHECKBOX, x, y, w, h);
1265 __n_assert(wgt, return -1);
1266
1267 N_GUI_CHECKBOX_DATA* cd = NULL;
1269 __n_assert(cd, Free(wgt); return -1);
1270 if (label) {
1271 strncpy(cd->label, label, N_GUI_ID_MAX - 1);
1272 cd->label[N_GUI_ID_MAX - 1] = '\0';
1273 }
1274 cd->checked = initial_checked ? 1 : 0;
1275 cd->on_toggle = on_toggle;
1276 cd->user_data = user_data;
1277 wgt->data = cd;
1278 wgt->norm_x = 0.0f;
1279 wgt->norm_y = 0.0f;
1280 wgt->norm_w = 0.0f;
1281 wgt->norm_h = 0.0f;
1282
1283 list_push(win->widgets, wgt, _destroy_widget);
1285 _register_widget(ctx, wgt);
1286 return wgt->id;
1287}
1288
1293int 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) {
1294 __n_assert(ctx, return -1);
1295 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1296 __n_assert(win, return -1);
1297
1298 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_SCROLLBAR, x, y, w, h);
1299 __n_assert(wgt, return -1);
1300
1301 N_GUI_SCROLLBAR_DATA* sb = NULL;
1303 __n_assert(sb, Free(wgt); return -1);
1304 sb->orientation = orientation;
1305 sb->content_size = content_size > 0 ? content_size : 1;
1306 sb->viewport_size = viewport_size > 0 ? viewport_size : 1;
1307 sb->scroll_pos = 0;
1308 sb->shape = shape;
1309 sb->on_scroll = on_scroll;
1310 sb->user_data = user_data;
1311 wgt->data = sb;
1312 wgt->norm_x = 0.0f;
1313 wgt->norm_y = 0.0f;
1314 wgt->norm_w = 0.0f;
1315 wgt->norm_h = 0.0f;
1316
1317 list_push(win->widgets, wgt, _destroy_widget);
1319 _register_widget(ctx, wgt);
1320 return wgt->id;
1321}
1322
1336int 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) {
1337 __n_assert(ctx, return -1);
1338 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1339 __n_assert(win, return -1);
1340
1341 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_LISTBOX, x, y, w, h);
1342 __n_assert(wgt, return -1);
1343
1344 N_GUI_LISTBOX_DATA* ld = NULL;
1345 Malloc(ld, N_GUI_LISTBOX_DATA, 1);
1346 __n_assert(ld, Free(wgt); return -1);
1347 ld->items = NULL;
1348 ld->nb_items = 0;
1349 ld->items_capacity = 0;
1350 ld->selection_mode = selection_mode;
1351 ld->scroll_offset = 0;
1353 ld->on_select = on_select;
1354 ld->user_data = user_data;
1355 wgt->data = ld;
1356 wgt->norm_x = 0.0f;
1357 wgt->norm_y = 0.0f;
1358 wgt->norm_w = 0.0f;
1359 wgt->norm_h = 0.0f;
1360
1361 list_push(win->widgets, wgt, _destroy_widget);
1363 _register_widget(ctx, wgt);
1364 return wgt->id;
1365}
1366
1371int 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) {
1372 __n_assert(ctx, return -1);
1373 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1374 __n_assert(win, return -1);
1375
1376 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_RADIOLIST, x, y, w, h);
1377 __n_assert(wgt, return -1);
1378
1379 N_GUI_RADIOLIST_DATA* rd = NULL;
1381 __n_assert(rd, Free(wgt); return -1);
1382 rd->items = NULL;
1383 rd->nb_items = 0;
1384 rd->items_capacity = 0;
1385 rd->selected_index = -1;
1386 rd->scroll_offset = 0;
1388 rd->on_select = on_select;
1389 rd->user_data = user_data;
1390 wgt->data = rd;
1391 wgt->norm_x = 0.0f;
1392 wgt->norm_y = 0.0f;
1393 wgt->norm_w = 0.0f;
1394 wgt->norm_h = 0.0f;
1395
1396 list_push(win->widgets, wgt, _destroy_widget);
1398 _register_widget(ctx, wgt);
1399 return wgt->id;
1400}
1401
1406int 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) {
1407 __n_assert(ctx, return -1);
1408 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1409 __n_assert(win, return -1);
1410
1411 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_COMBOBOX, x, y, w, h);
1412 __n_assert(wgt, return -1);
1413
1414 N_GUI_COMBOBOX_DATA* cd = NULL;
1416 __n_assert(cd, Free(wgt); return -1);
1417 cd->items = NULL;
1418 cd->nb_items = 0;
1419 cd->items_capacity = 0;
1420 cd->selected_index = -1;
1421 cd->is_open = 0;
1422 cd->scroll_offset = 0;
1423 cd->item_height = h;
1425 cd->on_select = on_select;
1426 cd->user_data = user_data;
1427 cd->flags = 0;
1428 wgt->data = cd;
1429 wgt->norm_x = 0.0f;
1430 wgt->norm_y = 0.0f;
1431 wgt->norm_w = 0.0f;
1432 wgt->norm_h = 0.0f;
1433
1434 list_push(win->widgets, wgt, _destroy_widget);
1436 _register_widget(ctx, wgt);
1437 return wgt->id;
1438}
1439
1446void n_gui_combobox_set_flags(N_GUI_CTX* ctx, int widget_id, int flags) {
1447 __n_assert(ctx, return);
1448 N_GUI_WIDGET* wgt = n_gui_get_widget(ctx, widget_id);
1449 __n_assert(wgt, return);
1450 if (wgt->type != N_GUI_TYPE_COMBOBOX) return;
1452 __n_assert(cd, return);
1453 cd->flags = flags;
1454}
1455
1468int 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) {
1469 __n_assert(ctx, return -1);
1470 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1471 __n_assert(win, return -1);
1472
1473 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_IMAGE, x, y, w, h);
1474 __n_assert(wgt, return -1);
1475
1476 N_GUI_IMAGE_DATA* id = NULL;
1477 Malloc(id, N_GUI_IMAGE_DATA, 1);
1478 __n_assert(id, Free(wgt); return -1);
1479 id->bitmap = bitmap;
1480 id->scale_mode = scale_mode;
1481 wgt->data = id;
1482 wgt->norm_x = 0.0f;
1483 wgt->norm_y = 0.0f;
1484 wgt->norm_w = 0.0f;
1485 wgt->norm_h = 0.0f;
1486
1487 list_push(win->widgets, wgt, _destroy_widget);
1489 _register_widget(ctx, wgt);
1490 return wgt->id;
1491}
1492
1497int n_gui_add_label(N_GUI_CTX* ctx, int window_id, const char* text, float x, float y, float w, float h, int align) {
1498 __n_assert(ctx, return -1);
1499 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
1500 __n_assert(win, return -1);
1501
1502 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_LABEL, x, y, w, h);
1503 __n_assert(wgt, return -1);
1504
1505 N_GUI_LABEL_DATA* lb = NULL;
1506 Malloc(lb, N_GUI_LABEL_DATA, 1);
1507 __n_assert(lb, Free(wgt); return -1);
1508 lb->text[0] = '\0';
1509 if (text) {
1510 strncpy(lb->text, text, N_GUI_TEXT_MAX - 1);
1511 lb->text[N_GUI_TEXT_MAX - 1] = '\0';
1512 _normalize_crlf(lb->text); /* normalize CRLF for Allegro5 compatibility */
1513 }
1514 lb->link[0] = '\0';
1515 lb->align = align;
1516 lb->scroll_y = 0.0f;
1517 lb->sel_start = -1;
1518 lb->sel_end = -1;
1519 lb->sel_dragging = 0;
1520 lb->on_link_click = NULL;
1521 lb->user_data = NULL;
1522 wgt->data = lb;
1523 wgt->norm_x = 0.0f;
1524 wgt->norm_y = 0.0f;
1525 wgt->norm_w = 0.0f;
1526 wgt->norm_h = 0.0f;
1527
1528 list_push(win->widgets, wgt, _destroy_widget);
1530 _register_widget(ctx, wgt);
1531 return wgt->id;
1532}
1533
1538int 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) {
1539 int wid = n_gui_add_label(ctx, window_id, text, x, y, w, h, align);
1540 if (wid < 0) return -1;
1541 N_GUI_WIDGET* wgt = n_gui_get_widget(ctx, wid);
1542 if (wgt && wgt->data) {
1544 if (link) {
1545 strncpy(lb->link, link, N_GUI_TEXT_MAX - 1);
1546 lb->link[N_GUI_TEXT_MAX - 1] = '\0';
1547 }
1549 lb->user_data = user_data;
1550 }
1551 return wid;
1552}
1553
1554/* =========================================================================
1555 * WIDGET ACCESS
1556 * ========================================================================= */
1557
1562 __n_assert(ctx, return NULL);
1563 char key[32];
1564 snprintf(key, sizeof(key), "%d", widget_id);
1565 void* ptr = NULL;
1566 if (ht_get_ptr(ctx->widgets_by_id, key, &ptr) == TRUE && ptr) {
1567 return (N_GUI_WIDGET*)ptr;
1568 }
1569 return NULL;
1570}
1571
1575void n_gui_set_widget_theme(N_GUI_CTX* ctx, int widget_id, N_GUI_THEME theme) {
1576 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1577 if (w) w->theme = theme;
1578}
1579
1583void n_gui_set_widget_visible(N_GUI_CTX* ctx, int widget_id, int visible) {
1584 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1585 if (w) w->visible = visible;
1586}
1587
1592void n_gui_set_widget_enabled(N_GUI_CTX* ctx, int widget_id, int enabled) {
1593 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1594 if (w) w->enabled = enabled;
1595}
1596
1601int n_gui_is_widget_enabled(N_GUI_CTX* ctx, int widget_id) {
1602 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1603 if (w) return w->enabled;
1604 return 0;
1605}
1606
1613void n_gui_set_focus(N_GUI_CTX* ctx, int widget_id) {
1614 __n_assert(ctx, return);
1615
1616 /* clear focus from previously focused widget */
1617 if (ctx->focused_widget_id >= 0) {
1619 if (prev) prev->state &= ~N_GUI_STATE_FOCUSED;
1620 }
1621
1622 if (widget_id < 0) {
1623 ctx->focused_widget_id = -1;
1624 return;
1625 }
1626
1627 N_GUI_WIDGET* wgt = n_gui_get_widget(ctx, widget_id);
1628 if (!wgt) {
1629 n_log(LOG_ERR, "n_gui_set_focus: widget %d not found", widget_id);
1630 ctx->focused_widget_id = -1;
1631 return;
1632 }
1633
1634 ctx->focused_widget_id = widget_id;
1635 wgt->state |= N_GUI_STATE_FOCUSED;
1636
1637 /* reset cursor blink timer for textareas */
1638 if (wgt->type == N_GUI_TYPE_TEXTAREA && wgt->data) {
1640 td->cursor_time = al_get_time();
1641 }
1642
1643 /* raise the parent window */
1644 list_foreach(wnode, ctx->windows) {
1645 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
1646 if (!win || !win->widgets) continue;
1647 list_foreach(wgnode, win->widgets) {
1648 const N_GUI_WIDGET* w = (const N_GUI_WIDGET*)wgnode->ptr;
1649 if (w && w->id == widget_id) {
1650 if (win->state & N_GUI_WIN_OPEN) {
1651 n_gui_raise_window(ctx, win->id);
1652 }
1653 return;
1654 }
1655 }
1656 }
1657}
1658
1659/* ---- slider helpers ---- */
1660
1667double n_gui_slider_get_value(N_GUI_CTX* ctx, int widget_id) {
1668 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1669 if (w && w->type == N_GUI_TYPE_SLIDER && w->data) {
1670 return ((N_GUI_SLIDER_DATA*)w->data)->value;
1671 }
1672 return 0.0;
1673}
1674
1681void n_gui_slider_set_value(N_GUI_CTX* ctx, int widget_id, double value) {
1682 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1683 if (w && w->type == N_GUI_TYPE_SLIDER && w->data) {
1685 if (sd->step > 0.0)
1686 sd->value = _slider_snap_value(value, sd->min_val, sd->max_val, sd->step);
1687 else
1688 sd->value = _clamp(value, sd->min_val, sd->max_val);
1689 }
1690}
1691
1699void n_gui_slider_set_range(N_GUI_CTX* ctx, int widget_id, double min_val, double max_val) {
1700 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1701 if (w && w->type == N_GUI_TYPE_SLIDER && w->data) {
1703 sd->min_val = min_val;
1704 sd->max_val = max_val;
1705 if (sd->step > 0.0)
1706 sd->value = _slider_snap_value(sd->value, min_val, max_val, sd->step);
1707 else
1708 sd->value = _clamp(sd->value, min_val, max_val);
1709 }
1710}
1711
1719void n_gui_slider_set_step(N_GUI_CTX* ctx, int widget_id, double step) {
1720 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1721 if (w && w->type == N_GUI_TYPE_SLIDER && w->data) {
1723 sd->step = step;
1724 if (step > 0.0) {
1725 sd->value = _slider_snap_value(sd->value, sd->min_val, sd->max_val, step);
1726 }
1727 }
1728}
1729
1730/* ---- textarea helpers ---- */
1731
1738const char* n_gui_textarea_get_text(N_GUI_CTX* ctx, int widget_id) {
1739 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1740 if (w && w->type == N_GUI_TYPE_TEXTAREA && w->data) {
1741 return ((N_GUI_TEXTAREA_DATA*)w->data)->text;
1742 }
1743 return "";
1744}
1745
1752void n_gui_textarea_set_text(N_GUI_CTX* ctx, int widget_id, const char* text) {
1753 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1754 if (w && w->type == N_GUI_TYPE_TEXTAREA && w->data) {
1756 if (text) {
1757 strncpy(td->text, text, td->char_limit);
1758 td->text[td->char_limit] = '\0';
1759 _normalize_crlf(td->text); /* normalize CRLF for Allegro5 compatibility */
1760 td->text_len = strlen(td->text);
1761 td->cursor_pos = td->text_len;
1762 } else {
1763 td->text[0] = '\0';
1764 td->text_len = 0;
1765 td->cursor_pos = 0;
1766 }
1767 td->scroll_x = 0.0f;
1768 }
1769}
1770
1771/* ---- checkbox helpers ---- */
1772
1779int n_gui_checkbox_is_checked(N_GUI_CTX* ctx, int widget_id) {
1780 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1781 if (w && w->type == N_GUI_TYPE_CHECKBOX && w->data) {
1782 return ((N_GUI_CHECKBOX_DATA*)w->data)->checked;
1783 }
1784 return 0;
1785}
1786
1793void n_gui_checkbox_set_checked(N_GUI_CTX* ctx, int widget_id, int checked) {
1794 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1795 if (w && w->type == N_GUI_TYPE_CHECKBOX && w->data) {
1796 ((N_GUI_CHECKBOX_DATA*)w->data)->checked = checked ? 1 : 0;
1797 }
1798}
1799
1800/* ---- scrollbar helpers ---- */
1801
1808double n_gui_scrollbar_get_pos(N_GUI_CTX* ctx, int widget_id) {
1809 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1810 if (w && w->type == N_GUI_TYPE_SCROLLBAR && w->data) {
1811 return ((N_GUI_SCROLLBAR_DATA*)w->data)->scroll_pos;
1812 }
1813 return 0.0;
1814}
1815
1822void n_gui_scrollbar_set_pos(N_GUI_CTX* ctx, int widget_id, double pos) {
1823 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1824 if (w && w->type == N_GUI_TYPE_SCROLLBAR && w->data) {
1826 double max_scroll = sb->content_size - sb->viewport_size;
1827 if (max_scroll < 0) max_scroll = 0;
1828 sb->scroll_pos = _clamp(pos, 0, max_scroll);
1829 }
1830}
1831
1839void n_gui_scrollbar_set_sizes(N_GUI_CTX* ctx, int widget_id, double content_size, double viewport_size) {
1840 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1841 if (w && w->type == N_GUI_TYPE_SCROLLBAR && w->data) {
1843 sb->content_size = content_size > 0 ? content_size : 1;
1844 sb->viewport_size = viewport_size > 0 ? viewport_size : 1;
1845 double max_scroll = sb->content_size - sb->viewport_size;
1846 if (max_scroll < 0) max_scroll = 0;
1847 sb->scroll_pos = _clamp(sb->scroll_pos, 0, max_scroll);
1848 }
1849}
1850
1851/* ---- listbox helpers ---- */
1852
1860int n_gui_listbox_add_item(N_GUI_CTX* ctx, int widget_id, const char* text) {
1861 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1862 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) return -1;
1864 if (!_items_grow(&ld->items, &ld->nb_items, &ld->items_capacity)) return -1;
1865 N_GUI_LISTITEM* item = &ld->items[ld->nb_items];
1866 item->text[0] = '\0';
1867 if (text) {
1868 strncpy(item->text, text, N_GUI_ID_MAX - 1);
1869 item->text[N_GUI_ID_MAX - 1] = '\0';
1870 }
1871 item->selected = 0;
1872 ld->nb_items++;
1873 return (int)(ld->nb_items - 1);
1874}
1875
1883int n_gui_listbox_remove_item(N_GUI_CTX* ctx, int widget_id, int index) {
1884 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1885 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) return -1;
1887 if (index < 0 || (size_t)index >= ld->nb_items) return -1;
1888 if ((size_t)index < ld->nb_items - 1) {
1889 memmove(&ld->items[index], &ld->items[index + 1],
1890 (ld->nb_items - (size_t)index - 1) * sizeof(N_GUI_LISTITEM));
1891 }
1892 ld->nb_items--;
1893 /* clamp scroll_offset after removal */
1894 if (ld->nb_items == 0) {
1895 ld->scroll_offset = 0;
1896 } else if (ld->scroll_offset > 0 && (size_t)ld->scroll_offset >= ld->nb_items) {
1897 ld->scroll_offset = (int)(ld->nb_items - 1);
1898 }
1899 return 0;
1900}
1901
1907void n_gui_listbox_clear(N_GUI_CTX* ctx, int widget_id) {
1908 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1909 if (w && w->type == N_GUI_TYPE_LISTBOX && w->data) {
1911 ld->nb_items = 0;
1912 ld->scroll_offset = 0;
1913 }
1914}
1915
1922int n_gui_listbox_get_count(N_GUI_CTX* ctx, int widget_id) {
1923 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1924 if (w && w->type == N_GUI_TYPE_LISTBOX && w->data) {
1925 return (int)((N_GUI_LISTBOX_DATA*)w->data)->nb_items;
1926 }
1927 return 0;
1928}
1929
1937const char* n_gui_listbox_get_item_text(N_GUI_CTX* ctx, int widget_id, int index) {
1938 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1939 if (w && w->type == N_GUI_TYPE_LISTBOX && w->data) {
1941 if (index >= 0 && (size_t)index < ld->nb_items) {
1942 return ld->items[index].text;
1943 }
1944 }
1945 return "";
1946}
1947
1954int n_gui_listbox_get_selected(N_GUI_CTX* ctx, int widget_id) {
1955 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1956 if (w && w->type == N_GUI_TYPE_LISTBOX && w->data) {
1957 const N_GUI_LISTBOX_DATA* ld = (const N_GUI_LISTBOX_DATA*)w->data;
1958 for (size_t i = 0; i < ld->nb_items; i++) {
1959 if (ld->items[i].selected) return (int)i;
1960 }
1961 }
1962 return -1;
1963}
1964
1972int n_gui_listbox_is_selected(N_GUI_CTX* ctx, int widget_id, int index) {
1973 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1974 if (w && w->type == N_GUI_TYPE_LISTBOX && w->data) {
1976 if (index >= 0 && (size_t)index < ld->nb_items) {
1977 return ld->items[index].selected;
1978 }
1979 }
1980 return 0;
1981}
1982
1990void n_gui_listbox_set_selected(N_GUI_CTX* ctx, int widget_id, int index, int selected) {
1991 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
1992 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) return;
1994 if (index < 0 || (size_t)index >= ld->nb_items) return;
1995 if (ld->selection_mode == N_GUI_SELECT_SINGLE && selected) {
1996 for (size_t i = 0; i < ld->nb_items; i++) ld->items[i].selected = 0;
1997 }
1998 ld->items[index].selected = selected ? 1 : 0;
1999}
2000
2002 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2003 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) return 0;
2004 const N_GUI_LISTBOX_DATA* ld = (const N_GUI_LISTBOX_DATA*)w->data;
2005 return ld->scroll_offset;
2006}
2007
2008void n_gui_listbox_set_scroll_offset(N_GUI_CTX* ctx, int widget_id, int offset) {
2009 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2010 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) return;
2012 int max_offset = (int)ld->nb_items - (int)(w->h / ld->item_height);
2013 if (max_offset < 0) max_offset = 0;
2014 if (offset < 0) offset = 0;
2015 if (offset > max_offset) offset = max_offset;
2016 ld->scroll_offset = offset;
2017}
2018
2019/* ---- radiolist helpers ---- */
2020
2028int n_gui_radiolist_add_item(N_GUI_CTX* ctx, int widget_id, const char* text) {
2029 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2030 if (!w || w->type != N_GUI_TYPE_RADIOLIST || !w->data) return -1;
2032 if (!_items_grow(&rd->items, &rd->nb_items, &rd->items_capacity)) return -1;
2033 N_GUI_LISTITEM* item = &rd->items[rd->nb_items];
2034 item->text[0] = '\0';
2035 if (text) {
2036 strncpy(item->text, text, N_GUI_ID_MAX - 1);
2037 item->text[N_GUI_ID_MAX - 1] = '\0';
2038 }
2039 item->selected = 0;
2040 rd->nb_items++;
2041 return (int)(rd->nb_items - 1);
2042}
2043
2049void n_gui_radiolist_clear(N_GUI_CTX* ctx, int widget_id) {
2050 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2051 if (w && w->type == N_GUI_TYPE_RADIOLIST && w->data) {
2053 rd->nb_items = 0;
2054 rd->selected_index = -1;
2055 rd->scroll_offset = 0;
2056 }
2057}
2058
2065int n_gui_radiolist_get_selected(N_GUI_CTX* ctx, int widget_id) {
2066 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2067 if (w && w->type == N_GUI_TYPE_RADIOLIST && w->data) {
2068 return ((N_GUI_RADIOLIST_DATA*)w->data)->selected_index;
2069 }
2070 return -1;
2071}
2072
2079void n_gui_radiolist_set_selected(N_GUI_CTX* ctx, int widget_id, int index) {
2080 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2081 if (w && w->type == N_GUI_TYPE_RADIOLIST && w->data) {
2083 if (index >= -1 && (index == -1 || (size_t)index < rd->nb_items)) {
2084 rd->selected_index = index;
2085 }
2086 }
2087}
2088
2089/* ---- combobox helpers ---- */
2090
2096void n_gui_combobox_clear(N_GUI_CTX* ctx, int widget_id) {
2097 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2098 if (!w || w->type != N_GUI_TYPE_COMBOBOX || !w->data) return;
2100 if (cd->items && cd->nb_items > 0) {
2101 memset(cd->items, 0, cd->nb_items * sizeof(N_GUI_LISTITEM));
2102 }
2103 cd->nb_items = 0;
2104 cd->selected_index = -1;
2105 cd->scroll_offset = 0;
2106 cd->is_open = 0;
2107}
2108
2109int n_gui_combobox_add_item(N_GUI_CTX* ctx, int widget_id, const char* text) {
2110 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2111 if (!w || w->type != N_GUI_TYPE_COMBOBOX || !w->data) return -1;
2113 if (!_items_grow(&cd->items, &cd->nb_items, &cd->items_capacity)) return -1;
2114 N_GUI_LISTITEM* item = &cd->items[cd->nb_items];
2115 item->text[0] = '\0';
2116 if (text) {
2117 strncpy(item->text, text, N_GUI_ID_MAX - 1);
2118 item->text[N_GUI_ID_MAX - 1] = '\0';
2119 }
2120 item->selected = 0;
2121 cd->nb_items++;
2122 return (int)(cd->nb_items - 1);
2123}
2124
2131int n_gui_combobox_get_selected(N_GUI_CTX* ctx, int widget_id) {
2132 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2133 if (w && w->type == N_GUI_TYPE_COMBOBOX && w->data) {
2134 return ((N_GUI_COMBOBOX_DATA*)w->data)->selected_index;
2135 }
2136 return -1;
2137}
2138
2145void n_gui_combobox_set_selected(N_GUI_CTX* ctx, int widget_id, int index) {
2146 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2147 if (w && w->type == N_GUI_TYPE_COMBOBOX && w->data) {
2149 if (index >= -1 && (index == -1 || (size_t)index < cd->nb_items)) {
2150 cd->selected_index = index;
2151 }
2152 }
2153}
2154
2155/* ---- image helpers ---- */
2156
2163void n_gui_image_set_bitmap(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* bitmap) {
2164 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2165 if (w && w->type == N_GUI_TYPE_IMAGE && w->data) {
2166 ((N_GUI_IMAGE_DATA*)w->data)->bitmap = bitmap;
2167 }
2168}
2169
2170/* ---- label helpers ---- */
2171
2178void n_gui_label_set_text(N_GUI_CTX* ctx, int widget_id, const char* text) {
2179 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2180 if (w && w->type == N_GUI_TYPE_LABEL && w->data) {
2182 if (text) {
2183 strncpy(lb->text, text, N_GUI_TEXT_MAX - 1);
2184 lb->text[N_GUI_TEXT_MAX - 1] = '\0';
2185 _normalize_crlf(lb->text); /* normalize CRLF for Allegro5 compatibility */
2186 } else {
2187 lb->text[0] = '\0';
2188 }
2189 }
2190}
2191
2198void n_gui_label_set_link(N_GUI_CTX* ctx, int widget_id, const char* link) {
2199 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2200 if (w && w->type == N_GUI_TYPE_LABEL && w->data) {
2202 if (link) {
2203 strncpy(lb->link, link, N_GUI_TEXT_MAX - 1);
2204 lb->link[N_GUI_TEXT_MAX - 1] = '\0';
2205 } else {
2206 lb->link[0] = '\0';
2207 }
2208 }
2209}
2210
2211/* =========================================================================
2212 * DROPDOWN MENU
2213 * ========================================================================= */
2214
2216static int _dropmenu_entries_grow(N_GUI_DROPMENU_ENTRY** entries, const size_t* nb, size_t* cap) {
2217 if (*nb >= *cap) {
2218 size_t new_cap = (*cap == 0) ? 8 : (*cap) * 2;
2219 if (new_cap < *cap) return 0; /* overflow */
2220 N_GUI_DROPMENU_ENTRY* tmp = NULL;
2221 Malloc(tmp, N_GUI_DROPMENU_ENTRY, new_cap);
2222 if (!tmp) return 0;
2223 if (*entries && *nb > 0) {
2224 memcpy(tmp, *entries, (*nb) * sizeof(N_GUI_DROPMENU_ENTRY));
2225 }
2226 FreeNoLog(*entries);
2227 *entries = tmp;
2228 *cap = new_cap;
2229 }
2230 return 1;
2231}
2232
2237int 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) {
2238 __n_assert(ctx, return -1);
2239 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2240 __n_assert(win, return -1);
2241
2242 N_GUI_WIDGET* wgt = _new_widget(ctx, N_GUI_TYPE_DROPMENU, x, y, w, h);
2243 __n_assert(wgt, return -1);
2244
2245 N_GUI_DROPMENU_DATA* dd = NULL;
2247 __n_assert(dd, Free(wgt); return -1);
2248 dd->label[0] = '\0';
2249 if (label) {
2250 strncpy(dd->label, label, N_GUI_ID_MAX - 1);
2251 dd->label[N_GUI_ID_MAX - 1] = '\0';
2252 }
2253 dd->entries = NULL;
2254 dd->nb_entries = 0;
2255 dd->entries_capacity = 0;
2256 dd->is_open = 0;
2257 dd->scroll_offset = 0;
2259 dd->item_height = h;
2260 dd->on_open = on_open;
2261 dd->on_open_user_data = on_open_user_data;
2262 wgt->data = dd;
2263 wgt->norm_x = 0.0f;
2264 wgt->norm_y = 0.0f;
2265 wgt->norm_w = 0.0f;
2266 wgt->norm_h = 0.0f;
2267
2268 list_push(win->widgets, wgt, _destroy_widget);
2270 _register_widget(ctx, wgt);
2271 return wgt->id;
2272}
2273
2278int 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) {
2279 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2280 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) return -1;
2282 if (!_dropmenu_entries_grow(&dd->entries, &dd->nb_entries, &dd->entries_capacity)) return -1;
2284 memset(e, 0, sizeof(*e));
2285 if (text) {
2286 strncpy(e->text, text, N_GUI_ID_MAX - 1);
2287 e->text[N_GUI_ID_MAX - 1] = '\0';
2288 }
2289 e->is_dynamic = 0;
2290 e->tag = tag;
2291 e->on_click = on_click;
2292 e->user_data = user_data;
2293 dd->nb_entries++;
2294 return (int)(dd->nb_entries - 1);
2295}
2296
2301int 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) {
2302 int idx = n_gui_dropmenu_add_entry(ctx, widget_id, text, tag, on_click, user_data);
2303 if (idx < 0)
2304 return idx;
2305
2306 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2307 if (!w) {
2308 n_log(LOG_ERR, "Invalid NULL widget for ctx %p widget_id %d", ctx, widget_id);
2309 return -1;
2310 }
2311
2313 if (!dd) {
2314 n_log(LOG_ERR, "Widget has NULL data for ctx %p widget_id %d", ctx, widget_id);
2315 return -1;
2316 }
2317
2318 dd->entries[idx].is_dynamic = 1;
2319
2320 return idx;
2321}
2322
2326void n_gui_dropmenu_clear_dynamic(N_GUI_CTX* ctx, int widget_id) {
2327 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2328 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) return;
2330 /* compact: keep only non-dynamic entries */
2331 size_t write = 0;
2332 for (size_t i = 0; i < dd->nb_entries; i++) {
2333 if (!dd->entries[i].is_dynamic) {
2334 if (write != i) {
2335 dd->entries[write] = dd->entries[i];
2336 }
2337 write++;
2338 }
2339 }
2340 dd->nb_entries = write;
2341 /* clamp scroll_offset after removal */
2342 if (dd->nb_entries == 0) {
2343 dd->scroll_offset = 0;
2344 } else {
2345 int max_vis = dd->max_visible > 0 ? dd->max_visible : 8;
2346 int dm_max = (int)dd->nb_entries - max_vis;
2347 if (dm_max < 0) dm_max = 0;
2348 if (dd->scroll_offset > dm_max) dd->scroll_offset = dm_max;
2349 }
2350}
2351
2355void n_gui_dropmenu_clear(N_GUI_CTX* ctx, int widget_id) {
2356 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2357 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) return;
2359 dd->nb_entries = 0;
2360 dd->scroll_offset = 0;
2361}
2362
2366void n_gui_dropmenu_set_entry_text(N_GUI_CTX* ctx, int widget_id, int index, const char* text) {
2367 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2368 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) return;
2370 if (index < 0 || (size_t)index >= dd->nb_entries) return;
2371 if (text) {
2372 strncpy(dd->entries[index].text, text, N_GUI_ID_MAX - 1);
2373 dd->entries[index].text[N_GUI_ID_MAX - 1] = '\0';
2374 }
2375}
2376
2380int n_gui_dropmenu_get_count(N_GUI_CTX* ctx, int widget_id) {
2381 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2382 if (w && w->type == N_GUI_TYPE_DROPMENU && w->data) {
2383 return (int)((N_GUI_DROPMENU_DATA*)w->data)->nb_entries;
2384 }
2385 return 0;
2386}
2387
2388/* =========================================================================
2389 * BITMAP SKINNING - setter functions
2390 * ========================================================================= */
2391
2397void n_gui_window_set_bitmaps(N_GUI_CTX* ctx, int window_id, ALLEGRO_BITMAP* bg, ALLEGRO_BITMAP* titlebar, int bg_scale_mode) {
2398 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
2399 if (!win) {
2400 n_log(LOG_WARNING, "n_gui_window_set_bitmaps: window %d not found", window_id);
2401 return;
2402 }
2403 win->bg_bitmap = bg;
2404 win->titlebar_bitmap = titlebar;
2405 win->bg_scale_mode = bg_scale_mode;
2406}
2407
2412void 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) {
2413 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2414 if (!w || w->type != N_GUI_TYPE_SLIDER || !w->data) {
2415 n_log(LOG_WARNING, "n_gui_slider_set_bitmaps: widget %d is not a slider", widget_id);
2416 return;
2417 }
2419 sd->track_bitmap = track;
2420 sd->fill_bitmap = fill;
2421 sd->handle_bitmap = handle;
2422 sd->handle_hover_bitmap = handle_hover;
2423 sd->handle_active_bitmap = handle_active;
2424}
2425
2430void 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) {
2431 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2432 if (!w || w->type != N_GUI_TYPE_SCROLLBAR || !w->data) {
2433 n_log(LOG_WARNING, "n_gui_scrollbar_set_bitmaps: widget %d is not a scrollbar", widget_id);
2434 return;
2435 }
2437 sb->track_bitmap = track;
2438 sb->thumb_bitmap = thumb;
2439 sb->thumb_hover_bitmap = thumb_hover;
2440 sb->thumb_active_bitmap = thumb_active;
2441}
2442
2447void n_gui_checkbox_set_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* box, ALLEGRO_BITMAP* box_checked, ALLEGRO_BITMAP* box_hover) {
2448 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2449 if (!w || w->type != N_GUI_TYPE_CHECKBOX || !w->data) {
2450 n_log(LOG_WARNING, "n_gui_checkbox_set_bitmaps: widget %d is not a checkbox", widget_id);
2451 return;
2452 }
2454 cd->box_bitmap = box;
2455 cd->box_checked_bitmap = box_checked;
2456 cd->box_hover_bitmap = box_hover;
2457}
2458
2463void n_gui_textarea_set_bitmap(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* bg) {
2464 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2465 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) {
2466 n_log(LOG_WARNING, "n_gui_textarea_set_bitmap: widget %d is not a textarea", widget_id);
2467 return;
2468 }
2469 ((N_GUI_TEXTAREA_DATA*)w->data)->bg_bitmap = bg;
2470}
2471
2477void n_gui_textarea_set_mask_char(N_GUI_CTX* ctx, int widget_id, char mask) {
2478 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2479 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) {
2480 n_log(LOG_WARNING, "n_gui_textarea_set_mask_char: widget %d is not a textarea", widget_id);
2481 return;
2482 }
2483 ((N_GUI_TEXTAREA_DATA*)w->data)->mask_char = mask;
2484}
2485
2490void n_gui_listbox_set_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* bg, ALLEGRO_BITMAP* item_bg, ALLEGRO_BITMAP* item_selected) {
2491 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2492 if (!w || w->type != N_GUI_TYPE_LISTBOX || !w->data) {
2493 n_log(LOG_WARNING, "n_gui_listbox_set_bitmaps: widget %d is not a listbox", widget_id);
2494 return;
2495 }
2497 ld->bg_bitmap = bg;
2498 ld->item_bg_bitmap = item_bg;
2499 ld->item_selected_bitmap = item_selected;
2500}
2501
2506void n_gui_radiolist_set_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* bg, ALLEGRO_BITMAP* item_bg, ALLEGRO_BITMAP* item_selected) {
2507 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2508 if (!w || w->type != N_GUI_TYPE_RADIOLIST || !w->data) {
2509 n_log(LOG_WARNING, "n_gui_radiolist_set_bitmaps: widget %d is not a radiolist", widget_id);
2510 return;
2511 }
2513 rd->bg_bitmap = bg;
2514 rd->item_bg_bitmap = item_bg;
2515 rd->item_selected_bitmap = item_selected;
2516}
2517
2522void n_gui_combobox_set_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* bg, ALLEGRO_BITMAP* item_bg, ALLEGRO_BITMAP* item_selected) {
2523 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2524 if (!w || w->type != N_GUI_TYPE_COMBOBOX || !w->data) {
2525 n_log(LOG_WARNING, "n_gui_combobox_set_bitmaps: widget %d is not a combobox", widget_id);
2526 return;
2527 }
2529 cd->bg_bitmap = bg;
2530 cd->item_bg_bitmap = item_bg;
2531 cd->item_selected_bitmap = item_selected;
2532}
2533
2538void n_gui_dropmenu_set_bitmaps(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* panel, ALLEGRO_BITMAP* item_hover) {
2539 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2540 if (!w || w->type != N_GUI_TYPE_DROPMENU || !w->data) {
2541 n_log(LOG_WARNING, "n_gui_dropmenu_set_bitmaps: widget %d is not a dropmenu", widget_id);
2542 return;
2543 }
2545 dd->panel_bitmap = panel;
2546 dd->item_hover_bitmap = item_hover;
2547}
2548
2553void n_gui_label_set_bitmap(N_GUI_CTX* ctx, int widget_id, ALLEGRO_BITMAP* bg) {
2554 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2555 if (!w || w->type != N_GUI_TYPE_LABEL || !w->data) {
2556 n_log(LOG_WARNING, "n_gui_label_set_bitmap: widget %d is not a label", widget_id);
2557 return;
2558 }
2559 ((N_GUI_LABEL_DATA*)w->data)->bg_bitmap = bg;
2560}
2561
2562/* =========================================================================
2563 * DRAWING - individual widget renderers
2564 * ========================================================================= */
2565
2568static void _draw_bitmap_scaled(ALLEGRO_BITMAP* bmp, float dx, float dy, float dw, float dh, int mode) {
2569 float bw = (float)al_get_bitmap_width(bmp);
2570 float bh = (float)al_get_bitmap_height(bmp);
2571 if (mode == N_GUI_IMAGE_STRETCH) {
2572 al_draw_scaled_bitmap(bmp, 0, 0, bw, bh, dx, dy, dw, dh, 0);
2573 } else if (mode == N_GUI_IMAGE_CENTER) {
2574 al_draw_bitmap(bmp, dx + (dw - bw) / 2.0f, dy + (dh - bh) / 2.0f, 0);
2575 } else {
2576 /* N_GUI_IMAGE_FIT: maintain aspect ratio */
2577 float sx = dw / bw;
2578 float sy = dh / bh;
2579 float s = (sx < sy) ? sx : sy;
2580 float rw = bw * s;
2581 float rh = bh * s;
2582 al_draw_scaled_bitmap(bmp, 0, 0, bw, bh, dx + (dw - rw) / 2.0f, dy + (dh - rh) / 2.0f, rw, rh, 0);
2583 }
2584}
2585
2589static ALLEGRO_BITMAP* _select_state_bitmap(int state, ALLEGRO_BITMAP* normal_bmp, ALLEGRO_BITMAP* hover_bmp, ALLEGRO_BITMAP* active_bmp) {
2590 if ((state & N_GUI_STATE_ACTIVE) && active_bmp) return active_bmp;
2591 if ((state & N_GUI_STATE_HOVER) && hover_bmp) return hover_bmp;
2592 if (normal_bmp) return normal_bmp;
2593 return NULL;
2594}
2595
2601static void _set_clipping_rect_transformed(int wx, int wy, int ww, int wh) {
2602 /* Transform all 4 corners from world space to screen space so that the
2603 * axis-aligned bounding box is computed correctly for any affine transform
2604 * (including negative scaling, rotation, or shear). */
2605 const ALLEGRO_TRANSFORM* tf = al_get_current_transform();
2606 float cx[4], cy[4];
2607 cx[0] = (float)wx;
2608 cy[0] = (float)wy;
2609 cx[1] = (float)(wx + ww);
2610 cy[1] = (float)wy;
2611 cx[2] = (float)wx;
2612 cy[2] = (float)(wy + wh);
2613 cx[3] = (float)(wx + ww);
2614 cy[3] = (float)(wy + wh);
2615 for (int i = 0; i < 4; i++)
2616 al_transform_coordinates(tf, &cx[i], &cy[i]);
2617
2618 float min_x = cx[0], max_x = cx[0];
2619 float min_y = cy[0], max_y = cy[0];
2620 for (int i = 1; i < 4; i++) {
2621 if (cx[i] < min_x) min_x = cx[i];
2622 if (cx[i] > max_x) max_x = cx[i];
2623 if (cy[i] < min_y) min_y = cy[i];
2624 if (cy[i] > max_y) max_y = cy[i];
2625 }
2626
2627 int new_x = (int)min_x;
2628 int new_y = (int)min_y;
2629 int new_w = (int)(max_x - min_x);
2630 int new_h = (int)(max_y - min_y);
2631
2632 /* intersect with the current (parent) clipping rectangle */
2633 int pcx, pcy, pcw, pch;
2634 al_get_clipping_rectangle(&pcx, &pcy, &pcw, &pch);
2635
2636 int right = new_x + new_w;
2637 int bottom = new_y + new_h;
2638 int prev_right = pcx + pcw;
2639 int prev_bottom = pcy + pch;
2640
2641 if (new_x < pcx) new_x = pcx;
2642 if (new_y < pcy) new_y = pcy;
2643 if (right > prev_right) right = prev_right;
2644 if (bottom > prev_bottom) bottom = prev_bottom;
2645
2646 new_w = right - new_x;
2647 new_h = bottom - new_y;
2648 if (new_w < 0) new_w = 0;
2649 if (new_h < 0) new_h = 0;
2650
2651 al_set_clipping_rectangle(new_x, new_y, new_w, new_h);
2652}
2653
2656static void _draw_text_truncated(ALLEGRO_FONT* font, ALLEGRO_COLOR color, float x, float y, float max_w, const char* text) {
2657 if (!font || !text || !text[0]) return;
2658 float tw = _text_w(font, text);
2659 if (tw <= max_w) {
2660 al_draw_text(font, color, x, y, 0, text);
2661 return;
2662 }
2663 /* find how many chars fit + "..." */
2664 float ellipsis_w = _text_w(font, "...");
2665 float avail = max_w - ellipsis_w;
2666 if (avail < 0) avail = 0;
2667 size_t len = strlen(text);
2668 char buf[N_GUI_TEXT_MAX];
2669 size_t fit = 0;
2670 for (size_t i = 0; i < len && i < (size_t)(N_GUI_TEXT_MAX - 5); i++) {
2671 buf[i] = text[i];
2672 buf[i + 1] = '\0';
2673 float cw = _text_w(font, buf);
2674 if (cw > avail) break;
2675 fit = i + 1;
2676 }
2677 buf[fit] = '.';
2678 buf[fit + 1] = '.';
2679 buf[fit + 2] = '.';
2680 buf[fit + 3] = '\0';
2681 al_draw_text(font, color, x, y, 0, buf);
2682}
2683
2688static void _draw_text_justified(ALLEGRO_FONT* font, ALLEGRO_COLOR color, float x, float y, float max_w, float max_h, const char* text) {
2689 if (!font || !text || !text[0]) return;
2690
2691 /* split text into words */
2692 char buf[N_GUI_TEXT_MAX];
2693 snprintf(buf, N_GUI_TEXT_MAX, "%s", text);
2694
2695 char* words[256];
2696 float word_widths[256];
2697 int nwords = 0;
2698 char* tok = strtok(buf, " \t");
2699 while (tok && nwords < 256) {
2700 words[nwords] = tok;
2701 word_widths[nwords] = _text_w(font, tok);
2702 nwords++;
2703 tok = strtok(NULL, " \t");
2704 }
2705 if (nwords == 0) return;
2706 if (nwords == 1) {
2707 _draw_text_truncated(font, color, x, y, max_w, text);
2708 return;
2709 }
2710
2711 float fh = (float)al_get_font_line_height(font);
2712 float space_w = _text_w(font, " ");
2713 float cy = y;
2714 int line_start = 0;
2715
2716 while (line_start < nwords) {
2717 /* stop if there is no vertical space for another line */
2718 if (cy + fh > y + max_h + 0.5f) break;
2719
2720 /* pack as many words as fit on this line with minimum (single-space) gaps */
2721 float line_words_w = word_widths[line_start];
2722 int line_end = line_start + 1;
2723 for (int i = line_start + 1; i < nwords; i++) {
2724 float test_w = line_words_w + space_w + word_widths[i];
2725 if (test_w > max_w) break;
2726 line_words_w = test_w;
2727 line_end = i + 1;
2728 }
2729
2730 int words_on_line = line_end - line_start;
2731 int is_last_line = (line_end >= nwords);
2732
2733 if (words_on_line == 1) {
2734 /* single word: truncate with "..." if it exceeds the line width */
2735 if (word_widths[line_start] > max_w) {
2736 _draw_text_truncated(font, color, x, cy, max_w, words[line_start]);
2737 } else {
2738 al_draw_text(font, color, x, cy, 0, words[line_start]);
2739 }
2740 } else if (is_last_line) {
2741 /* last line: left-align with normal spacing */
2742 float cx = x;
2743 for (int i = line_start; i < line_end; i++) {
2744 al_draw_text(font, color, cx, cy, 0, words[i]);
2745 cx += word_widths[i] + space_w;
2746 }
2747 } else {
2748 /* full line: justify by distributing extra space between words */
2749 float total_word_w = 0;
2750 for (int i = line_start; i < line_end; i++) total_word_w += word_widths[i];
2751 float total_space = max_w - total_word_w;
2752 if (total_space < 0) total_space = 0;
2753 float gap = total_space / (float)(words_on_line - 1);
2754 float cx = x;
2755 for (int i = line_start; i < line_end; i++) {
2756 al_draw_text(font, color, cx, cy, 0, words[i]);
2757 cx += word_widths[i] + gap;
2758 }
2759 }
2760
2761 line_start = line_end;
2762 cy += fh;
2763 }
2764}
2765
2767static int _utf8_char_len(unsigned char c) {
2768 if (c < 0x80) return 1;
2769 if ((c & 0xE0) == 0xC0) return 2;
2770 if ((c & 0xF0) == 0xE0) return 3;
2771 if ((c & 0xF8) == 0xF0) return 4;
2772 return 1; /* invalid byte, treat as single byte */
2773}
2774
2777static float _textarea_content_height(const N_GUI_TEXTAREA_DATA* td, ALLEGRO_FONT* font, float widget_w, float pad) {
2778 if (!font || td->text_len == 0) return 0;
2779 float fh = (float)al_get_font_line_height(font);
2780 float cx = 0;
2781 float cy = fh; /* at least one line */
2782 for (size_t i = 0; i < td->text_len;) {
2783 if (td->text[i] == '\n') {
2784 cx = 0;
2785 cy += fh;
2786 i++;
2787 continue;
2788 }
2789 int clen = _utf8_char_len((unsigned char)td->text[i]);
2790 if (i + (size_t)clen > td->text_len) clen = (int)(td->text_len - i);
2791 char ch[5];
2792 memcpy(ch, &td->text[i], (size_t)clen);
2793 ch[clen] = '\0';
2794 float cw = _text_w(font, ch);
2795 if (cx + cw > widget_w - pad * 2) {
2796 cx = 0;
2797 cy += fh;
2798 }
2799 cx += cw;
2800 i += (size_t)clen;
2801 }
2802 return cy;
2803}
2804
2811 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2812 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) return;
2814 if (!td->multiline || td->text_len == 0) return;
2815
2816 float pad = ctx->style.textarea_padding;
2817 ALLEGRO_FONT* font = w->font ? w->font : ctx->default_font;
2818 float view_h = w->h - pad * 2;
2819 float ch = _textarea_content_height(td, font, w->w, pad);
2820 float max_sy = ch - view_h;
2821 if (max_sy < 0) max_sy = 0;
2822 td->scroll_y = (int)max_sy;
2823} /* n_gui_textarea_scroll_to_bottom */
2824
2832void n_gui_textarea_set_selection(N_GUI_CTX* ctx, int widget_id, size_t start, size_t end) {
2833 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2834 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) return;
2836 if (start > td->text_len) start = td->text_len;
2837 if (end > td->text_len) end = td->text_len;
2838 td->sel_start = start;
2839 td->sel_end = end;
2840 td->cursor_pos = end;
2841} /* n_gui_textarea_set_selection */
2842
2849void n_gui_textarea_scroll_to_offset(N_GUI_CTX* ctx, int widget_id, size_t byte_offset) {
2850 N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2851 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) return;
2853 if (!td->multiline || td->text_len == 0) return;
2854
2855 float pad = ctx->style.textarea_padding;
2856 ALLEGRO_FONT* font = w->font ? w->font : ctx->default_font;
2857 if (!font) return;
2858 float fh = (float)al_get_font_line_height(font);
2859 float view_h = w->h - pad * 2;
2860
2861 /* Account for scrollbar width, matching the draw function logic:
2862 first check if scrollbar is needed, then use the narrower width */
2863 float content_h_initial = _textarea_content_height(td, font, w->w, pad);
2864 float sb_size = (content_h_initial > view_h) ? ctx->style.scrollbar_size : 0;
2865 float text_area_w = w->w - sb_size;
2866
2867 /* compute pixel Y of the target byte offset using line-wrap logic
2868 with the same text_area_w the draw function will use */
2869 float cx = 0;
2870 float cy = 0;
2871 size_t target = (byte_offset <= td->text_len) ? byte_offset : td->text_len;
2872 for (size_t i = 0; i < target && i < td->text_len;) {
2873 if (td->text[i] == '\n') {
2874 cx = 0;
2875 cy += fh;
2876 i++;
2877 continue;
2878 }
2879 int clen = _utf8_char_len((unsigned char)td->text[i]);
2880 if (i + (size_t)clen > td->text_len) clen = (int)(td->text_len - i);
2881 char ch[5];
2882 memcpy(ch, &td->text[i], (size_t)clen);
2883 ch[clen] = '\0';
2884 float cw = _text_w(font, ch);
2885 if (cx + cw > text_area_w - pad * 2) {
2886 cx = 0;
2887 cy += fh;
2888 }
2889 cx += cw;
2890 i += (size_t)clen;
2891 }
2892
2893 /* recompute content_h with the same text_area_w for consistent max_sy */
2894 float content_h = _textarea_content_height(td, font, text_area_w, pad);
2895 float max_sy = content_h - view_h;
2896 if (max_sy < 0) max_sy = 0;
2897 float ideal = cy - view_h / 2 + fh / 2;
2898 if (ideal < 0) ideal = 0;
2899 if (ideal > max_sy) ideal = max_sy;
2900 td->scroll_y = (int)ideal;
2901 td->scroll_from_wheel = 0;
2902} /* n_gui_textarea_scroll_to_offset */
2903
2910size_t n_gui_textarea_get_text_length(N_GUI_CTX* ctx, int widget_id) {
2911 const N_GUI_WIDGET* w = n_gui_get_widget(ctx, widget_id);
2912 if (!w || w->type != N_GUI_TYPE_TEXTAREA || !w->data) return 0;
2913 const N_GUI_TEXTAREA_DATA* td = (const N_GUI_TEXTAREA_DATA*)w->data;
2914 return td->text_len;
2915} /* n_gui_textarea_get_text_length */
2916
2920static 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) {
2921 if (!font || !text || !text[0]) return -1;
2922
2923 /* split text into words, tracking their byte offsets in original text */
2924 size_t text_len = strlen(text);
2925 char buf[N_GUI_TEXT_MAX];
2926 snprintf(buf, N_GUI_TEXT_MAX, "%s", text);
2927
2928 /* word start byte offsets in original text */
2929 int word_starts[256];
2930 int word_lens[256]; /* byte lengths */
2931 float word_widths[256];
2932 int nwords = 0;
2933
2934 size_t i = 0;
2935 while (i < text_len && nwords < 256) {
2936 /* skip whitespace */
2937 while (i < text_len && (text[i] == ' ' || text[i] == '\t')) i++;
2938 if (i >= text_len) break;
2939 size_t ws = i;
2940 while (i < text_len && text[i] != ' ' && text[i] != '\t') i++;
2941 int wlen = (int)(i - ws);
2942 word_starts[nwords] = (int)ws;
2943 word_lens[nwords] = wlen;
2944 char tmp[N_GUI_TEXT_MAX];
2945 memcpy(tmp, &text[ws], (size_t)wlen);
2946 tmp[wlen] = '\0';
2947 word_widths[nwords] = _text_w(font, tmp);
2948 nwords++;
2949 }
2950 if (nwords == 0) return 0;
2951
2952 float fh = (float)al_get_font_line_height(font);
2953 float space_w = _text_w(font, " ");
2954 float cy = text_y - scroll_y;
2955 float click_y_content = click_my;
2956
2957 /* find the target line */
2958 int line_start = 0;
2959 while (line_start < nwords) {
2960 float line_words_w = word_widths[line_start];
2961 int line_end = line_start + 1;
2962 for (int j = line_start + 1; j < nwords; j++) {
2963 float test_w = line_words_w + space_w + word_widths[j];
2964 if (test_w > max_w) break;
2965 line_words_w = test_w;
2966 line_end = j + 1;
2967 }
2968
2969 int words_on_line = line_end - line_start;
2970 int is_last_line = (line_end >= nwords);
2971
2972 if (click_y_content >= cy && click_y_content < cy + fh) {
2973 /* this is the target line - find character */
2974 float click_x_rel = click_mx - text_x;
2975 if (click_x_rel < 0) return word_starts[line_start];
2976
2977 /* compute gap for this line */
2978 float gap = space_w;
2979 if (!is_last_line && words_on_line > 1) {
2980 float total_word_w = 0;
2981 for (int j = line_start; j < line_end; j++) total_word_w += word_widths[j];
2982 float total_space = max_w - total_word_w;
2983 if (total_space < 0) total_space = 0;
2984 gap = total_space / (float)(words_on_line - 1);
2985 }
2986
2987 /* walk through words on this line */
2988 float cx = 0;
2989 for (int j = line_start; j < line_end; j++) {
2990 /* check within this word */
2991 if (click_x_rel >= cx && click_x_rel < cx + word_widths[j]) {
2992 /* find character within word */
2993 float wx = 0;
2994 int best_off = 0;
2995 float best_dist = click_x_rel - cx;
2996 if (best_dist < 0) best_dist = -best_dist;
2997 for (int k = 0; k < word_lens[j];) {
2998 int clen = _utf8_char_len((unsigned char)text[word_starts[j] + k]);
2999 if (k + clen > word_lens[j]) clen = word_lens[j] - k;
3000 char ch[5];
3001 memcpy(ch, &text[word_starts[j] + k], (size_t)clen);
3002 ch[clen] = '\0';
3003 float cw = _text_w(font, ch);
3004 wx += cw;
3005 float dist = (click_x_rel - cx) - wx;
3006 if (dist < 0) dist = -dist;
3007 if (dist < best_dist) {
3008 best_dist = dist;
3009 best_off = k + clen;
3010 }
3011 k += clen;
3012 }
3013 return word_starts[j] + best_off;
3014 }
3015 /* check in gap between words */
3016 if (j < line_end - 1 && click_x_rel >= cx + word_widths[j] && click_x_rel < cx + word_widths[j] + gap) {
3017 /* in the gap: snap to end of current word or start of next */
3018 float mid = cx + word_widths[j] + gap / 2.0f;
3019 if (click_x_rel < mid)
3020 return word_starts[j] + word_lens[j];
3021 else
3022 return word_starts[j + 1];
3023 }
3024 cx += word_widths[j] + gap;
3025 }
3026 /* past end of line */
3027 return word_starts[line_end - 1] + word_lens[line_end - 1];
3028 }
3029
3030 cy += fh;
3031 line_start = line_end;
3032 }
3033 /* below all text: return end */
3034 return (int)text_len;
3035}
3036
3039static 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) {
3040 if (!font || !text || !text[0] || sel_start == sel_end) return;
3041 int slo = sel_start < sel_end ? sel_start : sel_end;
3042 int shi = sel_start < sel_end ? sel_end : sel_start;
3043 size_t text_len = strlen(text);
3044 if ((size_t)slo > text_len) slo = (int)text_len;
3045 if ((size_t)shi > text_len) shi = (int)text_len;
3046
3047 /* split into words with byte offsets */
3048 int word_starts[256], word_lens[256];
3049 float word_widths[256];
3050 int nwords = 0;
3051 size_t i = 0;
3052 while (i < text_len && nwords < 256) {
3053 while (i < text_len && (text[i] == ' ' || text[i] == '\t')) i++;
3054 if (i >= text_len) break;
3055 size_t ws = i;
3056 while (i < text_len && text[i] != ' ' && text[i] != '\t') i++;
3057 int wlen = (int)(i - ws);
3058 word_starts[nwords] = (int)ws;
3059 word_lens[nwords] = wlen;
3060 char tmp[N_GUI_TEXT_MAX];
3061 memcpy(tmp, &text[ws], (size_t)wlen);
3062 tmp[wlen] = '\0';
3063 word_widths[nwords] = _text_w(font, tmp);
3064 nwords++;
3065 }
3066 if (nwords == 0) return;
3067
3068 float fh = (float)al_get_font_line_height(font);
3069 float space_w = _text_w(font, " ");
3070 float cy = y;
3071 int line_start = 0;
3072
3073 while (line_start < nwords) {
3074 if (cy + fh > y + max_h + 0.5f) break;
3075 float line_words_w = word_widths[line_start];
3076 int line_end = line_start + 1;
3077 for (int j = line_start + 1; j < nwords; j++) {
3078 float test_w = line_words_w + space_w + word_widths[j];
3079 if (test_w > max_w) break;
3080 line_words_w = test_w;
3081 line_end = j + 1;
3082 }
3083 int words_on_line = line_end - line_start;
3084 int is_last_line = (line_end >= nwords);
3085
3086 /* compute gap for this line */
3087 float gap = space_w;
3088 if (!is_last_line && words_on_line > 1) {
3089 float total_word_w = 0;
3090 for (int j = line_start; j < line_end; j++) total_word_w += word_widths[j];
3091 float total_space = max_w - total_word_w;
3092 if (total_space < 0) total_space = 0;
3093 gap = total_space / (float)(words_on_line - 1);
3094 }
3095
3096 /* walk words and draw selection rects where overlapping */
3097 float cx = x;
3098 for (int j = line_start; j < line_end; j++) {
3099 int ws2 = word_starts[j];
3100 int we = ws2 + word_lens[j];
3101 /* check if selection overlaps this word */
3102 if (slo < we && shi > ws2) {
3103 /* find pixel range within the word */
3104 float sx1 = 0, sx2 = word_widths[j];
3105 if (slo > ws2) {
3106 char tmp2[N_GUI_TEXT_MAX];
3107 int off = slo - ws2;
3108 memcpy(tmp2, &text[ws2], (size_t)off);
3109 tmp2[off] = '\0';
3110 sx1 = _text_w(font, tmp2);
3111 }
3112 if (shi < we) {
3113 char tmp2[N_GUI_TEXT_MAX];
3114 int off = shi - ws2;
3115 memcpy(tmp2, &text[ws2], (size_t)off);
3116 tmp2[off] = '\0';
3117 sx2 = _text_w(font, tmp2);
3118 }
3119 al_draw_filled_rectangle(cx + sx1, cy, cx + sx2, cy + fh, sel_color);
3120 }
3121 /* also highlight the gap (space) between words if selected */
3122 if (j < line_end - 1) {
3123 int gap_start = we; /* byte after word end */
3124 int gap_end = word_starts[j + 1]; /* byte of next word start */
3125 if (slo < gap_end && shi > gap_start) {
3126 al_draw_filled_rectangle(cx + word_widths[j], cy,
3127 cx + word_widths[j] + gap, cy + fh, sel_color);
3128 }
3129 }
3130 cx += word_widths[j] + gap;
3131 }
3132
3133 cy += fh;
3134 line_start = line_end;
3135 }
3136}
3137
3140static float _label_content_height(const char* text, ALLEGRO_FONT* font, float max_w) {
3141 if (!font || !text || !text[0]) return 0;
3142 float fh = (float)al_get_font_line_height(font);
3143 float space_w = _text_w(font, " ");
3144
3145 char buf[N_GUI_TEXT_MAX];
3146 snprintf(buf, N_GUI_TEXT_MAX, "%s", text);
3147
3148 float word_widths[256];
3149 int nwords = 0;
3150 const char* tok = strtok(buf, " \t");
3151 while (tok && nwords < 256) {
3152 word_widths[nwords] = _text_w(font, tok);
3153 nwords++;
3154 tok = strtok(NULL, " \t");
3155 }
3156 if (nwords == 0) return 0;
3157 if (nwords == 1) return fh;
3158
3159 int line_start = 0;
3160 int nlines = 0;
3161 while (line_start < nwords) {
3162 float line_words_w = word_widths[line_start];
3163 int line_end = line_start + 1;
3164 for (int i = line_start + 1; i < nwords; i++) {
3165 float test_w = line_words_w + space_w + word_widths[i];
3166 if (test_w > max_w) break;
3167 line_words_w = test_w;
3168 line_end = i + 1;
3169 }
3170 nlines++;
3171 line_start = line_end;
3172 }
3173 return (float)nlines * fh;
3174}
3175
3180static 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) {
3181 float sb_size = style->scrollbar_size;
3182 float sb_x = area_x + area_w - sb_size;
3183
3184 /* track */
3185 al_draw_filled_rectangle(sb_x, area_y, sb_x + sb_size, area_y + view_h,
3186 style->scrollbar_track_color);
3187
3188 /* thumb */
3189 float ratio = view_h / content_h;
3190 if (ratio > 1.0f) ratio = 1.0f;
3191 float thumb_h = ratio * view_h;
3192 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
3193 float max_scroll = content_h - view_h;
3194 float pos_ratio = (max_scroll > 0) ? scroll_y / max_scroll : 0;
3195 float thumb_y = area_y + pos_ratio * (view_h - thumb_h);
3196 al_draw_filled_rounded_rectangle(sb_x + style->scrollbar_thumb_padding, thumb_y,
3197 sb_x + sb_size - style->scrollbar_thumb_padding,
3198 thumb_y + thumb_h,
3200 style->scrollbar_thumb_color);
3201}
3202
3207static float _min_thickness(float requested) {
3208 const ALLEGRO_TRANSFORM* tf = al_get_current_transform();
3209 /* length of each axis basis vector gives the true scale for that axis */
3210 float sx = hypotf(tf->m[0][0], tf->m[0][1]);
3211 float sy = hypotf(tf->m[1][0], tf->m[1][1]);
3212 /* use the larger component to avoid overly thick lines on the minor axis */
3213 float scale = (sx > sy) ? sx : sy;
3214 if (scale < 0.01f) scale = 0.01f;
3215 /* ensure the line is at least 1 physical pixel */
3216 float min_t = 1.0f / scale;
3217 return (requested > min_t) ? requested : min_t;
3218}
3219
3221static ALLEGRO_COLOR _bg_for_state(const N_GUI_THEME* t, int state) {
3222 if (state & N_GUI_STATE_ACTIVE) return t->bg_active;
3223 if (state & N_GUI_STATE_HOVER) return t->bg_hover;
3224 return t->bg_normal;
3225}
3226
3227static ALLEGRO_COLOR _border_for_state(const N_GUI_THEME* t, int state) {
3228 if (state & N_GUI_STATE_ACTIVE) return t->border_active;
3229 if (state & N_GUI_STATE_HOVER) return t->border_hover;
3230 return t->border_normal;
3231}
3232
3233static ALLEGRO_COLOR _text_for_state(const N_GUI_THEME* t, int state) {
3234 if (state & N_GUI_STATE_ACTIVE) return t->text_active;
3235 if (state & N_GUI_STATE_HOVER) return t->text_hover;
3236 return t->text_normal;
3237}
3238
3243static void _draw_themed_rect(N_GUI_THEME* t, int state, float x, float y, float w, float h, int rounded) {
3244 ALLEGRO_COLOR bg = _bg_for_state(t, state);
3245 ALLEGRO_COLOR bd = _border_for_state(t, state);
3246 float thickness = _min_thickness(t->border_thickness);
3247 float ht = thickness * 0.5f;
3248 if (rounded) {
3249 al_draw_filled_rounded_rectangle(x + ht, y + ht, x + w - ht, y + h - ht, t->corner_rx, t->corner_ry, bg);
3250 al_draw_rounded_rectangle(x + ht, y + ht, x + w - ht, y + h - ht, t->corner_rx, t->corner_ry, bd, thickness);
3251 } else {
3252 al_draw_filled_rectangle(x, y, x + w, y + h, bg);
3253 al_draw_rectangle(x + ht, y + ht, x + w - ht, y + h - ht, bd, thickness);
3254 }
3255}
3256
3258static void _draw_button(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, const N_GUI_STYLE* style) {
3260 float ax = ox + wgt->x;
3261 float ay = oy + wgt->y;
3262 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
3263
3264 /* For toggle buttons, use the active visual state when toggled on */
3265 int draw_state = wgt->state;
3266 if (bd->toggle_mode && bd->toggled) {
3267 draw_state |= N_GUI_STATE_ACTIVE;
3268 }
3269
3270 if (bd->shape == N_GUI_SHAPE_BITMAP && bd->bitmap) {
3271 ALLEGRO_BITMAP* bmp = bd->bitmap;
3272 if ((draw_state & N_GUI_STATE_ACTIVE) && bd->bitmap_active)
3273 bmp = bd->bitmap_active;
3274 else if ((draw_state & N_GUI_STATE_HOVER) && bd->bitmap_hover)
3275 bmp = bd->bitmap_hover;
3276 al_draw_scaled_bitmap(bmp, 0, 0,
3277 (float)al_get_bitmap_width(bmp), (float)al_get_bitmap_height(bmp),
3278 ax, ay, wgt->w, wgt->h, 0);
3279 } else {
3280 int rounded = (bd->shape == N_GUI_SHAPE_ROUNDED) ? 1 : 0;
3281 _draw_themed_rect(&wgt->theme, draw_state, ax, ay, wgt->w, wgt->h, rounded);
3282 }
3283
3284 if (font && bd->label[0]) {
3285 ALLEGRO_COLOR tc = _text_for_state(&wgt->theme, draw_state);
3286 int bbx = 0, bby = 0, bbw = 0, bbh = 0;
3287 al_get_text_dimensions(font, bd->label, &bbx, &bby, &bbw, &bbh);
3288 float tw = (float)bbw;
3289 float th = (float)bbh;
3290 float pad = style->label_padding;
3291 float max_text_w = wgt->w - pad * 2.0f;
3292 if (tw > max_text_w) {
3293 _draw_text_truncated(font, tc, ax + pad, ay + (wgt->h - th) / 2.0f - (float)bby, max_text_w, bd->label);
3294 } else {
3295 al_draw_text(font, tc, ax + (wgt->w - tw) / 2.0f - (float)bbx, ay + (wgt->h - th) / 2.0f - (float)bby, 0, bd->label);
3296 }
3297 }
3298}
3299
3301static void _draw_slider(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
3303 float ax = ox + wgt->x;
3304 float ay = oy + wgt->y;
3305 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
3306
3307 double range = sd->max_val - sd->min_val;
3308 double ratio = (range > 0) ? (sd->value - sd->min_val) / range : 0;
3309
3310 if (sd->orientation == N_GUI_SLIDER_V) {
3311 /* ---- vertical slider ---- */
3312 float track_w = style->slider_track_size;
3313 float track_x = ax + (wgt->w - track_w) / 2.0f;
3314 if (sd->track_bitmap) {
3315 al_draw_scaled_bitmap(sd->track_bitmap, 0, 0,
3316 (float)al_get_bitmap_width(sd->track_bitmap), (float)al_get_bitmap_height(sd->track_bitmap),
3317 track_x, ay, track_w, wgt->h, 0);
3318 } else {
3319 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);
3320 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));
3321 }
3322
3323 /* fill from bottom up */
3324 float fill_h = (float)(ratio * (double)wgt->h);
3325 if (sd->fill_bitmap) {
3326 al_draw_scaled_bitmap(sd->fill_bitmap, 0, 0,
3327 (float)al_get_bitmap_width(sd->fill_bitmap), (float)al_get_bitmap_height(sd->fill_bitmap),
3328 track_x, ay + wgt->h - fill_h, track_w, fill_h, 0);
3329 } else {
3330 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);
3331 }
3332
3333 /* handle */
3334 float hy = ay + wgt->h - fill_h;
3335 float handle_r = wgt->w / 2.0f - style->slider_handle_edge_offset;
3336 if (handle_r < style->slider_handle_min_r) handle_r = style->slider_handle_min_r;
3337 ALLEGRO_BITMAP* hbmp = _select_state_bitmap(wgt->state, sd->handle_bitmap, sd->handle_hover_bitmap, sd->handle_active_bitmap);
3338 if (hbmp) {
3339 float hd = handle_r * 2.0f;
3340 al_draw_scaled_bitmap(hbmp, 0, 0,
3341 (float)al_get_bitmap_width(hbmp), (float)al_get_bitmap_height(hbmp),
3342 ax + wgt->w / 2.0f - handle_r, hy - handle_r, hd, hd, 0);
3343 } else {
3344 ALLEGRO_COLOR hc = _bg_for_state(&wgt->theme, wgt->state);
3345 ALLEGRO_COLOR hb = _border_for_state(&wgt->theme, wgt->state);
3346 al_draw_filled_circle(ax + wgt->w / 2.0f, hy, handle_r, hc);
3347 al_draw_circle(ax + wgt->w / 2.0f, hy, handle_r, hb, _min_thickness(style->slider_handle_border_thickness));
3348 }
3349
3350 /* value label */
3351 if (font) {
3352 char val_str[32];
3353 if (sd->mode == N_GUI_SLIDER_PERCENT)
3354 snprintf(val_str, sizeof(val_str), "%.0f%%", sd->value);
3355 else
3356 snprintf(val_str, sizeof(val_str), "%.1f", sd->value);
3357 int vbbx = 0, vbby = 0, vbbw = 0, vbbh = 0;
3358 al_get_text_dimensions(font, val_str, &vbbx, &vbby, &vbbw, &vbbh);
3359 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);
3360 }
3361 return;
3362 }
3363
3364 /* ---- horizontal slider (original) ---- */
3365
3366 /* track */
3367 float track_h = style->slider_track_size;
3368 float track_y = ay + (wgt->h - track_h) / 2.0f;
3369 if (sd->track_bitmap) {
3370 al_draw_scaled_bitmap(sd->track_bitmap, 0, 0,
3371 (float)al_get_bitmap_width(sd->track_bitmap), (float)al_get_bitmap_height(sd->track_bitmap),
3372 ax, track_y, wgt->w, track_h, 0);
3373 } else {
3374 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);
3375 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));
3376 }
3377
3378 /* fill */
3379 float fill_w = (float)(ratio * (double)wgt->w);
3380 if (sd->fill_bitmap) {
3381 al_draw_scaled_bitmap(sd->fill_bitmap, 0, 0,
3382 (float)al_get_bitmap_width(sd->fill_bitmap), (float)al_get_bitmap_height(sd->fill_bitmap),
3383 ax, track_y, fill_w, track_h, 0);
3384 } else {
3385 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);
3386 }
3387
3388 /* handle */
3389 float hx = ax + fill_w;
3390 float handle_r = wgt->h / 2.0f - style->slider_handle_edge_offset;
3391 if (handle_r < style->slider_handle_min_r) handle_r = style->slider_handle_min_r;
3392 ALLEGRO_BITMAP* hbmp = _select_state_bitmap(wgt->state, sd->handle_bitmap, sd->handle_hover_bitmap, sd->handle_active_bitmap);
3393 if (hbmp) {
3394 float hd = handle_r * 2.0f;
3395 al_draw_scaled_bitmap(hbmp, 0, 0,
3396 (float)al_get_bitmap_width(hbmp), (float)al_get_bitmap_height(hbmp),
3397 hx - handle_r, ay + wgt->h / 2.0f - handle_r, hd, hd, 0);
3398 } else {
3399 ALLEGRO_COLOR hc = _bg_for_state(&wgt->theme, wgt->state);
3400 ALLEGRO_COLOR hb = _border_for_state(&wgt->theme, wgt->state);
3401 al_draw_filled_circle(hx, ay + wgt->h / 2.0f, handle_r, hc);
3402 al_draw_circle(hx, ay + wgt->h / 2.0f, handle_r, hb, _min_thickness(style->slider_handle_border_thickness));
3403 }
3404
3405 /* value label */
3406 if (font) {
3407 char val_str[32];
3408 if (sd->mode == N_GUI_SLIDER_PERCENT) {
3409 snprintf(val_str, sizeof(val_str), "%.0f%%", sd->value);
3410 } else {
3411 snprintf(val_str, sizeof(val_str), "%.1f", sd->value);
3412 }
3413 int sbbx = 0, sbby = 0, sbbw = 0, sbbh = 0;
3414 al_get_text_dimensions(font, val_str, &sbbx, &sbby, &sbbw, &sbbh);
3415 float th = (float)sbbh;
3416 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);
3417 }
3418}
3419
3423static 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) {
3424 if (!font || td->text_len == 0) return 0;
3425 float fh = (float)al_get_font_line_height(font);
3426
3427 if (!td->multiline) {
3428 float click_x = mx - (ax + pad) + td->scroll_x;
3429 if (click_x < 0) click_x = 0;
3430 /* build display text for width calculation (masked if mask_char set) */
3431 char display[N_GUI_TEXT_MAX];
3432 if (td->mask_char) {
3433 size_t n = td->text_len;
3434 if (n >= N_GUI_TEXT_MAX) n = N_GUI_TEXT_MAX - 1;
3435 memset(display, td->mask_char, n);
3436 display[n] = '\0';
3437 } else {
3438 memcpy(display, td->text, td->text_len);
3439 display[td->text_len] = '\0';
3440 }
3441 size_t best = 0;
3442 float best_dist = click_x;
3443 char ctmp[N_GUI_TEXT_MAX];
3444 for (size_t ci = 0; ci < td->text_len;) {
3445 int clen = _utf8_char_len((unsigned char)td->text[ci]);
3446 if (ci + (size_t)clen > td->text_len) clen = (int)(td->text_len - ci);
3447 size_t pos = ci + (size_t)clen;
3448 memcpy(ctmp, display, pos);
3449 ctmp[pos] = '\0';
3450 float tw = _text_w(font, ctmp);
3451 float dist = click_x - tw;
3452 if (dist < 0) dist = -dist;
3453 if (dist < best_dist) {
3454 best_dist = dist;
3455 best = pos;
3456 } else {
3457 break;
3458 }
3459 ci += (size_t)clen;
3460 }
3461 return best;
3462 }
3463
3464 /* multiline */
3465 float view_h = widget_h - pad * 2;
3466 float content_h = _textarea_content_height(td, font, widget_w, pad);
3467 float sb_size = (content_h > view_h) ? scrollbar_size : 0.0f;
3468 float text_area_w = widget_w - sb_size;
3469 float avail_w = text_area_w - pad * 2;
3470 float click_x_rel = mx - (ax + pad);
3471 float click_y_content = my - (ay + pad) + (float)td->scroll_y;
3472 if (click_x_rel < 0) click_x_rel = 0;
3473 if (click_y_content < 0) click_y_content = 0;
3474 int target_line = (int)(click_y_content / fh);
3475
3476 float cur_cx = 0;
3477 int cur_line = 0;
3478 size_t best_pos = 0;
3479 float best_dist = click_x_rel;
3480 int found_line = (target_line == 0) ? 1 : 0;
3481
3482 for (size_t ci = 0; ci < td->text_len;) {
3483 if (cur_line > target_line) break;
3484 if (td->text[ci] == '\n') {
3485 if (cur_line == target_line) break;
3486 cur_cx = 0;
3487 cur_line++;
3488 if (cur_line == target_line) {
3489 found_line = 1;
3490 best_pos = ci + 1;
3491 best_dist = click_x_rel;
3492 }
3493 ci++;
3494 continue;
3495 }
3496 int clen = _utf8_char_len((unsigned char)td->text[ci]);
3497 if (ci + (size_t)clen > td->text_len) clen = (int)(td->text_len - ci);
3498 char ch2[5];
3499 memcpy(ch2, &td->text[ci], (size_t)clen);
3500 ch2[clen] = '\0';
3501 float cw = _text_w(font, ch2);
3502 if (cur_cx + cw > avail_w) {
3503 if (cur_line == target_line) break;
3504 cur_cx = 0;
3505 cur_line++;
3506 if (cur_line == target_line) {
3507 found_line = 1;
3508 best_pos = ci;
3509 best_dist = click_x_rel;
3510 }
3511 }
3512 if (cur_line == target_line) {
3513 float dist = click_x_rel - (cur_cx + cw);
3514 if (dist < 0) dist = -dist;
3515 if (dist < best_dist) {
3516 best_dist = dist;
3517 best_pos = ci + (size_t)clen;
3518 }
3519 }
3520 cur_cx += cw;
3521 ci += (size_t)clen;
3522 }
3523 if (!found_line) best_pos = td->text_len;
3524 return best_pos;
3525}
3526
3529 return td->sel_start != td->sel_end;
3530}
3531
3533static void _textarea_sel_range(const N_GUI_TEXTAREA_DATA* td, size_t* lo, size_t* hi) {
3534 if (td->sel_start <= td->sel_end) {
3535 *lo = td->sel_start;
3536 *hi = td->sel_end;
3537 } else {
3538 *lo = td->sel_end;
3539 *hi = td->sel_start;
3540 }
3541}
3542
3544static void _draw_textarea(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
3546 float ax = ox + wgt->x;
3547 float ay = oy + wgt->y;
3548 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
3549
3550 int focused = (wgt->state & N_GUI_STATE_FOCUSED) ? 1 : 0;
3551
3552 /* background */
3553 if (td->bg_bitmap) {
3554 al_draw_scaled_bitmap(td->bg_bitmap, 0, 0,
3555 (float)al_get_bitmap_width(td->bg_bitmap), (float)al_get_bitmap_height(td->bg_bitmap),
3556 ax, ay, wgt->w, wgt->h, 0);
3557 } else {
3558 ALLEGRO_COLOR bg = wgt->theme.bg_normal;
3559 al_draw_filled_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, bg);
3560 }
3561 ALLEGRO_COLOR bd = focused ? wgt->theme.border_active : wgt->theme.border_normal;
3562 al_draw_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, bd, _min_thickness(wgt->theme.border_thickness));
3563
3564 if (!font) return;
3565 float fh = (float)al_get_font_line_height(font);
3566 float pad = style->textarea_padding;
3567
3568 /* cursor blink: visible during first half of blink period */
3569 int cursor_visible = 0;
3570 if (focused) {
3571 double now = al_get_time();
3572 double elapsed = now - td->cursor_time;
3573 float period = style->textarea_cursor_blink_period;
3574 if (period <= 0.0f) period = 1.0f;
3575 double phase = elapsed - (double)(int)(elapsed / (double)period) * (double)period;
3576 cursor_visible = (phase < (double)period * 0.5) ? 1 : 0;
3577 }
3578 float cursor_w = _min_thickness(style->textarea_cursor_width);
3579
3580 if (td->multiline) {
3581 float view_h = wgt->h - pad * 2;
3582 /* two-pass: first check if scrollbar is needed at full width,
3583 * then recompute content_h at the narrower width if so */
3584 float content_h = _textarea_content_height(td, font, wgt->w, pad);
3585 int need_scrollbar = (content_h > view_h) ? 1 : 0;
3586 float sb_size = need_scrollbar ? style->scrollbar_size : 0;
3587 float text_area_w = wgt->w - sb_size;
3588 if (need_scrollbar) {
3589 content_h = _textarea_content_height(td, font, text_area_w, pad);
3590 }
3591
3592 /* auto-scroll to keep cursor visible (skip if user just scrolled with mouse wheel) */
3593 if (focused && !td->scroll_from_wheel) {
3594 /* compute cursor Y position within content */
3595 float cur_cx = 0;
3596 float cur_cy = 0;
3597 for (size_t i = 0; i < td->cursor_pos && i < td->text_len;) {
3598 if (td->text[i] == '\n') {
3599 cur_cx = 0;
3600 cur_cy += fh;
3601 i++;
3602 continue;
3603 }
3604 int clen = _utf8_char_len((unsigned char)td->text[i]);
3605 if (i + (size_t)clen > td->text_len) clen = (int)(td->text_len - i);
3606 char ch[5];
3607 memcpy(ch, &td->text[i], (size_t)clen);
3608 ch[clen] = '\0';
3609 float cw2 = _text_w(font, ch);
3610 if (cur_cx + cw2 > text_area_w - pad * 2) {
3611 cur_cx = 0;
3612 cur_cy += fh;
3613 }
3614 cur_cx += cw2;
3615 i += (size_t)clen;
3616 }
3617 /* ensure cursor line is within visible region */
3618 if (cur_cy < (float)td->scroll_y) {
3619 td->scroll_y = (int)cur_cy;
3620 }
3621 if (cur_cy + fh > (float)td->scroll_y + view_h) {
3622 td->scroll_y = (int)(cur_cy + fh - view_h);
3623 }
3624 }
3625
3626 /* clamp scroll_y */
3627 if (need_scrollbar) {
3628 float max_sy = content_h - view_h;
3629 if (max_sy < 0) max_sy = 0;
3630 if ((float)td->scroll_y > max_sy) td->scroll_y = (int)max_sy;
3631 if (td->scroll_y < 0) td->scroll_y = 0;
3632 } else {
3633 td->scroll_y = 0;
3634 }
3635
3636 /* clip to widget bounds (text area only, leaving room for scrollbar) */
3637 int pcx, pcy, pcw, pch;
3638 al_get_clipping_rectangle(&pcx, &pcy, &pcw, &pch);
3639 _set_clipping_rect_transformed((int)ax, (int)ay, (int)text_area_w, (int)wgt->h);
3640 /* selection range */
3641 size_t sel_lo = 0, sel_hi = 0;
3642 int has_sel = _textarea_has_selection(td);
3643 if (has_sel) _textarea_sel_range(td, &sel_lo, &sel_hi);
3644 ALLEGRO_COLOR sel_bg = wgt->theme.selection_color;
3645
3646 /* simple line-wrap drawing with UTF-8 support */
3647 float cx = ax + pad;
3648 float cy = ay + pad - (float)td->scroll_y;
3649 /* cursor position tracked during layout */
3650 float cursor_cx = cx;
3651 float cursor_cy = cy;
3652 for (size_t i = 0; i < td->text_len;) {
3653 if (i == td->cursor_pos) {
3654 cursor_cx = cx;
3655 cursor_cy = cy;
3656 }
3657 if (td->text[i] == '\n') {
3658 /* draw selection highlight on newline (small rect at end of line) */
3659 if (has_sel && i >= sel_lo && i < sel_hi && cy + fh > ay && cy < ay + wgt->h) {
3660 float space_w = _text_w(font, " ");
3661 al_draw_filled_rectangle(cx, cy, cx + space_w, cy + fh, sel_bg);
3662 }
3663 cx = ax + pad;
3664 cy += fh;
3665 i++;
3666 continue;
3667 }
3668 int clen = _utf8_char_len((unsigned char)td->text[i]);
3669 if (i + (size_t)clen > td->text_len) clen = (int)(td->text_len - i);
3670 char ch[5];
3671 memcpy(ch, &td->text[i], (size_t)clen);
3672 ch[clen] = '\0';
3673 float cw = _text_w(font, ch);
3674 if (cx + cw > ax + text_area_w - pad) {
3675 cx = ax + pad;
3676 cy += fh;
3677 if (i == td->cursor_pos) {
3678 cursor_cx = cx;
3679 cursor_cy = cy;
3680 }
3681 }
3682 if (cy + fh > ay && cy < ay + wgt->h) {
3683 /* draw selection highlight behind character */
3684 if (has_sel && i >= sel_lo && i < sel_hi) {
3685 al_draw_filled_rectangle(cx, cy, cx + cw, cy + fh, sel_bg);
3686 }
3687 al_draw_text(font, wgt->theme.text_normal, cx, cy, 0, ch);
3688 }
3689 cx += cw;
3690 /* check cursor positions within the multi-byte character */
3691 for (int b = 1; b < clen; b++) {
3692 if (i + (size_t)b == td->cursor_pos) {
3693 cursor_cx = cx;
3694 cursor_cy = cy;
3695 }
3696 }
3697 i += (size_t)clen;
3698 }
3699 /* cursor is after all text when cursor_pos >= text_len */
3700 if (td->cursor_pos >= td->text_len) {
3701 cursor_cx = cx;
3702 cursor_cy = cy;
3703 }
3704 /* cursor */
3705 if (cursor_visible) {
3706 al_draw_filled_rectangle(cursor_cx, cursor_cy, cursor_cx + cursor_w, cursor_cy + fh, wgt->theme.text_active);
3707 }
3708 al_set_clipping_rectangle(pcx, pcy, pcw, pch);
3709
3710 /* draw scrollbar outside clipping region */
3711 if (need_scrollbar) {
3712 _draw_widget_vscrollbar(ax, ay + pad, wgt->w, view_h, content_h, (float)td->scroll_y, style);
3713 }
3714 } else {
3715 /* single line with horizontal scrolling */
3716 float ty = ay + (wgt->h - fh) / 2.0f;
3717 float inner_w = wgt->w - pad * 2;
3718
3719 /* build display text (masked if mask_char is set) */
3720 char display_text[N_GUI_TEXT_MAX];
3721 if (td->mask_char && td->text_len > 0) {
3722 size_t n = td->text_len;
3723 if (n >= N_GUI_TEXT_MAX) n = N_GUI_TEXT_MAX - 1;
3724 memset(display_text, td->mask_char, n);
3725 display_text[n] = '\0';
3726 } else {
3727 memcpy(display_text, td->text, td->text_len);
3728 display_text[td->text_len] = '\0';
3729 }
3730
3731 /* compute cursor pixel offset from text start */
3732 char tmp[N_GUI_TEXT_MAX];
3733 size_t cpos = td->cursor_pos;
3734 if (cpos > td->text_len) cpos = td->text_len;
3735 memcpy(tmp, display_text, cpos);
3736 tmp[cpos] = '\0';
3737 float cursor_px = _text_w(font, tmp);
3738
3739 /* adjust scroll_x so cursor stays visible within the inner area */
3740 if (cursor_px - td->scroll_x < 0) {
3741 td->scroll_x = cursor_px;
3742 }
3743 if (cursor_px - td->scroll_x > inner_w - cursor_w) {
3744 td->scroll_x = cursor_px - inner_w + cursor_w;
3745 }
3746 if (td->scroll_x < 0) td->scroll_x = 0;
3747
3748 /* clip text to widget inner bounds */
3749 int pcx, pcy, pcw, pch;
3750 al_get_clipping_rectangle(&pcx, &pcy, &pcw, &pch);
3751 _set_clipping_rect_transformed((int)(ax + pad), (int)ay, (int)inner_w, (int)wgt->h);
3752 /* draw selection highlight for single-line */
3753 if (_textarea_has_selection(td)) {
3754 size_t sl, sh;
3755 _textarea_sel_range(td, &sl, &sh);
3756 char stmp[N_GUI_TEXT_MAX];
3757 memcpy(stmp, display_text, sl);
3758 stmp[sl] = '\0';
3759 float sel_x1 = _text_w(font, stmp);
3760 memcpy(stmp, display_text, sh);
3761 stmp[sh] = '\0';
3762 float sel_x2 = _text_w(font, stmp);
3763 float sx1 = ax + pad + sel_x1 - td->scroll_x;
3764 float sx2 = ax + pad + sel_x2 - td->scroll_x;
3765 al_draw_filled_rectangle(sx1, ty, sx2, ty + fh, wgt->theme.selection_color);
3766 }
3767 al_draw_text(font, wgt->theme.text_normal, ax + pad - td->scroll_x, ty, 0, display_text);
3768 /* cursor */
3769 if (cursor_visible) {
3770 float cx = ax + pad + cursor_px - td->scroll_x;
3771 al_draw_filled_rectangle(cx, ty, cx + cursor_w, ty + fh, wgt->theme.text_active);
3772 }
3773 al_set_clipping_rectangle(pcx, pcy, pcw, pch);
3774 }
3775}
3776
3778static void _draw_checkbox(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
3780 float ax = ox + wgt->x;
3781 float ay = oy + wgt->y;
3782 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
3783
3784 float box_size = wgt->h < style->checkbox_max_size ? wgt->h : style->checkbox_max_size;
3785 float box_y = ay + (wgt->h - box_size) / 2.0f;
3786
3787 /* select bitmap: hover > checked/unchecked > color theme */
3788 ALLEGRO_BITMAP* box_bmp = NULL;
3789 if ((wgt->state & N_GUI_STATE_HOVER) && cd->box_hover_bitmap) {
3790 box_bmp = cd->box_hover_bitmap;
3791 } else if (cd->checked && cd->box_checked_bitmap) {
3792 box_bmp = cd->box_checked_bitmap;
3793 } else if (cd->box_bitmap) {
3794 box_bmp = cd->box_bitmap;
3795 }
3796
3797 if (box_bmp) {
3798 al_draw_scaled_bitmap(box_bmp, 0, 0,
3799 (float)al_get_bitmap_width(box_bmp), (float)al_get_bitmap_height(box_bmp),
3800 ax, box_y, box_size, box_size, 0);
3801 } else {
3802 _draw_themed_rect(&wgt->theme, wgt->state, ax, box_y, box_size, box_size, 0);
3803
3804 if (cd->checked) {
3805 /* draw checkmark as two lines */
3806 ALLEGRO_COLOR tc = wgt->theme.text_active;
3807 float m = style->checkbox_mark_margin;
3808 al_draw_line(ax + m, box_y + box_size / 2.0f,
3809 ax + box_size / 2.0f, box_y + box_size - m, tc, _min_thickness(style->checkbox_mark_thickness));
3810 al_draw_line(ax + box_size / 2.0f, box_y + box_size - m,
3811 ax + box_size - m, box_y + m, tc, _min_thickness(style->checkbox_mark_thickness));
3812 }
3813 }
3814
3815 if (font && cd->label[0]) {
3816 int cbbx = 0, cbby = 0, cbbw = 0, cbbh = 0;
3817 al_get_text_dimensions(font, cd->label, &cbbx, &cbby, &cbbw, &cbbh);
3818 float fh = (float)cbbh;
3819 float label_max_w = wgt->w - box_size - style->checkbox_label_gap;
3821 ax + box_size + style->checkbox_label_offset, ay + (wgt->h - fh) / 2.0f - (float)cbby, label_max_w, cd->label);
3822 }
3823}
3824
3826static void _draw_scrollbar(N_GUI_WIDGET* wgt, float ox, float oy, const N_GUI_STYLE* style) {
3828 float ax = ox + wgt->x;
3829 float ay = oy + wgt->y;
3830 int rounded = (sb->shape == N_GUI_SHAPE_ROUNDED) ? 1 : 0;
3831
3832 /* track */
3833 if (sb->track_bitmap) {
3834 al_draw_scaled_bitmap(sb->track_bitmap, 0, 0,
3835 (float)al_get_bitmap_width(sb->track_bitmap), (float)al_get_bitmap_height(sb->track_bitmap),
3836 ax, ay, wgt->w, wgt->h, 0);
3837 } else {
3838 _draw_themed_rect(&wgt->theme, N_GUI_STATE_IDLE, ax, ay, wgt->w, wgt->h, rounded);
3839 }
3840
3841 /* compute thumb */
3842 double ratio = sb->viewport_size / sb->content_size;
3843 if (ratio > 1.0) ratio = 1.0;
3844 double max_scroll = sb->content_size - sb->viewport_size;
3845 if (max_scroll < 0) max_scroll = 0;
3846 double pos_ratio = (max_scroll > 0) ? sb->scroll_pos / max_scroll : 0;
3847
3848 float thumb_x, thumb_y, thumb_w, thumb_h;
3849 if (sb->orientation == N_GUI_SCROLLBAR_V) {
3850 thumb_h = (float)(ratio * (double)wgt->h);
3851 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
3852 thumb_w = wgt->w - style->scrollbar_thumb_padding * 2;
3853 thumb_x = ax + style->scrollbar_thumb_padding;
3854 float track_range = wgt->h - thumb_h;
3855 thumb_y = ay + (float)(pos_ratio * (double)track_range);
3856 } else {
3857 thumb_w = (float)(ratio * (double)wgt->w);
3858 if (thumb_w < style->scrollbar_thumb_min) thumb_w = style->scrollbar_thumb_min;
3859 thumb_h = wgt->h - style->scrollbar_thumb_padding * 2;
3860 thumb_y = ay + style->scrollbar_thumb_padding;
3861 float track_range = wgt->w - thumb_w;
3862 thumb_x = ax + (float)(pos_ratio * (double)track_range);
3863 }
3864
3865 ALLEGRO_BITMAP* tbmp = _select_state_bitmap(wgt->state, sb->thumb_bitmap, sb->thumb_hover_bitmap, sb->thumb_active_bitmap);
3866 if (tbmp) {
3867 al_draw_scaled_bitmap(tbmp, 0, 0,
3868 (float)al_get_bitmap_width(tbmp), (float)al_get_bitmap_height(tbmp),
3869 thumb_x, thumb_y, thumb_w, thumb_h, 0);
3870 } else {
3871 _draw_themed_rect(&wgt->theme, wgt->state, thumb_x, thumb_y, thumb_w, thumb_h, rounded);
3872 }
3873}
3874
3876static void _draw_listbox(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
3878 float ax = ox + wgt->x;
3879 float ay = oy + wgt->y;
3880 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
3881
3882 /* background */
3883 if (ld->bg_bitmap) {
3884 al_draw_scaled_bitmap(ld->bg_bitmap, 0, 0,
3885 (float)al_get_bitmap_width(ld->bg_bitmap), (float)al_get_bitmap_height(ld->bg_bitmap),
3886 ax, ay, wgt->w, wgt->h, 0);
3887 } else {
3888 al_draw_filled_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.bg_normal);
3889 }
3890 al_draw_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.border_normal, wgt->theme.border_thickness);
3891
3892 if (!font) return;
3893 float fh = (float)al_get_font_line_height(font);
3894 float ih = ld->item_height > fh ? ld->item_height : fh + style->item_height_pad;
3895 int visible_count = (int)(wgt->h / ih);
3896 float pad = style->item_text_padding;
3897
3898 /* clamp scroll_offset to valid range */
3899 int max_off = (int)ld->nb_items - visible_count;
3900 if (max_off < 0) max_off = 0;
3901 if (ld->scroll_offset > max_off) ld->scroll_offset = max_off;
3902 if (ld->scroll_offset < 0) ld->scroll_offset = 0;
3903
3904 int need_scrollbar = ((int)ld->nb_items > visible_count) ? 1 : 0;
3905 float sb_w = need_scrollbar ? style->scrollbar_size : 0;
3906 float item_area_w = wgt->w - sb_w;
3907
3908 for (int i = 0; i < visible_count && (size_t)(i + ld->scroll_offset) < ld->nb_items; i++) {
3909 int idx = i + ld->scroll_offset;
3910 float iy = ay + (float)i * ih;
3911 N_GUI_LISTITEM* item = &ld->items[idx];
3912
3913 if (item->selected && ld->item_selected_bitmap) {
3914 al_draw_scaled_bitmap(ld->item_selected_bitmap, 0, 0,
3915 (float)al_get_bitmap_width(ld->item_selected_bitmap), (float)al_get_bitmap_height(ld->item_selected_bitmap),
3916 ax + style->item_selection_inset, iy, item_area_w - style->item_selection_inset * 2, ih, 0);
3917 } else if (item->selected) {
3918 al_draw_filled_rectangle(ax + style->item_selection_inset, iy, ax + item_area_w - style->item_selection_inset, iy + ih, wgt->theme.bg_active);
3919 } else if (ld->item_bg_bitmap) {
3920 al_draw_scaled_bitmap(ld->item_bg_bitmap, 0, 0,
3921 (float)al_get_bitmap_width(ld->item_bg_bitmap), (float)al_get_bitmap_height(ld->item_bg_bitmap),
3922 ax + style->item_selection_inset, iy, item_area_w - style->item_selection_inset * 2, ih, 0);
3923 }
3924 float item_max_w = item_area_w - pad * 2.0f;
3925 _draw_text_truncated(font, item->selected ? wgt->theme.text_active : wgt->theme.text_normal,
3926 ax + pad, iy + (ih - fh) / 2.0f, item_max_w, item->text);
3927 }
3928
3929 /* draw scrollbar indicator when items overflow */
3930 if (need_scrollbar) {
3931 float sb_x = ax + wgt->w - sb_w;
3932 /* scrollbar track */
3933 al_draw_filled_rectangle(sb_x, ay, ax + wgt->w, ay + wgt->h, wgt->theme.bg_normal);
3934 al_draw_line(sb_x, ay, sb_x, ay + wgt->h, wgt->theme.border_normal, 1.0f);
3935 /* thumb */
3936 float ratio = (float)visible_count / (float)ld->nb_items;
3937 float thumb_h = ratio * wgt->h;
3938 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
3939 float track_range = wgt->h - thumb_h;
3940 float pos_ratio = (max_off > 0) ? (float)ld->scroll_offset / (float)max_off : 0;
3941 float thumb_y = ay + pos_ratio * track_range;
3942 float thumb_pad = style->scrollbar_thumb_padding;
3943 al_draw_filled_rounded_rectangle(sb_x + thumb_pad, thumb_y, ax + wgt->w - thumb_pad, thumb_y + thumb_h,
3944 2.0f, 2.0f, wgt->theme.bg_hover);
3945 }
3946}
3947
3949static void _draw_radiolist(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, const N_GUI_STYLE* style) {
3951 float ax = ox + wgt->x;
3952 float ay = oy + wgt->y;
3953 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
3954
3955 /* background */
3956 if (rd->bg_bitmap) {
3957 al_draw_scaled_bitmap(rd->bg_bitmap, 0, 0,
3958 (float)al_get_bitmap_width(rd->bg_bitmap), (float)al_get_bitmap_height(rd->bg_bitmap),
3959 ax, ay, wgt->w, wgt->h, 0);
3960 } else {
3961 al_draw_filled_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.bg_normal);
3962 }
3963 al_draw_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.border_normal, wgt->theme.border_thickness);
3964
3965 if (!font) return;
3966 float fh = (float)al_get_font_line_height(font);
3967 float ih = rd->item_height > fh ? rd->item_height : fh + style->item_height_pad;
3968 int visible_count = (int)(wgt->h / ih);
3969 float radio_r = (ih - style->item_height_pad * 2) / 2.0f;
3970 if (radio_r < style->radio_circle_min_r) radio_r = style->radio_circle_min_r;
3971 float pad = style->radio_label_gap;
3972
3973 /* clamp scroll_offset to valid range */
3974 int max_off = (int)rd->nb_items - visible_count;
3975 if (max_off < 0) max_off = 0;
3976 if (rd->scroll_offset > max_off) rd->scroll_offset = max_off;
3977 if (rd->scroll_offset < 0) rd->scroll_offset = 0;
3978
3979 int need_scrollbar = ((int)rd->nb_items > visible_count) ? 1 : 0;
3980
3981 for (int i = 0; i < visible_count && (size_t)(i + rd->scroll_offset) < rd->nb_items; i++) {
3982 int idx = i + rd->scroll_offset;
3983 float iy = ay + (float)i * ih;
3984 float cy = iy + ih / 2.0f;
3985 float cx = ax + pad + radio_r;
3986
3987 /* item background bitmap */
3988 if (idx == rd->selected_index && rd->item_selected_bitmap) {
3989 al_draw_scaled_bitmap(rd->item_selected_bitmap, 0, 0,
3990 (float)al_get_bitmap_width(rd->item_selected_bitmap), (float)al_get_bitmap_height(rd->item_selected_bitmap),
3991 ax, iy, wgt->w, ih, 0);
3992 } else if (rd->item_bg_bitmap) {
3993 al_draw_scaled_bitmap(rd->item_bg_bitmap, 0, 0,
3994 (float)al_get_bitmap_width(rd->item_bg_bitmap), (float)al_get_bitmap_height(rd->item_bg_bitmap),
3995 ax, iy, wgt->w, ih, 0);
3996 }
3997
3998 /* outer circle */
3999 al_draw_circle(cx, cy, radio_r, wgt->theme.border_normal, _min_thickness(style->radio_circle_border_thickness));
4000
4001 /* inner filled circle if selected */
4002 if (idx == rd->selected_index) {
4003 al_draw_filled_circle(cx, cy, radio_r - style->radio_inner_offset, wgt->theme.text_active);
4004 }
4005
4006 /* label */
4007 al_draw_text(font, wgt->theme.text_normal,
4008 cx + radio_r + pad, iy + (ih - fh) / 2.0f, 0, rd->items[idx].text);
4009 }
4010
4011 /* draw scrollbar indicator when items overflow */
4012 if (need_scrollbar) {
4013 float sb_w = style->scrollbar_size;
4014 float sb_x = ax + wgt->w - sb_w;
4015 al_draw_filled_rectangle(sb_x, ay, ax + wgt->w, ay + wgt->h, wgt->theme.bg_normal);
4016 al_draw_line(sb_x, ay, sb_x, ay + wgt->h, wgt->theme.border_normal, 1.0f);
4017 float ratio = (float)visible_count / (float)rd->nb_items;
4018 float thumb_h = ratio * wgt->h;
4019 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
4020 float track_range = wgt->h - thumb_h;
4021 float pos_ratio = (max_off > 0) ? (float)rd->scroll_offset / (float)max_off : 0;
4022 float thumb_y = ay + pos_ratio * track_range;
4023 float thumb_pad = style->scrollbar_thumb_padding;
4024 al_draw_filled_rounded_rectangle(sb_x + thumb_pad, thumb_y, ax + wgt->w - thumb_pad, thumb_y + thumb_h,
4025 2.0f, 2.0f, wgt->theme.bg_hover);
4026 }
4027}
4028
4030static void _draw_combobox(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, const N_GUI_STYLE* style) {
4032 float ax = ox + wgt->x;
4033 float ay = oy + wgt->y;
4034 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
4035
4036 if (cd->bg_bitmap) {
4037 al_draw_scaled_bitmap(cd->bg_bitmap, 0, 0,
4038 (float)al_get_bitmap_width(cd->bg_bitmap), (float)al_get_bitmap_height(cd->bg_bitmap),
4039 ax, ay, wgt->w, wgt->h, 0);
4040 } else {
4041 int rounded = 0;
4042 _draw_themed_rect(&wgt->theme, wgt->state, ax, ay, wgt->w, wgt->h, rounded);
4043 }
4044
4045 if (!font) return;
4046 float pad = style->item_text_padding;
4047
4048 /* display selected item text (truncated to fit) */
4049 const char* display_text = "";
4050 if (cd->selected_index >= 0 && (size_t)cd->selected_index < cd->nb_items) {
4051 display_text = cd->items[cd->selected_index].text;
4052 }
4053 int cbbbx = 0, cbbby = 0, cbbbw = 0, cbbbh = 0;
4054 al_get_text_dimensions(font, display_text[0] ? display_text : "Ay", &cbbbx, &cbbby, &cbbbw, &cbbbh);
4055 float fh = (float)cbbbh;
4056 float max_text_w = wgt->w - pad - style->dropdown_arrow_reserve;
4058 ax + pad, ay + (wgt->h - fh) / 2.0f - (float)cbbby, max_text_w, display_text);
4059
4060 /* dropdown arrow */
4061 float arrow_x = ax + wgt->w - style->dropdown_arrow_reserve;
4062 float arrow_y = ay + wgt->h / 2.0f;
4063 ALLEGRO_COLOR ac = wgt->theme.text_normal;
4064 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));
4065 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));
4066}
4067
4070 if (ctx->open_combobox_id < 0) return;
4072 if (!wgt || !wgt->data) return;
4074 if (!cd->is_open || cd->nb_items == 0) return;
4075
4076 /* find the window containing this widget to get absolute position */
4077 float ox = 0, oy = 0;
4078 if (!_find_widget_window(ctx, wgt->id, &ox, &oy)) return;
4079
4080 ALLEGRO_FONT* font = wgt->font ? wgt->font : ctx->default_font;
4081 if (!font) return;
4082
4083 float ax = ox + wgt->x;
4084 float ay = oy + wgt->y + wgt->h;
4085 float fh = (float)al_get_font_line_height(font);
4086 float ih = cd->item_height > fh ? cd->item_height : fh + ctx->style.item_height_pad;
4087 int vis = (int)cd->nb_items;
4088 if (vis > cd->max_visible) vis = cd->max_visible;
4089 float dropdown_h = (float)vis * ih;
4090 float pad = ctx->style.item_text_padding;
4091
4092 /* dropdown width — auto-expand if flag is set */
4093 float dropdown_w = wgt->w;
4094 if (cd->flags & N_GUI_COMBOBOX_AUTO_WIDTH) {
4095 float max_item_w = 0;
4096 for (size_t i = 0; i < cd->nb_items; i++) {
4097 float tw = _text_w(font, cd->items[i].text);
4098 if (tw > max_item_w) max_item_w = tw;
4099 }
4100 float needed = max_item_w + pad * 2;
4101 if (needed > dropdown_w) dropdown_w = needed;
4102 /* apply cap */
4103 float cap = ctx->style.combobox_max_dropdown_width;
4104 if (cap <= 0) cap = ctx->display_w > 0 ? (float)ctx->display_w : 4096.0f;
4105 if (dropdown_w > cap) dropdown_w = cap;
4106 /* clamp to display right edge */
4107 if (ctx->display_w > 0 && ax + dropdown_w > (float)ctx->display_w) {
4108 dropdown_w = (float)ctx->display_w - ax;
4109 if (dropdown_w < wgt->w) dropdown_w = wgt->w;
4110 }
4111 }
4112
4113 /* scrollbar geometry — only shown when items exceed max_visible */
4114 int need_scrollbar = ((int)cd->nb_items > cd->max_visible);
4115 float sb_size = ctx->style.scrollbar_size;
4116 if (sb_size < 10.0f) sb_size = 10.0f;
4117 float item_area_w = need_scrollbar ? dropdown_w - sb_size : dropdown_w;
4118
4119 /* dropdown background */
4120 if (cd->bg_bitmap) {
4121 al_draw_scaled_bitmap(cd->bg_bitmap, 0, 0,
4122 (float)al_get_bitmap_width(cd->bg_bitmap), (float)al_get_bitmap_height(cd->bg_bitmap),
4123 ax, ay, dropdown_w, dropdown_h, 0);
4124 } else {
4125 al_draw_filled_rectangle(ax, ay, ax + dropdown_w, ay + dropdown_h, wgt->theme.bg_normal);
4126 }
4127 al_draw_rectangle(ax, ay, ax + dropdown_w, ay + dropdown_h, wgt->theme.border_active, _min_thickness(ctx->style.dropdown_border_thickness));
4128
4129 /* clip item drawing to the dropdown area */
4130 int prev_cx = 0, prev_cy = 0, prev_cw = 0, prev_ch = 0;
4131 al_get_clipping_rectangle(&prev_cx, &prev_cy, &prev_cw, &prev_ch);
4132 al_set_clipping_rectangle((int)ax, (int)ay, (int)dropdown_w, (int)dropdown_h);
4133
4134 /* items */
4135 float max_text_w = item_area_w - pad * 2;
4136 for (int i = 0; i < vis && (size_t)(i + cd->scroll_offset) < cd->nb_items; i++) {
4137 int idx = i + cd->scroll_offset;
4138 float iy = ay + (float)i * ih;
4139 int hovered = _point_in_rect((float)ctx->mouse_x, (float)ctx->mouse_y,
4140 ax, iy, item_area_w, ih);
4141
4142 if (idx == cd->selected_index) {
4143 if (cd->item_selected_bitmap) {
4144 al_draw_scaled_bitmap(cd->item_selected_bitmap, 0, 0,
4145 (float)al_get_bitmap_width(cd->item_selected_bitmap), (float)al_get_bitmap_height(cd->item_selected_bitmap),
4146 ax + ctx->style.item_selection_inset, iy, item_area_w - ctx->style.item_selection_inset * 2, ih, 0);
4147 } else {
4148 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);
4149 }
4150 } else if (hovered) {
4151 if (cd->item_bg_bitmap) {
4152 al_draw_scaled_bitmap(cd->item_bg_bitmap, 0, 0,
4153 (float)al_get_bitmap_width(cd->item_bg_bitmap), (float)al_get_bitmap_height(cd->item_bg_bitmap),
4154 ax + ctx->style.item_selection_inset, iy, item_area_w - ctx->style.item_selection_inset * 2, ih, 0);
4155 } else {
4156 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);
4157 }
4158 }
4159
4160 ALLEGRO_COLOR tc = (idx == cd->selected_index) ? wgt->theme.text_active : wgt->theme.text_normal;
4161 _draw_text_truncated(font, tc, ax + pad, iy + (ih - fh) / 2.0f, max_text_w, cd->items[idx].text);
4162 }
4163
4164 al_set_clipping_rectangle(prev_cx, prev_cy, prev_cw, prev_ch);
4165
4166 /* scrollbar */
4167 if (need_scrollbar) {
4168 float sb_x = ax + dropdown_w - sb_size;
4169 /* track */
4170 al_draw_filled_rectangle(sb_x, ay, sb_x + sb_size, ay + dropdown_h,
4172 /* thumb — proportional to visible / total, positioned by scroll_offset */
4173 float ratio = (float)cd->max_visible / (float)cd->nb_items;
4174 if (ratio > 1.0f) ratio = 1.0f;
4175 float thumb_h = ratio * dropdown_h;
4176 float thumb_min = ctx->style.scrollbar_thumb_min;
4177 if (thumb_min < 12.0f) thumb_min = 12.0f;
4178 if (thumb_h < thumb_min) thumb_h = thumb_min;
4179 int max_offset = (int)cd->nb_items - cd->max_visible;
4180 float pos_ratio = (max_offset > 0) ? (float)cd->scroll_offset / (float)max_offset : 0.0f;
4181 float thumb_y = ay + pos_ratio * (dropdown_h - thumb_h);
4182 float thumb_pad = ctx->style.scrollbar_thumb_padding;
4183 float corner_r = ctx->style.scrollbar_thumb_corner_r;
4184 al_draw_filled_rounded_rectangle(sb_x + thumb_pad, thumb_y,
4185 sb_x + sb_size - thumb_pad,
4186 thumb_y + thumb_h,
4187 corner_r, corner_r,
4189 }
4190}
4191
4193static void _draw_dropmenu(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, const N_GUI_STYLE* style) {
4195 float ax = ox + wgt->x;
4196 float ay = oy + wgt->y;
4197 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
4198
4199 int draw_state = wgt->state;
4200 if (dd->is_open) draw_state |= N_GUI_STATE_ACTIVE;
4201
4202 _draw_themed_rect(&wgt->theme, draw_state, ax, ay, wgt->w, wgt->h, 0);
4203
4204 if (font && dd->label[0]) {
4205 ALLEGRO_COLOR tc = _text_for_state(&wgt->theme, draw_state);
4206 int dbbx = 0, dbby = 0, dbbw = 0, dbbh = 0;
4207 al_get_text_dimensions(font, dd->label, &dbbx, &dbby, &dbbw, &dbbh);
4208 float fh = (float)dbbh;
4209 float pad = style->item_text_padding;
4210 float max_text_w = wgt->w - pad - style->dropdown_arrow_reserve;
4211 _draw_text_truncated(font, tc, ax + pad, ay + (wgt->h - fh) / 2.0f - (float)dbby, max_text_w, dd->label);
4212 }
4213
4214 /* dropdown arrow */
4215 float arrow_x = ax + wgt->w - style->dropdown_arrow_reserve;
4216 float arrow_y = ay + wgt->h / 2.0f;
4217 ALLEGRO_COLOR ac = wgt->theme.text_normal;
4218 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));
4219 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));
4220}
4221
4224 if (ctx->open_dropmenu_id < 0) return;
4226 if (!wgt || !wgt->data) return;
4228 if (!dd->is_open || dd->nb_entries == 0) return;
4229
4230 /* find the window containing this widget to get absolute position */
4231 float ox = 0, oy = 0;
4232 if (!_find_widget_window(ctx, wgt->id, &ox, &oy)) return;
4233
4234 ALLEGRO_FONT* font = wgt->font ? wgt->font : ctx->default_font;
4235 if (!font) return;
4236
4237 float ax = ox + wgt->x;
4238 float ay = oy + wgt->y + wgt->h;
4239 float fh = (float)al_get_font_line_height(font);
4240 float ih = dd->item_height > fh ? dd->item_height : fh + ctx->style.item_height_pad;
4241 int vis = (int)dd->nb_entries;
4242 if (vis > dd->max_visible) vis = dd->max_visible;
4243 float panel_h = (float)vis * ih;
4244 float pad = ctx->style.item_text_padding;
4245
4246 /* scrollbar geometry — only shown when entries exceed max_visible */
4247 int need_scrollbar = ((int)dd->nb_entries > dd->max_visible);
4248 float sb_size = ctx->style.scrollbar_size;
4249 if (sb_size < 10.0f) sb_size = 10.0f;
4250 float item_area_w = need_scrollbar ? wgt->w - sb_size : wgt->w;
4251
4252 /* panel background */
4253 if (dd->panel_bitmap) {
4254 al_draw_scaled_bitmap(dd->panel_bitmap, 0, 0,
4255 (float)al_get_bitmap_width(dd->panel_bitmap), (float)al_get_bitmap_height(dd->panel_bitmap),
4256 ax, ay, wgt->w, panel_h, 0);
4257 } else {
4258 al_draw_filled_rectangle(ax, ay, ax + wgt->w, ay + panel_h, wgt->theme.bg_normal);
4259 }
4260 al_draw_rectangle(ax, ay, ax + wgt->w, ay + panel_h, wgt->theme.border_active, _min_thickness(ctx->style.dropdown_border_thickness));
4261
4262 /* entries */
4263 float max_text_w = item_area_w - pad * 2.0f;
4264 for (int i = 0; i < vis && (size_t)(i + dd->scroll_offset) < dd->nb_entries; i++) {
4265 int idx = i + dd->scroll_offset;
4266 float iy = ay + (float)i * ih;
4267 int hovered = _point_in_rect((float)ctx->mouse_x, (float)ctx->mouse_y,
4268 ax, iy, item_area_w, ih);
4269
4270 if (hovered) {
4271 if (dd->item_hover_bitmap) {
4272 al_draw_scaled_bitmap(dd->item_hover_bitmap, 0, 0,
4273 (float)al_get_bitmap_width(dd->item_hover_bitmap), (float)al_get_bitmap_height(dd->item_hover_bitmap),
4274 ax + ctx->style.item_selection_inset, iy, item_area_w - ctx->style.item_selection_inset * 2, ih, 0);
4275 } else {
4276 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);
4277 }
4278 }
4279
4280 ALLEGRO_COLOR tc = hovered ? wgt->theme.text_hover : wgt->theme.text_normal;
4281 _draw_text_truncated(font, tc, ax + pad, iy + (ih - fh) / 2.0f, max_text_w, dd->entries[idx].text);
4282 }
4283
4284 /* scrollbar */
4285 if (need_scrollbar) {
4286 float sb_x = ax + wgt->w - sb_size;
4287 /* track */
4288 al_draw_filled_rectangle(sb_x, ay, sb_x + sb_size, ay + panel_h,
4290 /* thumb — proportional to visible / total, positioned by scroll_offset */
4291 float ratio = (float)dd->max_visible / (float)dd->nb_entries;
4292 if (ratio > 1.0f) ratio = 1.0f;
4293 float thumb_h = ratio * panel_h;
4294 float thumb_min = ctx->style.scrollbar_thumb_min;
4295 if (thumb_min < 12.0f) thumb_min = 12.0f;
4296 if (thumb_h < thumb_min) thumb_h = thumb_min;
4297 int max_offset = (int)dd->nb_entries - dd->max_visible;
4298 float pos_ratio = (max_offset > 0) ? (float)dd->scroll_offset / (float)max_offset : 0.0f;
4299 float thumb_y = ay + pos_ratio * (panel_h - thumb_h);
4300 float thumb_pad = ctx->style.scrollbar_thumb_padding;
4301 float corner_r = ctx->style.scrollbar_thumb_corner_r;
4302 al_draw_filled_rounded_rectangle(sb_x + thumb_pad, thumb_y,
4303 sb_x + sb_size - thumb_pad,
4304 thumb_y + thumb_h,
4305 corner_r, corner_r,
4307 }
4308}
4309
4311static void _draw_image(N_GUI_WIDGET* wgt, float ox, float oy) {
4313 float ax = ox + wgt->x;
4314 float ay = oy + wgt->y;
4315
4316 /* border */
4317 al_draw_rectangle(ax, ay, ax + wgt->w, ay + wgt->h, wgt->theme.border_normal, wgt->theme.border_thickness);
4318
4319 if (!id->bitmap) return;
4320 float bw = (float)al_get_bitmap_width(id->bitmap);
4321 float bh = (float)al_get_bitmap_height(id->bitmap);
4322
4323 if (id->scale_mode == N_GUI_IMAGE_STRETCH) {
4324 al_draw_scaled_bitmap(id->bitmap, 0, 0, bw, bh, ax, ay, wgt->w, wgt->h, 0);
4325 } else if (id->scale_mode == N_GUI_IMAGE_CENTER) {
4326 float dx = ax + (wgt->w - bw) / 2.0f;
4327 float dy = ay + (wgt->h - bh) / 2.0f;
4328 al_draw_bitmap(id->bitmap, dx, dy, 0);
4329 } else {
4330 /* N_GUI_IMAGE_FIT */
4331 float scale_x = wgt->w / bw;
4332 float scale_y = wgt->h / bh;
4333 float scale = (scale_x < scale_y) ? scale_x : scale_y;
4334 float dw = bw * scale;
4335 float dh = bh * scale;
4336 float dx = ax + (wgt->w - dw) / 2.0f;
4337 float dy = ay + (wgt->h - dh) / 2.0f;
4338 al_draw_scaled_bitmap(id->bitmap, 0, 0, bw, bh, dx, dy, dw, dh, 0);
4339 }
4340}
4341
4347static int _label_char_at_x(const N_GUI_LABEL_DATA* lb, ALLEGRO_FONT* font, float click_x) {
4348 if (!font || !lb->text[0]) return -1;
4349 if (click_x <= 0) return 0;
4350 size_t len = strlen(lb->text);
4351 char tmp[N_GUI_TEXT_MAX];
4352 int best = 0;
4353 float best_dist = click_x;
4354 for (size_t i = 0; i < len;) {
4355 int clen = _utf8_char_len((unsigned char)lb->text[i]);
4356 if (i + (size_t)clen > len) clen = (int)(len - i);
4357 size_t pos = i + (size_t)clen;
4358 memcpy(tmp, lb->text, pos);
4359 tmp[pos] = '\0';
4360 float tw = _text_w(font, tmp);
4361 float dist = click_x - tw;
4362 if (dist < 0) dist = -dist;
4363 if (dist < best_dist) {
4364 best_dist = dist;
4365 best = (int)pos;
4366 }
4367 if (tw > click_x) break;
4368 i += (size_t)clen;
4369 }
4370 return best;
4371}
4372
4376static float _label_text_origin_x(N_GUI_LABEL_DATA* lb, ALLEGRO_FONT* font, float ax, float wgt_w, float win_w, float wgt_x, float label_padding) {
4377 float effective_w = wgt_w;
4378 if (win_w > 0) {
4379 float max_from_win = win_w - wgt_x;
4380 if (max_from_win > effective_w) effective_w = max_from_win;
4381 }
4382 int lbbx = 0, lbby = 0, lbbw = 0, lbbh = 0;
4383 al_get_text_dimensions(font, lb->text, &lbbx, &lbby, &lbbw, &lbbh);
4384 float tw = (float)lbbw;
4385 float max_text_w = effective_w - label_padding * 2;
4386 if (lb->align == N_GUI_ALIGN_CENTER) {
4387 if (tw > max_text_w) return ax + label_padding;
4388 return ax + (effective_w - tw) / 2.0f - (float)lbbx;
4389 }
4390 if (lb->align == N_GUI_ALIGN_RIGHT) {
4391 if (tw > max_text_w) return ax + label_padding;
4392 return ax + effective_w - tw - label_padding - (float)lbbx;
4393 }
4394 /* LEFT or JUSTIFIED (justified single-line fallback) */
4395 return ax + label_padding;
4396}
4397
4398static void _draw_label(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, float win_w, N_GUI_STYLE* style) {
4400 float ax = ox + wgt->x;
4401 float ay = oy + wgt->y;
4402 ALLEGRO_FONT* font = wgt->font ? wgt->font : default_font;
4403
4404 /* optional background bitmap */
4405 if (lb->bg_bitmap) {
4406 al_draw_scaled_bitmap(lb->bg_bitmap, 0, 0,
4407 (float)al_get_bitmap_width(lb->bg_bitmap), (float)al_get_bitmap_height(lb->bg_bitmap),
4408 ax, ay, wgt->w, wgt->h, 0);
4409 }
4410
4411 if (!font || !lb->text[0]) return;
4412
4413 int lbbbx = 0, lbbby = 0, lbbbw = 0, lbbbh = 0;
4414 al_get_text_dimensions(font, lb->text, &lbbbx, &lbbby, &lbbbw, &lbbbh);
4415 float fh = (float)lbbbh;
4416
4417 /* effective widget width: expand into available window space when resizable */
4418 float effective_w = wgt->w;
4419 if (win_w > 0) {
4420 float max_from_win = win_w - wgt->x;
4421 if (max_from_win > effective_w) effective_w = max_from_win;
4422 }
4423
4424 int is_link = (lb->link[0] != '\0') ? 1 : 0;
4425 ALLEGRO_COLOR tc;
4426 if (is_link) {
4427 /* links: show in a distinctive colour, underlined on hover */
4428 if (wgt->state & N_GUI_STATE_HOVER) {
4429 tc = style->link_color_hover;
4430 } else {
4431 tc = style->link_color_normal;
4432 }
4433 } else {
4434 tc = _text_for_state(&wgt->theme, wgt->state);
4435 }
4436
4437 float tw = (float)lbbbw;
4438 float max_text_w = effective_w - style->label_padding * 2; /* padding each side */
4439 float tx;
4440
4441 if (lb->align == N_GUI_ALIGN_JUSTIFIED) {
4442 float lpad = style->label_padding;
4443 float view_h = wgt->h - lpad;
4444 float content_h = _label_content_height(lb->text, font, max_text_w);
4445 int need_scrollbar = (content_h > view_h) ? 1 : 0;
4446 float sb_size = need_scrollbar ? style->scrollbar_size : 0;
4447 float text_w = max_text_w - sb_size;
4448
4449 /* clamp scroll_y */
4450 if (need_scrollbar) {
4451 float max_sy = content_h - view_h;
4452 if (max_sy < 0) max_sy = 0;
4453 if (lb->scroll_y > max_sy) lb->scroll_y = max_sy;
4454 if (lb->scroll_y < 0) lb->scroll_y = 0;
4455 } else {
4456 lb->scroll_y = 0;
4457 }
4458
4459 /* clip to widget bounds (minus scrollbar) */
4460 int pcx, pcy, pcw, pch;
4461 al_get_clipping_rectangle(&pcx, &pcy, &pcw, &pch);
4462 _set_clipping_rect_transformed((int)ax, (int)ay, (int)(wgt->w - sb_size), (int)wgt->h);
4463
4464 float text_y = ay + lpad / 2.0f - lb->scroll_y;
4465 /* draw selection highlight for justified text */
4466 if (lb->sel_start >= 0 && lb->sel_end >= 0 && lb->sel_start != lb->sel_end) {
4467 _draw_justified_selection(font, ax + lpad, text_y, text_w, content_h + fh,
4468 lb->text, lb->sel_start, lb->sel_end, wgt->theme.selection_color);
4469 }
4470 /* pass a large avail_h so _draw_text_justified doesn't truncate; clipping handles visibility */
4471 _draw_text_justified(font, tc, ax + lpad, text_y, text_w, content_h + fh, lb->text);
4472
4473 /* underline for links on hover */
4474 if (is_link && (wgt->state & N_GUI_STATE_HOVER)) {
4475 float draw_w = tw < text_w ? tw : text_w;
4476 al_draw_line(ax + lpad, text_y + fh, ax + lpad + draw_w, text_y + fh, tc, _min_thickness(style->link_underline_thickness));
4477 }
4478
4479 al_set_clipping_rectangle(pcx, pcy, pcw, pch);
4480
4481 /* draw scrollbar */
4482 if (need_scrollbar) {
4483 _draw_widget_vscrollbar(ax, ay + lpad / 2.0f, wgt->w, view_h, content_h, lb->scroll_y, style);
4484 }
4485 return;
4486 }
4487
4488 float ty = ay + (wgt->h - fh) / 2.0f - (float)lbbby;
4489
4490 if (lb->align == N_GUI_ALIGN_CENTER) {
4491 if (tw > max_text_w) {
4492 _draw_text_truncated(font, tc, ax + style->label_padding, ty, max_text_w, lb->text);
4493 tx = ax + style->label_padding;
4494 } else {
4495 tx = ax + (effective_w - tw) / 2.0f - (float)lbbbx;
4496 al_draw_text(font, tc, tx, ty, 0, lb->text);
4497 }
4498 } else if (lb->align == N_GUI_ALIGN_RIGHT) {
4499 if (tw > max_text_w) {
4500 _draw_text_truncated(font, tc, ax + style->label_padding, ty, max_text_w, lb->text);
4501 tx = ax + style->label_padding;
4502 } else {
4503 tx = ax + effective_w - tw - style->label_padding - (float)lbbbx;
4504 al_draw_text(font, tc, tx, ty, 0, lb->text);
4505 }
4506 } else {
4507 /* N_GUI_ALIGN_LEFT */
4508 tx = ax + style->label_padding;
4509 if (tw > max_text_w) {
4510 _draw_text_truncated(font, tc, tx, ty, max_text_w, lb->text);
4511 } else {
4512 al_draw_text(font, tc, tx, ty, 0, lb->text);
4513 }
4514 }
4515
4516 /* draw selection highlight for non-justified labels */
4517 if (lb->sel_start >= 0 && lb->sel_end >= 0 && lb->sel_start != lb->sel_end) {
4518 int slo = lb->sel_start < lb->sel_end ? lb->sel_start : lb->sel_end;
4519 int shi = lb->sel_start < lb->sel_end ? lb->sel_end : lb->sel_start;
4520 size_t tlen = strlen(lb->text);
4521 if ((size_t)slo > tlen) slo = (int)tlen;
4522 if ((size_t)shi > tlen) shi = (int)tlen;
4523 char stmp[N_GUI_TEXT_MAX];
4524 memcpy(stmp, lb->text, (size_t)slo);
4525 stmp[slo] = '\0';
4526 float sx1 = _text_w(font, stmp);
4527 memcpy(stmp, lb->text, (size_t)shi);
4528 stmp[shi] = '\0';
4529 float sx2 = _text_w(font, stmp);
4530 al_draw_filled_rectangle(tx + sx1, ty, tx + sx2, ty + fh, wgt->theme.selection_color);
4531 }
4532
4533 /* underline for links on hover */
4534 if (is_link && (wgt->state & N_GUI_STATE_HOVER)) {
4535 float draw_w = tw < max_text_w ? tw : max_text_w;
4536 al_draw_line(tx, ty + fh, tx + draw_w, ty + fh, tc, _min_thickness(style->link_underline_thickness));
4537 }
4538}
4539
4543static void _draw_widget(N_GUI_WIDGET* wgt, float ox, float oy, ALLEGRO_FONT* default_font, float win_w, N_GUI_STYLE* style) {
4544 if (!wgt || !wgt->visible) return;
4545
4546 /* disabled widgets: force idle state visually, draw with reduced opacity */
4547 int saved_state = wgt->state;
4548 if (!wgt->enabled) {
4549 wgt->state = N_GUI_STATE_IDLE;
4550 }
4551
4552 switch (wgt->type) {
4553 case N_GUI_TYPE_BUTTON:
4554 _draw_button(wgt, ox, oy, default_font, style);
4555 break;
4556 case N_GUI_TYPE_SLIDER:
4557 _draw_slider(wgt, ox, oy, default_font, style);
4558 break;
4560 _draw_textarea(wgt, ox, oy, default_font, style);
4561 break;
4563 _draw_checkbox(wgt, ox, oy, default_font, style);
4564 break;
4566 _draw_scrollbar(wgt, ox, oy, style);
4567 break;
4568 case N_GUI_TYPE_LISTBOX:
4569 _draw_listbox(wgt, ox, oy, default_font, style);
4570 break;
4572 _draw_radiolist(wgt, ox, oy, default_font, style);
4573 break;
4575 _draw_combobox(wgt, ox, oy, default_font, style);
4576 break;
4577 case N_GUI_TYPE_IMAGE:
4578 _draw_image(wgt, ox, oy);
4579 break;
4580 case N_GUI_TYPE_LABEL:
4581 _draw_label(wgt, ox, oy, default_font, win_w, style);
4582 break;
4584 _draw_dropmenu(wgt, ox, oy, default_font, style);
4585 break;
4586 default:
4587 break;
4588 }
4589
4590 /* restore state and draw dimming overlay for disabled widgets */
4591 if (!wgt->enabled) {
4592 wgt->state = saved_state;
4593 float dx = ox + wgt->x;
4594 float dy = oy + wgt->y;
4595 al_draw_filled_rectangle(dx, dy, dx + wgt->w, dy + wgt->h,
4596 al_map_rgba(0, 0, 0, 120));
4597 }
4598}
4599
4601static void _draw_window(N_GUI_WINDOW* win, ALLEGRO_FONT* default_font, N_GUI_STYLE* style) {
4602 if (!(win->state & N_GUI_WIN_OPEN)) return;
4603
4604 ALLEGRO_FONT* font = win->font ? win->font : default_font;
4605 int frameless = (win->flags & N_GUI_WIN_FRAMELESS) ? 1 : 0;
4606 float tbh = frameless ? 0.0f : win->titlebar_h;
4607
4608 /* title bar (skip for frameless windows) */
4609 if (!frameless) {
4610 if (win->titlebar_bitmap) {
4611 al_draw_scaled_bitmap(win->titlebar_bitmap, 0, 0,
4612 (float)al_get_bitmap_width(win->titlebar_bitmap), (float)al_get_bitmap_height(win->titlebar_bitmap),
4613 win->x, win->y, win->w, tbh, 0);
4614 } else {
4615 ALLEGRO_COLOR tb_bg = win->theme.bg_active;
4616 al_draw_filled_rounded_rectangle(win->x, win->y, win->x + win->w, win->y + tbh,
4617 win->theme.corner_rx, win->theme.corner_ry, tb_bg);
4618 }
4619 if (font && win->title[0]) {
4620 int tbbx = 0, tbby = 0, tbbw = 0, tbbh = 0;
4621 al_get_text_dimensions(font, win->title, &tbbx, &tbby, &tbbw, &tbbh);
4622 float fh = (float)tbbh;
4623 float max_title_w = win->w - style->title_max_w_reserve;
4625 win->x + style->title_padding, win->y + (tbh - fh) / 2.0f - (float)tbby, max_title_w, win->title);
4626 }
4627 }
4628
4629 ALLEGRO_COLOR tb_bd = win->theme.border_normal;
4630
4631 /* body (if not minimised) */
4632 if (!(win->state & N_GUI_WIN_MINIMISED)) {
4633 float body_y = win->y + tbh;
4634 float body_h = win->h - tbh;
4635
4636 /* determine if scrollbars are needed */
4637 int need_vscroll = 0;
4638 int need_hscroll = 0;
4639 float scrollbar_size = style->scrollbar_size;
4640
4641 if (win->flags & N_GUI_WIN_AUTO_SCROLLBAR) {
4642 _window_update_content_size(win, default_font);
4643 if (win->content_h > body_h) need_vscroll = 1;
4644 if (win->content_w > win->w) need_hscroll = 1;
4645 /* adjust for scrollbar space */
4646 if (need_vscroll && win->content_w > (win->w - scrollbar_size)) need_hscroll = 1;
4647 if (need_hscroll && win->content_h > (body_h - scrollbar_size)) need_vscroll = 1;
4648 }
4649
4650 float content_area_w = win->w - (need_vscroll ? scrollbar_size : 0);
4651 float content_area_h = body_h - (need_hscroll ? scrollbar_size : 0);
4652
4653 /* clamp scroll */
4654 if (need_vscroll) {
4655 float max_sy = win->content_h - content_area_h;
4656 if (max_sy < 0) max_sy = 0;
4657 if (win->scroll_y > max_sy) win->scroll_y = max_sy;
4658 if (win->scroll_y < 0) win->scroll_y = 0;
4659 } else {
4660 win->scroll_y = 0;
4661 }
4662 if (need_hscroll) {
4663 float max_sx = win->content_w - content_area_w;
4664 if (max_sx < 0) max_sx = 0;
4665 if (win->scroll_x > max_sx) win->scroll_x = max_sx;
4666 if (win->scroll_x < 0) win->scroll_x = 0;
4667 } else {
4668 win->scroll_x = 0;
4669 }
4670
4671 /* body background */
4672 if (win->bg_bitmap) {
4673 _draw_bitmap_scaled(win->bg_bitmap, win->x, body_y, win->w, body_h, win->bg_scale_mode);
4674 } else {
4675 al_draw_filled_rectangle(win->x, body_y, win->x + win->w, win->y + win->h,
4676 win->theme.bg_normal);
4677 }
4678 al_draw_rounded_rectangle(win->x, win->y, win->x + win->w, win->y + win->h,
4679 win->theme.corner_rx, win->theme.corner_ry, tb_bd,
4681
4682 /* draw widgets with clipping to content area */
4683 float ox = win->x - win->scroll_x;
4684 float oy = body_y - win->scroll_y;
4685
4686 /* set clipping rectangle to window content area */
4687 int prev_cx, prev_cy, prev_cw, prev_ch;
4688 al_get_clipping_rectangle(&prev_cx, &prev_cy, &prev_cw, &prev_ch);
4689 _set_clipping_rect_transformed((int)win->x + (int)style->scrollbar_thumb_padding, (int)body_y,
4690 (int)content_area_w - (int)style->scrollbar_thumb_padding, (int)content_area_h);
4691
4692 /* pass width hint so labels can adapt:
4693 * - auto-scrollbar windows: use content width so labels are never truncated
4694 * (the clipping rect + scrollbars handle overflow)
4695 * - resizable windows (without auto-scrollbar): use window width so labels expand
4696 * - fixed windows: 0 (use widget's own width) */
4697 float label_win_w = 0;
4699 label_win_w = win->content_w + style->title_max_w_reserve + style->label_padding;
4700 else if (win->flags & N_GUI_WIN_RESIZABLE)
4701 label_win_w = win->w;
4702 list_foreach(node, win->widgets) {
4703 _draw_widget((N_GUI_WIDGET*)node->ptr, ox, oy, default_font, label_win_w, style);
4704 }
4705
4706 /* restore clipping */
4707 al_set_clipping_rectangle(prev_cx, prev_cy, prev_cw, prev_ch);
4708
4709 /* draw automatic scrollbars */
4710 if (need_vscroll) {
4711 float sb_x = win->x + win->w - scrollbar_size;
4712 float sb_y = body_y;
4713 float sb_h = content_area_h;
4714 /* leave room for resize corner when no horizontal scrollbar */
4715 if ((win->flags & N_GUI_WIN_RESIZABLE) && !need_hscroll) sb_h -= scrollbar_size;
4716
4717 /* track */
4718 al_draw_filled_rectangle(sb_x, sb_y, sb_x + scrollbar_size, sb_y + sb_h,
4719 style->scrollbar_track_color);
4720
4721 /* thumb */
4722 float ratio = content_area_h / win->content_h;
4723 if (ratio > 1.0f) ratio = 1.0f;
4724 float thumb_h = ratio * sb_h;
4725 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
4726 float max_scroll = win->content_h - content_area_h;
4727 float pos_ratio = (max_scroll > 0) ? win->scroll_y / max_scroll : 0;
4728 float thumb_y = sb_y + pos_ratio * (sb_h - thumb_h);
4729 al_draw_filled_rounded_rectangle(sb_x + style->scrollbar_thumb_padding, thumb_y, sb_x + scrollbar_size - style->scrollbar_thumb_padding,
4730 thumb_y + thumb_h, style->scrollbar_thumb_corner_r, style->scrollbar_thumb_corner_r,
4731 style->scrollbar_thumb_color);
4732 }
4733 if (need_hscroll) {
4734 float sb_x = win->x;
4735 float sb_y = win->y + win->h - scrollbar_size;
4736 float sb_w = content_area_w;
4737 /* leave room for resize corner when no vertical scrollbar */
4738 if ((win->flags & N_GUI_WIN_RESIZABLE) && !need_vscroll) sb_w -= scrollbar_size;
4739
4740 /* track */
4741 al_draw_filled_rectangle(sb_x, sb_y, sb_x + sb_w, sb_y + scrollbar_size,
4742 style->scrollbar_track_color);
4743
4744 /* thumb */
4745 float ratio = content_area_w / win->content_w;
4746 if (ratio > 1.0f) ratio = 1.0f;
4747 float thumb_w = ratio * sb_w;
4748 if (thumb_w < style->scrollbar_thumb_min) thumb_w = style->scrollbar_thumb_min;
4749 float max_scroll = win->content_w - content_area_w;
4750 float pos_ratio = (max_scroll > 0) ? win->scroll_x / max_scroll : 0;
4751 float thumb_x = sb_x + pos_ratio * (sb_w - thumb_w);
4752 al_draw_filled_rounded_rectangle(thumb_x, sb_y + style->scrollbar_thumb_padding, thumb_x + thumb_w,
4753 sb_y + scrollbar_size - style->scrollbar_thumb_padding, style->scrollbar_thumb_corner_r, style->scrollbar_thumb_corner_r,
4754 style->scrollbar_thumb_color);
4755 }
4756
4757 /* draw resize handle if window is resizable */
4758 if (win->flags & N_GUI_WIN_RESIZABLE) {
4759 float rx = win->x + win->w;
4760 float ry = win->y + win->h;
4761 float grip_size = style->grip_size;
4762 ALLEGRO_COLOR grip_color = style->grip_color;
4763 /* three diagonal lines in bottom-right corner */
4764 float grip_thick = _min_thickness(style->grip_line_thickness);
4765 al_draw_line(rx - grip_size, ry - 2, rx - 2, ry - grip_size, grip_color, grip_thick);
4766 al_draw_line(rx - grip_size + 4, ry - 2, rx - 2, ry - grip_size + 4, grip_color, grip_thick);
4767 al_draw_line(rx - grip_size + 8, ry - 2, rx - 2, ry - grip_size + 8, grip_color, grip_thick);
4768 }
4769 } else {
4770 al_draw_rounded_rectangle(win->x, win->y, win->x + win->w, win->y + tbh,
4771 win->theme.corner_rx, win->theme.corner_ry, tb_bd,
4773 }
4774}
4775
4778 float max_r = 0, max_b = 0;
4779 list_foreach(wnode, ctx->windows) {
4780 const N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
4781 if (!(win->state & N_GUI_WIN_OPEN)) continue;
4782 float r = win->x + win->w;
4783 float b = win->y + win->h;
4784 if (r > max_r) max_r = r;
4785 if (b > max_b) max_b = b;
4786 }
4787 ctx->gui_bounds_w = max_r;
4788 ctx->gui_bounds_h = max_b;
4789}
4790
4801 __n_assert(ctx, return);
4802
4803 /* compute bounds and determine if global scrollbars are needed.
4804 * When virtual canvas is active, compare bounds against virtual dimensions
4805 * (since window positions are in virtual coordinates). */
4807 float scrollbar_size = ctx->style.global_scrollbar_size;
4808 int need_global_vscroll = 0;
4809 int need_global_hscroll = 0;
4810
4811 /* gate on both dimensions: virtual canvas is only active when both are set */
4812 int virtual_active = (ctx->virtual_w > 0 && ctx->virtual_h > 0);
4813 float eff_w = virtual_active ? ctx->virtual_w : ctx->display_w;
4814 float eff_h = virtual_active ? ctx->virtual_h : ctx->display_h;
4815
4816 if (eff_w > 0 && eff_h > 0) {
4817 if (ctx->gui_bounds_h > eff_h) need_global_vscroll = 1;
4818 if (ctx->gui_bounds_w > eff_w) need_global_hscroll = 1;
4819 /* account for scrollbar space */
4820 if (need_global_vscroll && ctx->gui_bounds_w > (eff_w - scrollbar_size)) need_global_hscroll = 1;
4821 if (need_global_hscroll && ctx->gui_bounds_h > (eff_h - scrollbar_size)) need_global_vscroll = 1;
4822
4823 /* clamp global scroll */
4824 float view_w = eff_w - (need_global_vscroll ? scrollbar_size : 0);
4825 float view_h = eff_h - (need_global_hscroll ? scrollbar_size : 0);
4826 if (need_global_vscroll) {
4827 float max_sy = ctx->gui_bounds_h - view_h;
4828 if (max_sy < 0) max_sy = 0;
4829 if (ctx->global_scroll_y > max_sy) ctx->global_scroll_y = max_sy;
4830 if (ctx->global_scroll_y < 0) ctx->global_scroll_y = 0;
4831 } else {
4832 ctx->global_scroll_y = 0;
4833 }
4834 if (need_global_hscroll) {
4835 float max_sx = ctx->gui_bounds_w - view_w;
4836 if (max_sx < 0) max_sx = 0;
4837 if (ctx->global_scroll_x > max_sx) ctx->global_scroll_x = max_sx;
4838 if (ctx->global_scroll_x < 0) ctx->global_scroll_x = 0;
4839 } else {
4840 ctx->global_scroll_x = 0;
4841 }
4842 }
4843
4844 /* apply global scroll in virtual units, then scale + offset to physical display */
4845 ALLEGRO_TRANSFORM global_tf, prev_tf;
4846 al_copy_transform(&prev_tf, al_get_current_transform());
4847 al_identity_transform(&global_tf);
4848 al_translate_transform(&global_tf, -ctx->global_scroll_x, -ctx->global_scroll_y);
4849 if (ctx->virtual_w > 0 && ctx->virtual_h > 0 && ctx->gui_scale > 0) {
4850 al_scale_transform(&global_tf, ctx->gui_scale, ctx->gui_scale);
4851 al_translate_transform(&global_tf, ctx->gui_offset_x, ctx->gui_offset_y);
4852 }
4853 al_compose_transform(&global_tf, &prev_tf);
4854 al_use_transform(&global_tf);
4855
4856 /* clip to the content area (excluding global scrollbar space) */
4857 int prev_cx, prev_cy, prev_cw, prev_ch;
4858 int set_clip = 0;
4859 if (need_global_vscroll || need_global_hscroll) {
4860 al_get_clipping_rectangle(&prev_cx, &prev_cy, &prev_cw, &prev_ch);
4861 /* clipping is in physical pixels; map virtual scrollbar area to screen */
4862 float clip_vw = eff_w - (need_global_vscroll ? scrollbar_size : 0);
4863 float clip_vh = eff_h - (need_global_hscroll ? scrollbar_size : 0);
4864 if (ctx->virtual_w > 0 && ctx->virtual_h > 0 && ctx->gui_scale > 0) {
4865 al_set_clipping_rectangle((int)ctx->gui_offset_x, (int)ctx->gui_offset_y,
4866 (int)(clip_vw * ctx->gui_scale),
4867 (int)(clip_vh * ctx->gui_scale));
4868 } else {
4869 al_set_clipping_rectangle(0, 0, (int)clip_vw, (int)clip_vh);
4870 }
4871 set_clip = 1;
4872 }
4873
4874 /* auto-apply autofit for windows that have it configured */
4875 list_foreach(anode, ctx->windows) {
4876 const N_GUI_WINDOW* awin = (N_GUI_WINDOW*)anode->ptr;
4877 if (awin && awin->autofit_flags) {
4878 n_gui_window_apply_autofit(ctx, awin->id);
4879 }
4880 }
4881
4882 list_foreach(node, ctx->windows) {
4883 _draw_window((N_GUI_WINDOW*)node->ptr, ctx->default_font, &ctx->style);
4884 }
4885 /* draw combobox dropdown overlay on top of everything */
4887 /* draw dropdown menu panel overlay on top of everything */
4889
4890 /* restore transform and clipping */
4891 if (set_clip) {
4892 al_set_clipping_rectangle(prev_cx, prev_cy, prev_cw, prev_ch);
4893 }
4894
4895 /* draw global scrollbars: apply virtual canvas transform (without scroll)
4896 * so scrollbars appear at the edges of the virtual canvas area */
4897 if (need_global_vscroll || need_global_hscroll) {
4898 ALLEGRO_TRANSFORM sb_tf;
4899 al_identity_transform(&sb_tf);
4900 if (ctx->virtual_w > 0 && ctx->virtual_h > 0 && ctx->gui_scale > 0) {
4901 al_scale_transform(&sb_tf, ctx->gui_scale, ctx->gui_scale);
4902 al_translate_transform(&sb_tf, ctx->gui_offset_x, ctx->gui_offset_y);
4903 }
4904 al_compose_transform(&sb_tf, &prev_tf);
4905 al_use_transform(&sb_tf);
4906 }
4907
4908 if (need_global_vscroll) {
4909 float sb_x = eff_w - scrollbar_size;
4910 float sb_y = 0;
4911 float sb_h = eff_h - (need_global_hscroll ? scrollbar_size : 0);
4912 float view_h = sb_h;
4913
4914 /* track */
4915 al_draw_filled_rectangle(sb_x, sb_y, sb_x + scrollbar_size, sb_y + sb_h,
4917
4918 /* thumb */
4919 float ratio = view_h / ctx->gui_bounds_h;
4920 if (ratio > 1.0f) ratio = 1.0f;
4921 float thumb_h = ratio * sb_h;
4922 if (thumb_h < ctx->style.global_scrollbar_thumb_min) thumb_h = ctx->style.global_scrollbar_thumb_min;
4923 float max_scroll = ctx->gui_bounds_h - view_h;
4924 float pos_ratio = (max_scroll > 0) ? ctx->global_scroll_y / max_scroll : 0;
4925 float thumb_y = sb_y + pos_ratio * (sb_h - thumb_h);
4926 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,
4929 al_draw_rounded_rectangle(sb_x + ctx->style.global_scrollbar_thumb_padding, thumb_y, sb_x + scrollbar_size - ctx->style.global_scrollbar_thumb_padding,
4932 }
4933 if (need_global_hscroll) {
4934 float sb_x = 0;
4935 float sb_y = eff_h - scrollbar_size;
4936 float sb_w = eff_w - (need_global_vscroll ? scrollbar_size : 0);
4937 float view_w = sb_w;
4938
4939 /* track */
4940 al_draw_filled_rectangle(sb_x, sb_y, sb_x + sb_w, sb_y + scrollbar_size,
4942
4943 /* thumb */
4944 float ratio = view_w / ctx->gui_bounds_w;
4945 if (ratio > 1.0f) ratio = 1.0f;
4946 float thumb_w = ratio * sb_w;
4947 if (thumb_w < ctx->style.global_scrollbar_thumb_min) thumb_w = ctx->style.global_scrollbar_thumb_min;
4948 float max_scroll = ctx->gui_bounds_w - view_w;
4949 float pos_ratio = (max_scroll > 0) ? ctx->global_scroll_x / max_scroll : 0;
4950 float thumb_x = sb_x + pos_ratio * (sb_w - thumb_w);
4951 al_draw_filled_rounded_rectangle(thumb_x, sb_y + ctx->style.global_scrollbar_thumb_padding, thumb_x + thumb_w,
4954 al_draw_rounded_rectangle(thumb_x, sb_y + ctx->style.global_scrollbar_thumb_padding, thumb_x + thumb_w,
4957 }
4958
4959 al_use_transform(&prev_tf);
4960}
4961
4962/* =========================================================================
4963 * VIRTUAL CANVAS / DISPLAY / DPI
4964 * ========================================================================= */
4965
4974void n_gui_set_virtual_size(N_GUI_CTX* ctx, float w, float h) {
4975 __n_assert(ctx, return);
4976 if ((w > 0) != (h > 0)) {
4977 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);
4978 return;
4979 }
4980 ctx->virtual_w = w;
4981 ctx->virtual_h = h;
4983}
4984
4993 __n_assert(ctx, return);
4994
4995 if (ctx->virtual_w <= 0 || ctx->virtual_h <= 0 ||
4996 ctx->display_w <= 0 || ctx->display_h <= 0) {
4997 ctx->gui_scale = 1.0f;
4998 ctx->gui_offset_x = 0.0f;
4999 ctx->gui_offset_y = 0.0f;
5000 return;
5001 }
5002
5003 float sx = ctx->display_w / ctx->virtual_w;
5004 float sy = ctx->display_h / ctx->virtual_h;
5005 ctx->gui_scale = (sx < sy) ? sx : sy;
5006 if (ctx->gui_scale < 0.01f) ctx->gui_scale = 0.01f;
5007
5008 float scaled_w = ctx->virtual_w * ctx->gui_scale;
5009 float scaled_h = ctx->virtual_h * ctx->gui_scale;
5010 ctx->gui_offset_x = (ctx->display_w - scaled_w) * 0.5f;
5011 ctx->gui_offset_y = (ctx->display_h - scaled_h) * 0.5f;
5012}
5013
5019void n_gui_screen_to_virtual(const N_GUI_CTX* ctx, float sx, float sy, float* vx, float* vy) {
5020 __n_assert(ctx, return);
5021
5022 if (ctx->virtual_w <= 0 || ctx->virtual_h <= 0 || ctx->gui_scale <= 0) {
5023 if (vx) *vx = sx;
5024 if (vy) *vy = sy;
5025 return;
5026 }
5027
5028 float scale = ctx->gui_scale;
5029 if (vx) *vx = (sx - ctx->gui_offset_x) / scale;
5030 if (vy) *vy = (sy - ctx->gui_offset_y) / scale;
5031}
5032
5033/* =========================================================================
5034 * ADAPTIVE RESIZE PUBLIC API
5035 * ========================================================================= */
5036
5043 __n_assert(ctx, return);
5044 ctx->resize_mode = mode;
5045
5046 if (mode == N_GUI_RESIZE_ADAPTIVE) {
5047 /* disable virtual canvas scaling — virtual tracks display */
5048 ctx->virtual_w = 0;
5049 ctx->virtual_h = 0;
5050 ctx->gui_scale = 1.0f;
5051 ctx->gui_offset_x = 0.0f;
5052 ctx->gui_offset_y = 0.0f;
5053
5054 /* set reference display size */
5055 ctx->ref_display_w = ctx->display_w;
5056 ctx->ref_display_h = ctx->display_h;
5057
5058 /* capture normalized coords for all windows */
5059 list_foreach(wnode, ctx->windows) {
5060 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
5062 }
5063 }
5064}
5065
5070// cppcheck-suppress constParameterPointer ; public API uses non-const for consistency
5072 __n_assert(ctx, return 0);
5073 return ctx->resize_mode;
5074}
5075
5081void n_gui_window_set_resize_policy(N_GUI_CTX* ctx, int window_id, int policy) {
5082 __n_assert(ctx, return);
5083 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
5084 if (!win) return;
5085 win->resize_policy = policy;
5087}
5088
5093// cppcheck-suppress constParameterPointer ; public API uses non-const for consistency
5095 __n_assert(ctx, return 0);
5096 // cppcheck-suppress constVariablePointer ; returned from non-const API
5097 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
5098 if (!win) return 0;
5099 return win->resize_policy;
5100}
5101
5108 __n_assert(ctx, return);
5109 N_GUI_WINDOW* win = n_gui_get_window(ctx, window_id);
5110 if (!win) return;
5111
5112 /* update reference to current display size before recapture */
5113 ctx->ref_display_w = ctx->display_w;
5114 ctx->ref_display_h = ctx->display_h;
5116}
5117
5123void n_gui_apply_adaptive_resize(N_GUI_CTX* ctx, float new_w, float new_h) {
5124 __n_assert(ctx, return);
5125 if (new_w <= 0 || new_h <= 0) return;
5126
5127 list_foreach(wnode, ctx->windows) {
5128 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
5129
5130 switch (win->resize_policy) {
5132 win->x = win->norm_x * new_w;
5133 win->y = win->norm_y * new_h;
5134 /* size unchanged */
5135 break;
5136
5138 win->x = win->norm_x * new_w;
5139 win->y = win->norm_y * new_h;
5140 win->w = win->norm_w * new_w;
5141 win->h = win->norm_h * new_h;
5142
5143 /* enforce minimums only on user-resizable (non-frameless) windows */
5144 if (!(win->flags & N_GUI_WIN_FRAMELESS)) {
5145 if (win->w < win->min_w) win->w = win->min_w;
5146 if (win->h < win->min_h) win->h = win->min_h;
5147 }
5148
5149 /* scale child widgets proportionally to new window size */
5150 list_foreach(wgn, win->widgets) {
5151 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
5152 wgt->x = wgt->norm_x * win->w;
5153 wgt->y = wgt->norm_y * win->h;
5154 wgt->w = wgt->norm_w * win->w;
5155 wgt->h = wgt->norm_h * win->h;
5156 }
5157 break;
5158 }
5159
5161 default:
5162 /* do nothing */
5163 break;
5164 }
5165 }
5166
5167 /* keep ref_display_w/h at the ORIGINAL creation size so norms remain
5168 stable across multiple resizes (always relative to initial layout) */
5169}
5170
5176void n_gui_set_display_size(N_GUI_CTX* ctx, float w, float h) {
5177 __n_assert(ctx, return);
5178 ctx->display_w = w;
5179 ctx->display_h = h;
5180 if (ctx->resize_mode == N_GUI_RESIZE_ADAPTIVE) {
5181 n_gui_apply_adaptive_resize(ctx, w, h);
5182 } else {
5183 if (ctx->virtual_w > 0 && ctx->virtual_h > 0) {
5185 }
5186 }
5187}
5188
5192void n_gui_set_display(N_GUI_CTX* ctx, ALLEGRO_DISPLAY* display) {
5193 __n_assert(ctx, return);
5194 ctx->display = display;
5195}
5196
5200void n_gui_set_dpi_scale(N_GUI_CTX* ctx, float scale) {
5201 __n_assert(ctx, return);
5202 if (scale < 0.25f) scale = 0.25f;
5203 if (scale > 8.0f) scale = 8.0f;
5204 ctx->dpi_scale = scale;
5205}
5206
5211 __n_assert(ctx, return 1.0f);
5212 return ctx->dpi_scale;
5213}
5214
5234float n_gui_detect_dpi_scale(N_GUI_CTX* ctx, ALLEGRO_DISPLAY* display) {
5235 __n_assert(ctx, return 1.0f);
5236 __n_assert(display, return 1.0f);
5237
5238 float scale = 1.0f;
5239
5240 /* Method: compare physical pixel size to logical window size.
5241 * On HiDPI displays, al_get_display_width returns the logical size while
5242 * the backbuffer bitmap has the physical pixel size. */
5243 int logical_w = al_get_display_width(display);
5244 int logical_h = al_get_display_height(display);
5245 ALLEGRO_BITMAP* backbuffer = al_get_backbuffer(display);
5246 if (backbuffer && logical_w > 0 && logical_h > 0) {
5247 int physical_w = al_get_bitmap_width(backbuffer);
5248 int physical_h = al_get_bitmap_height(backbuffer);
5249 float scale_x = (float)physical_w / (float)logical_w;
5250 float scale_y = (float)physical_h / (float)logical_h;
5251 scale = (scale_x > scale_y) ? scale_x : scale_y;
5252 if (scale < 0.5f) scale = 1.0f; /* sanity */
5253 }
5254
5255#ifdef ALLEGRO_ANDROID
5256 /* On Android, use the display DPI option if available.
5257 * Android standard baseline is 160 DPI; scale = actual_dpi / 160. */
5258 int dpi = al_get_display_option(display, ALLEGRO_DEFAULT_DISPLAY_ADAPTER);
5259 if (dpi <= 0) {
5260 /* fallback: try the pixel ratio we already computed */
5261 } else {
5262 float android_scale = (float)dpi / 160.0f;
5263 if (android_scale >= 0.5f) scale = android_scale;
5264 }
5265#endif
5266
5267 if (scale < 0.25f) scale = 0.25f;
5268 if (scale > 8.0f) scale = 8.0f;
5269 ctx->dpi_scale = scale;
5270 return scale;
5271}
5272
5273/* =========================================================================
5274 * EVENT PROCESSING
5275 * ========================================================================= */
5276
5278static void _slider_update_from_mouse(N_GUI_WIDGET* wgt, float mx, float my, float win_x, float win_content_y) {
5280 if (wgt->w <= 0) return;
5281 if (wgt->h <= 0) return;
5282 double ratio;
5283
5284 if (sd->orientation == N_GUI_SLIDER_V) {
5285 float ay = win_content_y + wgt->y;
5286 float local_y = my - ay;
5287 /* vertical slider: bottom = max, top = min, so invert */
5288 ratio = 1.0 - (double)local_y / (double)wgt->h;
5289 } else {
5290 float ax = win_x + wgt->x;
5291 float local_x = mx - ax;
5292 ratio = (double)local_x / (double)wgt->w;
5293 }
5294 if (ratio < 0.0) ratio = 0.0;
5295 if (ratio > 1.0) ratio = 1.0;
5296 double new_val = sd->min_val + ratio * (sd->max_val - sd->min_val);
5297 if (sd->step > 0.0)
5298 new_val = _slider_snap_value(new_val, sd->min_val, sd->max_val, sd->step);
5299 else
5300 new_val = _clamp(new_val, sd->min_val, sd->max_val);
5301 if (new_val != sd->value) {
5302 sd->value = new_val;
5303 if (sd->on_change) {
5304 sd->on_change(wgt->id, sd->value, sd->user_data);
5305 }
5306 }
5307}
5308
5310static 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) {
5312 float ax = win_x + wgt->x;
5313 float ay = win_content_y + wgt->y;
5314
5315 double ratio_viewport = sb->viewport_size / sb->content_size;
5316 if (ratio_viewport > 1.0) ratio_viewport = 1.0;
5317 double max_scroll = sb->content_size - sb->viewport_size;
5318 if (max_scroll < 0) max_scroll = 0;
5319
5320 double pos_ratio;
5321 if (sb->orientation == N_GUI_SCROLLBAR_V) {
5322 float thumb_h = (float)(ratio_viewport * (double)wgt->h);
5323 if (thumb_h < style->scrollbar_thumb_min) thumb_h = style->scrollbar_thumb_min;
5324 float track_range = wgt->h - thumb_h;
5325 if (track_range <= 0) return;
5326 pos_ratio = (double)(my - ay - thumb_h / 2.0f) / (double)track_range;
5327 } else {
5328 float thumb_w = (float)(ratio_viewport * (double)wgt->w);
5329 if (thumb_w < style->scrollbar_thumb_min) thumb_w = style->scrollbar_thumb_min;
5330 float track_range = wgt->w - thumb_w;
5331 if (track_range <= 0) return;
5332 pos_ratio = (double)(mx - ax - thumb_w / 2.0f) / (double)track_range;
5333 }
5334 if (pos_ratio < 0.0) pos_ratio = 0.0;
5335 if (pos_ratio > 1.0) pos_ratio = 1.0;
5336 sb->scroll_pos = pos_ratio * max_scroll;
5337 if (sb->on_scroll) {
5338 sb->on_scroll(wgt->id, sb->scroll_pos, sb->user_data);
5339 }
5340}
5341
5343static int _utf8_encode(int cp, char* out) {
5344 if (cp < 0x80) {
5345 out[0] = (char)cp;
5346 return 1;
5347 }
5348 if (cp < 0x800) {
5349 out[0] = (char)(0xC0 | (cp >> 6));
5350 out[1] = (char)(0x80 | (cp & 0x3F));
5351 return 2;
5352 }
5353 if (cp < 0x10000) {
5354 out[0] = (char)(0xE0 | (cp >> 12));
5355 out[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
5356 out[2] = (char)(0x80 | (cp & 0x3F));
5357 return 3;
5358 }
5359 if (cp < 0x110000) {
5360 out[0] = (char)(0xF0 | (cp >> 18));
5361 out[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
5362 out[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
5363 out[3] = (char)(0x80 | (cp & 0x3F));
5364 return 4;
5365 }
5366 return 0;
5367}
5368
5371 td->sel_start = td->cursor_pos;
5372 td->sel_end = td->cursor_pos;
5373}
5374
5378 if (!_textarea_has_selection(td)) return;
5379 size_t lo, hi;
5380 _textarea_sel_range(td, &lo, &hi);
5381 size_t del_len = hi - lo;
5382 memmove(&td->text[lo], &td->text[hi], td->text_len - hi + 1);
5383 td->text_len -= del_len;
5384 td->cursor_pos = lo;
5386 if (td->on_change) td->on_change(wgt->id, td->text, td->user_data);
5387}
5388
5390static int _textarea_copy_to_clipboard(const N_GUI_TEXTAREA_DATA* td, ALLEGRO_DISPLAY* display) {
5391 if (!display || !_textarea_has_selection(td)) return 0;
5392 size_t lo, hi;
5393 _textarea_sel_range(td, &lo, &hi);
5394 size_t len = hi - lo;
5395 char* tmp = NULL;
5396 Malloc(tmp, char, len + 1);
5397 if (!tmp) return 0;
5398 memcpy(tmp, &td->text[lo], len);
5399 tmp[len] = '\0';
5400 al_set_clipboard_text(display, tmp);
5401 FreeNoLog(tmp);
5402 return 1;
5403}
5404
5408static int _textarea_handle_key(N_GUI_WIDGET* wgt, ALLEGRO_EVENT* ev, ALLEGRO_FONT* font, float pad, float sb_size, N_GUI_CTX* ctx) {
5410
5411 /* reset blink timer so cursor stays visible during typing */
5412 td->cursor_time = al_get_time();
5413 /* clear mouse-wheel scroll flag so auto-scroll resumes following the cursor */
5414 td->scroll_from_wheel = 0;
5415
5416 int shift = (ev->keyboard.modifiers & ALLEGRO_KEYMOD_SHIFT) ? 1 : 0;
5417 int ctrl = (ev->keyboard.modifiers & ALLEGRO_KEYMOD_CTRL) ? 1 : 0;
5418
5419 /* Ctrl+A: select all */
5420 if (ctrl && ev->keyboard.keycode == ALLEGRO_KEY_A) {
5421 td->sel_start = 0;
5422 td->sel_end = td->text_len;
5423 td->cursor_pos = td->text_len;
5424 return 1;
5425 }
5426
5427 /* Ctrl+C: copy */
5428 if (ctrl && ev->keyboard.keycode == ALLEGRO_KEY_C) {
5429 if (ctx && ctx->display) {
5431 }
5432 return 1;
5433 }
5434
5435 /* Ctrl+X: cut */
5436 if (ctrl && ev->keyboard.keycode == ALLEGRO_KEY_X) {
5437 if (ctx && ctx->display && _textarea_has_selection(td)) {
5440 }
5441 return 1;
5442 }
5443
5444 /* Ctrl+V: paste */
5445 if (ctrl && ev->keyboard.keycode == ALLEGRO_KEY_V) {
5446 if (ctx && ctx->display) {
5447 char* clip = al_get_clipboard_text(ctx->display);
5448 if (clip) {
5449 /* delete selection first if any */
5450 if (_textarea_has_selection(td)) {
5452 }
5453 size_t clip_len = strlen(clip);
5454 /* filter out non-printable except newline in multiline mode;
5455 * skip \r to normalize CRLF for Allegro5 compatibility */
5456 char filtered[N_GUI_TEXT_MAX];
5457 size_t flen = 0;
5458 for (size_t ci = 0; ci < clip_len && flen < N_GUI_TEXT_MAX - 1; ci++) {
5459 if (clip[ci] == '\r') {
5460 continue; /* strip CR — CRLF becomes just LF */
5461 } else if (clip[ci] == '\n') {
5462 if (td->multiline) filtered[flen++] = '\n';
5463 } else if ((unsigned char)clip[ci] >= 32 || ((unsigned char)clip[ci] & 0xC0) == 0x80) {
5464 filtered[flen++] = clip[ci];
5465 } else if (((unsigned char)clip[ci] & 0xC0) == 0xC0) {
5466 filtered[flen++] = clip[ci]; /* UTF-8 lead byte */
5467 }
5468 }
5469 filtered[flen] = '\0';
5470 if (flen > 0 && td->text_len + flen <= td->char_limit) {
5471 memmove(&td->text[td->cursor_pos + flen], &td->text[td->cursor_pos], td->text_len - td->cursor_pos + 1);
5472 memcpy(&td->text[td->cursor_pos], filtered, flen);
5473 td->cursor_pos += flen;
5474 td->text_len += flen;
5476 if (td->on_change) td->on_change(wgt->id, td->text, td->user_data);
5477 }
5478 al_free(clip);
5479 }
5480 }
5481 return 1;
5482 }
5483
5484 if (ev->keyboard.keycode == ALLEGRO_KEY_BACKSPACE) {
5485 if (_textarea_has_selection(td)) {
5487 } else if (td->cursor_pos > 0 && td->text_len > 0) {
5488 size_t erase_start = td->cursor_pos;
5489 int cont_count = 0;
5490 do {
5491 erase_start--;
5492 cont_count++;
5493 } while (erase_start > 0 && cont_count <= 3 && ((unsigned char)td->text[erase_start] & 0xC0) == 0x80);
5494 size_t erase_len = td->cursor_pos - erase_start;
5495 memmove(&td->text[erase_start], &td->text[td->cursor_pos], td->text_len - td->cursor_pos + 1);
5496 td->cursor_pos = erase_start;
5497 td->text_len -= erase_len;
5499 if (td->on_change) td->on_change(wgt->id, td->text, td->user_data);
5500 }
5501 return 1;
5502 }
5503 if (ev->keyboard.keycode == ALLEGRO_KEY_DELETE) {
5504 if (_textarea_has_selection(td)) {
5506 } else if (td->cursor_pos < td->text_len) {
5507 int clen = _utf8_char_len((unsigned char)td->text[td->cursor_pos]);
5508 if (td->cursor_pos + (size_t)clen > td->text_len) clen = (int)(td->text_len - td->cursor_pos);
5509 memmove(&td->text[td->cursor_pos], &td->text[td->cursor_pos + (size_t)clen], td->text_len - td->cursor_pos - (size_t)clen + 1);
5510 td->text_len -= (size_t)clen;
5512 if (td->on_change) td->on_change(wgt->id, td->text, td->user_data);
5513 }
5514 return 1;
5515 }
5516 if (ev->keyboard.keycode == ALLEGRO_KEY_LEFT) {
5517 if (!shift && _textarea_has_selection(td)) {
5518 /* collapse selection to left edge */
5519 size_t lo, hi;
5520 _textarea_sel_range(td, &lo, &hi);
5521 td->cursor_pos = lo;
5523 } else if (td->cursor_pos > 0) {
5524 if (shift && !_textarea_has_selection(td)) {
5525 td->sel_start = td->cursor_pos;
5526 }
5527 int skip = 0;
5528 do {
5529 td->cursor_pos--;
5530 } while (skip++ < 3 && td->cursor_pos > 0 && ((unsigned char)td->text[td->cursor_pos] & 0xC0) == 0x80);
5531 if (shift)
5532 td->sel_end = td->cursor_pos;
5533 else
5535 }
5536 return 1;
5537 }
5538 if (ev->keyboard.keycode == ALLEGRO_KEY_RIGHT) {
5539 if (!shift && _textarea_has_selection(td)) {
5540 /* collapse selection to right edge */
5541 size_t lo, hi;
5542 _textarea_sel_range(td, &lo, &hi);
5543 td->cursor_pos = hi;
5545 } else if (td->cursor_pos < td->text_len) {
5546 if (shift && !_textarea_has_selection(td)) {
5547 td->sel_start = td->cursor_pos;
5548 }
5549 int clen = _utf8_char_len((unsigned char)td->text[td->cursor_pos]);
5550 td->cursor_pos += (size_t)clen;
5551 if (td->cursor_pos > td->text_len) td->cursor_pos = td->text_len;
5552 if (shift)
5553 td->sel_end = td->cursor_pos;
5554 else
5556 }
5557 return 1;
5558 }
5559 if (ev->keyboard.keycode == ALLEGRO_KEY_HOME) {
5560 if (shift && !_textarea_has_selection(td)) {
5561 td->sel_start = td->cursor_pos;
5562 }
5563 td->cursor_pos = 0;
5564 if (shift)
5565 td->sel_end = td->cursor_pos;
5566 else
5568 return 1;
5569 }
5570 if (ev->keyboard.keycode == ALLEGRO_KEY_END) {
5571 if (shift && !_textarea_has_selection(td)) {
5572 td->sel_start = td->cursor_pos;
5573 }
5574 td->cursor_pos = td->text_len;
5575 if (shift)
5576 td->sel_end = td->cursor_pos;
5577 else
5579 return 1;
5580 }
5581
5582 /* UP/DOWN arrow keys: move cursor to previous/next visual line in multiline mode */
5583 if (ev->keyboard.keycode == ALLEGRO_KEY_UP || ev->keyboard.keycode == ALLEGRO_KEY_DOWN) {
5584 if (!td->multiline || !font) return 1;
5585 if (shift && !_textarea_has_selection(td)) {
5586 td->sel_start = td->cursor_pos;
5587 }
5588
5589 /* compute wrap width consistently with rendering: subtract scrollbar
5590 * width when content overflows, then subtract padding. */
5591 float base_wrap_w = wgt->w - pad * 2;
5592 float text_area_h = wgt->h - pad * 2;
5593 float content_h = _textarea_content_height(td, font, wgt->w, pad);
5594 float wrap_w = (content_h > text_area_h)
5595 ? (wgt->w - sb_size - pad * 2)
5596 : base_wrap_w;
5597
5598 /* pass 1: find the cursor's visual line number and x offset */
5599 float cx = 0;
5600 int line = 0;
5601 int cursor_line = 0;
5602 float cursor_x = 0;
5603
5604 for (size_t i = 0; i < td->text_len;) {
5605 if (i == td->cursor_pos) {
5606 cursor_line = line;
5607 cursor_x = cx;
5608 }
5609 if (td->text[i] == '\n') {
5610 cx = 0;
5611 line++;
5612 i++;
5613 continue;
5614 }
5615 int clen = _utf8_char_len((unsigned char)td->text[i]);
5616 if (i + (size_t)clen > td->text_len) clen = (int)(td->text_len - i);
5617 char ch[5];
5618 memcpy(ch, &td->text[i], (size_t)clen);
5619 ch[clen] = '\0';
5620 float cw = _text_w(font, ch);
5621 if (cx + cw > wrap_w) {
5622 cx = 0;
5623 line++;
5624 if (i == td->cursor_pos) {
5625 cursor_line = line;
5626 cursor_x = cx;
5627 }
5628 }
5629 cx += cw;
5630 i += (size_t)clen;
5631 }
5632 if (td->cursor_pos >= td->text_len) {
5633 cursor_line = line;
5634 cursor_x = cx;
5635 }
5636 int total_lines = line;
5637
5638 /* determine target line */
5639 int target_line;
5640 if (ev->keyboard.keycode == ALLEGRO_KEY_UP) {
5641 if (cursor_line <= 0) {
5642 td->cursor_pos = 0;
5643 return 1;
5644 }
5645 target_line = cursor_line - 1;
5646 } else {
5647 if (cursor_line >= total_lines) {
5648 td->cursor_pos = td->text_len;
5649 return 1;
5650 }
5651 target_line = cursor_line + 1;
5652 }
5653
5654 /* pass 2: find the byte position on target_line closest to cursor_x */
5655 cx = 0;
5656 line = 0;
5657 size_t best_pos = 0;
5658 float best_dist = 1e9f;
5659
5660 /* check position 0 */
5661 if (line == target_line) {
5662 best_dist = (cursor_x >= 0) ? cursor_x : -cursor_x;
5663 best_pos = 0;
5664 }
5665
5666 for (size_t i = 0; i < td->text_len;) {
5667 if (td->text[i] == '\n') {
5668 if (line >= target_line) break;
5669 cx = 0;
5670 line++;
5671 if (line == target_line) {
5672 float dist = (cursor_x >= cx) ? (cursor_x - cx) : (cx - cursor_x);
5673 if (dist < best_dist) {
5674 best_dist = dist;
5675 best_pos = i + 1;
5676 }
5677 }
5678 i++;
5679 continue;
5680 }
5681 int clen = _utf8_char_len((unsigned char)td->text[i]);
5682 if (i + (size_t)clen > td->text_len) clen = (int)(td->text_len - i);
5683 char ch[5];
5684 memcpy(ch, &td->text[i], (size_t)clen);
5685 ch[clen] = '\0';
5686 float cw = _text_w(font, ch);
5687 if (cx + cw > wrap_w) {
5688 if (line >= target_line) break;
5689 cx = 0;
5690 line++;
5691 if (line == target_line) {
5692 float dist = (cursor_x >= cx) ? (cursor_x - cx) : (cx - cursor_x);
5693 if (dist < best_dist) {
5694 best_dist = dist;
5695 best_pos = i;
5696 }
5697 }
5698 }
5699 cx += cw;
5700 /* check position after this character */
5701 if (line == target_line) {
5702 float dist = (cursor_x >= cx) ? (cursor_x - cx) : (cx - cursor_x);
5703 if (dist < best_dist) {
5704 best_dist = dist;
5705 best_pos = i + (size_t)clen;
5706 if (best_pos > td->text_len) best_pos = td->text_len;
5707 }
5708 }
5709 i += (size_t)clen;
5710 }
5711
5712 td->cursor_pos = best_pos;
5713 if (shift)
5714 td->sel_end = td->cursor_pos;
5715 else
5717 return 1;
5718 }
5719
5720 if (ev->keyboard.keycode == ALLEGRO_KEY_ENTER) {
5721 if (td->multiline) {
5722 if (_textarea_has_selection(td)) {
5724 }
5725 if (td->text_len < td->char_limit) {
5726 memmove(&td->text[td->cursor_pos + 1], &td->text[td->cursor_pos], td->text_len - td->cursor_pos + 1);
5727 td->text[td->cursor_pos] = '\n';
5728 td->cursor_pos++;
5729 td->text_len++;
5731 if (td->on_change) td->on_change(wgt->id, td->text, td->user_data);
5732 }
5733 }
5734 /* in single-line mode, let ENTER pass through to button keybinds */
5735 return td->multiline ? 1 : 0;
5736 }
5737
5738 /* printable character: encode Unicode code point as UTF-8 */
5739 if (ev->keyboard.unichar >= 32) {
5740 /* delete selection first */
5741 if (_textarea_has_selection(td)) {
5743 }
5744 char utf8[4];
5745 int utf8_len = _utf8_encode(ev->keyboard.unichar, utf8);
5746 if (utf8_len > 0 && td->text_len + (size_t)utf8_len <= td->char_limit) {
5747 memmove(&td->text[td->cursor_pos + (size_t)utf8_len], &td->text[td->cursor_pos], td->text_len - td->cursor_pos + 1);
5748 memcpy(&td->text[td->cursor_pos], utf8, (size_t)utf8_len);
5749 td->cursor_pos += (size_t)utf8_len;
5750 td->text_len += (size_t)utf8_len;
5752 if (td->on_change) td->on_change(wgt->id, td->text, td->user_data);
5753 }
5754 }
5755 return 1;
5756}
5757
5767int n_gui_process_event(N_GUI_CTX* ctx, ALLEGRO_EVENT event) {
5768 __n_assert(ctx, return 0);
5769
5770 int event_consumed = 0;
5771
5772 /* When the display loses focus (e.g. OS window is moved to another monitor via the
5773 * title bar, or another window steals focus), examples typically call
5774 * al_flush_event_queue() which discards any pending MOUSE_BUTTON_UP events.
5775 * Without resetting our state here, the GUI would keep its drag/resize flags set
5776 * and continue moving/resizing windows on the next MOUSE_AXES event, which is the
5777 * root cause of GUI windows being unintentionally resized when the OS window is
5778 * moved between monitors. */
5779 if (event.type == ALLEGRO_EVENT_DISPLAY_SWITCH_OUT) {
5780 ctx->mouse_b1 = 0;
5781 ctx->mouse_b1_prev = 0;
5782 list_foreach(wnode, ctx->windows) {
5783 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
5785 }
5786 ctx->global_vscroll_drag = 0;
5787 ctx->global_hscroll_drag = 0;
5788 return 0;
5789 }
5790
5791 /* track mouse */
5792 if (event.type == ALLEGRO_EVENT_MOUSE_AXES) {
5793 ctx->mouse_x = event.mouse.x;
5794 ctx->mouse_y = event.mouse.y;
5795 }
5796 if (event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN && event.mouse.button == 1) {
5797 ctx->mouse_b1_prev = ctx->mouse_b1;
5798 ctx->mouse_b1 = 1;
5799 ctx->mouse_x = event.mouse.x;
5800 ctx->mouse_y = event.mouse.y;
5801 }
5802 if (event.type == ALLEGRO_EVENT_MOUSE_BUTTON_UP && event.mouse.button == 1) {
5803 ctx->mouse_b1_prev = ctx->mouse_b1;
5804 ctx->mouse_b1 = 0;
5805 ctx->scrollbar_drag_widget_id = -1;
5806 }
5807
5808 /* Unified widget scrollbar drag: update scroll position while mouse held */
5809 if (ctx->scrollbar_drag_widget_id >= 0 && ctx->mouse_b1 &&
5810 (event.type == ALLEGRO_EVENT_MOUSE_AXES || event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN)) {
5812 if (drag_wgt && drag_wgt->data) {
5813 float d_ox = 0, d_oy = 0;
5814 const N_GUI_WINDOW* d_win = _find_widget_window(ctx, drag_wgt->id, &d_ox, &d_oy);
5815 if (d_win) {
5816 ALLEGRO_FONT* d_font = drag_wgt->font ? drag_wgt->font : ctx->default_font;
5817 float d_fh = d_font ? (float)al_get_font_line_height(d_font) : 16.0f;
5818 float d_my = (float)ctx->mouse_y;
5819 switch (drag_wgt->type) {
5820 case N_GUI_TYPE_COMBOBOX: {
5821 N_GUI_COMBOBOX_DATA* cbd = (N_GUI_COMBOBOX_DATA*)drag_wgt->data;
5822 if (cbd->is_open && (int)cbd->nb_items > cbd->max_visible) {
5823 float ih = cbd->item_height > d_fh ? cbd->item_height : d_fh + ctx->style.item_height_pad;
5824 float d_ay = d_oy + drag_wgt->y + drag_wgt->h;
5825 float dd_h = (float)cbd->max_visible * ih;
5826 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);
5827 }
5828 break;
5829 }
5830 case N_GUI_TYPE_DROPMENU: {
5831 N_GUI_DROPMENU_DATA* dmd = (N_GUI_DROPMENU_DATA*)drag_wgt->data;
5832 if (dmd->is_open && (int)dmd->nb_entries > dmd->max_visible) {
5833 float ih = dmd->item_height > d_fh ? dmd->item_height : d_fh + ctx->style.item_height_pad;
5834 float d_ay = d_oy + drag_wgt->y + drag_wgt->h;
5835 float dd_h = (float)dmd->max_visible * ih;
5836 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);
5837 }
5838 break;
5839 }
5840 case N_GUI_TYPE_LISTBOX: {
5841 N_GUI_LISTBOX_DATA* lbd = (N_GUI_LISTBOX_DATA*)drag_wgt->data;
5842 float ih = lbd->item_height > d_fh ? lbd->item_height : d_fh + ctx->style.item_height_pad;
5843 float d_ay = d_oy + drag_wgt->y;
5844 int visible = (int)(drag_wgt->h / ih);
5845 lbd->scroll_offset = _scrollbar_calc_scroll_int(d_my, d_ay, drag_wgt->h, visible, (int)lbd->nb_items, ctx->style.scrollbar_thumb_min);
5846 break;
5847 }
5848 case N_GUI_TYPE_RADIOLIST: {
5850 float ih = rld->item_height > d_fh ? rld->item_height : d_fh + ctx->style.item_height_pad;
5851 float d_ay = d_oy + drag_wgt->y;
5852 int visible = (int)(drag_wgt->h / ih);
5853 rld->scroll_offset = _scrollbar_calc_scroll_int(d_my, d_ay, drag_wgt->h, visible, (int)rld->nb_items, ctx->style.scrollbar_thumb_min);
5854 break;
5855 }
5856 case N_GUI_TYPE_TEXTAREA: {
5857 N_GUI_TEXTAREA_DATA* tatd = (N_GUI_TEXTAREA_DATA*)drag_wgt->data;
5858 if (tatd->multiline) {
5859 float ta_pad = ctx->style.textarea_padding;
5860 float ta_view_h = drag_wgt->h - ta_pad * 2;
5861 float ta_sb_w = ctx->style.scrollbar_size;
5862 float ta_text_w = drag_wgt->w - ta_sb_w;
5863 float ta_content_h = _textarea_content_height(tatd, d_font, ta_text_w, ta_pad);
5864 float d_ay = d_oy + drag_wgt->y + ta_pad;
5865 tatd->scroll_y = (int)_scrollbar_calc_scroll(
5866 d_my, d_ay, ta_view_h,
5867 ta_view_h, ta_content_h,
5869 }
5870 break;
5871 }
5872 default:
5873 break;
5874 }
5875 return 1;
5876 }
5877 }
5878 }
5879
5880 /* screen-space mouse (for global scrollbar interaction) */
5881 float screen_mx = (float)ctx->mouse_x;
5882 float screen_my = (float)ctx->mouse_y;
5883 /* virtual-space mouse: reverse the virtual canvas transform first */
5884 float virtual_mx = screen_mx;
5885 float virtual_my = screen_my;
5886 if (ctx->virtual_w > 0 && ctx->virtual_h > 0 && ctx->gui_scale > 0) {
5887 virtual_mx = (screen_mx - ctx->gui_offset_x) / ctx->gui_scale;
5888 virtual_my = (screen_my - ctx->gui_offset_y) / ctx->gui_scale;
5889 }
5890 /* GUI-space mouse (offset by global scroll) */
5891 float mx = virtual_mx + ctx->global_scroll_x;
5892 float my = virtual_my + ctx->global_scroll_y;
5893 int just_pressed = (ctx->mouse_b1 == 1 && ctx->mouse_b1_prev == 0 &&
5894 event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN)
5895 ? 1
5896 : 0;
5897 int just_released = (ctx->mouse_b1 == 0 && ctx->mouse_b1_prev == 1 &&
5898 event.type == ALLEGRO_EVENT_MOUSE_BUTTON_UP)
5899 ? 1
5900 : 0;
5901
5902 /* effective dimensions: gate on both dimensions for virtual canvas */
5903 int virtual_active = (ctx->virtual_w > 0 && ctx->virtual_h > 0);
5904 float eff_w = virtual_active ? ctx->virtual_w : ctx->display_w;
5905 float eff_h = virtual_active ? ctx->virtual_h : ctx->display_h;
5906
5907 /* ---- handle global scrollbar drag ---- */
5908 if (ctx->global_vscroll_drag || ctx->global_hscroll_drag) {
5909 if (ctx->mouse_b1 && (event.type == ALLEGRO_EVENT_MOUSE_AXES || event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN)) {
5910 float scrollbar_size = ctx->style.global_scrollbar_size;
5912 if (ctx->global_vscroll_drag && eff_h > 0) {
5913 int need_hscroll = (ctx->gui_bounds_w > eff_w) ? 1 : 0;
5914 float sb_h = eff_h - (need_hscroll ? scrollbar_size : 0);
5915 float view_h = sb_h;
5916 float ratio = view_h / ctx->gui_bounds_h;
5917 if (ratio > 1.0f) ratio = 1.0f;
5918 float thumb_h = ratio * sb_h;
5919 if (thumb_h < ctx->style.global_scrollbar_thumb_min) thumb_h = ctx->style.global_scrollbar_thumb_min;
5920 float max_scroll = ctx->gui_bounds_h - view_h;
5921 float track_range = sb_h - thumb_h;
5922 if (track_range > 0 && max_scroll > 0) {
5923 float pos_ratio = (virtual_my - thumb_h / 2.0f) / track_range;
5924 if (pos_ratio < 0) pos_ratio = 0;
5925 if (pos_ratio > 1) pos_ratio = 1;
5926 ctx->global_scroll_y = pos_ratio * max_scroll;
5927 }
5928 }
5929 if (ctx->global_hscroll_drag && eff_w > 0) {
5930 int need_vscroll = (ctx->gui_bounds_h > eff_h) ? 1 : 0;
5931 float sb_w = eff_w - (need_vscroll ? scrollbar_size : 0);
5932 float view_w = sb_w;
5933 float ratio = view_w / ctx->gui_bounds_w;
5934 if (ratio > 1.0f) ratio = 1.0f;
5935 float thumb_w = ratio * sb_w;
5936 if (thumb_w < ctx->style.global_scrollbar_thumb_min) thumb_w = ctx->style.global_scrollbar_thumb_min;
5937 float max_scroll = ctx->gui_bounds_w - view_w;
5938 float track_range = sb_w - thumb_w;
5939 if (track_range > 0 && max_scroll > 0) {
5940 float pos_ratio = (virtual_mx - thumb_w / 2.0f) / track_range;
5941 if (pos_ratio < 0) pos_ratio = 0;
5942 if (pos_ratio > 1) pos_ratio = 1;
5943 ctx->global_scroll_x = pos_ratio * max_scroll;
5944 }
5945 }
5946 return 1;
5947 }
5948 if (just_released) {
5949 ctx->global_vscroll_drag = 0;
5950 ctx->global_hscroll_drag = 0;
5951 }
5952 return 1;
5953 }
5954
5955 /* ---- check global scrollbar click ---- */
5956 if (just_pressed && eff_w > 0 && eff_h > 0) {
5957 float scrollbar_size = ctx->style.global_scrollbar_size;
5959 int need_vscroll = (ctx->gui_bounds_h > eff_h) ? 1 : 0;
5960 int need_hscroll = (ctx->gui_bounds_w > eff_w) ? 1 : 0;
5961 if (need_vscroll && ctx->gui_bounds_w > (eff_w - scrollbar_size)) need_hscroll = 1;
5962 if (need_hscroll && ctx->gui_bounds_h > (eff_h - scrollbar_size)) need_vscroll = 1;
5963
5964 if (need_vscroll) {
5965 float sb_x = eff_w - scrollbar_size;
5966 float sb_h = eff_h - (need_hscroll ? scrollbar_size : 0);
5967 if (_point_in_rect(virtual_mx, virtual_my, sb_x, 0, scrollbar_size, sb_h)) {
5968 ctx->global_vscroll_drag = 1;
5969 return 1;
5970 }
5971 }
5972 if (need_hscroll) {
5973 float sb_y = eff_h - scrollbar_size;
5974 float sb_w = eff_w - (need_vscroll ? scrollbar_size : 0);
5975 if (_point_in_rect(virtual_mx, virtual_my, 0, sb_y, sb_w, scrollbar_size)) {
5976 ctx->global_hscroll_drag = 1;
5977 return 1;
5978 }
5979 }
5980 }
5981
5982 /* ---- handle open combobox dropdown first (it overlays everything) ---- */
5983 if (just_pressed && ctx->open_combobox_id >= 0) {
5984 N_GUI_WIDGET* cb_wgt = n_gui_get_widget(ctx, ctx->open_combobox_id);
5985 if (cb_wgt && cb_wgt->data) {
5987
5988 /* find absolute position of the combobox (account for window scroll) */
5989 float cb_ox = 0, cb_oy = 0;
5990 _find_widget_window(ctx, cb_wgt->id, &cb_ox, &cb_oy);
5991
5992 ALLEGRO_FONT* cb_font = cb_wgt->font ? cb_wgt->font : ctx->default_font;
5993 float cb_fh = cb_font ? (float)al_get_font_line_height(cb_font) : 16.0f;
5994 float cb_ih = cbd->item_height > cb_fh ? cbd->item_height : cb_fh + ctx->style.item_height_pad;
5995 float cb_ax = cb_ox + cb_wgt->x;
5996 float cb_ay = cb_oy + cb_wgt->y + cb_wgt->h;
5997 float cb_pad = ctx->style.item_text_padding;
5998 int cb_vis = (int)cbd->nb_items;
5999 if (cb_vis > cbd->max_visible) cb_vis = cbd->max_visible;
6000 float cb_dd_h = (float)cb_vis * cb_ih;
6001
6002 /* compute effective dropdown width (mirrors _draw_combobox_dropdown) */
6003 float cb_dd_w = cb_wgt->w;
6004 if ((cbd->flags & N_GUI_COMBOBOX_AUTO_WIDTH) && cb_font) {
6005 float cb_max_tw = 0;
6006 for (size_t ci = 0; ci < cbd->nb_items; ci++) {
6007 float tw = _text_w(cb_font, cbd->items[ci].text);
6008 if (tw > cb_max_tw) cb_max_tw = tw;
6009 }
6010 float cb_needed = cb_max_tw + cb_pad * 2;
6011 if (cb_needed > cb_dd_w) cb_dd_w = cb_needed;
6012 float cb_cap = ctx->style.combobox_max_dropdown_width;
6013 if (cb_cap <= 0) cb_cap = ctx->display_w > 0 ? (float)ctx->display_w : 4096.0f;
6014 if (cb_dd_w > cb_cap) cb_dd_w = cb_cap;
6015 if (ctx->display_w > 0 && cb_ax + cb_dd_w > (float)ctx->display_w) {
6016 cb_dd_w = (float)ctx->display_w - cb_ax;
6017 if (cb_dd_w < cb_wgt->w) cb_dd_w = cb_wgt->w;
6018 }
6019 }
6020
6021 if (_point_in_rect(mx, my, cb_ax, cb_ay, cb_dd_w, cb_dd_h)) {
6022 /* Check if click is on the scrollbar area (right edge) */
6023 int cb_need_sb = ((int)cbd->nb_items > cbd->max_visible);
6024 float cb_sb_size = ctx->style.scrollbar_size;
6025 if (cb_sb_size < 10.0f) cb_sb_size = 10.0f;
6026 float cb_item_w = cb_need_sb ? cb_dd_w - cb_sb_size : cb_dd_w;
6027
6028 if (cb_need_sb && mx >= cb_ax + cb_item_w) {
6029 /* Clicked on scrollbar — scroll to position, start drag */
6030 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);
6031 ctx->scrollbar_drag_widget_id = cb_wgt->id;
6032 return 1; /* consumed, keep dropdown open */
6033 }
6034
6035 /* Clicked on item area — select item */
6036 int clicked_idx = cbd->scroll_offset + (int)((my - cb_ay) / cb_ih);
6037 if (clicked_idx >= 0 && (size_t)clicked_idx < cbd->nb_items) {
6038 cbd->selected_index = clicked_idx;
6039 if (cbd->on_select) cbd->on_select(cb_wgt->id, clicked_idx, cbd->user_data);
6040 }
6041 cbd->is_open = 0;
6042 ctx->open_combobox_id = -1;
6043 return 1; /* event consumed by dropdown */
6044 } else {
6045 /* clicked outside dropdown - close it */
6046 cbd->is_open = 0;
6047 ctx->open_combobox_id = -1;
6048 /* fall through to normal processing */
6049 }
6050 } else {
6051 ctx->open_combobox_id = -1;
6052 }
6053 }
6054
6055 /* ---- handle open dropdown menu panel first ---- */
6056 if (just_pressed && ctx->open_dropmenu_id >= 0) {
6057 N_GUI_WIDGET* dm_wgt = n_gui_get_widget(ctx, ctx->open_dropmenu_id);
6058 if (dm_wgt && dm_wgt->data) {
6060
6061 /* find absolute position of the dropmenu widget (account for window scroll) */
6062 float dm_ox = 0, dm_oy = 0;
6063 _find_widget_window(ctx, dm_wgt->id, &dm_ox, &dm_oy);
6064
6065 ALLEGRO_FONT* dm_font = dm_wgt->font ? dm_wgt->font : ctx->default_font;
6066 float dm_fh = dm_font ? (float)al_get_font_line_height(dm_font) : 16.0f;
6067 float dm_ih = dmd->item_height > dm_fh ? dmd->item_height : dm_fh + ctx->style.item_height_pad;
6068 float dm_ax = dm_ox + dm_wgt->x;
6069 float dm_ay = dm_oy + dm_wgt->y + dm_wgt->h;
6070 int dm_vis = (int)dmd->nb_entries;
6071 if (dm_vis > dmd->max_visible) dm_vis = dmd->max_visible;
6072 float dm_panel_h = (float)dm_vis * dm_ih;
6073
6074 if (_point_in_rect(mx, my, dm_ax, dm_ay, dm_wgt->w, dm_panel_h)) {
6075 /* check if click is on the scrollbar area (right edge) */
6076 int dm_need_sb = ((int)dmd->nb_entries > dmd->max_visible);
6077 float dm_sb_size = ctx->style.scrollbar_size;
6078 if (dm_sb_size < 10.0f) dm_sb_size = 10.0f;
6079 float dm_item_w = dm_need_sb ? dm_wgt->w - dm_sb_size : dm_wgt->w;
6080
6081 if (dm_need_sb && mx >= dm_ax + dm_item_w) {
6082 /* Clicked on scrollbar — scroll to position, start drag */
6083 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);
6084 ctx->scrollbar_drag_widget_id = dm_wgt->id;
6085 return 1; /* consumed, keep panel open */
6086 }
6087
6088 /* clicked on item area */
6089 int clicked_idx = dmd->scroll_offset + (int)((my - dm_ay) / dm_ih);
6090 if (clicked_idx >= 0 && (size_t)clicked_idx < dmd->nb_entries) {
6091 N_GUI_DROPMENU_ENTRY* entry = &dmd->entries[clicked_idx];
6092 if (entry->on_click) {
6093 entry->on_click(dm_wgt->id, clicked_idx, entry->tag, entry->user_data);
6094 }
6095 }
6096 dmd->is_open = 0;
6097 ctx->open_dropmenu_id = -1;
6098 return 1; /* event consumed by dropdown menu */
6099 } else {
6100 /* clicked outside panel - close the menu */
6101 dmd->is_open = 0;
6102 ctx->open_dropmenu_id = -1;
6103 /* if click landed on the button itself, consume event so widget handler doesn't re-open */
6104 float dm_btn_ax = dm_ox + dm_wgt->x;
6105 float dm_btn_ay = dm_oy + dm_wgt->y;
6106 if (_point_in_rect(mx, my, dm_btn_ax, dm_btn_ay, dm_wgt->w, dm_wgt->h)) {
6107 return 1;
6108 }
6109 /* fall through to normal processing */
6110 }
6111 } else {
6112 ctx->open_dropmenu_id = -1;
6113 }
6114 }
6115
6116 /* iterate windows back-to-front, but for hit testing we want front-to-back
6117 (last in list = front), so we walk backward */
6118 if (event.type == ALLEGRO_EVENT_MOUSE_AXES ||
6119 event.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN ||
6120 event.type == ALLEGRO_EVENT_MOUSE_BUTTON_UP) {
6121 /* reset hover for all widgets */
6122 list_foreach(wnode, ctx->windows) {
6123 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
6124 if (!(win->state & N_GUI_WIN_OPEN)) continue;
6125 list_foreach(wgn, win->widgets) {
6126 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
6127 if (wgt) wgt->state &= ~N_GUI_STATE_HOVER;
6128 }
6129 }
6130
6131 /* check if any window has an active drag/resize/scroll (mouse capture) */
6132 LIST_NODE* captured_wnode = NULL;
6133 for (LIST_NODE* wnode = ctx->windows->end; wnode; wnode = wnode->prev) {
6134 const N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
6135 if (!(win->state & N_GUI_WIN_OPEN)) continue;
6137 captured_wnode = wnode;
6138 break;
6139 }
6140 }
6141
6142 /* find topmost window hit (iterate from end), skip if captured */
6143 LIST_NODE* hit_wnode = captured_wnode;
6144 if (!hit_wnode) {
6145 for (LIST_NODE* wnode = ctx->windows->end; wnode; wnode = wnode->prev) {
6146 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
6147 if (!(win->state & N_GUI_WIN_OPEN)) continue;
6148
6149 float win_h_check = (win->state & N_GUI_WIN_MINIMISED) ? _win_tbh(win) : win->h;
6150 if (_point_in_rect(mx, my, win->x, win->y, win->w, win_h_check)) {
6151 hit_wnode = wnode;
6152 break;
6153 }
6154 }
6155 }
6156
6157 if (hit_wnode) {
6158 event_consumed = 1;
6159 N_GUI_WINDOW* win = (N_GUI_WINDOW*)hit_wnode->ptr;
6160
6161 /* only process new clicks/interactions when not in a captured operation */
6162 if (!captured_wnode) {
6163 /* bring to front on click */
6164 if (just_pressed) {
6165 n_gui_raise_window(ctx, win->id);
6166 /* re-fetch since node may have moved */
6167 win = n_gui_get_window(ctx, win->id);
6168 if (!win) return 1;
6169
6170 /* clear focus from previously focused widget on a new primary click
6171 * inside a window; textarea clicks will re-set focus below */
6172 if (ctx->focused_widget_id >= 0) {
6173 N_GUI_WIDGET* prev_fw = n_gui_get_widget(ctx, ctx->focused_widget_id);
6174 if (prev_fw) prev_fw->state &= ~N_GUI_STATE_FOCUSED;
6175 ctx->focused_widget_id = -1;
6176 }
6177 }
6178
6179 /* check auto-scrollbar clicks before resize handle */
6180 int auto_scrollbar_hit = 0;
6181 if ((win->flags & N_GUI_WIN_AUTO_SCROLLBAR) && !(win->state & N_GUI_WIN_MINIMISED) && just_pressed) {
6182 float scrollbar_size = ctx->style.scrollbar_size;
6183 float body_h = win->h - _win_tbh(win);
6184 float body_y = win->y + _win_tbh(win);
6185 int need_vscroll = 0, need_hscroll = 0;
6187 if (win->content_h > body_h) need_vscroll = 1;
6188 if (win->content_w > win->w) need_hscroll = 1;
6189 if (need_vscroll && win->content_w > (win->w - scrollbar_size)) need_hscroll = 1;
6190 if (need_hscroll && win->content_h > (body_h - scrollbar_size)) need_vscroll = 1;
6191 float content_area_w = win->w - (need_vscroll ? scrollbar_size : 0);
6192 float content_area_h = body_h - (need_hscroll ? scrollbar_size : 0);
6193
6194 /* vertical auto-scrollbar track */
6195 if (need_vscroll) {
6196 float sb_x = win->x + win->w - scrollbar_size;
6197 float sb_y = body_y;
6198 float sb_h = content_area_h;
6199 /* leave room for resize corner when no horizontal scrollbar */
6200 if ((win->flags & N_GUI_WIN_RESIZABLE) && !need_hscroll) sb_h -= scrollbar_size;
6201 if (_point_in_rect(mx, my, sb_x, sb_y, scrollbar_size, sb_h)) {
6203 auto_scrollbar_hit = 1;
6204 win->scroll_y = _scrollbar_calc_scroll(my, sb_y, sb_h, content_area_h, win->content_h, ctx->style.scrollbar_thumb_min);
6205 }
6206 }
6207 /* horizontal auto-scrollbar track */
6208 if (need_hscroll && !auto_scrollbar_hit) {
6209 float sb_x = win->x;
6210 float sb_y = win->y + win->h - scrollbar_size;
6211 float sb_w = content_area_w;
6212 /* leave room for resize corner when no vertical scrollbar */
6213 if ((win->flags & N_GUI_WIN_RESIZABLE) && !need_vscroll) sb_w -= scrollbar_size;
6214 if (_point_in_rect(mx, my, sb_x, sb_y, sb_w, scrollbar_size)) {
6216 auto_scrollbar_hit = 1;
6217 win->scroll_x = _scrollbar_calc_scroll(mx, sb_x, sb_w, content_area_w, win->content_w, ctx->style.scrollbar_thumb_min);
6218 }
6219 }
6220 }
6221
6222 /* check resize handle (bottom-right corner, 14x14 px) - skip if auto-scrollbar was hit */
6223 if (!auto_scrollbar_hit && (win->flags & N_GUI_WIN_RESIZABLE) && !(win->state & N_GUI_WIN_MINIMISED)) {
6224 float grip = ctx->style.grip_size + 2.0f;
6225 float rx = win->x + win->w - grip;
6226 float ry = win->y + win->h - grip;
6227 /* when auto-scrollbars are visible, restrict resize to the corner square only */
6228 int in_resize_area = 0;
6229 if (win->flags & N_GUI_WIN_AUTO_SCROLLBAR) {
6230 float scrollbar_size = ctx->style.scrollbar_size;
6231 float body_h = win->h - _win_tbh(win);
6232 int need_vscroll = 0, need_hscroll = 0;
6234 if (win->content_h > body_h) need_vscroll = 1;
6235 if (win->content_w > win->w) need_hscroll = 1;
6236 if (need_vscroll && win->content_w > (win->w - scrollbar_size)) need_hscroll = 1;
6237 if (need_hscroll && win->content_h > (body_h - scrollbar_size)) need_vscroll = 1;
6238 if (need_vscroll || need_hscroll) {
6239 /* resize corner is only the small square at bottom-right where scrollbars don't reach */
6240 float corner_x = win->x + win->w - scrollbar_size;
6241 float corner_y = win->y + win->h - scrollbar_size;
6242 in_resize_area = just_pressed && _point_in_rect(mx, my, corner_x, corner_y, scrollbar_size, scrollbar_size);
6243 } else {
6244 in_resize_area = just_pressed && _point_in_rect(mx, my, rx, ry, grip, grip);
6245 }
6246 } else {
6247 in_resize_area = just_pressed && _point_in_rect(mx, my, rx, ry, grip, grip);
6248 }
6249 if (in_resize_area) {
6250 win->state |= N_GUI_WIN_RESIZING;
6251 win->drag_ox = win->w - (mx - win->x);
6252 win->drag_oy = win->h - (my - win->y);
6253 }
6254 }
6255
6256 /* title bar: drag / minimize */
6258 _point_in_rect(mx, my, win->x, win->y, win->w, _win_tbh(win))) {
6259 if (just_pressed && !(win->flags & N_GUI_WIN_FIXED_POSITION)) {
6260 /* double click could minimize - for now, start drag */
6261 win->state |= N_GUI_WIN_DRAGGING;
6262 win->drag_ox = mx - win->x;
6263 win->drag_oy = my - win->y;
6264 }
6266 /* widget area (account for scroll offsets) */
6267 float content_x = win->x - win->scroll_x;
6268 float content_y = win->y + _win_tbh(win) - win->scroll_y;
6269
6270 list_foreach(wgn, win->widgets) {
6271 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
6272 if (!wgt || !wgt->visible || !wgt->enabled) continue;
6273 float ax = content_x + wgt->x;
6274 float ay = content_y + wgt->y;
6275
6276 if (_point_in_rect(mx, my, ax, ay, wgt->w, wgt->h)) {
6277 wgt->state |= N_GUI_STATE_HOVER;
6278
6279 if (just_pressed) {
6280 wgt->state |= N_GUI_STATE_ACTIVE;
6281
6282 /* set focus on any interactive widget type */
6283 if (wgt->type == N_GUI_TYPE_SLIDER ||
6284 wgt->type == N_GUI_TYPE_CHECKBOX ||
6285 wgt->type == N_GUI_TYPE_LISTBOX ||
6286 wgt->type == N_GUI_TYPE_RADIOLIST ||
6287 wgt->type == N_GUI_TYPE_COMBOBOX ||
6288 wgt->type == N_GUI_TYPE_SCROLLBAR ||
6289 wgt->type == N_GUI_TYPE_DROPMENU ||
6290 wgt->type == N_GUI_TYPE_TEXTAREA) {
6291 wgt->state |= N_GUI_STATE_FOCUSED;
6292 ctx->focused_widget_id = wgt->id;
6293 }
6294
6295 if (wgt->type == N_GUI_TYPE_BUTTON) {
6296 /* click callback on release */
6297 }
6298 if (wgt->type == N_GUI_TYPE_CHECKBOX) {
6300 ckd->checked = !ckd->checked;
6301 if (ckd->on_toggle) ckd->on_toggle(wgt->id, ckd->checked, ckd->user_data);
6302 }
6303 if (wgt->type == N_GUI_TYPE_SLIDER) {
6304 _slider_update_from_mouse(wgt, mx, my, content_x, content_y);
6305 }
6306 if (wgt->type == N_GUI_TYPE_SCROLLBAR) {
6307 _scrollbar_update_from_mouse(wgt, mx, my, content_x, content_y, &ctx->style);
6308 }
6309 if (wgt->type == N_GUI_TYPE_TEXTAREA) {
6310 /* clear any label selection when clicking a textarea */
6311 if (ctx->selected_label_id >= 0) {
6313 if (prev && prev->type == N_GUI_TYPE_LABEL && prev->data) {
6314 N_GUI_LABEL_DATA* plb = (N_GUI_LABEL_DATA*)prev->data;
6315 plb->sel_start = -1;
6316 plb->sel_end = -1;
6317 plb->sel_dragging = 0;
6318 }
6319 ctx->selected_label_id = -1;
6320 }
6321 N_GUI_TEXTAREA_DATA* click_td = (N_GUI_TEXTAREA_DATA*)wgt->data;
6322 click_td->cursor_time = al_get_time();
6323 ALLEGRO_FONT* tf = wgt->font ? wgt->font : ctx->default_font;
6324 if (tf) {
6325 /* check if click is on the scrollbar area */
6326 float text_pad = ctx->style.textarea_padding;
6327 if (click_td->multiline) {
6328 float ta_view_h = wgt->h - text_pad * 2;
6329 float ta_content_h = _textarea_content_height(click_td, tf, wgt->w, text_pad);
6330 int ta_need_sb = (ta_content_h > ta_view_h) ? 1 : 0;
6331 float ta_sb_w = ta_need_sb ? ctx->style.scrollbar_size : 0;
6332 if (ta_need_sb && mx > ax + wgt->w - ta_sb_w) {
6333 /* scrollbar track click: jump + start drag */
6334 float ta_text_w = wgt->w - ta_sb_w;
6335 ta_content_h = _textarea_content_height(click_td, tf, ta_text_w, text_pad);
6336 click_td->scroll_y = (int)_scrollbar_calc_scroll(
6337 my, ay + text_pad, ta_view_h,
6338 ta_view_h, ta_content_h,
6340 ctx->scrollbar_drag_widget_id = wgt->id;
6341 goto textarea_click_done;
6342 }
6343 }
6344 size_t pos = _textarea_pos_from_mouse(click_td, tf, mx, my,
6345 ax, ay, wgt->w, wgt->h,
6346 text_pad, ctx->style.scrollbar_size);
6347 click_td->cursor_pos = pos;
6348 click_td->sel_start = pos;
6349 click_td->sel_end = pos;
6350 }
6351 textarea_click_done:;
6352 }
6353 if (wgt->type == N_GUI_TYPE_LABEL) {
6354 N_GUI_LABEL_DATA* click_lb = (N_GUI_LABEL_DATA*)wgt->data;
6355 if (click_lb) {
6356 /* clear previous label selection */
6357 if (ctx->selected_label_id >= 0 && ctx->selected_label_id != wgt->id) {
6359 if (prev && prev->type == N_GUI_TYPE_LABEL && prev->data) {
6360 N_GUI_LABEL_DATA* plb = (N_GUI_LABEL_DATA*)prev->data;
6361 plb->sel_start = -1;
6362 plb->sel_end = -1;
6363 plb->sel_dragging = 0;
6364 }
6365 }
6366 ALLEGRO_FONT* lf = wgt->font ? wgt->font : ctx->default_font;
6367 if (lf) {
6368 int char_pos;
6369 if (click_lb->align == N_GUI_ALIGN_JUSTIFIED) {
6370 float lpad = ctx->style.label_padding;
6371 float effective_w = wgt->w;
6372 float lww = 0;
6374 lww = win->content_w + ctx->style.title_max_w_reserve + lpad;
6375 else if (win->flags & N_GUI_WIN_RESIZABLE)
6376 lww = win->w;
6377 if (lww > 0) {
6378 float mfw = lww - wgt->x;
6379 if (mfw > effective_w) effective_w = mfw;
6380 }
6381 float max_text_w = effective_w - lpad * 2;
6382 float content_h = _label_content_height(click_lb->text, lf, max_text_w);
6383 float view_h = wgt->h - lpad;
6384 float sb_sz = (content_h > view_h) ? ctx->style.scrollbar_size : 0;
6385 float tw2 = max_text_w - sb_sz;
6386 char_pos = _justified_char_at_pos(click_lb->text, lf, tw2,
6387 ax + lpad, ay + lpad / 2.0f, click_lb->scroll_y, mx, my);
6388 } else {
6389 float lww = 0;
6391 lww = win->content_w + ctx->style.title_max_w_reserve + ctx->style.label_padding;
6392 else if (win->flags & N_GUI_WIN_RESIZABLE)
6393 lww = win->w;
6394 float text_ox = _label_text_origin_x(click_lb, lf, ax, wgt->w, lww, wgt->x, ctx->style.label_padding);
6395 float click_x = mx - text_ox;
6396 char_pos = _label_char_at_x(click_lb, lf, click_x);
6397 }
6398 if (char_pos >= 0) {
6399 click_lb->sel_start = char_pos;
6400 click_lb->sel_end = char_pos;
6401 click_lb->sel_dragging = 1;
6402 ctx->selected_label_id = wgt->id;
6403 }
6404 }
6405 }
6406 }
6407 if (wgt->type == N_GUI_TYPE_COMBOBOX) {
6409 cbd->is_open = !cbd->is_open;
6410 if (cbd->is_open) {
6411 ctx->open_combobox_id = wgt->id;
6412 /* auto-scroll to center the selected item in the visible area */
6413 if (cbd->selected_index >= 0 && cbd->nb_items > (size_t)cbd->max_visible) {
6414 int target = cbd->selected_index - cbd->max_visible / 2;
6415 if (target < 0) target = 0;
6416 int max_off = (int)cbd->nb_items - cbd->max_visible;
6417 if (target > max_off) target = max_off;
6418 cbd->scroll_offset = target;
6419 } else {
6420 cbd->scroll_offset = 0;
6421 }
6422 } else {
6423 ctx->open_combobox_id = -1;
6424 }
6425 }
6426 if (wgt->type == N_GUI_TYPE_LISTBOX) {
6428 ALLEGRO_FONT* lf = wgt->font ? wgt->font : ctx->default_font;
6429 float lfh = lf ? (float)al_get_font_line_height(lf) : 16.0f;
6430 float lih = lbd->item_height > lfh ? lbd->item_height : lfh + ctx->style.item_height_pad;
6431 int lb_visible = (int)(wgt->h / lih);
6432 int lb_need_sb = ((int)lbd->nb_items > lb_visible) ? 1 : 0;
6433 float lb_sb_w = lb_need_sb ? ctx->style.scrollbar_size : 0;
6434 /* clamp scroll_offset */
6435 int lb_max_off = (int)lbd->nb_items - lb_visible;
6436 if (lb_max_off < 0) lb_max_off = 0;
6437 if (lbd->scroll_offset > lb_max_off) lbd->scroll_offset = lb_max_off;
6438 if (lbd->scroll_offset < 0) lbd->scroll_offset = 0;
6439 /* skip click if on the scrollbar area */
6440 if (lb_need_sb && mx > ax + wgt->w - lb_sb_w) {
6441 /* scrollbar track click: jump scroll position + start drag */
6442 lbd->scroll_offset = _scrollbar_calc_scroll_int(my, ay, wgt->h, lb_visible, (int)lbd->nb_items, ctx->style.scrollbar_thumb_min);
6443 ctx->scrollbar_drag_widget_id = wgt->id;
6444 } else {
6445 int clicked_idx = lbd->scroll_offset + (int)((my - ay) / lih);
6446 if (clicked_idx >= 0 && (size_t)clicked_idx < lbd->nb_items) {
6447 if (lbd->selection_mode == N_GUI_SELECT_SINGLE) {
6448 for (size_t si = 0; si < lbd->nb_items; si++) lbd->items[si].selected = 0;
6449 lbd->items[clicked_idx].selected = 1;
6450 if (lbd->on_select) lbd->on_select(wgt->id, clicked_idx, 1, lbd->user_data);
6451 } else if (lbd->selection_mode == N_GUI_SELECT_MULTIPLE) {
6452 lbd->items[clicked_idx].selected = !lbd->items[clicked_idx].selected;
6453 if (lbd->on_select) lbd->on_select(wgt->id, clicked_idx, lbd->items[clicked_idx].selected, lbd->user_data);
6454 }
6455 /* N_GUI_SELECT_NONE: no selection change */
6456 }
6457 }
6458 }
6459 if (wgt->type == N_GUI_TYPE_RADIOLIST) {
6461 ALLEGRO_FONT* rf = wgt->font ? wgt->font : ctx->default_font;
6462 float rfh = rf ? (float)al_get_font_line_height(rf) : 16.0f;
6463 float rih = rld->item_height > rfh ? rld->item_height : rfh + ctx->style.item_height_pad;
6464 int rl_visible = (int)(wgt->h / rih);
6465 int rl_need_sb = ((int)rld->nb_items > rl_visible) ? 1 : 0;
6466 float rl_sb_w = rl_need_sb ? ctx->style.scrollbar_size : 0;
6467 /* clamp scroll_offset */
6468 int rl_max_off = (int)rld->nb_items - rl_visible;
6469 if (rl_max_off < 0) rl_max_off = 0;
6470 if (rld->scroll_offset > rl_max_off) rld->scroll_offset = rl_max_off;
6471 if (rld->scroll_offset < 0) rld->scroll_offset = 0;
6472 /* skip click if on the scrollbar area */
6473 if (rl_need_sb && mx > ax + wgt->w - rl_sb_w) {
6474 /* scrollbar track click: jump scroll position + start drag */
6475 rld->scroll_offset = _scrollbar_calc_scroll_int(my, ay, wgt->h, rl_visible, (int)rld->nb_items, ctx->style.scrollbar_thumb_min);
6476 ctx->scrollbar_drag_widget_id = wgt->id;
6477 } else {
6478 int clicked_idx = rld->scroll_offset + (int)((my - ay) / rih);
6479 if (clicked_idx >= 0 && (size_t)clicked_idx < rld->nb_items) {
6480 rld->selected_index = clicked_idx;
6481 if (rld->on_select) rld->on_select(wgt->id, clicked_idx, rld->user_data);
6482 }
6483 }
6484 }
6485 if (wgt->type == N_GUI_TYPE_LABEL) {
6487 if (lbl->link[0] && lbl->on_link_click) {
6488 lbl->on_link_click(wgt->id, lbl->link, lbl->user_data);
6489 }
6490 }
6491 if (wgt->type == N_GUI_TYPE_DROPMENU) {
6493 dmd->is_open = !dmd->is_open;
6494 if (dmd->is_open) {
6495 /* close any other open dropdown/combobox first */
6496 if (ctx->open_combobox_id >= 0) {
6497 N_GUI_WIDGET* old_cb = n_gui_get_widget(ctx, ctx->open_combobox_id);
6498 if (old_cb && old_cb->data) ((N_GUI_COMBOBOX_DATA*)old_cb->data)->is_open = 0;
6499 ctx->open_combobox_id = -1;
6500 }
6501 if (ctx->open_dropmenu_id >= 0 && ctx->open_dropmenu_id != wgt->id) {
6502 N_GUI_WIDGET* old_dm = n_gui_get_widget(ctx, ctx->open_dropmenu_id);
6503 if (old_dm && old_dm->data) ((N_GUI_DROPMENU_DATA*)old_dm->data)->is_open = 0;
6504 }
6505 ctx->open_dropmenu_id = wgt->id;
6506 /* call on_open callback to rebuild dynamic entries */
6507 if (dmd->on_open) {
6508 dmd->on_open(wgt->id, dmd->on_open_user_data);
6509 }
6510 } else {
6511 ctx->open_dropmenu_id = -1;
6512 }
6513 }
6514 }
6515 break; /* only top widget gets the event */
6516 }
6517 }
6518
6519 /* frameless windows: drag from empty body area */
6520 if ((win->flags & N_GUI_WIN_FRAMELESS) && just_pressed &&
6521 !(win->flags & N_GUI_WIN_FIXED_POSITION) &&
6522 !(win->state & N_GUI_WIN_DRAGGING)) {
6523 win->state |= N_GUI_WIN_DRAGGING;
6524 win->drag_ox = mx - win->x;
6525 win->drag_oy = my - win->y;
6526 }
6527 }
6528
6529 } /* end if (!captured_wnode) */
6530
6531 /* window dragging */
6532 if ((win->state & N_GUI_WIN_DRAGGING) && ctx->mouse_b1 && !(win->flags & N_GUI_WIN_FIXED_POSITION)) {
6533 win->x = mx - win->drag_ox;
6534 win->y = my - win->drag_oy;
6535 }
6536 if (just_released && (win->state & N_GUI_WIN_DRAGGING) && !(win->flags & N_GUI_WIN_FIXED_POSITION)) {
6537 win->state &= ~N_GUI_WIN_DRAGGING;
6538 if (ctx->resize_mode == N_GUI_RESIZE_ADAPTIVE) {
6540 }
6541 }
6542
6543 /* window resizing */
6544 if ((win->state & N_GUI_WIN_RESIZING) && ctx->mouse_b1) {
6545 float new_w = (mx - win->x) + win->drag_ox;
6546 float new_h = (my - win->y) + win->drag_oy;
6547 if (new_w < win->min_w) new_w = win->min_w;
6548 if (new_h < win->min_h) new_h = win->min_h;
6549 win->w = new_w;
6550 win->h = new_h;
6551 }
6552 if (just_released && (win->state & N_GUI_WIN_RESIZING)) {
6553 win->state &= ~N_GUI_WIN_RESIZING;
6554 if (ctx->resize_mode == N_GUI_RESIZE_ADAPTIVE) {
6556 }
6557 }
6558
6559 /* auto-scrollbar dragging */
6561 float scrollbar_size = ctx->style.scrollbar_size;
6562 float body_h = win->h - _win_tbh(win);
6563 float body_y = win->y + _win_tbh(win);
6564 int need_vscroll = 0, need_hscroll = 0;
6566 if (win->content_h > body_h) need_vscroll = 1;
6567 if (win->content_w > win->w) need_hscroll = 1;
6568 if (need_vscroll && win->content_w > (win->w - scrollbar_size)) need_hscroll = 1;
6569 if (need_hscroll && win->content_h > (body_h - scrollbar_size)) need_vscroll = 1;
6570 float content_area_w = win->w - (need_vscroll ? scrollbar_size : 0);
6571 float content_area_h = body_h - (need_hscroll ? scrollbar_size : 0);
6572
6573 if (win->state & N_GUI_WIN_VSCROLL_DRAG) {
6574 float sb_y = body_y;
6575 float sb_h = content_area_h;
6576 if ((win->flags & N_GUI_WIN_RESIZABLE) && !need_hscroll) sb_h -= scrollbar_size;
6577 win->scroll_y = _scrollbar_calc_scroll(my, sb_y, sb_h, content_area_h, win->content_h, ctx->style.scrollbar_thumb_min);
6578 }
6579 if (win->state & N_GUI_WIN_HSCROLL_DRAG) {
6580 float sb_x = win->x;
6581 float sb_w = content_area_w;
6582 if ((win->flags & N_GUI_WIN_RESIZABLE) && !need_vscroll) sb_w -= scrollbar_size;
6583 win->scroll_x = _scrollbar_calc_scroll(mx, sb_x, sb_w, content_area_w, win->content_w, ctx->style.scrollbar_thumb_min);
6584 }
6585 }
6586 if (just_released && (win->state & (N_GUI_WIN_VSCROLL_DRAG | N_GUI_WIN_HSCROLL_DRAG))) {
6588 }
6589 } else {
6590 /* clicked outside all windows */
6591 if (just_pressed) {
6592 if (ctx->focused_widget_id != -1) {
6594 if (fw) fw->state &= ~N_GUI_STATE_FOCUSED;
6595 ctx->focused_widget_id = -1;
6596 }
6597 }
6598 }
6599
6600 /* handle active slider/scrollbar drag even outside widget bounds */
6601 if (ctx->mouse_b1 && event.type == ALLEGRO_EVENT_MOUSE_AXES) {
6602 list_foreach(wnode, ctx->windows) {
6603 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
6604 if (!(win->state & N_GUI_WIN_OPEN)) continue;
6605 float content_x = win->x - win->scroll_x;
6606 float content_y = win->y + _win_tbh(win) - win->scroll_y;
6607 list_foreach(wgn, win->widgets) {
6608 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
6609 if (!wgt) continue;
6610 if (wgt->state & N_GUI_STATE_ACTIVE) {
6611 if (wgt->type == N_GUI_TYPE_SLIDER) {
6612 _slider_update_from_mouse(wgt, mx, my, content_x, content_y);
6613 }
6614 if (wgt->type == N_GUI_TYPE_SCROLLBAR) {
6615 _scrollbar_update_from_mouse(wgt, mx, my, content_x, content_y, &ctx->style);
6616 }
6617 if (wgt->type == N_GUI_TYPE_LABEL) {
6618 N_GUI_LABEL_DATA* drag_lb = (N_GUI_LABEL_DATA*)wgt->data;
6619 if (drag_lb && drag_lb->sel_dragging) {
6620 ALLEGRO_FONT* lf = wgt->font ? wgt->font : ctx->default_font;
6621 if (lf) {
6622 float lax = content_x + wgt->x;
6623 float lay = content_y + wgt->y;
6624 int char_pos;
6625 if (drag_lb->align == N_GUI_ALIGN_JUSTIFIED) {
6626 float lpad = ctx->style.label_padding;
6627 float effective_w = wgt->w;
6628 float lww = 0;
6630 lww = win->content_w + ctx->style.title_max_w_reserve + lpad;
6631 else if (win->flags & N_GUI_WIN_RESIZABLE)
6632 lww = win->w;
6633 if (lww > 0) {
6634 float mfw = lww - wgt->x;
6635 if (mfw > effective_w) effective_w = mfw;
6636 }
6637 float max_text_w = effective_w - lpad * 2;
6638 float content_h2 = _label_content_height(drag_lb->text, lf, max_text_w);
6639 float view_h = wgt->h - lpad;
6640 float sb_sz = (content_h2 > view_h) ? ctx->style.scrollbar_size : 0;
6641 float tw2 = max_text_w - sb_sz;
6642 char_pos = _justified_char_at_pos(drag_lb->text, lf, tw2,
6643 lax + lpad, lay + lpad / 2.0f, drag_lb->scroll_y, mx, my);
6644 } else {
6645 float lww = 0;
6647 lww = win->content_w + ctx->style.title_max_w_reserve + ctx->style.label_padding;
6648 else if (win->flags & N_GUI_WIN_RESIZABLE)
6649 lww = win->w;
6650 float text_ox = _label_text_origin_x(drag_lb, lf, lax, wgt->w, lww, wgt->x, ctx->style.label_padding);
6651 float click_x = mx - text_ox;
6652 char_pos = _label_char_at_x(drag_lb, lf, click_x);
6653 }
6654 if (char_pos >= 0) {
6655 drag_lb->sel_end = char_pos;
6656 }
6657 }
6658 }
6659 }
6660 if (wgt->type == N_GUI_TYPE_TEXTAREA) {
6662 ALLEGRO_FONT* tf = wgt->font ? wgt->font : ctx->default_font;
6663 if (tf && drag_td) {
6664 float ta_ax = content_x + wgt->x;
6665 float ta_ay = content_y + wgt->y;
6666 float text_pad = ctx->style.textarea_padding;
6667 size_t pos = _textarea_pos_from_mouse(drag_td, tf, mx, my,
6668 ta_ax, ta_ay, wgt->w, wgt->h,
6669 text_pad, ctx->style.scrollbar_size);
6670 drag_td->cursor_pos = pos;
6671 drag_td->sel_end = pos;
6672 drag_td->cursor_time = al_get_time();
6673 }
6674 }
6675 }
6676 }
6677 }
6678 }
6679
6680 /* release active state on mouse up */
6681 if (just_released) {
6682 list_foreach(wnode, ctx->windows) {
6683 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
6684 list_foreach(wgn, win->widgets) {
6685 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
6686 if (!wgt) continue;
6687 if (wgt->state & N_GUI_STATE_ACTIVE) {
6688 /* fire button callback on release if still hovering */
6689 if (wgt->type == N_GUI_TYPE_BUTTON) {
6690 float content_x = win->x - win->scroll_x;
6691 float content_y = win->y + _win_tbh(win) - win->scroll_y;
6692 float ax = content_x + wgt->x;
6693 float ay = content_y + wgt->y;
6694 if (_point_in_rect(mx, my, ax, ay, wgt->w, wgt->h)) {
6696 /* Toggle mode: flip toggled state on each click */
6697 if (bd->toggle_mode) {
6698 bd->toggled = !bd->toggled;
6699 }
6700 if (bd->on_click) bd->on_click(wgt->id, bd->user_data);
6701 }
6702 }
6703 /* stop label drag selection on release */
6704 if (wgt->type == N_GUI_TYPE_LABEL) {
6705 N_GUI_LABEL_DATA* rel_lb = (N_GUI_LABEL_DATA*)wgt->data;
6706 if (rel_lb) rel_lb->sel_dragging = 0;
6707 }
6708 wgt->state &= ~N_GUI_STATE_ACTIVE;
6709 }
6710 }
6711 }
6712 }
6713 }
6714
6715 /* Tab / Shift+Tab: focus navigation across widgets in the active window.
6716 * Ctrl+Tab in a textarea inserts a literal tab — handled below by the textarea key handler. */
6717 if (!event_consumed && event.type == ALLEGRO_EVENT_KEY_CHAR && event.keyboard.keycode == ALLEGRO_KEY_TAB) {
6718 int ctrl = (event.keyboard.modifiers & ALLEGRO_KEYMOD_CTRL) ? 1 : 0;
6719 int shift = (event.keyboard.modifiers & ALLEGRO_KEYMOD_SHIFT) ? 1 : 0;
6720
6721 /* Ctrl+Tab in a focused textarea inserts a tab character — skip navigation */
6722 int is_ctrl_tab_in_textarea = 0;
6723 if (ctrl && ctx->focused_widget_id >= 0) {
6724 const N_GUI_WIDGET* fw = n_gui_get_widget(ctx, ctx->focused_widget_id);
6725 if (fw && fw->type == N_GUI_TYPE_TEXTAREA && (fw->state & N_GUI_STATE_FOCUSED))
6726 is_ctrl_tab_in_textarea = 1;
6727 }
6728
6729 if (!is_ctrl_tab_in_textarea) {
6730 /* find the window that contains the focused widget (or the topmost window) */
6731 N_GUI_WINDOW* tab_win = NULL;
6732 if (ctx->focused_widget_id >= 0) {
6733 tab_win = _find_focused_window(ctx);
6734 }
6735 if (!tab_win && ctx->windows && ctx->windows->end) {
6736 /* no focus yet — use topmost open window */
6737 for (LIST_NODE* wn = ctx->windows->end; wn; wn = wn->prev) {
6738 N_GUI_WINDOW* w = (N_GUI_WINDOW*)wn->ptr;
6739 if (w && (w->state & N_GUI_WIN_OPEN) && !(w->state & N_GUI_WIN_MINIMISED)) {
6740 tab_win = w;
6741 break;
6742 }
6743 }
6744 }
6745
6746 if (tab_win && tab_win->widgets && tab_win->widgets->nb_items > 0) {
6747 /* build an ordered list of focusable widget ids */
6748 int focusable_ids[512];
6749 int nfocusable = 0;
6750 list_foreach(wgn, tab_win->widgets) {
6751 const N_GUI_WIDGET* w = (N_GUI_WIDGET*)wgn->ptr;
6752 if (w && w->visible && w->enabled && _is_focusable_type(w->type) && nfocusable < 512) {
6753 focusable_ids[nfocusable++] = w->id;
6754 }
6755 }
6756 if (nfocusable > 0) {
6757 /* find current position */
6758 int cur_idx = -1;
6759 for (int i = 0; i < nfocusable; i++) {
6760 if (focusable_ids[i] == ctx->focused_widget_id) {
6761 cur_idx = i;
6762 break;
6763 }
6764 }
6765 int next_idx;
6766 if (shift) {
6767 next_idx = (cur_idx <= 0) ? nfocusable - 1 : cur_idx - 1;
6768 } else {
6769 next_idx = (cur_idx < 0 || cur_idx >= nfocusable - 1) ? 0 : cur_idx + 1;
6770 }
6771 n_gui_set_focus(ctx, focusable_ids[next_idx]);
6772 }
6773 }
6774 event_consumed = 1;
6775 }
6776 }
6777
6778 /* keyboard events -> focused textarea */
6779 if (!event_consumed && event.type == ALLEGRO_EVENT_KEY_CHAR && ctx->focused_widget_id != -1) {
6781 if (fw && fw->type == N_GUI_TYPE_TEXTAREA && (fw->state & N_GUI_STATE_FOCUSED)) {
6782 ALLEGRO_FONT* ta_font = fw->font ? fw->font : ctx->default_font;
6783 float ta_pad = ctx->style.textarea_padding;
6784 float ta_sb = ctx->style.scrollbar_size;
6785 if (_textarea_handle_key(fw, &event, ta_font, ta_pad, ta_sb, ctx)) {
6786 event_consumed = 1;
6787 }
6788 }
6789 }
6790
6791 /* keyboard events -> focused non-textarea widgets (slider, listbox, radiolist, combobox, scrollbar, checkbox) */
6792 if (!event_consumed && event.type == ALLEGRO_EVENT_KEY_CHAR && ctx->focused_widget_id >= 0) {
6794 if (fw && fw->visible && fw->enabled && (fw->state & N_GUI_STATE_FOCUSED) && fw->data) {
6795 int kc = event.keyboard.keycode;
6796
6797 /* ---- slider keyboard ---- */
6798 if (fw->type == N_GUI_TYPE_SLIDER) {
6800 double step = sd->step > 0.0 ? sd->step : (sd->max_val - sd->min_val) / 20.0;
6801 if (step <= 0) step = 1.0;
6802 double new_val = sd->value;
6803 int handled = 0;
6804
6805 if (sd->orientation == N_GUI_SLIDER_H) {
6806 if (kc == ALLEGRO_KEY_RIGHT) {
6807 new_val += step;
6808 handled = 1;
6809 } else if (kc == ALLEGRO_KEY_LEFT) {
6810 new_val -= step;
6811 handled = 1;
6812 }
6813 } else {
6814 if (kc == ALLEGRO_KEY_UP) {
6815 new_val += step;
6816 handled = 1;
6817 } else if (kc == ALLEGRO_KEY_DOWN) {
6818 new_val -= step;
6819 handled = 1;
6820 }
6821 }
6822 if (kc == ALLEGRO_KEY_HOME) {
6823 new_val = sd->min_val;
6824 handled = 1;
6825 } else if (kc == ALLEGRO_KEY_END) {
6826 new_val = sd->max_val;
6827 handled = 1;
6828 }
6829
6830 if (handled) {
6831 if (sd->step > 0.0)
6832 new_val = _slider_snap_value(new_val, sd->min_val, sd->max_val, sd->step);
6833 else
6834 new_val = _clamp(new_val, sd->min_val, sd->max_val);
6835 if (new_val != sd->value) {
6836 sd->value = new_val;
6837 if (sd->on_change) sd->on_change(fw->id, sd->value, sd->user_data);
6838 }
6839 event_consumed = 1;
6840 }
6841 }
6842
6843 /* ---- listbox keyboard ---- */
6844 if (fw->type == N_GUI_TYPE_LISTBOX) {
6846 if (ld->selection_mode != N_GUI_SELECT_NONE && ld->nb_items > 0) {
6847 int handled = 0;
6848 /* find first selected item for single-select navigation */
6849 int cur_sel = -1;
6851 for (size_t i = 0; i < ld->nb_items; i++) {
6852 if (ld->items[i].selected) {
6853 cur_sel = (int)i;
6854 break;
6855 }
6856 }
6857 }
6858 int new_sel = cur_sel;
6859 if (kc == ALLEGRO_KEY_UP && cur_sel > 0) {
6860 new_sel = cur_sel - 1;
6861 handled = 1;
6862 } else if (kc == ALLEGRO_KEY_DOWN && cur_sel < (int)ld->nb_items - 1) {
6863 new_sel = cur_sel + 1;
6864 handled = 1;
6865 } else if (kc == ALLEGRO_KEY_DOWN && cur_sel < 0) {
6866 new_sel = 0;
6867 handled = 1;
6868 } else if (kc == ALLEGRO_KEY_HOME) {
6869 new_sel = 0;
6870 handled = 1;
6871 } else if (kc == ALLEGRO_KEY_END) {
6872 new_sel = (int)ld->nb_items - 1;
6873 handled = 1;
6874 }
6875
6876 if (handled && ld->selection_mode == N_GUI_SELECT_SINGLE && new_sel != cur_sel) {
6877 for (size_t i = 0; i < ld->nb_items; i++) ld->items[i].selected = 0;
6878 ld->items[new_sel].selected = 1;
6879 if (ld->on_select) ld->on_select(fw->id, new_sel, 1, ld->user_data);
6880 /* auto-scroll to keep selection visible */
6881 ALLEGRO_FONT* lf = fw->font ? fw->font : ctx->default_font;
6882 float lfh = lf ? (float)al_get_font_line_height(lf) : 16.0f;
6883 float lih = ld->item_height > lfh ? ld->item_height : lfh + ctx->style.item_height_pad;
6884 int visible = (int)(fw->h / lih);
6885 if (new_sel < ld->scroll_offset) ld->scroll_offset = new_sel;
6886 if (new_sel >= ld->scroll_offset + visible) ld->scroll_offset = new_sel - visible + 1;
6887 event_consumed = 1;
6888 }
6889 }
6890 }
6891
6892 /* ---- radiolist keyboard ---- */
6893 if (fw->type == N_GUI_TYPE_RADIOLIST) {
6895 if (rd->nb_items > 0) {
6896 int cur_sel = rd->selected_index;
6897 int new_sel = cur_sel;
6898 int handled = 0;
6899 if (kc == ALLEGRO_KEY_UP && cur_sel > 0) {
6900 new_sel = cur_sel - 1;
6901 handled = 1;
6902 } else if (kc == ALLEGRO_KEY_DOWN && cur_sel < (int)rd->nb_items - 1) {
6903 new_sel = cur_sel + 1;
6904 handled = 1;
6905 } else if (kc == ALLEGRO_KEY_DOWN && cur_sel < 0) {
6906 new_sel = 0;
6907 handled = 1;
6908 } else if (kc == ALLEGRO_KEY_HOME) {
6909 new_sel = 0;
6910 handled = 1;
6911 } else if (kc == ALLEGRO_KEY_END) {
6912 new_sel = (int)rd->nb_items - 1;
6913 handled = 1;
6914 }
6915
6916 if (handled && new_sel != cur_sel) {
6917 rd->selected_index = new_sel;
6918 if (rd->on_select) rd->on_select(fw->id, new_sel, rd->user_data);
6919 /* auto-scroll to keep selection visible */
6920 ALLEGRO_FONT* rf = fw->font ? fw->font : ctx->default_font;
6921 float rfh = rf ? (float)al_get_font_line_height(rf) : 16.0f;
6922 float rih = rd->item_height > rfh ? rd->item_height : rfh + ctx->style.item_height_pad;
6923 int visible = (int)(fw->h / rih);
6924 if (new_sel < rd->scroll_offset) rd->scroll_offset = new_sel;
6925 if (new_sel >= rd->scroll_offset + visible) rd->scroll_offset = new_sel - visible + 1;
6926 event_consumed = 1;
6927 }
6928 }
6929 }
6930
6931 /* ---- combobox keyboard (when closed) ---- */
6932 if (fw->type == N_GUI_TYPE_COMBOBOX) {
6934 if (!cd->is_open && cd->nb_items > 0) {
6935 int cur_sel = cd->selected_index;
6936 int new_sel = cur_sel;
6937 int handled = 0;
6938 if (kc == ALLEGRO_KEY_UP && cur_sel > 0) {
6939 new_sel = cur_sel - 1;
6940 handled = 1;
6941 } else if (kc == ALLEGRO_KEY_DOWN && cur_sel < (int)cd->nb_items - 1) {
6942 new_sel = cur_sel + 1;
6943 handled = 1;
6944 } else if (kc == ALLEGRO_KEY_DOWN && cur_sel < 0) {
6945 new_sel = 0;
6946 handled = 1;
6947 } else if (kc == ALLEGRO_KEY_HOME) {
6948 new_sel = 0;
6949 handled = 1;
6950 } else if (kc == ALLEGRO_KEY_END) {
6951 new_sel = (int)cd->nb_items - 1;
6952 handled = 1;
6953 }
6954
6955 if (handled && new_sel != cur_sel) {
6956 cd->selected_index = new_sel;
6957 if (cd->on_select) cd->on_select(fw->id, new_sel, cd->user_data);
6958 event_consumed = 1;
6959 }
6960 }
6961 }
6962
6963 /* ---- scrollbar keyboard ---- */
6964 if (fw->type == N_GUI_TYPE_SCROLLBAR) {
6966 double max_scroll = sb->content_size - sb->viewport_size;
6967 if (max_scroll > 0) {
6968 double step = max_scroll / 20.0;
6969 if (step <= 0) step = 1.0;
6970 double new_pos = sb->scroll_pos;
6971 int handled = 0;
6972 if (sb->orientation == N_GUI_SCROLLBAR_V) {
6973 if (kc == ALLEGRO_KEY_UP) {
6974 new_pos -= step;
6975 handled = 1;
6976 } else if (kc == ALLEGRO_KEY_DOWN) {
6977 new_pos += step;
6978 handled = 1;
6979 }
6980 } else {
6981 if (kc == ALLEGRO_KEY_LEFT) {
6982 new_pos -= step;
6983 handled = 1;
6984 } else if (kc == ALLEGRO_KEY_RIGHT) {
6985 new_pos += step;
6986 handled = 1;
6987 }
6988 }
6989 if (kc == ALLEGRO_KEY_HOME) {
6990 new_pos = 0;
6991 handled = 1;
6992 } else if (kc == ALLEGRO_KEY_END) {
6993 new_pos = max_scroll;
6994 handled = 1;
6995 }
6996
6997 if (handled) {
6998 if (new_pos < 0) new_pos = 0;
6999 if (new_pos > max_scroll) new_pos = max_scroll;
7000 if (new_pos != sb->scroll_pos) {
7001 sb->scroll_pos = new_pos;
7002 if (sb->on_scroll) sb->on_scroll(fw->id, sb->scroll_pos, sb->user_data);
7003 }
7004 event_consumed = 1;
7005 }
7006 }
7007 }
7008
7009 /* ---- checkbox keyboard (Space/Enter to toggle) ---- */
7010 if (fw->type == N_GUI_TYPE_CHECKBOX) {
7011 if (kc == ALLEGRO_KEY_SPACE || kc == ALLEGRO_KEY_ENTER) {
7013 cd->checked = !cd->checked;
7014 if (cd->on_toggle) cd->on_toggle(fw->id, cd->checked, cd->user_data);
7015 event_consumed = 1;
7016 }
7017 }
7018 }
7019 }
7020
7021 /* Ctrl+C on label selection: copy selected text from the most recently selected label */
7022 if (!event_consumed && event.type == ALLEGRO_EVENT_KEY_DOWN &&
7023 (event.keyboard.modifiers & ALLEGRO_KEYMOD_CTRL) &&
7024 event.keyboard.keycode == ALLEGRO_KEY_C && ctx->display &&
7025 ctx->selected_label_id >= 0) {
7026 N_GUI_WIDGET* sel_wgt = n_gui_get_widget(ctx, ctx->selected_label_id);
7027 if (sel_wgt && sel_wgt->type == N_GUI_TYPE_LABEL && sel_wgt->data) {
7028 N_GUI_LABEL_DATA* lb = (N_GUI_LABEL_DATA*)sel_wgt->data;
7029 if (lb->sel_start >= 0 && lb->sel_end >= 0 && lb->sel_start != lb->sel_end) {
7030 int slo = lb->sel_start < lb->sel_end ? lb->sel_start : lb->sel_end;
7031 int shi = lb->sel_start < lb->sel_end ? lb->sel_end : lb->sel_start;
7032 size_t tlen = strlen(lb->text);
7033 if ((size_t)slo > tlen) slo = (int)tlen;
7034 if ((size_t)shi > tlen) shi = (int)tlen;
7035 int len = shi - slo;
7036 if (len > 0) {
7037 char clip[N_GUI_TEXT_MAX];
7038 memcpy(clip, &lb->text[slo], (size_t)len);
7039 clip[len] = '\0';
7040 al_set_clipboard_text(ctx->display, clip);
7041 event_consumed = 1;
7042 }
7043 }
7044 }
7045 }
7046
7047 /* keyboard events -> button key bindings.
7048 * Two passes:
7049 * 1. Focused bindings (key_focus_only=1): fire only when the focused widget
7050 * is the button itself or one of the button's key_sources. These are
7051 * checked first and bypass the widget_focused guard so they work even
7052 * when a textarea has focus.
7053 * 2. Global bindings (key_focus_only=0): fire when no interactive widget
7054 * has focus. Exception: ENTER passes through single-line textareas. */
7055 if (event.type == ALLEGRO_EVENT_KEY_DOWN && !event_consumed) {
7056 int kc = event.keyboard.keycode;
7057 int ev_mods = event.keyboard.modifiers & N_GUI_KEY_MOD_MASK;
7058
7059 /* Pass 1: focused key bindings — check before the widget_focused guard */
7060 for (LIST_NODE* wnode = ctx->windows->end; wnode && !event_consumed; wnode = wnode->prev) {
7061 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
7062 if (!(win->state & N_GUI_WIN_OPEN)) {
7063 continue;
7064 }
7065 list_foreach(wgn, win->widgets) {
7066 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
7067 if (!wgt || !wgt->visible || !wgt->enabled || wgt->type != N_GUI_TYPE_BUTTON) {
7068 continue;
7069 }
7071 if (!bd || !bd->key_focus_only || bd->keycode != kc) {
7072 continue;
7073 }
7074 if (bd->key_modifiers != 0 && ev_mods != bd->key_modifiers) {
7075 continue;
7076 }
7077 /* check if focused widget is this button or a listed source */
7078 int focus_match = (ctx->focused_widget_id == wgt->id);
7079 if (!focus_match) {
7080 for (int i = 0; i < N_GUI_KEY_SOURCES_MAX && bd->key_sources[i] >= 0; i++) {
7081 if (ctx->focused_widget_id == bd->key_sources[i]) {
7082 focus_match = 1;
7083 break;
7084 }
7085 }
7086 }
7087 if (!focus_match) {
7088 continue;
7089 }
7090 if (bd->toggle_mode) {
7091 bd->toggled = !bd->toggled;
7092 }
7093 if (bd->on_click) {
7094 bd->on_click(wgt->id, bd->user_data);
7095 }
7096 event_consumed = 1;
7097 break;
7098 }
7099 }
7100
7101 /* Pass 2: global key bindings — skip when an interactive widget has focus.
7102 * Exception: for single-line textareas, ENTER passes through. */
7103 if (!event_consumed) {
7104 int widget_focused = 0;
7105 if (ctx->focused_widget_id >= 0) {
7107 if (fw && (fw->state & N_GUI_STATE_FOCUSED)) {
7108 if (fw->type == N_GUI_TYPE_TEXTAREA) {
7109 const N_GUI_TEXTAREA_DATA* ftd = (N_GUI_TEXTAREA_DATA*)fw->data;
7110 /* for single-line textareas, allow ENTER to pass through */
7111 if (ftd && (ftd->multiline || kc != ALLEGRO_KEY_ENTER)) {
7112 widget_focused = 1;
7113 }
7114 } else if (_is_focusable_type(fw->type)) {
7115 widget_focused = 1;
7116 }
7117 }
7118 }
7119 if (!widget_focused) {
7120 /* Iterate windows from frontmost (end of list) to backmost (start)
7121 * and stop as soon as a button consumes the event. */
7122 for (LIST_NODE* wnode = ctx->windows->end; wnode && !event_consumed; wnode = wnode->prev) {
7123 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
7124 if (!(win->state & N_GUI_WIN_OPEN)) {
7125 continue;
7126 }
7127 list_foreach(wgn, win->widgets) {
7128 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
7129 if (!wgt || !wgt->visible || !wgt->enabled || wgt->type != N_GUI_TYPE_BUTTON) {
7130 continue;
7131 }
7133 if (bd && !bd->key_focus_only && bd->keycode == kc) {
7134 if (bd->key_modifiers != 0 && ev_mods != bd->key_modifiers) {
7135 continue;
7136 }
7137 if (bd->toggle_mode) {
7138 bd->toggled = !bd->toggled;
7139 }
7140 if (bd->on_click) {
7141 bd->on_click(wgt->id, bd->user_data);
7142 }
7143 event_consumed = 1;
7144 break;
7145 }
7146 }
7147 }
7148 }
7149 }
7150 }
7151
7152 /* mouse wheel scrolling for listbox/radiolist/combobox/dropmenu and auto-scrollbar windows.
7153 * Iterate from topmost window (end of list) to bottommost (start) so that the
7154 * frontmost window under the cursor receives the scroll event first. */
7155 if (event.type == ALLEGRO_EVENT_MOUSE_AXES && event.mouse.dz != 0) {
7156 int scroll_consumed = 0;
7157
7158 /* Check if an open combobox dropdown should consume the scroll */
7159 if (ctx->open_combobox_id >= 0) {
7160 N_GUI_WIDGET* cb_wgt = n_gui_get_widget(ctx, ctx->open_combobox_id);
7161 if (cb_wgt && cb_wgt->data) {
7163 if (cbd->is_open && (int)cbd->nb_items > cbd->max_visible) {
7164 cbd->scroll_offset -= event.mouse.dz;
7165 if (cbd->scroll_offset < 0) cbd->scroll_offset = 0;
7166 int max_off = (int)cbd->nb_items - cbd->max_visible;
7167 if (cbd->scroll_offset > max_off) cbd->scroll_offset = max_off;
7168 scroll_consumed = 1;
7169 }
7170 }
7171 }
7172 /* Check if an open dropmenu panel should consume the scroll */
7173 if (!scroll_consumed && ctx->open_dropmenu_id >= 0) {
7174 N_GUI_WIDGET* dm_wgt = n_gui_get_widget(ctx, ctx->open_dropmenu_id);
7175 if (dm_wgt && dm_wgt->data) {
7177 if (dmd->is_open && (int)dmd->nb_entries > dmd->max_visible) {
7178 dmd->scroll_offset -= event.mouse.dz;
7179 if (dmd->scroll_offset < 0) dmd->scroll_offset = 0;
7180 int max_off = (int)dmd->nb_entries - dmd->max_visible;
7181 if (max_off < 0) max_off = 0;
7182 if (dmd->scroll_offset > max_off) dmd->scroll_offset = max_off;
7183 scroll_consumed = 1;
7184 }
7185 }
7186 }
7187 if (scroll_consumed) return 1;
7188
7189 for (LIST_NODE* wnode = ctx->windows->end; wnode; wnode = wnode->prev) {
7190 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
7191 if (!(win->state & N_GUI_WIN_OPEN) || (win->state & N_GUI_WIN_MINIMISED)) continue;
7192
7193 float win_h = win->h;
7194 if (!_point_in_rect(mx, my, win->x, win->y, win->w, win_h)) continue;
7195
7196 float content_x = win->x;
7197 float content_y = win->y + _win_tbh(win);
7198
7199 /* first check if a widget wants the scroll */
7200 list_foreach(wgn, win->widgets) {
7201 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
7202 if (!wgt || !wgt->visible || !wgt->enabled) continue;
7203 float ax = content_x + wgt->x - win->scroll_x;
7204 float ay = content_y + wgt->y - win->scroll_y;
7205 if (!_point_in_rect(mx, my, ax, ay, wgt->w, wgt->h)) continue;
7206
7207 if (wgt->type == N_GUI_TYPE_LISTBOX) {
7209 ld->scroll_offset -= event.mouse.dz;
7210 if (ld->scroll_offset < 0) ld->scroll_offset = 0;
7211 ALLEGRO_FONT* lf = wgt->font ? wgt->font : ctx->default_font;
7212 float lfh = lf ? (float)al_get_font_line_height(lf) : 16.0f;
7213 float lih = ld->item_height > lfh ? ld->item_height : lfh + ctx->style.item_height_pad;
7214 int visible_count = (int)(wgt->h / lih);
7215 int max_off = (int)ld->nb_items - visible_count;
7216 if (max_off < 0) max_off = 0;
7217 if (ld->scroll_offset > max_off) ld->scroll_offset = max_off;
7218 scroll_consumed = 1;
7219 }
7220 if (wgt->type == N_GUI_TYPE_RADIOLIST) {
7222 rd->scroll_offset -= event.mouse.dz;
7223 if (rd->scroll_offset < 0) rd->scroll_offset = 0;
7224 ALLEGRO_FONT* rf = wgt->font ? wgt->font : ctx->default_font;
7225 float rfh = rf ? (float)al_get_font_line_height(rf) : 16.0f;
7226 float rih = rd->item_height > rfh ? rd->item_height : rfh + ctx->style.item_height_pad;
7227 int visible_count = (int)(wgt->h / rih);
7228 int max_off = (int)rd->nb_items - visible_count;
7229 if (max_off < 0) max_off = 0;
7230 if (rd->scroll_offset > max_off) rd->scroll_offset = max_off;
7231 scroll_consumed = 1;
7232 }
7233 if (wgt->type == N_GUI_TYPE_TEXTAREA) {
7235 if (std->multiline) {
7236 float scroll_step = ctx->style.scroll_step;
7237 std->scroll_y -= (int)((float)event.mouse.dz * scroll_step);
7238 std->scroll_from_wheel = 1;
7239 /* clamp is done at draw time */
7240 scroll_consumed = 1;
7241 }
7242 }
7243 if (wgt->type == N_GUI_TYPE_SLIDER) {
7245 double step = sd->step > 0.0 ? sd->step : (sd->max_val - sd->min_val) / 20.0;
7246 if (step <= 0) step = 1.0;
7247 double new_val = sd->value + (double)event.mouse.dz * step;
7248 if (sd->step > 0.0)
7249 new_val = _slider_snap_value(new_val, sd->min_val, sd->max_val, sd->step);
7250 else
7251 new_val = _clamp(new_val, sd->min_val, sd->max_val);
7252 if (new_val != sd->value) {
7253 sd->value = new_val;
7254 if (sd->on_change) {
7255 sd->on_change(wgt->id, sd->value, sd->user_data);
7256 }
7257 }
7258 scroll_consumed = 1;
7259 }
7260 if (wgt->type == N_GUI_TYPE_SCROLLBAR) {
7262 double max_scroll = sbd->content_size - sbd->viewport_size;
7263 if (max_scroll > 0) {
7264 double step = max_scroll / 20.0;
7265 if (step <= 0) step = 1.0;
7266 sbd->scroll_pos -= (double)event.mouse.dz * step;
7267 if (sbd->scroll_pos < 0) sbd->scroll_pos = 0;
7268 if (sbd->scroll_pos > max_scroll) sbd->scroll_pos = max_scroll;
7269 if (sbd->on_scroll) {
7270 sbd->on_scroll(wgt->id, sbd->scroll_pos, sbd->user_data);
7271 }
7272 scroll_consumed = 1;
7273 }
7274 }
7275 if (wgt->type == N_GUI_TYPE_COMBOBOX) {
7277 if (cbd->is_open && cbd->nb_items > 0) {
7278 cbd->scroll_offset -= event.mouse.dz;
7279 if (cbd->scroll_offset < 0) cbd->scroll_offset = 0;
7280 int max_vis = cbd->max_visible > 0 ? cbd->max_visible : 8;
7281 int max_off = (int)cbd->nb_items - max_vis;
7282 if (max_off < 0) max_off = 0;
7283 if (cbd->scroll_offset > max_off) cbd->scroll_offset = max_off;
7284 scroll_consumed = 1;
7285 }
7286 }
7287 if (wgt->type == N_GUI_TYPE_DROPMENU) {
7289 if (dmd->is_open && dmd->nb_entries > 0) {
7290 dmd->scroll_offset -= event.mouse.dz;
7291 if (dmd->scroll_offset < 0) dmd->scroll_offset = 0;
7292 int max_vis = dmd->max_visible > 0 ? dmd->max_visible : 8;
7293 int max_off = (int)dmd->nb_entries - max_vis;
7294 if (max_off < 0) max_off = 0;
7295 if (dmd->scroll_offset > max_off) dmd->scroll_offset = max_off;
7296 scroll_consumed = 1;
7297 }
7298 }
7299 if (wgt->type == N_GUI_TYPE_LABEL) {
7301 if (sld->align == N_GUI_ALIGN_JUSTIFIED) {
7302 float scroll_step = ctx->style.scroll_step;
7303 sld->scroll_y -= (float)event.mouse.dz * scroll_step;
7304 /* clamp is done at draw time */
7305 scroll_consumed = 1;
7306 }
7307 }
7308 break;
7309 }
7310
7311 /* if no widget consumed the scroll, let the window auto-scroll */
7312 if (!scroll_consumed && (win->flags & N_GUI_WIN_AUTO_SCROLLBAR)) {
7313 float scroll_step = ctx->style.scroll_step;
7314 win->scroll_y -= (float)event.mouse.dz * scroll_step;
7315 /* clamp is done at draw time */
7316 scroll_consumed = 1;
7317 }
7318 if (scroll_consumed) event_consumed = 1;
7319 break; /* only one window gets scroll */
7320 }
7321
7322 /* if no window consumed the scroll and global scrollbars are available,
7323 * scroll the global viewport */
7324 if (!scroll_consumed && eff_w > 0 && eff_h > 0) {
7326 if (ctx->gui_bounds_h > eff_h || ctx->gui_bounds_w > eff_w) {
7327 float scroll_step = ctx->style.global_scroll_step;
7328 ctx->global_scroll_y -= (float)event.mouse.dz * scroll_step;
7329 /* clamp is done at draw time */
7330 event_consumed = 1;
7331 }
7332 }
7333 }
7334
7335 return event_consumed;
7336}
7337
7345 __n_assert(ctx, return 0);
7346 float mx = (float)ctx->mouse_x;
7347 float my = (float)ctx->mouse_y;
7348 /* transform to virtual space when virtual canvas is active */
7349 if (ctx->virtual_w > 0 && ctx->virtual_h > 0 && ctx->gui_scale > 0) {
7350 mx = (mx - ctx->gui_offset_x) / ctx->gui_scale;
7351 my = (my - ctx->gui_offset_y) / ctx->gui_scale;
7352 }
7353 list_foreach(wnode, ctx->windows) {
7354 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
7355 if (!(win->state & N_GUI_WIN_OPEN)) continue;
7356 float win_h = (win->state & N_GUI_WIN_MINIMISED) ? _win_tbh(win) : win->h;
7357 if (_point_in_rect(mx, my, win->x, win->y, win->w, win_h)) {
7358 return 1;
7359 }
7360 }
7361 return 0;
7362}
7363
7364/* =========================================================================
7365 * JSON THEME I/O (requires cJSON)
7366 * ========================================================================= */
7367
7368#ifdef HAVE_CJSON
7369
7371static void _json_add_color(cJSON* parent, const char* name, ALLEGRO_COLOR c) {
7372 unsigned char r, g, b, a;
7373 al_unmap_rgba(c, &r, &g, &b, &a);
7374 cJSON* arr = cJSON_CreateArray();
7375 cJSON_AddItemToArray(arr, cJSON_CreateNumber(r));
7376 cJSON_AddItemToArray(arr, cJSON_CreateNumber(g));
7377 cJSON_AddItemToArray(arr, cJSON_CreateNumber(b));
7378 cJSON_AddItemToArray(arr, cJSON_CreateNumber(a));
7379 cJSON_AddItemToObject(parent, name, arr);
7380}
7381
7383static ALLEGRO_COLOR _json_get_color(cJSON* parent, const char* name, ALLEGRO_COLOR fallback) {
7384 cJSON* arr = cJSON_GetObjectItemCaseSensitive(parent, name);
7385 if (!arr || !cJSON_IsArray(arr) || cJSON_GetArraySize(arr) < 4) return fallback;
7386 int r = (int)cJSON_GetArrayItem(arr, 0)->valuedouble;
7387 int g = (int)cJSON_GetArrayItem(arr, 1)->valuedouble;
7388 int b = (int)cJSON_GetArrayItem(arr, 2)->valuedouble;
7389 int a = (int)cJSON_GetArrayItem(arr, 3)->valuedouble;
7390 return al_map_rgba((unsigned char)r, (unsigned char)g, (unsigned char)b, (unsigned char)a);
7391}
7392
7394static float _json_get_float(cJSON* parent, const char* name, float fallback) {
7395 cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
7396 if (!item || !cJSON_IsNumber(item)) return fallback;
7397 return (float)item->valuedouble;
7398}
7399
7401static int _json_get_int(cJSON* parent, const char* name, int fallback) {
7402 cJSON* item = cJSON_GetObjectItemCaseSensitive(parent, name);
7403 if (!item || !cJSON_IsNumber(item)) return fallback;
7404 return (int)item->valuedouble;
7405}
7406
7413int n_gui_save_theme_json(N_GUI_CTX* ctx, const char* filepath) {
7414 __n_assert(ctx, return -1);
7415 __n_assert(filepath, return -1);
7416
7417 cJSON* root = cJSON_CreateObject();
7418 if (!root) return -1;
7419
7420 /* theme colours */
7421 cJSON* theme = cJSON_CreateObject();
7422 N_GUI_THEME* t = &ctx->default_theme;
7423 _json_add_color(theme, "bg_normal", t->bg_normal);
7424 _json_add_color(theme, "bg_hover", t->bg_hover);
7425 _json_add_color(theme, "bg_active", t->bg_active);
7426 _json_add_color(theme, "border_normal", t->border_normal);
7427 _json_add_color(theme, "border_hover", t->border_hover);
7428 _json_add_color(theme, "border_active", t->border_active);
7429 _json_add_color(theme, "text_normal", t->text_normal);
7430 _json_add_color(theme, "text_hover", t->text_hover);
7431 _json_add_color(theme, "text_active", t->text_active);
7432 _json_add_color(theme, "selection_color", t->selection_color);
7433 cJSON_AddNumberToObject(theme, "border_thickness", t->border_thickness);
7434 cJSON_AddNumberToObject(theme, "corner_rx", t->corner_rx);
7435 cJSON_AddNumberToObject(theme, "corner_ry", t->corner_ry);
7436 cJSON_AddItemToObject(root, "theme", theme);
7437
7438 /* style values */
7439 cJSON* sty = cJSON_CreateObject();
7440 N_GUI_STYLE* s = &ctx->style;
7441 cJSON_AddNumberToObject(sty, "titlebar_h", s->titlebar_h);
7442 cJSON_AddNumberToObject(sty, "min_win_w", s->min_win_w);
7443 cJSON_AddNumberToObject(sty, "min_win_h", s->min_win_h);
7444 cJSON_AddNumberToObject(sty, "title_padding", s->title_padding);
7445 cJSON_AddNumberToObject(sty, "title_max_w_reserve", s->title_max_w_reserve);
7446
7447 cJSON_AddNumberToObject(sty, "scrollbar_size", s->scrollbar_size);
7448 cJSON_AddNumberToObject(sty, "scrollbar_thumb_min", s->scrollbar_thumb_min);
7449 cJSON_AddNumberToObject(sty, "scrollbar_thumb_padding", s->scrollbar_thumb_padding);
7450 cJSON_AddNumberToObject(sty, "scrollbar_thumb_corner_r", s->scrollbar_thumb_corner_r);
7451 _json_add_color(sty, "scrollbar_track_color", s->scrollbar_track_color);
7452 _json_add_color(sty, "scrollbar_thumb_color", s->scrollbar_thumb_color);
7453
7454 cJSON_AddNumberToObject(sty, "global_scrollbar_size", s->global_scrollbar_size);
7455 cJSON_AddNumberToObject(sty, "global_scrollbar_thumb_min", s->global_scrollbar_thumb_min);
7456 cJSON_AddNumberToObject(sty, "global_scrollbar_thumb_padding", s->global_scrollbar_thumb_padding);
7457 cJSON_AddNumberToObject(sty, "global_scrollbar_thumb_corner_r", s->global_scrollbar_thumb_corner_r);
7458 cJSON_AddNumberToObject(sty, "global_scrollbar_border_thickness", s->global_scrollbar_border_thickness);
7459 _json_add_color(sty, "global_scrollbar_track_color", s->global_scrollbar_track_color);
7460 _json_add_color(sty, "global_scrollbar_thumb_color", s->global_scrollbar_thumb_color);
7461 _json_add_color(sty, "global_scrollbar_thumb_border_color", s->global_scrollbar_thumb_border_color);
7462
7463 cJSON_AddNumberToObject(sty, "grip_size", s->grip_size);
7464 cJSON_AddNumberToObject(sty, "grip_line_thickness", s->grip_line_thickness);
7465 _json_add_color(sty, "grip_color", s->grip_color);
7466
7467 cJSON_AddNumberToObject(sty, "slider_track_size", s->slider_track_size);
7468 cJSON_AddNumberToObject(sty, "slider_track_corner_r", s->slider_track_corner_r);
7469 cJSON_AddNumberToObject(sty, "slider_track_border_thickness", s->slider_track_border_thickness);
7470 cJSON_AddNumberToObject(sty, "slider_handle_min_r", s->slider_handle_min_r);
7471 cJSON_AddNumberToObject(sty, "slider_handle_edge_offset", s->slider_handle_edge_offset);
7472 cJSON_AddNumberToObject(sty, "slider_handle_border_thickness", s->slider_handle_border_thickness);
7473 cJSON_AddNumberToObject(sty, "slider_value_label_offset", s->slider_value_label_offset);
7474
7475 cJSON_AddNumberToObject(sty, "textarea_padding", s->textarea_padding);
7476 cJSON_AddNumberToObject(sty, "textarea_cursor_width", s->textarea_cursor_width);
7477 cJSON_AddNumberToObject(sty, "textarea_cursor_blink_period", s->textarea_cursor_blink_period);
7478
7479 cJSON_AddNumberToObject(sty, "checkbox_max_size", s->checkbox_max_size);
7480 cJSON_AddNumberToObject(sty, "checkbox_mark_margin", s->checkbox_mark_margin);
7481 cJSON_AddNumberToObject(sty, "checkbox_mark_thickness", s->checkbox_mark_thickness);
7482 cJSON_AddNumberToObject(sty, "checkbox_label_gap", s->checkbox_label_gap);
7483 cJSON_AddNumberToObject(sty, "checkbox_label_offset", s->checkbox_label_offset);
7484
7485 cJSON_AddNumberToObject(sty, "radio_circle_min_r", s->radio_circle_min_r);
7486 cJSON_AddNumberToObject(sty, "radio_circle_border_thickness", s->radio_circle_border_thickness);
7487 cJSON_AddNumberToObject(sty, "radio_inner_offset", s->radio_inner_offset);
7488 cJSON_AddNumberToObject(sty, "radio_label_gap", s->radio_label_gap);
7489
7490 cJSON_AddNumberToObject(sty, "listbox_default_item_height", s->listbox_default_item_height);
7491 cJSON_AddNumberToObject(sty, "radiolist_default_item_height", s->radiolist_default_item_height);
7492 cJSON_AddNumberToObject(sty, "combobox_max_visible", s->combobox_max_visible);
7493 cJSON_AddNumberToObject(sty, "dropmenu_max_visible", s->dropmenu_max_visible);
7494 cJSON_AddNumberToObject(sty, "item_text_padding", s->item_text_padding);
7495 cJSON_AddNumberToObject(sty, "item_selection_inset", s->item_selection_inset);
7496 cJSON_AddNumberToObject(sty, "item_height_pad", s->item_height_pad);
7497
7498 cJSON_AddNumberToObject(sty, "dropdown_arrow_reserve", s->dropdown_arrow_reserve);
7499 cJSON_AddNumberToObject(sty, "dropdown_arrow_thickness", s->dropdown_arrow_thickness);
7500 cJSON_AddNumberToObject(sty, "dropdown_arrow_half_h", s->dropdown_arrow_half_h);
7501 cJSON_AddNumberToObject(sty, "dropdown_arrow_half_w", s->dropdown_arrow_half_w);
7502 cJSON_AddNumberToObject(sty, "dropdown_border_thickness", s->dropdown_border_thickness);
7503
7504 cJSON_AddNumberToObject(sty, "label_padding", s->label_padding);
7505 cJSON_AddNumberToObject(sty, "link_underline_thickness", s->link_underline_thickness);
7506 _json_add_color(sty, "link_color_normal", s->link_color_normal);
7507 _json_add_color(sty, "link_color_hover", s->link_color_hover);
7508
7509 cJSON_AddNumberToObject(sty, "scroll_step", s->scroll_step);
7510 cJSON_AddNumberToObject(sty, "global_scroll_step", s->global_scroll_step);
7511 cJSON_AddNumberToObject(sty, "combobox_max_dropdown_width", s->combobox_max_dropdown_width);
7512 cJSON_AddItemToObject(root, "style", sty);
7513
7514 /* write to file */
7515 char* json_str = cJSON_Print(root);
7516 cJSON_Delete(root);
7517 if (!json_str) return -1;
7518
7519 FILE* fp = fopen(filepath, "w");
7520 if (!fp) {
7521 cJSON_free(json_str);
7522 return -1;
7523 }
7524 fprintf(fp, "%s\n", json_str);
7525 fclose(fp);
7526 cJSON_free(json_str);
7527 return 0;
7528}
7529
7536int n_gui_load_theme_json(N_GUI_CTX* ctx, const char* filepath) {
7537 __n_assert(ctx, return -1);
7538 __n_assert(filepath, return -1);
7539
7540 FILE* fp = fopen(filepath, "r");
7541 if (!fp) return -1;
7542
7543 fseek(fp, 0, SEEK_END);
7544 long fsize = ftell(fp);
7545 fseek(fp, 0, SEEK_SET);
7546 if (fsize <= 0 || fsize > 1024 * 1024) { /* sanity: max 1 MB */
7547 fclose(fp);
7548 return -1;
7549 }
7550
7551 char* buf = NULL;
7552 Malloc(buf, char, (size_t)fsize + 1);
7553 if (!buf) {
7554 fclose(fp);
7555 return -1;
7556 }
7557 size_t nread = fread(buf, 1, (size_t)fsize, fp);
7558 fclose(fp);
7559 buf[nread] = '\0';
7560
7561 cJSON* root = cJSON_Parse(buf);
7562 FreeNoLog(buf);
7563 if (!root) return -1;
7564
7565 /* parse theme colours */
7566 cJSON* theme = cJSON_GetObjectItemCaseSensitive(root, "theme");
7567 if (theme) {
7568 N_GUI_THEME* t = &ctx->default_theme;
7569 t->bg_normal = _json_get_color(theme, "bg_normal", t->bg_normal);
7570 t->bg_hover = _json_get_color(theme, "bg_hover", t->bg_hover);
7571 t->bg_active = _json_get_color(theme, "bg_active", t->bg_active);
7572 t->border_normal = _json_get_color(theme, "border_normal", t->border_normal);
7573 t->border_hover = _json_get_color(theme, "border_hover", t->border_hover);
7574 t->border_active = _json_get_color(theme, "border_active", t->border_active);
7575 t->text_normal = _json_get_color(theme, "text_normal", t->text_normal);
7576 t->text_hover = _json_get_color(theme, "text_hover", t->text_hover);
7577 t->text_active = _json_get_color(theme, "text_active", t->text_active);
7578 t->selection_color = _json_get_color(theme, "selection_color", t->selection_color);
7579 t->border_thickness = _json_get_float(theme, "border_thickness", t->border_thickness);
7580 t->corner_rx = _json_get_float(theme, "corner_rx", t->corner_rx);
7581 t->corner_ry = _json_get_float(theme, "corner_ry", t->corner_ry);
7582
7583 /* apply to all existing windows and widgets */
7584 list_foreach(wnode, ctx->windows) {
7585 N_GUI_WINDOW* win = (N_GUI_WINDOW*)wnode->ptr;
7586 win->theme = *t;
7587 list_foreach(wgn, win->widgets) {
7588 N_GUI_WIDGET* wgt = (N_GUI_WIDGET*)wgn->ptr;
7589 if (wgt) wgt->theme = *t;
7590 }
7591 }
7592 }
7593
7594 /* parse style values */
7595 cJSON* sty = cJSON_GetObjectItemCaseSensitive(root, "style");
7596 if (sty) {
7597 N_GUI_STYLE* s = &ctx->style;
7598 s->titlebar_h = _json_get_float(sty, "titlebar_h", s->titlebar_h);
7599 s->min_win_w = _json_get_float(sty, "min_win_w", s->min_win_w);
7600 s->min_win_h = _json_get_float(sty, "min_win_h", s->min_win_h);
7601 s->title_padding = _json_get_float(sty, "title_padding", s->title_padding);
7602 s->title_max_w_reserve = _json_get_float(sty, "title_max_w_reserve", s->title_max_w_reserve);
7603
7604 s->scrollbar_size = _json_get_float(sty, "scrollbar_size", s->scrollbar_size);
7605 s->scrollbar_thumb_min = _json_get_float(sty, "scrollbar_thumb_min", s->scrollbar_thumb_min);
7606 s->scrollbar_thumb_padding = _json_get_float(sty, "scrollbar_thumb_padding", s->scrollbar_thumb_padding);
7607 s->scrollbar_thumb_corner_r = _json_get_float(sty, "scrollbar_thumb_corner_r", s->scrollbar_thumb_corner_r);
7608 s->scrollbar_track_color = _json_get_color(sty, "scrollbar_track_color", s->scrollbar_track_color);
7609 s->scrollbar_thumb_color = _json_get_color(sty, "scrollbar_thumb_color", s->scrollbar_thumb_color);
7610
7611 s->global_scrollbar_size = _json_get_float(sty, "global_scrollbar_size", s->global_scrollbar_size);
7612 s->global_scrollbar_thumb_min = _json_get_float(sty, "global_scrollbar_thumb_min", s->global_scrollbar_thumb_min);
7613 s->global_scrollbar_thumb_padding = _json_get_float(sty, "global_scrollbar_thumb_padding", s->global_scrollbar_thumb_padding);
7614 s->global_scrollbar_thumb_corner_r = _json_get_float(sty, "global_scrollbar_thumb_corner_r", s->global_scrollbar_thumb_corner_r);
7615 s->global_scrollbar_border_thickness = _json_get_float(sty, "global_scrollbar_border_thickness", s->global_scrollbar_border_thickness);
7616 s->global_scrollbar_track_color = _json_get_color(sty, "global_scrollbar_track_color", s->global_scrollbar_track_color);
7617 s->global_scrollbar_thumb_color = _json_get_color(sty, "global_scrollbar_thumb_color", s->global_scrollbar_thumb_color);
7618 s->global_scrollbar_thumb_border_color = _json_get_color(sty, "global_scrollbar_thumb_border_color", s->global_scrollbar_thumb_border_color);
7619
7620 s->grip_size = _json_get_float(sty, "grip_size", s->grip_size);
7621 s->grip_line_thickness = _json_get_float(sty, "grip_line_thickness", s->grip_line_thickness);
7622 s->grip_color = _json_get_color(sty, "grip_color", s->grip_color);
7623
7624 s->slider_track_size = _json_get_float(sty, "slider_track_size", s->slider_track_size);
7625 s->slider_track_corner_r = _json_get_float(sty, "slider_track_corner_r", s->slider_track_corner_r);
7626 s->slider_track_border_thickness = _json_get_float(sty, "slider_track_border_thickness", s->slider_track_border_thickness);
7627 s->slider_handle_min_r = _json_get_float(sty, "slider_handle_min_r", s->slider_handle_min_r);
7628 s->slider_handle_edge_offset = _json_get_float(sty, "slider_handle_edge_offset", s->slider_handle_edge_offset);
7629 s->slider_handle_border_thickness = _json_get_float(sty, "slider_handle_border_thickness", s->slider_handle_border_thickness);
7630 s->slider_value_label_offset = _json_get_float(sty, "slider_value_label_offset", s->slider_value_label_offset);
7631
7632 s->textarea_padding = _json_get_float(sty, "textarea_padding", s->textarea_padding);
7633 s->textarea_cursor_width = _json_get_float(sty, "textarea_cursor_width", s->textarea_cursor_width);
7634 s->textarea_cursor_blink_period = _json_get_float(sty, "textarea_cursor_blink_period", s->textarea_cursor_blink_period);
7635
7636 s->checkbox_max_size = _json_get_float(sty, "checkbox_max_size", s->checkbox_max_size);
7637 s->checkbox_mark_margin = _json_get_float(sty, "checkbox_mark_margin", s->checkbox_mark_margin);
7638 s->checkbox_mark_thickness = _json_get_float(sty, "checkbox_mark_thickness", s->checkbox_mark_thickness);
7639 s->checkbox_label_gap = _json_get_float(sty, "checkbox_label_gap", s->checkbox_label_gap);
7640 s->checkbox_label_offset = _json_get_float(sty, "checkbox_label_offset", s->checkbox_label_offset);
7641
7642 s->radio_circle_min_r = _json_get_float(sty, "radio_circle_min_r", s->radio_circle_min_r);
7643 s->radio_circle_border_thickness = _json_get_float(sty, "radio_circle_border_thickness", s->radio_circle_border_thickness);
7644 s->radio_inner_offset = _json_get_float(sty, "radio_inner_offset", s->radio_inner_offset);
7645 s->radio_label_gap = _json_get_float(sty, "radio_label_gap", s->radio_label_gap);
7646
7647 s->listbox_default_item_height = _json_get_float(sty, "listbox_default_item_height", s->listbox_default_item_height);
7648 s->radiolist_default_item_height = _json_get_float(sty, "radiolist_default_item_height", s->radiolist_default_item_height);
7649 s->combobox_max_visible = _json_get_int(sty, "combobox_max_visible", s->combobox_max_visible);
7650 s->dropmenu_max_visible = _json_get_int(sty, "dropmenu_max_visible", s->dropmenu_max_visible);
7651 s->item_text_padding = _json_get_float(sty, "item_text_padding", s->item_text_padding);
7652 s->item_selection_inset = _json_get_float(sty, "item_selection_inset", s->item_selection_inset);
7653 s->item_height_pad = _json_get_float(sty, "item_height_pad", s->item_height_pad);
7654
7655 s->dropdown_arrow_reserve = _json_get_float(sty, "dropdown_arrow_reserve", s->dropdown_arrow_reserve);
7656 s->dropdown_arrow_thickness = _json_get_float(sty, "dropdown_arrow_thickness", s->dropdown_arrow_thickness);
7657 s->dropdown_arrow_half_h = _json_get_float(sty, "dropdown_arrow_half_h", s->dropdown_arrow_half_h);
7658 s->dropdown_arrow_half_w = _json_get_float(sty, "dropdown_arrow_half_w", s->dropdown_arrow_half_w);
7659 s->dropdown_border_thickness = _json_get_float(sty, "dropdown_border_thickness", s->dropdown_border_thickness);
7660
7661 s->label_padding = _json_get_float(sty, "label_padding", s->label_padding);
7662 s->link_underline_thickness = _json_get_float(sty, "link_underline_thickness", s->link_underline_thickness);
7663 s->link_color_normal = _json_get_color(sty, "link_color_normal", s->link_color_normal);
7664 s->link_color_hover = _json_get_color(sty, "link_color_hover", s->link_color_hover);
7665
7666 s->scroll_step = _json_get_float(sty, "scroll_step", s->scroll_step);
7667 s->global_scroll_step = _json_get_float(sty, "global_scroll_step", s->global_scroll_step);
7668 s->combobox_max_dropdown_width = _json_get_float(sty, "combobox_max_dropdown_width", s->combobox_max_dropdown_width);
7669 }
7670
7671 cJSON_Delete(root);
7672 return 0;
7673}
7674
7675#else /* !HAVE_CJSON */
7676
7683int n_gui_save_theme_json(N_GUI_CTX* ctx, const char* filepath) {
7684 (void)ctx;
7685 (void)filepath;
7686 n_log(LOG_ERR, "n_gui_save_theme_json: cJSON not available (compile with -DHAVE_CJSON)");
7687 return -1;
7688}
7689
7696int n_gui_load_theme_json(N_GUI_CTX* ctx, const char* filepath) {
7697 (void)ctx;
7698 (void)filepath;
7699 n_log(LOG_ERR, "n_gui_load_theme_json: cJSON not available (compile with -DHAVE_CJSON)");
7700 return -1;
7701}
7702
7703#endif /* HAVE_CJSON */
7704
7705static void _n_gui_tab_button_clicked(int widget_id, void* user_data) {
7706 N_GUI_TAB_PANEL* panel = (N_GUI_TAB_PANEL*)user_data;
7707 if (!panel) return;
7708 int clicked = -1;
7709 for (int i = 0; i < panel->nb_tabs; i++) {
7710 if (panel->button_ids[i] == widget_id) {
7711 clicked = i;
7712 break;
7713 }
7714 }
7715 if (clicked < 0) return;
7716 n_gui_tab_set_active(panel, clicked);
7717 if (panel->on_tab_change) panel->on_tab_change(clicked, panel->user_data);
7718}
7719
7720N_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) {
7721 if (!ctx) return NULL;
7722 N_GUI_TAB_PANEL* panel = NULL;
7723 Malloc(panel, N_GUI_TAB_PANEL, 1);
7724 if (!panel) return NULL;
7725 panel->ctx = ctx;
7726 panel->parent_window_id = window_id;
7727 panel->nb_tabs = 0;
7728 panel->active_tab = -1;
7729 panel->x = x;
7730 panel->y = y;
7731 panel->button_w = button_w;
7732 panel->button_h = button_h;
7733 panel->on_tab_change = on_tab_change;
7734 panel->user_data = user_data;
7735 for (int i = 0; i < N_GUI_TAB_MAX; i++) {
7736 panel->button_ids[i] = -1;
7737 panel->content_window_ids[i] = -1;
7738 }
7739 return panel;
7740}
7741
7742int n_gui_tab_add(N_GUI_TAB_PANEL* panel, const char* label) {
7743 if (!panel || !label || panel->nb_tabs >= N_GUI_TAB_MAX) return -1;
7744 int idx = panel->nb_tabs;
7745 float bx = panel->x + (float)idx * panel->button_w;
7746 int btn_id = n_gui_add_toggle_button(panel->ctx, panel->parent_window_id,
7747 label, bx, panel->y, panel->button_w, panel->button_h,
7748 N_GUI_SHAPE_RECT, (idx == 0) ? 1 : 0,
7750 if (btn_id < 0) return -1;
7751 panel->button_ids[idx] = btn_id;
7752 panel->nb_tabs++;
7753 if (idx == 0) panel->active_tab = 0;
7754 return idx;
7755}
7756
7757void n_gui_tab_set_content_window(N_GUI_TAB_PANEL* panel, int tab_index, int window_id) {
7758 if (!panel || tab_index < 0 || tab_index >= panel->nb_tabs) return;
7759 panel->content_window_ids[tab_index] = window_id;
7760}
7761
7762void n_gui_tab_set_active(N_GUI_TAB_PANEL* panel, int index) {
7763 if (!panel || index < 0 || index >= panel->nb_tabs) return;
7764 for (int i = 0; i < panel->nb_tabs; i++) {
7765 n_gui_button_set_toggled(panel->ctx, panel->button_ids[i], (i == index) ? 1 : 0);
7766 if (panel->content_window_ids[i] >= 0) {
7767 if (i == index)
7768 n_gui_open_window(panel->ctx, panel->content_window_ids[i]);
7769 else
7770 n_gui_close_window(panel->ctx, panel->content_window_ids[i]);
7771 }
7772 }
7773 panel->active_tab = index;
7774}
7775
7777 return panel ? panel->active_tab : -1;
7778}
7779
7781 if (!panel || !*panel) return;
7782 FreeNoLog(*panel);
7783 *panel = NULL;
7784}
7785
7786/* ---- TREE VIEW ---- */
7787
7788static void _n_gui_tree_listbox_selected(int widget_id, int index, int selected, void* user_data) {
7789 (void)widget_id;
7790 N_GUI_TREE* tree = (N_GUI_TREE*)user_data;
7791 if (!tree || !selected) return;
7792 if (index < 0 || index >= tree->nb_visible) return;
7793 int node_idx = tree->visible_map[index];
7794 if (node_idx < 0 || node_idx >= tree->nb_nodes) return;
7795 if (tree->nodes[node_idx].has_children) n_gui_tree_toggle_expand(tree, node_idx);
7796 if (tree->on_select) tree->on_select(node_idx, tree->user_data);
7797}
7798
7799N_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) {
7800 if (!ctx) return NULL;
7801 N_GUI_TREE* tree = NULL;
7802 Malloc(tree, N_GUI_TREE, 1);
7803 if (!tree) return NULL;
7804 tree->ctx = ctx;
7805 tree->nb_nodes = 0;
7806 tree->nb_visible = 0;
7807 tree->on_select = on_select;
7808 tree->user_data = user_data;
7809 tree->listbox_id = n_gui_add_listbox(ctx, window_id, x, y, w, h,
7811 if (tree->listbox_id < 0) {
7812 FreeNoLog(tree);
7813 return NULL;
7814 }
7815 return tree;
7816}
7817
7818static int _n_gui_tree_node_visible(N_GUI_TREE* tree, int node_index) {
7819 int pi = tree->nodes[node_index].parent_index;
7820 while (pi >= 0) {
7821 if (!tree->nodes[pi].expanded) return 0;
7822 pi = tree->nodes[pi].parent_index;
7823 }
7824 return 1;
7825}
7826
7827int n_gui_tree_add_node(N_GUI_TREE* tree, const char* label, int parent_index, void* user_data) {
7828 if (!tree || !label || tree->nb_nodes >= N_GUI_TREE_MAX) return -1;
7829 if (parent_index >= tree->nb_nodes) return -1;
7830 int idx = tree->nb_nodes;
7831 N_GUI_TREE_NODE* node = &tree->nodes[idx];
7832 snprintf(node->label, sizeof(node->label), "%s", label);
7833 node->parent_index = parent_index;
7834 node->expanded = 0;
7835 node->has_children = 0;
7836 node->user_data = user_data;
7837 if (parent_index < 0) {
7838 node->depth = 0;
7839 } else {
7840 node->depth = tree->nodes[parent_index].depth + 1;
7841 tree->nodes[parent_index].has_children = 1;
7842 }
7843 tree->nb_nodes++;
7844 n_gui_tree_rebuild(tree);
7845 return idx;
7846}
7847
7848void n_gui_tree_toggle_expand(N_GUI_TREE* tree, int node_index) {
7849 if (!tree || node_index < 0 || node_index >= tree->nb_nodes) return;
7850 if (!tree->nodes[node_index].has_children) return;
7851 tree->nodes[node_index].expanded = !tree->nodes[node_index].expanded;
7852 n_gui_tree_rebuild(tree);
7853}
7854
7856 if (!tree) return;
7857 n_gui_listbox_clear(tree->ctx, tree->listbox_id);
7858 tree->nb_visible = 0;
7859 for (int i = 0; i < tree->nb_nodes; i++) {
7860 if (!_n_gui_tree_node_visible(tree, i)) continue;
7861 N_GUI_TREE_NODE* node = &tree->nodes[i];
7862 char display[256];
7863 char indent[64];
7864 int pad = node->depth * 2;
7865 if (pad >= (int)sizeof(indent) - 1) pad = (int)sizeof(indent) - 2;
7866 memset(indent, ' ', (size_t)pad);
7867 indent[pad] = '\0';
7868 const char* marker = node->has_children ? (node->expanded ? "v " : "> ") : " ";
7869 snprintf(display, sizeof(display), "%s%s%s", indent, marker, node->label);
7871 if (tree->nb_visible < N_GUI_TREE_MAX) {
7872 tree->visible_map[tree->nb_visible] = i;
7873 tree->nb_visible++;
7874 }
7875 }
7876}
7877
7879 if (!tree || !*tree) return;
7880 FreeNoLog(*tree);
7881 *tree = NULL;
7882}
7883
7884/* ---- KEY-VALUE TABLE ---- */
7885
7886static void _n_gui_kv_add_clicked(int widget_id, void* user_data) {
7887 (void)widget_id;
7888 N_GUI_KVTABLE* table = (N_GUI_KVTABLE*)user_data;
7889 if (!table) return;
7890 n_gui_kvtable_add_row(table, "", "", "", 1);
7891}
7892
7893static void _n_gui_kv_remove_clicked(int widget_id, void* user_data) {
7894 N_GUI_KVTABLE* table = (N_GUI_KVTABLE*)user_data;
7895 if (!table) return;
7896 for (int i = 0; i < table->nb_rows; i++) {
7897 if (table->rows[i].remove_id == widget_id && table->rows[i].active) {
7898 n_gui_kvtable_remove_row(table, i);
7899 if (table->on_remove) table->on_remove(i, table->user_data);
7900 return;
7901 }
7902 }
7903}
7904
7906 float cy = table->padding + table->header_height;
7907 for (int i = 0; i < table->nb_rows; i++) {
7908 if (!table->rows[i].active) continue;
7909 const N_GUI_KV_ROW* row = &table->rows[i];
7910 N_GUI_WIDGET* w;
7911 w = n_gui_get_widget(table->ctx, row->key_id);
7912 if (w) w->y = cy;
7913 w = n_gui_get_widget(table->ctx, row->value_id);
7914 if (w) w->y = cy;
7915 w = n_gui_get_widget(table->ctx, row->desc_id);
7916 if (w) w->y = cy;
7917 w = n_gui_get_widget(table->ctx, row->enabled_id);
7918 if (w) w->y = cy;
7919 w = n_gui_get_widget(table->ctx, row->remove_id);
7920 if (w) w->y = cy;
7921 cy += table->row_height;
7922 }
7923 /* reposition "+" button below last active row */
7924 N_GUI_WIDGET* add_w = n_gui_get_widget(table->ctx, table->btn_add);
7925 if (add_w) add_w->y = cy;
7926}
7927
7928N_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) {
7929 if (!ctx) return NULL;
7930 N_GUI_KVTABLE* table = NULL;
7931 Malloc(table, N_GUI_KVTABLE, 1);
7932 if (!table) return NULL;
7933 memset(table, 0, sizeof(*table));
7934 table->ctx = ctx;
7935 table->window_id = window_id;
7936 table->nb_rows = 0;
7937 table->nb_active = 0;
7938 table->row_height = row_height;
7939 table->padding = padding;
7940 table->on_remove = on_remove;
7941 table->user_data = user_data;
7942
7943 /* column header labels */
7944 float hdr_h = 18.0f;
7945 table->header_height = hdr_h + 2.0f;
7946 float px = padding;
7947 const N_GUI_WINDOW* wptr = n_gui_get_window(ctx, window_id);
7948 float total_w = wptr ? (wptr->w - padding * 2.0f) : 400.0f;
7949 float gap = 6.0f;
7950 float col_w = total_w * 0.27f;
7951 float key_w = col_w, val_w = col_w, desc_w = col_w;
7952 float chk_w = row_height - 4.0f;
7953
7954 table->lbl_key = n_gui_add_label(ctx, window_id, "Key",
7955 px, padding, key_w, hdr_h, N_GUI_ALIGN_LEFT);
7956 table->lbl_value = n_gui_add_label(ctx, window_id, "Value",
7957 px + key_w, padding, val_w, hdr_h, N_GUI_ALIGN_LEFT);
7958 table->lbl_desc = n_gui_add_label(ctx, window_id, "Description",
7959 px + key_w + val_w, padding, desc_w, hdr_h, N_GUI_ALIGN_LEFT);
7960 table->lbl_enabled = n_gui_add_label(ctx, window_id, "On",
7961 px + key_w + val_w + desc_w + gap, padding, chk_w, hdr_h, N_GUI_ALIGN_LEFT);
7962
7963 /* "+" add row button (positioned below rows, updated by _reposition) */
7964 float btn_y = padding + table->header_height;
7965 table->btn_add = n_gui_add_button(ctx, window_id,
7966 "+", px, btn_y, 30, row_height - 4.0f,
7968
7969 return table;
7970}
7971
7973 const char* key,
7974 const char* value,
7975 const char* description,
7976 int enabled) {
7977 if (!table || table->nb_rows >= N_GUI_KV_MAX) return -1;
7978 int idx = table->nb_rows;
7979 float cy = table->padding + table->header_height + (float)table->nb_active * table->row_height;
7980 float px = table->padding;
7981 float rh = table->row_height - 4.0f;
7982 N_GUI_CTX* ctx = table->ctx;
7983 int win = table->window_id;
7984 const N_GUI_WINDOW* wptr = n_gui_get_window(ctx, win);
7985 float total_w = wptr ? (wptr->w - table->padding * 2.0f) : 400.0f;
7986 float gap = 6.0f;
7987 float col_w = total_w * 0.27f;
7988 float key_w = col_w, val_w = col_w, desc_w = col_w, chk_w = rh, rem_w = rh;
7989 float fx = px;
7990 N_GUI_KV_ROW* row = &table->rows[idx];
7991 row->key_id = n_gui_add_textarea(ctx, win, fx, cy, key_w, rh, 0, 256, NULL, NULL);
7992 fx += key_w;
7993 row->value_id = n_gui_add_textarea(ctx, win, fx, cy, val_w, rh, 0, 1024, NULL, NULL);
7994 fx += val_w;
7995 row->desc_id = n_gui_add_textarea(ctx, win, fx, cy, desc_w, rh, 0, 256, NULL, NULL);
7996 fx += desc_w + gap;
7997 row->enabled_id = n_gui_add_checkbox(ctx, win, "", fx, cy, chk_w, rh, enabled, NULL, NULL);
7998 fx += chk_w + gap;
7999 row->remove_id = n_gui_add_button(ctx, win, "x", fx, cy, rem_w, rh, N_GUI_SHAPE_RECT, _n_gui_kv_remove_clicked, table);
8000 row->active = 1;
8001 if (key) n_gui_textarea_set_text(ctx, row->key_id, key);
8002 if (value) n_gui_textarea_set_text(ctx, row->value_id, value);
8003 if (description) n_gui_textarea_set_text(ctx, row->desc_id, description);
8004 table->nb_rows++;
8005 table->nb_active++;
8006 _n_gui_kv_reposition(table);
8007 return idx;
8008}
8009
8010void n_gui_kvtable_remove_row(N_GUI_KVTABLE* table, int row_index) {
8011 if (!table || row_index < 0 || row_index >= table->nb_rows) return;
8012 if (!table->rows[row_index].active) return;
8013 N_GUI_KV_ROW* row = &table->rows[row_index];
8014 n_gui_set_widget_visible(table->ctx, row->key_id, 0);
8015 n_gui_set_widget_visible(table->ctx, row->value_id, 0);
8016 n_gui_set_widget_visible(table->ctx, row->desc_id, 0);
8017 n_gui_set_widget_visible(table->ctx, row->enabled_id, 0);
8018 n_gui_set_widget_visible(table->ctx, row->remove_id, 0);
8019 row->active = 0;
8020 table->nb_active--;
8021 _n_gui_kv_reposition(table);
8022}
8023
8025 return table ? table->nb_active : 0;
8026}
8027
8029 if (!table || !*table) return;
8030 FreeNoLog(*table);
8031 *table = NULL;
8032}
ALLEGRO_DISPLAY * display
Definition ex_fluid.c:53
void on_link_click(int widget_id, const char *link, void *user_data)
Definition ex_gui.c:140
void on_scroll(int widget_id, double pos, void *user_data)
Definition ex_gui.c:103
static int mode
char * key
#define FreeNoLog(__ptr)
Free Handler without log.
Definition n_common.h:271
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
Definition n_common.h:203
#define __n_assert(__ptr, __ret)
macro to assert things
Definition n_common.h:278
#define Free(__ptr)
Free Handler to get errors.
Definition n_common.h:262
char title[128]
window title
Definition n_gui.h:659
ALLEGRO_BITMAP * thumb_bitmap
optional bitmap for the thumb/handle, normal state (NULL = color theme)
Definition n_gui.h:431
float h
height
Definition n_gui.h:629
float scroll_y
vertical scroll offset for overflowing text (pixels)
Definition n_gui.h:555
float scroll_y
vertical scroll offset for auto-scrollbar (pixels)
Definition n_gui.h:689
int visible
visibility flag
Definition n_gui.h:633
ALLEGRO_BITMAP * panel_bitmap
optional bitmap for the dropdown panel background (NULL = color theme)
Definition n_gui.h:603
ALLEGRO_BITMAP * handle_hover_bitmap
optional bitmap for the handle on hover (NULL = fallback to handle_bitmap)
Definition n_gui.h:352
float button_h
height of each tab button
Definition n_gui.h:1434
int is_open
is dropdown currently open
Definition n_gui.h:517
int content_window_ids[16]
content window IDs (-1 = none)
Definition n_gui.h:1428
int w
bounding box width in pixels
Definition n_gui.h:249
int open_combobox_id
id of the combobox whose dropdown is currently open, or -1
Definition n_gui.h:906
ALLEGRO_COLOR scrollbar_track_color
scrollbar track colour
Definition n_gui.h:754
int max_visible
max visible items in dropdown
Definition n_gui.h:523
int selected_index
currently selected index (-1 = none)
Definition n_gui.h:515
void(* on_remove)(int, void *)
callback on row removal
Definition n_gui.h:1516
size_t items_capacity
allocated capacity
Definition n_gui.h:457
ALLEGRO_COLOR grip_color
grip line colour
Definition n_gui.h:782
float dropdown_border_thickness
dropdown panel border thickness
Definition n_gui.h:856
char * text
text content (dynamically allocated, text_alloc bytes)
Definition n_gui.h:364
int nb_rows
total rows (including removed)
Definition n_gui.h:1506
float slider_handle_edge_offset
handle circle offset from edge
Definition n_gui.h:794
int parent_index
parent node index, or -1 for root
Definition n_gui.h:1458
int shape
shape type: N_GUI_SHAPE_RECT or N_GUI_SHAPE_ROUNDED
Definition n_gui.h:427
float content_h
total content height computed from widgets (internal)
Definition n_gui.h:693
float global_scrollbar_thumb_padding
global thumb inset from track edge
Definition n_gui.h:764
char label[128]
menu label (shown on the button)
Definition n_gui.h:587
size_t items_capacity
allocated capacity
Definition n_gui.h:513
float x
position x on screen
Definition n_gui.h:661
float scrollbar_size
scrollbar track width/height
Definition n_gui.h:746
float norm_y
normalized position y (0.0–1.0 fraction of reference display height)
Definition n_gui.h:719
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:497
float scrollbar_thumb_corner_r
thumb corner radius
Definition n_gui.h:752
int is_dynamic
is this entry dynamic (rebuilt via callback each time menu opens)
Definition n_gui.h:575
float title_padding
padding left of title text
Definition n_gui.h:740
float min_win_h
minimum window height (for resize)
Definition n_gui.h:738
float dropdown_arrow_half_w
horizontal half-extent of the arrow chevron
Definition n_gui.h:854
int toggled
toggle state: 0 = off/unclicked, 1 = on/clicked (only used when toggle_mode=1)
Definition n_gui.h:309
int lbl_enabled
label widget ID for "On" header
Definition n_gui.h:1514
ALLEGRO_BITMAP * item_bg_bitmap
optional bitmap for per-item background, normal state (NULL = color theme)
Definition n_gui.h:467
int scroll_offset
scroll offset in items
Definition n_gui.h:487
float ref_display_h
reference display height at the time normalized values were last captured
Definition n_gui.h:950
float slider_track_corner_r
track corner radius
Definition n_gui.h:788
int y
bounding box y offset from draw origin
Definition n_gui.h:248
float titlebar_h
title bar height
Definition n_gui.h:669
ALLEGRO_BITMAP * handle_active_bitmap
optional bitmap for the handle while dragging (NULL = fallback to handle_bitmap)
Definition n_gui.h:354
void(* on_tab_change)(int, void *)
callback on tab switch (may be NULL)
Definition n_gui.h:1435
float norm_y
normalized position y (fraction of parent window h, used for SCALE resize)
Definition n_gui.h:645
int dropmenu_max_visible
default max visible items for dropmenu panel
Definition n_gui.h:838
float norm_x
normalized position x (0.0–1.0 fraction of reference display width)
Definition n_gui.h:717
float textarea_padding
inner padding
Definition n_gui.h:802
ALLEGRO_BITMAP * bg_bitmap
optional bitmap for the label background (NULL = no background)
Definition n_gui.h:563
float item_height
item height
Definition n_gui.h:601
N_GUI_THEME theme
color theme for this widget
Definition n_gui.h:637
float title_max_w_reserve
pixels reserved right of title for truncation
Definition n_gui.h:742
char text[128]
item display text
Definition n_gui.h:445
int nb_tabs
number of tabs
Definition n_gui.h:1429
int key_sources[8]
widget IDs that can trigger this focused key binding.
Definition n_gui.h:324
float checkbox_label_gap
gap between box and label text
Definition n_gui.h:816
int open_dropmenu_id
id of the dropdown menu whose panel is currently open, or -1
Definition n_gui.h:910
float global_scrollbar_thumb_corner_r
global thumb corner radius
Definition n_gui.h:766
N_GUI_THEME default_theme
default theme (applied to new widgets/windows)
Definition n_gui.h:894
float dpi_scale
DPI scale factor (1.0 = normal, 1.25 = 125%, 2.0 = HiDPI, etc.)
Definition n_gui.h:924
void * user_data
user data for callback
Definition n_gui.h:567
int scroll_from_wheel
set to 1 when scroll was changed by mouse wheel, cleared on key input
Definition n_gui.h:385
int btn_add
button widget ID for "+" add row
Definition n_gui.h:1515
ALLEGRO_BITMAP * bg_bitmap
optional bitmap for the window body background (NULL = color fill)
Definition n_gui.h:701
float scrollbar_thumb_padding
thumb inset from track edge
Definition n_gui.h:750
float w
width
Definition n_gui.h:627
int visible_map[512]
visible row to node index
Definition n_gui.h:1471
ALLEGRO_COLOR text_normal
text normal
Definition n_gui.h:270
int remove_id
button widget ID for the remove action
Definition n_gui.h:1497
float w
window width
Definition n_gui.h:665
int combobox_max_visible
default max visible items for combobox dropdown
Definition n_gui.h:836
ALLEGRO_BITMAP * item_bg_bitmap
optional bitmap for per-item background, normal state (NULL = color theme)
Definition n_gui.h:527
float norm_h
normalized height (fraction of parent window h)
Definition n_gui.h:649
void * user_data
user data for callback
Definition n_gui.h:1474
void(* on_open)(int widget_id, void *user_data)
callback to rebuild dynamic entries each time the menu is opened.
Definition n_gui.h:609
int mouse_b1_prev
previous mouse button 1 state
Definition n_gui.h:904
int listbox_id
listbox widget ID
Definition n_gui.h:1468
float corner_rx
corner radius for rounded shapes
Definition n_gui.h:278
ALLEGRO_COLOR selection_color
text selection highlight colour (semi-transparent recommended)
Definition n_gui.h:282
ALLEGRO_BITMAP * bitmap
optional bitmap for the button (NULL = color theme)
Definition n_gui.h:299
int flags
bitmask of N_GUI_COMBOBOX_* flags
Definition n_gui.h:535
float gui_offset_x
horizontal letterbox offset for virtual canvas
Definition n_gui.h:932
int key_focus_only
if 1, keycode fires only when the button itself or a source widget has focus.
Definition n_gui.h:321
N_GUI_KV_ROW rows[128]
row storage
Definition n_gui.h:1505
int type
widget type (N_GUI_TYPE_*)
Definition n_gui.h:621
int state
current state flags (N_GUI_STATE_*)
Definition n_gui.h:631
float scroll_step
pixels per mouse wheel notch for window auto-scroll
Definition n_gui.h:870
int multiline
0 = single line, 1 = multiline
Definition n_gui.h:372
int key_id
textarea widget ID for the key
Definition n_gui.h:1493
ALLEGRO_BITMAP * bitmap_hover
optional bitmap for hover state
Definition n_gui.h:301
int button_ids[16]
toggle button widget IDs
Definition n_gui.h:1427
int max_visible
max visible items
Definition n_gui.h:599
float gui_bounds_w
total bounding box width of all windows (computed internally)
Definition n_gui.h:920
float x
position x relative to parent window
Definition n_gui.h:623
float global_scrollbar_size
global scrollbar track width/height
Definition n_gui.h:760
float textarea_cursor_width
cursor width in pixels
Definition n_gui.h:804
char text[4096]
text to display
Definition n_gui.h:549
void * user_data
user data for callback
Definition n_gui.h:1436
float norm_h
normalized height (fraction of reference display height, used in SCALE mode)
Definition n_gui.h:723
float min_win_w
minimum window width (for resize)
Definition n_gui.h:736
ALLEGRO_BITMAP * bg_bitmap
optional bitmap for the combobox background (NULL = color theme)
Definition n_gui.h:525
void(* on_change)(int widget_id, double value, void *user_data)
callback on value change
Definition n_gui.h:356
float item_height
item height in pixels
Definition n_gui.h:489
float drag_ox
drag offset x (internal)
Definition n_gui.h:681
void(* on_click)(int widget_id, void *user_data)
callback on click (widget_id passed)
Definition n_gui.h:326
float row_height
height of each row in pixels
Definition n_gui.h:1508
int expanded
1 if expanded
Definition n_gui.h:1459
N_GUI_LISTITEM * items
dynamic array of items
Definition n_gui.h:509
char label[128]
display text
Definition n_gui.h:1457
N_GUI_THEME theme
color theme for this window chrome
Definition n_gui.h:677
int id
unique widget id
Definition n_gui.h:619
int desc_id
textarea widget ID for the description
Definition n_gui.h:1495
int next_window_id
next window id to assign
Definition n_gui.h:890
float item_selection_inset
item highlight inset from left/right edges
Definition n_gui.h:842
ALLEGRO_COLOR border_normal
border normal
Definition n_gui.h:264
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:579
int has_children
1 if node has children
Definition n_gui.h:1461
void * user_data
user data for callback
Definition n_gui.h:581
int scroll_offset
scroll offset
Definition n_gui.h:597
ALLEGRO_COLOR text_hover
text hover
Definition n_gui.h:272
float dropdown_arrow_half_h
vertical half-extent of the arrow chevron
Definition n_gui.h:852
int z_value
z-value for N_GUI_ZORDER_FIXED mode (lower = behind, higher = on top)
Definition n_gui.h:699
ALLEGRO_BITMAP * item_bg_bitmap
optional bitmap for per-item background, normal state (NULL = color theme)
Definition n_gui.h:493
double cursor_time
timestamp of last cursor activity (for blink reset)
Definition n_gui.h:387
size_t entries_capacity
allocated capacity
Definition n_gui.h:593
int mouse_y
mouse state tracking
Definition n_gui.h:900
float grip_line_thickness
grip line thickness
Definition n_gui.h:780
int lbl_key
label widget ID for "Key" header
Definition n_gui.h:1511
size_t sel_end
selection end position (tracks cursor during selection)
Definition n_gui.h:379
int resize_policy
per-window resize policy: N_GUI_WIN_RESIZE_NONE, _MOVE, or _SCALE
Definition n_gui.h:715
size_t nb_items
number of items
Definition n_gui.h:481
void * user_data
user data for callback
Definition n_gui.h:533
float padding
horizontal padding
Definition n_gui.h:1509
void(* on_select)(int, void *)
selection callback
Definition n_gui.h:1473
int shape
shape type: N_GUI_SHAPE_RECT, N_GUI_SHAPE_ROUNDED, N_GUI_SHAPE_BITMAP
Definition n_gui.h:305
int z_order
z-order mode (N_GUI_ZORDER_NORMAL, _ALWAYS_ON_TOP, _ALWAYS_BEHIND, _FIXED)
Definition n_gui.h:697
int tag
user-defined tag for the entry (e.g.
Definition n_gui.h:577
int mouse_b1
mouse button 1 state
Definition n_gui.h:902
float button_w
width of each tab button
Definition n_gui.h:1433
float display_w
display/viewport width (set via n_gui_set_display_size)
Definition n_gui.h:912
N_GUI_LISTITEM * items
dynamic array of items
Definition n_gui.h:479
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:409
int h
bounding box height in pixels
Definition n_gui.h:250
float grip_size
grip area size
Definition n_gui.h:778
float radio_label_gap
gap between circle and label text
Definition n_gui.h:828
N_GUI_CTX * ctx
GUI context.
Definition n_gui.h:1467
ALLEGRO_DISPLAY * display
display pointer for clipboard operations (set via n_gui_set_display)
Definition n_gui.h:942
float norm_w
normalized width (fraction of reference display width, used in SCALE mode)
Definition n_gui.h:721
float content_w
total content width computed from widgets (internal)
Definition n_gui.h:695
void * user_data
user data for this node
Definition n_gui.h:1462
float norm_x
normalized position x (fraction of parent window w, used for SCALE resize)
Definition n_gui.h:643
ALLEGRO_COLOR global_scrollbar_thumb_color
global scrollbar thumb colour
Definition n_gui.h:772
int selected_label_id
id of the label widget with the most recent text selection, or -1
Definition n_gui.h:944
float radio_inner_offset
inner filled circle shrink from outer
Definition n_gui.h:826
float min_w
minimum width
Definition n_gui.h:685
int selected_index
currently selected index (-1 = none)
Definition n_gui.h:485
ALLEGRO_BITMAP * track_bitmap
optional bitmap for the track/rail background (NULL = color theme)
Definition n_gui.h:346
int mouse_x
mouse state tracking
Definition n_gui.h:898
ALLEGRO_COLOR scrollbar_thumb_color
scrollbar thumb colour
Definition n_gui.h:756
int active_tab
currently active tab index
Definition n_gui.h:1430
float checkbox_max_size
maximum box size
Definition n_gui.h:810
size_t nb_entries
number of entries
Definition n_gui.h:591
float checkbox_mark_thickness
checkmark line thickness
Definition n_gui.h:814
int sel_start
selection start byte offset (-1 = no selection)
Definition n_gui.h:557
ALLEGRO_BITMAP * bitmap
bitmap to display (not owned, not freed)
Definition n_gui.h:541
ALLEGRO_BITMAP * fill_bitmap
optional bitmap for the filled portion of the track (NULL = color theme)
Definition n_gui.h:348
float header_height
height of the column header row
Definition n_gui.h:1510
float global_scroll_step
pixels per mouse wheel notch for global scroll
Definition n_gui.h:872
float h
window height
Definition n_gui.h:667
float item_height
item height in dropdown
Definition n_gui.h:521
void * user_data
user data for callback
Definition n_gui.h:413
double scroll_pos
current scroll position
Definition n_gui.h:425
void(* on_change)(int widget_id, const char *text, void *user_data)
callback on text change
Definition n_gui.h:393
ALLEGRO_BITMAP * item_selected_bitmap
optional bitmap for per-item background, selected/highlighted (NULL = color theme)
Definition n_gui.h:529
void * user_data
user data for callback
Definition n_gui.h:358
float autofit_origin_y
original insertion point y for N_GUI_AUTOFIT_CENTER (set by n_gui_window_set_autofit)
Definition n_gui.h:713
int scroll_offset
scroll offset in items
Definition n_gui.h:461
int selected
selection state (for listbox)
Definition n_gui.h:447
ALLEGRO_BITMAP * item_hover_bitmap
optional bitmap for the hovered entry background (NULL = color theme)
Definition n_gui.h:605
float virtual_h
virtual canvas height (0 = disabled / identity transform)
Definition n_gui.h:928
N_GUI_CTX * ctx
GUI context.
Definition n_gui.h:1503
float global_scroll_x
global horizontal scroll offset (when GUI exceeds display)
Definition n_gui.h:916
float checkbox_mark_margin
checkmark inset from box edge
Definition n_gui.h:812
ALLEGRO_BITMAP * track_bitmap
optional bitmap for the scrollbar track background (NULL = color theme)
Definition n_gui.h:429
float checkbox_label_offset
horizontal offset from box edge to label text
Definition n_gui.h:818
ALLEGRO_BITMAP * titlebar_bitmap
optional bitmap for the titlebar background (NULL = color fill)
Definition n_gui.h:703
int scroll_offset
scroll offset in dropdown
Definition n_gui.h:519
N_GUI_CTX * ctx
GUI context.
Definition n_gui.h:1425
void * data
widget-specific data (union via void pointer)
Definition n_gui.h:641
double value
current value (always snapped to step)
Definition n_gui.h:338
N_GUI_DROPMENU_ENTRY * entries
dynamic array of entries
Definition n_gui.h:589
float drag_oy
drag offset y (internal)
Definition n_gui.h:683
float slider_track_border_thickness
track outline thickness
Definition n_gui.h:790
int nb_active
active row count
Definition n_gui.h:1507
ALLEGRO_COLOR link_color_normal
link colour (normal)
Definition n_gui.h:864
float slider_handle_min_r
minimum handle circle radius
Definition n_gui.h:792
int scale_mode
scale mode: N_GUI_IMAGE_FIT, N_GUI_IMAGE_STRETCH, N_GUI_IMAGE_CENTER
Definition n_gui.h:543
ALLEGRO_BITMAP * item_selected_bitmap
optional bitmap for per-item background, selected/highlighted (NULL = color theme)
Definition n_gui.h:495
ALLEGRO_BITMAP * bg_bitmap
optional bitmap for the radiolist background (NULL = color theme)
Definition n_gui.h:491
N_GUI_TREE_NODE nodes[512]
node storage
Definition n_gui.h:1469
ALLEGRO_BITMAP * box_bitmap
optional bitmap for the checkbox square, unchecked state (NULL = color theme)
Definition n_gui.h:405
void * on_open_user_data
user data for on_open callback
Definition n_gui.h:611
float y
y origin of tab button row
Definition n_gui.h:1432
float dropdown_arrow_reserve
horizontal space reserved for the arrow on the right
Definition n_gui.h:848
float y
position y relative to parent window
Definition n_gui.h:625
int lbl_desc
label widget ID for "Description" header
Definition n_gui.h:1513
int orientation
orientation: N_GUI_SLIDER_H or N_GUI_SLIDER_V
Definition n_gui.h:344
float global_scrollbar_border_thickness
global thumb border thickness
Definition n_gui.h:768
int state
state flags (N_GUI_WIN_*)
Definition n_gui.h:671
int flags
feature flags (N_GUI_WIN_AUTO_SCROLLBAR, N_GUI_WIN_RESIZABLE, etc.)
Definition n_gui.h:673
void * user_data
user data for callback
Definition n_gui.h:439
int active
1 if active, 0 if removed
Definition n_gui.h:1498
double max_val
maximum value
Definition n_gui.h:336
HASH_TABLE * widgets_by_id
hash table for fast widget lookup by id
Definition n_gui.h:886
float border_thickness
border thickness
Definition n_gui.h:276
float link_underline_thickness
link underline thickness
Definition n_gui.h:862
ALLEGRO_BITMAP * box_checked_bitmap
optional bitmap for the checkbox square, checked state (NULL = color theme)
Definition n_gui.h:407
int autofit_flags
bitmask of N_GUI_AUTOFIT_* flags (0 = no auto-fitting)
Definition n_gui.h:707
N_GUI_STYLE style
configurable style (sizes, colours, paddings)
Definition n_gui.h:940
ALLEGRO_FONT * font
font for the title bar
Definition n_gui.h:679
void * user_data
user data for callback
Definition n_gui.h:395
ALLEGRO_COLOR text_active
text active
Definition n_gui.h:274
float ref_display_w
reference display width at the time normalized values were last captured
Definition n_gui.h:948
int nb_nodes
number of nodes
Definition n_gui.h:1470
size_t items_capacity
allocated capacity
Definition n_gui.h:483
int parent_window_id
parent window for tab buttons
Definition n_gui.h:1426
double content_size
total content size
Definition n_gui.h:421
float autofit_origin_x
original insertion point x for N_GUI_AUTOFIT_CENTER (set by n_gui_window_set_autofit)
Definition n_gui.h:711
void * user_data
user data for callback
Definition n_gui.h:473
double step
step increment (0 is treated as 1).
Definition n_gui.h:340
float global_scroll_y
global vertical scroll offset (when GUI exceeds display)
Definition n_gui.h:918
int scrollbar_drag_widget_id
id of the widget whose scrollbar is being dragged, or -1
Definition n_gui.h:908
float min_h
minimum height
Definition n_gui.h:687
float listbox_default_item_height
default item height for listbox
Definition n_gui.h:832
ALLEGRO_COLOR global_scrollbar_thumb_border_color
global scrollbar thumb border colour
Definition n_gui.h:774
ALLEGRO_COLOR border_active
border active
Definition n_gui.h:268
float scroll_x
horizontal scroll offset for single-line text (pixels)
Definition n_gui.h:383
float label_padding
horizontal padding each side
Definition n_gui.h:860
float titlebar_h
title bar height
Definition n_gui.h:734
float gui_offset_y
vertical letterbox offset for virtual canvas
Definition n_gui.h:934
ALLEGRO_BITMAP * item_selected_bitmap
optional bitmap for per-item background, selected/highlighted (NULL = color theme)
Definition n_gui.h:469
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:471
float item_height
item height in pixels
Definition n_gui.h:463
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:531
int focused_widget_id
id of the widget that currently has focus, or -1
Definition n_gui.h:896
int id
unique window id
Definition n_gui.h:657
int global_vscroll_drag
1 if global scrollbar vertical thumb is being dragged
Definition n_gui.h:936
int sel_dragging
1 if mouse is actively dragging to select text
Definition n_gui.h:561
int nb_visible
number of visible rows
Definition n_gui.h:1472
ALLEGRO_COLOR global_scrollbar_track_color
global scrollbar track colour
Definition n_gui.h:770
float item_height_pad
min padding added to font height for item height
Definition n_gui.h:844
float display_h
display/viewport height
Definition n_gui.h:914
N_GUI_LISTITEM * items
dynamic array of items
Definition n_gui.h:453
void * user_data
user data for callback
Definition n_gui.h:499
float corner_ry
corner radius Y for rounded shapes
Definition n_gui.h:280
int checked
checked state
Definition n_gui.h:403
int sel_end
selection end byte offset (-1 = no selection)
Definition n_gui.h:559
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:705
size_t text_alloc
allocated buffer size (char_limit + 1)
Definition n_gui.h:368
ALLEGRO_COLOR bg_normal
background normal
Definition n_gui.h:258
ALLEGRO_BITMAP * handle_bitmap
optional bitmap for the draggable handle, normal state (NULL = color theme)
Definition n_gui.h:350
ALLEGRO_FONT * font
font used by this widget (NULL = context default)
Definition n_gui.h:639
float norm_w
normalized width (fraction of parent window w)
Definition n_gui.h:647
int resize_mode
context resize mode: N_GUI_RESIZE_VIRTUAL or N_GUI_RESIZE_ADAPTIVE
Definition n_gui.h:946
float virtual_w
virtual canvas width (0 = disabled / identity transform)
Definition n_gui.h:926
float radiolist_default_item_height
default item height for radiolist
Definition n_gui.h:834
char label[128]
label displayed next to the checkbox
Definition n_gui.h:401
float slider_value_label_offset
gap between slider end and value label
Definition n_gui.h:798
size_t nb_items
number of items
Definition n_gui.h:455
int depth
depth in tree (0 = root)
Definition n_gui.h:1460
ALLEGRO_COLOR bg_active
background active/pressed
Definition n_gui.h:262
char mask_char
mask character for password fields (0 = no masking, e.g.
Definition n_gui.h:391
int global_hscroll_drag
1 if global scrollbar horizontal thumb is being dragged
Definition n_gui.h:938
LIST * windows
ordered list of N_GUI_WINDOW* (back to front)
Definition n_gui.h:884
char text[128]
display text
Definition n_gui.h:573
int scroll_y
scroll offset for long text
Definition n_gui.h:381
void(* on_scroll)(int widget_id, double scroll_pos, void *user_data)
callback on scroll
Definition n_gui.h:437
int x
bounding box x offset from draw origin
Definition n_gui.h:247
float gui_bounds_h
total bounding box height of all windows (computed internally)
Definition n_gui.h:922
double min_val
minimum value
Definition n_gui.h:334
float textarea_cursor_blink_period
cursor blink period in seconds (full cycle on+off)
Definition n_gui.h:806
int lbl_value
label widget ID for "Value" header
Definition n_gui.h:1512
void(* on_toggle)(int widget_id, int checked, void *user_data)
callback on toggle
Definition n_gui.h:411
ALLEGRO_COLOR bg_hover
background hover
Definition n_gui.h:260
size_t sel_start
selection anchor position (where shift-click/shift-arrow started).
Definition n_gui.h:377
int window_id
parent window
Definition n_gui.h:1504
float dropdown_arrow_thickness
arrow stroke thickness
Definition n_gui.h:850
void * user_data
user data for callback
Definition n_gui.h:1517
ALLEGRO_COLOR link_color_hover
link colour (hover)
Definition n_gui.h:866
float radio_circle_min_r
minimum outer circle radius
Definition n_gui.h:822
ALLEGRO_BITMAP * bg_bitmap
optional bitmap for the listbox background (NULL = color theme)
Definition n_gui.h:465
float item_text_padding
text padding inside list/combo/dropmenu items
Definition n_gui.h:840
int next_widget_id
next widget id to assign
Definition n_gui.h:888
int enabled_id
checkbox widget ID for the enabled toggle
Definition n_gui.h:1496
float autofit_border
padding/border around content for auto-fit (pixels, applied on each side)
Definition n_gui.h:709
float gui_scale
computed uniform scale factor for virtual canvas
Definition n_gui.h:930
size_t cursor_pos
cursor position in text
Definition n_gui.h:374
void * user_data
user data for callback
Definition n_gui.h:328
int mode
mode: N_GUI_SLIDER_VALUE or N_GUI_SLIDER_PERCENT
Definition n_gui.h:342
size_t char_limit
maximum character limit (0 = N_GUI_TEXT_MAX)
Definition n_gui.h:370
int toggle_mode
toggle mode: 0 = momentary (default), 1 = toggle (stays clicked/unclicked)
Definition n_gui.h:307
int is_open
is the menu currently open
Definition n_gui.h:595
ALLEGRO_COLOR border_hover
border hover
Definition n_gui.h:266
float scrollbar_thumb_min
minimum thumb dimension
Definition n_gui.h:748
float x
x origin of first tab button
Definition n_gui.h:1431
ALLEGRO_BITMAP * thumb_hover_bitmap
optional bitmap for the thumb on hover (NULL = fallback to thumb_bitmap)
Definition n_gui.h:433
int value_id
textarea widget ID for the value
Definition n_gui.h:1494
char label[128]
label displayed on the button
Definition n_gui.h:297
ALLEGRO_BITMAP * bg_bitmap
optional bitmap for the text area background (NULL = color theme)
Definition n_gui.h:389
int align
text alignment: N_GUI_ALIGN_LEFT, N_GUI_ALIGN_CENTER, N_GUI_ALIGN_RIGHT
Definition n_gui.h:553
size_t text_len
current text length
Definition n_gui.h:366
char link[4096]
optional hyperlink URL (empty string = no link)
Definition n_gui.h:551
int orientation
orientation: N_GUI_SCROLLBAR_H or N_GUI_SCROLLBAR_V
Definition n_gui.h:419
float global_scrollbar_thumb_min
global minimum thumb dimension
Definition n_gui.h:762
double viewport_size
visible viewport size
Definition n_gui.h:423
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:565
ALLEGRO_FONT * default_font
default font (must be set before adding widgets)
Definition n_gui.h:892
float combobox_max_dropdown_width
maximum dropdown width for N_GUI_COMBOBOX_AUTO_WIDTH (0 = display width)
Definition n_gui.h:876
int selection_mode
selection mode: N_GUI_SELECT_NONE, N_GUI_SELECT_SINGLE, N_GUI_SELECT_MULTIPLE
Definition n_gui.h:459
size_t nb_items
number of items
Definition n_gui.h:511
int key_modifiers
required modifier key flags for the keybind (0 = no modifier requirement, matches any modifier state ...
Definition n_gui.h:318
LIST * widgets
list of N_GUI_WIDGET* contained in this window
Definition n_gui.h:675
float y
position y on screen
Definition n_gui.h:663
ALLEGRO_BITMAP * bitmap_active
optional bitmap for active/pressed state
Definition n_gui.h:303
int enabled
enabled flag (1 = enabled, 0 = disabled: drawn dimmed and ignores input)
Definition n_gui.h:635
float radio_circle_border_thickness
outer circle border thickness
Definition n_gui.h:824
int keycode
bound keyboard keycode (0 = none).
Definition n_gui.h:312
ALLEGRO_BITMAP * thumb_active_bitmap
optional bitmap for the thumb while dragging (NULL = fallback to thumb_bitmap)
Definition n_gui.h:435
float slider_track_size
track width (vertical) or height (horizontal)
Definition n_gui.h:786
float scroll_x
horizontal scroll offset for auto-scrollbar (pixels)
Definition n_gui.h:691
float slider_handle_border_thickness
handle border thickness
Definition n_gui.h:796
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:5176
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:2522
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:7536
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:2001
void n_gui_dropmenu_clear(N_GUI_CTX *ctx, int widget_id)
Remove all entries from a dropdown menu.
Definition n_gui.c:2355
#define N_GUI_STATE_HOVER
mouse is hovering the widget
Definition n_gui.h:168
void n_gui_update_transform(N_GUI_CTX *ctx)
Recalculate scale and offset from virtual canvas to physical display.
Definition n_gui.c:4992
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:1086
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:5234
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:825
#define N_GUI_TREE_MAX
maximum number of nodes in a tree view
Definition n_gui.h:1453
#define N_GUI_ZORDER_NORMAL
default z-order: window participates in normal raise/lower ordering
Definition n_gui.h:230
#define N_GUI_ALIGN_LEFT
left aligned text
Definition n_gui.h:156
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:745
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:1160
void n_gui_combobox_set_flags(N_GUI_CTX *ctx, int widget_id, int flags)
Set combobox feature flags.
Definition n_gui.c:1446
void n_gui_window_apply_autofit(N_GUI_CTX *ctx, int window_id)
Trigger auto-fit recalculation for a window.
Definition n_gui.c:910
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:1752
#define N_GUI_SLIDER_PERCENT
slider uses 0-100 percentage
Definition n_gui.h:124
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:1808
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:7928
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:1200
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:5192
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:2397
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:1575
int n_gui_wants_mouse(N_GUI_CTX *ctx)
Check if the mouse is currently over any open GUI window.
Definition n_gui.c:7344
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:2380
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:2028
#define N_GUI_WIN_VSCROLL_DRAG
window vertical auto-scrollbar is being dragged
Definition n_gui.h:186
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:763
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:2178
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:1336
#define N_GUI_COMBOBOX_AUTO_WIDTH
dropdown panel expands to fit the longest item text
Definition n_gui.h:504
#define N_GUI_TYPE_LABEL
widget type: static text label (with optional hyperlink)
Definition n_gui.h:108
#define N_GUI_WIN_RESIZE_SCALE
reposition AND resize proportionally, child widgets scale too
Definition n_gui.h:212
#define N_GUI_TYPE_CHECKBOX
widget type: checkbox
Definition n_gui.h:96
#define N_GUI_KV_MAX
maximum number of rows in a KV table
Definition n_gui.h:1489
#define N_GUI_TYPE_DROPMENU
widget type: dropdown menu with static and dynamic entries
Definition n_gui.h:110
int n_gui_process_event(N_GUI_CTX *ctx, ALLEGRO_EVENT event)
Process an allegro event through the GUI system.
Definition n_gui.c:5767
int n_gui_window_get_flags(N_GUI_CTX *ctx, int window_id)
Get feature flags of a window.
Definition n_gui.c:852
void n_gui_minimize_window(N_GUI_CTX *ctx, int window_id)
Minimise a window (show title bar only)
Definition n_gui.c:662
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:1497
void n_gui_set_widget_enabled(N_GUI_CTX *ctx, int widget_id, int enabled)
Enable or disable a widget.
Definition n_gui.c:1592
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:1371
#define N_GUI_WIN_FIXED_POSITION
disable window dragging (default:enable)
Definition n_gui.h:196
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:7780
void n_gui_close_window(N_GUI_CTX *ctx, int window_id)
Close (hide) a window.
Definition n_gui.c:646
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:2490
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:2198
#define N_GUI_RESIZE_VIRTUAL
fixed virtual canvas with uniform scaling (default/existing behavior)
Definition n_gui.h:202
void n_gui_kvtable_free(N_GUI_KVTABLE **table)
free a KV table (does not destroy the N_GUI widgets)
Definition n_gui.c:8028
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:5081
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:1139
void n_gui_radiolist_clear(N_GUI_CTX *ctx, int widget_id)
remove all items from a radiolist widget
Definition n_gui.c:2049
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:1990
#define N_GUI_SELECT_NONE
no selection allowed (display only)
Definition n_gui.h:140
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:817
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:1667
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:1860
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:4974
float n_gui_get_dpi_scale(const N_GUI_CTX *ctx)
Get current DPI scale factor.
Definition n_gui.c:5210
N_GUI_THEME n_gui_default_theme(void)
Build a sensible default colour theme.
Definition n_gui.c:294
#define N_GUI_WIN_RESIZE_NONE
no adaptation: absolute position and size unchanged
Definition n_gui.h:208
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:779
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:2065
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:7757
#define N_GUI_TYPE_SLIDER
widget type: slider
Definition n_gui.h:92
#define N_GUI_WIN_OPEN
window is visible
Definition n_gui.h:178
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:7413
#define N_GUI_SELECT_SINGLE
single item selection
Definition n_gui.h:142
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:2109
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:862
void n_gui_set_focus(N_GUI_CTX *ctx, int widget_id)
Set keyboard focus to a specific widget.
Definition n_gui.c:1613
void n_gui_tree_rebuild(N_GUI_TREE *tree)
rebuild the listbox to reflect current tree state
Definition n_gui.c:7855
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:2832
#define N_GUI_SELECT_MULTIPLE
multiple item selection
Definition n_gui.h:144
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:2430
#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:198
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:1406
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:1097
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:7762
void n_gui_slider_set_step(N_GUI_CTX *ctx, int widget_id, double step)
set slider step increment.
Definition n_gui.c:1719
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:1070
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:8010
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:1538
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:2477
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:1954
#define N_GUI_AUTOFIT_EXPAND_LEFT
expand leftward instead of rightward when adjusting width
Definition n_gui.h:222
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:1468
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:2553
void n_gui_draw(N_GUI_CTX *ctx)
Draw all visible windows and their widgets.
Definition n_gui.c:4800
#define N_GUI_WIN_MINIMISED
window is minimised (title bar only)
Definition n_gui.h:180
#define N_GUI_SLIDER_H
horizontal slider (default)
Definition n_gui.h:128
#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:226
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:7742
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:2412
int n_gui_window_get_zorder(N_GUI_CTX *ctx, int window_id)
Get window z-order mode.
Definition n_gui.c:796
void n_gui_tree_free(N_GUI_TREE **tree)
free a tree view (does not destroy the N_GUI listbox)
Definition n_gui.c:7878
#define N_GUI_TYPE_IMAGE
widget type: image display
Definition n_gui.h:106
int n_gui_window_get_resize_policy(N_GUI_CTX *ctx, int window_id)
Get per-window resize policy.
Definition n_gui.c:5094
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:1793
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:2301
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:5123
int n_gui_is_widget_enabled(N_GUI_CTX *ctx, int widget_id)
Check if a widget is enabled.
Definition n_gui.c:1601
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:1129
int n_gui_window_is_open(N_GUI_CTX *ctx, int window_id)
Check if a window is currently visible.
Definition n_gui.c:834
void n_gui_set_dpi_scale(N_GUI_CTX *ctx, float scale)
Set DPI scale factor manually (default 1.0)
Definition n_gui.c:5200
#define N_GUI_TYPE_RADIOLIST
widget type: radio list (single select radio buttons)
Definition n_gui.h:102
#define N_GUI_SLIDER_V
vertical slider
Definition n_gui.h:130
int n_gui_get_resize_mode(N_GUI_CTX *ctx)
Get current resize mode.
Definition n_gui.c:5071
N_GUI_WIDGET * n_gui_get_widget(N_GUI_CTX *ctx, int widget_id)
Get a widget pointer by id.
Definition n_gui.c:1561
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:2278
#define N_GUI_ZORDER_ALWAYS_BEHIND
always drawn behind normal windows, cannot be raised above them
Definition n_gui.h:234
#define N_GUI_IMAGE_STRETCH
stretch to fill bounds
Definition n_gui.h:150
N_GUI_WINDOW * n_gui_get_window(N_GUI_CTX *ctx, int window_id)
Get a window pointer by id.
Definition n_gui.c:637
#define N_GUI_ZORDER_ALWAYS_ON_TOP
always drawn on top of normal windows, cannot be lowered behind them
Definition n_gui.h:232
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:2538
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:581
N_GUI_CTX * n_gui_new_ctx(ALLEGRO_FONT *default_font)
Create a new GUI context.
Definition n_gui.c:466
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:1699
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:1937
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:2506
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:7799
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:1042
#define N_GUI_IMAGE_CENTER
draw at original size, centered
Definition n_gui.h:152
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:5042
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:1110
#define N_GUI_ALIGN_CENTER
center aligned text
Definition n_gui.h:158
void n_gui_tree_toggle_expand(N_GUI_TREE *tree, int node_index)
toggle expand/collapse of a node
Definition n_gui.c:7848
#define N_GUI_TYPE_SCROLLBAR
widget type: scrollbar
Definition n_gui.h:98
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:2849
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:2463
void n_gui_listbox_clear(N_GUI_CTX *ctx, int widget_id)
remove all items from a listbox widget
Definition n_gui.c:1907
#define N_GUI_TYPE_BUTTON
widget type: button
Definition n_gui.h:90
#define N_GUI_TYPE_COMBOBOX
widget type: combo box (dropdown)
Definition n_gui.h:104
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:1293
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:58
int n_gui_kvtable_get_count(N_GUI_KVTABLE *table)
get the number of active rows
Definition n_gui.c:8024
void n_gui_destroy_ctx(N_GUI_CTX **ctx)
Destroy a GUI context and all its windows/widgets.
Definition n_gui.c:523
#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:241
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:1738
int n_gui_tab_get_active(N_GUI_TAB_PANEL *panel)
get the active tab index
Definition n_gui.c:7776
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:7972
#define N_GUI_WIN_HSCROLL_DRAG
window horizontal auto-scrollbar is being dragged
Definition n_gui.h:188
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:2447
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:2008
#define N_GUI_TEXT_MAX
maximum length for textarea content
Definition n_gui.h:86
#define N_GUI_STATE_IDLE
widget is idle / normal state
Definition n_gui.h:166
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:2810
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:2910
void n_gui_open_window(N_GUI_CTX *ctx, int window_id)
Open (show) a window.
Definition n_gui.c:654
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:806
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:2366
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:7827
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:1922
int n_gui_checkbox_is_checked(N_GUI_CTX *ctx, int widget_id)
check if a checkbox widget is checked
Definition n_gui.c:1779
#define N_GUI_WIN_RESIZE_MOVE
reposition proportionally, keep pixel size
Definition n_gui.h:210
#define N_GUI_STATE_ACTIVE
widget is being pressed / dragged
Definition n_gui.h:170
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:2131
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:2163
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:1681
#define N_GUI_AUTOFIT_EXPAND_UP
expand upward instead of downward when adjusting height
Definition n_gui.h:224
#define N_GUI_SHAPE_BITMAP
bitmap-based rendering
Definition n_gui.h:118
#define N_GUI_SHAPE_ROUNDED
rounded rectangle shape
Definition n_gui.h:116
#define N_GUI_ALIGN_RIGHT
right aligned text
Definition n_gui.h:160
#define N_GUI_AUTOFIT_H
auto-adjust window height to content
Definition n_gui.h:218
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:2079
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:843
#define N_GUI_KEY_MOD_MASK
mask of supported modifier flags for button keybind matching
Definition n_gui.h:287
#define N_GUI_ALIGN_JUSTIFIED
justified text (spread words to fill width)
Definition n_gui.h:162
void n_gui_set_widget_visible(N_GUI_CTX *ctx, int widget_id, int visible)
Show or hide a widget.
Definition n_gui.c:1583
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:7720
#define N_GUI_ID_MAX
maximum length for widget id/name strings
Definition n_gui.h:84
N_GUI_STYLE n_gui_default_style(void)
Build sensible default style (all configurable sizes, colours, paddings)
Definition n_gui.c:365
#define N_GUI_TAB_MAX
maximum number of tabs in a single tab panel
Definition n_gui.h:1421
#define N_GUI_AUTOFIT_W
auto-adjust window width to content
Definition n_gui.h:216
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:1839
#define N_GUI_WIN_AUTO_SCROLLBAR
enable automatic scrollbars when content exceeds window size
Definition n_gui.h:192
#define N_GUI_WIN_RESIZING
window is being resized
Definition n_gui.h:184
#define N_GUI_RESIZE_ADAPTIVE
virtual size tracks display; windows adapt per their resize_policy
Definition n_gui.h:204
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:1214
#define N_GUI_TYPE_LISTBOX
widget type: listbox (selectable list)
Definition n_gui.h:100
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:1972
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:5107
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:1259
void n_gui_combobox_clear(N_GUI_CTX *ctx, int widget_id)
remove all items from a combobox widget
Definition n_gui.c:2096
#define N_GUI_SHAPE_RECT
rectangle shape (default)
Definition n_gui.h:114
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:2145
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:1883
#define N_GUI_SCROLLBAR_V
vertical scrollbar
Definition n_gui.h:136
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:328
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:2237
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:5019
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:1001
#define N_GUI_KEY_SOURCES_MAX
maximum number of source widgets for a focused key binding
Definition n_gui.h:290
#define N_GUI_WIN_RESIZABLE
enable user-resizable window with a drag handle at bottom-right
Definition n_gui.h:194
#define N_GUI_STATE_FOCUSED
widget has keyboard focus
Definition n_gui.h:172
#define N_GUI_WIN_DRAGGING
window is being dragged
Definition n_gui.h:182
#define N_GUI_TYPE_TEXTAREA
widget type: text area
Definition n_gui.h:94
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:891
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:1822
void n_gui_dropmenu_clear_dynamic(N_GUI_CTX *ctx, int widget_id)
Remove all dynamic entries (keep static ones)
Definition n_gui.c:2326
button specific data
Definition n_gui.h:295
checkbox specific data
Definition n_gui.h:399
combo box specific data
Definition n_gui.h:507
The top-level GUI context that holds all windows.
Definition n_gui.h:882
dropdown menu specific data
Definition n_gui.h:585
A single entry in a dropdown menu (static or dynamic)
Definition n_gui.h:571
image widget specific data
Definition n_gui.h:539
a single row in the KV table
Definition n_gui.h:1492
key-value table built from textareas, checkboxes, and buttons
Definition n_gui.h:1502
static text label specific data
Definition n_gui.h:547
listbox specific data
Definition n_gui.h:451
a single item in a list/radio/combo widget
Definition n_gui.h:443
radio list specific data
Definition n_gui.h:477
scrollbar specific data
Definition n_gui.h:417
slider specific data
Definition n_gui.h:332
Global style holding every configurable layout constant.
Definition n_gui.h:731
tab panel built from toggle buttons with content window management
Definition n_gui.h:1424
bounding box dimensions returned by n_gui_get_text_dims()
Definition n_gui.h:246
text area specific data
Definition n_gui.h:362
Color theme for a widget.
Definition n_gui.h:256
tree view built from an N_GUI listbox
Definition n_gui.h:1466
a single node in the tree view
Definition n_gui.h:1456
A single GUI widget.
Definition n_gui.h:617
A pseudo window that contains widgets.
Definition n_gui.h:655
int ht_get_ptr(HASH_TABLE *table, const char *key, void **val)
get pointer at 'key' from 'table'
Definition n_hash.c:2100
int destroy_ht(HASH_TABLE **table)
empty a table and destroy it
Definition n_hash.c:2234
HASH_TABLE * new_ht(size_t size)
Create a hash table with the given size.
Definition n_hash.c:2001
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:2154
LIST_NODE * end
pointer to the end of the list
Definition n_list.h:67
void * ptr
void pointer to store
Definition n_list.h:45
LIST_NODE * start
pointer to the start of the list
Definition n_list.h:65
size_t nb_items
number of item currently in the list
Definition n_list.h:60
void(* destroy_func)(void *ptr)
pointer to destructor function if any, else NULL
Definition n_list.h:48
#define UNLIMITED_LIST_ITEMS
flag to pass to new_generic_list for an unlimited number of item in the list.
Definition n_list.h:72
int list_push(LIST *list, void *ptr, void(*destructor)(void *ptr))
Add a pointer to the end of the list.
Definition n_list.c:227
#define list_foreach(__ITEM_, __LIST_)
ForEach macro helper, safe for node removal during iteration.
Definition n_list.h:88
int list_unshift(LIST *list, void *ptr, void(*destructor)(void *ptr))
Add a pointer at the start of the list.
Definition n_list.c:316
int list_destroy(LIST **list)
Empty and Free a list container.
Definition n_list.c:547
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:75
LIST * new_generic_list(size_t max_items)
Initialiaze a generic list container to max_items pointers.
Definition n_list.c:36
Structure of a generic list node.
Definition n_list.h:43
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:88
#define LOG_ERR
error conditions
Definition n_log.h:75
#define LOG_WARNING
warning conditions
Definition n_log.h:77
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:3039
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:3423
static float _text_w(ALLEGRO_FONT *font, const char *text)
helper: get the advance width of text.
Definition n_gui.c:105
static void _register_widget(N_GUI_CTX *ctx, N_GUI_WIDGET *w)
helper: register a widget in the hash table
Definition n_gui.c:239
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:7394
static N_GUI_WINDOW * _find_focused_window(N_GUI_CTX *ctx)
helper: find the window containing a focused widget
Definition n_gui.c:77
static ALLEGRO_COLOR _bg_for_state(const N_GUI_THEME *t, int state)
helper: pick colour based on widget state
Definition n_gui.c:3221
static void _n_gui_tree_listbox_selected(int widget_id, int index, int selected, void *user_data)
Definition n_gui.c:7788
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:2656
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:3544
static void _normalize_crlf(char *s)
helper: strip CR from a string in-place, normalizing CRLF to LF.
Definition n_gui.c:92
static int _is_focusable_type(int type)
helper: check if a widget type is focusable via Tab navigation
Definition n_gui.c:65
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:3301
static void _draw_image(N_GUI_WIDGET *wgt, float ox, float oy)
draw an image widget
Definition n_gui.c:4311
static int _zorder_group(const N_GUI_WINDOW *w)
Definition n_gui.c:670
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:3243
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:4193
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:3140
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:173
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:2589
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:2601
static int _n_gui_tree_node_visible(N_GUI_TREE *tree, int node_index)
Definition n_gui.c:7818
static void _draw_dropmenu_panel(N_GUI_CTX *ctx)
draw the dropdown menu panel overlay (called after all windows)
Definition n_gui.c:4223
static void _draw_widget(N_GUI_WIDGET *wgt, float ox, float oy, ALLEGRO_FONT *default_font, float win_w, N_GUI_STYLE *style)
draw a single widget.
Definition n_gui.c:4543
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:964
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:539
static float _min_thickness(float requested)
helper: compute minimum line thickness so it stays >= 1 physical pixel.
Definition n_gui.c:3207
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:131
static void _textarea_clear_selection(N_GUI_TEXTAREA_DATA *td)
clear the selection (set both anchors to cursor_pos)
Definition n_gui.c:5370
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:3826
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:3533
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:3180
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:689
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:2920
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:2777
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:5408
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:4030
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:248
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:154
static void _destroy_widget(void *ptr)
helper: destroy a single widget (called by list destructor)
Definition n_gui.c:191
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:116
static ALLEGRO_COLOR _text_for_state(const N_GUI_THEME *t, int state)
Definition n_gui.c:3233
static double _clamp(double v, double lo, double hi)
helper: clamp a double between lo and hi
Definition n_gui.c:38
static void _textarea_delete_selection(N_GUI_WIDGET *wgt)
delete the currently selected text, move cursor to selection start
Definition n_gui.c:5376
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:7401
static float _win_tbh(N_GUI_WINDOW *win)
helper: effective title bar height (0 for frameless windows)
Definition n_gui.c:111
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:266
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:47
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:3949
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:2568
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:3778
static void _draw_combobox_dropdown(N_GUI_CTX *ctx)
draw the combobox dropdown overlay (called after all windows)
Definition n_gui.c:4069
static void _n_gui_kv_add_clicked(int widget_id, void *user_data)
Definition n_gui.c:7886
static void _n_gui_tab_button_clicked(int widget_id, void *user_data)
Definition n_gui.c:7705
static int _textarea_has_selection(const N_GUI_TEXTAREA_DATA *td)
return 1 if the textarea has an active selection
Definition n_gui.c:3528
static ALLEGRO_COLOR _border_for_state(const N_GUI_THEME *t, int state)
Definition n_gui.c:3227
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:228
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:2688
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:2216
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:3876
static int _textarea_copy_to_clipboard(const N_GUI_TEXTAREA_DATA *td, ALLEGRO_DISPLAY *display)
copy selected text to clipboard.
Definition n_gui.c:5390
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:5278
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:3258
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:5310
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:4601
static float _label_text_origin_x(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:4376
static void _compute_gui_bounds(N_GUI_CTX *ctx)
helper: compute the bounding box of all open windows
Definition n_gui.c:4777
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:4398
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:564
static void _destroy_window(void *ptr)
helper: destroy a single window (called by list destructor)
Definition n_gui.c:218
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:7383
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:2767
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:7371
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:4347
static void _n_gui_kv_remove_clicked(int widget_id, void *user_data)
Definition n_gui.c:7893
static void _n_gui_kv_reposition(N_GUI_KVTABLE *table)
Definition n_gui.c:7905
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:5343
GUI system: buttons, sliders, text areas, checkboxes, scrollbars, dropdown menus, windows.