summaryrefslogtreecommitdiffstats
path: root/src/gui.cpp
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-27 20:30:15 +0100
committersalmonik <salmonik@tuta.io>2026-10-27 20:30:15 +0100
commit5ee13d8cee52505056a14c3ba3793d0e56afa202 (patch)
tree856c32e86382d1a4a0161271fdc7cc714560cee2 /src/gui.cpp
parentStucklaunch: alternate attack/use and expose spam rate (diff)
downloadhl2h-5ee13d8cee52505056a14c3ba3793d0e56afa202.tar.gz
hl2h-5ee13d8cee52505056a14c3ba3793d0e56afa202.zip
GUI: wrap all text instead of truncating
- Remove ellipsis truncation from text_in; text is never cut to fit. - Add a wrapping helper that breaks at spaces and, for long words such as paths, at character boundaries. Card titles, control labels, info rows, sections, tooltips and toasts now wrap and their rows/panels grow. - Use ISurface::GetTextSize (slot 75) for real glyph widths, with the old estimate only as a fallback before the surface/fonts exist. - Dynamic card header height for wrapped titles; Active/Bind controls stay vertically centered. Slider controls are vertically centered in taller wrapped rows. - Auto-size System buttons to their labels. - Tooltips wrap to a panel sized by the widest line. - Drop the duplicate config path from the footer; the System page shows it wrapped, so the footer hint fits without clipping.
Diffstat (limited to '')
-rw-r--r--src/gui.cpp419
1 files changed, 245 insertions, 174 deletions
diff --git a/src/gui.cpp b/src/gui.cpp
index fc95d1f..f4ba009 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -30,7 +30,8 @@ enum {
30 GS_DrawPrintText = 22, 30 GS_DrawPrintText = 22,
31 GS_GetScreenSize = 38, 31 GS_GetScreenSize = 38,
32 GS_CreateFont = 66, 32 GS_CreateFont = 66,
33 GS_AddGlyphSet = 67 33 GS_AddGlyphSet = 67,
34 GS_GetTextSize = 75
34}; 35};
35 36
36static unsigned long g_font_body = 0; 37static unsigned long g_font_body = 0;
@@ -87,6 +88,11 @@ static void s_screen(int &w, int &h) {
87 using Fn = void (*)(void *, int &, int &); 88 using Fn = void (*)(void *, int &, int &);
88 vfunc<Fn>(g_surface, GS_GetScreenSize)(g_surface, w, h); 89 vfunc<Fn>(g_surface, GS_GetScreenSize)(g_surface, w, h);
89} 90}
91static void s_text_size(unsigned long font, const wchar_t *text, int &w,
92 int &h) {
93 using Fn = void (*)(void *, unsigned long, const wchar_t *, int &, int &);
94 vfunc<Fn>(g_surface, GS_GetTextSize)(g_surface, font, text, w, h);
95}
90 96
91// --------------------------------------------------------------------------- 97// ---------------------------------------------------------------------------
92// SDL polling 98// SDL polling
@@ -511,17 +517,98 @@ static void ensure_fonts() {
511} 517}
512 518
513static int text_w(const char *s, int tall) { 519static int text_w(const char *s, int tall) {
514 if (!s) return 0; 520 if (!s)
521 return 0;
522 unsigned long font =
523 (g_font_title && tall == g_title_h) ? g_font_title : g_font_body;
524 if (g_surface && font) {
525 wchar_t wbuf[1024];
526 size_t n = mbstowcs(wbuf, s, 1023);
527 if (n != (size_t)-1) {
528 wbuf[n] = 0;
529 int w = 0, h = 0;
530 s_text_size(font, wbuf, w, h);
531 if (w > 0)
532 return w;
533 }
534 }
535 // Fallback when the surface/font is not ready yet.
515 return (int)(strlen(s) * tall * 0.52f + 0.5f); 536 return (int)(strlen(s) * tall * 0.52f + 0.5f);
516} 537}
517 538
518static void draw_text_raw(int x, int y, const char *s, unsigned long font, 539static void draw_text_raw(int x, int y, const char *s, unsigned long font,
540 int tall);
541
542// Break text into lines that fit max_w. Words that are too wide (paths, long
543// identifiers) are split by character so nothing is ever truncated.
544static void wrap_text(const char *text, int max_w, int tall,
545 std::vector<std::string> &out) {
546 out.clear();
547 if (!text || !*text || max_w <= 0)
548 return;
549 const char *p = text;
550 std::string line;
551 while (*p) {
552 while (*p == ' ')
553 p++;
554 if (!*p)
555 break;
556 const char *start = p;
557 while (*p && *p != ' ')
558 p++;
559 std::string word(start, (size_t)(p - start));
560 if (!line.empty()) {
561 std::string cand = line + " " + word;
562 if (text_w(cand.c_str(), tall) <= max_w) {
563 line = cand;
564 continue;
565 }
566 out.push_back(line);
567 line.clear();
568 }
569 while (text_w(word.c_str(), tall) > max_w && word.size() > 1) {
570 size_t n = word.size();
571 while (n > 1 && text_w(word.substr(0, n).c_str(), tall) > max_w)
572 n--;
573 out.push_back(word.substr(0, n));
574 word = word.substr(n);
575 }
576 line = word;
577 }
578 if (!line.empty())
579 out.push_back(line);
580}
581
582static int wrap_count(const char *text, int max_w, int tall) {
583 std::vector<std::string> lines;
584 wrap_text(text, max_w, tall, lines);
585 return (int)lines.size();
586}
587
588static void draw_wrapped(const Rect &clip, int x, int y, const char *text,
589 unsigned long font, int tall, int max_w,
590 const Col &c, int line_gap = 2) {
591 if (!text || !font)
592 return;
593 std::vector<std::string> lines;
594 wrap_text(text, max_w, tall, lines);
595 s_text_color(c.r, c.g, c.b, c.a);
596 int lh = tall + line_gap;
597 for (size_t i = 0; i < lines.size(); i++) {
598 int ly = y + (int)i * lh;
599 if (ly < clip.y || ly + tall > clip.y + clip.h)
600 continue;
601 draw_text_raw(x, ly, lines[i].c_str(), font, tall);
602 }
603}
604
605static void draw_text_raw(int x, int y, const char *s, unsigned long font,
519 int tall) { 606 int tall) {
520 (void)tall; 607 (void)tall;
521 if (!s || !font) 608 if (!s || !font)
522 return; 609 return;
523 wchar_t w[256]; 610 wchar_t w[1024];
524 size_t n = mbstowcs(w, s, 255); 611 size_t n = mbstowcs(w, s, 1023);
525 if (n == (size_t)-1) 612 if (n == (size_t)-1)
526 return; 613 return;
527 w[n] = 0; 614 w[n] = 0;
@@ -532,46 +619,20 @@ static void draw_text_raw(int x, int y, const char *s, unsigned long font,
532 619
533static void text_in(const Rect &clip, int x, int y, const char *s, 620static void text_in(const Rect &clip, int x, int y, const char *s,
534 unsigned long font, int tall, int max_w, const Col &c) { 621 unsigned long font, int tall, int max_w, const Col &c) {
622 (void)max_w; // callers must size the layout; text is never truncated
535 if (!s || !font) 623 if (!s || !font)
536 return; 624 return;
537 if (y < clip.y || y + tall > clip.y + clip.h) 625 if (y < clip.y || y + tall > clip.y + clip.h)
538 return; 626 return;
539 if (x >= clip.x + clip.w) 627 if (x >= clip.x + clip.w)
540 return; 628 return;
541
542 // Available space from x to the clip edge, optionally capped by max_w.
543 // Truncate first, then clamp: shifting before truncating used to draw the
544 // text outside the tooltip/button box.
545 int avail = clip.x + clip.w - x;
546 if (max_w > 0 && max_w < avail)
547 avail = max_w;
548 if (avail <= 0)
549 return;
550
551 char buf[256];
552 const char *draw = s;
553 int w = text_w(s, tall); 629 int w = text_w(s, tall);
554 if (w > avail) {
555 snprintf(buf, sizeof(buf), "%s", s);
556 int n = (int)strlen(buf);
557 int dots = text_w("...", tall);
558 while (n > 0 && text_w(buf, tall) + dots > avail)
559 buf[--n] = 0;
560 if (n > 0 && n + 3 < (int)sizeof(buf)) {
561 buf[n++] = '.';
562 buf[n++] = '.';
563 buf[n++] = '.';
564 buf[n] = 0;
565 }
566 draw = buf;
567 w = text_w(draw, tall);
568 }
569 if (x + w > clip.x + clip.w) 630 if (x + w > clip.x + clip.w)
570 x = clip.x + clip.w - w; 631 x = clip.x + clip.w - w;
571 if (x < clip.x) 632 if (x < clip.x)
572 x = clip.x; 633 x = clip.x;
573 s_text_color(c.r, c.g, c.b, c.a); 634 s_text_color(c.r, c.g, c.b, c.a);
574 draw_text_raw(x, y, draw, font, tall); 635 draw_text_raw(x, y, s, font, tall);
575} 636}
576 637
577static Rect clip_rect(const Rect &r, const Rect &clip) { 638static Rect clip_rect(const Rect &r, const Rect &clip) {
@@ -640,13 +701,48 @@ static void layout() {
640 if (g_win_y + g_win_h > h) g_win_y = h - g_win_h; 701 if (g_win_y + g_win_h > h) g_win_y = h - g_win_h;
641} 702}
642 703
704static int card_content_w() { return g_win_w - S(190.0f) - S(28.0f); }
705
706static int title_max_w() { return card_content_w() - S(210.0f); }
707
708static int header_h(const Card &c) {
709 int n = wrap_count(c.title, title_max_w(), g_title_h);
710 if (n < 1)
711 n = 1;
712 int h = n * (g_title_h + S(2.0f)) + S(12.0f);
713 return h < S(36.0f) ? S(36.0f) : h;
714}
715
716static int label_max_w(const Ctrl &c) {
717 if (c.kind == CK_SLIDER)
718 return S(214.0f);
719 return card_content_w() - S(16.0f) - S(110.0f);
720}
721
722static int button_w(const char *label) {
723 int bw = text_w(label, g_body_h) + S(20.0f);
724 if (bw < S(160.0f))
725 bw = S(160.0f);
726 int maxw = card_content_w() - S(24.0f);
727 if (bw > maxw)
728 bw = maxw;
729 return bw;
730}
731
643static int row_h(const Ctrl &c) { 732static int row_h(const Ctrl &c) {
644 switch (c.kind) { 733 if (c.kind == CK_BUTTON)
645 case CK_SECTION: return S(24.0f); 734 return S(32.0f);
646 case CK_INFO: return S(20.0f); 735 int maxw = (c.kind == CK_INFO || c.kind == CK_SECTION)
647 case CK_BUTTON: return S(32.0f); 736 ? card_content_w() - S(24.0f)
648 default: return S(27.0f); 737 : label_max_w(c);
649 } 738 int n = wrap_count(c.label, maxw, g_body_h);
739 if (n < 1)
740 n = 1;
741 int h = n * (g_body_h + S(2.0f)) + S(8.0f);
742 int minh = (c.kind == CK_INFO) ? S(20.0f)
743 : (c.kind == CK_SECTION) ? S(24.0f)
744 : S(27.0f);
745 return h < minh ? minh : h;
650} 746}
651 747
652static int body_h(const Card &c) { 748static int body_h(const Card &c) {
@@ -658,7 +754,7 @@ static int body_h(const Card &c) {
658 return h + S(6.0f); 754 return h + S(6.0f);
659} 755}
660 756
661static int card_h(const Card &c) { return S(36.0f) + body_h(c) + S(8.0f); } 757static int card_h(const Card &c) { return header_h(c) + body_h(c) + S(8.0f); }
662 758
663static Rect card_rect(int index, int y) { 759static Rect card_rect(int index, int y) {
664 Card &c = g_cards[index]; 760 Card &c = g_cards[index];
@@ -866,14 +962,15 @@ static void handle_mouse_click() {
866 Card &c = g_cards[i]; 962 Card &c = g_cards[i];
867 Rect cr = card_rect(i, y); 963 Rect cr = card_rect(i, y);
868 int ch = card_h(c); 964 int ch = card_h(c);
965 int hh = header_h(c);
869 if (my >= cr.y && my < cr.y + ch) { 966 if (my >= cr.y && my < cr.y + ch) {
870 // header 967 // header
871 if (my < cr.y + S(36.0f)) { 968 if (my < cr.y + hh) {
872 if (c.feat) { 969 if (c.feat) {
873 Rect active{cr.x + cr.w - S(170.0f), cr.y + S(9.0f), S(18.0f), 970 Rect active{cr.x + cr.w - S(170.0f),
874 S(18.0f)}; 971 cr.y + (hh - S(18.0f)) / 2, S(18.0f), S(18.0f)};
875 Rect bind{cr.x + cr.w - S(112.0f), cr.y + S(7.0f), S(100.0f), 972 Rect bind{cr.x + cr.w - S(112.0f),
876 S(22.0f)}; 973 cr.y + (hh - S(22.0f)) / 2, S(100.0f), S(22.0f)};
877 if (active.has(mx, my)) { 974 if (active.has(mx, my)) {
878 c.feat->active = !c.feat->active; 975 c.feat->active = !c.feat->active;
879 config_save(); 976 config_save();
@@ -895,7 +992,7 @@ static void handle_mouse_click() {
895 return; 992 return;
896 } 993 }
897 // body 994 // body
898 int by = cr.y + S(36.0f) + S(6.0f); 995 int by = cr.y + hh + S(6.0f);
899 for (int j = 0; j < c.n; j++) { 996 for (int j = 0; j < c.n; j++) {
900 Ctrl &ct = g_ctrls[c.c0 + j]; 997 Ctrl &ct = g_ctrls[c.c0 + j];
901 int rh = row_h(ct); 998 int rh = row_h(ct);
@@ -908,7 +1005,8 @@ static void handle_mouse_click() {
908 return; 1005 return;
909 } 1006 }
910 if (ct.kind == CK_SLIDER) { 1007 if (ct.kind == CK_SLIDER) {
911 Rect track{cr.x + S(16.0f) + S(230.0f), by + S(9.0f), 1008 Rect track{cr.x + S(16.0f) + S(230.0f),
1009 by + (rh - S(8.0f)) / 2,
912 cr.w - S(16.0f) - S(230.0f) - S(64.0f), S(8.0f)}; 1010 cr.w - S(16.0f) - S(230.0f) - S(64.0f), S(8.0f)};
913 if (mx >= track.x && mx < track.x + track.w) { 1011 if (mx >= track.x && mx < track.x + track.w) {
914 float t = (float)(mx - track.x) / (float)track.w; 1012 float t = (float)(mx - track.x) / (float)track.w;
@@ -921,8 +1019,9 @@ static void handle_mouse_click() {
921 return; 1019 return;
922 } 1020 }
923 if (ct.kind == CK_BUTTON) { 1021 if (ct.kind == CK_BUTTON) {
924 Rect b{cr.x + cr.w - S(176.0f), by + S(3.0f), S(160.0f), 1022 int bw = button_w(ct.label);
925 S(26.0f)}; 1023 Rect b{cr.x + cr.w - S(16.0f) - bw,
1024 by + (rh - S(26.0f)) / 2, bw, S(26.0f)};
926 if (b.has(mx, my)) { 1025 if (b.has(mx, my)) {
927 if (ct.action == ACT_RELOAD) { 1026 if (ct.action == ACT_RELOAD) {
928 config_reload(); 1027 config_reload();
@@ -964,13 +1063,14 @@ static void handle_hover() {
964 Card &c = g_cards[i]; 1063 Card &c = g_cards[i];
965 Rect cr = card_rect(i, y); 1064 Rect cr = card_rect(i, y);
966 int ch = card_h(c); 1065 int ch = card_h(c);
1066 int hh = header_h(c);
967 if (my >= cr.y && my < cr.y + ch) { 1067 if (my >= cr.y && my < cr.y + ch) {
968 if (my < cr.y + S(36.0f)) { 1068 if (my < cr.y + hh) {
969 if (c.feat) { 1069 if (c.feat) {
970 Rect active{cr.x + cr.w - S(170.0f), cr.y + S(9.0f), S(18.0f), 1070 Rect active{cr.x + cr.w - S(170.0f),
971 S(18.0f)}; 1071 cr.y + (hh - S(18.0f)) / 2, S(18.0f), S(18.0f)};
972 Rect bind{cr.x + cr.w - S(112.0f), cr.y + S(7.0f), S(100.0f), 1072 Rect bind{cr.x + cr.w - S(112.0f),
973 S(22.0f)}; 1073 cr.y + (hh - S(22.0f)) / 2, S(100.0f), S(22.0f)};
974 if (active.has(mx, my)) 1074 if (active.has(mx, my))
975 g_hover_desc = "Enable this feature. A bind, when set, must be " 1075 g_hover_desc = "Enable this feature. A bind, when set, must be "
976 "held for it to run."; 1076 "held for it to run.";
@@ -987,7 +1087,7 @@ static void handle_hover() {
987 } 1087 }
988 return; 1088 return;
989 } 1089 }
990 int by = cr.y + S(36.0f) + S(6.0f); 1090 int by = cr.y + hh + S(6.0f);
991 for (int j = 0; j < c.n; j++) { 1091 for (int j = 0; j < c.n; j++) {
992 Ctrl &ct = g_ctrls[c.c0 + j]; 1092 Ctrl &ct = g_ctrls[c.c0 + j];
993 int rh = row_h(ct); 1093 int rh = row_h(ct);
@@ -1165,11 +1265,13 @@ static void draw_checkbox(int x, int y, bool on) {
1165 } 1265 }
1166} 1266}
1167 1267
1168static void draw_slider(const Rect &clip, const Rect &cr, int y, const Ctrl &ct) { 1268static void draw_slider(const Rect &clip, const Rect &cr, int y, int rh,
1269 const Ctrl &ct) {
1169 int track_x = cr.x + S(16.0f) + S(230.0f); 1270 int track_x = cr.x + S(16.0f) + S(230.0f);
1170 int track_w = cr.w - S(16.0f) - S(230.0f) - S(64.0f); 1271 int track_w = cr.w - S(16.0f) - S(230.0f) - S(64.0f);
1171 if (track_w < S(30.0f)) track_w = S(30.0f); 1272 if (track_w < S(30.0f)) track_w = S(30.0f);
1172 Rect track{track_x, y + S(9.0f), track_w, S(8.0f)}; 1273 int track_y = y + (rh - S(8.0f)) / 2;
1274 Rect track{track_x, track_y, track_w, S(8.0f)};
1173 fill(track, Col{38, 42, 50, 255}, &clip); 1275 fill(track, Col{38, 42, 50, 255}, &clip);
1174 int value = ctrl_val(ct); 1276 int value = ctrl_val(ct);
1175 float t = (ct.max > ct.min) 1277 float t = (ct.max > ct.min)
@@ -1181,8 +1283,8 @@ static void draw_slider(const Rect &clip, const Rect &cr, int y, const Ctrl &ct)
1181 fill(Rect{kx, track.y - S(4.0f), S(8.0f), S(16.0f)}, C_ACCENT, &clip); 1283 fill(Rect{kx, track.y - S(4.0f), S(8.0f), S(16.0f)}, C_ACCENT, &clip);
1182 char val[32]; 1284 char val[32];
1183 snprintf(val, sizeof(val), "%d", value); 1285 snprintf(val, sizeof(val), "%d", value);
1184 text_in(clip, cr.x + cr.w - S(52.0f), y + S(4.0f), val, g_font_body, g_body_h, 1286 text_in(clip, cr.x + cr.w - S(52.0f), y + (rh - g_body_h) / 2, val,
1185 S(44.0f), C_TEXT); 1287 g_font_body, g_body_h, 0, C_TEXT);
1186} 1288}
1187 1289
1188static void draw_card(const Rect &clip, int index, int y) { 1290static void draw_card(const Rect &clip, int index, int y) {
@@ -1193,10 +1295,12 @@ static void draw_card(const Rect &clip, int index, int y) {
1193 return; 1295 return;
1194 fill(cr, C_PANEL, &clip); 1296 fill(cr, C_PANEL, &clip);
1195 outline(cr, Col{52, 57, 67, 255}, &clip); 1297 outline(cr, Col{52, 57, 67, 255}, &clip);
1196 Rect head{cr.x, cr.y, cr.w, S(36.0f)}; 1298 int hh = header_h(c);
1299 Rect head{cr.x, cr.y, cr.w, hh};
1197 fill(head, C_HEADER, &clip); 1300 fill(head, C_HEADER, &clip);
1301
1198 // expand arrow 1302 // expand arrow
1199 int ax = cr.x + S(10.0f), ay = cr.y + S(13.0f); 1303 int ax = cr.x + S(10.0f), ay = cr.y + (hh - S(8.0f)) / 2;
1200 if (c.expanded) { 1304 if (c.expanded) {
1201 line(ax, ay, ax + S(4.0f), ay + S(5.0f), C_DIM); 1305 line(ax, ay, ax + S(4.0f), ay + S(5.0f), C_DIM);
1202 line(ax + S(4.0f), ay + S(5.0f), ax + S(8.0f), ay, C_DIM); 1306 line(ax + S(4.0f), ay + S(5.0f), ax + S(8.0f), ay, C_DIM);
@@ -1204,40 +1308,53 @@ static void draw_card(const Rect &clip, int index, int y) {
1204 line(ax, ay, ax + S(5.0f), ay + S(4.0f), C_DIM); 1308 line(ax, ay, ax + S(5.0f), ay + S(4.0f), C_DIM);
1205 line(ax + S(5.0f), ay + S(4.0f), ax, ay + S(8.0f), C_DIM); 1309 line(ax + S(5.0f), ay + S(4.0f), ax, ay + S(8.0f), C_DIM);
1206 } 1310 }
1207 text_in(clip, cr.x + S(24.0f), cr.y + S(8.0f), c.title, g_font_title, 1311
1208 g_title_h, cr.w - S(180.0f), C_TEXT); 1312 // title (wraps instead of truncating)
1313 std::vector<std::string> title_lines;
1314 wrap_text(c.title, title_max_w(), g_title_h, title_lines);
1315 int tlh = g_title_h + S(2.0f);
1316 int title_h = (int)title_lines.size() * tlh - S(2.0f);
1317 int ty = cr.y + (hh - title_h) / 2;
1318 draw_wrapped(clip, cr.x + S(24.0f), ty, c.title, g_font_title, g_title_h,
1319 title_max_w(), C_TEXT, S(2.0f));
1320
1209 if (c.feat) { 1321 if (c.feat) {
1210 bool active = c.feat->active; 1322 bool active = c.feat->active;
1211 draw_checkbox(cr.x + cr.w - S(170.0f), cr.y + S(10.0f), active); 1323 int active_y = cr.y + (hh - S(18.0f)) / 2;
1212 text_in(clip, cr.x + cr.w - S(148.0f), cr.y + S(9.0f), 1324 draw_checkbox(cr.x + cr.w - S(170.0f), active_y, active);
1213 active ? "On" : "Off", g_font_body, g_body_h, S(32.0f), 1325 text_in(clip, cr.x + cr.w - S(148.0f),
1214 active ? C_GOOD : C_DIM); 1326 active_y + (S(18.0f) - g_body_h) / 2, active ? "On" : "Off",
1215 Rect bind{cr.x + cr.w - S(112.0f), cr.y + S(7.0f), S(100.0f), S(22.0f)}; 1327 g_font_body, g_body_h, 0, active ? C_GOOD : C_DIM);
1328 Rect bind{cr.x + cr.w - S(112.0f), cr.y + (hh - S(22.0f)) / 2, S(100.0f),
1329 S(22.0f)};
1216 fill(bind, Col{45, 50, 60, 255}, &clip); 1330 fill(bind, Col{45, 50, 60, 255}, &clip);
1217 outline(bind, C_BORDER, &clip); 1331 outline(bind, C_BORDER, &clip);
1218 const char *bl = (g_capture_card == index) 1332 const char *bl = (g_capture_card == index)
1219 ? "Press..." 1333 ? "Press..."
1220 : scancode_name(c.feat->bind); 1334 : scancode_name(c.feat->bind);
1221 text_in(clip, bind.x + S(7.0f), bind.y + S(3.0f), bl, g_font_body, g_body_h, 1335 text_in(clip, bind.x + S(7.0f), bind.y + (bind.h - g_body_h) / 2, bl,
1222 bind.w - S(22.0f), C_TEXT); 1336 g_font_body, g_body_h, 0, C_TEXT);
1223 text_in(clip, bind.x + bind.w - S(16.0f), bind.y + S(3.0f), "x", 1337 text_in(clip, bind.x + bind.w - S(16.0f),
1224 g_font_body, g_body_h, S(10.0f), C_DIM); 1338 bind.y + (bind.h - g_body_h) / 2, "x", g_font_body, g_body_h, 0,
1339 C_DIM);
1225 } 1340 }
1226 if (!c.expanded) 1341 if (!c.expanded)
1227 return; 1342 return;
1228 1343
1229 int by = cr.y + S(36.0f) + S(6.0f); 1344 int by = cr.y + hh + S(6.0f);
1230 for (int j = 0; j < c.n; j++) { 1345 for (int j = 0; j < c.n; j++) {
1231 Ctrl &ct = g_ctrls[c.c0 + j]; 1346 Ctrl &ct = g_ctrls[c.c0 + j];
1232 int rh = row_h(ct); 1347 int rh = row_h(ct);
1233 if (ct.kind == CK_SECTION) { 1348 if (ct.kind == CK_SECTION) {
1234 text_in(clip, cr.x + S(16.0f), by + S(3.0f), ct.label, g_font_body, 1349 draw_wrapped(clip, cr.x + S(16.0f), by + S(3.0f), ct.label, g_font_body,
1235 g_body_h, cr.w - S(24.0f), C_ACCENT); 1350 g_body_h, card_content_w() - S(24.0f), C_ACCENT, S(2.0f));
1236 } else if (ct.kind == CK_INFO) { 1351 } else if (ct.kind == CK_INFO) {
1237 text_in(clip, cr.x + S(16.0f), by + S(1.0f), ct.label, g_font_body, 1352 draw_wrapped(clip, cr.x + S(16.0f), by + S(3.0f), ct.label, g_font_body,
1238 g_body_h, cr.w - S(24.0f), C_DIM); 1353 g_body_h, card_content_w() - S(24.0f), C_DIM, S(2.0f));
1239 } else if (ct.kind == CK_BUTTON) { 1354 } else if (ct.kind == CK_BUTTON) {
1240 Rect b{cr.x + cr.w - S(176.0f), by + S(3.0f), S(160.0f), S(26.0f)}; 1355 int bw = button_w(ct.label);
1356 Rect b{cr.x + cr.w - S(16.0f) - bw, by + (rh - S(26.0f)) / 2, bw,
1357 S(26.0f)};
1241 fill(b, Col{45, 50, 60, 255}, &clip); 1358 fill(b, Col{45, 50, 60, 255}, &clip);
1242 outline(b, C_BORDER, &clip); 1359 outline(b, C_BORDER, &clip);
1243 const char *label = ct.label; 1360 const char *label = ct.label;
@@ -1247,19 +1364,25 @@ static void draw_card(const Rect &clip, int index, int y) {
1247 else 1364 else
1248 label = "Confirm reset"; 1365 label = "Confirm reset";
1249 } 1366 }
1250 text_in(clip, b.x + S(10.0f), b.y + S(4.0f), label, g_font_body, 1367 text_in(clip, b.x + S(10.0f), b.y + (b.h - g_body_h) / 2, label,
1251 g_body_h, b.w - S(16.0f), C_TEXT); 1368 g_font_body, g_body_h, 0, C_TEXT);
1252 } else { 1369 } else {
1253 text_in(clip, cr.x + S(16.0f), by + S(4.0f), ct.label, g_font_body, 1370 int maxw = label_max_w(ct);
1254 g_body_h, S(220.0f), C_TEXT); 1371 std::vector<std::string> lines;
1372 wrap_text(ct.label, maxw, g_body_h, lines);
1373 int lh = g_body_h + S(2.0f);
1374 int text_h = (int)lines.size() * lh - S(2.0f);
1375 int ly = by + (rh - text_h) / 2;
1376 draw_wrapped(clip, cr.x + S(16.0f), ly, ct.label, g_font_body, g_body_h,
1377 maxw, C_TEXT, S(2.0f));
1255 if (ct.kind == CK_TOGGLE) { 1378 if (ct.kind == CK_TOGGLE) {
1256 bool on = ctrl_val(ct) != 0; 1379 bool on = ctrl_val(ct) != 0;
1257 draw_checkbox(cr.x + cr.w - S(31.0f), by + S(5.0f), on); 1380 draw_checkbox(cr.x + cr.w - S(31.0f), by + (rh - S(15.0f)) / 2, on);
1258 text_in(clip, cr.x + cr.w - S(58.0f), by + S(3.0f), 1381 text_in(clip, cr.x + cr.w - S(58.0f), by + (rh - g_body_h) / 2,
1259 on ? "On" : "Off", g_font_body, g_body_h, S(26.0f), 1382 on ? "On" : "Off", g_font_body, g_body_h, 0,
1260 on ? C_GOOD : C_DIM); 1383 on ? C_GOOD : C_DIM);
1261 } else if (ct.kind == CK_SLIDER) { 1384 } else if (ct.kind == CK_SLIDER) {
1262 draw_slider(clip, cr, by, ct); 1385 draw_slider(clip, cr, by, rh, ct);
1263 } 1386 }
1264 } 1387 }
1265 by += rh; 1388 by += rh;
@@ -1272,94 +1395,46 @@ static void draw_tooltip() {
1272 float d = now_sec() - (float)g_hover_since; 1395 float d = now_sec() - (float)g_hover_since;
1273 if (d < 0.4f) 1396 if (d < 0.4f)
1274 return; 1397 return;
1275 int tw = text_w(g_hover_desc, g_body_h) + S(16.0f);
1276 int maxw = S(380.0f); 1398 int maxw = S(380.0f);
1277 if (tw > maxw) tw = maxw; 1399 if (maxw > g_scr_w - S(32.0f))
1278 int th = g_body_h + S(14.0f); 1400 maxw = g_scr_w - S(32.0f);
1401 std::vector<std::string> lines;
1402 wrap_text(g_hover_desc, maxw, g_body_h, lines);
1403 if (lines.empty())
1404 return;
1405 int line_w = 0;
1406 for (auto &line : lines) {
1407 int w = text_w(line.c_str(), g_body_h);
1408 if (w > line_w)
1409 line_w = w;
1410 }
1411 int line_h = g_body_h + S(3.0f);
1412 int tw = line_w + S(16.0f);
1413 int th = (int)lines.size() * line_h + S(12.0f) - S(3.0f);
1279 int x = g_hover_x + S(14.0f); 1414 int x = g_hover_x + S(14.0f);
1280 int y = g_hover_y + S(16.0f); 1415 int y = g_hover_y + S(16.0f);
1281 if (x + tw > g_scr_w - S(4.0f)) x = g_hover_x - tw - S(10.0f); 1416 if (x + tw > g_scr_w - S(4.0f))
1282 if (y + th > g_scr_h - S(4.0f)) y = g_hover_y - th - S(10.0f); 1417 x = g_hover_x - tw - S(10.0f);
1418 if (y + th > g_scr_h - S(4.0f))
1419 y = g_hover_y - th - S(10.0f);
1420 if (x < S(4.0f))
1421 x = S(4.0f);
1422 if (y < S(4.0f))
1423 y = S(4.0f);
1283 Rect r{x, y, tw, th}; 1424 Rect r{x, y, tw, th};
1284 fill(r, Col{12, 14, 18, 248}); 1425 fill(r, Col{12, 14, 18, 248});
1285 outline(r, C_ACCENT_DIM); 1426 outline(r, C_ACCENT_DIM);
1286 Rect inner{x + S(7.0f), y + S(6.0f), tw - S(14.0f), g_body_h}; 1427 s_text_font(g_font_body);
1287 text_in(inner, inner.x, inner.y, g_hover_desc, g_font_body, g_body_h, 1428 s_text_color(C_TEXT.r, C_TEXT.g, C_TEXT.b, C_TEXT.a);
1288 inner.w, C_TEXT); 1429 for (size_t i = 0; i < lines.size(); i++)
1430 draw_text_raw(x + S(7.0f), y + S(6.0f) + (int)i * line_h,
1431 lines[i].c_str(), g_font_body, g_body_h);
1289} 1432}
1290 1433
1291// --------------------------------------------------------------------------- 1434// ---------------------------------------------------------------------------
1292// Toast drawing 1435// Toast drawing
1293// --------------------------------------------------------------------------- 1436// ---------------------------------------------------------------------------
1294 1437
1295static int wrap_toast(const char *text, int max_w, int tall,
1296 char lines[][256], int max_lines) {
1297 int n = 0;
1298 char cur[256] = {};
1299 bool truncated = false;
1300 const char *p = text;
1301 while (*p) {
1302 while (*p == ' ')
1303 p++;
1304 if (!*p)
1305 break;
1306 const char *start = p;
1307 while (*p && *p != ' ')
1308 p++;
1309 size_t wl = (size_t)(p - start);
1310 if (wl > 120)
1311 wl = 120;
1312 char word[128];
1313 memcpy(word, start, wl);
1314 word[wl] = 0;
1315
1316 char cand[256];
1317 if (cur[0])
1318 snprintf(cand, sizeof(cand), "%s %s", cur, word);
1319 else
1320 snprintf(cand, sizeof(cand), "%s", word);
1321
1322 if (text_w(cand, tall) <= max_w || !cur[0]) {
1323 snprintf(cur, sizeof(cur), "%s", cand);
1324 if (text_w(cur, tall) > max_w) {
1325 // A single word longer than the panel: hard-truncate it.
1326 int len = (int)strlen(cur);
1327 while (len > 3 && text_w(cur, tall) > max_w)
1328 cur[--len] = 0;
1329 if (len > 3) {
1330 cur[len - 3] = '.';
1331 cur[len - 2] = '.';
1332 cur[len - 1] = '.';
1333 }
1334 truncated = true;
1335 }
1336 } else {
1337 if (n < max_lines)
1338 snprintf(lines[n++], 256, "%s", cur);
1339 else
1340 truncated = true;
1341 snprintf(cur, sizeof(cur), "%s", word);
1342 }
1343 }
1344 if (cur[0]) {
1345 if (n < max_lines)
1346 snprintf(lines[n++], 256, "%s", cur);
1347 else
1348 truncated = true;
1349 }
1350 if (truncated && n > 0) {
1351 char *last = lines[n - 1];
1352 int len = (int)strlen(last);
1353 if (len + 3 < 256) {
1354 last[len++] = '.';
1355 last[len++] = '.';
1356 last[len++] = '.';
1357 last[len] = 0;
1358 }
1359 }
1360 return n;
1361}
1362
1363void gui_draw_toasts() { 1438void gui_draw_toasts() {
1364 if (!g_surface) 1439 if (!g_surface)
1365 return; 1440 return;
@@ -1401,13 +1476,14 @@ void gui_draw_toasts() {
1401 if (a > 1.0f) a = 1.0f; 1476 if (a > 1.0f) a = 1.0f;
1402 int alpha = (int)(a * 255.0f); 1477 int alpha = (int)(a * 255.0f);
1403 1478
1404 char lines[4][256]; 1479 std::vector<std::string> lines;
1405 int nl = wrap_toast(t.text, max_w, g_body_h, lines, 4); 1480 wrap_text(t.text, max_w, g_body_h, lines);
1481 int nl = (int)lines.size();
1406 if (nl == 0) 1482 if (nl == 0)
1407 continue; 1483 continue;
1408 int line_w = 0; 1484 int line_w = 0;
1409 for (int j = 0; j < nl; j++) { 1485 for (int j = 0; j < nl; j++) {
1410 int w = text_w(lines[j], g_body_h); 1486 int w = text_w(lines[j].c_str(), g_body_h);
1411 if (w > line_w) 1487 if (w > line_w)
1412 line_w = w; 1488 line_w = w;
1413 } 1489 }
@@ -1432,7 +1508,7 @@ void gui_draw_toasts() {
1432 for (int j = 0; j < nl; j++) { 1508 for (int j = 0; j < nl; j++) {
1433 s_text_pos(x + pad + S(6.0f), y + pad + j * line_h); 1509 s_text_pos(x + pad + S(6.0f), y + pad + j * line_h);
1434 wchar_t w[256]; 1510 wchar_t w[256];
1435 size_t wn = mbstowcs(w, lines[j], 255); 1511 size_t wn = mbstowcs(w, lines[j].c_str(), 255);
1436 if (wn == (size_t)-1) 1512 if (wn == (size_t)-1)
1437 continue; 1513 continue;
1438 w[wn] = 0; 1514 w[wn] = 0;
@@ -1515,15 +1591,10 @@ void gui_draw() {
1515 g_capture_card >= 0 ? "press a key / Esc" : "INS close"; 1591 g_capture_card >= 0 ? "press a key / Esc" : "INS close";
1516 int hint_w = text_w(hint, g_body_h); 1592 int hint_w = text_w(hint, g_body_h);
1517 int hint_x = foot.x + foot.w - hint_w - S(12.0f); 1593 int hint_x = foot.x + foot.w - hint_w - S(12.0f);
1518 int left_x = foot.x + S(12.0f); 1594 if (hint_x < foot.x + S(12.0f))
1519 if (hint_x < left_x) 1595 hint_x = foot.x + S(12.0f);
1520 hint_x = left_x; 1596 text_in(foot, hint_x, foot.y + S(6.0f), hint, g_font_body, g_body_h, 0,
1521 int left_max = hint_x - left_x - S(12.0f); 1597 C_DIM);
1522 if (left_max > 0)
1523 text_in(foot, left_x, foot.y + S(6.0f), g_config_label, g_font_body,
1524 g_body_h, left_max, C_DIM);
1525 text_in(foot, hint_x, foot.y + S(6.0f), hint, g_font_body, g_body_h,
1526 hint_w + S(4.0f), C_DIM);
1527 1598
1528 draw_tooltip(); 1599 draw_tooltip();
1529} 1600}