summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CONTEXT.md28
-rwxr-xr-xbuild.sh2
-rw-r--r--docs/adr/0001-custom-isurface-gui.md58
-rw-r--r--src/gui.cpp1199
-rw-r--r--src/hl2h.cpp19
-rw-r--r--src/hl2h.h5
-rw-r--r--src/menu.cpp19
7 files changed, 1326 insertions, 4 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index 861fbf8..1c43b1f 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -99,6 +99,10 @@ Starts fully collapsed on first open. `Insert` toggles; `Up`/`Down` navigate;
99Bind rows enter bind-select mode: press a key/button to assign, `Escape` sets 99Bind rows enter bind-select mode: press a key/button to assign, `Escape` sets
100None (always-on), `Enter` cancels. 100None (always-on), `Enter` cancels.
101 101
102> The text menu below is being replaced by the GUI described in the next
103> section. The old tree is kept only until the GUI reaches parity. The current
104> preview shell opens with F2; Insert still opens the text tree.
105
102``` 106```
103HL2hake 107HL2hake
104 Bunnyhop 108 Bunnyhop
@@ -136,6 +140,30 @@ HL2hake
136 140
137Binds use SDL scancodes; mouse buttons are pseudo-scancodes 600..604. 141Binds use SDL scancodes; mouse buttons are pseudo-scancodes 600..604.
138 142
143## GUI vocabulary / decisions
144
145- **GUI**: custom immediate-mode settings surface drawn through the existing
146 `ISurface` paint path. No Dear ImGui, no OpenGL hook, no new runtime
147 dependency.
148- **Category**: sidebar group. Movement, Combat, Visuals, Weapons, System.
149- **Card**: per-feature panel. Header always shows Active + Bind; body is a
150 collapsible set of settings.
151- **Freeze**: while the GUI is open, the usercmd movement/buttons/impulses/view
152 are held and combat features are skipped; the game keeps simulating and
153 ESP/FOV circle/speed graph keep drawing. No invulnerability.
154- **Native cursor**: on open, leave SDL relative-mouse mode and show the OS
155 cursor; restore on close. Absolute mouse coordinates are used.
156- **Bind capture**: click Bind, press a key/button; Esc or clicking elsewhere
157 cancels; Clear sets None; Insert is reserved for the GUI.
158- **UI scale**: `clamp(min(screen_w/1920, screen_h/1080), 0.75, 1.5)`; base
159 window 960x620, body/title fonts 15/20 px at scale 1.
160- Every feature gets Active + Bind; bind None means always-on.
161- System page: config path/build info, Reload config, and a two-click Reset
162 that restores feature settings/binds but keeps GUI layout.
163- Window: fixed-size, draggable by title bar, clamped to screen, position,
164 category and expanded cards persisted under `[gui]`.
165
166
139## Bones / ESP 167## Bones / ESP
140 168
141- `SetupBones(BONE_USED_BY_ANYTHING)` with engine `Time()`; studio header via 169- `SetupBones(BONE_USED_BY_ANYTHING)` with engine `Time()`; studio header via
diff --git a/build.sh b/build.sh
index 69dad99..b72bd2f 100755
--- a/build.sh
+++ b/build.sh
@@ -3,6 +3,6 @@ set -e
3cd "$(dirname "$0")" 3cd "$(dirname "$0")"
4g++ -m32 -O2 -std=c++17 -Wall -Wextra -Werror -fPIC -shared -fvisibility=hidden -fno-omit-frame-pointer \ 4g++ -m32 -O2 -std=c++17 -Wall -Wextra -Werror -fPIC -shared -fvisibility=hidden -fno-omit-frame-pointer \
5 -o libhl2hake.so \ 5 -o libhl2hake.so \
6 src/hl2h.cpp src/modules.cpp src/cinput.cpp src/netvars.cpp src/menu.cpp src/esp.cpp src/server.cpp src/bones.cpp src/vischeck.cpp \ 6 src/hl2h.cpp src/modules.cpp src/cinput.cpp src/netvars.cpp src/menu.cpp src/gui.cpp src/esp.cpp src/server.cpp src/bones.cpp src/vischeck.cpp \
7 -ldl 7 -ldl
8file libhl2hake.so 8file libhl2hake.so
diff --git a/docs/adr/0001-custom-isurface-gui.md b/docs/adr/0001-custom-isurface-gui.md
new file mode 100644
index 0000000..9da3c0c
--- /dev/null
+++ b/docs/adr/0001-custom-isurface-gui.md
@@ -0,0 +1,58 @@
1# 0001 — Custom immediate-mode GUI on ISurface
2
3- Status: Accepted
4- Date: 2026-10-27
5
6## Context
7
8The text-only menu is becoming cumbersome as features accumulate. The cheat
9already has a proven overlay draw path through `ISurface` (`PaintTraverseEx`),
10native text with the bundled Liberation Sans font, text metrics, filled/outlined
11rects, lines and textured polygons. The game runs Source on Linux with a
1232-bit process; the cheat is a single `LD_PRELOAD` shared object.
13
14The main alternatives were:
15
161. Dear ImGui with its standard OpenGL renderer, hooked into the game's
17 swap/present path.
182. Dear ImGui rendered through `ISurface`.
193. A custom immediate-mode GUI on `ISurface`.
204. Keep the text menu and add categories/pages.
21
22`ISurface::Vertex_t` only carries position and UV, not per-vertex color, so a
23standard ImGui draw-list renderer would lose per-widget text colors unless
24draws were split or text was rendered through a separate path. An OpenGL ImGui
25backend would require locating and hooking the swap path, saving/restoring GL
26state, uploading the font atlas, and integrating SDL input events — an
27unverified risk in this build. The existing ISurface path already draws in-game
28and in menus without a new graphics context.
29
30## Decision
31
32Build a custom immediate-mode GUI directly on the existing `ISurface` draw
33path.
34
35- Fixed dark theme with automatic scale:
36 `clamp(min(screen_w/1920, screen_h/1080), 0.75, 1.5)`.
37- Base window 960x620, body/title fonts 15/20 px at scale 1.
38- Left sidebar categories (Movement, Combat, Visuals, Weapons, System) and a
39 content pane of feature cards with collapsible settings.
40- Native OS cursor by releasing SDL relative-mouse mode while the GUI is open.
41- While open, freeze usercmd input/view and skip combat features; keep the
42 game simulating and keep ESP/FOV circle/speed graph drawing.
43- Replace the text menu with the GUI. No permanent fallback menu; git history
44 retains the old implementation.
45
46## Consequences
47
48- We own the widget set (scrollbar, sliders, toggles, keybind capture, tooltips)
49 and the layout/clipping code. Text clipping and wheel events need explicit
50 handling because ISurface has no clip-rect API and SDL does not expose wheel
51 state by polling.
52- No new runtime dependency or GL context management.
53- The GUI remains inside the engine's VGUI paint path, so it inherits the
54 existing font registration and in-game overlay behavior.
55- The old text menu is removed once the GUI reaches feature parity. During
56 development the GUI is built alongside the text menu and opened with a
57 temporary F2 key; the replacement is complete only when the old tree is
58 deleted.
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;