summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-27 18:02:10 +0100
committersalmonik <salmonik@tuta.io>2026-10-27 18:02:10 +0100
commit0e9f3422e37d0b2d1344efec5ceb07c2246bfcd7 (patch)
treea8aa0b5a4718f0e7d407a31135823523a77744b0 /src
parentSpeedgraph: lower the graph (diff)
downloadhl2h-0e9f3422e37d0b2d1344efec5ceb07c2246bfcd7.tar.gz
hl2h-0e9f3422e37d0b2d1344efec5ceb07c2246bfcd7.zip
GUI: add F2 preview shell with dummy controls
First vertical slice for the custom ISurface GUI: - window/sidebar/content shell with Movement, Combat, Visuals, Weapons and System categories; - cards with dummy Active/Bind, collapsible settings, toggles, sliders, buttons, bind capture, scrolling and tooltips; - native OS cursor by releasing SDL relative mode; reasserted while open; - F2 toggles the preview; Insert still opens the old text tree; - while the GUI is open, do_move freezes movement/buttons/impulses/view and skips combat features; ESP and the speed graph keep drawing. Dummy controls do not touch the real config yet. The text menu is removed after feature parity in the next pass.
Diffstat (limited to 'src')
-rw-r--r--src/gui.cpp1199
-rw-r--r--src/hl2h.cpp19
-rw-r--r--src/hl2h.h5
-rw-r--r--src/menu.cpp19
4 files changed, 1239 insertions, 3 deletions
diff --git a/src/gui.cpp b/src/gui.cpp
new file mode 100644
index 0000000..74dd537
--- /dev/null
+++ b/src/gui.cpp
@@ -0,0 +1,1199 @@
1// gui.cpp — custom immediate-mode GUI shell (temporary F2 preview)
2//
3// This is the first vertical slice: the visual shell, categories, cards,
4// dummy controls, cursor handling and input freeze. It does not read or write
5// the real feature settings yet; the next pass replaces the dummy state and
6// removes the text menu.
7
8#include "hl2h.h"
9
10#include <atomic>
11#include <cmath>
12#include <cstdio>
13#include <cstdlib>
14#include <cstring>
15#include <cwchar>
16#include <dlfcn.h>
17#include <time.h>
18
19// ---------------------------------------------------------------------------
20// ISurface drawing (same binary slots as menu.cpp / esp.cpp)
21// ---------------------------------------------------------------------------
22
23enum {
24 GS_DrawSetColor = 10,
25 GS_DrawFilledRect = 12,
26 GS_DrawOutlinedRect = 14,
27 GS_DrawLine = 15,
28 GS_DrawSetTextFont = 17,
29 GS_DrawSetTextColor = 18,
30 GS_DrawSetTextPos = 20,
31 GS_DrawPrintText = 22,
32 GS_GetScreenSize = 38,
33 GS_CreateFont = 66,
34 GS_AddGlyphSet = 67
35};
36
37static unsigned long g_font_body = 0;
38static unsigned long g_font_title = 0;
39static int g_body_h = 15;
40static int g_title_h = 20;
41static float g_font_scale = 0.0f;
42static int g_scr_w = 0, g_scr_h = 0;
43
44static void s_color(int r, int g, int b, int a) {
45 using Fn = void (*)(void *, int, int, int, int);
46 vfunc<Fn>(g_surface, GS_DrawSetColor)(g_surface, r, g, b, a);
47}
48static void s_fill(int x0, int y0, int x1, int y1) {
49 using Fn = void (*)(void *, int, int, int, int);
50 vfunc<Fn>(g_surface, GS_DrawFilledRect)(g_surface, x0, y0, x1, y1);
51}
52static void s_outline(int x0, int y0, int x1, int y1) {
53 using Fn = void (*)(void *, int, int, int, int);
54 vfunc<Fn>(g_surface, GS_DrawOutlinedRect)(g_surface, x0, y0, x1, y1);
55}
56static void s_line(int x0, int y0, int x1, int y1) {
57 using Fn = void (*)(void *, int, int, int, int);
58 vfunc<Fn>(g_surface, GS_DrawLine)(g_surface, x0, y0, x1, y1);
59}
60static void s_text_font(unsigned long f) {
61 using Fn = void (*)(void *, unsigned long);
62 vfunc<Fn>(g_surface, GS_DrawSetTextFont)(g_surface, f);
63}
64static void s_text_color(int r, int g, int b, int a) {
65 using Fn = void (*)(void *, int, int, int, int);
66 vfunc<Fn>(g_surface, GS_DrawSetTextColor)(g_surface, r, g, b, a);
67}
68static void s_text_pos(int x, int y) {
69 using Fn = void (*)(void *, int, int);
70 vfunc<Fn>(g_surface, GS_DrawSetTextPos)(g_surface, x, y);
71}
72static void s_print(const wchar_t *text, int n) {
73 using Fn = void (*)(void *, const wchar_t *, int, int);
74 vfunc<Fn>(g_surface, GS_DrawPrintText)(g_surface, text, n, 0);
75}
76static unsigned long s_create_font() {
77 using Fn = unsigned long (*)(void *);
78 return vfunc<Fn>(g_surface, GS_CreateFont)(g_surface);
79}
80static bool s_set_font(unsigned long font, const char *name, int tall, int weight,
81 int flags, int lo, int hi) {
82 using Fn = bool (*)(void *, unsigned long, const char *, int, int, int, int,
83 int, int, int);
84 return vfunc<Fn>(g_surface, GS_AddGlyphSet)(g_surface, font, name, tall,
85 weight, 0, 0, flags, lo, hi);
86}
87static void s_screen(int &w, int &h) {
88 using Fn = void (*)(void *, int &, int &);
89 vfunc<Fn>(g_surface, GS_GetScreenSize)(g_surface, w, h);
90}
91
92// ---------------------------------------------------------------------------
93// SDL polling
94// ---------------------------------------------------------------------------
95
96using GetKeysFn = const unsigned char *(*)(int *);
97using GetMouseFn = unsigned int (*)(int *, int *);
98using ShowCursorFn = int (*)(int);
99using SetRelativeFn = int (*)(int);
100using GetRelativeFn = int (*)(void);
101using AddWatchFn = int (*)(int (*)(void *, void *), void *);
102using GetTicksFn = unsigned int (*)(void);
103
104static GetKeysFn sd_get_keys = nullptr;
105static GetMouseFn sd_get_mouse = nullptr;
106static ShowCursorFn sd_show_cursor = nullptr;
107static SetRelativeFn sd_set_relative = nullptr;
108static GetRelativeFn sd_get_relative = nullptr;
109static AddWatchFn sd_add_watch = nullptr;
110static GetTicksFn sd_get_ticks = nullptr;
111static bool g_sdl_ready = false;
112static bool g_sdl_init_tried = false;
113
114static std::atomic<int> g_wheel{0};
115static int sdl_event_watch(void *, void *event) {
116 if (!event)
117 return 0;
118 const unsigned char *p = (const unsigned char *)event;
119 unsigned int type = *(const unsigned int *)p;
120 if (type == 0x403) { // SDL_MOUSEWHEEL
121 int y = *(const int *)(p + 20);
122 g_wheel.fetch_add(y);
123 }
124 return 0;
125}
126
127static void gui_init_sdl() {
128 if (g_sdl_init_tried)
129 return;
130 g_sdl_init_tried = true;
131 sd_get_keys = (GetKeysFn)dlsym(RTLD_DEFAULT, "SDL_GetKeyboardState");
132 sd_get_mouse = (GetMouseFn)dlsym(RTLD_DEFAULT, "SDL_GetMouseState");
133 sd_show_cursor = (ShowCursorFn)dlsym(RTLD_DEFAULT, "SDL_ShowCursor");
134 sd_set_relative = (SetRelativeFn)dlsym(RTLD_DEFAULT, "SDL_SetRelativeMouseMode");
135 sd_get_relative = (GetRelativeFn)dlsym(RTLD_DEFAULT, "SDL_GetRelativeMouseMode");
136 sd_add_watch = (AddWatchFn)dlsym(RTLD_DEFAULT, "SDL_AddEventWatch");
137 sd_get_ticks = (GetTicksFn)dlsym(RTLD_DEFAULT, "SDL_GetTicks");
138 g_sdl_ready = sd_get_keys && sd_get_mouse;
139 if (!g_sdl_ready) {
140 logf("gui: SDL polling unavailable");
141 return;
142 }
143 if (sd_add_watch)
144 sd_add_watch(&sdl_event_watch, nullptr);
145}
146
147// ---------------------------------------------------------------------------
148// Layout data (dummy shell)
149// ---------------------------------------------------------------------------
150
151struct Rect {
152 int x, y, w, h;
153 bool has(int px, int py) const {
154 return px >= x && py >= y && px < x + w && py < y + h;
155 }
156};
157struct Col {
158 int r, g, b, a;
159};
160static const Col C_BG{17, 19, 23, 248};
161static const Col C_PANEL{27, 30, 36, 248};
162static const Col C_SIDEBAR{21, 23, 28, 248};
163static const Col C_HEADER{36, 40, 48, 255};
164static const Col C_BORDER{70, 76, 88, 255};
165static const Col C_TEXT{229, 232, 238, 255};
166static const Col C_DIM{150, 156, 168, 255};
167static const Col C_ACCENT{255, 145, 64, 255};
168static const Col C_ACCENT_DIM{120, 70, 32, 255};
169static const Col C_GOOD{86, 210, 135, 255};
170
171enum CtrlKind {
172 CK_TOGGLE,
173 CK_SLIDER,
174 CK_BIND,
175 CK_SECTION,
176 CK_INFO,
177 CK_BUTTON
178};
179
180struct Ctrl {
181 int kind;
182 const char *label;
183 const char *desc;
184 int *value;
185 int min, max;
186};
187
188struct Card {
189 int cat;
190 const char *title;
191 const char *desc;
192 bool expanded;
193 bool active;
194 int bind;
195 int c0, n;
196};
197
198static Ctrl g_ctrls[192];
199static int g_nctrl = 0;
200static Card g_cards[40];
201static int g_ncards = 0;
202static int g_vals[192];
203static bool g_demo_built = false;
204
205static const char *g_cats[] = {"Movement", "Combat", "Visuals", "Weapons",
206 "System"};
207static constexpr int CAT_COUNT = 5;
208
209static int add_card(int cat, const char *title, const char *desc,
210 bool expanded) {
211 Card &c = g_cards[g_ncards];
212 c.cat = cat;
213 c.title = title;
214 c.desc = desc;
215 c.expanded = expanded;
216 c.active = true;
217 c.bind = 0;
218 c.c0 = g_nctrl;
219 c.n = 0;
220 return g_ncards++;
221}
222static void add_ctrl(int card, int kind, const char *label, const char *desc,
223 int min, int max, int init) {
224 int i = g_nctrl++;
225 g_vals[i] = init;
226 g_ctrls[i].kind = kind;
227 g_ctrls[i].label = label;
228 g_ctrls[i].desc = desc;
229 g_ctrls[i].value = &g_vals[i];
230 g_ctrls[i].min = min;
231 g_ctrls[i].max = max;
232 g_cards[card].n++;
233}
234static void add_toggle(int card, const char *label, const char *desc,
235 int init) {
236 add_ctrl(card, CK_TOGGLE, label, desc, 0, 1, init);
237}
238static void add_slider(int card, const char *label, const char *desc, int min,
239 int max, int init) {
240 add_ctrl(card, CK_SLIDER, label, desc, min, max, init);
241}
242static void add_section(int card, const char *label) {
243 add_ctrl(card, CK_SECTION, label, nullptr, 0, 0, 0);
244}
245static void add_info(int card, const char *label) {
246 add_ctrl(card, CK_INFO, label, nullptr, 0, 0, 0);
247}
248static void add_button(int card, const char *label, const char *desc) {
249 add_ctrl(card, CK_BUTTON, label, desc, 0, 0, 0);
250}
251
252static void build_demo() {
253 if (g_demo_built)
254 return;
255 g_demo_built = true;
256
257 int bhop = add_card(0, "Bunnyhop", "Automatic jump on landing.", true);
258 add_toggle(bhop, "Uni-directional auto-strafe",
259 "Rotate airborne movement into the velocity.", 1);
260
261 int abh = add_card(0, "ABH", "Accelerate on the jump tick by facing away "
262 "from the velocity.", true);
263 (void)abh;
264
265 int jb = add_card(0, "Jumpbug", "Duck+jump when landing would hurt.", true);
266 add_toggle(jb, "Assist timing",
267 "Nudge the final command into the unduck window.", 0);
268
269 int aim = add_card(1, "Aimbot", "Smoothly snap to a hostile target.", true);
270 g_cards[aim].bind = 604;
271 add_toggle(aim, "Vischeck", "Require world line of sight.", 1);
272 add_slider(aim, "FOV", "Maximum angular distance from the crosshair.", 1, 180,
273 60);
274 add_slider(aim, "Smoothness", "Maximum degrees per tick.", 1, 45, 5);
275 add_toggle(aim, "Show FOV circle", "Draw the acquisition cone on screen.", 1);
276 add_section(aim, "Bones");
277 add_toggle(aim, "Head", "Aim at the head hitbox.", 1);
278 add_toggle(aim, "Torso", "Aim at the chest/stomach.", 0);
279 add_toggle(aim, "Left arm", "Include the left arm.", 0);
280 add_toggle(aim, "Right arm", "Include the right arm.", 0);
281 add_toggle(aim, "Left leg", "Include the left leg.", 0);
282 add_toggle(aim, "Right leg", "Include the right leg.", 0);
283
284 int sil = add_card(1, "Silent aim", "Fire at the target without turning "
285 "the rendered view.", true);
286 g_cards[sil].bind = 602;
287 add_slider(sil, "FOV", "Maximum angular distance from the crosshair.", 1, 180,
288 60);
289
290 int tb = add_card(1, "Triggerbot", "Fire when the crosshair is on a hostile.",
291 true);
292 g_cards[tb].bind = 603;
293
294 int melee = add_card(1, "Melee killbot", "360-degree crowbar silent aim.",
295 true);
296
297 int esp = add_card(2, "ESP", "Draw hostile NPC information.", true);
298 add_toggle(esp, "Show neutral", "Include neutral wildlife.", 1);
299 add_toggle(esp, "Show healthbar", "Draw a health bar.", 1);
300 add_toggle(esp, "Show healthnum", "Draw the numeric health value.", 1);
301 add_toggle(esp, "Show name", "Draw the model name.", 1);
302 add_toggle(esp, "Show skeleton", "Draw the animated bone links.", 1);
303 add_toggle(esp, "Show box", "Draw a bounding box.", 1);
304 add_toggle(esp, "Show speed graph", "Draw the lower-center speed graph.", 1);
305
306 int spread = add_card(3, "Spread control",
307 "Choose a server shot seed that tightens the cone.",
308 true);
309 add_toggle(spread, "No spread",
310 "Override the FireBullets seed with the best offset.", 1);
311
312 int sys = add_card(4, "System", "Configuration and maintenance.", true);
313 add_info(sys, "Config: ~/.config/hl2hake/config");
314 add_info(sys, "Build: GUI shell preview");
315 add_button(sys, "Reload config", "Re-read the INI without restarting.");
316 add_button(sys, "Reset defaults", "Restore feature defaults; GUI layout kept.");
317
318 (void)g_cards[melee];
319}
320
321// ---------------------------------------------------------------------------
322// Window state / input
323// ---------------------------------------------------------------------------
324
325static bool g_open = false;
326static bool g_was_open = false;
327static bool g_dragging = false;
328static int g_drag_dx = 0, g_drag_dy = 0;
329static int g_win_x = 0, g_win_y = 0, g_win_w = 0, g_win_h = 0;
330static float g_scale = 1.0f;
331static bool g_positioned = false;
332static int g_cat = 0;
333static float g_scroll = 0.0f;
334static float g_scroll_max = 0.0f;
335static bool g_scroll_drag = false;
336static int g_scroll_drag_dy = 0;
337static float g_scroll_drag_start = 0.0f;
338static int g_hover_slider = -1;
339static int g_slider_drag = -1;
340static int g_capture_card = -1;
341static QAngle g_frozen{0, 0, 0};
342static int g_mouse_x = 0, g_mouse_y = 0;
343static bool g_mouse_click = false;
344static bool g_mouse_down = false;
345static bool g_mouse_rclick = false;
346static unsigned int g_prev_mask = 0;
347static unsigned char g_prev_keys[512] = {};
348static bool g_prev_cursor = true;
349static bool g_prev_relative = true;
350static double g_hover_since = 0.0;
351static const char *g_hover_desc = nullptr;
352static const char *g_hover_prev = nullptr;
353static int g_hover_x = 0, g_hover_y = 0;
354static int g_hover_last_mx = -1, g_hover_last_my = -1;
355
356static int S(float v) { return (int)(v * g_scale + 0.5f); }
357static float now_sec() {
358 struct timespec ts;
359 clock_gettime(CLOCK_MONOTONIC, &ts);
360 return (float)ts.tv_sec + (float)ts.tv_nsec * 1e-9f;
361}
362
363static void ensure_fonts() {
364 if (g_font_body && fabsf(g_font_scale - g_scale) < 0.001f)
365 return;
366 g_font_scale = g_scale;
367 g_body_h = (int)(15.0f * g_scale + 0.5f);
368 g_title_h = (int)(20.0f * g_scale + 0.5f);
369 if (g_body_h < 11) g_body_h = 11;
370 if (g_title_h < 15) g_title_h = 15;
371 static const char *families[] = {"Liberation Sans", "DejaVu Sans",
372 "Noto Sans", "Tahoma", "Verdana", "Arial",
373 nullptr};
374 if (!g_font_body) {
375 for (int i = 0; families[i] && !g_font_body; i++) {
376 unsigned long f = s_create_font();
377 if (!f)
378 break;
379 if (s_set_font(f, families[i], g_body_h, 400, 0x400, 0x20, 0xFF))
380 g_font_body = f;
381 }
382 }
383 if (!g_font_title) {
384 for (int i = 0; families[i] && !g_font_title; i++) {
385 unsigned long f = s_create_font();
386 if (!f)
387 break;
388 if (s_set_font(f, families[i], g_title_h, 400, 0x400, 0x20, 0xFF))
389 g_font_title = f;
390 }
391 }
392 if (!g_font_title)
393 g_font_title = g_font_body;
394 if (!g_font_body)
395 logf("gui: no usable body font");
396}
397
398static int text_w(const char *s, int tall) {
399 if (!s) return 0;
400 return (int)(strlen(s) * tall * 0.52f + 0.5f);
401}
402
403static void draw_text_raw(int x, int y, const char *s, unsigned long font,
404 int tall) {
405 (void)tall;
406 if (!s || !font)
407 return;
408 wchar_t w[256];
409 size_t n = mbstowcs(w, s, 255);
410 if (n == (size_t)-1)
411 return;
412 w[n] = 0;
413 s_text_font(font);
414 s_text_pos(x, y);
415 s_print(w, (int)n);
416}
417
418static void text_in(const Rect &clip, int x, int y, const char *s,
419 unsigned long font, int tall, int max_w, const Col &c) {
420 if (!s || !font)
421 return;
422 if (y < clip.y || y + tall > clip.y + clip.h)
423 return;
424 if (x >= clip.x + clip.w)
425 return;
426 if (x + text_w(s, tall) > clip.x + clip.w)
427 x = clip.x + clip.w - text_w(s, tall);
428 if (max_w > 0 && text_w(s, tall) > max_w) {
429 char buf[256];
430 snprintf(buf, sizeof(buf), "%s", s);
431 int n = (int)strlen(buf);
432 while (n > 3 && text_w(buf, tall) > max_w)
433 buf[--n] = 0;
434 if (n > 3 && n < (int)sizeof(buf) - 3) {
435 buf[n++] = '.';
436 buf[n++] = '.';
437 buf[n++] = '.';
438 buf[n] = 0;
439 }
440 s_text_color(c.r, c.g, c.b, c.a);
441 draw_text_raw(x, y, buf, font, tall);
442 return;
443 }
444 s_text_color(c.r, c.g, c.b, c.a);
445 draw_text_raw(x, y, s, font, tall);
446}
447
448static Rect clip_rect(const Rect &r, const Rect &clip) {
449 Rect o = r;
450 int x0 = r.x > clip.x ? r.x : clip.x;
451 int y0 = r.y > clip.y ? r.y : clip.y;
452 int x1 = (r.x + r.w) < (clip.x + clip.w) ? (r.x + r.w) : (clip.x + clip.w);
453 int y1 = (r.y + r.h) < (clip.y + clip.h) ? (r.y + r.h) : (clip.y + clip.h);
454 o.x = x0;
455 o.y = y0;
456 o.w = x1 - x0;
457 o.h = y1 - y0;
458 return o;
459}
460static void fill(const Rect &r, const Col &c, const Rect *clip = nullptr) {
461 if (r.w <= 0 || r.h <= 0)
462 return;
463 Rect q = clip ? clip_rect(r, *clip) : r;
464 if (q.w <= 0 || q.h <= 0)
465 return;
466 s_color(c.r, c.g, c.b, c.a);
467 s_fill(q.x, q.y, q.x + q.w, q.y + q.h);
468}
469static void outline(const Rect &r, const Col &c, const Rect *clip = nullptr) {
470 if (r.w <= 0 || r.h <= 0)
471 return;
472 Rect q = clip ? clip_rect(r, *clip) : r;
473 if (q.w <= 0 || q.h <= 0)
474 return;
475 s_color(c.r, c.g, c.b, c.a);
476 s_outline(q.x, q.y, q.x + q.w, q.y + q.h);
477}
478static void line(int x0, int y0, int x1, int y1, const Col &c) {
479 s_color(c.r, c.g, c.b, c.a);
480 s_line(x0, y0, x1, y1);
481}
482
483static const char *bind_label(int b) {
484 switch (b) {
485 case 0: return "None";
486 case 600: return "MouseL";
487 case 601: return "MouseR";
488 case 602: return "MouseM";
489 case 603: return "Mouse4";
490 case 604: return "Mouse5";
491 case 41: return "Esc";
492 case 73: return "Insert";
493 case 59: return "F2";
494 default: return "Key";
495 }
496}
497
498static void layout() {
499 int w = g_scr_w, h = g_scr_h;
500 if (g_surface) {
501 s_screen(w, h);
502 if (w > 0) g_scr_w = w;
503 if (h > 0) g_scr_h = h;
504 }
505 w = g_scr_w;
506 h = g_scr_h;
507 if (w <= 0 || h <= 0)
508 return;
509 float sc = fminf((float)w / 1920.0f, (float)h / 1080.0f);
510 if (sc < 0.75f) sc = 0.75f;
511 if (sc > 1.5f) sc = 1.5f;
512 g_scale = sc;
513 int nw = S(960.0f), nh = S(620.0f);
514 if (!g_positioned || nw != g_win_w || nh != g_win_h) {
515 if (!g_positioned) {
516 g_win_x = (w - nw) / 2;
517 g_win_y = (h - nh) / 2;
518 g_positioned = true;
519 }
520 g_win_w = nw;
521 g_win_h = nh;
522 }
523 if (g_win_x < 0) g_win_x = 0;
524 if (g_win_y < 0) g_win_y = 0;
525 if (g_win_x + g_win_w > w) g_win_x = w - g_win_w;
526 if (g_win_y + g_win_h > h) g_win_y = h - g_win_h;
527}
528
529static int row_h(const Ctrl &c) {
530 switch (c.kind) {
531 case CK_SECTION: return S(24.0f);
532 case CK_INFO: return S(20.0f);
533 case CK_BUTTON: return S(32.0f);
534 default: return S(27.0f);
535 }
536}
537
538static int body_h(const Card &c) {
539 if (!c.expanded)
540 return 0;
541 int h = S(6.0f);
542 for (int i = 0; i < c.n; i++)
543 h += row_h(g_ctrls[c.c0 + i]);
544 return h + S(6.0f);
545}
546
547static int card_h(const Card &c) { return S(36.0f) + body_h(c) + S(8.0f); }
548
549static Rect card_rect(int index, int y) {
550 Card &c = g_cards[index];
551 int x = g_win_x + S(190.0f) + S(10.0f);
552 int w = g_win_w - S(190.0f) - S(28.0f);
553 return Rect{x, y, w, card_h(c)};
554}
555
556static bool card_in_category(int i) { return g_cards[i].cat == g_cat; }
557
558static int content_height() {
559 int h = S(8.0f);
560 for (int i = 0; i < g_ncards; i++)
561 if (card_in_category(i))
562 h += card_h(g_cards[i]);
563 return h;
564}
565
566// ---------------------------------------------------------------------------
567// Input
568// ---------------------------------------------------------------------------
569
570static void close_gui() {
571 if (!g_open)
572 return;
573 g_open = false;
574 g_capture_card = -1;
575 g_dragging = false;
576 g_scroll_drag = false;
577 g_slider_drag = -1;
578 if (g_sdl_ready) {
579 if (sd_set_relative)
580 sd_set_relative(g_prev_relative ? 1 : 0);
581 if (sd_show_cursor)
582 sd_show_cursor(g_prev_cursor ? 1 : 0);
583 }
584 logf("gui: closed");
585}
586
587static void open_gui() {
588 if (g_open)
589 return;
590 int w = 0, h = 0;
591 if (g_surface) {
592 s_screen(w, h);
593 if (w > 0) g_scr_w = w;
594 if (h > 0) g_scr_h = h;
595 }
596 if (!g_iface.engine) {
597 g_frozen = QAngle{0, 0, 0};
598 } else {
599 using GetViewFn = void (*)(void *, QAngle *);
600 QAngle v{0, 0, 0};
601 vfunc<GetViewFn>(g_iface.engine, 19)(g_iface.engine, &v);
602 g_frozen = v;
603 }
604 if (g_sdl_ready) {
605 if (sd_get_relative)
606 g_prev_relative = sd_get_relative() != 0;
607 if (sd_show_cursor)
608 g_prev_cursor = sd_show_cursor(-1) != 0;
609 if (sd_set_relative)
610 sd_set_relative(0);
611 if (sd_show_cursor)
612 sd_show_cursor(1);
613 }
614 g_open = true;
615 g_capture_card = -1;
616 g_wheel.exchange(0);
617 g_scroll = 0.0f;
618 g_cat = 0;
619 menu_force_close();
620 logf("gui: opened");
621}
622
623static void update_mouse() {
624 if (!g_sdl_ready)
625 return;
626 int mx = 0, my = 0;
627 unsigned int mask = sd_get_mouse(&mx, &my);
628 g_mouse_x = mx;
629 g_mouse_y = my;
630 g_mouse_down = (mask & 1u) != 0;
631 g_mouse_click = g_mouse_down && !(g_prev_mask & 1u);
632 g_mouse_rclick = ((mask & 2u) != 0) && !(g_prev_mask & 2u);
633 if (g_mouse_x < 0) g_mouse_x = 0;
634 if (g_mouse_y < 0) g_mouse_y = 0;
635 g_prev_mask = mask;
636}
637
638static void handle_capture(const unsigned char *keys) {
639 if (g_capture_card < 0)
640 return;
641 if (!g_open) {
642 g_capture_card = -1;
643 return;
644 }
645 if (keys[41]) { // Esc cancels
646 g_capture_card = -1;
647 return;
648 }
649 if (g_mouse_click) {
650 g_cards[g_capture_card].bind = 600;
651 g_capture_card = -1;
652 return;
653 }
654 if (g_mouse_rclick) {
655 g_cards[g_capture_card].bind = 601;
656 g_capture_card = -1;
657 return;
658 }
659 for (int k = 0; k < 512; k++) {
660 if (!keys[k] || g_prev_keys[k])
661 continue;
662 if (k == 73 || k == 59) // Insert reserved; F2 closes the preview
663 continue;
664 g_cards[g_capture_card].bind = k;
665 g_capture_card = -1;
666 return;
667 }
668}
669
670static void handle_mouse_click() {
671 if (!g_mouse_click)
672 return;
673 int mx = g_mouse_x, my = g_mouse_y;
674
675 // title bar: drag or close
676 Rect title{g_win_x, g_win_y, g_win_w, S(38.0f)};
677 Rect close{g_win_x + g_win_w - S(38.0f), g_win_y, S(38.0f), S(38.0f)};
678 if (title.has(mx, my)) {
679 if (close.has(mx, my)) {
680 close_gui();
681 return;
682 }
683 g_dragging = true;
684 g_drag_dx = mx - g_win_x;
685 g_drag_dy = my - g_win_y;
686 return;
687 }
688
689 // sidebar
690 int side_x = g_win_x;
691 int side_y = g_win_y + S(38.0f);
692 int side_w = S(190.0f);
693 int cat_h = S(34.0f);
694 for (int i = 0; i < CAT_COUNT; i++) {
695 Rect r{side_x, side_y + i * cat_h, side_w, cat_h};
696 if (r.has(mx, my)) {
697 g_cat = i;
698 g_scroll = 0.0f;
699 return;
700 }
701 }
702
703 // content
704 Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f),
705 g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)};
706 if (!content.has(mx, my))
707 return;
708
709 // scrollbar
710 int sb_x = content.x + content.w - S(8.0f);
711 if (mx >= sb_x) {
712 if (g_scroll_max > 0.5f) {
713 int thumb_h = (int)((float)content.h * (float)content.h /
714 (float)(content.h + (int)g_scroll_max));
715 if (thumb_h < S(24.0f)) thumb_h = S(24.0f);
716 int travel = content.h - thumb_h;
717 int thumb_y = content.y + (int)((g_scroll_max > 0.0f)
718 ? (g_scroll / g_scroll_max) * travel
719 : 0);
720 if (my >= thumb_y && my < thumb_y + thumb_h) {
721 g_scroll_drag = true;
722 g_scroll_drag_dy = my - thumb_y;
723 g_scroll_drag_start = g_scroll;
724 } else {
725 float t = (float)(my - content.y) / (float)content.h;
726 g_scroll = t * g_scroll_max;
727 }
728 }
729 return;
730 }
731
732 int y = content.y - (int)g_scroll + S(8.0f);
733 for (int i = 0; i < g_ncards; i++) {
734 if (!card_in_category(i))
735 continue;
736 Card &c = g_cards[i];
737 Rect cr = card_rect(i, y);
738 int ch = card_h(c);
739 if (my >= cr.y && my < cr.y + ch) {
740 // header
741 if (my < cr.y + S(36.0f)) {
742 if (c.cat != 4) {
743 Rect active{cr.x + cr.w - S(150.0f), cr.y + S(9.0f), S(18.0f),
744 S(18.0f)};
745 Rect bind{cr.x + cr.w - S(112.0f), cr.y + S(7.0f), S(100.0f),
746 S(22.0f)};
747 if (active.has(mx, my)) {
748 c.active = !c.active;
749 return;
750 }
751 if (bind.has(mx, my)) {
752 if (mx >= bind.x + bind.w - S(18.0f)) {
753 c.bind = 0;
754 return;
755 }
756 g_capture_card = i;
757 return;
758 }
759 }
760 c.expanded = !c.expanded;
761 return;
762 }
763 // body
764 int by = cr.y + S(36.0f) + S(6.0f);
765 for (int j = 0; j < c.n; j++) {
766 Ctrl &ct = g_ctrls[c.c0 + j];
767 int rh = row_h(ct);
768 if (my < by || my >= by + rh) {
769 by += rh;
770 continue;
771 }
772 if (ct.kind == CK_TOGGLE) {
773 *ct.value = !*ct.value;
774 return;
775 }
776 if (ct.kind == CK_SLIDER) {
777 Rect track{cr.x + S(16.0f) + S(230.0f), by + S(9.0f),
778 cr.w - S(16.0f) - S(230.0f) - S(64.0f), S(8.0f)};
779 if (mx >= track.x && mx < track.x + track.w) {
780 float t = (float)(mx - track.x) / (float)track.w;
781 int v = ct.min + (int)(t * (ct.max - ct.min) + 0.5f);
782 if (v < ct.min) v = ct.min;
783 if (v > ct.max) v = ct.max;
784 *ct.value = v;
785 g_slider_drag = c.c0 + j;
786 }
787 return;
788 }
789 if (ct.kind == CK_BUTTON) {
790 Rect b{cr.x + cr.w - S(130.0f), by + S(3.0f), S(114.0f),
791 S(26.0f)};
792 if (b.has(mx, my)) {
793 logf("gui: dummy action %s", ct.label ? ct.label : "?");
794 return;
795 }
796 }
797 return;
798 }
799 }
800 y += ch;
801 }
802}
803
804static void handle_hover() {
805 g_hover_desc = nullptr;
806 g_hover_slider = -1;
807 int mx = g_mouse_x, my = g_mouse_y;
808 Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f),
809 g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)};
810 if (!content.has(mx, my))
811 return;
812 int y = content.y - (int)g_scroll + S(8.0f);
813 for (int i = 0; i < g_ncards; i++) {
814 if (!card_in_category(i))
815 continue;
816 Card &c = g_cards[i];
817 Rect cr = card_rect(i, y);
818 int ch = card_h(c);
819 if (my >= cr.y && my < cr.y + ch) {
820 if (my < cr.y + S(36.0f)) {
821 g_hover_desc = c.desc;
822 return;
823 }
824 int by = cr.y + S(36.0f) + S(6.0f);
825 for (int j = 0; j < c.n; j++) {
826 Ctrl &ct = g_ctrls[c.c0 + j];
827 int rh = row_h(ct);
828 if (my >= by && my < by + rh) {
829 g_hover_desc = ct.desc;
830 if (ct.kind == CK_SLIDER)
831 g_hover_slider = c.c0 + j;
832 return;
833 }
834 by += rh;
835 }
836 return;
837 }
838 y += ch;
839 }
840}
841
842void gui_handle_input() {
843 gui_init_sdl();
844 if (!g_surface)
845 return;
846 build_demo();
847 layout();
848 if (g_scr_w <= 0 || g_scr_h <= 0)
849 return;
850 ensure_fonts();
851 if (!g_sdl_ready)
852 return;
853
854 int nk = 0;
855 const unsigned char *keys = sd_get_keys(&nk);
856 if (!keys)
857 return;
858 if (nk > 512) nk = 512;
859 update_mouse();
860
861 bool f2 = (nk > 59) && keys[59];
862 bool f2_edge = f2 && !g_prev_keys[59];
863 bool esc = (nk > 41) && keys[41];
864 bool esc_edge = esc && !g_prev_keys[41];
865 bool left = (nk > 80) && keys[80];
866 bool right = (nk > 79) && keys[79];
867 bool left_edge = left && !g_prev_keys[80];
868 bool right_edge = right && !g_prev_keys[79];
869
870 if (f2_edge) {
871 if (g_open)
872 close_gui();
873 else
874 open_gui();
875 }
876 if (!g_open) {
877 memcpy(g_prev_keys, keys, nk);
878 g_was_open = false;
879 return;
880 }
881
882 if (esc_edge) {
883 if (g_capture_card >= 0)
884 g_capture_card = -1;
885 else
886 close_gui();
887 }
888
889 // Keep the native cursor free even if the game re-grabs it.
890 if (sd_set_relative)
891 sd_set_relative(0);
892 if (sd_show_cursor)
893 sd_show_cursor(1);
894
895 handle_capture(keys);
896
897 if (g_dragging) {
898 if (!g_mouse_down) {
899 g_dragging = false;
900 } else {
901 g_win_x = g_mouse_x - g_drag_dx;
902 g_win_y = g_mouse_y - g_drag_dy;
903 if (g_win_x < 0) g_win_x = 0;
904 if (g_win_y < 0) g_win_y = 0;
905 if (g_win_x + g_win_w > g_scr_w) g_win_x = g_scr_w - g_win_w;
906 if (g_win_y + g_win_h > g_scr_h) g_win_y = g_scr_h - g_win_h;
907 }
908 } else {
909 handle_mouse_click();
910 }
911
912 if (g_slider_drag >= 0) {
913 if (!g_mouse_down) {
914 g_slider_drag = -1;
915 } else {
916 Ctrl &ct = g_ctrls[g_slider_drag];
917 Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f),
918 g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)};
919 int x0 = g_win_x + S(190.0f) + S(16.0f) + S(230.0f);
920 int x1 = g_win_x + g_win_w - S(28.0f) - S(64.0f);
921 (void)content;
922 float t = (float)(g_mouse_x - x0) / (float)(x1 - x0);
923 int v = ct.min + (int)(t * (ct.max - ct.min) + 0.5f);
924 if (v < ct.min) v = ct.min;
925 if (v > ct.max) v = ct.max;
926 *ct.value = v;
927 }
928 }
929
930 if (g_scroll_drag) {
931 if (!g_mouse_down) {
932 g_scroll_drag = false;
933 } else {
934 Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f),
935 g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)};
936 int thumb_h = (int)((float)content.h * (float)content.h /
937 (float)(content.h + (int)g_scroll_max));
938 if (thumb_h < S(24.0f)) thumb_h = S(24.0f);
939 int travel = content.h - thumb_h;
940 if (travel > 0) {
941 float t = (float)(g_mouse_y - content.y - g_scroll_drag_dy) /
942 (float)travel;
943 g_scroll = t * g_scroll_max;
944 }
945 }
946 }
947
948 int wheel = g_wheel.exchange(0);
949 if (wheel != 0)
950 g_scroll -= wheel * S(44.0f);
951
952 if (left_edge || right_edge) {
953 if (g_hover_slider >= 0) {
954 Ctrl &ct = g_ctrls[g_hover_slider];
955 *ct.value += right_edge ? 1 : -1;
956 if (*ct.value < ct.min) *ct.value = ct.min;
957 if (*ct.value > ct.max) *ct.value = ct.max;
958 }
959 }
960
961 int total = content_height();
962 Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f),
963 g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)};
964 g_scroll_max = (float)(total + S(8.0f) - content.h);
965 if (g_scroll_max < 0.0f) g_scroll_max = 0.0f;
966 if (g_scroll < 0.0f) g_scroll = 0.0f;
967 if (g_scroll > g_scroll_max) g_scroll = g_scroll_max;
968
969 handle_hover();
970 {
971 double now = now_sec();
972 int dx = abs(g_mouse_x - g_hover_last_mx);
973 int dy = abs(g_mouse_y - g_hover_last_my);
974 if (g_hover_desc != g_hover_prev || dx > 2 || dy > 2) {
975 g_hover_prev = g_hover_desc;
976 g_hover_last_mx = g_mouse_x;
977 g_hover_last_my = g_mouse_y;
978 g_hover_since = now;
979 g_hover_x = g_mouse_x;
980 g_hover_y = g_mouse_y;
981 }
982 }
983
984 memcpy(g_prev_keys, keys, nk);
985 g_was_open = true;
986}
987
988// ---------------------------------------------------------------------------
989// Draw
990// ---------------------------------------------------------------------------
991
992static void draw_checkbox(int x, int y, bool on) {
993 Rect box{x, y, S(15.0f), S(15.0f)};
994 fill(box, on ? C_ACCENT : Col{30, 34, 42, 255});
995 outline(box, C_BORDER);
996 if (on) {
997 line(x + S(3.0f), y + S(8.0f), x + S(7.0f), y + S(12.0f), C_BG);
998 line(x + S(7.0f), y + S(12.0f), x + S(13.0f), y + S(4.0f), C_BG);
999 }
1000}
1001
1002static void draw_slider(const Rect &clip, const Rect &cr, int y, const Ctrl &ct) {
1003 int track_x = cr.x + S(16.0f) + S(230.0f);
1004 int track_w = cr.w - S(16.0f) - S(230.0f) - S(64.0f);
1005 if (track_w < S(30.0f)) track_w = S(30.0f);
1006 Rect track{track_x, y + S(9.0f), track_w, S(8.0f)};
1007 fill(track, Col{38, 42, 50, 255}, &clip);
1008 float t = (ct.max > ct.min)
1009 ? (float)(*ct.value - ct.min) / (float)(ct.max - ct.min)
1010 : 0.0f;
1011 int fw = (int)(track_w * t + 0.5f);
1012 fill(Rect{track.x, track.y, fw, track.h}, C_ACCENT_DIM, &clip);
1013 int kx = track.x + fw - S(4.0f);
1014 fill(Rect{kx, track.y - S(4.0f), S(8.0f), S(16.0f)}, C_ACCENT, &clip);
1015 char val[32];
1016 snprintf(val, sizeof(val), "%d", *ct.value);
1017 text_in(clip, cr.x + cr.w - S(52.0f), y + S(4.0f), val, g_font_body, g_body_h,
1018 S(44.0f), C_TEXT);
1019}
1020
1021static void draw_card(const Rect &clip, int index, int y) {
1022 Card &c = g_cards[index];
1023 Rect cr = card_rect(index, y);
1024 Rect bg = clip_rect(cr, clip);
1025 if (bg.w <= 0 || bg.h <= 0)
1026 return;
1027 fill(cr, C_PANEL, &clip);
1028 outline(cr, Col{52, 57, 67, 255}, &clip);
1029 Rect head{cr.x, cr.y, cr.w, S(36.0f)};
1030 fill(head, C_HEADER, &clip);
1031 // expand arrow
1032 int ax = cr.x + S(10.0f), ay = cr.y + S(13.0f);
1033 if (c.expanded) {
1034 line(ax, ay, ax + S(4.0f), ay + S(5.0f), C_DIM);
1035 line(ax + S(4.0f), ay + S(5.0f), ax + S(8.0f), ay, C_DIM);
1036 } else {
1037 line(ax, ay, ax + S(5.0f), ay + S(4.0f), C_DIM);
1038 line(ax + S(5.0f), ay + S(4.0f), ax, ay + S(8.0f), C_DIM);
1039 }
1040 text_in(clip, cr.x + S(24.0f), cr.y + S(8.0f), c.title, g_font_title,
1041 g_title_h, cr.w - S(180.0f), C_TEXT);
1042 if (c.cat != 4) {
1043 draw_checkbox(cr.x + cr.w - S(150.0f), cr.y + S(10.0f), c.active);
1044 text_in(clip, cr.x + cr.w - S(128.0f), cr.y + S(9.0f),
1045 c.active ? "On" : "Off", g_font_body, g_body_h, S(30.0f),
1046 c.active ? C_GOOD : C_DIM);
1047 Rect bind{cr.x + cr.w - S(112.0f), cr.y + S(7.0f), S(100.0f), S(22.0f)};
1048 fill(bind, Col{45, 50, 60, 255}, &clip);
1049 outline(bind, C_BORDER, &clip);
1050 const char *bl = (g_capture_card == index) ? "Press..." : bind_label(c.bind);
1051 text_in(clip, bind.x + S(7.0f), bind.y + S(3.0f), bl, g_font_body, g_body_h,
1052 bind.w - S(22.0f), C_TEXT);
1053 text_in(clip, bind.x + bind.w - S(16.0f), bind.y + S(3.0f), "x",
1054 g_font_body, g_body_h, S(10.0f), C_DIM);
1055 }
1056 if (!c.expanded)
1057 return;
1058
1059 int by = cr.y + S(36.0f) + S(6.0f);
1060 for (int j = 0; j < c.n; j++) {
1061 Ctrl &ct = g_ctrls[c.c0 + j];
1062 int rh = row_h(ct);
1063 if (ct.kind == CK_SECTION) {
1064 text_in(clip, cr.x + S(16.0f), by + S(3.0f), ct.label, g_font_body,
1065 g_body_h, cr.w - S(24.0f), C_ACCENT);
1066 } else if (ct.kind == CK_INFO) {
1067 text_in(clip, cr.x + S(16.0f), by + S(1.0f), ct.label, g_font_body,
1068 g_body_h, cr.w - S(24.0f), C_DIM);
1069 } else if (ct.kind == CK_BUTTON) {
1070 Rect b{cr.x + cr.w - S(130.0f), by + S(3.0f), S(114.0f), S(26.0f)};
1071 fill(b, Col{45, 50, 60, 255}, &clip);
1072 outline(b, C_BORDER, &clip);
1073 text_in(clip, b.x + S(10.0f), b.y + S(4.0f), ct.label, g_font_body,
1074 g_body_h, b.w - S(16.0f), C_TEXT);
1075 } else {
1076 text_in(clip, cr.x + S(16.0f), by + S(4.0f), ct.label, g_font_body,
1077 g_body_h, S(220.0f), C_TEXT);
1078 if (ct.kind == CK_TOGGLE) {
1079 draw_checkbox(cr.x + cr.w - S(31.0f), by + S(5.0f), *ct.value != 0);
1080 text_in(clip, cr.x + cr.w - S(58.0f), by + S(3.0f),
1081 *ct.value ? "On" : "Off", g_font_body, g_body_h, S(26.0f),
1082 *ct.value ? C_GOOD : C_DIM);
1083 } else if (ct.kind == CK_SLIDER) {
1084 draw_slider(clip, cr, by, ct);
1085 }
1086 }
1087 by += rh;
1088 }
1089}
1090
1091static void draw_tooltip() {
1092 if (!g_hover_desc || !g_hover_desc[0])
1093 return;
1094 float d = now_sec() - (float)g_hover_since;
1095 if (d < 0.4f)
1096 return;
1097 int tw = text_w(g_hover_desc, g_body_h) + S(16.0f);
1098 int maxw = S(380.0f);
1099 if (tw > maxw) tw = maxw;
1100 int th = g_body_h + S(14.0f);
1101 int x = g_hover_x + S(14.0f);
1102 int y = g_hover_y + S(16.0f);
1103 if (x + tw > g_scr_w - S(4.0f)) x = g_hover_x - tw - S(10.0f);
1104 if (y + th > g_scr_h - S(4.0f)) y = g_hover_y - th - S(10.0f);
1105 Rect r{x, y, tw, th};
1106 fill(r, Col{12, 14, 18, 248});
1107 outline(r, C_ACCENT_DIM);
1108 Rect inner{x + S(7.0f), y + S(6.0f), tw - S(14.0f), g_body_h};
1109 text_in(inner, inner.x, inner.y, g_hover_desc, g_font_body, g_body_h,
1110 inner.w, C_TEXT);
1111}
1112
1113void gui_draw() {
1114 if (!g_open || !g_surface)
1115 return;
1116 layout();
1117 ensure_fonts();
1118 int w = g_scr_w, h = g_scr_h;
1119 if (w <= 0 || h <= 0)
1120 return;
1121 Rect win{g_win_x, g_win_y, g_win_w, g_win_h};
1122 fill(Rect{win.x + S(6.0f), win.y + S(6.0f), win.w, win.h},
1123 Col{0, 0, 0, 100});
1124 fill(win, C_BG);
1125 outline(win, C_BORDER);
1126 Rect title{win.x, win.y, win.w, S(38.0f)};
1127 fill(title, Col{31, 35, 42, 255});
1128 fill(Rect{win.x, win.y, S(4.0f), win.h}, C_ACCENT);
1129 text_in(win, win.x + S(16.0f), win.y + S(7.0f), "HL2hake", g_font_title,
1130 g_title_h, win.w - S(80.0f), C_TEXT);
1131 text_in(win, win.x + S(110.0f), win.y + S(10.0f), "GUI shell preview",
1132 g_font_body, g_body_h, win.w - S(180.0f), C_DIM);
1133 Rect close{win.x + win.w - S(38.0f), win.y, S(38.0f), S(38.0f)};
1134 fill(close, Col{45, 48, 56, 255});
1135 line(close.x + S(13.0f), close.y + S(12.0f), close.x + S(25.0f),
1136 close.y + S(25.0f), C_DIM);
1137 line(close.x + S(25.0f), close.y + S(12.0f), close.x + S(13.0f),
1138 close.y + S(25.0f), C_DIM);
1139
1140 Rect side{win.x, win.y + S(38.0f), S(190.0f),
1141 win.h - S(38.0f) - S(30.0f)};
1142 fill(side, C_SIDEBAR);
1143 int cat_h = S(34.0f);
1144 for (int i = 0; i < CAT_COUNT; i++) {
1145 Rect r{side.x, side.y + i * cat_h, side.w, cat_h};
1146 if (i == g_cat) {
1147 fill(r, Col{42, 45, 54, 255});
1148 fill(Rect{r.x, r.y, S(3.0f), r.h}, C_ACCENT);
1149 }
1150 text_in(side, r.x + S(18.0f), r.y + S(8.0f), g_cats[i], g_font_body,
1151 g_body_h, r.w - S(24.0f), i == g_cat ? C_TEXT : C_DIM);
1152 }
1153
1154 Rect content{win.x + S(190.0f), win.y + S(38.0f),
1155 win.w - S(190.0f), win.h - S(38.0f) - S(30.0f)};
1156 fill(content, Col{20, 22, 27, 255});
1157 Rect clip{content.x + S(4.0f), content.y + S(4.0f), content.w - S(20.0f),
1158 content.h - S(8.0f)};
1159 int y = content.y - (int)g_scroll + S(8.0f);
1160 for (int i = 0; i < g_ncards; i++) {
1161 if (!card_in_category(i))
1162 continue;
1163 Card &c = g_cards[i];
1164 int ch = card_h(c);
1165 if (y + ch >= clip.y && y <= clip.y + clip.h)
1166 draw_card(clip, i, y);
1167 y += ch;
1168 }
1169
1170 // scrollbar
1171 if (g_scroll_max > 0.5f) {
1172 Rect track{content.x + content.w - S(8.0f), content.y, S(8.0f),
1173 content.h};
1174 fill(track, Col{30, 33, 40, 255});
1175 int thumb_h = (int)((float)content.h * (float)content.h /
1176 (float)(content.h + (int)g_scroll_max));
1177 if (thumb_h < S(24.0f)) thumb_h = S(24.0f);
1178 int travel = content.h - thumb_h;
1179 int thumb_y = content.y + (int)((g_scroll_max > 0.0f)
1180 ? (g_scroll / g_scroll_max) * travel
1181 : 0);
1182 fill(Rect{track.x, thumb_y, track.w, thumb_h}, C_ACCENT_DIM);
1183 }
1184
1185 Rect foot{win.x, win.y + win.h - S(30.0f), win.w, S(30.0f)};
1186 fill(foot, Col{24, 27, 33, 255});
1187 line(foot.x, foot.y, foot.x + foot.w, foot.y, C_BORDER);
1188 text_in(foot, foot.x + S(12.0f), foot.y + S(6.0f),
1189 "config: ~/.config/hl2hake/config", g_font_body, g_body_h,
1190 foot.w - S(220.0f), C_DIM);
1191 text_in(foot, foot.x + foot.w - S(200.0f), foot.y + S(6.0f),
1192 g_capture_card >= 0 ? "press a key / Esc" : "F2 close | Insert text",
1193 g_font_body, g_body_h, S(190.0f), C_DIM);
1194
1195 draw_tooltip();
1196}
1197
1198bool gui_is_open() { return g_open; }
1199QAngle gui_frozen_view() { return g_frozen; }
diff --git a/src/hl2h.cpp b/src/hl2h.cpp
index c4f8aad..c0c27af 100644
--- a/src/hl2h.cpp
+++ b/src/hl2h.cpp
@@ -540,6 +540,25 @@ static void do_move(int seq, float frametime) {
540 bool inwater = (flags & FL_INWATER) != 0; 540 bool inwater = (flags & FL_INWATER) != 0;
541 const float *vel = (const float *)(e + g_net.velocity); 541 const float *vel = (const float *)(e + g_net.velocity);
542 542
543 if (gui_is_open()) {
544 QAngle f = gui_frozen_view();
545 g_cam_view = Vector{f.x, f.y, f.z};
546 cmd->viewangles[0] = f.x;
547 cmd->viewangles[1] = f.y;
548 cmd->viewangles[2] = f.z;
549 cmd->forwardmove = 0.0f;
550 cmd->sidemove = 0.0f;
551 cmd->upmove = 0.0f;
552 cmd->buttons = 0;
553 cmd->impulse = 0;
554 cmd->weaponselect = 0;
555 cmd->weaponsubtype = 0;
556 cmd->mousedx = 0;
557 cmd->mousedy = 0;
558 note_modified(cmd, flags, vel);
559 return;
560 }
561
543 bool bhop_on = feature_active(FEAT_BHOP); 562 bool bhop_on = feature_active(FEAT_BHOP);
544 bool abh_on = feature_active(FEAT_ABH); 563 bool abh_on = feature_active(FEAT_ABH);
545 564
diff --git a/src/hl2h.h b/src/hl2h.h
index 285b591..11b9014 100644
--- a/src/hl2h.h
+++ b/src/hl2h.h
@@ -230,6 +230,11 @@ bool head_position(int idx, Vector &out);
230int aim_points(int idx, unsigned group_mask, Vector *out, int max); 230int aim_points(int idx, unsigned group_mask, Vector *out, int max);
231 231
232void outer_draw(); 232void outer_draw();
233void menu_force_close();
234void gui_handle_input();
235void gui_draw();
236bool gui_is_open();
237QAngle gui_frozen_view();
233// ISurface pointer (defined in menu.cpp, used for drawing tests) 238// ISurface pointer (defined in menu.cpp, used for drawing tests)
234struct ISurface; 239struct ISurface;
235extern ISurface *g_surface; 240extern ISurface *g_surface;
diff --git a/src/menu.cpp b/src/menu.cpp
index 9dffc9e..56fe6fa 100644
--- a/src/menu.cpp
+++ b/src/menu.cpp
@@ -625,9 +625,15 @@ int silentaim_fov() { return g_silent_fov; }
625 625
626bool aimbot_fov_circle() { return g_aim_fov_circle; } 626bool aimbot_fov_circle() { return g_aim_fov_circle; }
627 627
628void menu_force_close() {
629 g_menu_open = false;
630 g_bind_mode = false;
631 g_insert_latch = false;
632 g_menu_draw_logged = false;
633}
634
628static void menu_handle_input() { 635static void menu_handle_input() {
629 poll_keyboard(); 636 poll_keyboard();
630
631 // SDL state can be sampled several times per rendered frame (both ISurface 637 // SDL state can be sampled several times per rendered frame (both ISurface
632 // hooks call outer_draw). Use a physical-key latch instead of the one-poll 638 // hooks call outer_draw). Use a physical-key latch instead of the one-poll
633 // edge so one press cannot open and close the menu in the same frame. 639 // edge so one press cannot open and close the menu in the same frame.
@@ -838,11 +844,13 @@ void draw_text_col(int x, int y, const char *text, int r, int g, int b) {
838// (see hkdDrawPrintText hook in hl2h.cpp): we wrap our draws in 844// (see hkdDrawPrintText hook in hl2h.cpp): we wrap our draws in
839// PushMakeCurrent/PopMakeCurrent so they are submitted with a clean clip. 845// PushMakeCurrent/PopMakeCurrent so they are submitted with a clean clip.
840void outer_draw() { 846void outer_draw() {
841 menu_handle_input(); 847 gui_handle_input();
848 if (!gui_is_open())
849 menu_handle_input();
842 // HL2H_DRAW_TEST=1 draws a probe rect without needing the menu open 850 // HL2H_DRAW_TEST=1 draws a probe rect without needing the menu open
843 static bool draw_test = getenv("HL2H_DRAW_TEST") != nullptr; 851 static bool draw_test = getenv("HL2H_DRAW_TEST") != nullptr;
844 bool esp_on = feature_active(FEAT_ESP); 852 bool esp_on = feature_active(FEAT_ESP);
845 if (!g_surface || (!g_menu_open && !draw_test && !esp_on)) 853 if (!g_surface || (!g_menu_open && !gui_is_open() && !draw_test && !esp_on))
846 return; 854 return;
847 855
848 // Level/menu -> game transitions can invalidate the VGUI font handle. 856 // Level/menu -> game transitions can invalidate the VGUI font handle.
@@ -896,6 +904,11 @@ void outer_draw() {
896 surf_draw_set_color(255, 255, 255, 255); 904 surf_draw_set_color(255, 255, 255, 255);
897 draw_text(28, 36, "HL2H DRAW TEST"); 905 draw_text(28, 36, "HL2H DRAW TEST");
898 } 906 }
907 if (gui_is_open()) {
908 gui_draw();
909 surf_pop_make_current(panel);
910 return;
911 }
899 if (!g_menu_open) { 912 if (!g_menu_open) {
900 surf_pop_make_current(panel); 913 surf_pop_make_current(panel);
901 return; 914 return;