// gui.cpp — custom immediate-mode GUI // // Categories, feature cards and controls are wired to the real settings in // menu.cpp. This is the only menu; Insert toggles it. #include "hl2h.h" #include #include #include #include #include #include #include #include #include // --------------------------------------------------------------------------- // ISurface drawing (same binary slots as menu.cpp / esp.cpp) // --------------------------------------------------------------------------- // ISurface access lives in overlay.cpp. These thin wrappers keep the GUI code // readable without repeating the vtable indices in this file. static unsigned long g_font_body = 0; static unsigned long g_font_title = 0; static int g_body_h = 15; static int g_title_h = 20; static float g_font_scale = 0.0f; static void *g_font_local = (void *)-1; static bool g_font_fail_logged = false; static int g_scr_w = 0, g_scr_h = 0; static void s_color(int r, int g, int b, int a) { overlay_color(r, g, b, a); } static void s_fill(int x0, int y0, int x1, int y1) { overlay_fill(x0, y0, x1, y1); } static void s_line(int x0, int y0, int x1, int y1) { overlay_line(x0, y0, x1, y1); } static void s_text_font(HFont f) { overlay_text_font(f); } static void s_text_color(int r, int g, int b, int a) { overlay_text_color(r, g, b, a); } static void s_text_pos(int x, int y) { overlay_text_pos(x, y); } static void s_print(const wchar_t *text, int n) { overlay_print_text(text, n); } static HFont s_create_font() { return overlay_create_font(); } static bool s_set_font(HFont font, const char *name, int tall, int weight, int flags, int lo, int hi) { return overlay_add_glyph_set(font, name, tall, weight, 0, 0, flags, lo, hi); } static void s_screen(int &w, int &h) { overlay_screen_size(w, h); } static void s_text_size(HFont font, const wchar_t *text, int &w, int &h) { overlay_text_size(font, text, w, h); } // --------------------------------------------------------------------------- // SDL polling // --------------------------------------------------------------------------- using GetKeysFn = const unsigned char *(*)(int *); using GetMouseFn = unsigned int (*)(int *, int *); using ShowCursorFn = int (*)(int); using SetRelativeFn = int (*)(int); using GetRelativeFn = int (*)(void); using AddWatchFn = int (*)(int (*)(void *, void *), void *); using GetTicksFn = unsigned int (*)(void); static GetKeysFn sd_get_keys = nullptr; static GetMouseFn sd_get_mouse = nullptr; static ShowCursorFn sd_show_cursor = nullptr; static SetRelativeFn sd_set_relative = nullptr; static GetRelativeFn sd_get_relative = nullptr; static AddWatchFn sd_add_watch = nullptr; static GetTicksFn sd_get_ticks = nullptr; static bool g_sdl_ready = false; static bool g_sdl_init_tried = false; static std::atomic g_wheel{0}; static int sdl_event_watch(void *, void *event) { if (!event) return 0; const unsigned char *p = (const unsigned char *)event; unsigned int type = *(const unsigned int *)p; if (type == 0x403) { // SDL_MOUSEWHEEL int y = *(const int *)(p + 20); g_wheel.fetch_add(y); } return 0; } static void gui_init_sdl() { if (g_sdl_init_tried) return; g_sdl_init_tried = true; sd_get_keys = (GetKeysFn)dlsym(RTLD_DEFAULT, "SDL_GetKeyboardState"); sd_get_mouse = (GetMouseFn)dlsym(RTLD_DEFAULT, "SDL_GetMouseState"); // RTLD_NEXT, not RTLD_DEFAULT: this object is LD_PRELOADed first, so // RTLD_DEFAULT resolves to our own exported wrapper and the "real" pointer // would point back into this file. sd_show_cursor = (ShowCursorFn)dlsym(RTLD_NEXT, "SDL_ShowCursor"); sd_set_relative = (SetRelativeFn)dlsym(RTLD_NEXT, "SDL_SetRelativeMouseMode"); sd_get_relative = (GetRelativeFn)dlsym(RTLD_NEXT, "SDL_GetRelativeMouseMode"); sd_add_watch = (AddWatchFn)dlsym(RTLD_DEFAULT, "SDL_AddEventWatch"); sd_get_ticks = (GetTicksFn)dlsym(RTLD_DEFAULT, "SDL_GetTicks"); g_sdl_ready = sd_get_keys && sd_get_mouse; if (!g_sdl_ready) { logf("gui: SDL polling unavailable"); toastf(TOAST_ERROR, "GUI input unavailable: SDL not found"); return; } if (sd_add_watch) sd_add_watch(&sdl_event_watch, nullptr); } // --------------------------------------------------------------------------- // Layout data (dummy shell) // --------------------------------------------------------------------------- struct Rect { int x, y, w, h; bool has(int px, int py) const { return px >= x && py >= y && px < x + w && py < y + h; } }; struct Col { int r, g, b, a; }; static const Col C_BG{17, 19, 23, 248}; static const Col C_PANEL{27, 30, 36, 248}; static const Col C_SIDEBAR{21, 23, 28, 248}; static const Col C_HEADER{36, 40, 48, 255}; static const Col C_BORDER{70, 76, 88, 255}; static const Col C_TEXT{229, 232, 238, 255}; static const Col C_DIM{150, 156, 168, 255}; static const Col C_ACCENT{255, 145, 64, 255}; static const Col C_ACCENT_DIM{120, 70, 32, 255}; static const Col C_GOOD{86, 210, 135, 255}; enum CtrlKind { CK_TOGGLE, CK_SLIDER, CK_BIND, CK_SECTION, CK_INFO, CK_BUTTON, CK_COLOR, CK_DROPDOWN }; enum CtrlAction { ACT_NONE = 0, ACT_RELOAD, ACT_RESET, ACT_SKY_RELOAD }; struct Ctrl { int kind; const char *label; const char *desc; bool *bval; // CK_TOGGLE int *ival; // CK_SLIDER int *rgb; // CK_COLOR: three or four ints, 0..255 (see has_alpha) bool has_alpha; // CK_COLOR: rgb[3] is an editable alpha channel const char *const *opts; // CK_DROPDOWN int nopts; // CK_DROPDOWN int min, max; int action; // CK_BUTTON bool zero_unlimited; // slider: value 0 displays as "All" // CK_COLOR picker state, kept while the picker is open so grey colours do // not lose their hue when the user drags value/saturation. float hue, sat, val; }; struct Card { int cat; FeatureState *feat; // nullptr for non-feature cards (System) const char *title; const char *desc; bool expanded; int c0, n; }; static Ctrl g_ctrls[192]; static int g_nctrl = 0; static Card g_cards[40]; static int g_ncards = 0; static bool g_cards_built = false; static bool g_layout_loaded = false; static bool g_reset_armed = false; static double g_reset_armed_at = 0.0; static char g_config_label[512]; static char g_build_label[128]; static const char *g_cats[] = {"Movement", "Combat", "Visuals", "Weapons", "System"}; static constexpr int CAT_COUNT = 5; static int add_card(int cat, FeatureState *feat, const char *title, const char *desc, bool expanded) { Card &c = g_cards[g_ncards]; c.cat = cat; c.feat = feat; c.title = title; c.desc = desc; c.expanded = expanded; c.c0 = g_nctrl; c.n = 0; return g_ncards++; } static void add_ctrl(int card, int kind, const char *label, const char *desc, int min, int max, bool *bval, int *ival, int action) { int i = g_nctrl++; Ctrl &c = g_ctrls[i]; c.kind = kind; c.label = label; c.desc = desc; c.bval = bval; c.ival = ival; c.min = min; c.max = max; c.action = action; c.zero_unlimited = false; c.rgb = nullptr; c.has_alpha = false; c.opts = nullptr; c.nopts = 0; c.hue = 0.0f; c.sat = 0.0f; c.val = 1.0f; g_cards[card].n++; } static void add_toggle(int card, const char *label, const char *desc, bool *value) { add_ctrl(card, CK_TOGGLE, label, desc, 0, 1, value, nullptr, ACT_NONE); } static void add_slider(int card, const char *label, const char *desc, int min, int max, int *value) { add_ctrl(card, CK_SLIDER, label, desc, min, max, nullptr, value, ACT_NONE); } static void add_section(int card, const char *label) { add_ctrl(card, CK_SECTION, label, nullptr, 0, 0, nullptr, nullptr, ACT_NONE); } static void add_info(int card, const char *label) { add_ctrl(card, CK_INFO, label, nullptr, 0, 0, nullptr, nullptr, ACT_NONE); } static void add_button(int card, const char *label, const char *desc, int action) { add_ctrl(card, CK_BUTTON, label, desc, 0, 0, nullptr, nullptr, action); } static void add_color(int card, const char *label, const char *desc, int *rgb) { int i = g_nctrl; add_ctrl(card, CK_COLOR, label, desc, 0, 255, nullptr, nullptr, ACT_NONE); g_ctrls[i].rgb = rgb; } // Same picker, plus an editable alpha bar; rgb must hold four ints. static void add_color_alpha(int card, const char *label, const char *desc, int *rgba) { int i = g_nctrl; add_ctrl(card, CK_COLOR, label, desc, 0, 255, nullptr, nullptr, ACT_NONE); g_ctrls[i].rgb = rgba; g_ctrls[i].has_alpha = true; } static void add_dropdown(int card, const char *label, const char *desc, const char *const *opts, int nopts, int *value) { int i = g_nctrl; add_ctrl(card, CK_DROPDOWN, label, desc, 0, nopts > 0 ? nopts - 1 : 0, nullptr, value, ACT_NONE); g_ctrls[i].opts = opts; g_ctrls[i].nopts = nopts; } static int ctrl_val(const Ctrl &c) { if (c.kind == CK_TOGGLE && c.bval) return *c.bval ? 1 : 0; if (c.kind == CK_DROPDOWN) return c.ival ? *c.ival : 0; if (c.kind == CK_SLIDER && c.ival) return *c.ival; return 0; } static void ctrl_set(Ctrl &c, int v) { if (c.kind == CK_TOGGLE && c.bval) { *c.bval = v != 0; config_save(); } else if (c.kind == CK_DROPDOWN && c.ival) { if (v < c.min) v = c.min; if (v > c.max) v = c.max; if (*c.ival != v) { *c.ival = v; config_save(); } } else if (c.kind == CK_SLIDER && c.ival) { if (v < c.min) v = c.min; if (v > c.max) v = c.max; if (*c.ival != v) { *c.ival = v; // Slider drags persist on mouse release; discrete changes persist now. } } } static void build_cards() { if (g_cards_built) return; g_cards_built = true; int bhop = add_card(0, &g_bhop, "Bunnyhop", "Automatic jump on landing.", true); add_toggle(bhop, "Uni-directional auto-strafe", "Rotate airborne movement into the velocity.", &g_bhop_autostrafe); add_card(0, &g_abh, "ABH", "Accelerate on the jump tick by facing away " "from the velocity.", true); int jump = add_card(0, nullptr, "Jump assist", "Extra jump handling applied while moving.", true); add_toggle(jump, "Auto crouch-jump", "Duck during the ascent of a held jump, release while falling.", &g_auto_crouchjump); add_toggle(jump, "Edge jump", "Jump when the ground ahead of a running player disappears.", &g_edge_jump); int jb = add_card(0, &g_jumpbug, "Jumpbug", "Duck+jump when landing would hurt.", true); add_toggle(jb, "Assist timing", "Nudge the final command into the unduck window.", &g_jumpbug_assist); int aim = add_card(1, &g_aimbot, "Aimbot", "Smoothly snap to a hostile target.", true); add_toggle(aim, "Vischeck", "Require world line of sight.", &g_aim_vischeck); add_slider(aim, "FOV", "Maximum angular distance from the crosshair.", 1, 180, &g_aim_fov); add_slider(aim, "Smoothness", "Maximum degrees per tick.", 1, 45, &g_aim_smooth); add_toggle(aim, "Show FOV circle", "Draw the acquisition cone on screen.", &g_aim_fov_circle); add_toggle(aim, "Compensate aim punch", "Aim against the server's recoil so shots land on target.", &g_aim_punch); add_section(aim, "Bones"); static const char *bone_names[AIM_BONE_COUNT] = { "Head", "Torso", "Left arm", "Right arm", "Left leg", "Right leg"}; static const char *bone_descs[AIM_BONE_COUNT] = { "Aim at the head hitbox.", "Aim at the chest/stomach.", "Include the left arm.", "Include the right arm.", "Include the left leg.", "Include the right leg."}; for (int i = 0; i < AIM_BONE_COUNT; i++) add_toggle(aim, bone_names[i], bone_descs[i], &g_aim_bones[i]); int sil = add_card(1, &g_silentaim, "Silent aim", "Fire at the target without turning the rendered view. " "Uses the Aimbot FOV, vischeck and bone settings.", true); add_info(sil, "Shares the Aimbot FOV, vischeck and bones."); add_card(1, &g_triggerbot, "Triggerbot", "Fire when the crosshair is on a hostile.", true); add_card(1, &g_melee, "Melee killbot", "360-degree crowbar swing, or a gravity-gun launch at VULN " "targets while holding an object.", true); int esp = add_card(2, &g_esp, "ESP", "Draw hostile NPC information.", true); static const char *esp_names[ESP_OPT_COUNT] = { "Show neutral", "Show healthbar", "Show healthnum", "Show name", "Show skeleton", "Show box", "Show speed graph", "Damage tags", "Alert state", "Enemy weapon", "Off-screen arrows"}; static const char *esp_descs[ESP_OPT_COUNT] = { "Include neutral wildlife.", "Draw a health bar.", "Draw the numeric health value.", "Draw the model name.", "Draw the animated bone links.", "Draw a bounding box.", "Draw the lower-center speed graph.", "Label ARMORED (needs blast/RPG) and VULN (gravity gun) units.", "Label IDLE / ALERT / COMBAT.", "Label the NPC's active weapon.", "Point toward hostiles that are outside the view."}; for (int i = 0; i < ESP_OPT_COUNT; i++) add_toggle(esp, esp_names[i], esp_descs[i], &g_esp_opt[i]); add_section(esp, "Colours"); add_color(esp, "Hostile", "Box, skeleton and name colour for hostiles.", g_esp_col_hostile); add_color(esp, "Neutral", "Box, skeleton and name colour for neutral units.", g_esp_col_neutral); add_color(esp, "Friendly", "Box, skeleton and name colour for friendlies.", g_esp_col_friendly); int items = add_card(2, &g_item_esp, "Valuable items", "Tag medkits, ammo, armor, weapons and supply crates.", true); add_toggle(items, "Medkits / health", "Tag health kits, health vials and health chargers.", &g_item_health); add_toggle(items, "Ammo", "Tag ammo boxes, crates and grenades.", &g_item_ammo); add_toggle(items, "Armor / suit", "Tag batteries, HEV suits and chargers.", &g_item_armor); add_toggle(items, "Weapons", "Tag world weapon pickups.", &g_item_weapons); add_toggle(items, "Supply crates", "Tag item and beacon crates.", &g_item_crates); add_toggle(items, "Show distance", "Append the distance in units.", &g_item_distance); add_toggle(items, "Only if usable", "Hide health kits at full health and armor at full suit.", &g_item_usable); add_slider(items, "Max distance", "0 = unlimited; otherwise items farther away are hidden.", 0, 10000, &g_item_max_dist); g_ctrls[g_nctrl - 1].zero_unlimited = true; int proj = add_card(2, &g_projectiles, "Projectiles", "Tag grenades, rockets, bombs and AR2 balls.", true); add_toggle(proj, "Fuse timer", "Show the remaining time until a grenade or ball detonates.", &g_proj_fuse); add_toggle(proj, "Blast radius", "Draw the projectile's damage radius on the ground.", &g_proj_radius); add_toggle(proj, "Grenade trajectory", "Predict the frag grenade path, bounces and landing point.", &g_proj_traj); int chams = add_card(2, &g_chams, "Chams", "Per-relationship model colours.", true); static const char *const chams_mat_names[] = { "Flat", "Wireframe", "Additive glow", "Solid", "White", "Fullbright", "Solid + contour"}; auto add_chams_group = [&](const char *title, ChamsGroup &g) { add_section(chams, title); add_toggle(chams, "Visible", "Draw the normal-depth pass.", &g.vis_on); add_dropdown(chams, "Visible material", "Material for the visible pass.", chams_mat_names, 7, &g.vis_mat); add_color_alpha(chams, "Visible colour", "Colour and alpha for the visible pass.", g.vis_rgb); add_toggle(chams, "Through walls", "Draw a pass with the depth test off.", &g.inv_on); add_dropdown(chams, "Invisible material", "Material for the through-wall pass.", chams_mat_names, 7, &g.inv_mat); add_color_alpha(chams, "Invisible colour", "Colour and alpha for the through-wall pass.", g.inv_rgb); }; add_chams_group("Hostile", g_chams_hostile); add_chams_group("Neutral", g_chams_neutral); add_chams_group("Friendly", g_chams_friendly); add_section(chams, "Viewmodel"); add_toggle(chams, "Viewmodel (hands/weapon)", "Apply chams to the first-person hands and weapon.", &g_chams_viewmodel); add_dropdown(chams, "Viewmodel material", "Material for the viewmodel.", chams_mat_names, 7, &g_chams_vm_mat); add_color_alpha(chams, "Viewmodel colour", "Colour and alpha for the viewmodel.", g_chams_vm_rgb); int view = add_card(2, nullptr, "Camera / display", "Camera FOV, viewmodel FOV and HDR brightness.", true); add_slider(view, "Camera FOV", "World camera field of view (40-140; stock 75).", 40, 140, &g_fov_camera); add_slider(view, "Viewmodel FOV", "Weapon model field of view (20-120; stock 54).", 20, 120, &g_fov_viewmodel); add_slider(view, "HDR brightness", "mat_force_tonemap_scale x10 (0 = automatic). In-engine " "brightness for HDR maps; 10 = 1.0. No effect while Fullbright " "is on.", 0, 30, &g_tonemap); add_card(2, &g_fullbright, "Fullbright", "Ignore world lighting so dark areas are visible.", true); int tint = add_card(2, &g_world_tint, "World tint", "Multiply world and skybox material colour. " "Independent of Fullbright lighting.", true); add_toggle(tint, "World brushes", "Tint world textures (sky and water excluded from this part).", &g_tint_world_on); add_color(tint, "World colour", "Click the swatch to open the picker.", g_tint_world_rgb); add_toggle(tint, "Skybox", "Tint the skybox textures.", &g_tint_sky_on); add_color(tint, "Sky colour", "Click the swatch to open the picker.", g_tint_sky_rgb); add_toggle(tint, "Props", "Tint model materials under models/props (map props, static " "and physics).", &g_tint_prop_on); add_color(tint, "Prop colour", "Click the swatch to open the picker.", g_tint_prop_rgb); int fog = add_card(2, &g_fog, "Fog", "Override the map's fog colour and distances.", true); add_color(fog, "Colour", "Click the swatch to open the picker.", g_fog_rgb); add_slider(fog, "Start", "Distance in units where fog starts.", 0, 8000, &g_fog_start); add_slider(fog, "End", "Distance in units where fog is fully opaque.", 0, 20000, &g_fog_end); add_toggle(fog, "Skybox fog", "Apply the same fog to the 3D skybox.", &g_fog_skybox); add_toggle(fog, "Remove fog", "Disable fog entirely instead of recolouring it.", &g_fog_disable); static char sky_label[512]; snprintf(sky_label, sizeof(sky_label), "Image: %s", config_file_cstr("skybox.png")); int sky = add_card(2, &g_skybox, "Skybox", "Replace the map skybox with a PNG image from the " "config directory (all six faces).", true); add_info(sky, sky_label); add_info(sky, "Edit skybox.png, then Reload image. Off restores the map sky."); add_button(sky, "Reload image", "Re-read the PNG and apply it again.", ACT_SKY_RELOAD); int spread = add_card(3, &g_spread, "Spread control", "Choose a server shot seed that tightens the cone.", true); add_toggle(spread, "No spread", "Override the FireBullets seed with the best offset.", &g_spread_no_spread); add_toggle(spread, "Engine spread override", "Zero the pellet random offsets directly (shotgun one-hole). " "Off = seed selection only.", &g_spread_engine); int sys = add_card(4, nullptr, "System", "Configuration and maintenance.", true); snprintf(g_config_label, sizeof(g_config_label), "Config: %s", config_path_cstr()); snprintf(g_build_label, sizeof(g_build_label), "Build: %s %s", __DATE__, __TIME__); add_info(sys, g_config_label); add_info(sys, g_build_label); add_button(sys, "Reload config", "Re-read the INI without restarting.", ACT_RELOAD); add_button(sys, "Reset defaults", "Click twice to restore feature defaults; GUI layout kept.", ACT_RESET); } // --------------------------------------------------------------------------- // Window state / input // --------------------------------------------------------------------------- static bool g_open = false; static bool g_was_open = false; static bool g_dragging = false; static int g_drag_dx = 0, g_drag_dy = 0; static int g_win_x = 0, g_win_y = 0, g_win_w = 0, g_win_h = 0; static float g_scale = 1.0f; static bool g_positioned = false; static int g_cat = 0; static float g_scroll = 0.0f; static float g_scroll_max = 0.0f; static bool g_scroll_drag = false; static int g_scroll_drag_dy = 0; static float g_scroll_drag_start = 0.0f; static int g_hover_slider = -1; static int g_slider_drag = -1; static int g_picker_ctrl = -1; // CK_COLOR control with its picker expanded static int g_picker_drag = 0; // 1 = SV square, 2 = hue bar static int g_capture_card = -1; static QAngle g_frozen{0, 0, 0}; static int g_mouse_x = 0, g_mouse_y = 0; static bool g_mouse_click = false; static bool g_mouse_down = false; static bool g_mouse_rclick = false; static unsigned int g_prev_mask = 0; static int g_mouse_press = 0; // 1..5 on the frame a button goes down static unsigned char g_prev_keys[512] = {}; static bool g_prev_cursor = true; static bool g_prev_relative = true; // Last state the game asked for while the GUI held the mouse; restored on // close instead of the (possibly stale) pre-open snapshot. static bool g_game_wants_cursor = true; static bool g_game_wants_relative = false; static double g_hover_since = 0.0; static const char *g_hover_desc = nullptr; static const char *g_hover_prev = nullptr; static int g_hover_x = 0, g_hover_y = 0; static int g_hover_last_mx = -1, g_hover_last_my = -1; static int S(float v) { return (int)(v * g_scale + 0.5f); } static float now_sec() { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (float)ts.tv_sec + (float)ts.tv_nsec * 1e-9f; } // --------------------------------------------------------------------------- // Toasts (shown even when the GUI is closed) // --------------------------------------------------------------------------- struct Toast { char text[256]; int level; double born; // 0 = waiting for the first draw double ttl; }; static Toast g_toasts[8]; static int g_ntoasts = 0; static pthread_mutex_t g_toast_mu = PTHREAD_MUTEX_INITIALIZER; void toastf(int level, const char *fmt, ...) { char buf[256]; va_list ap; va_start(ap, fmt); vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); pthread_mutex_lock(&g_toast_mu); for (int i = 0; i < g_ntoasts; i++) { if (strcmp(g_toasts[i].text, buf) == 0) { g_toasts[i].level = level; g_toasts[i].born = 0.0; pthread_mutex_unlock(&g_toast_mu); return; } } if (g_ntoasts == 8) { memmove(&g_toasts[0], &g_toasts[1], sizeof(Toast) * 7); g_ntoasts = 7; } Toast &t = g_toasts[g_ntoasts++]; snprintf(t.text, sizeof(t.text), "%s", buf); t.level = level; t.born = 0.0; t.ttl = level == TOAST_ERROR ? 12.0 : level == TOAST_WARN ? 9.0 : 6.0; pthread_mutex_unlock(&g_toast_mu); } bool gui_toasts_active() { pthread_mutex_lock(&g_toast_mu); bool active = g_ntoasts > 0; pthread_mutex_unlock(&g_toast_mu); return active; } static bool font_valid(unsigned long f) { if (!f || !g_surface) return false; int w = 0, h = 0; s_text_size(f, L"M", w, h); return w > 0 && h > 0; } static void gui_reset_fonts() { g_font_body = 0; g_font_title = 0; g_font_scale = 0.0f; } static unsigned long create_gui_font(int tall) { static const char *families[] = {"Liberation Sans", "DejaVu Sans", "Noto Sans", "Tahoma", "Verdana", "Arial", nullptr}; // Create one handle and add glyph sets to it. Creating a handle per family // leaked one HFont per failed family, and ISurface has no DestroyFont. unsigned long f = s_create_font(); if (!f) return 0; for (int i = 0; families[i]; i++) { if (s_set_font(f, families[i], tall, 400, 0x400, 0x20, 0xFF) && font_valid(f)) { logd("gui: font %dpx family %s", tall, families[i]); return f; } } return 0; } static void ensure_fonts() { void *le = nullptr; if (g_iface.engine && g_iface.entitylist) { int local = engine_get_local_player(); if (local > 0) le = entitylist_get(local); } if (le != g_font_local) { g_font_local = le; // VGUI fonts are global; a level change alone does not invalidate them. // Dropping valid handles here leaked a fresh pair on every death, respawn // and map load. Only reset when the manager actually lost them. if (!font_valid(g_font_body) || !font_valid(g_font_title)) { gui_reset_fonts(); logd("gui: fonts reset (invalid after local entity change)"); } } bool scale_changed = fabsf(g_font_scale - g_scale) >= 0.001f; if (g_font_body && g_font_title && !scale_changed && font_valid(g_font_body) && font_valid(g_font_title)) return; // Level transitions can silently invalidate VGUI font handles. Recreate // them instead of drawing invisible text. if ((g_font_body && !font_valid(g_font_body)) || (g_font_title && !font_valid(g_font_title))) gui_reset_fonts(); if (scale_changed) { gui_reset_fonts(); logd("gui: fonts reset (scale change)"); } g_font_scale = g_scale; int req_body = (int)(15.0f * g_scale + 0.5f); int req_title = (int)(20.0f * g_scale + 0.5f); if (req_body < 11) req_body = 11; if (req_title < 15) req_title = 15; g_body_h = req_body; g_title_h = req_title; if (!g_font_body) g_font_body = create_gui_font(g_body_h); if (!g_font_title) g_font_title = create_gui_font(g_title_h); // Small scaled sizes can fail in the font manager; 16 px is the proven ESP // size, so use it as a readable fallback. if (!g_font_body) { g_body_h = 16; g_font_body = create_gui_font(g_body_h); } if (!g_font_title) { g_title_h = 16; g_font_title = create_gui_font(g_title_h); } if (!g_font_title) g_font_title = g_font_body; if (!g_font_body) { if (!g_font_fail_logged) { g_font_fail_logged = true; logf("gui: no usable body font"); toastf(TOAST_ERROR, "No usable GUI font"); } return; } g_font_fail_logged = false; if (g_body_h != req_body || g_title_h != req_title) logd("gui: font fallback body=%d title=%d (requested %d/%d)", g_body_h, g_title_h, req_body, req_title); } static int text_w(const char *s, int tall) { if (!s) return 0; unsigned long font = (g_font_title && tall == g_title_h) ? g_font_title : g_font_body; if (g_surface && font) { wchar_t wbuf[1024]; size_t n = mbstowcs(wbuf, s, 1023); if (n != (size_t)-1) { wbuf[n] = 0; int w = 0, h = 0; s_text_size(font, wbuf, w, h); if (w > 0) return w; } } // Fallback when the surface/font is not ready yet. return (int)(strlen(s) * tall * 0.52f + 0.5f); } static void draw_text_raw(int x, int y, const char *s, unsigned long font, int tall); // Break text into lines that fit max_w. Words that are too wide (paths, long // identifiers) are split by character so nothing is ever truncated. static void wrap_text(const char *text, int max_w, int tall, std::vector &out) { out.clear(); if (!text || !*text || max_w <= 0) return; const char *p = text; std::string line; while (*p) { while (*p == ' ') p++; if (!*p) break; const char *start = p; while (*p && *p != ' ') p++; std::string word(start, (size_t)(p - start)); if (!line.empty()) { std::string cand = line + " " + word; if (text_w(cand.c_str(), tall) <= max_w) { line = cand; continue; } out.push_back(line); line.clear(); } while (text_w(word.c_str(), tall) > max_w && word.size() > 1) { size_t n = word.size(); while (n > 1 && text_w(word.substr(0, n).c_str(), tall) > max_w) n--; out.push_back(word.substr(0, n)); word = word.substr(n); } line = word; } if (!line.empty()) out.push_back(line); } static int wrap_count(const char *text, int max_w, int tall) { std::vector lines; wrap_text(text, max_w, tall, lines); return (int)lines.size(); } static void draw_wrapped(const Rect &clip, int x, int y, const char *text, unsigned long font, int tall, int max_w, const Col &c, int line_gap = 2) { if (!text || !font) return; std::vector lines; wrap_text(text, max_w, tall, lines); s_text_color(c.r, c.g, c.b, c.a); int lh = tall + line_gap; for (size_t i = 0; i < lines.size(); i++) { int ly = y + (int)i * lh; if (ly < clip.y || ly + tall > clip.y + clip.h) continue; draw_text_raw(x, ly, lines[i].c_str(), font, tall); } } static void draw_text_raw(int x, int y, const char *s, unsigned long font, int tall) { (void)tall; if (!s || !font) return; wchar_t w[1024]; size_t n = mbstowcs(w, s, 1023); if (n == (size_t)-1) return; w[n] = 0; s_text_font(font); s_text_pos(x, y); s_print(w, (int)n); } static void text_in(const Rect &clip, int x, int y, const char *s, unsigned long font, int tall, int max_w, const Col &c) { if (!s || !font) return; if (y < clip.y || y + tall > clip.y + clip.h) return; if (x >= clip.x + clip.w) return; // Available width: the clip edge, further limited by the layout's column // width. Overflowing text used to be shifted left over its neighbour; now it // is truncated with an ellipsis instead. int avail = clip.x + clip.w - x; if (max_w > 0 && max_w < avail) avail = max_w; if (avail <= 0) return; std::string out = s; if (text_w(out.c_str(), tall) > avail) { while (!out.empty() && text_w((out + "...").c_str(), tall) > avail) out.pop_back(); if (out.empty()) return; out += "..."; } s_text_color(c.r, c.g, c.b, c.a); draw_text_raw(x, y, out.c_str(), font, tall); } static Rect clip_rect(const Rect &r, const Rect &clip) { Rect o = r; int x0 = r.x > clip.x ? r.x : clip.x; int y0 = r.y > clip.y ? r.y : clip.y; int x1 = (r.x + r.w) < (clip.x + clip.w) ? (r.x + r.w) : (clip.x + clip.w); int y1 = (r.y + r.h) < (clip.y + clip.h) ? (r.y + r.h) : (clip.y + clip.h); o.x = x0; o.y = y0; o.w = x1 - x0; o.h = y1 - y0; return o; } static void fill(const Rect &r, const Col &c, const Rect *clip = nullptr) { if (r.w <= 0 || r.h <= 0) return; Rect q = clip ? clip_rect(r, *clip) : r; if (q.w <= 0 || q.h <= 0) return; s_color(c.r, c.g, c.b, c.a); s_fill(q.x, q.y, q.x + q.w, q.y + q.h); } static void outline(const Rect &r, const Col &c, const Rect *clip = nullptr) { if (r.w <= 0 || r.h <= 0) return; // Draw the four edges as thin filled rects. Outlining the *clamped* rect used // to paint a false border along the clip edge whenever a card was partially // scrolled out of the content pane. int t = S(1.0f); if (t < 1) t = 1; fill(Rect{r.x, r.y, r.w, t}, c, clip); fill(Rect{r.x, r.y + r.h - t, r.w, t}, c, clip); fill(Rect{r.x, r.y, t, r.h}, c, clip); fill(Rect{r.x + r.w - t, r.y, t, r.h}, c, clip); } // Liang–Barsky line clip. `clip` is optional so the existing call sites that // draw inside an already-safe area keep working unchanged. static void line(int x0, int y0, int x1, int y1, const Col &c, const Rect *clip = nullptr) { if (clip) { float dx = (float)(x1 - x0), dy = (float)(y1 - y0); float t0 = 0.0f, t1 = 1.0f; float p[4] = {-dx, dx, -dy, dy}; float q[4] = {(float)(x0 - clip->x), (float)(clip->x + clip->w - x0), (float)(y0 - clip->y), (float)(clip->y + clip->h - y0)}; for (int i = 0; i < 4; i++) { if (fabsf(p[i]) < 1e-6f) { if (q[i] < 0.0f) return; // parallel and outside continue; } float t = q[i] / p[i]; if (p[i] < 0.0f) { if (t > t1) return; if (t > t0) t0 = t; } else { if (t < t0) return; if (t < t1) t1 = t; } } int nx0 = (int)((float)x0 + t0 * dx + 0.5f); int ny0 = (int)((float)y0 + t0 * dy + 0.5f); int nx1 = (int)((float)x0 + t1 * dx + 0.5f); int ny1 = (int)((float)y0 + t1 * dy + 0.5f); x0 = nx0; y0 = ny0; x1 = nx1; y1 = ny1; } s_color(c.r, c.g, c.b, c.a); s_line(x0, y0, x1, y1); } static void layout() { int w = g_scr_w, h = g_scr_h; if (g_surface) { s_screen(w, h); if (w > 0) g_scr_w = w; if (h > 0) g_scr_h = h; } w = g_scr_w; h = g_scr_h; if (w <= 0 || h <= 0) return; float sc = fminf((float)w / 1920.0f, (float)h / 1080.0f); if (sc < 0.75f) sc = 0.75f; if (sc > 1.5f) sc = 1.5f; g_scale = sc; int nw = S(960.0f), nh = S(620.0f); if (!g_positioned || nw != g_win_w || nh != g_win_h) { if (!g_positioned) { g_win_x = (w - nw) / 2; g_win_y = (h - nh) / 2; g_positioned = true; } g_win_w = nw; g_win_h = nh; } if (g_win_x < 0) g_win_x = 0; if (g_win_y < 0) g_win_y = 0; if (g_win_x + g_win_w > w) g_win_x = w - g_win_w; if (g_win_y + g_win_h > h) g_win_y = h - g_win_h; } static int card_content_w() { return g_win_w - S(190.0f) - S(28.0f); } static int title_max_w() { return card_content_w() - S(210.0f); } static int header_h(const Card &c) { int n = wrap_count(c.title, title_max_w(), g_title_h); if (n < 1) n = 1; int h = n * (g_title_h + S(2.0f)) + S(12.0f); return h < S(36.0f) ? S(36.0f) : h; } static int label_max_w(const Ctrl &c) { if (c.kind == CK_SLIDER || c.kind == CK_DROPDOWN) return S(214.0f); return card_content_w() - S(16.0f) - S(110.0f); } static int button_w(const char *label) { int bw = text_w(label, g_body_h) + S(20.0f); if (bw < S(160.0f)) bw = S(160.0f); int maxw = card_content_w() - S(24.0f); if (bw > maxw) bw = maxw; return bw; } static int label_row_h(const Ctrl &c); static bool picker_open_for(const Ctrl &c); static int picker_panel_h(bool with_alpha); static bool card_in_category(int i); static Rect card_rect(int index, int y); static int card_h(const Card &c); static int row_h(const Ctrl &c) { int h = label_row_h(c); if (picker_open_for(c)) h += picker_panel_h(c.has_alpha); return h; } // --------------------------------------------------------------------------- // Colour picker (CK_COLOR) // --------------------------------------------------------------------------- static constexpr int PICKER_SV_W = 180; static constexpr int PICKER_SV_H = 110; static constexpr int PICKER_HUE_H = 12; static constexpr int PICKER_ALPHA_H = 12; static constexpr int PICKER_GAP = 8; static bool picker_open_for(const Ctrl &c) { return c.kind == CK_COLOR && g_picker_ctrl >= 0 && &c == &g_ctrls[g_picker_ctrl]; } static int picker_panel_h(bool with_alpha) { int h = PICKER_GAP + PICKER_SV_H + PICKER_GAP + PICKER_HUE_H + PICKER_GAP; if (with_alpha) h += PICKER_ALPHA_H + PICKER_GAP; return S((float)h); } static int label_row_h(const Ctrl &c) { if (c.kind == CK_BUTTON) return S(32.0f); int maxw = (c.kind == CK_INFO || c.kind == CK_SECTION) ? card_content_w() - S(24.0f) : label_max_w(c); int n = wrap_count(c.label, maxw, g_body_h); if (n < 1) n = 1; int h = n * (g_body_h + S(2.0f)) + S(8.0f); int minh = (c.kind == CK_INFO) ? S(20.0f) : (c.kind == CK_SECTION) ? S(24.0f) : S(27.0f); return h < minh ? minh : h; } static void hsv_to_rgb(float h, float s, float v, int &r, int &g, int &b) { if (h < 0.0f) h = 0.0f; h = fmodf(h, 360.0f); if (h < 0.0f) h += 360.0f; if (s < 0.0f) s = 0.0f; if (s > 1.0f) s = 1.0f; if (v < 0.0f) v = 0.0f; if (v > 1.0f) v = 1.0f; float c = v * s; float x = c * (1.0f - fabsf(fmodf(h / 60.0f, 2.0f) - 1.0f)); float m = v - c; float rr = 0.0f, gg = 0.0f, bb = 0.0f; if (h < 60.0f) { rr = c; gg = x; } else if (h < 120.0f) { rr = x; gg = c; } else if (h < 180.0f) { gg = c; bb = x; } else if (h < 240.0f) { gg = x; bb = c; } else if (h < 300.0f) { rr = x; bb = c; } else { rr = c; bb = x; } r = (int)((rr + m) * 255.0f + 0.5f); g = (int)((gg + m) * 255.0f + 0.5f); b = (int)((bb + m) * 255.0f + 0.5f); } static void rgb_to_hsv(int r, int g, int b, float &h, float &s, float &v) { float rf = r / 255.0f, gf = g / 255.0f, bf = b / 255.0f; float mx = fmaxf(rf, fmaxf(gf, bf)); float mn = fminf(rf, fminf(gf, bf)); float d = mx - mn; v = mx; s = mx > 0.0f ? d / mx : 0.0f; if (d <= 0.0f) { h = 0.0f; return; } if (mx == rf) h = 60.0f * fmodf((gf - bf) / d, 6.0f); else if (mx == gf) h = 60.0f * ((bf - rf) / d + 2.0f); else h = 60.0f * ((rf - gf) / d + 4.0f); if (h < 0.0f) h += 360.0f; } struct PickerRects { Rect panel, sv, hue, alpha, preview; }; static PickerRects picker_rects(const Rect &cr, int row_y, int base_h, bool with_alpha) { PickerRects p{}; int px = cr.x + S(16.0f); int py = row_y + base_h; int pw = card_content_w() - S(32.0f); if (pw > S(340.0f)) pw = S(340.0f); p.panel = Rect{px, py, pw, picker_panel_h(with_alpha)}; int preview_w = S(84.0f); int svw = S((float)PICKER_SV_W); if (svw + preview_w + S(16.0f) > pw) svw = pw - preview_w - S(16.0f); if (svw < S(60.0f)) svw = S(60.0f); p.sv = Rect{px + S(PICKER_GAP), py + S(PICKER_GAP), svw, S((float)PICKER_SV_H)}; p.hue = Rect{p.sv.x, p.sv.y + p.sv.h + S(PICKER_GAP), svw, S((float)PICKER_HUE_H)}; p.alpha = Rect{p.hue.x, p.hue.y + p.hue.h + S(PICKER_GAP), svw, S((float)PICKER_ALPHA_H)}; p.preview = Rect{p.sv.x + p.sv.w + S(12.0f), p.sv.y, preview_w, S(52.0f)}; return p; } // Widget row/card used to map a control index back to screen space. Needed by // the drag handler, which runs outside click/layout context. static bool control_rect(int ctrl_index, Rect &out_card, Rect &out_row) { Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f), g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)}; int y = content.y - (int)g_scroll + S(8.0f); for (int i = 0; i < g_ncards; i++) { if (!card_in_category(i)) continue; Card &c = g_cards[i]; Rect cr = card_rect(i, y); if (c.expanded) { int by = cr.y + header_h(c) + S(6.0f); for (int j = 0; j < c.n; j++) { Ctrl &ct = g_ctrls[c.c0 + j]; int rh = row_h(ct); if (c.c0 + j == ctrl_index) { out_card = cr; out_row = Rect{cr.x, by, cr.w, rh}; return true; } by += rh; } } y += card_h(c); } return false; } // Dropdown popup state (one open at a time). The value is the Ctrl index. static int g_dropdown_ctrl = -1; static bool dropdown_rects(int ctrl_index, Rect &btn, Rect &pop, int &item_h) { Rect card, row; if (!control_rect(ctrl_index, card, row)) return false; Ctrl &ct = g_ctrls[ctrl_index]; if (ct.kind != CK_DROPDOWN || !ct.opts || ct.nopts <= 0) return false; int bw = S(150.0f); btn = Rect{card.x + card.w - S(16.0f) - bw, row.y + (row.h - S(26.0f)) / 2, bw, S(26.0f)}; item_h = S(22.0f); pop = Rect{btn.x, btn.y + btn.h, btn.w, item_h * ct.nopts}; if (pop.y + pop.h > g_win_y + g_win_h - S(34.0f)) pop.y = btn.y - pop.h; return true; } static void draw_dropdown() { if (g_dropdown_ctrl < 0) return; Rect btn, pop; int ih = 0; if (!dropdown_rects(g_dropdown_ctrl, btn, pop, ih)) { g_dropdown_ctrl = -1; return; } Ctrl &ct = g_ctrls[g_dropdown_ctrl]; fill(pop, Col{25, 28, 34, 255}); outline(pop, C_BORDER); int cur = ct.ival ? *ct.ival : 0; for (int i = 0; i < ct.nopts; i++) { Rect it{pop.x, pop.y + i * ih, pop.w, ih}; if (it.has(g_mouse_x, g_mouse_y)) fill(it, Col{45, 50, 60, 255}); text_in(pop, it.x + S(9), it.y + (ih - g_body_h) / 2, ct.opts[i], g_font_body, g_body_h, it.w - S(28), i == cur ? C_ACCENT : C_TEXT); if (i == cur) fill(Rect{it.x + it.w - S(14), it.y + ih / 2 - S(3), S(6), S(6)}, C_ACCENT); } } static bool picker_rects_for(int ctrl_index, PickerRects &out) { if (ctrl_index < 0 || ctrl_index >= g_nctrl) return false; Rect card, row; if (!control_rect(ctrl_index, card, row)) return false; Ctrl &ct = g_ctrls[ctrl_index]; out = picker_rects(card, row.y, label_row_h(ct), ct.has_alpha); return true; } static void picker_apply(Ctrl &ct, const PickerRects &p) { if (!ct.rgb || p.sv.w <= 0 || p.hue.w <= 0) return; if (g_picker_drag == 1) { float s = (float)(g_mouse_x - p.sv.x) / (float)p.sv.w; float v = 1.0f - (float)(g_mouse_y - p.sv.y) / (float)p.sv.h; if (s < 0.0f) s = 0.0f; if (s > 1.0f) s = 1.0f; if (v < 0.0f) v = 0.0f; if (v > 1.0f) v = 1.0f; ct.sat = s; ct.val = v; hsv_to_rgb(ct.hue, ct.sat, ct.val, ct.rgb[0], ct.rgb[1], ct.rgb[2]); } else if (g_picker_drag == 2) { float h = 360.0f * (float)(g_mouse_x - p.hue.x) / (float)p.hue.w; if (h < 0.0f) h = 0.0f; if (h >= 360.0f) h = 359.9f; ct.hue = h; hsv_to_rgb(ct.hue, ct.sat, ct.val, ct.rgb[0], ct.rgb[1], ct.rgb[2]); } else if (g_picker_drag == 3 && ct.has_alpha) { if (p.alpha.w <= 0) return; int a = (int)(255.0f * (float)(g_mouse_x - p.alpha.x) / (float)p.alpha.w + 0.5f); if (a < 0) a = 0; if (a > 255) a = 255; ct.rgb[3] = a; } } static void draw_checker(const Rect &r, const Rect *clip) { int sq = S(6.0f); if (sq < 4) sq = 4; for (int yy = 0; yy < r.h; yy += sq) { for (int xx = 0; xx < r.w; xx += sq) { int w = sq < r.w - xx ? sq : r.w - xx; int h = sq < r.h - yy ? sq : r.h - yy; bool odd = ((xx / sq) + (yy / sq)) & 1; fill(Rect{r.x + xx, r.y + yy, w, h}, odd ? Col{62, 66, 74, 255} : Col{38, 41, 47, 255}, clip); } } } static void draw_picker(const Rect &clip, const Rect &cr, const Ctrl &ct, int row_y) { PickerRects p = picker_rects(cr, row_y, label_row_h(ct), ct.has_alpha); int pr = ct.rgb ? ct.rgb[0] : 255; int pg = ct.rgb ? ct.rgb[1] : 255; int pb = ct.rgb ? ct.rgb[2] : 255; int pa = (ct.has_alpha && ct.rgb) ? ct.rgb[3] : 255; fill(p.panel, Col{20, 23, 29, 250}, &clip); outline(p.panel, C_BORDER, &clip); // Saturation/value field: solid rows of a white->hue gradient scaled by V. int sx = S(4.0f); int sy = S(4.0f); if (sx < 2) sx = 2; if (sy < 2) sy = 2; for (int yy = 0; yy < p.sv.h; yy += sy) { float v = 1.0f - ((float)yy + (float)sy * 0.5f) / (float)p.sv.h; int rh = sy < p.sv.h - yy ? sy : p.sv.h - yy; for (int xx = 0; xx < p.sv.w; xx += sx) { float s = ((float)xx + (float)sx * 0.5f) / (float)p.sv.w; int r, g, b; hsv_to_rgb(ct.hue, s, v, r, g, b); int rw = sx < p.sv.w - xx ? sx : p.sv.w - xx; fill(Rect{p.sv.x + xx, p.sv.y + yy, rw, rh}, Col{r, g, b, 255}, &clip); } } outline(p.sv, Col{0, 0, 0, 255}, &clip); // Hue bar. int hx = S(4.0f); if (hx < 2) hx = 2; for (int xx = 0; xx < p.hue.w; xx += hx) { float h = 360.0f * ((float)xx + (float)hx * 0.5f) / (float)p.hue.w; int r, g, b; hsv_to_rgb(h, 1.0f, 1.0f, r, g, b); int rw = hx < p.hue.w - xx ? hx : p.hue.w - xx; fill(Rect{p.hue.x + xx, p.hue.y, rw, p.hue.h}, Col{r, g, b, 255}, &clip); } outline(p.hue, Col{0, 0, 0, 255}, &clip); // Alpha bar: checkerboard behind a transparent->opaque colour gradient. if (ct.has_alpha) { draw_checker(p.alpha, &clip); int ax = S(4.0f); if (ax < 2) ax = 2; for (int xx = 0; xx < p.alpha.w; xx += ax) { int a = p.alpha.w > 1 ? 255 * xx / (p.alpha.w - 1) : 255; int rw = ax < p.alpha.w - xx ? ax : p.alpha.w - xx; fill(Rect{p.alpha.x + xx, p.alpha.y, rw, p.alpha.h}, Col{pr, pg, pb, a}, &clip); } outline(p.alpha, Col{0, 0, 0, 255}, &clip); int amx = p.alpha.x + (int)((float)pa / 255.0f * (float)p.alpha.w); line(amx, p.alpha.y - S(2.0f), amx, p.alpha.y + p.alpha.h + S(2.0f), Col{255, 255, 255, 255}, &clip); } // Selection markers. int mx = p.sv.x + (int)(ct.sat * (float)p.sv.w); int my = p.sv.y + (int)((1.0f - ct.val) * (float)p.sv.h); Rect svm{mx - S(4.0f), my - S(4.0f), S(8.0f), S(8.0f)}; outline(svm, Col{0, 0, 0, 255}, &clip); Rect svm2{mx - S(3.0f), my - S(3.0f), S(6.0f), S(6.0f)}; outline(svm2, Col{255, 255, 255, 255}, &clip); int hxm = p.hue.x + (int)((ct.hue / 360.0f) * (float)p.hue.w); line(hxm, p.hue.y - S(2.0f), hxm, p.hue.y + p.hue.h + S(2.0f), Col{255, 255, 255, 255}, &clip); // Preview swatch and hex value. draw_checker(p.preview, &clip); fill(p.preview, Col{pr, pg, pb, pa}, &clip); outline(p.preview, C_BORDER, &clip); char hex[16]; if (ct.has_alpha) snprintf(hex, sizeof(hex), "#%02X%02X%02X%02X", pr, pg, pb, pa); else snprintf(hex, sizeof(hex), "#%02X%02X%02X", pr, pg, pb); text_in(clip, p.preview.x, p.preview.y + p.preview.h + S(8.0f), hex, g_font_body, g_body_h, p.preview.w, C_TEXT); char rgb[48]; if (ct.has_alpha) snprintf(rgb, sizeof(rgb), "%d %d %d %d", pr, pg, pb, pa); else snprintf(rgb, sizeof(rgb), "%d %d %d", pr, pg, pb); text_in(clip, p.preview.x, p.preview.y + p.preview.h + S(8.0f) + g_body_h, rgb, g_font_body, g_body_h, p.preview.w, C_DIM); } static int body_h(const Card &c) { if (!c.expanded) return 0; int h = S(6.0f); for (int i = 0; i < c.n; i++) h += row_h(g_ctrls[c.c0 + i]); return h + S(6.0f); } static int card_h(const Card &c) { return header_h(c) + body_h(c) + S(8.0f); } static Rect card_rect(int index, int y) { Card &c = g_cards[index]; int x = g_win_x + S(190.0f) + S(10.0f); int w = g_win_w - S(190.0f) - S(28.0f); return Rect{x, y, w, card_h(c)}; } // Slider track inside a card: the single source of truth for drawing, // hit-testing and dragging. The drag path used to recompute this by hand and // omitted the card's +S(10) left margin, so the value jumped on mouse-down. static Rect slider_track(const Rect &cr, int y, int rh) { int w = cr.w - S(16.0f) - S(230.0f) - S(64.0f); if (w < S(30.0f)) w = S(30.0f); return Rect{cr.x + S(16.0f) + S(230.0f), y + (rh - S(8.0f)) / 2, w, S(8.0f)}; } static int card_for_ctrl(int ci) { for (int i = 0; i < g_ncards; i++) if (ci >= g_cards[i].c0 && ci < g_cards[i].c0 + g_cards[i].n) return i; return -1; } static bool card_in_category(int i) { return g_cards[i].cat == g_cat; } static int content_height() { int h = S(8.0f); for (int i = 0; i < g_ncards; i++) if (card_in_category(i)) h += card_h(g_cards[i]); return h; } // --------------------------------------------------------------------------- // Input // --------------------------------------------------------------------------- static void close_gui() { if (!g_open) return; g_open = false; g_capture_card = -1; g_dragging = false; g_scroll_drag = false; g_slider_drag = -1; g_picker_ctrl = -1; g_picker_drag = 0; g_dropdown_ctrl = -1; g_reset_armed = false; input_clear_mouse_states(); if (g_sdl_ready) { // Restore what the game actually wants now, not the pre-open snapshot: the // game keeps requesting relative mode/cursor state while the GUI is open // (menu -> map transition), and those requests were being dropped. if (sd_set_relative) sd_set_relative(g_game_wants_relative ? 1 : 0); if (sd_show_cursor) sd_show_cursor(g_game_wants_cursor ? 1 : 0); } config_save(); logf("gui: closed"); } static void open_gui() { if (g_open) return; int w = 0, h = 0; if (g_surface) { s_screen(w, h); if (w > 0) g_scr_w = w; if (h > 0) g_scr_h = h; } if (!g_iface.engine) { g_frozen = QAngle{0, 0, 0}; } else { using GetViewFn = void (*)(void *, QAngle *); QAngle v{0, 0, 0}; vfunc(g_iface.engine, 19)(g_iface.engine, &v); g_frozen = v; } if (g_sdl_ready) { if (sd_get_relative) g_prev_relative = sd_get_relative() != 0; if (sd_show_cursor) g_prev_cursor = sd_show_cursor(-1) != 0; // Until the game asks for something else, restore what it had before. g_game_wants_relative = g_prev_relative; g_game_wants_cursor = g_prev_cursor; if (sd_set_relative) sd_set_relative(0); if (sd_show_cursor) sd_show_cursor(1); } input_clear_mouse_states(); g_open = true; g_capture_card = -1; g_wheel.exchange(0); g_scroll = 0.0f; if (g_cat < 0 || g_cat >= CAT_COUNT) g_cat = 0; logf("gui: opened"); } static void update_mouse() { if (!g_sdl_ready) return; int mx = 0, my = 0; unsigned int mask = sd_get_mouse(&mx, &my); g_mouse_x = mx; g_mouse_y = my; g_mouse_down = (mask & 1u) != 0; g_mouse_press = 0; unsigned int pressed = mask & ~g_prev_mask; for (int b = 0; b < 5; b++) { if (pressed & (1u << b)) { g_mouse_press = b + 1; break; } } g_mouse_click = g_mouse_press == 1; g_mouse_rclick = g_mouse_press == 2; if (g_mouse_x < 0) g_mouse_x = 0; if (g_mouse_y < 0) g_mouse_y = 0; g_prev_mask = mask; } static void handle_capture(const unsigned char *keys) { if (g_capture_card < 0) return; if (!g_open) { g_capture_card = -1; return; } if (keys[41]) { // Esc cancels g_capture_card = -1; return; } Card &card = g_cards[g_capture_card]; if (g_mouse_press >= 1 && g_mouse_press <= 5 && card.feat) { card.feat->bind = 600 + (g_mouse_press - 1); // Consume the click: it must not immediately re-arm capture on the same // bind box. g_mouse_click = false; g_mouse_rclick = false; g_capture_card = -1; config_save(); toastf(TOAST_INFO, "%s bound to %s", card.title, scancode_name(card.feat->bind)); if (!card.feat->active) toastf(TOAST_WARN, "%s is not Active yet", card.title); return; } for (int k = 0; k < 512; k++) { if (!keys[k] || g_prev_keys[k]) continue; if (k == 73) // Insert is reserved for opening/closing the GUI continue; if (card.feat) card.feat->bind = k; g_capture_card = -1; config_save(); if (card.feat) { toastf(TOAST_INFO, "%s bound to %s", card.title, scancode_name(card.feat->bind)); if (!card.feat->active) toastf(TOAST_WARN, "%s is not Active yet", card.title); } return; } } static void handle_mouse_click() { if (!g_mouse_click) return; int mx = g_mouse_x, my = g_mouse_y; // Open dropdown: consume the click on its popup first. if (g_dropdown_ctrl >= 0) { Rect btn, pop; int ih = 0; if (dropdown_rects(g_dropdown_ctrl, btn, pop, ih)) { if (pop.has(mx, my)) { Ctrl &ct = g_ctrls[g_dropdown_ctrl]; int i = (my - pop.y) / ih; if (i >= 0 && i < ct.nopts && ct.ival) { *ct.ival = i; config_save(); } g_dropdown_ctrl = -1; return; } if (btn.has(mx, my)) { g_dropdown_ctrl = -1; return; } } g_dropdown_ctrl = -1; // Consume the dismissal click: it must not also activate whatever control // happened to be underneath the popup. g_mouse_click = false; return; } // title bar: drag Rect title{g_win_x, g_win_y, g_win_w, S(38.0f)}; if (title.has(mx, my)) { g_dragging = true; g_drag_dx = mx - g_win_x; g_drag_dy = my - g_win_y; return; } // sidebar int side_x = g_win_x; int side_y = g_win_y + S(38.0f); int side_w = S(190.0f); int cat_h = S(34.0f); for (int i = 0; i < CAT_COUNT; i++) { Rect r{side_x, side_y + i * cat_h, side_w, cat_h}; if (r.has(mx, my)) { g_cat = i; g_scroll = 0.0f; config_save(); return; } } // content Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f), g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)}; if (!content.has(mx, my)) return; // scrollbar int sb_x = content.x + content.w - S(8.0f); if (mx >= sb_x) { if (g_scroll_max > 0.5f) { int thumb_h = (int)((float)content.h * (float)content.h / (float)(content.h + (int)g_scroll_max)); if (thumb_h < S(24.0f)) thumb_h = S(24.0f); int travel = content.h - thumb_h; int thumb_y = content.y + (int)((g_scroll_max > 0.0f) ? (g_scroll / g_scroll_max) * travel : 0); if (my >= thumb_y && my < thumb_y + thumb_h) { g_scroll_drag = true; g_scroll_drag_dy = my - thumb_y; g_scroll_drag_start = g_scroll; } else { float t = (float)(my - content.y) / (float)content.h; g_scroll = t * g_scroll_max; } } return; } int y = content.y - (int)g_scroll + S(8.0f); for (int i = 0; i < g_ncards; i++) { if (!card_in_category(i)) continue; Card &c = g_cards[i]; Rect cr = card_rect(i, y); int ch = card_h(c); int hh = header_h(c); if (my >= cr.y && my < cr.y + ch) { // header if (my < cr.y + hh) { if (c.feat) { Rect active{cr.x + cr.w - S(170.0f), cr.y + (hh - S(18.0f)) / 2, S(18.0f), S(18.0f)}; Rect bind{cr.x + cr.w - S(112.0f), cr.y + (hh - S(22.0f)) / 2, S(100.0f), S(22.0f)}; if (active.has(mx, my)) { c.feat->active = !c.feat->active; config_save(); return; } if (bind.has(mx, my)) { if (mx >= bind.x + bind.w - S(18.0f)) { c.feat->bind = 0; config_save(); toastf(TOAST_INFO, "%s unbound (always-on)", c.title); return; } g_capture_card = i; return; } } c.expanded = !c.expanded; config_save(); return; } // body int by = cr.y + hh + S(6.0f); for (int j = 0; j < c.n; j++) { Ctrl &ct = g_ctrls[c.c0 + j]; int rh = row_h(ct); if (my < by || my >= by + rh) { by += rh; continue; } if (ct.kind == CK_TOGGLE) { ctrl_set(ct, !ctrl_val(ct)); return; } if (ct.kind == CK_SLIDER) { Rect track = slider_track(cr, by, rh); if (mx >= track.x && mx < track.x + track.w) { float t = (float)(mx - track.x) / (float)track.w; int v = ct.min + (int)(t * (ct.max - ct.min) + 0.5f); if (v < ct.min) v = ct.min; if (v > ct.max) v = ct.max; ctrl_set(ct, v); g_slider_drag = c.c0 + j; } return; } if (ct.kind == CK_DROPDOWN) { int idx = c.c0 + j; Rect btn, pop; int ih = 0; if (dropdown_rects(idx, btn, pop, ih) && btn.has(mx, my)) g_dropdown_ctrl = (g_dropdown_ctrl == idx) ? -1 : idx; return; } if (ct.kind == CK_COLOR) { int idx = c.c0 + j; int base = label_row_h(ct); bool is_open = (g_picker_ctrl == idx); if (is_open) { PickerRects p = picker_rects(cr, by, base, ct.has_alpha); if (p.sv.has(mx, my)) { g_picker_drag = 1; picker_apply(g_ctrls[idx], p); return; } if (p.hue.has(mx, my)) { g_picker_drag = 2; picker_apply(g_ctrls[idx], p); return; } if (ct.has_alpha && p.alpha.has(mx, my)) { g_picker_drag = 3; picker_apply(g_ctrls[idx], p); return; } } Rect sw{cr.x + cr.w - S(96.0f), by + (base - S(20.0f)) / 2, S(72.0f), S(20.0f)}; if (my < by + base || sw.has(mx, my)) { if (is_open) { g_picker_ctrl = -1; } else { g_picker_ctrl = idx; if (ct.rgb) rgb_to_hsv(ct.rgb[0], ct.rgb[1], ct.rgb[2], ct.hue, ct.sat, ct.val); } } return; } if (ct.kind == CK_BUTTON) { int bw = button_w(ct.label); Rect b{cr.x + cr.w - S(16.0f) - bw, by + (rh - S(26.0f)) / 2, bw, S(26.0f)}; if (b.has(mx, my)) { if (ct.action == ACT_RELOAD) { config_reload(); logf("gui: config reloaded"); } else if (ct.action == ACT_RESET) { double now = now_sec(); if (g_reset_armed && now - g_reset_armed_at < 3.0) { config_reset_defaults(); g_reset_armed = false; logf("gui: defaults restored"); toastf(TOAST_INFO, "Feature defaults restored"); } else { g_reset_armed = true; g_reset_armed_at = now; } } else if (ct.action == ACT_SKY_RELOAD) { skybox_reload_image(); toastf(TOAST_INFO, "Reloading skybox.png"); } return; } } return; } } y += ch; } } static void handle_hover() { g_hover_desc = nullptr; g_hover_slider = -1; // An open popup/picker or an active drag owns the cursor; arming hover on the // controls underneath let Left/Right arrows adjust a hidden slider. if (g_dropdown_ctrl >= 0 || g_picker_ctrl >= 0 || g_picker_drag || g_slider_drag >= 0 || g_dragging || g_scroll_drag) return; int mx = g_mouse_x, my = g_mouse_y; Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f), g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)}; if (!content.has(mx, my)) return; int y = content.y - (int)g_scroll + S(8.0f); for (int i = 0; i < g_ncards; i++) { if (!card_in_category(i)) continue; Card &c = g_cards[i]; Rect cr = card_rect(i, y); int ch = card_h(c); int hh = header_h(c); if (my >= cr.y && my < cr.y + ch) { if (my < cr.y + hh) { if (c.feat) { Rect active{cr.x + cr.w - S(170.0f), cr.y + (hh - S(18.0f)) / 2, S(18.0f), S(18.0f)}; Rect bind{cr.x + cr.w - S(112.0f), cr.y + (hh - S(22.0f)) / 2, S(100.0f), S(22.0f)}; if (active.has(mx, my)) g_hover_desc = "Enable this feature. A bind, when set, must be " "held for it to run."; else if (bind.has(mx, my)) g_hover_desc = (mx >= bind.x + bind.w - S(18.0f)) ? "Clear the bind. An unbound feature is " "always-on." : "Click, then press a key or mouse button. " "Esc cancels."; else g_hover_desc = c.desc; } else { g_hover_desc = c.desc; } return; } int by = cr.y + hh + S(6.0f); for (int j = 0; j < c.n; j++) { Ctrl &ct = g_ctrls[c.c0 + j]; int rh = row_h(ct); if (my >= by && my < by + rh) { g_hover_desc = ct.desc; if (ct.kind == CK_SLIDER) g_hover_slider = c.c0 + j; return; } by += rh; } return; } y += ch; } } void gui_handle_input() { gui_init_sdl(); if (!g_surface) return; build_cards(); layout(); if (g_scr_w <= 0 || g_scr_h <= 0) return; ensure_fonts(); if (!g_sdl_ready) return; int nk = 0; const unsigned char *keys = sd_get_keys(&nk); if (!keys) return; if (nk > 512) nk = 512; update_mouse(); bool toggle_key = (nk > 73) && keys[73]; bool toggle_edge = toggle_key && !g_prev_keys[73]; bool esc = (nk > 41) && keys[41]; bool esc_edge = esc && !g_prev_keys[41]; bool left = (nk > 80) && keys[80]; bool right = (nk > 79) && keys[79]; bool left_edge = left && !g_prev_keys[80]; bool right_edge = right && !g_prev_keys[79]; if (toggle_edge) { if (g_open) close_gui(); else open_gui(); } if (!g_open) { memcpy(g_prev_keys, keys, nk); g_was_open = false; return; } if (esc_edge && g_capture_card >= 0) g_capture_card = -1; // Keep the native cursor free even if the game re-grabs it. if (sd_set_relative) sd_set_relative(0); if (sd_show_cursor) sd_show_cursor(1); if (g_iface.engine) { QAngle f = g_frozen; using SetViewFn = void (*)(void *, QAngle *); vfunc(g_iface.engine, 20)(g_iface.engine, &f); } handle_capture(keys); if (g_dragging) { if (!g_mouse_down) { g_dragging = false; config_save(); } else { g_win_x = g_mouse_x - g_drag_dx; g_win_y = g_mouse_y - g_drag_dy; if (g_win_x < 0) g_win_x = 0; if (g_win_y < 0) g_win_y = 0; if (g_win_x + g_win_w > g_scr_w) g_win_x = g_scr_w - g_win_w; if (g_win_y + g_win_h > g_scr_h) g_win_y = g_scr_h - g_win_h; } } else { handle_mouse_click(); } if (g_slider_drag >= 0) { if (!g_mouse_down) { g_slider_drag = -1; config_save(); } else { Ctrl &ct = g_ctrls[g_slider_drag]; int ci = card_for_ctrl(g_slider_drag); if (ci >= 0) { Rect track = slider_track(card_rect(ci, 0), 0, 0); float t = (float)(g_mouse_x - track.x) / (float)track.w; if (t < 0.0f) t = 0.0f; if (t > 1.0f) t = 1.0f; int v = ct.min + (int)(t * (ct.max - ct.min) + 0.5f); ctrl_set(ct, v); } } } if (g_picker_drag) { if (!g_mouse_down) { g_picker_drag = 0; config_save(); } else if (g_picker_ctrl >= 0) { PickerRects p; if (picker_rects_for(g_picker_ctrl, p)) picker_apply(g_ctrls[g_picker_ctrl], p); } } if (g_scroll_drag) { if (!g_mouse_down) { g_scroll_drag = false; } else { Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f), g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)}; int thumb_h = (int)((float)content.h * (float)content.h / (float)(content.h + (int)g_scroll_max)); if (thumb_h < S(24.0f)) thumb_h = S(24.0f); int travel = content.h - thumb_h; if (travel > 0) { float t = (float)(g_mouse_y - content.y - g_scroll_drag_dy) / (float)travel; g_scroll = t * g_scroll_max; } } } int wheel = g_wheel.exchange(0); if (wheel != 0) g_scroll -= wheel * S(44.0f); if (left_edge || right_edge) { if (g_hover_slider >= 0) { Ctrl &ct = g_ctrls[g_hover_slider]; ctrl_set(ct, ctrl_val(ct) + (right_edge ? 1 : -1)); config_save(); } } int total = content_height(); Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f), g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)}; g_scroll_max = (float)(total + S(8.0f) - content.h); if (g_scroll_max < 0.0f) g_scroll_max = 0.0f; if (g_scroll < 0.0f) g_scroll = 0.0f; if (g_scroll > g_scroll_max) g_scroll = g_scroll_max; handle_hover(); { double now = now_sec(); int dx = abs(g_mouse_x - g_hover_last_mx); int dy = abs(g_mouse_y - g_hover_last_my); if (g_hover_desc != g_hover_prev || dx > 2 || dy > 2) { g_hover_prev = g_hover_desc; g_hover_last_mx = g_mouse_x; g_hover_last_my = g_mouse_y; g_hover_since = now; g_hover_x = g_mouse_x; g_hover_y = g_mouse_y; } } memcpy(g_prev_keys, keys, nk); g_was_open = true; } // --------------------------------------------------------------------------- // Draw // --------------------------------------------------------------------------- static void draw_checkbox(int x, int y, bool on, const Rect *clip = nullptr) { Rect box{x, y, S(15.0f), S(15.0f)}; fill(box, on ? C_ACCENT : Col{30, 34, 42, 255}, clip); outline(box, C_BORDER, clip); if (on) { line(x + S(3.0f), y + S(8.0f), x + S(7.0f), y + S(12.0f), C_BG, clip); line(x + S(7.0f), y + S(12.0f), x + S(13.0f), y + S(4.0f), C_BG, clip); } } static void draw_slider(const Rect &clip, const Rect &cr, int y, int rh, const Ctrl &ct) { Rect track = slider_track(cr, y, rh); int track_w = track.w; fill(track, Col{38, 42, 50, 255}, &clip); int value = ctrl_val(ct); float t = (ct.max > ct.min) ? (float)(value - ct.min) / (float)(ct.max - ct.min) : 0.0f; int fw = (int)(track_w * t + 0.5f); fill(Rect{track.x, track.y, fw, track.h}, C_ACCENT_DIM, &clip); int kx = track.x + fw - S(4.0f); fill(Rect{kx, track.y - S(4.0f), S(8.0f), S(16.0f)}, C_ACCENT, &clip); char val[32]; if (ct.zero_unlimited && value == 0) snprintf(val, sizeof(val), "All"); else snprintf(val, sizeof(val), "%d", value); text_in(clip, cr.x + cr.w - S(52.0f), y + (rh - g_body_h) / 2, val, g_font_body, g_body_h, 0, C_TEXT); } static void draw_card(const Rect &clip, int index, int y) { Card &c = g_cards[index]; Rect cr = card_rect(index, y); Rect bg = clip_rect(cr, clip); if (bg.w <= 0 || bg.h <= 0) return; fill(cr, C_PANEL, &clip); outline(cr, Col{52, 57, 67, 255}, &clip); int hh = header_h(c); Rect head{cr.x, cr.y, cr.w, hh}; fill(head, C_HEADER, &clip); // expand arrow (clipped like the rest of the card; header rows of cards // scrolled out of view must not leak into the window chrome) int ax = cr.x + S(10.0f), ay = cr.y + (hh - S(8.0f)) / 2; if (ay >= clip.y && ay + S(8.0f) <= clip.y + clip.h) { if (c.expanded) { line(ax, ay, ax + S(4.0f), ay + S(5.0f), C_DIM); line(ax + S(4.0f), ay + S(5.0f), ax + S(8.0f), ay, C_DIM); } else { line(ax, ay, ax + S(5.0f), ay + S(4.0f), C_DIM); line(ax + S(5.0f), ay + S(4.0f), ax, ay + S(8.0f), C_DIM); } } // title (wraps instead of truncating) std::vector title_lines; wrap_text(c.title, title_max_w(), g_title_h, title_lines); int tlh = g_title_h + S(2.0f); int title_h = (int)title_lines.size() * tlh - S(2.0f); int ty = cr.y + (hh - title_h) / 2; draw_wrapped(clip, cr.x + S(24.0f), ty, c.title, g_font_title, g_title_h, title_max_w(), C_TEXT, S(2.0f)); if (c.feat) { bool active = c.feat->active; int active_y = cr.y + (hh - S(18.0f)) / 2; draw_checkbox(cr.x + cr.w - S(170.0f), active_y, active, &clip); text_in(clip, cr.x + cr.w - S(148.0f), active_y + (S(18.0f) - g_body_h) / 2, active ? "On" : "Off", g_font_body, g_body_h, 0, active ? C_GOOD : C_DIM); Rect bind{cr.x + cr.w - S(112.0f), cr.y + (hh - S(22.0f)) / 2, S(100.0f), S(22.0f)}; fill(bind, Col{45, 50, 60, 255}, &clip); outline(bind, C_BORDER, &clip); const char *bl = (g_capture_card == index) ? "Press..." : scancode_name(c.feat->bind); text_in(clip, bind.x + S(7.0f), bind.y + (bind.h - g_body_h) / 2, bl, g_font_body, g_body_h, 0, C_TEXT); text_in(clip, bind.x + bind.w - S(16.0f), bind.y + (bind.h - g_body_h) / 2, "x", g_font_body, g_body_h, 0, C_DIM); } if (!c.expanded) return; int by = cr.y + hh + S(6.0f); for (int j = 0; j < c.n; j++) { Ctrl &ct = g_ctrls[c.c0 + j]; int rh = row_h(ct); if (ct.kind == CK_SECTION) { draw_wrapped(clip, cr.x + S(16.0f), by + S(3.0f), ct.label, g_font_body, g_body_h, card_content_w() - S(24.0f), C_ACCENT, S(2.0f)); } else if (ct.kind == CK_INFO) { draw_wrapped(clip, cr.x + S(16.0f), by + S(3.0f), ct.label, g_font_body, g_body_h, card_content_w() - S(24.0f), C_DIM, S(2.0f)); } else if (ct.kind == CK_BUTTON) { int bw = button_w(ct.label); Rect b{cr.x + cr.w - S(16.0f) - bw, by + (rh - S(26.0f)) / 2, bw, S(26.0f)}; fill(b, Col{45, 50, 60, 255}, &clip); outline(b, C_BORDER, &clip); const char *label = ct.label; if (ct.action == ACT_RESET && g_reset_armed) { if (now_sec() - g_reset_armed_at >= 3.0) g_reset_armed = false; else label = "Confirm reset"; } text_in(clip, b.x + S(10.0f), b.y + (b.h - g_body_h) / 2, label, g_font_body, g_body_h, 0, C_TEXT); } else { int maxw = label_max_w(ct); std::vector lines; wrap_text(ct.label, maxw, g_body_h, lines); int lh = g_body_h + S(2.0f); int text_h = (int)lines.size() * lh - S(2.0f); int base_h = label_row_h(ct); int ly = by + (base_h - text_h) / 2; draw_wrapped(clip, cr.x + S(16.0f), ly, ct.label, g_font_body, g_body_h, maxw, C_TEXT, S(2.0f)); if (ct.kind == CK_TOGGLE) { bool on = ctrl_val(ct) != 0; draw_checkbox(cr.x + cr.w - S(31.0f), by + (rh - S(15.0f)) / 2, on, &clip); text_in(clip, cr.x + cr.w - S(58.0f), by + (rh - g_body_h) / 2, on ? "On" : "Off", g_font_body, g_body_h, 0, on ? C_GOOD : C_DIM); } else if (ct.kind == CK_SLIDER) { draw_slider(clip, cr, by, rh, ct); } else if (ct.kind == CK_DROPDOWN) { int cur = ct.ival ? *ct.ival : 0; if (cur < 0) cur = 0; if (cur >= ct.nopts) cur = ct.nopts - 1; const char *opt = (ct.opts && cur >= 0 && cur < ct.nopts) ? ct.opts[cur] : "?"; int bw = S(150.0f); Rect btn{cr.x + cr.w - S(16.0f) - bw, by + (base_h - S(26.0f)) / 2, bw, S(26.0f)}; fill(btn, Col{45, 50, 60, 255}, &clip); outline(btn, g_dropdown_ctrl == c.c0 + j ? C_ACCENT : C_BORDER, &clip); text_in(clip, btn.x + S(9), btn.y + (btn.h - g_body_h) / 2, opt, g_font_body, g_body_h, btn.w - S(30), C_TEXT); text_in(clip, btn.x + btn.w - S(16), btn.y + (btn.h - g_body_h) / 2, "v", g_font_body, g_body_h, 0, C_DIM); } else if (ct.kind == CK_COLOR) { int pr = ct.rgb ? ct.rgb[0] : 255; int pg = ct.rgb ? ct.rgb[1] : 255; int pb = ct.rgb ? ct.rgb[2] : 255; int pa = (ct.has_alpha && ct.rgb) ? ct.rgb[3] : 255; Rect sw{cr.x + cr.w - S(96.0f), by + (base_h - S(20.0f)) / 2, S(72.0f), S(20.0f)}; if (pa < 255) draw_checker(sw, &clip); fill(sw, Col{pr, pg, pb, pa}, &clip); outline(sw, picker_open_for(ct) ? C_ACCENT : C_BORDER, &clip); if (picker_open_for(ct)) draw_picker(clip, cr, ct, by); } } by += rh; } } static void draw_tooltip() { if (g_dropdown_ctrl >= 0) { Rect btn, pop; int ih = 0; if (dropdown_rects(g_dropdown_ctrl, btn, pop, ih) && (pop.has(g_mouse_x, g_mouse_y) || btn.has(g_mouse_x, g_mouse_y))) return; } if (!g_hover_desc || !g_hover_desc[0]) return; float d = now_sec() - (float)g_hover_since; if (d < 0.4f) return; int maxw = S(380.0f); if (maxw > g_scr_w - S(32.0f)) maxw = g_scr_w - S(32.0f); std::vector lines; wrap_text(g_hover_desc, maxw, g_body_h, lines); if (lines.empty()) return; int line_w = 0; for (auto &line : lines) { int w = text_w(line.c_str(), g_body_h); if (w > line_w) line_w = w; } int line_h = g_body_h + S(3.0f); int tw = line_w + S(16.0f); int th = (int)lines.size() * line_h + S(12.0f) - S(3.0f); int x = g_hover_x + S(14.0f); int y = g_hover_y + S(16.0f); if (x + tw > g_scr_w - S(4.0f)) x = g_hover_x - tw - S(10.0f); if (y + th > g_scr_h - S(4.0f)) y = g_hover_y - th - S(10.0f); if (x < S(4.0f)) x = S(4.0f); if (y < S(4.0f)) y = S(4.0f); Rect r{x, y, tw, th}; fill(r, Col{12, 14, 18, 248}); outline(r, C_ACCENT_DIM); s_text_font(g_font_body); s_text_color(C_TEXT.r, C_TEXT.g, C_TEXT.b, C_TEXT.a); for (size_t i = 0; i < lines.size(); i++) draw_text_raw(x + S(7.0f), y + S(6.0f) + (int)i * line_h, lines[i].c_str(), g_font_body, g_body_h); } // --------------------------------------------------------------------------- // Toast drawing // --------------------------------------------------------------------------- void gui_draw_toasts() { if (!g_surface) return; ensure_fonts(); if (!g_font_body || g_scr_w <= 0 || g_scr_h <= 0) return; Toast local[8]; int n = 0; double now = now_sec(); pthread_mutex_lock(&g_toast_mu); for (int i = 0; i < g_ntoasts; i++) { Toast &t = g_toasts[i]; if (t.born == 0.0) t.born = now; if (now - t.born > t.ttl) continue; local[n++] = t; } g_ntoasts = 0; for (int i = 0; i < n; i++) g_toasts[g_ntoasts++] = local[i]; pthread_mutex_unlock(&g_toast_mu); int margin = S(16.0f); int overhead = S(12.0f) * 2 + S(6.0f); // panel padding + accent stripe gap int max_w = S(420.0f); if (max_w + overhead > g_scr_w - margin * 2) max_w = g_scr_w - margin * 2 - overhead; if (max_w < S(60.0f)) max_w = S(60.0f); int y = margin; for (int i = 0; i < n; i++) { const Toast &t = local[i]; float age = (float)(now - t.born); float a = 1.0f; if (age < 0.2f) a = age / 0.2f; else if (age > t.ttl - 0.5f) a = (float)((t.ttl - age) / 0.5); if (a < 0.0f) a = 0.0f; if (a > 1.0f) a = 1.0f; int alpha = (int)(a * 255.0f); std::vector lines; wrap_text(t.text, max_w, g_body_h, lines); int nl = (int)lines.size(); if (nl == 0) continue; int line_w = 0; for (int j = 0; j < nl; j++) { int w = text_w(lines[j].c_str(), g_body_h); if (w > line_w) line_w = w; } int pad = S(12.0f); int line_h = g_body_h + S(4.0f); int panel_w = line_w + pad * 2 + S(6.0f); int panel_h = nl * line_h + pad * 2 - S(4.0f); int x = g_scr_w - margin - panel_w; if (x < margin) x = margin; Col accent = t.level == TOAST_ERROR ? Col{240, 90, 90, 255} : t.level == TOAST_WARN ? Col{255, 200, 80, 255} : C_ACCENT; fill(Rect{x, y, panel_w, panel_h}, Col{16, 18, 22, (int)(235 * a)}); outline(Rect{x, y, panel_w, panel_h}, Col{accent.r, accent.g, accent.b, alpha}); fill(Rect{x, y, S(4.0f), panel_h}, Col{accent.r, accent.g, accent.b, alpha}); s_text_font(g_font_body); s_text_color(C_TEXT.r, C_TEXT.g, C_TEXT.b, alpha); for (int j = 0; j < nl; j++) { s_text_pos(x + pad + S(6.0f), y + pad + j * line_h); wchar_t w[256]; size_t wn = mbstowcs(w, lines[j].c_str(), 255); if (wn == (size_t)-1) continue; w[wn] = 0; s_print(w, (int)wn); } y += panel_h + S(8.0f); } } void gui_draw() { if (!g_open || !g_surface) return; layout(); ensure_fonts(); int w = g_scr_w, h = g_scr_h; if (w <= 0 || h <= 0) return; Rect win{g_win_x, g_win_y, g_win_w, g_win_h}; fill(Rect{win.x + S(6.0f), win.y + S(6.0f), win.w, win.h}, Col{0, 0, 0, 100}); fill(win, C_BG); outline(win, C_BORDER); Rect title{win.x, win.y, win.w, S(38.0f)}; fill(title, Col{31, 35, 42, 255}); fill(Rect{win.x, win.y, S(4.0f), win.h}, C_ACCENT); text_in(win, win.x + S(16.0f), win.y + S(7.0f), "HL2hake", g_font_title, g_title_h, win.w - S(80.0f), C_TEXT); text_in(win, win.x + S(110.0f), win.y + S(10.0f), "Settings", g_font_body, g_body_h, win.w - S(180.0f), C_DIM); Rect side{win.x, win.y + S(38.0f), S(190.0f), win.h - S(38.0f) - S(30.0f)}; fill(side, C_SIDEBAR); int cat_h = S(34.0f); for (int i = 0; i < CAT_COUNT; i++) { Rect r{side.x, side.y + i * cat_h, side.w, cat_h}; if (i == g_cat) { fill(r, Col{42, 45, 54, 255}); fill(Rect{r.x, r.y, S(3.0f), r.h}, C_ACCENT); } text_in(side, r.x + S(18.0f), r.y + S(8.0f), g_cats[i], g_font_body, g_body_h, r.w - S(24.0f), i == g_cat ? C_TEXT : C_DIM); } Rect content{win.x + S(190.0f), win.y + S(38.0f), win.w - S(190.0f), win.h - S(38.0f) - S(30.0f)}; fill(content, Col{20, 22, 27, 255}); Rect clip{content.x + S(4.0f), content.y + S(4.0f), content.w - S(20.0f), content.h - S(8.0f)}; int y = content.y - (int)g_scroll + S(8.0f); for (int i = 0; i < g_ncards; i++) { if (!card_in_category(i)) continue; Card &c = g_cards[i]; int ch = card_h(c); if (y + ch >= clip.y && y <= clip.y + clip.h) draw_card(clip, i, y); y += ch; } // scrollbar if (g_scroll_max > 0.5f) { Rect track{content.x + content.w - S(8.0f), content.y, S(8.0f), content.h}; fill(track, Col{30, 33, 40, 255}); int thumb_h = (int)((float)content.h * (float)content.h / (float)(content.h + (int)g_scroll_max)); if (thumb_h < S(24.0f)) thumb_h = S(24.0f); int travel = content.h - thumb_h; int thumb_y = content.y + (int)((g_scroll_max > 0.0f) ? (g_scroll / g_scroll_max) * travel : 0); fill(Rect{track.x, thumb_y, track.w, thumb_h}, C_ACCENT_DIM); } Rect foot{win.x, win.y + win.h - S(30.0f), win.w, S(30.0f)}; fill(foot, Col{24, 27, 33, 255}); line(foot.x, foot.y, foot.x + foot.w, foot.y, C_BORDER); const char *hint = g_capture_card >= 0 ? "press a key / Esc" : "INS close"; int hint_w = text_w(hint, g_body_h); int hint_x = foot.x + foot.w - hint_w - S(12.0f); if (hint_x < foot.x + S(12.0f)) hint_x = foot.x + S(12.0f); text_in(foot, hint_x, foot.y + S(6.0f), hint, g_font_body, g_body_h, 0, C_DIM); draw_dropdown(); draw_tooltip(); } bool gui_is_open() { return g_open; } bool gui_bind_capture_active() { return g_open && g_capture_card >= 0; } QAngle gui_frozen_view() { return g_frozen; } // --------------------------------------------------------------------------- // [gui] layout persistence // --------------------------------------------------------------------------- static void gui_load_layout() { FILE *f = fopen(config_path_cstr(), "r"); if (!f) return; bool in_gui = false, have_x = false, have_y = false; int x = 0, y = 0, cat = 0; unsigned expanded = 0; bool have_expanded = false; char line[256]; while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\r\n")] = 0; if (line[0] == '[') { in_gui = strstr(line, "[gui]") != nullptr; continue; } if (!in_gui) continue; char *eq = strchr(line, '='); if (!eq) continue; *eq = 0; const char *key = line, *val = eq + 1; if (strcmp(key, "x") == 0) { x = atoi(val); have_x = true; } else if (strcmp(key, "y") == 0) { y = atoi(val); have_y = true; } else if (strcmp(key, "category") == 0) { cat = atoi(val); } else if (strcmp(key, "expanded") == 0) { expanded = (unsigned)strtoul(val, nullptr, 10); have_expanded = true; } } fclose(f); if (have_x && have_y) { g_win_x = x; g_win_y = y; g_positioned = true; } if (cat >= 0 && cat < CAT_COUNT) g_cat = cat; if (have_expanded) { for (int i = 0; i < g_ncards && i < 32; i++) g_cards[i].expanded = (expanded & (1u << i)) != 0; } } void gui_config_init() { build_cards(); if (g_layout_loaded) return; g_layout_loaded = true; gui_load_layout(); } void gui_config_write(FILE *f) { if (!f || !g_cards_built || !g_positioned) return; unsigned expanded = 0; for (int i = 0; i < g_ncards && i < 32; i++) if (g_cards[i].expanded) expanded |= 1u << i; fprintf(f, "[gui]\nx=%d\ny=%d\ncategory=%d\nexpanded=%u\n", g_win_x, g_win_y, g_cat, expanded); } // --------------------------------------------------------------------------- // SDL cursor interposition // --------------------------------------------------------------------------- // The game re-asserts relative mouse mode / hides the cursor every frame. While // the GUI is open, force the opposite so the native cursor stays usable. static int (*real_show_cursor)(int) = nullptr; static int (*real_set_relative)(int) = nullptr; static void (*real_warp_mouse)(void *, int, int) = nullptr; static void (*real_set_grab)(void *, int) = nullptr; extern "C" __attribute__((visibility("default"))) int SDL_ShowCursor(int toggle) { if (!real_show_cursor) real_show_cursor = (int (*)(int))dlsym(RTLD_NEXT, "SDL_ShowCursor"); if (g_open && toggle != -1) { // Remember what the game asked for so close_gui() can restore it. g_game_wants_cursor = toggle != 0; if (real_show_cursor) real_show_cursor(1); return 1; } return real_show_cursor ? real_show_cursor(toggle) : 0; } extern "C" __attribute__((visibility("default"))) int SDL_SetRelativeMouseMode(int enabled) { if (!real_set_relative) real_set_relative = (int (*)(int))dlsym(RTLD_NEXT, "SDL_SetRelativeMouseMode"); if (g_open) { g_game_wants_relative = enabled != 0; if (real_set_relative) real_set_relative(0); return 0; } return real_set_relative ? real_set_relative(enabled) : 0; } extern "C" __attribute__((visibility("default"))) void SDL_WarpMouseInWindow(void *window, int x, int y) { if (!real_warp_mouse) real_warp_mouse = (void (*)(void *, int, int))dlsym(RTLD_NEXT, "SDL_WarpMouseInWindow"); if (g_open) return; if (real_warp_mouse) real_warp_mouse(window, x, y); } extern "C" __attribute__((visibility("default"))) void SDL_SetWindowGrab(void *window, int grabbed) { if (!real_set_grab) real_set_grab = (void (*)(void *, int))dlsym(RTLD_NEXT, "SDL_SetWindowGrab"); if (g_open) { if (real_set_grab) real_set_grab(window, 0); return; } if (real_set_grab) real_set_grab(window, grabbed); } static void (*real_set_mouse_grab)(void *, int) = nullptr; static void (*real_warp_global)(int, int) = nullptr; extern "C" __attribute__((visibility("default"))) void SDL_SetWindowMouseGrab(void *window, int grabbed) { if (!real_set_mouse_grab) real_set_mouse_grab = (void (*)(void *, int))dlsym(RTLD_NEXT, "SDL_SetWindowMouseGrab"); if (g_open) { if (real_set_mouse_grab) real_set_mouse_grab(window, 0); return; } if (real_set_mouse_grab) real_set_mouse_grab(window, grabbed); } extern "C" __attribute__((visibility("default"))) void SDL_WarpMouseGlobal(int x, int y) { if (!real_warp_global) real_warp_global = (void (*)(int, int))dlsym(RTLD_NEXT, "SDL_WarpMouseGlobal"); if (g_open) return; if (real_warp_global) real_warp_global(x, y); } static unsigned int (*real_get_rel_mouse)(int *, int *) = nullptr; static int (*real_poll_event)(void *) = nullptr; extern "C" __attribute__((visibility("default"))) unsigned int SDL_GetRelativeMouseState(int *x, int *y) { if (!real_get_rel_mouse) real_get_rel_mouse = (unsigned int (*)(int *, int *))dlsym(RTLD_NEXT, "SDL_GetRelativeMouseState"); unsigned int mask = real_get_rel_mouse ? real_get_rel_mouse(x, y) : 0; if (g_open) { if (x) *x = 0; if (y) *y = 0; } return mask; } extern "C" __attribute__((visibility("default"))) int SDL_PollEvent(void *event) { if (!real_poll_event) real_poll_event = (int (*)(void *))dlsym(RTLD_NEXT, "SDL_PollEvent"); if (!real_poll_event) return 0; for (;;) { int r = real_poll_event(event); if (!r) return 0; if (!g_open || !event) return r; unsigned int type = *(unsigned int *)event; if (type != 0x400) // SDL_MOUSEMOTION return r; // Keep the event (VGUI hover and SDL's internal cursor state need it) but // zero the relative deltas so the game does not accumulate look movement. // SDL_MouseMotionEvent: xrel at +28, yrel at +32. *(int *)((char *)event + 28) = 0; *(int *)((char *)event + 32) = 0; return r; } }