From 6b5425ebd35c28e20680f5f28c887b705827ba1c Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 27 Oct 2026 18:53:32 +0100 Subject: GUI: add on-screen toasts and atomic config writes - Toast queue drawn in the top-right by the overlay, independent of the GUI being open. Info/warn/error colors and 6/9/12 s lifetimes; timers start on the first painted frame so startup messages are not missed. - Queue a load toast after install_menu, plus warnings/errors for missing modules/interfaces/netvars, input hooks, fonts, ISurface, server datamap, and spread seed support. - Toast on config reload, reset, bind/unbind and config save failure. - Config saves now write `config.tmp`, flush, then rename it over the real file; failures leave the previous config intact. - config_load reports whether a file was found so Reload can distinguish a successful reload from a missing file. - Update CONTEXT and ISURFACE_STATUS for the single GUI, toasts and atomic config writes. --- src/gui.cpp | 217 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 216 insertions(+), 1 deletion(-) (limited to 'src/gui.cpp') diff --git a/src/gui.cpp b/src/gui.cpp index 132068e..0d5b458 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -6,6 +6,7 @@ #include "hl2h.h" #include +#include #include #include #include @@ -136,6 +137,7 @@ static void gui_init_sdl() { g_sdl_ready = sd_get_keys && sd_get_mouse; if (!g_sdl_ready) { logf("gui: SDL polling unavailable"); + toastf(TOAST_ERROR, "GUI input unavailable: SDL not found"); return; } if (sd_add_watch) @@ -406,6 +408,56 @@ static float now_sec() { return (float)ts.tv_sec + (float)ts.tv_nsec * 1e-9f; } +// --------------------------------------------------------------------------- +// Toasts (shown even when the GUI is closed) +// --------------------------------------------------------------------------- + +struct Toast { + char text[256]; + int level; + double born; // 0 = waiting for the first draw + double ttl; +}; + +static Toast g_toasts[8]; +static int g_ntoasts = 0; +static pthread_mutex_t g_toast_mu = PTHREAD_MUTEX_INITIALIZER; + +void toastf(int level, const char *fmt, ...) { + char buf[256]; + va_list ap; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + + pthread_mutex_lock(&g_toast_mu); + for (int i = 0; i < g_ntoasts; i++) { + if (strcmp(g_toasts[i].text, buf) == 0) { + g_toasts[i].level = level; + g_toasts[i].born = 0.0; + pthread_mutex_unlock(&g_toast_mu); + return; + } + } + if (g_ntoasts == 8) { + memmove(&g_toasts[0], &g_toasts[1], sizeof(Toast) * 7); + g_ntoasts = 7; + } + Toast &t = g_toasts[g_ntoasts++]; + snprintf(t.text, sizeof(t.text), "%s", buf); + t.level = level; + t.born = 0.0; + t.ttl = level == TOAST_ERROR ? 12.0 : level == TOAST_WARN ? 9.0 : 6.0; + pthread_mutex_unlock(&g_toast_mu); +} + +bool gui_toasts_active() { + pthread_mutex_lock(&g_toast_mu); + bool active = g_ntoasts > 0; + pthread_mutex_unlock(&g_toast_mu); + return active; +} + static void ensure_fonts() { if (g_font_body && fabsf(g_font_scale - g_scale) < 0.001f) return; @@ -437,8 +489,10 @@ static void ensure_fonts() { } if (!g_font_title) g_font_title = g_font_body; - if (!g_font_body) + if (!g_font_body) { logf("gui: no usable body font"); + toastf(TOAST_ERROR, "No usable GUI font"); + } } static int text_w(const char *s, int tall) { @@ -698,6 +752,8 @@ static void handle_capture(const unsigned char *keys) { g_mouse_rclick = false; g_capture_card = -1; config_save(); + toastf(TOAST_INFO, "%s bound to %s", card.title, + scancode_name(card.feat->bind)); return; } for (int k = 0; k < 512; k++) { @@ -709,6 +765,9 @@ static void handle_capture(const unsigned char *keys) { card.feat->bind = k; g_capture_card = -1; config_save(); + if (card.feat) + toastf(TOAST_INFO, "%s bound to %s", card.title, + scancode_name(card.feat->bind)); return; } } @@ -795,6 +854,7 @@ static void handle_mouse_click() { if (mx >= bind.x + bind.w - S(18.0f)) { c.feat->bind = 0; config_save(); + toastf(TOAST_INFO, "%s unbound (always-on)", c.title); return; } g_capture_card = i; @@ -844,6 +904,7 @@ static void handle_mouse_click() { config_reset_defaults(); g_reset_armed = false; logf("gui: defaults restored"); + toastf(TOAST_INFO, "Feature defaults restored"); } else { g_reset_armed = true; g_reset_armed_at = now; @@ -1198,6 +1259,160 @@ static void draw_tooltip() { inner.w, C_TEXT); } +// --------------------------------------------------------------------------- +// Toast drawing +// --------------------------------------------------------------------------- + +static int wrap_toast(const char *text, int max_w, int tall, + char lines[][256], int max_lines) { + int n = 0; + char cur[256] = {}; + bool truncated = false; + const char *p = text; + while (*p) { + while (*p == ' ') + p++; + if (!*p) + break; + const char *start = p; + while (*p && *p != ' ') + p++; + size_t wl = (size_t)(p - start); + if (wl > 120) + wl = 120; + char word[128]; + memcpy(word, start, wl); + word[wl] = 0; + + char cand[256]; + if (cur[0]) + snprintf(cand, sizeof(cand), "%s %s", cur, word); + else + snprintf(cand, sizeof(cand), "%s", word); + + if (text_w(cand, tall) <= max_w || !cur[0]) { + snprintf(cur, sizeof(cur), "%s", cand); + if (text_w(cur, tall) > max_w) { + // A single word longer than the panel: hard-truncate it. + int len = (int)strlen(cur); + while (len > 3 && text_w(cur, tall) > max_w) + cur[--len] = 0; + if (len > 3) { + cur[len - 3] = '.'; + cur[len - 2] = '.'; + cur[len - 1] = '.'; + } + truncated = true; + } + } else { + if (n < max_lines) + snprintf(lines[n++], 256, "%s", cur); + else + truncated = true; + snprintf(cur, sizeof(cur), "%s", word); + } + } + if (cur[0]) { + if (n < max_lines) + snprintf(lines[n++], 256, "%s", cur); + else + truncated = true; + } + if (truncated && n > 0) { + char *last = lines[n - 1]; + int len = (int)strlen(last); + if (len + 3 < 256) { + last[len++] = '.'; + last[len++] = '.'; + last[len++] = '.'; + last[len] = 0; + } + } + return n; +} + +void gui_draw_toasts() { + if (!g_surface) + return; + ensure_fonts(); + if (!g_font_body || g_scr_w <= 0 || g_scr_h <= 0) + return; + + Toast local[8]; + int n = 0; + double now = now_sec(); + pthread_mutex_lock(&g_toast_mu); + for (int i = 0; i < g_ntoasts; i++) { + Toast &t = g_toasts[i]; + if (t.born == 0.0) + t.born = now; + if (now - t.born > t.ttl) + continue; + local[n++] = t; + } + g_ntoasts = 0; + for (int i = 0; i < n; i++) + g_toasts[g_ntoasts++] = local[i]; + pthread_mutex_unlock(&g_toast_mu); + + int margin = S(16.0f); + int max_w = S(420.0f); + if (max_w > g_scr_w - margin * 2) + max_w = g_scr_w - margin * 2; + int y = margin; + for (int i = 0; i < n; i++) { + const Toast &t = local[i]; + float age = (float)(now - t.born); + float a = 1.0f; + if (age < 0.2f) + a = age / 0.2f; + else if (age > t.ttl - 0.5f) + a = (float)((t.ttl - age) / 0.5); + if (a < 0.0f) a = 0.0f; + if (a > 1.0f) a = 1.0f; + int alpha = (int)(a * 255.0f); + + char lines[4][256]; + int nl = wrap_toast(t.text, max_w, g_body_h, lines, 4); + if (nl == 0) + continue; + int line_w = 0; + for (int j = 0; j < nl; j++) { + int w = text_w(lines[j], g_body_h); + if (w > line_w) + line_w = w; + } + int pad = S(12.0f); + int line_h = g_body_h + S(4.0f); + int panel_w = line_w + pad * 2 + S(6.0f); + int panel_h = nl * line_h + pad * 2 - S(4.0f); + int x = g_scr_w - margin - panel_w; + if (x < margin) + x = margin; + + Col accent = t.level == TOAST_ERROR + ? Col{240, 90, 90, 255} + : t.level == TOAST_WARN ? Col{255, 200, 80, 255} + : C_ACCENT; + fill(Rect{x, y, panel_w, panel_h}, Col{16, 18, 22, (int)(235 * a)}); + outline(Rect{x, y, panel_w, panel_h}, Col{accent.r, accent.g, accent.b, alpha}); + fill(Rect{x, y, S(4.0f), panel_h}, + Col{accent.r, accent.g, accent.b, alpha}); + s_text_font(g_font_body); + s_text_color(C_TEXT.r, C_TEXT.g, C_TEXT.b, alpha); + for (int j = 0; j < nl; j++) { + s_text_pos(x + pad + S(6.0f), y + pad + j * line_h); + wchar_t w[256]; + size_t wn = mbstowcs(w, lines[j], 255); + if (wn == (size_t)-1) + continue; + w[wn] = 0; + s_print(w, (int)wn); + } + y += panel_h + S(8.0f); + } +} + void gui_draw() { if (!g_open || !g_surface) return; -- cgit v1.2.3