From bdf59c4bbf118cb192f0019153b6b87db25f33eb Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:02:32 +0200 Subject: [PATCH 01/54] refactor(render): measure text via a stable font, not the live ImGui context Features call Render::Measure from PostRender (game thread) while the external overlay switches/destroys ImGui's global current-context on the Present thread; measuring through ImGui::GetFont() raced that global and crashed (access violation in GetFont). Cache the game context's default font once after init and measure through it - ImFont::CalcTextSizeA is a const, context-free call - so text measurement never touches GImGui and the cross-thread race is gone at the source. Supersedes the earlier null-context guard. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/gui/Gui.h | 6 ++++++ Internal/render/ImGuiRenderer.h | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Internal/menu/gui/Gui.h b/Internal/menu/gui/Gui.h index cf1b668..c93b855 100644 --- a/Internal/menu/gui/Gui.h +++ b/Internal/menu/gui/Gui.h @@ -78,6 +78,12 @@ namespace GUI ImGui_ImplWin32_Init(Window::WindowHandle); ImGui_ImplDX11_Init(Window::Device, Window::DeviceContext); ImGui_ImplDX11_CreateDeviceObjects(); + + // Cache this context's default font for Render::Measure, so features measure text without ever + // reading ImGui's global current-context (which the external overlay switches on another thread). + // Fonts[0] is valid now that the atlas has been built above. + if (ImFontAtlas* fonts = ImGui::GetIO().Fonts; fonts && fonts->Fonts.Size > 0) + Render::imgui.measureFont = fonts->Fonts[0]; ImGui::GetMainViewport()->PlatformHandleRaw = Window::WindowHandle; Window::OldWindowProcess = (WNDPROC)SetWindowLongPtr(Window::WindowHandle, GWLP_WNDPROC, (__int3264)(LONG_PTR)Window::WndProc); diff --git a/Internal/render/ImGuiRenderer.h b/Internal/render/ImGuiRenderer.h index 39890b0..f7c826a 100644 --- a/Internal/render/ImGuiRenderer.h +++ b/Internal/render/ImGuiRenderer.h @@ -68,6 +68,13 @@ class ImGuiRenderer : public Renderer std::mutex mtx; public: + /// A stable font used only for text measurement (Measure/TextSize/StrLen). Set once from the + /// game-window ImGui context's atlas after init; ImFont::CalcTextSizeA is a const, context-free + /// call, so measuring through this pointer never touches ImGui's global current-context (which the + /// streamproof overlay switches/destroys on another thread). Null until set — falls back to an + /// approximation. + ImFont* measureFont = nullptr; + void Line(const Render::Vec2& a, const Render::Vec2& b, float thickness, const Render::Color& color) override { std::lock_guard guard(mtx); @@ -82,10 +89,11 @@ class ImGuiRenderer : public Renderer Render::Vec2 TextSize(const std::string& text, float scale) override { - // GetFont() dereferences the current ImGui context, which can momentarily be null while the - // external overlay is switching/destroying its context on another thread — guard the pointer - // itself (GetCurrentContext is a plain read) before calling GetFont, or this crashes. - ImFont* font = ImGui::GetCurrentContext() ? ImGui::GetFont() : nullptr; + // Measure through the stable measureFont, never ImGui::GetFont(): features call this from + // PostRender (game thread) while the streamproof overlay switches/destroys ImGui's global + // current-context on the Present thread — reading GetFont() there raced and crashed. + // CalcTextSizeA is a const, context-free method on the font object, so this is race-free. + ImFont* font = measureFont; if (!font) return {text.length() * scale * 7.f, scale * 14.f}; const ImVec2 size = font->CalcTextSizeA(font->FontSize * scale, FLT_MAX, 0.f, text.c_str()); return {size.x, size.y}; From e6e9f88e6c674fb121afd61aa16aac15b3996c4f Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 08:04:06 +0200 Subject: [PATCH 02/54] chore(visuals): log external-overlay draw stats to diagnose the blank window Log (first frames + periodically) the focus state, back-buffer size, the frame's total vertex count, and the Present HRESULT, so the next run tells us why the overlay shows nothing: vtx==0 = recorded commands aren't reaching this draw list; vtx>0 but blank = DirectComposition isn't compositing; present!=0 = Present failed. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/gui/ExternalWindow.h | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Internal/menu/gui/ExternalWindow.h b/Internal/menu/gui/ExternalWindow.h index 0f4541a..e93abf9 100644 --- a/Internal/menu/gui/ExternalWindow.h +++ b/Internal/menu/gui/ExternalWindow.h @@ -42,6 +42,8 @@ #pragma comment(lib, "dxgi.lib") #pragma comment(lib, "dcomp.lib") +#include + #include #include "imgui_Impl_dx11.h" #include "imgui_Impl_Win32.h" @@ -348,8 +350,19 @@ namespace ExternalWindow const float transparent[4] = {0.f, 0.f, 0.f, 0.f}; Context->OMSetRenderTargets(1, &Rtv, nullptr); Context->ClearRenderTargetView(Rtv, transparent); - ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); - - SwapChain->Present(0, 0); // no vsync: we're on the game thread and must not block on it + ImDrawData* drawData = ImGui::GetDrawData(); + ImGui_ImplDX11_RenderDrawData(drawData); + + const HRESULT present = SwapChain->Present(0, 0); // no vsync: we're on the game thread, must not block + + // Diagnostic (first frames + periodic): why nothing shows. vtx==0 => the recorded commands + // aren't reaching this draw list (recording/drain problem); vtx>0 but nothing on screen => + // the DirectComposition swap chain isn't compositing; present!=0 => the Present itself failed. + static int diagFrames = 0; + if (diagFrames < 5 || (diagFrames % 600) == 0) + Logger::Log(SUCCEEDED(present) ? "INFO" : "ERROR", + std::format("[Overlay] render: focused={} size={}x{} vtx={} present=0x{:08X}", + focused, Width, Height, drawData ? drawData->TotalVtxCount : -1, static_cast(present))); + diagFrames++; } } // namespace ExternalWindow From 00a0a969f8c6cc8a66f14948545b1da75e1251bf Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:50:55 +0200 Subject: [PATCH 03/54] feat(settings): add MenuBackend (imgui/canvas) menu-engine setting Selects which GUI engine draws the menu, independent of RendererMode (the ESP overlay renderer). Mirrors the RendererMode enum + NLOHMANN_JSON_SERIALIZE_ENUM and persists via the MenuSettings macro (missing key -> ImGui default). Co-Authored-By: Claude Opus 4.8 --- Internal/settings/Settings.h | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Internal/settings/Settings.h b/Internal/settings/Settings.h index 131cd2b..4b6efca 100644 --- a/Internal/settings/Settings.h +++ b/Internal/settings/Settings.h @@ -52,6 +52,19 @@ NLOHMANN_JSON_SERIALIZE_ENUM(RendererMode, { {RendererMode::External, "external"}, }) +/// Which GUI engine draws the menu itself (independent of RendererMode, which is for the ESP/overlay). +/// ImGui draws in the Present hook; Canvas draws through the UE canvas (ZeroGUI) in PostRender. +enum class MenuBackend +{ + ImGui, + Canvas, +}; + +NLOHMANN_JSON_SERIALIZE_ENUM(MenuBackend, { + {MenuBackend::ImGui, "imgui"}, + {MenuBackend::Canvas, "canvas"}, + }) + /// Menu appearance, the show/hide hotkey, and how the overlays are rendered. struct MenuSettings { @@ -61,9 +74,10 @@ struct MenuSettings int ShowHotkey = VK_INSERT; ///< virtual-key code toggling the GUI (default Insert) bool Rgb = false; ///< cycle the watermark, menu accent, and radar self-icon through a rainbow; off = their defaults (red / white) RendererMode Renderer = RendererMode::Canvas; ///< how the overlays are drawn (canvas / imgui / null / external) + MenuBackend Backend = MenuBackend::ImGui; ///< which GUI engine draws the menu (imgui / canvas) }; -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(MenuSettings, ShowMenu, ShowWatermark, ShowHotkey, Rgb, Renderer) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(MenuSettings, ShowMenu, ShowWatermark, ShowHotkey, Rgb, Renderer, Backend) /// Gameplay feature toggles and tunables (the Exploits tab). /// Which camera the Camera feature drives. First person is the game default (no override). From f1081152f81a7baed9d9b0a548404c8f71c1db84 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:51:04 +0200 Subject: [PATCH 04/54] feat(menu): recover ZeroGUI/ZeroInput and add the Menu::UI widget facade Recovers the project's original UE-canvas immediate-mode GUI (ZeroGUI/ZeroInput) into menu/canvas/, adapted to the neutral Render API: all drawing routes through Render::canvas (no direct K2_Draw*, no per-scanline fill loops), s2wc dropped, colors normalized to 0-1, everything inline, and an array-form Combobox for the facade. ZeroInput samples all mouse buttons + keys each frame. Menu::UI (menu/ui/UI.h) is the backend-neutral widget vocabulary the sections call, dispatching on Settings.MENU.Backend: ImGui forwards to native ImGui / Custom.h widgets, Canvas forwards to ZeroGUI via Render::canvas. IsImGui()/ IsCanvas() gate the ImGui-only idioms; Vis() strips ##id suffixes for canvas text. Co-Authored-By: Claude Opus 4.8 --- Internal/Internal.vcxproj | 3 + Internal/menu/canvas/ZeroGUI.h | 1210 ++++++++++++++++++++++++++++++ Internal/menu/canvas/ZeroInput.h | 83 ++ Internal/menu/ui/UI.h | 210 ++++++ 4 files changed, 1506 insertions(+) create mode 100644 Internal/menu/canvas/ZeroGUI.h create mode 100644 Internal/menu/canvas/ZeroInput.h create mode 100644 Internal/menu/ui/UI.h diff --git a/Internal/Internal.vcxproj b/Internal/Internal.vcxproj index def500d..95d0554 100644 --- a/Internal/Internal.vcxproj +++ b/Internal/Internal.vcxproj @@ -264,6 +264,9 @@ + + + diff --git a/Internal/menu/canvas/ZeroGUI.h b/Internal/menu/canvas/ZeroGUI.h new file mode 100644 index 0000000..c5a1f0a --- /dev/null +++ b/Internal/menu/canvas/ZeroGUI.h @@ -0,0 +1,1210 @@ +#pragma once + +/// @file +/// @brief ZeroGUI — the immediate-mode UE-canvas menu backend. Recovered from the project's original +/// GUI and adapted to the neutral Render API: every primitive now draws through `Render::canvas` +/// (which targets `Engine::Canvas`), so there is no direct K2_Draw* here and no per-scanline fill +/// loops. Positions stay FVector2D and colors FLinearColor (they convert implicitly to Render::Vec2 / +/// Render::Color). Everything is `inline` — this header is included in multiple translation units. + +#include +#include +#include +#include + +#include "ZeroInput.h" +#include "../../ue/Engine.h" +#include "../../render/Render.h" + +namespace ZeroGUI +{ + /// Cream/red theme (0-1 float RGBA). MainColor is retinted from the RGB rainbow each tick. + namespace Colors + { + inline FLinearColor MainColor{1.0f, 0.0f, 0.0f, 1.0f}; + + inline FLinearColor Text{0.10f, 0.10f, 0.10f, 1.0f}; + + inline FLinearColor Window_Background{0.92f, 0.91f, 0.88f, 1.0f}; + inline FLinearColor Window_Header{1.0f, 0.0f, 0.0f, 1.0f}; + inline FLinearColor Window_Tabs_Background{0.80f, 0.79f, 0.76f, 1.0f}; + + inline FLinearColor Button_Idle{1.0f, 0.0f, 0.0f, 1.0f}; + inline FLinearColor Button_Hovered{1.0f, 0.0f, 0.0f, 1.0f}; + inline FLinearColor Button_Active{1.0f, 0.0f, 0.0f, 1.0f}; + + inline FLinearColor Checkbox_Idle{1.0f, 0.0f, 0.0f, 1.0f}; + inline FLinearColor Checkbox_Hovered{1.0f, 0.0f, 0.0f, 1.0f}; + inline FLinearColor Checkbox_Enabled{0.0f, 0.80f, 0.28f, 1.0f}; + + inline FLinearColor Combobox_Idle{0.85f, 0.84f, 0.81f, 1.0f}; + inline FLinearColor Combobox_Hovered{0.85f, 0.84f, 0.81f, 1.0f}; + inline FLinearColor Combobox_Elements{0.24f, 0.42f, 0.82f, 1.0f}; + + inline FLinearColor Slider_Idle{0.80f, 0.79f, 0.76f, 1.0f}; + inline FLinearColor Slider_Hovered{0.80f, 0.79f, 0.76f, 1.0f}; + inline FLinearColor Slider_Progress{1.0f, 0.0f, 0.0f, 1.0f}; + inline FLinearColor Slider_Button{0.70f, 0.70f, 0.70f, 1.0f}; + + inline FLinearColor ColorPicker_Background{0.90f, 0.89f, 0.86f, 1.0f}; + } // namespace Colors + + // Forward declarations of the drawing helpers (PostRenderer's dispatch calls back into them). + inline void drawFilledRect(FVector2D initial_pos, float w, float h, FLinearColor color); + inline void TextLeft(const char* name, FVector2D pos, FLinearColor color, bool outline); + inline void TextCenter(const char* name, FVector2D pos, FLinearColor color, bool outline); + inline void Draw_Line(FVector2D from, FVector2D to, int thickness, FLinearColor color); + + /// Deferred draw queue: pop-ups (combo dropdowns, color-picker swatches) enqueue here so they + /// replay last, on top of the widgets drawn earlier in the frame. Render() drains it. + namespace PostRenderer + { + struct DrawList + { + int type = -1; // 1 = FilledRect, 2 = TextLeft, 3 = TextCenter, 4 = Draw_Line + FVector2D pos; + FVector2D size; + FLinearColor color; + const char* name; + bool outline; + + FVector2D from; + FVector2D to; + int thickness; + }; + inline DrawList drawlist[128]; + + inline void drawFilledRect(FVector2D pos, float w, float h, FLinearColor color) + { + for (int i = 0; i < 128; i++) + { + if (drawlist[i].type == -1) + { + drawlist[i].type = 1; + drawlist[i].pos = pos; + drawlist[i].size = FVector2D{w, h}; + drawlist[i].color = color; + return; + } + } + } + inline void TextLeft(const char* name, FVector2D pos, FLinearColor color, bool outline) + { + for (int i = 0; i < 128; i++) + { + if (drawlist[i].type == -1) + { + drawlist[i].type = 2; + drawlist[i].name = name; + drawlist[i].pos = pos; + drawlist[i].outline = outline; + drawlist[i].color = color; + return; + } + } + } + inline void TextCenter(const char* name, FVector2D pos, FLinearColor color, bool outline) + { + for (int i = 0; i < 128; i++) + { + if (drawlist[i].type == -1) + { + drawlist[i].type = 3; + drawlist[i].name = name; + drawlist[i].pos = pos; + drawlist[i].outline = outline; + drawlist[i].color = color; + return; + } + } + } + inline void Draw_Line(FVector2D from, FVector2D to, int thickness, FLinearColor color) + { + for (int i = 0; i < 128; i++) + { + if (drawlist[i].type == -1) + { + drawlist[i].type = 4; + drawlist[i].from = from; + drawlist[i].to = to; + drawlist[i].thickness = thickness; + drawlist[i].color = color; + return; + } + } + } + } // namespace PostRenderer + + // --- Immediate-mode layout state (single window). --- + inline bool hover_element = false; + inline FVector2D menu_pos = FVector2D{0, 0}; + inline float offset_x = 0.0f; + inline float offset_y = 0.0f; + + inline FVector2D first_element_pos = FVector2D{0, 0}; + + inline FVector2D last_element_pos = FVector2D{0, 0}; + inline FVector2D last_element_size = FVector2D{0, 0}; + + inline int current_element = -1; + inline FVector2D current_element_pos = FVector2D{0, 0}; + inline FVector2D current_element_size = FVector2D{0, 0}; + inline int elements_count = 0; + + inline bool sameLine = false; + + inline bool pushY = false; + inline float pushYvalue = 0.0f; + + /// Point the canvas backend at the frame's UCanvas (Render::canvas draws through Engine::Canvas). + inline void SetupCanvas(UCanvas* _canvas) + { + Engine::Canvas = _canvas; + } + + inline FVector2D CursorPos() + { + POINT cursorPos; + GetCursorPos(&cursorPos); + + ScreenToClient(GetActiveWindow(), &cursorPos); + + return FVector2D{(float)cursorPos.x, (float)cursorPos.y}; + } + inline bool MouseInZone(FVector2D pos, FVector2D size) + { + FVector2D cursor_pos = CursorPos(); + + if (cursor_pos.X > pos.X && cursor_pos.Y > pos.Y) + if (cursor_pos.X < pos.X + size.X && cursor_pos.Y < pos.Y + size.Y) + return true; + + return false; + } + + /// Software arrow cursor built from line segments (the Canvas menu owns its cursor; it does not + /// borrow ImGui's software cursor, keeping the two backends fully decoupled). + inline void Draw_Cursor(bool toogle) + { + if (toogle) + { + FVector2D cursorPos = CursorPos(); + Render::canvas.Line(FVector2D{cursorPos.X, cursorPos.Y}, FVector2D{cursorPos.X + 35, cursorPos.Y + 10}, 1, FLinearColor{0.30f, 0.30f, 0.80f, 1.0f}); + + int x = 35; + int y = 10; + while (y != 30) // 20 steps + { + x -= 1; + if (x < 15) x = 15; + y += 1; + if (y > 30) y = 30; + + Render::canvas.Line(FVector2D{cursorPos.X, cursorPos.Y}, FVector2D{cursorPos.X + x, cursorPos.Y + y}, 1, FLinearColor{0.30f, 0.30f, 0.80f, 1.0f}); + } + + Render::canvas.Line(FVector2D{cursorPos.X, cursorPos.Y}, FVector2D{cursorPos.X + 15, cursorPos.Y + 30}, 1, FLinearColor{0.30f, 0.30f, 0.80f, 1.0f}); + Render::canvas.Line(FVector2D{cursorPos.X + 35, cursorPos.Y + 10}, FVector2D{cursorPos.X + 15, cursorPos.Y + 30}, 1, FLinearColor{0.30f, 0.30f, 0.80f, 1.0f}); + } + } + + inline void SameLine() + { + sameLine = true; + } + inline void PushNextElementY(float y, bool from_last_element = true) + { + pushY = true; + if (from_last_element) + pushYvalue = last_element_pos.Y + last_element_size.Y + y; + else + pushYvalue = y; + } + inline void NextColumn(float x) + { + offset_x = x; + PushNextElementY(first_element_pos.Y, false); + } + inline void ClearFirstPos() + { + first_element_pos = FVector2D{0, 0}; + } + + inline void TextLeft(const char* name, FVector2D pos, FLinearColor color, bool outline) + { + Render::canvas.Text(pos, std::string(name), 0.97f, color, false); + } + inline void TextCenter(const char* name, FVector2D pos, FLinearColor color, bool outline) + { + Render::canvas.Text(pos, std::string(name), 0.97f, color, true); + } + + inline void GetColor(FLinearColor* color, float* r, float* g, float* b, float* a) + { + *r = color->R; + *g = color->G; + *b = color->B; + *a = color->A; + } + inline UINT32 GetColorUINT(int r, int g, int b, int a) + { + UINT32 result = (BYTE(a) << 24) + (BYTE(r) << 16) + (BYTE(g) << 8) + BYTE(b); + return result; + } + + inline void Draw_Line(FVector2D from, FVector2D to, int thickness, FLinearColor color) + { + Render::canvas.Line(FVector2D{from.X, from.Y}, FVector2D{to.X, to.Y}, (float)thickness, color); + } + inline void drawFilledRect(FVector2D initial_pos, float w, float h, FLinearColor color) + { + Render::canvas.RectFilled(FVector2D{initial_pos.X, initial_pos.Y}, FVector2D{initial_pos.X + w, initial_pos.Y + h}, color); + } + inline void DrawFilledCircle(FVector2D pos, float r, FLinearColor color) + { + Render::canvas.CircleFilled(FVector2D{pos.X, pos.Y}, r, color); + } + inline void DrawCircle(FVector2D pos, int radius, int numSides, FLinearColor Color) + { + float P_I = 3.1415927f; + + float Step = P_I * 2.0f / numSides; + int Count = 0; + FVector2D V[128]; + for (float a = 0; a < P_I * 2.0f; a += Step) + { + float X1 = radius * cosf(a) + pos.X; + float Y1 = radius * sinf(a) + pos.Y; + float X2 = radius * cosf(a + Step) + pos.X; + float Y2 = radius * sinf(a + Step) + pos.Y; + V[Count].X = X1; + V[Count].Y = Y1; + V[Count + 1].X = X2; + V[Count + 1].Y = Y2; + Draw_Line(FVector2D{V[Count].X, V[Count].Y}, FVector2D{X2, Y2}, 1, Color); // Circle Around + } + } + + inline FVector2D dragPos; + inline bool Window(const char* name, FVector2D* pos, FVector2D size, bool isOpen) + { + elements_count = 0; + static HWND HWND = FindWindow((L"UnrealWindow"), (L"PortalWars ")); + + if (!isOpen || (GetActiveWindow() != HWND)) + { + return false; + }; + + bool isHovered = MouseInZone(FVector2D{pos->X, pos->Y}, size); + + // Drop last element + if (current_element != -1 && !GetAsyncKeyState(0x1)) + { + current_element = -1; + } + + // Drag + if (hover_element && GetAsyncKeyState(0x1)) + { + } + else if ((isHovered || dragPos.X != 0) && !hover_element) + { + if (Input::IsMouseClicked(0, elements_count, true)) + { + FVector2D cursorPos = CursorPos(); + + cursorPos.X -= size.X; + cursorPos.Y -= size.Y; + + if (dragPos.X == 0) + { + dragPos.X = (cursorPos.X - pos->X); + dragPos.Y = (cursorPos.Y - pos->Y); + } + pos->X = cursorPos.X - dragPos.X; + pos->Y = cursorPos.Y - dragPos.Y; + } + else + { + dragPos = FVector2D{0, 0}; + } + } + else + { + hover_element = false; + } + + offset_x = 0.0f; + offset_y = 0.0f; + menu_pos = FVector2D{pos->X, pos->Y}; + first_element_pos = FVector2D{0, 0}; + current_element_pos = FVector2D{0, 0}; + current_element_size = FVector2D{0, 0}; + + // Bg + drawFilledRect(FVector2D{pos->X, pos->Y}, size.X, size.Y, Colors::Window_Background); + drawFilledRect(FVector2D{pos->X, pos->Y}, 122, size.Y, Colors::Window_Tabs_Background); + + // Header + drawFilledRect(FVector2D{pos->X, pos->Y}, size.X, 25.0f, Colors::MainColor); + + offset_y += 25.0f; + + // Title + FVector2D titlePos = FVector2D{pos->X + size.X / 2, pos->Y + 25 / 2}; + TextCenter(name, titlePos, Colors::Text, false); + + return true; + } + + inline void Text(const char* text, bool center = false, bool outline = false) + { + elements_count++; + + float size = 25; + FVector2D padding = FVector2D{10, 10}; + FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; + if (sameLine) + { + pos.X = last_element_pos.X + last_element_size.X + padding.X; + pos.Y = last_element_pos.Y; + } + if (pushY) + { + pos.Y = pushYvalue; + pushY = false; + pushYvalue = 0.0f; + offset_y = pos.Y - menu_pos.Y; + } + + if (!sameLine) + offset_y += size + padding.Y; + + // Text + FVector2D textPos = FVector2D{pos.X + 5.0f, pos.Y + size / 2}; + if (center) + TextCenter(text, textPos, Colors::Text, outline); + else + TextLeft(text, textPos, Colors::Text, outline); + + sameLine = false; + last_element_pos = pos; + if (first_element_pos.X == 0.0f) + first_element_pos = pos; + } + inline bool ButtonTab(const char* name, FVector2D size, bool active) + { + elements_count++; + + FVector2D padding = FVector2D{5, 10}; + FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; + if (sameLine) + { + pos.X = last_element_pos.X + last_element_size.X + padding.X; + pos.Y = last_element_pos.Y; + } + if (pushY) + { + pos.Y = pushYvalue; + pushY = false; + pushYvalue = 0.0f; + offset_y = pos.Y - menu_pos.Y; + } + bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, size); + + // Bg + if (active) + { + drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::MainColor); + } + else if (isHovered) + { + drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::MainColor); + hover_element = true; + } + else + { + drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::MainColor); + } + + if (!sameLine) + offset_y += size.Y + padding.Y; + + // Text + FVector2D textPos = FVector2D{pos.X + size.X / 2, pos.Y + size.Y / 2}; + TextCenter(name, textPos, Colors::Text, false); + + sameLine = false; + last_element_pos = pos; + last_element_size = size; + if (first_element_pos.X == 0.0f) + first_element_pos = pos; + + if (isHovered && Input::IsMouseClicked(0, elements_count, false)) + return true; + + return false; + } + inline bool Button(const char* name, FVector2D size) + { + elements_count++; + + FVector2D padding = FVector2D{5, 10}; + FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; + if (sameLine) + { + pos.X = last_element_pos.X + last_element_size.X + padding.X; + pos.Y = last_element_pos.Y; + } + if (pushY) + { + pos.Y = pushYvalue; + pushY = false; + pushYvalue = 0.0f; + offset_y = pos.Y - menu_pos.Y; + } + bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, size); + + // Bg + if (isHovered) + { + drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::MainColor); + hover_element = true; + } + else + { + drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::MainColor); + } + + if (!sameLine) + offset_y += size.Y + padding.Y; + + // Text + FVector2D textPos = FVector2D{pos.X + size.X / 2, pos.Y + size.Y / 2}; + TextCenter(name, textPos, Colors::Text, false); + + sameLine = false; + last_element_pos = pos; + last_element_size = size; + if (first_element_pos.X == 0.0f) + first_element_pos = pos; + + if (isHovered && Input::IsMouseClicked(0, elements_count, false)) + return true; + + return false; + } + inline bool Checkbox(const char* name, bool* value) + { + elements_count++; + + float size = 18; + FVector2D padding = FVector2D{10, 10}; + FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; + if (sameLine) + { + pos.X = last_element_pos.X + last_element_size.X + padding.X; + pos.Y = last_element_pos.Y; + } + if (pushY) + { + pos.Y = pushYvalue; + pushY = false; + pushYvalue = 0.0f; + offset_y = pos.Y - menu_pos.Y; + } + bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, FVector2D{size, size}); + + // Bg + if (isHovered) + { + drawFilledRect(FVector2D{pos.X, pos.Y}, size, size, Colors::MainColor); + hover_element = true; + } + else + { + drawFilledRect(FVector2D{pos.X, pos.Y}, size, size, Colors::MainColor); + } + + if (!sameLine) + offset_y += size + padding.Y; + + if (*value) + { + drawFilledRect(FVector2D{pos.X + 3, pos.Y + 3}, size - 6, size - 6, Colors::Checkbox_Enabled); + } + + // Text + FVector2D textPos = FVector2D{pos.X + size + 5.0f, pos.Y + size / 2}; + TextLeft(name, textPos, Colors::Text, false); + + sameLine = false; + last_element_pos = pos; + if (first_element_pos.X == 0.0f) + first_element_pos = pos; + + if (isHovered && Input::IsMouseClicked(0, elements_count, false)) + { + *value = !*value; + return true; + } + return false; + } + inline void SliderInt(const char* name, int* value, int min, int max) + { + elements_count++; + + FVector2D size = FVector2D{240, 50}; + FVector2D slider_size = FVector2D{200, 10}; + FVector2D padding = FVector2D{10, 15}; + FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; + if (sameLine) + { + pos.X = last_element_pos.X + last_element_size.X + padding.X; + pos.Y = last_element_pos.Y; + } + if (pushY) + { + pos.Y = pushYvalue; + pushY = false; + pushYvalue = 0.0f; + offset_y = pos.Y - menu_pos.Y; + } + bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, slider_size); + + if (!sameLine) + offset_y += size.Y + padding.Y; + + // Bg + if (isHovered || current_element == elements_count) + { + // Drag + if (Input::IsMouseClicked(0, elements_count, true)) + { + current_element = elements_count; + + FVector2D cursorPos = CursorPos(); + *value = (int)(((cursorPos.X - pos.X) * ((max - min) / slider_size.X)) + min); + if (*value < min) *value = min; + if (*value > max) *value = max; + } + + drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, slider_size.X, slider_size.Y, Colors::Slider_Hovered); + drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y + 5.0f}, 5.0f, 5.0f, Colors::Slider_Progress); + + hover_element = true; + } + else + { + drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, slider_size.X, slider_size.Y, Colors::Slider_Idle); + drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y + 5.0f}, 5.0f, 5.0f, Colors::Slider_Progress); + } + + // Value + float oneP = slider_size.X / (max - min); + drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, oneP * (*value - min), slider_size.Y, Colors::Slider_Progress); + DrawFilledCircle(FVector2D{pos.X + oneP * (*value - min), pos.Y + slider_size.Y + 3.3f + padding.Y}, 10.0f, Colors::Slider_Button); + DrawFilledCircle(FVector2D{pos.X + oneP * (*value - min), pos.Y + slider_size.Y + 3.3f + padding.Y}, 5.0f, Colors::Slider_Progress); + + char buffer[32]; + sprintf_s(buffer, "%i", *value); + FVector2D valuePos = FVector2D{pos.X + oneP * (*value - min), pos.Y + slider_size.Y + 25 + padding.Y}; + TextCenter(buffer, valuePos, Colors::Text, false); + + // Text + FVector2D textPos = FVector2D{pos.X + 5, pos.Y + 10}; + TextLeft(name, textPos, Colors::Text, false); + + sameLine = false; + last_element_pos = pos; + last_element_size = size; + if (first_element_pos.X == 0.0f) + first_element_pos = pos; + } + inline void SliderFloat(const char* name, float* value, float min, float max, const char* format = "%.0f") + { + elements_count++; + + FVector2D size = FVector2D{210, 40}; + FVector2D slider_size = FVector2D{170, 7}; + FVector2D adjust_zone = FVector2D{0, 20}; + FVector2D padding = FVector2D{10, 15}; + FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; + if (sameLine) + { + pos.X = last_element_pos.X + last_element_size.X + padding.X; + pos.Y = last_element_pos.Y; + } + if (pushY) + { + pos.Y = pushYvalue; + pushY = false; + pushYvalue = 0.0f; + offset_y = pos.Y - menu_pos.Y; + } + bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y - adjust_zone.Y}, FVector2D{slider_size.X, slider_size.Y + adjust_zone.Y * 1.5f}); + + if (!sameLine) + offset_y += size.Y + padding.Y; + + // Bg + if (isHovered || current_element == elements_count) + { + // Drag + if (Input::IsMouseClicked(0, elements_count, true)) + { + current_element = elements_count; + + FVector2D cursorPos = CursorPos(); + *value = ((cursorPos.X - pos.X) * ((max - min) / slider_size.X)) + min; + if (*value < min) *value = min; + if (*value > max) *value = max; + } + + drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, slider_size.X, slider_size.Y, Colors::Slider_Hovered); + DrawFilledCircle(FVector2D{pos.X, pos.Y + padding.Y + 9.3f}, 3.1f, Colors::Slider_Progress); + DrawFilledCircle(FVector2D{pos.X + slider_size.X, pos.Y + padding.Y + 9.3f}, 3.1f, Colors::Slider_Hovered); + + hover_element = true; + } + else + { + drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, slider_size.X, slider_size.Y, Colors::Slider_Idle); + DrawFilledCircle(FVector2D{pos.X, pos.Y + padding.Y + 9.3f}, 3.1f, Colors::Slider_Progress); + DrawFilledCircle(FVector2D{pos.X + slider_size.X, pos.Y + padding.Y + 9.3f}, 3.1f, Colors::Slider_Idle); + } + + // Text + FVector2D textPos = FVector2D{pos.X, pos.Y + 5}; + TextLeft(name, textPos, Colors::Text, false); + + // Value + float oneP = slider_size.X / (max - min); + drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, oneP * (*value - min), slider_size.Y, Colors::Slider_Progress); + DrawFilledCircle(FVector2D{pos.X + oneP * (*value - min), pos.Y + slider_size.Y + 2.66f + padding.Y}, 8.0f, Colors::Slider_Button); + DrawFilledCircle(FVector2D{pos.X + oneP * (*value - min), pos.Y + slider_size.Y + 2.66f + padding.Y}, 4.0f, Colors::Slider_Progress); + + char buffer[32]; + sprintf_s(buffer, format, *value); + FVector2D valuePos = FVector2D{pos.X + oneP * (*value - min), pos.Y + slider_size.Y + 20 + padding.Y}; + TextCenter(buffer, valuePos, Colors::Text, false); + + sameLine = false; + last_element_pos = pos; + last_element_size = size; + if (first_element_pos.X == 0.0f) + first_element_pos = pos; + } + + inline bool checkbox_enabled[256]; + /// Dropdown combo over an array of @p count option strings (array form, for the widget facade). + inline bool Combobox(const char* name, FVector2D size, int* value, const char* const* items, int count) + { + elements_count++; + bool changed = false; + + FVector2D padding = FVector2D{5, 10}; + FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; + if (sameLine) + { + pos.X = last_element_pos.X + last_element_size.X + padding.X; + pos.Y = last_element_pos.Y; + } + if (pushY) + { + pos.Y = pushYvalue; + pushY = false; + pushYvalue = 0.0f; + offset_y = pos.Y - menu_pos.Y; + } + bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, size); + + // Bg + if (isHovered || checkbox_enabled[elements_count]) + { + drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::Combobox_Hovered); + hover_element = true; + } + else + { + drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::Combobox_Idle); + } + + if (!sameLine) + offset_y += size.Y + padding.Y; + + // Text (label) + FVector2D textPos = FVector2D{pos.X + size.X + 5.0f, pos.Y + size.Y / 2}; + TextLeft(name, textPos, Colors::Text, false); + + // Elements + bool isHovered2 = false; + FVector2D element_pos = pos; + + for (int num = 0; num < count; num++) + { + const char* arg = items[num]; + + // Selected element (drawn on the closed combo) + if (num == *value) + { + FVector2D _textPos = FVector2D{pos.X + size.X / 2, pos.Y + size.Y / 2}; + TextCenter(arg, _textPos, Colors::Text, false); + } + + if (checkbox_enabled[elements_count]) + { + element_pos.Y += 25.0f; + + isHovered2 = MouseInZone(FVector2D{element_pos.X, element_pos.Y}, FVector2D{size.X, 25.0f}); + if (isHovered2) + { + hover_element = true; + PostRenderer::drawFilledRect(FVector2D{element_pos.X, element_pos.Y}, size.X, 25.0f, Colors::Combobox_Hovered); + + // Click + if (Input::IsMouseClicked(0, elements_count, false)) + { + *value = num; + changed = true; + checkbox_enabled[elements_count] = false; + } + } + else + { + PostRenderer::drawFilledRect(FVector2D{element_pos.X, element_pos.Y}, size.X, 25.0f, Colors::Combobox_Idle); + } + + PostRenderer::TextLeft(arg, FVector2D{element_pos.X + 5.0f, element_pos.Y + 15.0f}, Colors::Text, false); + } + } + + sameLine = false; + last_element_pos = pos; + last_element_size = size; + if (first_element_pos.X == 0.0f) + first_element_pos = pos; + + if (isHovered && Input::IsMouseClicked(0, elements_count, false)) + { + checkbox_enabled[elements_count] = !checkbox_enabled[elements_count]; + } + if (!isHovered && !isHovered2 && Input::IsMouseClicked(0, elements_count, false)) + { + checkbox_enabled[elements_count] = false; + } + + return changed; + } + + inline int active_hotkey = -1; + inline bool already_pressed = false; + inline std::string VirtualKeyCodeToString(UCHAR virtualKey) + { + UINT scanCode = MapVirtualKey(virtualKey, MAPVK_VK_TO_VSC); + + if (virtualKey == VK_LBUTTON) return "MOUSE0"; + if (virtualKey == VK_RBUTTON) return "MOUSE1"; + if (virtualKey == VK_MBUTTON) return "MBUTTON"; + if (virtualKey == VK_XBUTTON1) return "XBUTTON1"; + if (virtualKey == VK_XBUTTON2) return "XBUTTON2"; + + CHAR szName[128]; + int result = 0; + switch (virtualKey) + { + case VK_LEFT: + case VK_UP: + case VK_RIGHT: + case VK_DOWN: + case VK_RCONTROL: + case VK_RMENU: + case VK_LWIN: + case VK_RWIN: + case VK_APPS: + case VK_PRIOR: + case VK_NEXT: + case VK_END: + case VK_HOME: + case VK_INSERT: + case VK_DELETE: + case VK_DIVIDE: + case VK_NUMLOCK: + scanCode |= KF_EXTENDED; + default: + result = GetKeyNameTextA(scanCode << 16, szName, 128); + } + + return szName; + } + inline bool Hotkey(const char* name, FVector2D size, int* key) + { + elements_count++; + bool changed = false; + + FVector2D padding = FVector2D{5, 10}; + FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; + if (sameLine) + { + pos.X = last_element_pos.X + last_element_size.X + padding.X; + pos.Y = last_element_pos.Y + (last_element_size.Y / 2) - size.Y / 2; + } + if (pushY) + { + pos.Y = pushYvalue; + pushY = false; + pushYvalue = 0.0f; + offset_y = pos.Y - menu_pos.Y; + } + bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, size); + + // Bg + drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::MainColor); + if (isHovered) hover_element = true; + + if (!sameLine) + offset_y += size.Y + padding.Y; + + FVector2D textPos = FVector2D{pos.X + size.X / 2, pos.Y + size.Y / 2}; + if (active_hotkey == elements_count) + { + TextCenter("[Press Key]", textPos, Colors::Text, false); + + if (!ZeroGUI::Input::IsAnyMouseDown()) + { + already_pressed = false; + } + + if (!already_pressed) + { + for (int code = 0; code < 255; code++) + { + if (GetAsyncKeyState(code) & 0x8000) + { + *key = code; + changed = true; + active_hotkey = -1; + } + } + } + } + else + { + TextCenter(VirtualKeyCodeToString(*key).c_str(), textPos, Colors::Text, false); + + if (isHovered) + { + if (Input::IsMouseClicked(0, elements_count, false)) + { + already_pressed = true; + active_hotkey = elements_count; + + // Queue fix: drain the currently-pressed keys so the initiating click doesn't bind + for (int code = 0; code < 255; code++) + if (GetAsyncKeyState(code)) + { + } + } + } + else + { + if (Input::IsMouseClicked(0, elements_count, false)) + { + active_hotkey = -1; + } + } + } + + sameLine = false; + last_element_pos = pos; + last_element_size = size; + if (first_element_pos.X == 0.0f) + first_element_pos = pos; + + return changed; + } + + inline int active_picker = -1; + inline FLinearColor saved_color; + inline bool ColorPixel(FVector2D pos, FVector2D size, FLinearColor* original, FLinearColor color) + { + PostRenderer::drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, color); + + // Selected swatch outline + if (original->R == color.R && original->G == color.G && original->B == color.B) + { + PostRenderer::Draw_Line(FVector2D{pos.X, pos.Y}, FVector2D{pos.X + size.X - 1, pos.Y}, 1, FLinearColor{0.0f, 0.0f, 0.0f, 1.0f}); + PostRenderer::Draw_Line(FVector2D{pos.X, pos.Y + size.Y - 1}, FVector2D{pos.X + size.X - 1, pos.Y + size.Y - 1}, 1, FLinearColor{0.0f, 0.0f, 0.0f, 1.0f}); + PostRenderer::Draw_Line(FVector2D{pos.X, pos.Y}, FVector2D{pos.X, pos.Y + size.Y - 1}, 1, FLinearColor{0.0f, 0.0f, 0.0f, 1.0f}); + PostRenderer::Draw_Line(FVector2D{pos.X + size.X - 1, pos.Y}, FVector2D{pos.X + size.X - 1, pos.Y + size.Y - 1}, 1, FLinearColor{0.0f, 0.0f, 0.0f, 1.0f}); + } + + // Change color on click + bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, size); + if (isHovered) + { + if (Input::IsMouseClicked(0, elements_count, false)) + *original = color; + } + + return true; + } + inline bool ColorPicker(const char* name, FLinearColor* color) + { + elements_count++; + + float size = 25; + FVector2D padding = FVector2D{10, 10}; + FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; + if (sameLine) + { + pos.X = last_element_pos.X + last_element_size.X + padding.X; + pos.Y = last_element_pos.Y; + } + if (pushY) + { + pos.Y = pushYvalue; + pushY = false; + pushYvalue = 0.0f; + offset_y = pos.Y - menu_pos.Y; + } + bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, FVector2D{size, size}); + + if (!sameLine) + offset_y += size + padding.Y; + + if (active_picker == elements_count) + { + hover_element = true; + + float sizePickerX = 250; + float sizePickerY = 250; + bool isHoveredPicker = MouseInZone(FVector2D{pos.X, pos.Y}, FVector2D{sizePickerX, sizePickerY - 60}); + + // Background + PostRenderer::drawFilledRect(FVector2D{pos.X, pos.Y}, sizePickerX, sizePickerY - 65, Colors::ColorPicker_Background); + + FVector2D pixelSize = FVector2D{sizePickerX / 12, sizePickerY / 12}; + + // 0 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{174 / 255.f, 235 / 255.f, 253 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{136 / 255.f, 225 / 255.f, 251 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{108 / 255.f, 213 / 255.f, 250 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{89 / 255.f, 175 / 255.f, 213 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{76 / 255.f, 151 / 255.f, 177 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{60 / 255.f, 118 / 255.f, 140 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{43 / 255.f, 85 / 255.f, 100 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{32 / 255.f, 62 / 255.f, 74 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{255 / 255.f, 255 / 255.f, 255 / 255.f, 1.0f}); + } + // 1 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{175 / 255.f, 205 / 255.f, 252 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{132 / 255.f, 179 / 255.f, 252 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{90 / 255.f, 152 / 255.f, 250 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{55 / 255.f, 120 / 255.f, 250 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{49 / 255.f, 105 / 255.f, 209 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{38 / 255.f, 83 / 255.f, 165 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{28 / 255.f, 61 / 255.f, 120 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{20 / 255.f, 43 / 255.f, 86 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{247 / 255.f, 247 / 255.f, 247 / 255.f, 1.0f}); + } + // 2 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{153 / 255.f, 139 / 255.f, 250 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{101 / 255.f, 79 / 255.f, 249 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{64 / 255.f, 50 / 255.f, 230 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{54 / 255.f, 38 / 255.f, 175 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{39 / 255.f, 31 / 255.f, 144 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{32 / 255.f, 25 / 255.f, 116 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{21 / 255.f, 18 / 255.f, 82 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{16 / 255.f, 13 / 255.f, 61 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{228 / 255.f, 228 / 255.f, 228 / 255.f, 1.0f}); + } + // 3 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{194 / 255.f, 144 / 255.f, 251 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{165 / 255.f, 87 / 255.f, 249 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{142 / 255.f, 57 / 255.f, 239 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{116 / 255.f, 45 / 255.f, 184 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{92 / 255.f, 37 / 255.f, 154 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{73 / 255.f, 29 / 255.f, 121 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{53 / 255.f, 21 / 255.f, 88 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{37 / 255.f, 15 / 255.f, 63 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{203 / 255.f, 203 / 255.f, 203 / 255.f, 1.0f}); + } + // 4 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{224 / 255.f, 162 / 255.f, 197 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{210 / 255.f, 112 / 255.f, 166 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{199 / 255.f, 62 / 255.f, 135 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{159 / 255.f, 49 / 255.f, 105 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{132 / 255.f, 41 / 255.f, 89 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{104 / 255.f, 32 / 255.f, 71 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{75 / 255.f, 24 / 255.f, 51 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{54 / 255.f, 14 / 255.f, 36 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{175 / 255.f, 175 / 255.f, 175 / 255.f, 1.0f}); + } + // 5 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{235 / 255.f, 175 / 255.f, 176 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{227 / 255.f, 133 / 255.f, 135 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{219 / 255.f, 87 / 255.f, 88 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{215 / 255.f, 50 / 255.f, 36 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{187 / 255.f, 25 / 255.f, 7 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{149 / 255.f, 20 / 255.f, 6 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{107 / 255.f, 14 / 255.f, 4 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{77 / 255.f, 9 / 255.f, 3 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{144 / 255.f, 144 / 255.f, 144 / 255.f, 1.0f}); + } + // 6 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{241 / 255.f, 187 / 255.f, 171 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{234 / 255.f, 151 / 255.f, 126 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{229 / 255.f, 115 / 255.f, 76 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{227 / 255.f, 82 / 255.f, 24 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{190 / 255.f, 61 / 255.f, 15 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{150 / 255.f, 48 / 255.f, 12 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{107 / 255.f, 34 / 255.f, 8 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{79 / 255.f, 25 / 255.f, 6 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{113 / 255.f, 113 / 255.f, 113 / 255.f, 1.0f}); + } + // 7 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{245 / 255.f, 207 / 255.f, 169 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{240 / 255.f, 183 / 255.f, 122 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{236 / 255.f, 159 / 255.f, 74 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{234 / 255.f, 146 / 255.f, 37 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{193 / 255.f, 111 / 255.f, 28 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{152 / 255.f, 89 / 255.f, 22 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{110 / 255.f, 64 / 255.f, 16 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{80 / 255.f, 47 / 255.f, 12 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{82 / 255.f, 82 / 255.f, 82 / 255.f, 1.0f}); + } + // 8 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{247 / 255.f, 218 / 255.f, 170 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{244 / 255.f, 200 / 255.f, 124 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{241 / 255.f, 182 / 255.f, 77 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{239 / 255.f, 174 / 255.f, 44 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{196 / 255.f, 137 / 255.f, 34 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{154 / 255.f, 108 / 255.f, 27 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{111 / 255.f, 77 / 255.f, 19 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{80 / 255.f, 56 / 255.f, 14 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{54 / 255.f, 54 / 255.f, 54 / 255.f, 1.0f}); + } + // 9 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{254 / 255.f, 243 / 255.f, 187 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{253 / 255.f, 237 / 255.f, 153 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{253 / 255.f, 231 / 255.f, 117 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{254 / 255.f, 232 / 255.f, 85 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{242 / 255.f, 212 / 255.f, 53 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{192 / 255.f, 169 / 255.f, 42 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{138 / 255.f, 120 / 255.f, 30 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{101 / 255.f, 87 / 255.f, 22 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{29 / 255.f, 29 / 255.f, 29 / 255.f, 1.0f}); + } + // 10 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{247 / 255.f, 243 / 255.f, 185 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{243 / 255.f, 239 / 255.f, 148 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{239 / 255.f, 232 / 255.f, 111 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{235 / 255.f, 229 / 255.f, 76 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{208 / 255.f, 200 / 255.f, 55 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{164 / 255.f, 157 / 255.f, 43 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{118 / 255.f, 114 / 255.f, 31 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{86 / 255.f, 82 / 255.f, 21 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{9 / 255.f, 9 / 255.f, 9 / 255.f, 1.0f}); + } + // 11 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{218 / 255.f, 232 / 255.f, 182 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{198 / 255.f, 221 / 255.f, 143 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{181 / 255.f, 210 / 255.f, 103 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{154 / 255.f, 186 / 255.f, 76 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{130 / 255.f, 155 / 255.f, 64 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{102 / 255.f, 121 / 255.f, 50 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{74 / 255.f, 88 / 255.f, 36 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{54 / 255.f, 64 / 255.f, 26 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{0 / 255.f, 0 / 255.f, 0 / 255.f, 1.0f}); + } + + if (isHoveredPicker) + { + if (Input::IsMouseClicked(0, elements_count, false)) + { + } + } + else + { + if (Input::IsMouseClicked(0, elements_count, false)) + { + active_picker = -1; + } + } + } + else + { + // Bg + drawFilledRect(FVector2D{pos.X, pos.Y}, size, size, Colors::MainColor); + if (isHovered) hover_element = true; + + // Color + drawFilledRect(FVector2D{pos.X + 4, pos.Y + 4}, size - 8, size - 8, *color); + + // Text + FVector2D textPos = FVector2D{pos.X + size + 5.0f, pos.Y + size / 2}; + TextLeft(name, textPos, Colors::Text, false); + + if (isHovered && Input::IsMouseClicked(0, elements_count, false)) + { + saved_color = *color; + active_picker = elements_count; + } + } + + sameLine = false; + last_element_pos = pos; + if (first_element_pos.X == 0.0f) + first_element_pos = pos; + + // Report a change when the live color differs from the value saved when the picker opened. + return active_picker != elements_count && (color->R != saved_color.R || color->G != saved_color.G || color->B != saved_color.B); + } + + /// Drain the deferred draw queue (dropdowns / swatches) so they land on top. Call at frame end. + inline void Render() + { + for (int i = 0; i < 128; i++) + { + if (PostRenderer::drawlist[i].type != -1) + { + // Filled Rect + if (PostRenderer::drawlist[i].type == 1) + { + ZeroGUI::drawFilledRect(PostRenderer::drawlist[i].pos, PostRenderer::drawlist[i].size.X, PostRenderer::drawlist[i].size.Y, PostRenderer::drawlist[i].color); + } + // TextLeft + else if (PostRenderer::drawlist[i].type == 2) + { + ZeroGUI::TextLeft(PostRenderer::drawlist[i].name, PostRenderer::drawlist[i].pos, PostRenderer::drawlist[i].color, PostRenderer::drawlist[i].outline); + } + // TextCenter + else if (PostRenderer::drawlist[i].type == 3) + { + ZeroGUI::TextCenter(PostRenderer::drawlist[i].name, PostRenderer::drawlist[i].pos, PostRenderer::drawlist[i].color, PostRenderer::drawlist[i].outline); + } + // Draw_Line + else if (PostRenderer::drawlist[i].type == 4) + { + Draw_Line(PostRenderer::drawlist[i].from, PostRenderer::drawlist[i].to, PostRenderer::drawlist[i].thickness, PostRenderer::drawlist[i].color); + } + + PostRenderer::drawlist[i].type = -1; + } + } + } +} // namespace ZeroGUI diff --git a/Internal/menu/canvas/ZeroInput.h b/Internal/menu/canvas/ZeroInput.h new file mode 100644 index 0000000..6e7a69b --- /dev/null +++ b/Internal/menu/canvas/ZeroInput.h @@ -0,0 +1,83 @@ +#pragma once + +/// @file +/// @brief Win32 mouse/keyboard polling for the Canvas (ZeroGUI) menu backend. Sampled once per +/// frame by Handle(); the widgets read the per-button state and edge-detect clicks per widget id. +/// Everything is `inline` because this header is pulled into multiple translation units. + +#include + +namespace ZeroGUI +{ + namespace Input + { + inline bool mouseDown[5]; + inline bool mouseDownAlready[256]; + + inline bool keysDown[256]; + inline bool keysDownAlready[256]; + + inline bool IsAnyMouseDown() + { + if (mouseDown[0]) return true; + if (mouseDown[1]) return true; + if (mouseDown[2]) return true; + if (mouseDown[3]) return true; + if (mouseDown[4]) return true; + + return false; + } + + /// Rising-edge (or, with @p repeat, level) detection of button @p button for widget @p element_id. + inline bool IsMouseClicked(int button, int element_id, bool repeat) + { + if (mouseDown[button]) + { + if (!mouseDownAlready[element_id]) + { + mouseDownAlready[element_id] = true; + return true; + } + if (repeat) + return true; + } + else + { + mouseDownAlready[element_id] = false; + } + return false; + } + + inline bool IsKeyPressed(int key, bool repeat) + { + if (keysDown[key]) + { + if (!keysDownAlready[key]) + { + keysDownAlready[key] = true; + return true; + } + if (repeat) + return true; + } + else + { + keysDownAlready[key] = false; + } + return false; + } + + /// Sample every mouse button and key once per frame (high bit = currently down). + inline void Handle() + { + mouseDown[0] = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0; + mouseDown[1] = (GetAsyncKeyState(VK_RBUTTON) & 0x8000) != 0; + mouseDown[2] = (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0; + mouseDown[3] = (GetAsyncKeyState(VK_XBUTTON1) & 0x8000) != 0; + mouseDown[4] = (GetAsyncKeyState(VK_XBUTTON2) & 0x8000) != 0; + + for (int i = 0; i < 256; i++) + keysDown[i] = (GetAsyncKeyState(i) & 0x8000) != 0; + } + } // namespace Input +} // namespace ZeroGUI diff --git a/Internal/menu/ui/UI.h b/Internal/menu/ui/UI.h new file mode 100644 index 0000000..9e04728 --- /dev/null +++ b/Internal/menu/ui/UI.h @@ -0,0 +1,210 @@ +#pragma once + +/// @file +/// @brief Menu::UI — the backend-neutral widget facade the tab sections call. Each function +/// dispatches on Settings.MENU.Backend: the ImGui backend forwards to the native ImGui / Custom.h +/// widgets (drawn in the Present frame), the Canvas backend forwards to the ZeroGUI widgets (drawn +/// through Render::canvas in the PostRender frame). Only one backend is active per frame and each +/// hook drives only its own backend's sections, so every call always lands in a valid context. +/// +/// Value widgets return `bool changed` (true on the change frame) so the sections keep the +/// `changed |= UI::Toggle(...); if (changed) Dispatch(SettingsChanged);` pattern across both backends. + +#include +#include +#include +#include + +#include + +#include "../../settings/Settings.h" +#include "../../scripting/Events.h" +#include "../gui/Custom.h" // ImGui::ToggleButton / HotKey / Tooltip / ... +#include "../canvas/ZeroGUI.h" + +namespace Menu +{ + namespace UI + { + /// True when the ImGui menu backend is active (the only place sections may call ImGui:: directly, + /// inside an `if (UI::IsImGui())` guard — that branch only runs in the Present/ImGui path). + inline bool IsImGui() + { + return Settings.MENU.Backend == MenuBackend::ImGui; + } + inline bool IsCanvas() + { + return Settings.MENU.Backend == MenuBackend::Canvas; + } + + /// The visible part of an ImGui label: everything before a "##id" disambiguation suffix (ImGui + /// hides it; the Canvas backend would otherwise draw it literally). Kept alive by the caller for + /// the duration of the widget call. + inline std::string Vis(const char* label) + { + const char* p = std::strstr(label, "##"); + return p ? std::string(label, static_cast(p - label)) : std::string(label); + } + + // --- Default Canvas widget sizes (px). ImGui sizes itself from its layout. --- + inline constexpr float ButtonW = 150.f, ButtonH = 25.f; + inline constexpr float SmallButtonW = 90.f, SmallButtonH = 20.f; + inline constexpr float ComboW = 150.f, ComboH = 25.f; + inline constexpr float HotKeyW = 90.f, HotKeyH = 22.f; + + /// Animated on/off toggle. @return true on the frame it flipped. + inline bool Toggle(const char* label, bool* v) + { + if (IsImGui()) return ImGui::ToggleButton(label, v); + return ZeroGUI::Checkbox(Vis(label).c_str(), v); + } + + /// Toggle that dispatches SettingsChanged (payload name=label, value=0/1) when flipped. + inline bool ToggleSetting(const char* label, bool* v) + { + if (!Toggle(label, v)) return false; + Events::Dispatch(Events::Type::SettingsChanged, Events::Payload{.value = *v ? 1.f : 0.f, .name = label}); + return true; + } + + inline bool Checkbox(const char* label, bool* v) + { + if (IsImGui()) return ImGui::Checkbox(label, v); + return ZeroGUI::Checkbox(Vis(label).c_str(), v); + } + + inline bool Button(const char* label) + { + if (IsImGui()) return ImGui::Button(label); + return ZeroGUI::Button(Vis(label).c_str(), FVector2D{ButtonW, ButtonH}); + } + + inline bool SmallButton(const char* label) + { + if (IsImGui()) return ImGui::SmallButton(label); + return ZeroGUI::Button(Vis(label).c_str(), FVector2D{SmallButtonW, SmallButtonH}); + } + + /// Clickable radio row. @return true on the frame it was clicked. + inline bool RadioButton(const char* label, bool active) + { + if (IsImGui()) return ImGui::RadioButton(label, active); + return ZeroGUI::Button(Vis(label).c_str(), FVector2D{ButtonW, ButtonH}); + } + + inline bool SliderFloat(const char* label, float* v, float min, float max, const char* fmt = "%.0f") + { + if (IsImGui()) return ImGui::SliderFloat(label, v, min, max, fmt); + const float old = *v; + ZeroGUI::SliderFloat(Vis(label).c_str(), v, min, max, fmt); + return *v != old; + } + + inline bool SliderInt(const char* label, int* v, int min, int max) + { + if (IsImGui()) return ImGui::SliderInt(label, v, min, max); + const int old = *v; + ZeroGUI::SliderInt(Vis(label).c_str(), v, min, max); + return *v != old; + } + + inline bool Combo(const char* label, int* v, const char* const* items, int count) + { + if (IsImGui()) return ImGui::Combo(label, v, items, count); + return ZeroGUI::Combobox(Vis(label).c_str(), FVector2D{ComboW, ComboH}, v, items, count); + } + + /// RGBA color editor over a settings ::Color (layout-compatible with FLinearColor). + /// @return true if the color changed this frame. + inline bool ColorEdit(const char* label, ::Color* c) + { + if (IsImGui()) return ImGui::ColorEdit4(label, &c->R); + return ZeroGUI::ColorPicker(Vis(label).c_str(), reinterpret_cast(c)); + } + + /// Rebindable hotkey row. @return true on the frame the key changed. + inline bool HotKey(const char* label, int* key) + { + if (IsImGui()) + { + const int old = *key; + ImGui::HotKey(label, key); + return *key != old; + } + return ZeroGUI::Hotkey(Vis(label).c_str(), FVector2D{HotKeyW, HotKeyH}, key); + } + + inline void SameLine() + { + if (IsImGui()) + ImGui::SameLine(); + else + ZeroGUI::SameLine(); + } + + /// Section header with a label. + inline void SeparatorText(const char* label) + { + if (IsImGui()) + ImGui::SeparatorText(label); + else + ZeroGUI::Text(Vis(label).c_str()); + } + + /// Hover tooltip on the previous widget (ImGui only; a no-op on the Canvas backend). + inline void Tooltip(const char* text) + { + if (IsImGui()) ImGui::Tooltip(text); + } + + // --- Text (printf-style). Formats once, then routes to the active backend. --- + inline void TextV(const char* fmt, va_list args) + { + char buf[512]; + vsnprintf(buf, sizeof(buf), fmt, args); + if (IsImGui()) + ImGui::TextUnformatted(buf); + else + ZeroGUI::Text(buf); + } + inline void Text(const char* fmt, ...) + { + va_list args; + va_start(args, fmt); + TextV(fmt, args); + va_end(args); + } + inline void TextDisabled(const char* fmt, ...) + { + va_list args; + va_start(args, fmt); + if (IsImGui()) + { + char buf[512]; + vsnprintf(buf, sizeof(buf), fmt, args); + ImGui::TextDisabled("%s", buf); + } + else + { + TextV(fmt, args); + } + va_end(args); + } + inline void BulletText(const char* fmt, ...) + { + va_list args; + va_start(args, fmt); + if (IsImGui()) + { + char buf[512]; + vsnprintf(buf, sizeof(buf), fmt, args); + ImGui::BulletText("%s", buf); + } + else + { + TextV(fmt, args); + } + va_end(args); + } + } // namespace UI +} // namespace Menu From 4acacd69c17154db7073a6d6daf7d84fa43d73c3 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:51:11 +0200 Subject: [PATCH 05/54] feat(menu): add the Canvas menu tick and wire both backends into the hooks Menu::Tick draws the ZeroGUI menu through Render::canvas from PostRender (Engine:: Canvas is now published before the in-game gate, so the canvas menu works at the main menu / loading too) with a left tab column + content column and its own software cursor. The Ins toggle is factored into Menu::HandleToggle, shared by Draw (ImGui) and Tick (Canvas). Present gates Menu::Draw on the ImGui backend and only paints ImGui's cursor there; game-input capture stays keyed on ShowMenu. Co-Authored-By: Claude Opus 4.8 --- Internal/hook/functions/PostRender.h | 10 ++- Internal/menu/Menu.h | 111 ++++++++++++++++++++++++--- Internal/menu/gui/Gui.h | 10 ++- 3 files changed, 117 insertions(+), 14 deletions(-) diff --git a/Internal/hook/functions/PostRender.h b/Internal/hook/functions/PostRender.h index bfc8005..c70b6a0 100644 --- a/Internal/hook/functions/PostRender.h +++ b/Internal/hook/functions/PostRender.h @@ -13,6 +13,7 @@ #include "../../cache/ActorCache.h" #include "../../render/Render.h" #include "../../features/Features.h" +#include "../../menu/Menu.h" #include "../../utils/Input.h" /// @brief Hook code for UGameViewportClient::PostRender. @@ -38,10 +39,17 @@ namespace PostRender Engine::PlayerController = PlayerController; Engine::IsInGame = PlayerController && PlayerController->IsInGame(); + // The UCanvas arg is valid every frame (main menu / loading included), so publish it before the + // in-game gate — the Canvas menu draws through it unconditionally, while features still gate on + // PlayerController below. + Engine::Canvas = Canvas; + + if (Settings.MENU.Backend == MenuBackend::Canvas) + Menu::Tick(); // ZeroGUI menu, drawn immediately through Render::canvas + if (PlayerController) { Engine::World = World; - Engine::Canvas = Canvas; // Edge-detect hotkeys once per frame → Events::HotKeyPressed (press-once actions subscribe). Input::DispatchHotKeys(); diff --git a/Internal/menu/Menu.h b/Internal/menu/Menu.h index 28040ca..520f200 100644 --- a/Internal/menu/Menu.h +++ b/Internal/menu/Menu.h @@ -7,6 +7,8 @@ #include "../scripting/Events.h" #include "../utils/Input.h" #include "../utils/Rgb.h" +#include "ui/UI.h" +#include "canvas/ZeroGUI.h" #include "sections/Misc.h" #include "sections/Exploits.h" #include "sections/Visuals.h" @@ -29,19 +31,30 @@ namespace Menu static int tab = 0; static ImGuiTabBarFlags tabFlags = ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_NoCloseWithMiddleMouseButton; - /// @brief Renders one full frame of the GUI. - /// Always draws the watermark; toggles menu visibility on the configured hotkey / gamepad Start - /// (dispatching MenuOpened/MenuClosed), and when the menu is visible draws the main window with its - /// tab bar (Misc, Exploits, Visuals, Settings, Debug) plus the optional ImGui demo/style-editor windows. - void Draw() + /// Canvas-backend window position (draggable) and active tab index. ImGui keeps its own state. + inline FVector2D canvasPos = {400.f, 200.f}; + inline int canvasTab = 0; + + /// Flip menu visibility on the show/hotkey (or @p extra, e.g. the gamepad Start button), dispatching + /// MenuOpened/MenuClosed. Factored out so both the ImGui (Present) and Canvas (PostRender) paths share + /// one toggle; the two backends are mutually exclusive per frame, so this never double-fires. + inline void HandleToggle(bool extra = false) { - // The watermark is its own Watermark feature now (drawn through the Render backend), so it - // follows the active renderer — including the streamproof external window — and isn't drawn here. - if (Input::Pressed(Settings.MENU.ShowHotkey) || ImGui::IsKeyPressed(ImGuiKey_GamepadStart)) + if (Input::Pressed(Settings.MENU.ShowHotkey) || extra) { Settings.MENU.ShowMenu = !Settings.MENU.ShowMenu; Events::Dispatch(Settings.MENU.ShowMenu ? Events::Type::MenuOpened : Events::Type::MenuClosed); } + } + + /// @brief Renders one full frame of the ImGui menu (from the Present hook, ImGui backend only). + /// Toggles menu visibility on the configured hotkey / gamepad Start, and when visible draws the main + /// window with its tab bar plus the optional ImGui demo/style-editor windows. + void Draw() + { + // The watermark is its own Watermark feature now (drawn through the Render backend), so it + // follows the active renderer — including the streamproof external window — and isn't drawn here. + HandleToggle(ImGui::IsKeyPressed(ImGuiKey_GamepadStart)); if (!Settings.MENU.ShowMenu) return; if (Settings.DEBUG.ShowDemoWindow) @@ -68,7 +81,8 @@ namespace Menu static const std::array defaults = [&] { std::array saved{}; - for (size_t i = 0; i < saved.size(); ++i) saved[i] = colors[accentSlots[i]]; + for (size_t i = 0; i < saved.size(); ++i) + saved[i] = colors[accentSlots[i]]; return saved; }(); @@ -76,11 +90,13 @@ namespace Menu { const Color rgb = Rgb::Current(); const ImVec4 accent(rgb.R, rgb.G, rgb.B, rgb.A); - for (ImGuiCol slot : accentSlots) colors[slot] = accent; + for (ImGuiCol slot : accentSlots) + colors[slot] = accent; } else { - for (size_t i = 0; i < defaults.size(); ++i) colors[accentSlots[i]] = defaults[i]; + for (size_t i = 0; i < defaults.size(); ++i) + colors[accentSlots[i]] = defaults[i]; } } @@ -159,4 +175,77 @@ namespace Menu ImGui::EndTabBar(); ImGui::End(); }; + + /// @brief Renders one full frame of the Canvas (ZeroGUI) menu, drawn through Render::canvas from the + /// PostRender hook (Canvas backend only). Works everywhere the UE canvas is valid — in a match and at + /// the main menu / loading. Owns its own software cursor, so it never depends on ImGui. + void Tick() + { + ZeroGUI::Input::Handle(); // sample mouse/keyboard once per frame + + HandleToggle(); + if (!Settings.MENU.ShowMenu) return; + + // RGB accent: retint the red accent group from the rainbow when enabled, else the theme red. + { + const FLinearColor accent = Settings.MENU.Rgb + ? [] + { const Color c = Rgb::Current(); return FLinearColor{c.R, c.G, c.B, c.A}; }() + : FLinearColor{1.f, 0.f, 0.f, 1.f}; + ZeroGUI::Colors::MainColor = accent; + ZeroGUI::Colors::Window_Header = accent; + ZeroGUI::Colors::Button_Idle = accent; + ZeroGUI::Colors::Button_Hovered = accent; + ZeroGUI::Colors::Button_Active = accent; + ZeroGUI::Colors::Slider_Progress = accent; + } + + const FVector2D winSize{700.f, 500.f}; + if (!ZeroGUI::Window("Splitgate Internal", &canvasPos, winSize, true)) return; + + // Left column: one tab button per section (stacks under the header). + static const char* const tabs[] = {"Misc", "Exploits", "Visuals", "Aim", "Network", "Config", "Scripts", "SDK", "Discord", "Debug"}; + for (int i = 0; i < IM_ARRAYSIZE(tabs); i++) + if (ZeroGUI::ButtonTab(tabs[i], FVector2D{112.f, 30.f}, canvasTab == i)) + canvasTab = i; + + // Content column: only the active tab's section (so inactive sections don't draw over it). + ZeroGUI::NextColumn(130.f); + switch (canvasTab) + { + case 0: + Sections::MiscTab(); + break; + case 1: + Sections::ExploitsTab(); + break; + case 2: + Sections::VisualsTab(); + break; + case 3: + Sections::AimTab(); + break; + case 4: + Sections::NetworkTab(); + break; + case 5: + Sections::ConfigTab(); + break; + case 6: + Sections::ScriptsTab(); + break; + case 7: + Sections::SdkTab(); + break; + case 8: + Sections::DiscordTab(); + break; + case 9: + Sections::DebugTab(); + break; + } + + ZeroGUI::Render(); // drain deferred pop-ups (combo dropdowns, color swatches) on top + ZeroGUI::Draw_Cursor(true); // the Canvas menu's own cursor + }; }; // namespace Menu \ No newline at end of file diff --git a/Internal/menu/gui/Gui.h b/Internal/menu/gui/Gui.h index c93b855..1a3394f 100644 --- a/Internal/menu/gui/Gui.h +++ b/Internal/menu/gui/Gui.h @@ -150,14 +150,20 @@ namespace GUI ImGui::SetNextWindowPos(ImVec2(mainViewport->WorkPos.x + 550, mainViewport->WorkPos.y + 20), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(550, 350), ImGuiCond_FirstUseEver); + // Only the ImGui menu backend draws here (and paints ImGui's software cursor). The Canvas menu + // draws in PostRender and owns its own cursor, so ImGui must not also show one. Game-input + // capture stays keyed on ShowMenu alone (backend-agnostic) so input is blocked whenever the + // menu is open, regardless of which backend is drawing it. + const bool imguiMenu = Settings.MENU.Backend == MenuBackend::ImGui; + ImGuiIO& io = ImGui::GetIO(); (void)io; - io.MouseDrawCursor = Settings.MENU.ShowMenu; + io.MouseDrawCursor = Settings.MENU.ShowMenu && imguiMenu; io.WantCaptureMouse = Settings.MENU.ShowMenu; io.WantTextInput = Settings.MENU.ShowMenu; io.WantCaptureKeyboard = Settings.MENU.ShowMenu; - Menu::Draw(); + if (imguiMenu) Menu::Draw(); ImGui::EndFrame(); ImGui::Render(); From 6391ba205a9d7f829b288b611fbe4f9f9cf2ec54 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:51:18 +0200 Subject: [PATCH 06/54] refactor(menu): drive the sections through Menu::UI, degrade hard tabs on Canvas All 10 tab sections now call Menu::UI::* instead of ImGui:: directly, so the same section tree renders under either backend. The Visuals tab hosts the new Menu- backend combo. ImGui-only idioms (searchable map/class/skin combos in Misc, the SDK explorer, the Network redirect editor, Config profiles/share-codes, the Scripts/Debug child lists) stay behind if (UI::IsImGui()) guards and show a 'use the ImGui menu backend' note in Canvas mode. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/sections/Aim.h | 55 ++-- Internal/menu/sections/Config.h | 44 +-- Internal/menu/sections/Debug.h | 48 +-- Internal/menu/sections/Discord.h | 32 +- Internal/menu/sections/Exploits.h | 68 ++-- Internal/menu/sections/Misc.h | 509 +++++++++++++++--------------- Internal/menu/sections/Network.h | 17 +- Internal/menu/sections/Scripts.h | 27 +- Internal/menu/sections/Sdk.h | 25 +- Internal/menu/sections/Visuals.h | 167 +++++----- 10 files changed, 536 insertions(+), 456 deletions(-) diff --git a/Internal/menu/sections/Aim.h b/Internal/menu/sections/Aim.h index dea3e14..04a601e 100644 --- a/Internal/menu/sections/Aim.h +++ b/Internal/menu/sections/Aim.h @@ -5,6 +5,7 @@ #include "../../settings/Settings.h" #include "../../scripting/Events.h" +#include "../ui/UI.h" namespace Menu { @@ -16,40 +17,40 @@ namespace Menu bool changed = false; auto& a = Settings.AIM; - ImGui::SeparatorText("Aimbot"); - changed |= ImGui::ToggleButton("Enable##aim", &a.Aimbot); - ImGui::HotKey("Aim key", &a.AimKey); - changed |= ImGui::SliderFloat("FOV (px)", &a.AimFov, 10.f, 500.f, "%.0f"); - changed |= ImGui::SliderFloat("Smoothing", &a.AimSmooth, 0.05f, 1.f, "%.2f"); - ImGui::Tooltip("1.0 snaps instantly; lower is smoother."); + UI::SeparatorText("Aimbot"); + changed |= UI::Toggle("Enable##aim", &a.Aimbot); + UI::HotKey("Aim key", &a.AimKey); + changed |= UI::SliderFloat("FOV (px)", &a.AimFov, 10.f, 500.f, "%.0f"); + changed |= UI::SliderFloat("Smoothing", &a.AimSmooth, 0.05f, 1.f, "%.2f"); + UI::Tooltip("1.0 snaps instantly; lower is smoother."); const char* bones[] = {"Head", "Chest", "Pelvis"}; - changed |= ImGui::Combo("Bone", &a.AimBone, bones, IM_ARRAYSIZE(bones)); - changed |= ImGui::ToggleButton("Team check##aim", &a.AimTeamCheck); - changed |= ImGui::ToggleButton("Ignore bots", &a.IgnoreBots); - ImGui::Tooltip("Aimbot and triggerbot target only real players, never AI bots."); - changed |= ImGui::ToggleButton("Silent aim", &a.SilentAim); - ImGui::Tooltip("Snap to the target only while firing (left click), ignoring the aim key."); - changed |= ImGui::ToggleButton("Visibility check", &a.AimVisibleCheck); - ImGui::Tooltip("Only lock onto targets that were recently rendered (visible). Also applies to the triggerbot."); + changed |= UI::Combo("Bone", &a.AimBone, bones, IM_ARRAYSIZE(bones)); + changed |= UI::Toggle("Team check##aim", &a.AimTeamCheck); + changed |= UI::Toggle("Ignore bots", &a.IgnoreBots); + UI::Tooltip("Aimbot and triggerbot target only real players, never AI bots."); + changed |= UI::Toggle("Silent aim", &a.SilentAim); + UI::Tooltip("Snap to the target only while firing (left click), ignoring the aim key."); + changed |= UI::Toggle("Visibility check", &a.AimVisibleCheck); + UI::Tooltip("Only lock onto targets that were recently rendered (visible). Also applies to the triggerbot."); if (a.AimVisibleCheck) { - changed |= ImGui::ToggleButton("Per-bone visibility", &a.AimVisiblePerBone); - ImGui::Tooltip("Stricter: line-trace each bone and aim at the first one in line of sight\n(e.g. skip the head when only the legs are exposed). Skips targets with no visible bone."); + changed |= UI::Toggle("Per-bone visibility", &a.AimVisiblePerBone); + UI::Tooltip("Stricter: line-trace each bone and aim at the first one in line of sight\n(e.g. skip the head when only the legs are exposed). Skips targets with no visible bone."); } - changed |= ImGui::ToggleButton("Aim assist", &a.AimAssist); - ImGui::Tooltip("Amplify the weapon's built-in aim-assist/magnetism (soft aim, view isn't moved).\nMay only take effect on controller input - verify in-game."); + changed |= UI::Toggle("Aim assist", &a.AimAssist); + UI::Tooltip("Amplify the weapon's built-in aim-assist/magnetism (soft aim, view isn't moved).\nMay only take effect on controller input - verify in-game."); if (a.AimAssist) - changed |= ImGui::SliderFloat("Aim assist strength", &a.AimAssistStrength, 1.f, 8.f, "%.1fx"); + changed |= UI::SliderFloat("Aim assist strength", &a.AimAssistStrength, 1.f, 8.f, "%.1fx"); - changed |= ImGui::ToggleButton("Draw FOV circle", &a.DrawAimFov); - ImGui::ColorEdit4("FOV circle color", &a.AimFovColor.R); + changed |= UI::Toggle("Draw FOV circle", &a.DrawAimFov); + UI::ColorEdit("FOV circle color", &a.AimFovColor); - ImGui::SeparatorText("Triggerbot"); - changed |= ImGui::ToggleButton("Enable##trig", &a.Triggerbot); - ImGui::HotKey("Trigger key", &a.TriggerKey); - changed |= ImGui::SliderFloat("Trigger FOV (px)", &a.TriggerFov, 1.f, 30.f, "%.0f"); - changed |= ImGui::SliderInt("Delay (ms)", &a.TriggerDelay, 0, 500); - changed |= ImGui::ToggleButton("Team check##trig", &a.TriggerTeamCheck); + UI::SeparatorText("Triggerbot"); + changed |= UI::Toggle("Enable##trig", &a.Triggerbot); + UI::HotKey("Trigger key", &a.TriggerKey); + changed |= UI::SliderFloat("Trigger FOV (px)", &a.TriggerFov, 1.f, 30.f, "%.0f"); + changed |= UI::SliderInt("Delay (ms)", &a.TriggerDelay, 0, 500); + changed |= UI::Toggle("Team check##trig", &a.TriggerTeamCheck); if (changed) Events::Dispatch(Events::Type::SettingsChanged); } diff --git a/Internal/menu/sections/Config.h b/Internal/menu/sections/Config.h index 547e9cd..6a572c0 100644 --- a/Internal/menu/sections/Config.h +++ b/Internal/menu/sections/Config.h @@ -8,6 +8,7 @@ #include "../../settings/Profiles.h" #include "../../../shared/Utilities.h" +#include "../ui/UI.h" namespace Menu { @@ -16,39 +17,39 @@ namespace Menu /// @brief Renders the Config tab. void ConfigTab() { - ImGui::SeparatorText("Config"); - if (ImGui::Button("Save")) SettingsHelper::File().Save(); - ImGui::SameLine(); - if (ImGui::Button("Reload")) + UI::SeparatorText("Config"); + if (UI::Button("Save")) SettingsHelper::File().Save(); + UI::SameLine(); + if (UI::Button("Reload")) { SettingsHelper::File().Load(); Events::Dispatch(Events::Type::SettingsChanged); } - ImGui::SameLine(); - if (ImGui::Button("Reset defaults")) + UI::SameLine(); + if (UI::Button("Reset defaults")) { SettingsHelper::File().Reset(); Events::Dispatch(Events::Type::SettingsChanged); } - ImGui::SameLine(); - if (ImGui::Button("Open folder")) + UI::SameLine(); + if (UI::Button("Open folder")) Shared::Utilities::OpenFolder(SettingsHelper::File().Path().parent_path()); - ImGui::Tooltip("Open the folder holding the settings file."); + UI::Tooltip("Open the folder holding the settings file."); - if (ImGui::ToggleButton("Autosave on change", &Settings.MISC.AutoSave)) + if (UI::Toggle("Autosave on change", &Settings.MISC.AutoSave)) Events::Dispatch(Events::Type::SettingsChanged); - if (ImGui::ToggleButton("Delete config on crash", &Settings.DEBUG.DeleteSettingsOnCrash)) + if (UI::Toggle("Delete config on crash", &Settings.DEBUG.DeleteSettingsOnCrash)) Events::Dispatch(Events::Type::SettingsChanged); - ImGui::SeparatorText("Menu"); - ImGui::HotKey("Open Menu", &Settings.MENU.ShowHotkey); - ImGui::ToggleButton("Watermark", &Settings.MENU.ShowWatermark); - ImGui::ToggleButton("RGB", &Settings.MENU.Rgb); - ImGui::Tooltip("Cycle the watermark, menu accent, and radar self-icon through a rainbow. Off = the defaults (red / white)."); + UI::SeparatorText("Menu"); + UI::HotKey("Open Menu", &Settings.MENU.ShowHotkey); + UI::Toggle("Watermark", &Settings.MENU.ShowWatermark); + UI::Toggle("RGB", &Settings.MENU.Rgb); + UI::Tooltip("Cycle the watermark, menu accent, and radar self-icon through a rainbow. Off = the defaults (red / white)."); // Restore every hotkey (menu, no-clip, aim, trigger) to its struct default, leaving the // rest of the config untouched. - if (ImGui::Button("Reset keybinds")) + if (UI::Button("Reset keybinds")) { Settings.MENU.ShowHotkey = MenuSettings{}.ShowHotkey; Settings.EXPLOITS.NoClip = ExploitsSettings{}.NoClip; @@ -56,7 +57,14 @@ namespace Menu Settings.AIM.TriggerKey = AimSettings{}.TriggerKey; Events::Dispatch(Events::Type::SettingsChanged); } - ImGui::Tooltip("Reset all hotkeys (menu, no-clip, aim, trigger) to their defaults."); + UI::Tooltip("Reset all hotkeys (menu, no-clip, aim, trigger) to their defaults."); + + // Named profiles and share codes use InputText / multiline / clipboard — ImGui-only. + if (!UI::IsImGui()) + { + UI::Text("Profiles and share codes use the ImGui menu backend."); + return; + } ImGui::SeparatorText("Save current config"); static char nameBuffer[64] = ""; diff --git a/Internal/menu/sections/Debug.h b/Internal/menu/sections/Debug.h index d680a29..8af3232 100644 --- a/Internal/menu/sections/Debug.h +++ b/Internal/menu/sections/Debug.h @@ -7,6 +7,7 @@ #include "../../scripting/Events.h" #include "../../hook/Hook.h" #include "../../../shared/Utilities.h" +#include "../ui/UI.h" namespace Menu { @@ -22,33 +23,41 @@ namespace Menu { bool changed = false; - ImGui::SeparatorText("Logging"); - changed |= ImGui::ToggleButton("Log ProcessEvent", &Settings.DEBUG.LogProcessEvent); - changed |= ImGui::ToggleButton("Features Logging", &Settings.DEBUG.FeaturesLogging); + UI::SeparatorText("Logging"); + changed |= UI::Toggle("Log ProcessEvent", &Settings.DEBUG.LogProcessEvent); + changed |= UI::Toggle("Features Logging", &Settings.DEBUG.FeaturesLogging); - ImGui::SeparatorText("GUI"); - changed |= ImGui::ToggleButton("Show demo window", &Settings.DEBUG.ShowDemoWindow); - changed |= ImGui::ToggleButton("Show style editor", &Settings.DEBUG.ShowStyleEditor); + UI::SeparatorText("GUI"); + changed |= UI::Toggle("Show demo window", &Settings.DEBUG.ShowDemoWindow); + changed |= UI::Toggle("Show style editor", &Settings.DEBUG.ShowStyleEditor); - ImGui::SeparatorText("Performance"); - changed |= ImGui::ToggleButton("Native WorldToScreen", &Settings.DEBUG.NativeWorldToScreen); - ImGui::Tooltip("Project overlays with math instead of the game's ProjectWorldLocationToScreen UFunction. Turn off if boxes/names are misplaced."); + UI::SeparatorText("Performance"); + changed |= UI::Toggle("Native WorldToScreen", &Settings.DEBUG.NativeWorldToScreen); + UI::Tooltip("Project overlays with math instead of the game's ProjectWorldLocationToScreen UFunction. Turn off if boxes/names are misplaced."); if (!Settings.DEBUG.NativeWorldToScreen) { - changed |= ImGui::ToggleButton("Custom projection", &Settings.DEBUG.CustomProjection); - ImGui::Tooltip("With native off: use PortalWars' ProjectWorldLocationToScreenCustom instead of the stock UFunction."); + changed |= UI::Toggle("Custom projection", &Settings.DEBUG.CustomProjection); + UI::Tooltip("With native off: use PortalWars' ProjectWorldLocationToScreenCustom instead of the stock UFunction."); } - changed |= ImGui::ToggleButton("Native bones", &Settings.DEBUG.NativeBones); - ImGui::Tooltip("Project the ESP skeleton via native GetBoneMatrix + WorldToScreen. Off falls back to the game's bone projection."); - changed |= ImGui::ToggleButton("Native actor location", &Settings.DEBUG.NativeActorLocation); - ImGui::Tooltip("Read actor location from RootComponent->RelativeLocation (no ProcessEvent). Off uses K2_GetActorLocation."); + changed |= UI::Toggle("Native bones", &Settings.DEBUG.NativeBones); + UI::Tooltip("Project the ESP skeleton via native GetBoneMatrix + WorldToScreen. Off falls back to the game's bone projection."); + changed |= UI::Toggle("Native actor location", &Settings.DEBUG.NativeActorLocation); + UI::Tooltip("Read actor location from RootComponent->RelativeLocation (no ProcessEvent). Off uses K2_GetActorLocation."); if (changed) Events::Dispatch(Events::Type::SettingsChanged); - ImGui::SeparatorText("Files"); - if (ImGui::Button("Open app folder")) + UI::SeparatorText("Files"); + if (UI::Button("Open app folder")) Shared::Utilities::OpenFolder(Shared::AppDataPath(SettingsHelper::AppFolder)); - ImGui::Tooltip("Open the SplitgateInternal data folder (settings, logs, dumps)."); + UI::Tooltip("Open the SplitgateInternal data folder (settings, logs, dumps)."); + + // The console-command input, feature tree and log child region use ImGui InputText / + // TreeNode / child regions — ImGui-only. The Canvas backend shows a note. + if (!UI::IsImGui()) + { + UI::Text("Console command, feature tree and logs use the ImGui menu backend."); + return; + } ImGui::SeparatorText("Console command"); static char consoleBuffer[256] = ""; @@ -76,7 +85,8 @@ namespace Menu if (ImGui::Button("Copy##logs")) { std::string out; - for (const auto& line : Logger::Recent()) out += line + "\n"; + for (const auto& line : Logger::Recent()) + out += line + "\n"; ImGui::SetClipboardText(out.c_str()); } ImGui::Tooltip("Copy the recent log lines to the clipboard."); diff --git a/Internal/menu/sections/Discord.h b/Internal/menu/sections/Discord.h index 4d86dfd..efea45c 100644 --- a/Internal/menu/sections/Discord.h +++ b/Internal/menu/sections/Discord.h @@ -8,6 +8,7 @@ #include "../../settings/Settings.h" #include "../../scripting/Events.h" #include "../../discord/rpc.h" +#include "../ui/UI.h" namespace Menu { @@ -16,9 +17,9 @@ namespace Menu /// @brief Renders the Discord tab. void DiscordTab() { - ImGui::SeparatorText("Rich Presence"); + UI::SeparatorText("Rich Presence"); - if (ImGui::ToggleButton("Enable", &Settings.MISC.DiscordRPCEnabled)) + if (UI::Toggle("Enable", &Settings.MISC.DiscordRPCEnabled)) { Events::Dispatch(Events::Type::SettingsChanged); // features (incl. DiscordPresence) refresh if (Settings.MISC.DiscordRPCEnabled) @@ -26,23 +27,26 @@ namespace Menu else Discord_ClearPresence(); // hide it from your profile while off } - ImGui::Tooltip("Show a Rich Presence on your Discord profile. Updates with the live game state\n(map + K/D) every few seconds."); + UI::Tooltip("Show a Rich Presence on your Discord profile. Updates with the live game state\n(map + K/D) every few seconds."); - if (!Settings.MISC.DiscordRPCEnabled) ImGui::BeginDisabled(); + // BeginDisabled/EndDisabled greys out the block in ImGui; the Canvas backend has no equivalent, + // so it's only applied under the ImGui backend. + const bool disabled = !Settings.MISC.DiscordRPCEnabled; + if (UI::IsImGui() && disabled) ImGui::BeginDisabled(); - ImGui::SeparatorText("Live"); + UI::SeparatorText("Live"); const char* state = DiscordRPC::GetState(); - ImGui::Text("State: %s", (state && state[0]) ? state : "(none)"); - ImGui::Text("Details: %s", Settings.MENU.Watermark.c_str()); - if (ImGui::Button("Refresh now")) DiscordRPC::UpdateGameState(); - ImGui::Tooltip("Push the current map + K/D to Discord immediately (also happens automatically ~every 5s)."); + UI::Text("State: %s", (state && state[0]) ? state : "(none)"); + UI::Text("Details: %s", Settings.MENU.Watermark.c_str()); + if (UI::Button("Refresh now")) DiscordRPC::UpdateGameState(); + UI::Tooltip("Push the current map + K/D to Discord immediately (also happens automatically ~every 5s)."); - ImGui::SeparatorText("Info"); - ImGui::Text("App ID: %s", Settings.MISC.DiscordAppID.c_str()); - ImGui::Text("Image: %s", "icon"); - ImGui::TextDisabled("App ID / Steam app id are runtime-only and set at startup."); + UI::SeparatorText("Info"); + UI::Text("App ID: %s", Settings.MISC.DiscordAppID.c_str()); + UI::Text("Image: %s", "icon"); + UI::TextDisabled("App ID / Steam app id are runtime-only and set at startup."); - if (!Settings.MISC.DiscordRPCEnabled) ImGui::EndDisabled(); + if (UI::IsImGui() && disabled) ImGui::EndDisabled(); } } // namespace Sections } // namespace Menu diff --git a/Internal/menu/sections/Exploits.h b/Internal/menu/sections/Exploits.h index 3e00e99..5533ac5 100644 --- a/Internal/menu/sections/Exploits.h +++ b/Internal/menu/sections/Exploits.h @@ -5,69 +5,69 @@ #include "../../settings/Settings.h" #include "../../scripting/Events.h" -#include "../Widgets.h" +#include "../ui/UI.h" namespace Menu { namespace Sections { /// @brief Renders the Exploits tab. Each toggle dispatches SettingsChanged tagged with its - /// own label (via ToggleSetting), plus a No Clip hotkey binding. + /// own label (via UI::ToggleSetting), plus a No Clip hotkey binding. void ExploitsTab() { - ImGui::SeparatorText("Misc"); - ToggleSetting("God Mode", &Settings.EXPLOITS.GodMode); - ToggleSetting("Spin Bot", &Settings.EXPLOITS.SpinBot); - ToggleSetting("Enable all input", &Settings.EXPLOITS.EnableAllInput); - ImGui::Tooltip("Forces UI input actions enabled (e.g. a greyed-out Play button)"); + UI::SeparatorText("Misc"); + UI::ToggleSetting("God Mode", &Settings.EXPLOITS.GodMode); + UI::ToggleSetting("Spin Bot", &Settings.EXPLOITS.SpinBot); + UI::ToggleSetting("Enable all input", &Settings.EXPLOITS.EnableAllInput); + UI::Tooltip("Forces UI input actions enabled (e.g. a greyed-out Play button)"); - ImGui::SeparatorText("Camera"); + UI::SeparatorText("Camera"); const char* cameras[] = {"First person", "Third person", "Free cam"}; int camera = static_cast(Settings.EXPLOITS.Camera); - if (ImGui::Combo("Camera", &camera, cameras, IM_ARRAYSIZE(cameras))) + if (UI::Combo("Camera", &camera, cameras, IM_ARRAYSIZE(cameras))) { Settings.EXPLOITS.Camera = static_cast(camera); Events::Dispatch(Events::Type::SettingsChanged); } - ImGui::Tooltip("First person (default), a custom over-the-shoulder third person, or the game's debug free-fly cam."); + UI::Tooltip("First person (default), a custom over-the-shoulder third person, or the game's debug free-fly cam."); if (Settings.EXPLOITS.Camera == CameraMode::ThirdPerson) { - ImGui::SliderFloat("TP distance", &Settings.EXPLOITS.ThirdPersonDistance, 50.f, 600.f, "%.0f"); - ImGui::SliderFloat("TP height", &Settings.EXPLOITS.ThirdPersonHeight, 0.f, 200.f, "%.0f"); + UI::SliderFloat("TP distance", &Settings.EXPLOITS.ThirdPersonDistance, 50.f, 600.f, "%.0f"); + UI::SliderFloat("TP height", &Settings.EXPLOITS.ThirdPersonHeight, 0.f, 200.f, "%.0f"); } - ImGui::SeparatorText("Movement"); - ImGui::HotKey("No Clip", &Settings.EXPLOITS.NoClip); - ToggleSetting("Infinite Jetpack", &Settings.EXPLOITS.InfinteJetpack); + UI::SeparatorText("Movement"); + UI::HotKey("No Clip", &Settings.EXPLOITS.NoClip); + UI::ToggleSetting("Infinite Jetpack", &Settings.EXPLOITS.InfinteJetpack); - ToggleSetting("Super Jump", &Settings.EXPLOITS.SuperJump); + UI::ToggleSetting("Super Jump", &Settings.EXPLOITS.SuperJump); if (Settings.EXPLOITS.SuperJump) { - ImGui::HotKey("Super Jump key", &Settings.EXPLOITS.SuperJumpKey); - ImGui::SliderFloat("Jump force", &Settings.EXPLOITS.SuperJumpForce, 500.f, 5000.f, "%.0f"); + UI::HotKey("Super Jump key", &Settings.EXPLOITS.SuperJumpKey); + UI::SliderFloat("Jump force", &Settings.EXPLOITS.SuperJumpForce, 500.f, 5000.f, "%.0f"); } - ToggleSetting("Teleport", &Settings.EXPLOITS.Teleport); - ImGui::Tooltip("Teleport forward, toward where you're looking, on the hotkey."); + UI::ToggleSetting("Teleport", &Settings.EXPLOITS.Teleport); + UI::Tooltip("Teleport forward, toward where you're looking, on the hotkey."); if (Settings.EXPLOITS.Teleport) { - ImGui::HotKey("Teleport key", &Settings.EXPLOITS.TeleportKey); - ImGui::SliderFloat("Teleport distance", &Settings.EXPLOITS.TeleportDistance, 200.f, 5000.f, "%.0f"); + UI::HotKey("Teleport key", &Settings.EXPLOITS.TeleportKey); + UI::SliderFloat("Teleport distance", &Settings.EXPLOITS.TeleportDistance, 200.f, 5000.f, "%.0f"); } - ImGui::SeparatorText("Weapon"); - ToggleSetting("God Melee", &Settings.EXPLOITS.GodMelee); - ToggleSetting("No Recoil", &Settings.EXPLOITS.NoRecoil); - ToggleSetting("Infinite Ammo", &Settings.EXPLOITS.InfiniteAmmo); - ToggleSetting("No Reload", &Settings.EXPLOITS.NoReload); - ToggleSetting("Phasing bullets", &Settings.EXPLOITS.PhasingBullets); - ImGui::Tooltip("Disable collision on cover (CullableActor) so shots pass through it.\nLeaves the floor intact."); - ToggleSetting("Bullet TP", &Settings.EXPLOITS.BulletTp); - ImGui::Tooltip("Teleport your own projectiles onto the aim bone of the enemy nearest the crosshair.\nUses the Aim tab's Bone selection and Team-check / Ignore-bots filters."); - ToggleSetting("Bullet speed", &Settings.EXPLOITS.BulletSpeed); - ImGui::Tooltip("Push your own projectiles further along their velocity each frame (faster bullets)."); + UI::SeparatorText("Weapon"); + UI::ToggleSetting("God Melee", &Settings.EXPLOITS.GodMelee); + UI::ToggleSetting("No Recoil", &Settings.EXPLOITS.NoRecoil); + UI::ToggleSetting("Infinite Ammo", &Settings.EXPLOITS.InfiniteAmmo); + UI::ToggleSetting("No Reload", &Settings.EXPLOITS.NoReload); + UI::ToggleSetting("Phasing bullets", &Settings.EXPLOITS.PhasingBullets); + UI::Tooltip("Disable collision on cover (CullableActor) so shots pass through it.\nLeaves the floor intact."); + UI::ToggleSetting("Bullet TP", &Settings.EXPLOITS.BulletTp); + UI::Tooltip("Teleport your own projectiles onto the aim bone of the enemy nearest the crosshair.\nUses the Aim tab's Bone selection and Team-check / Ignore-bots filters."); + UI::ToggleSetting("Bullet speed", &Settings.EXPLOITS.BulletSpeed); + UI::Tooltip("Push your own projectiles further along their velocity each frame (faster bullets)."); if (Settings.EXPLOITS.BulletSpeed) - ImGui::SliderFloat("Bullet boost", &Settings.EXPLOITS.BulletSpeedBoost, 100.f, 3000.f, "%.0f"); + UI::SliderFloat("Bullet boost", &Settings.EXPLOITS.BulletSpeedBoost, 100.f, 3000.f, "%.0f"); } } // namespace Sections } // namespace Menu diff --git a/Internal/menu/sections/Misc.h b/Internal/menu/sections/Misc.h index 4822933..d7e57b4 100644 --- a/Internal/menu/sections/Misc.h +++ b/Internal/menu/sections/Misc.h @@ -11,6 +11,7 @@ #include "../../scripting/Events.h" #include "../../cache/ClassCache.h" // shared class list for the spawn picker #include "../../utils/Logger.h" // Logger::SetConsoleVisibility +#include "../ui/UI.h" // Forward-declared instead of including hook/Hook.h: that header transitively includes this menu // (via Features -> GUI -> Menu), so including it here would be circular. The inline definition in @@ -36,315 +37,329 @@ namespace Menu // can't catch. Everything below already gates on isInGame. bool isInGame = Engine::IsInGame; - ImGui::SeparatorText("Player"); + UI::SeparatorText("Player"); - ImGui::SetNextItemWidth(180.f); - ImGui::SliderFloat("##fov", &Settings.EXPLOITS.FOV, 80.0f, 160.0f, "FOV %.0f"); - ImGui::SameLine(); - if (ImGui::SmallButton("Reset##fov")) + if (UI::IsImGui()) ImGui::SetNextItemWidth(180.f); + UI::SliderFloat("##fov", &Settings.EXPLOITS.FOV, 80.0f, 160.0f, "FOV %.0f"); + UI::SameLine(); + if (UI::SmallButton("Reset##fov")) { Settings.EXPLOITS.FOV = ExploitsSettings{}.FOV; Events::Dispatch(Events::Type::SettingsChanged); } - if (!isInGame) ImGui::BeginDisabled(); - ImGui::SetNextItemWidth(180.f); - ImGui::SliderFloat("##speed", &Settings.EXPLOITS.PlayerSpeed, 0.2f, 4.f, "Speed %.2f"); - ImGui::SameLine(); - if (ImGui::SmallButton("Reset##speed")) + if (UI::IsImGui() && !isInGame) ImGui::BeginDisabled(); + if (UI::IsImGui()) ImGui::SetNextItemWidth(180.f); + UI::SliderFloat("##speed", &Settings.EXPLOITS.PlayerSpeed, 0.2f, 4.f, "Speed %.2f"); + UI::SameLine(); + if (UI::SmallButton("Reset##speed")) { Settings.EXPLOITS.PlayerSpeed = ExploitsSettings{}.PlayerSpeed; Events::Dispatch(Events::Type::SettingsChanged); } - if (!isInGame) ImGui::EndDisabled(); + if (UI::IsImGui() && !isInGame) ImGui::EndDisabled(); - ImGui::SeparatorText("Game"); + UI::SeparatorText("Game"); - // "Load into map" is usable whenever you're out of a game (e.g. back in the menu after a - // match), disabled only while already in one. The dropdown beside it picks the target level - // SwitchLevel travels to. These are the game's Content/Maps package names (leaf, no path or - // .BuiltData); Simulation_Alpha (the firing range) is index 0 and the default. The selection - // rides in on the event payload. Enumerated from the FName pool via the SDK tab. - static const char* const levels[] = { - // Simulation / firing-range maps - "Simulation_Alpha", // default - "Simulation_Bravo", - "Simulation_Charlie", - "Simulation_Delta", - "Simulation_Echo", - "Simulation_Foxtrot", - "Simulation_Golf", - "Simulation_Hotel", - "Simulation_India", - "Simulation_Juliet", - // Arena maps - "Abyss", - "Atlantis", - "Crag", - "Foregone_Destruction", - "Helix", - "Highwind", - "Impact", - "Karman_Station", - "Lavawell", - "Oasis", - "Olympus", - "Pantheon", - "Silo", - "Stadium", - // Special / system maps - "MainMenu", - "Lobby", - "Tutorial", - "PracticeRange", - "TravelMap", - "Forge_Island", - "Forge_Flat_Earth", - "Abyss_Cinematics", - // Blockout / work-in-progress maps - "Maya_Blockout", - "Noboru_Temple_Blockout", - "Decay_Blockout_WIP", - "Drift_Blockout_WIP", - "Titan_Blockout_WIP", - "Toxic_Blockout_Wip", - "Vessel_Blockout_WIP", - "Vintage_Blockout_WIP", - }; - // Searchable dropdown (same pattern as the spawn picker): filter the static list, click a - // row to select. selectedLevel points into the static array above, so it stays valid to hand - // to the event payload. - static std::vector levelFiltered; - static char levelSearch[128] = ""; - static std::string levelLastKey = "\x01"; // sentinel: forces the first filter build - static const char* selectedLevel = levels[0]; - - if (isInGame) ImGui::BeginDisabled(); - ImGui::SetNextItemWidth(220.f); - if (ImGui::BeginCombo("##level", selectedLevel)) + // Load-into-map, the spawn picker and the cosmetics pickers are all searchable dropdowns + // (BeginCombo + InputText + clipped Selectable list) — ImGui-only. In the Canvas backend the + // tab shows a short note instead. + if (!UI::IsImGui()) { - ImGui::SetNextItemWidth(-1.f); - ImGui::InputTextWithHint("##levelsearch", "filter maps...", levelSearch, sizeof(levelSearch)); - - // Rebuild the filtered index list only when the search text changes. - if (levelSearch != levelLastKey) - { - levelLastKey = levelSearch; - levelFiltered.clear(); - const std::string needle = levelSearch; - for (int i = 0; i < IM_ARRAYSIZE(levels); i++) - if (needle.empty() || std::string(levels[i]).find(needle) != std::string::npos) - levelFiltered.push_back(i); - } - - ImGui::BeginChild("##levellist", ImVec2(240, 200)); - ImGuiListClipper clipper; - clipper.Begin(static_cast(levelFiltered.size())); - while (clipper.Step()) - for (int r = clipper.DisplayStart; r < clipper.DisplayEnd; r++) - { - const char* name = levels[levelFiltered[r]]; - if (ImGui::Selectable(name, name == selectedLevel)) selectedLevel = name; - } - ImGui::EndChild(); - ImGui::EndCombo(); + UI::Text("Load into map, spawning and cosmetics use the ImGui menu backend."); } - ImGui::SameLine(); - if (ImGui::Button("Load into map")) + else { - Events::Payload payload; - payload.name = selectedLevel; - Events::Dispatch(Events::Type::LoadIntoMap, payload); - } - if (isInGame) ImGui::EndDisabled(); + // "Load into map" is usable whenever you're out of a game (e.g. back in the menu after a + // match), disabled only while already in one. The dropdown beside it picks the target level + // SwitchLevel travels to. These are the game's Content/Maps package names (leaf, no path or + // .BuiltData); Simulation_Alpha (the firing range) is index 0 and the default. The selection + // rides in on the event payload. Enumerated from the FName pool via the SDK tab. + static const char* const levels[] = { + // Simulation / firing-range maps + "Simulation_Alpha", // default + "Simulation_Bravo", + "Simulation_Charlie", + "Simulation_Delta", + "Simulation_Echo", + "Simulation_Foxtrot", + "Simulation_Golf", + "Simulation_Hotel", + "Simulation_India", + "Simulation_Juliet", + // Arena maps + "Abyss", + "Atlantis", + "Crag", + "Foregone_Destruction", + "Helix", + "Highwind", + "Impact", + "Karman_Station", + "Lavawell", + "Oasis", + "Olympus", + "Pantheon", + "Silo", + "Stadium", + // Special / system maps + "MainMenu", + "Lobby", + "Tutorial", + "PracticeRange", + "TravelMap", + "Forge_Island", + "Forge_Flat_Earth", + "Abyss_Cinematics", + // Blockout / work-in-progress maps + "Maya_Blockout", + "Noboru_Temple_Blockout", + "Decay_Blockout_WIP", + "Drift_Blockout_WIP", + "Titan_Blockout_WIP", + "Toxic_Blockout_Wip", + "Vessel_Blockout_WIP", + "Vintage_Blockout_WIP", + }; + // Searchable dropdown (same pattern as the spawn picker): filter the static list, click a + // row to select. selectedLevel points into the static array above, so it stays valid to hand + // to the event payload. + static std::vector levelFiltered; + static char levelSearch[128] = ""; + static std::string levelLastKey = "\x01"; // sentinel: forces the first filter build + static const char* selectedLevel = levels[0]; - ImGui::SameLine(); - if (!isInGame) ImGui::BeginDisabled(); - if (ImGui::Button("Respawn")) - if (auto* character = reinterpret_cast(Engine::PlayerController->Character)) - character->RequestSuicide(); - if (!isInGame) ImGui::EndDisabled(); - ImGui::Tooltip("Kill your character so it respawns (RequestSuicide)."); - - // Spawn picker: a searchable dropdown of spawnable actor classes (bots, pawns, guns, ...) - // scanned from GObjects, plus a Spawn button that spawns the selection in front of you. - { - static std::vector filtered; // indices into the shared ClassCache - static char search[128] = ""; - static std::string lastKey = "\x01"; // sentinel: forces the first filter build - static size_t lastCacheSize = SIZE_MAX; // re-filter when the cache is rebuilt - static std::string selected; - static const char* keywords[] = {"Bot", "Pawn", "Gun", "Weapon", "Character", "Projectile", "Grenade", "Vehicle"}; - - ImGui::SetNextItemWidth(240.f); - if (ImGui::BeginCombo("##spawnclass", selected.empty() ? "Spawn class..." : selected.c_str())) + if (isInGame) ImGui::BeginDisabled(); + ImGui::SetNextItemWidth(220.f); + if (ImGui::BeginCombo("##level", selectedLevel)) { - const auto& classes = ClassCache::Get(); // shared, built once - ImGui::SetNextItemWidth(-1.f); - ImGui::InputTextWithHint("##spawnsearch", "filter: bot, gun, pawn...", search, sizeof(search)); + ImGui::InputTextWithHint("##levelsearch", "filter maps...", levelSearch, sizeof(levelSearch)); - // Rebuild the filtered index list only when the search or the underlying cache changes. - if (search != lastKey || classes.size() != lastCacheSize) + // Rebuild the filtered index list only when the search text changes. + if (levelSearch != levelLastKey) { - lastKey = search; - lastCacheSize = classes.size(); - filtered.clear(); - const std::string needle = search; - for (int i = 0; i < static_cast(classes.size()); i++) - { - const std::string& name = classes[i].name; - bool spawnable = false; // narrow to bots/pawns/guns/... so it's a spawn list, not every class - for (const char* kw : keywords) - if (name.find(kw) != std::string::npos) { spawnable = true; break; } - if (!spawnable) continue; - if (!needle.empty() && name.find(needle) == std::string::npos) continue; - filtered.push_back(i); - } + levelLastKey = levelSearch; + levelFiltered.clear(); + const std::string needle = levelSearch; + for (int i = 0; i < IM_ARRAYSIZE(levels); i++) + if (needle.empty() || std::string(levels[i]).find(needle) != std::string::npos) + levelFiltered.push_back(i); } - // Clip to the visible rows so a few-thousand-class list isn't laid out in full each frame. - ImGui::BeginChild("##spawnlist", ImVec2(340, 220)); + ImGui::BeginChild("##levellist", ImVec2(240, 200)); ImGuiListClipper clipper; - clipper.Begin(static_cast(filtered.size())); + clipper.Begin(static_cast(levelFiltered.size())); while (clipper.Step()) for (int r = clipper.DisplayStart; r < clipper.DisplayEnd; r++) { - const std::string& name = classes[filtered[r]].name; - if (ImGui::Selectable(name.c_str(), name == selected)) selected = name; + const char* name = levels[levelFiltered[r]]; + if (ImGui::Selectable(name, name == selectedLevel)) selectedLevel = name; } ImGui::EndChild(); ImGui::EndCombo(); } ImGui::SameLine(); - if (ImGui::SmallButton("Refresh##spawn")) ClassCache::Rebuild(); - - ImGui::SameLine(); - if (!isInGame || selected.empty()) ImGui::BeginDisabled(); - if (ImGui::Button("Spawn") && isInGame && !selected.empty() && Engine::PlayerController) + if (ImGui::Button("Load into map")) { - UObject* cls = Engine::GObjects->FindObject(selected.c_str()); - auto* pawn = Engine::PlayerController->AcknowledgedPawn; - if (cls && pawn) - { - FVector loc = reinterpret_cast(pawn)->K2_GetActorLocation(); - loc.X += 200.f; // a bit in front of the player - AActor* actor = SpawnActor(reinterpret_cast(Engine::PlayerController), reinterpret_cast(cls), - loc, ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn, nullptr); - Logger::Log(actor ? "SUCCESS" : "ERROR", (actor ? "Spawned " : "Spawn failed: ") + selected); - } - else - Logger::Log("ERROR", "Spawn: class not found: " + selected); + Events::Payload payload; + payload.name = selectedLevel; + Events::Dispatch(Events::Type::LoadIntoMap, payload); } - if (!isInGame || selected.empty()) ImGui::EndDisabled(); - ImGui::Tooltip("Pick a spawnable class (searchable — bots, pawns, guns, ...) and Spawn it in front\nof you. Refresh rescans GObjects. Uses the deferred SpawnActor."); - } + if (isInGame) ImGui::EndDisabled(); - ImGui::SeparatorText("Cosmetics"); - { - static std::vector skinFiltered; // indices into the shared ClassCache - static char skinSearch[128] = ""; - static std::string skinLastKey = "\x01"; - static size_t skinLastCacheSize = SIZE_MAX; - static std::string skinSelected; + ImGui::SameLine(); + if (!isInGame) ImGui::BeginDisabled(); + if (ImGui::Button("Respawn")) + if (auto* character = reinterpret_cast(Engine::PlayerController->Character)) + character->RequestSuicide(); + if (!isInGame) ImGui::EndDisabled(); + ImGui::Tooltip("Kill your character so it respawns (RequestSuicide)."); - ImGui::SetNextItemWidth(240.f); - if (ImGui::BeginCombo("##skinclass", skinSelected.empty() ? "Skin class..." : skinSelected.c_str())) + // Spawn picker: a searchable dropdown of spawnable actor classes (bots, pawns, guns, ...) + // scanned from GObjects, plus a Spawn button that spawns the selection in front of you. { - const auto& classes = ClassCache::Get(); - - ImGui::SetNextItemWidth(-1.f); - ImGui::InputTextWithHint("##skinsearch", "filter: skin name...", skinSearch, sizeof(skinSearch)); + static std::vector filtered; // indices into the shared ClassCache + static char search[128] = ""; + static std::string lastKey = "\x01"; // sentinel: forces the first filter build + static size_t lastCacheSize = SIZE_MAX; // re-filter when the cache is rebuilt + static std::string selected; + static const char* keywords[] = {"Bot", "Pawn", "Gun", "Weapon", "Character", "Projectile", "Grenade", "Vehicle"}; - if (skinSearch != skinLastKey || classes.size() != skinLastCacheSize) + ImGui::SetNextItemWidth(240.f); + if (ImGui::BeginCombo("##spawnclass", selected.empty() ? "Spawn class..." : selected.c_str())) { - skinLastKey = skinSearch; - skinLastCacheSize = classes.size(); - skinFiltered.clear(); - const std::string needle = skinSearch; - for (int i = 0; i < static_cast(classes.size()); i++) + const auto& classes = ClassCache::Get(); // shared, built once + + ImGui::SetNextItemWidth(-1.f); + ImGui::InputTextWithHint("##spawnsearch", "filter: bot, gun, pawn...", search, sizeof(search)); + + // Rebuild the filtered index list only when the search or the underlying cache changes. + if (search != lastKey || classes.size() != lastCacheSize) { - const std::string& name = classes[i].name; - if (name.find("Skin") == std::string::npos) continue; // skins only - if (!needle.empty() && name.find(needle) == std::string::npos) continue; - skinFiltered.push_back(i); + lastKey = search; + lastCacheSize = classes.size(); + filtered.clear(); + const std::string needle = search; + for (int i = 0; i < static_cast(classes.size()); i++) + { + const std::string& name = classes[i].name; + bool spawnable = false; // narrow to bots/pawns/guns/... so it's a spawn list, not every class + for (const char* kw : keywords) + if (name.find(kw) != std::string::npos) + { + spawnable = true; + break; + } + if (!spawnable) continue; + if (!needle.empty() && name.find(needle) == std::string::npos) continue; + filtered.push_back(i); + } } + + // Clip to the visible rows so a few-thousand-class list isn't laid out in full each frame. + ImGui::BeginChild("##spawnlist", ImVec2(340, 220)); + ImGuiListClipper clipper; + clipper.Begin(static_cast(filtered.size())); + while (clipper.Step()) + for (int r = clipper.DisplayStart; r < clipper.DisplayEnd; r++) + { + const std::string& name = classes[filtered[r]].name; + if (ImGui::Selectable(name.c_str(), name == selected)) selected = name; + } + ImGui::EndChild(); + ImGui::EndCombo(); } + ImGui::SameLine(); + if (ImGui::SmallButton("Refresh##spawn")) ClassCache::Rebuild(); - ImGui::BeginChild("##skinlist", ImVec2(340, 220)); - ImGuiListClipper clipper; - clipper.Begin(static_cast(skinFiltered.size())); - while (clipper.Step()) - for (int r = clipper.DisplayStart; r < clipper.DisplayEnd; r++) + ImGui::SameLine(); + if (!isInGame || selected.empty()) ImGui::BeginDisabled(); + if (ImGui::Button("Spawn") && isInGame && !selected.empty() && Engine::PlayerController) + { + UObject* cls = Engine::GObjects->FindObject(selected.c_str()); + auto* pawn = Engine::PlayerController->AcknowledgedPawn; + if (cls && pawn) { - const std::string& name = classes[skinFiltered[r]].name; - if (ImGui::Selectable(name.c_str(), name == skinSelected)) skinSelected = name; + FVector loc = reinterpret_cast(pawn)->K2_GetActorLocation(); + loc.X += 200.f; // a bit in front of the player + AActor* actor = SpawnActor(reinterpret_cast(Engine::PlayerController), reinterpret_cast(cls), + loc, ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn, nullptr); + Logger::Log(actor ? "SUCCESS" : "ERROR", (actor ? "Spawned " : "Spawn failed: ") + selected); } - ImGui::EndChild(); - ImGui::EndCombo(); + else + Logger::Log("ERROR", "Spawn: class not found: " + selected); + } + if (!isInGame || selected.empty()) ImGui::EndDisabled(); + ImGui::Tooltip("Pick a spawnable class (searchable — bots, pawns, guns, ...) and Spawn it in front\nof you. Refresh rescans GObjects. Uses the deferred SpawnActor."); } - ImGui::SameLine(); - if (ImGui::SmallButton("Refresh##skin")) ClassCache::Rebuild(); - ImGui::SameLine(); - if (!isInGame || skinSelected.empty()) ImGui::BeginDisabled(); - if (ImGui::Button("Apply skin") && isInGame && !skinSelected.empty() && Engine::PlayerController) + ImGui::SeparatorText("Cosmetics"); { - UObject* cls = Engine::GObjects->FindObject(skinSelected.c_str()); - auto* character = reinterpret_cast(Engine::PlayerController->Character); - if (cls && character) + static std::vector skinFiltered; // indices into the shared ClassCache + static char skinSearch[128] = ""; + static std::string skinLastKey = "\x01"; + static size_t skinLastCacheSize = SIZE_MAX; + static std::string skinSelected; + + ImGui::SetNextItemWidth(240.f); + if (ImGui::BeginCombo("##skinclass", skinSelected.empty() ? "Skin class..." : skinSelected.c_str())) { - character->CharacterSkinClass = reinterpret_cast(cls); - character->UpdateSkins(); - Logger::Log("SUCCESS", "Applied skin: " + skinSelected); + const auto& classes = ClassCache::Get(); + + ImGui::SetNextItemWidth(-1.f); + ImGui::InputTextWithHint("##skinsearch", "filter: skin name...", skinSearch, sizeof(skinSearch)); + + if (skinSearch != skinLastKey || classes.size() != skinLastCacheSize) + { + skinLastKey = skinSearch; + skinLastCacheSize = classes.size(); + skinFiltered.clear(); + const std::string needle = skinSearch; + for (int i = 0; i < static_cast(classes.size()); i++) + { + const std::string& name = classes[i].name; + if (name.find("Skin") == std::string::npos) continue; // skins only + if (!needle.empty() && name.find(needle) == std::string::npos) continue; + skinFiltered.push_back(i); + } + } + + ImGui::BeginChild("##skinlist", ImVec2(340, 220)); + ImGuiListClipper clipper; + clipper.Begin(static_cast(skinFiltered.size())); + while (clipper.Step()) + for (int r = clipper.DisplayStart; r < clipper.DisplayEnd; r++) + { + const std::string& name = classes[skinFiltered[r]].name; + if (ImGui::Selectable(name.c_str(), name == skinSelected)) skinSelected = name; + } + ImGui::EndChild(); + ImGui::EndCombo(); } - else - Logger::Log("ERROR", "Apply skin: class not found: " + skinSelected); - } - if (!isInGame || skinSelected.empty()) ImGui::EndDisabled(); - ImGui::Tooltip("Pick a skin class, then apply it to your character, gun or jetpack.\nClient-side (sets the *SkinClass + UpdateSkins); the server may re-assert your real skins. Refresh rescans classes."); + ImGui::SameLine(); + if (ImGui::SmallButton("Refresh##skin")) ClassCache::Rebuild(); - // Apply the selected class to the gun / jetpack too (they use their own skin types). - if (!isInGame || skinSelected.empty()) ImGui::BeginDisabled(); - if (ImGui::Button("Apply gun skin") && isInGame && !skinSelected.empty() && Engine::PlayerController) - { - UObject* cls = Engine::GObjects->FindObject(skinSelected.c_str()); - auto* character = reinterpret_cast(Engine::PlayerController->Character); - if (cls && character && character->CurrentWeapon) + ImGui::SameLine(); + if (!isInGame || skinSelected.empty()) ImGui::BeginDisabled(); + if (ImGui::Button("Apply skin") && isInGame && !skinSelected.empty() && Engine::PlayerController) { - character->CurrentWeapon->WeaponSkinClass = reinterpret_cast(cls); - character->CurrentWeapon->UpdateSkins(); - Logger::Log("SUCCESS", "Applied gun skin: " + skinSelected); + UObject* cls = Engine::GObjects->FindObject(skinSelected.c_str()); + auto* character = reinterpret_cast(Engine::PlayerController->Character); + if (cls && character) + { + character->CharacterSkinClass = reinterpret_cast(cls); + character->UpdateSkins(); + Logger::Log("SUCCESS", "Applied skin: " + skinSelected); + } + else + Logger::Log("ERROR", "Apply skin: class not found: " + skinSelected); } - } - ImGui::SameLine(); - if (ImGui::Button("Apply jetpack skin") && isInGame && !skinSelected.empty() && Engine::PlayerController) - { - UObject* cls = Engine::GObjects->FindObject(skinSelected.c_str()); - auto* character = reinterpret_cast(Engine::PlayerController->Character); - if (cls && character) + if (!isInGame || skinSelected.empty()) ImGui::EndDisabled(); + ImGui::Tooltip("Pick a skin class, then apply it to your character, gun or jetpack.\nClient-side (sets the *SkinClass + UpdateSkins); the server may re-assert your real skins. Refresh rescans classes."); + + // Apply the selected class to the gun / jetpack too (they use their own skin types). + if (!isInGame || skinSelected.empty()) ImGui::BeginDisabled(); + if (ImGui::Button("Apply gun skin") && isInGame && !skinSelected.empty() && Engine::PlayerController) + { + UObject* cls = Engine::GObjects->FindObject(skinSelected.c_str()); + auto* character = reinterpret_cast(Engine::PlayerController->Character); + if (cls && character && character->CurrentWeapon) + { + character->CurrentWeapon->WeaponSkinClass = reinterpret_cast(cls); + character->CurrentWeapon->UpdateSkins(); + Logger::Log("SUCCESS", "Applied gun skin: " + skinSelected); + } + } + ImGui::SameLine(); + if (ImGui::Button("Apply jetpack skin") && isInGame && !skinSelected.empty() && Engine::PlayerController) { - character->JetpackSkinClass = reinterpret_cast(cls); - character->UpdateSkins(); - Logger::Log("SUCCESS", "Applied jetpack skin: " + skinSelected); + UObject* cls = Engine::GObjects->FindObject(skinSelected.c_str()); + auto* character = reinterpret_cast(Engine::PlayerController->Character); + if (cls && character) + { + character->JetpackSkinClass = reinterpret_cast(cls); + character->UpdateSkins(); + Logger::Log("SUCCESS", "Applied jetpack skin: " + skinSelected); + } } + if (!isInGame || skinSelected.empty()) ImGui::EndDisabled(); } - if (!isInGame || skinSelected.empty()) ImGui::EndDisabled(); - } - ImGui::SeparatorText("Program"); - if (ImGui::Button("Toggle Console")) + } // else (ImGui-only Game / Cosmetics) + + UI::SeparatorText("Program"); + if (UI::Button("Toggle Console")) { Settings.MISC.ShowConsole = !Settings.MISC.ShowConsole; Logger::SetConsoleVisibility(Settings.MISC.ShowConsole); } - ImGui::SameLine(); - if (ImGui::Button("Unload")) Hook::RequestUnload(); + UI::SameLine(); + if (UI::Button("Unload")) Hook::RequestUnload(); - if (ImGui::ToggleButton("Announce toggles in chat", &Settings.MISC.AnnounceToggles)) + if (UI::Toggle("Announce toggles in chat", &Settings.MISC.AnnounceToggles)) Events::Dispatch(Events::Type::SettingsChanged); - ImGui::Tooltip("Post a local (client-only) chat line when you toggle a feature, e.g. \"[ESP] Enabled\".\nShown only to you, not sent to the server."); - + UI::Tooltip("Post a local (client-only) chat line when you toggle a feature, e.g. \"[ESP] Enabled\".\nShown only to you, not sent to the server."); } } // namespace Sections } // namespace Menu \ No newline at end of file diff --git a/Internal/menu/sections/Network.h b/Internal/menu/sections/Network.h index c08d8ca..9367a58 100644 --- a/Internal/menu/sections/Network.h +++ b/Internal/menu/sections/Network.h @@ -11,6 +11,7 @@ #include "../../settings/Settings.h" #include "../../scripting/Events.h" #include "../../../shared/LauncherSettings.h" // Shared::LauncherSettings (launcher.settings) +#include "../ui/UI.h" namespace Menu { @@ -38,6 +39,14 @@ namespace Menu /// SettingsChanged on any change. void NetworkTab() { + // The redirect-map editor and mitmproxy config are InputText / multiline / child-based, so + // this whole tab is ImGui-only; the Canvas backend shows a note. + if (!UI::IsImGui()) + { + UI::Text("The backend-redirect editor uses the ImGui menu backend."); + return; + } + bool changed = false; ImGui::SeparatorText("Backend redirect"); @@ -78,11 +87,11 @@ namespace Menu changed = true; } - changed |= ImGui::ToggleButton("Bypass SSL verification", &Settings.NETWORK.BypassSslVerify); - ImGui::Tooltip("Force curl's cert/host verification off so a redirected host can serve a self-signed cert.\nDisables TLS verification for ALL curl traffic while on."); + changed |= ImGui::ToggleButton("Bypass SSL verification", &Settings.NETWORK.BypassSslVerify); + ImGui::Tooltip("Force curl's cert/host verification off so a redirected host can serve a self-signed cert.\nDisables TLS verification for ALL curl traffic while on."); - // Mitmproxy script — a launcher-only setting (launcher.settings), so it lives outside - // the DLL's SETTINGS. Only relevant when the launcher will spawn mitmproxy. + // Mitmproxy script — a launcher-only setting (launcher.settings), so it lives outside + // the DLL's SETTINGS. Only relevant when the launcher will spawn mitmproxy. if (Settings.NETWORK.Proxy == ProxyMode::Mitmproxy) { ImGui::SeparatorText("Mitmproxy script"); diff --git a/Internal/menu/sections/Scripts.h b/Internal/menu/sections/Scripts.h index f3c4883..c2aff5c 100644 --- a/Internal/menu/sections/Scripts.h +++ b/Internal/menu/sections/Scripts.h @@ -9,6 +9,7 @@ #include "../../settings/Settings.h" #include "../../scripting/Scripts.h" #include "../../scripting/Events.h" +#include "../ui/UI.h" namespace Menu { @@ -17,18 +18,26 @@ namespace Menu /// @brief Renders the Scripts tab. void ScriptsTab() { - ImGui::SeparatorText("User Scripts"); - ImGui::Tooltip("Python scripts from Documents\\SplitgateInternal\\UserScripts. See docs/scripting.md."); + UI::SeparatorText("User Scripts"); + UI::Tooltip("Python scripts from Documents\\SplitgateInternal\\UserScripts. See docs/scripting.md."); - if (ImGui::ToggleButton("Enable", &Settings.MISC.UserScriptsEnabled)) + if (UI::Toggle("Enable", &Settings.MISC.UserScriptsEnabled)) Events::Dispatch(Events::Type::SettingsChanged); - ImGui::SameLine(); - if (ImGui::Button("Reload")) Scripts::Reload(); - ImGui::Tooltip("Re-scan the UserScripts folder and re-import every script (edits take effect\n" - "without a relaunch). Previously-registered script event handlers are cleared\n" - "first, so reloading doesn't stack duplicates."); + UI::SameLine(); + if (UI::Button("Reload")) Scripts::Reload(); + UI::Tooltip("Re-scan the UserScripts folder and re-import every script (edits take effect\n" + "without a relaunch). Previously-registered script event handlers are cleared\n" + "first, so reloading doesn't stack duplicates."); + + UI::SeparatorText("Loaded Scripts"); + + // The scrollable list with per-script Run buttons uses ImGui child regions — ImGui-only. + if (!UI::IsImGui()) + { + UI::Text("The loaded-script list uses the ImGui menu backend."); + return; + } - ImGui::SeparatorText("Loaded Scripts"); if (Scripts::scriptList.empty()) { ImGui::TextDisabled("No scripts found. Drop a .py with a main() into the UserScripts folder, then Reload."); diff --git a/Internal/menu/sections/Sdk.h b/Internal/menu/sections/Sdk.h index 4706e13..0281b0b 100644 --- a/Internal/menu/sections/Sdk.h +++ b/Internal/menu/sections/Sdk.h @@ -24,6 +24,7 @@ #include "../../cache/NameCache.h" #include "../../ue/Engine.h" #include "../../utils/Logger.h" +#include "../ui/UI.h" namespace Menu { @@ -40,6 +41,14 @@ namespace Menu /// @brief Renders the SDK tab. void SdkTab() { + // The object explorer is search boxes + clipped, scrollable result lists (InputText / child / + // clipper), so this whole tab is ImGui-only; the Canvas backend shows a note. + if (!UI::IsImGui()) + { + UI::Text("The SDK object explorer uses the ImGui menu backend."); + return; + } + ImGui::SeparatorText("Object search"); ImGui::Tooltip("Scan every GObject and list those whose full name contains the text.\nOn demand (a full walk, like Dump GObjects)."); @@ -71,7 +80,8 @@ namespace Menu if (ImGui::Button("Copy##names")) { std::string out; - for (const auto& row : nameResults) out += std::format("[{}] {}\n", row.index, row.name); + for (const auto& row : nameResults) + out += std::format("[{}] {}\n", row.index, row.name); ImGui::SetClipboardText(out.c_str()); } ImGui::Tooltip("Copy the listed results to the clipboard."); @@ -144,8 +154,8 @@ namespace Menu static bool caseSensitive = false; // default: case-insensitive static bool useRegex = false; // plain substring by default static std::vector nameFiltered; - static std::string regexError; // last regex compile error, shown when useRegex - static std::string lastNameKey = "\x01"; // sentinel: forces the first filter build + static std::string regexError; // last regex compile error, shown when useRegex + static std::string lastNameKey = "\x01"; // sentinel: forces the first filter build static size_t lastNameCacheSize = SIZE_MAX; // re-filter when the cache is rebuilt static bool lastCase = false, lastRegex = false; @@ -173,7 +183,8 @@ namespace Menu const std::string needle = nameFilter2; if (needle.empty()) { - for (int i = 0; i < static_cast(allNames.size()); i++) nameFiltered.push_back(i); + for (int i = 0; i < static_cast(allNames.size()); i++) + nameFiltered.push_back(i); } else if (useRegex) { @@ -212,7 +223,8 @@ namespace Menu if (ImGui::Button("Copy##namepool")) { std::string out; - for (int idx : nameFiltered) out += allNames[idx] + "\n"; + for (int idx : nameFiltered) + out += allNames[idx] + "\n"; ImGui::SetClipboardText(out.c_str()); } ImGui::Tooltip("Copy the filtered names to the clipboard."); @@ -274,7 +286,8 @@ namespace Menu if (ImGui::Button("Copy##instances")) { std::string out; - for (const auto& row : instanceResults) out += std::format("[{}] 0x{:x} {}\n", row.index, row.address, row.name); + for (const auto& row : instanceResults) + out += std::format("[{}] 0x{:x} {}\n", row.index, row.address, row.name); ImGui::SetClipboardText(out.c_str()); } ImGui::Tooltip("Copy the listed instances (index, address, name) to the clipboard."); diff --git a/Internal/menu/sections/Visuals.h b/Internal/menu/sections/Visuals.h index 9e59cb9..8fbe830 100644 --- a/Internal/menu/sections/Visuals.h +++ b/Internal/menu/sections/Visuals.h @@ -5,6 +5,7 @@ #include "../../settings/Settings.h" #include "../../scripting/Events.h" +#include "../ui/UI.h" namespace Menu { @@ -19,114 +20,124 @@ namespace Menu bool changed = false; auto& v = Settings.VISUALS; - ImGui::SeparatorText("Renderer"); + UI::SeparatorText("Renderer"); const char* renderers[] = {"UE Canvas", "ImGui (faster)", "None", "External (streamproof)"}; int renderer = static_cast(Settings.MENU.Renderer); - if (ImGui::Combo("Draw with", &renderer, renderers, IM_ARRAYSIZE(renderers))) + if (UI::Combo("Draw with", &renderer, renderers, IM_ARRAYSIZE(renderers))) { Settings.MENU.Renderer = static_cast(renderer); changed = true; } - ImGui::Tooltip("ImGui draws the overlay without a ProcessEvent per line/text - much faster for a busy ESP.\n" - "External draws the ESP, watermark, and everything the renderer produces into a separate\n" - "window hidden from screen capture (OBS, Game Bar); the menu stays on the game window."); - - ImGui::SeparatorText("Player ESP"); - changed |= ImGui::ToggleButton("Enable", &v.Esp); - - ImGui::SeparatorText("Elements"); - changed |= ImGui::ToggleButton("Name", &v.Name); - changed |= ImGui::ToggleButton("Box", &v.Box); - changed |= ImGui::ToggleButton("3D Box", &v.Box3D); - changed |= ImGui::ToggleButton("Bones", &v.Bones); - changed |= ImGui::ToggleButton("Snaplines", &v.Snaplines); - changed |= ImGui::ToggleButton("Health", &v.Health); - changed |= ImGui::ToggleButton("Distance", &v.Distance); - changed |= ImGui::ToggleButton("K/D", &v.KD); - ImGui::Tooltip("Draw each player's kills/deaths, and [killstreak] for the current life."); - changed |= ImGui::ToggleButton("Rank", &v.Rank); - ImGui::Tooltip("Draw each player's rank/level (from their player state)."); - - ImGui::SeparatorText("Range"); - changed |= ImGui::SliderFloat("Max distance (m)", &v.MaxDistance, 0.f, 300.f, v.MaxDistance <= 0.f ? "unlimited" : "%.0f"); - ImGui::Tooltip("Only draw enemies within this many metres. 0 = unlimited."); - - ImGui::SeparatorText("Visibility"); - changed |= ImGui::ToggleButton("Visibility check", &v.EspVisibleCheck); - ImGui::Tooltip("Recolor visible (recently-rendered) enemies in the Visible color below;\noccluded enemies keep the normal box/bone/snapline colors."); - changed |= ImGui::ToggleButton("Hide bots", &v.HideBots); - ImGui::Tooltip("Don't draw AI bots in the ESP at all."); - changed |= ImGui::ToggleButton("Bot tag", &v.BotTag); - ImGui::Tooltip("Prefix an AI bot's name with a colored \"[BOT]\" tag."); + UI::Tooltip("ImGui draws the overlay without a ProcessEvent per line/text - much faster for a busy ESP.\n" + "External draws the ESP, watermark, and everything the renderer produces into a separate\n" + "window hidden from screen capture (OBS, Game Bar); the menu stays on the game window."); + + const char* backends[] = {"ImGui", "UE Canvas"}; + int backend = static_cast(Settings.MENU.Backend); + if (UI::Combo("Menu backend", &backend, backends, IM_ARRAYSIZE(backends))) + { + Settings.MENU.Backend = static_cast(backend); + changed = true; + } + UI::Tooltip("Which GUI engine draws THIS menu (independent of the ESP renderer above).\n" + "ImGui = the Present overlay; UE Canvas = drawn on the game canvas (works at the main menu too)."); + + UI::SeparatorText("Player ESP"); + changed |= UI::Toggle("Enable", &v.Esp); + + UI::SeparatorText("Elements"); + changed |= UI::Toggle("Name", &v.Name); + changed |= UI::Toggle("Box", &v.Box); + changed |= UI::Toggle("3D Box", &v.Box3D); + changed |= UI::Toggle("Bones", &v.Bones); + changed |= UI::Toggle("Snaplines", &v.Snaplines); + changed |= UI::Toggle("Health", &v.Health); + changed |= UI::Toggle("Distance", &v.Distance); + changed |= UI::Toggle("K/D", &v.KD); + UI::Tooltip("Draw each player's kills/deaths, and [killstreak] for the current life."); + changed |= UI::Toggle("Rank", &v.Rank); + UI::Tooltip("Draw each player's rank/level (from their player state)."); + + UI::SeparatorText("Range"); + changed |= UI::SliderFloat("Max distance (m)", &v.MaxDistance, 0.f, 300.f, v.MaxDistance <= 0.f ? "unlimited" : "%.0f"); + UI::Tooltip("Only draw enemies within this many metres. 0 = unlimited."); + + UI::SeparatorText("Visibility"); + changed |= UI::Toggle("Visibility check", &v.EspVisibleCheck); + UI::Tooltip("Recolor visible (recently-rendered) enemies in the Visible color below;\noccluded enemies keep the normal box/bone/snapline colors."); + changed |= UI::Toggle("Hide bots", &v.HideBots); + UI::Tooltip("Don't draw AI bots in the ESP at all."); + changed |= UI::Toggle("Bot tag", &v.BotTag); + UI::Tooltip("Prefix an AI bot's name with a colored \"[BOT]\" tag."); if (v.BotTag) - ImGui::ColorEdit4("Bot tag color", &v.BotTagColor.R); + UI::ColorEdit("Bot tag color", &v.BotTagColor); - ImGui::SeparatorText("Teams"); - changed |= ImGui::ToggleButton("Show teammates", &v.ShowFriendly); - ImGui::Tooltip("Also draw teammates (ESP + radar), in the friendly color below."); + UI::SeparatorText("Teams"); + changed |= UI::Toggle("Show teammates", &v.ShowFriendly); + UI::Tooltip("Also draw teammates (ESP + radar), in the friendly color below."); - ImGui::SeparatorText("Radar"); - changed |= ImGui::ToggleButton("Enable Radar", &v.Radar); - changed |= ImGui::ToggleButton("Radar teammates", &v.RadarShowFriendly); + UI::SeparatorText("Radar"); + changed |= UI::Toggle("Enable Radar", &v.Radar); + changed |= UI::Toggle("Radar teammates", &v.RadarShowFriendly); - ImGui::SeparatorText("Debug"); - changed |= ImGui::ToggleButton("Draw all object names", &v.DrawAllNames); - ImGui::Tooltip("Draws the UObject name of every actor in the world (not just players)."); + UI::SeparatorText("Debug"); + changed |= UI::Toggle("Draw all object names", &v.DrawAllNames); + UI::Tooltip("Draws the UObject name of every actor in the world (not just players)."); - ImGui::SeparatorText("Text"); - changed |= ImGui::SliderFloat("Font size", &v.FontScale, 0.5f, 3.f, "%.2f"); + UI::SeparatorText("Text"); + changed |= UI::SliderFloat("Font size", &v.FontScale, 0.5f, 3.f, "%.2f"); - ImGui::SeparatorText("Crosshair"); - changed |= ImGui::ToggleButton("Crosshair", &v.Crosshair); + UI::SeparatorText("Crosshair"); + changed |= UI::Toggle("Crosshair", &v.Crosshair); if (v.Crosshair) { - changed |= ImGui::SliderFloat("Size", &v.CrosshairSize, 1.f, 30.f, "%.0f"); - changed |= ImGui::SliderFloat("Gap", &v.CrosshairGap, 0.f, 20.f, "%.0f"); - changed |= ImGui::SliderFloat("Thickness", &v.CrosshairThickness, 1.f, 6.f, "%.0f"); - ImGui::ColorEdit4("Crosshair color", &v.CrosshairColor.R); - ImGui::Tooltip("Overridden by the RGB rainbow when RGB is on."); + changed |= UI::SliderFloat("Size", &v.CrosshairSize, 1.f, 30.f, "%.0f"); + changed |= UI::SliderFloat("Gap", &v.CrosshairGap, 0.f, 20.f, "%.0f"); + changed |= UI::SliderFloat("Thickness", &v.CrosshairThickness, 1.f, 6.f, "%.0f"); + UI::ColorEdit("Crosshair color", &v.CrosshairColor); + UI::Tooltip("Overridden by the RGB rainbow when RGB is on."); } - ImGui::SeparatorText("Bullet traces"); - changed |= ImGui::ToggleButton("Bullet traces", &v.BulletTraces); - ImGui::Tooltip("Draw a fading trail behind each projectile (PortalWars.Projectile and subclasses)."); + UI::SeparatorText("Bullet traces"); + changed |= UI::Toggle("Bullet traces", &v.BulletTraces); + UI::Tooltip("Draw a fading trail behind each projectile (PortalWars.Projectile and subclasses)."); if (v.BulletTraces) { - changed |= ImGui::SliderFloat("Trail duration", &v.BulletTraceDuration, 0.5f, 6.f, "%.1fs"); - ImGui::ColorEdit4("Trail color", &v.BulletTraceColor.R); - ImGui::Tooltip("Overridden by the RGB rainbow when RGB is on."); + changed |= UI::SliderFloat("Trail duration", &v.BulletTraceDuration, 0.5f, 6.f, "%.1fs"); + UI::ColorEdit("Trail color", &v.BulletTraceColor); + UI::Tooltip("Overridden by the RGB rainbow when RGB is on."); } - ImGui::SeparatorText("Glow / chams"); - changed |= ImGui::ToggleButton("Glow enemies", &v.GlowEnemy); - ImGui::Tooltip("Force a custom-depth outline on enemies, visible through walls.\nRides on the game's team-outline post-process (verify color mapping in-game)."); + UI::SeparatorText("Glow / chams"); + changed |= UI::Toggle("Glow enemies", &v.GlowEnemy); + UI::Tooltip("Force a custom-depth outline on enemies, visible through walls.\nRides on the game's team-outline post-process (verify color mapping in-game)."); if (v.GlowEnemy) { - ImGui::ColorEdit4("Enemy glow", &v.GlowEnemyColor.R); - ImGui::Tooltip("Overridden by the RGB rainbow when RGB is on."); + UI::ColorEdit("Enemy glow", &v.GlowEnemyColor); + UI::Tooltip("Overridden by the RGB rainbow when RGB is on."); } - changed |= ImGui::ToggleButton("Glow teammates", &v.GlowFriendly); + changed |= UI::Toggle("Glow teammates", &v.GlowFriendly); if (v.GlowFriendly) { - ImGui::ColorEdit4("Teammate glow", &v.GlowFriendlyColor.R); - ImGui::Tooltip("Overridden by the RGB rainbow when RGB is on."); + UI::ColorEdit("Teammate glow", &v.GlowFriendlyColor); + UI::Tooltip("Overridden by the RGB rainbow when RGB is on."); } - changed |= ImGui::ToggleButton("Glow self", &v.GlowSelf); - ImGui::Tooltip("Outline your own pawn - only visible in third person."); + changed |= UI::Toggle("Glow self", &v.GlowSelf); + UI::Tooltip("Outline your own pawn - only visible in third person."); if (v.GlowSelf) { - ImGui::ColorEdit4("Self glow", &v.GlowSelfColor.R); - ImGui::Tooltip("Overridden by the RGB rainbow when RGB is on."); + UI::ColorEdit("Self glow", &v.GlowSelfColor); + UI::Tooltip("Overridden by the RGB rainbow when RGB is on."); } - ImGui::SeparatorText("Colors"); - ImGui::ColorEdit4("Name", &v.NameColor.R); - ImGui::ColorEdit4("Box", &v.BoxColor.R); - ImGui::ColorEdit4("Bones", &v.BonesColor.R); - ImGui::ColorEdit4("Snaplines", &v.SnaplineColor.R); - ImGui::ColorEdit4("Friendly", &v.FriendColor.R); - ImGui::ColorEdit4("Visible", &v.VisibleColor.R); - ImGui::Tooltip("Color for visible enemies when the visibility check is on."); + UI::SeparatorText("Colors"); + UI::ColorEdit("Name", &v.NameColor); + UI::ColorEdit("Box", &v.BoxColor); + UI::ColorEdit("Bones", &v.BonesColor); + UI::ColorEdit("Snaplines", &v.SnaplineColor); + UI::ColorEdit("Friendly", &v.FriendColor); + UI::ColorEdit("Visible", &v.VisibleColor); + UI::Tooltip("Color for visible enemies when the visibility check is on."); if (changed) Events::Dispatch(Events::Type::SettingsChanged); } From 04a832279140a965716ebcc38b97883f81cc7227 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:54:08 +0200 Subject: [PATCH 07/54] chore: update world assignment --- Internal/hook/functions/PostRender.h | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Internal/hook/functions/PostRender.h b/Internal/hook/functions/PostRender.h index c70b6a0..29a6e13 100644 --- a/Internal/hook/functions/PostRender.h +++ b/Internal/hook/functions/PostRender.h @@ -39,9 +39,7 @@ namespace PostRender Engine::PlayerController = PlayerController; Engine::IsInGame = PlayerController && PlayerController->IsInGame(); - // The UCanvas arg is valid every frame (main menu / loading included), so publish it before the - // in-game gate — the Canvas menu draws through it unconditionally, while features still gate on - // PlayerController below. + Engine::World = World; Engine::Canvas = Canvas; if (Settings.MENU.Backend == MenuBackend::Canvas) @@ -49,8 +47,6 @@ namespace PostRender if (PlayerController) { - Engine::World = World; - // Edge-detect hotkeys once per frame → Events::HotKeyPressed (press-once actions subscribe). Input::DispatchHotKeys(); From e2c6fab30f7a472d0ea0fe50be08ed7bd9649e31 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:12:55 +0200 Subject: [PATCH 08/54] refactor(render): rename Palette::Cream to Background, add Success/Muted Retint the ImGui style to the renamed token; Success (green) and Muted (gray) back the canvas checkbox mark and slider knob. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/gui/Styles.h | 6 +++--- Internal/render/Colors.h | 16 +++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Internal/menu/gui/Styles.h b/Internal/menu/gui/Styles.h index 84ac297..46f7605 100644 --- a/Internal/menu/gui/Styles.h +++ b/Internal/menu/gui/Styles.h @@ -37,11 +37,11 @@ namespace GUI colors[ImGuiCol_Text] = Palette::Text.To(); colors[ImGuiCol_TextDisabled] = Palette::Text.Alpha(0.77f).To(); - colors[ImGuiCol_WindowBg] = Palette::Cream.Alpha(0.82f).To(); + colors[ImGuiCol_WindowBg] = Palette::Background.Alpha(0.82f).To(); colors[ImGuiCol_ChildBg] = Palette::Frame.Alpha(0.58f).To(); - colors[ImGuiCol_PopupBg] = Palette::Cream.Alpha(0.92f).To(); + colors[ImGuiCol_PopupBg] = Palette::Background.Alpha(0.92f).To(); colors[ImGuiCol_Border] = Palette::Border.Alpha(0.65f).To(); - colors[ImGuiCol_BorderShadow] = Palette::Cream.Alpha(0.00f).To(); + colors[ImGuiCol_BorderShadow] = Palette::Background.Alpha(0.00f).To(); colors[ImGuiCol_FrameBg] = Palette::Frame.To(); colors[ImGuiCol_FrameBgHovered] = ImVec4(1.00f, 0.40f, 0.40f, 0.78f); // one-off light red colors[ImGuiCol_FrameBgActive] = Palette::Primary.To(); diff --git a/Internal/render/Colors.h b/Internal/render/Colors.h index 5f9f3d2..7fed28f 100644 --- a/Internal/render/Colors.h +++ b/Internal/render/Colors.h @@ -13,14 +13,16 @@ namespace Render /// The menu's cream/red theme palette. Reuse `.Alpha(a)` for a color at a different opacity. namespace Palette { - inline constexpr Color Primary{1.00f, 0.00f, 0.00f, 1.00f}; ///< the accent red (titles, active controls) - inline constexpr Color Text{0.40f, 0.39f, 0.38f, 1.00f}; ///< body text on the cream ground - inline constexpr Color Cream{0.92f, 0.91f, 0.88f, 1.00f}; ///< window background - inline constexpr Color Frame{1.00f, 0.98f, 0.95f, 1.00f}; ///< lighter frame/child/title background - inline constexpr Color Border{0.84f, 0.83f, 0.80f, 1.00f}; ///< border tint + inline constexpr Color Primary{1.00f, 0.00f, 0.00f, 1.00f}; ///< the accent red (titles, active controls) + inline constexpr Color Text{0.40f, 0.39f, 0.38f, 1.00f}; ///< body text on the cream ground + inline constexpr Color Background{0.92f, 0.91f, 0.88f, 1.00f}; ///< window background + inline constexpr Color Frame{1.00f, 0.98f, 0.95f, 1.00f}; ///< lighter frame/child/title background + inline constexpr Color Border{0.84f, 0.83f, 0.80f, 1.00f}; ///< border tint inline constexpr Color White{1.00f, 1.00f, 1.00f, 1.00f}; inline constexpr Color Black{0.00f, 0.00f, 0.00f, 1.00f}; - inline constexpr Color Blue{0.10f, 0.40f, 0.75f, 1.00f}; ///< separator/interaction accent - inline constexpr Color Slate{0.43f, 0.43f, 0.50f, 1.00f}; ///< neutral separator gray + inline constexpr Color Blue{0.10f, 0.40f, 0.75f, 1.00f}; ///< separator/interaction accent + inline constexpr Color Slate{0.43f, 0.43f, 0.50f, 1.00f}; ///< neutral separator gray + inline constexpr Color Success{0.00f, 0.80f, 0.28f, 1.00f}; ///< enabled/on state (e.g. checked toggles) + inline constexpr Color Muted{0.70f, 0.70f, 0.70f, 1.00f}; ///< muted gray (e.g. slider knob) } // namespace Palette } // namespace Render From bf760e43779aa92fc6d6128ee6545c19996b1a0c Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:12:56 +0200 Subject: [PATCH 09/54] feat(shared): add PasteFromClipboard to Shared::Utilities The read counterpart to CopyToClipboard, so the DLL can paste without its own Win32 helper. Co-Authored-By: Claude Opus 4.8 --- shared/Utilities.h | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/shared/Utilities.h b/shared/Utilities.h index cad8d2b..fd9cabe 100644 --- a/shared/Utilities.h +++ b/shared/Utilities.h @@ -1,8 +1,8 @@ #pragma once /// @file -/// @brief Small cross-project helpers shared by the launcher and the DLL: putting text on the -/// clipboard and opening a folder in the system file browser. +/// @brief Small cross-project helpers shared by the launcher and the DLL: putting text on and +/// reading text off the clipboard, and opening a folder in the system file browser. #include #include @@ -47,6 +47,27 @@ namespace Shared::Utilities return ok; } + /** + * Reads CF_TEXT off the clipboard. Best-effort — returns an empty string if the clipboard + * couldn't be opened or holds no text. + * @return the clipboard text (empty on failure). + */ + inline std::string PasteFromClipboard() + { + std::string text; + if (!OpenClipboard(nullptr)) return text; + + if (HANDLE data = GetClipboardData(CF_TEXT)) + if (const char* src = static_cast(GlobalLock(data))) + { + text = src; + GlobalUnlock(data); + } + + CloseClipboard(); + return text; + } + /// Opens @p path in the system file browser (Explorer), creating the folder first so the call /// succeeds even on a fresh install. No-op on an empty path. inline void OpenFolder(const std::filesystem::path& path) From 96d511c40493c51c48a0aa3846971aa58894e8bf Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:12:56 +0200 Subject: [PATCH 10/54] feat(input): add immediate-mode GUI helpers to the Input namespace IsMouseClicked/IsKeyPressed/IsAnyMouseDown/Handle + per-element state, for the UE-canvas menu (replaces the standalone UCanvasInput/ZeroInput). Co-Authored-By: Claude Opus 4.8 --- Internal/utils/Input.h | 75 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/Internal/utils/Input.h b/Internal/utils/Input.h index 92a60e2..f794feb 100644 --- a/Internal/utils/Input.h +++ b/Internal/utils/Input.h @@ -68,4 +68,79 @@ namespace Input prev[vk] = down; } } + + // --- Immediate-mode GUI input (used by the UE-canvas menu / UCanvasGUI). --- + // Sampled once per frame by Handle() and edge-detected per widget id, independent of the + // game-focus-gated Down()/Pressed() above (the canvas menu gates itself via its active-window + // check). Kept as a distinct per-element scheme so overlapping menu widgets each see their own + // click; not for feature hotkeys. + + inline bool mouseDown[5]; + inline bool mouseDownAlready[256]; + + inline bool keysDown[256]; + inline bool keysDownAlready[256]; + + inline bool IsAnyMouseDown() + { + if (mouseDown[0]) return true; + if (mouseDown[1]) return true; + if (mouseDown[2]) return true; + if (mouseDown[3]) return true; + if (mouseDown[4]) return true; + + return false; + } + + /// Rising-edge (or, with @p repeat, level) detection of button @p button for widget @p element_id. + inline bool IsMouseClicked(int button, int element_id, bool repeat) + { + if (mouseDown[button]) + { + if (!mouseDownAlready[element_id]) + { + mouseDownAlready[element_id] = true; + return true; + } + if (repeat) + return true; + } + else + { + mouseDownAlready[element_id] = false; + } + return false; + } + + inline bool IsKeyPressed(int key, bool repeat) + { + if (keysDown[key]) + { + if (!keysDownAlready[key]) + { + keysDownAlready[key] = true; + return true; + } + if (repeat) + return true; + } + else + { + keysDownAlready[key] = false; + } + return false; + } + + /// Sample every mouse button and key once per frame (high bit = currently down). + inline void Handle() + { + mouseDown[0] = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0; + mouseDown[1] = (GetAsyncKeyState(VK_RBUTTON) & 0x8000) != 0; + mouseDown[2] = (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0; + mouseDown[3] = (GetAsyncKeyState(VK_XBUTTON1) & 0x8000) != 0; + mouseDown[4] = (GetAsyncKeyState(VK_XBUTTON2) & 0x8000) != 0; + + for (int i = 0; i < 256; i++) + keysDown[i] = (GetAsyncKeyState(i) & 0x8000) != 0; + } } // namespace Input From 58276731a52dd68fb7b03e880b96a8fa48f2bdcf Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:12:56 +0200 Subject: [PATCH 11/54] feat(menu): add UCanvasGUI::Colors theme seeded from the palette Mutable FLinearColor members defaulting to Render::Palette; Accent is retinted per-frame for the RGB feature. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/canvas/Colors.h | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Internal/menu/canvas/Colors.h diff --git a/Internal/menu/canvas/Colors.h b/Internal/menu/canvas/Colors.h new file mode 100644 index 0000000..78fa93d --- /dev/null +++ b/Internal/menu/canvas/Colors.h @@ -0,0 +1,27 @@ +#pragma once + +/// @file +/// @brief UCanvasGUI::Colors — the canvas menu's theme colors (mutable FLinearColor). Each member is +/// seeded at startup from a Render::Palette entry (bridged to FLinearColor by the UE adapter). They +/// stay mutable so the canvas backend can retint the accent per frame for the RGB feature and reset +/// it to the palette default when that feature is off. + +#include "../../ue/sdk/FLinearColor.h" +#include "../../render/Colors.h" +#include "../../render/adapters/Ue.h" + +namespace UCanvasGUI +{ + /// The canvas menu theme (0-1 float RGBA), defaulting to the shared Render::Palette. Only `Accent` + /// changes at runtime (RGB retint); the rest are effectively constants sourced from the palette. + namespace Colors + { + inline FLinearColor Accent = Render::Palette::Primary.To(); ///< titles, active tabs, buttons, slider fill (retinted by the RGB feature) + inline FLinearColor Text = Render::Palette::Text.To(); ///< body and label text + inline FLinearColor Background = Render::Palette::Background.To(); ///< window, popup, and child backgrounds + inline FLinearColor Frame = Render::Palette::Border.To(); ///< neutral element backgrounds (tab strip, sliders, combos, headers, fields) + inline FLinearColor Selection = Render::Palette::Blue.To(); ///< highlighted / selected row + inline FLinearColor Enabled = Render::Palette::Success.To(); ///< checked-toggle mark + inline FLinearColor Knob = Render::Palette::Muted.To(); ///< slider knob + } // namespace Colors +} // namespace UCanvasGUI From e35848e16d6b1087c064970d9f79144ae4da3f9a Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:08 +0200 Subject: [PATCH 12/54] refactor(menu): rename ZeroGUI to UCanvasGUI and split it into per-widget components The monolithic ZeroGUI.h becomes an umbrella (UCanvasGUI.h) over components/: Core (shared state, draw primitives, layout, PostRenderer queue) plus one file per widget (Window, Text, Tab, Button, Checkbox, Slider, Combobox, Hotkey, ColorPicker, TextField, Selectable, CollapsingHeader, Combo, Child). Widgets draw through the active Render backend and the project Input namespace; dead helpers removed; naming brought to the project conventions. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/canvas/UCanvasGUI.h | 25 + Internal/menu/canvas/ZeroGUI.h | 1210 ----------------- Internal/menu/canvas/ZeroInput.h | 83 -- Internal/menu/canvas/components/Button.h | 26 + Internal/menu/canvas/components/Checkbox.h | 35 + Internal/menu/canvas/components/Child.h | 25 + .../menu/canvas/components/CollapsingHeader.h | 33 + Internal/menu/canvas/components/ColorPicker.h | 221 +++ Internal/menu/canvas/components/Combo.h | 35 + Internal/menu/canvas/components/Combobox.h | 73 + Internal/menu/canvas/components/Core.h | 245 ++++ Internal/menu/canvas/components/Hotkey.h | 109 ++ Internal/menu/canvas/components/Selectable.h | 29 + Internal/menu/canvas/components/Slider.h | 96 ++ Internal/menu/canvas/components/Tab.h | 26 + Internal/menu/canvas/components/Text.h | 25 + Internal/menu/canvas/components/TextField.h | 90 ++ Internal/menu/canvas/components/Window.h | 70 + 18 files changed, 1163 insertions(+), 1293 deletions(-) create mode 100644 Internal/menu/canvas/UCanvasGUI.h delete mode 100644 Internal/menu/canvas/ZeroGUI.h delete mode 100644 Internal/menu/canvas/ZeroInput.h create mode 100644 Internal/menu/canvas/components/Button.h create mode 100644 Internal/menu/canvas/components/Checkbox.h create mode 100644 Internal/menu/canvas/components/Child.h create mode 100644 Internal/menu/canvas/components/CollapsingHeader.h create mode 100644 Internal/menu/canvas/components/ColorPicker.h create mode 100644 Internal/menu/canvas/components/Combo.h create mode 100644 Internal/menu/canvas/components/Combobox.h create mode 100644 Internal/menu/canvas/components/Core.h create mode 100644 Internal/menu/canvas/components/Hotkey.h create mode 100644 Internal/menu/canvas/components/Selectable.h create mode 100644 Internal/menu/canvas/components/Slider.h create mode 100644 Internal/menu/canvas/components/Tab.h create mode 100644 Internal/menu/canvas/components/Text.h create mode 100644 Internal/menu/canvas/components/TextField.h create mode 100644 Internal/menu/canvas/components/Window.h diff --git a/Internal/menu/canvas/UCanvasGUI.h b/Internal/menu/canvas/UCanvasGUI.h new file mode 100644 index 0000000..299cf59 --- /dev/null +++ b/Internal/menu/canvas/UCanvasGUI.h @@ -0,0 +1,25 @@ +#pragma once + +/// @file +/// @brief UCanvasGUI — the immediate-mode UE-canvas menu backend. Recovered from the project's original +/// GUI and adapted to the neutral Render API: every primitive draws through the active `Render` +/// backend (canvas or the ImGui recorder), so there is no direct K2_Draw* here and no per-scanline +/// fill loops. Positions stay FVector2D and colors FLinearColor (they convert implicitly to Render::Vec2 / +/// Render::Color). This is the umbrella header: it pulls in the shared core plus every widget +/// component from `components/`; include it to use the GUI. Everything is `inline`. + +#include "components/Core.h" +#include "components/Window.h" +#include "components/Text.h" +#include "components/Tab.h" +#include "components/Button.h" +#include "components/Checkbox.h" +#include "components/Slider.h" +#include "components/Combobox.h" +#include "components/Hotkey.h" +#include "components/ColorPicker.h" +#include "components/TextField.h" +#include "components/Selectable.h" +#include "components/CollapsingHeader.h" +#include "components/Combo.h" +#include "components/Child.h" diff --git a/Internal/menu/canvas/ZeroGUI.h b/Internal/menu/canvas/ZeroGUI.h deleted file mode 100644 index c5a1f0a..0000000 --- a/Internal/menu/canvas/ZeroGUI.h +++ /dev/null @@ -1,1210 +0,0 @@ -#pragma once - -/// @file -/// @brief ZeroGUI — the immediate-mode UE-canvas menu backend. Recovered from the project's original -/// GUI and adapted to the neutral Render API: every primitive now draws through `Render::canvas` -/// (which targets `Engine::Canvas`), so there is no direct K2_Draw* here and no per-scanline fill -/// loops. Positions stay FVector2D and colors FLinearColor (they convert implicitly to Render::Vec2 / -/// Render::Color). Everything is `inline` — this header is included in multiple translation units. - -#include -#include -#include -#include - -#include "ZeroInput.h" -#include "../../ue/Engine.h" -#include "../../render/Render.h" - -namespace ZeroGUI -{ - /// Cream/red theme (0-1 float RGBA). MainColor is retinted from the RGB rainbow each tick. - namespace Colors - { - inline FLinearColor MainColor{1.0f, 0.0f, 0.0f, 1.0f}; - - inline FLinearColor Text{0.10f, 0.10f, 0.10f, 1.0f}; - - inline FLinearColor Window_Background{0.92f, 0.91f, 0.88f, 1.0f}; - inline FLinearColor Window_Header{1.0f, 0.0f, 0.0f, 1.0f}; - inline FLinearColor Window_Tabs_Background{0.80f, 0.79f, 0.76f, 1.0f}; - - inline FLinearColor Button_Idle{1.0f, 0.0f, 0.0f, 1.0f}; - inline FLinearColor Button_Hovered{1.0f, 0.0f, 0.0f, 1.0f}; - inline FLinearColor Button_Active{1.0f, 0.0f, 0.0f, 1.0f}; - - inline FLinearColor Checkbox_Idle{1.0f, 0.0f, 0.0f, 1.0f}; - inline FLinearColor Checkbox_Hovered{1.0f, 0.0f, 0.0f, 1.0f}; - inline FLinearColor Checkbox_Enabled{0.0f, 0.80f, 0.28f, 1.0f}; - - inline FLinearColor Combobox_Idle{0.85f, 0.84f, 0.81f, 1.0f}; - inline FLinearColor Combobox_Hovered{0.85f, 0.84f, 0.81f, 1.0f}; - inline FLinearColor Combobox_Elements{0.24f, 0.42f, 0.82f, 1.0f}; - - inline FLinearColor Slider_Idle{0.80f, 0.79f, 0.76f, 1.0f}; - inline FLinearColor Slider_Hovered{0.80f, 0.79f, 0.76f, 1.0f}; - inline FLinearColor Slider_Progress{1.0f, 0.0f, 0.0f, 1.0f}; - inline FLinearColor Slider_Button{0.70f, 0.70f, 0.70f, 1.0f}; - - inline FLinearColor ColorPicker_Background{0.90f, 0.89f, 0.86f, 1.0f}; - } // namespace Colors - - // Forward declarations of the drawing helpers (PostRenderer's dispatch calls back into them). - inline void drawFilledRect(FVector2D initial_pos, float w, float h, FLinearColor color); - inline void TextLeft(const char* name, FVector2D pos, FLinearColor color, bool outline); - inline void TextCenter(const char* name, FVector2D pos, FLinearColor color, bool outline); - inline void Draw_Line(FVector2D from, FVector2D to, int thickness, FLinearColor color); - - /// Deferred draw queue: pop-ups (combo dropdowns, color-picker swatches) enqueue here so they - /// replay last, on top of the widgets drawn earlier in the frame. Render() drains it. - namespace PostRenderer - { - struct DrawList - { - int type = -1; // 1 = FilledRect, 2 = TextLeft, 3 = TextCenter, 4 = Draw_Line - FVector2D pos; - FVector2D size; - FLinearColor color; - const char* name; - bool outline; - - FVector2D from; - FVector2D to; - int thickness; - }; - inline DrawList drawlist[128]; - - inline void drawFilledRect(FVector2D pos, float w, float h, FLinearColor color) - { - for (int i = 0; i < 128; i++) - { - if (drawlist[i].type == -1) - { - drawlist[i].type = 1; - drawlist[i].pos = pos; - drawlist[i].size = FVector2D{w, h}; - drawlist[i].color = color; - return; - } - } - } - inline void TextLeft(const char* name, FVector2D pos, FLinearColor color, bool outline) - { - for (int i = 0; i < 128; i++) - { - if (drawlist[i].type == -1) - { - drawlist[i].type = 2; - drawlist[i].name = name; - drawlist[i].pos = pos; - drawlist[i].outline = outline; - drawlist[i].color = color; - return; - } - } - } - inline void TextCenter(const char* name, FVector2D pos, FLinearColor color, bool outline) - { - for (int i = 0; i < 128; i++) - { - if (drawlist[i].type == -1) - { - drawlist[i].type = 3; - drawlist[i].name = name; - drawlist[i].pos = pos; - drawlist[i].outline = outline; - drawlist[i].color = color; - return; - } - } - } - inline void Draw_Line(FVector2D from, FVector2D to, int thickness, FLinearColor color) - { - for (int i = 0; i < 128; i++) - { - if (drawlist[i].type == -1) - { - drawlist[i].type = 4; - drawlist[i].from = from; - drawlist[i].to = to; - drawlist[i].thickness = thickness; - drawlist[i].color = color; - return; - } - } - } - } // namespace PostRenderer - - // --- Immediate-mode layout state (single window). --- - inline bool hover_element = false; - inline FVector2D menu_pos = FVector2D{0, 0}; - inline float offset_x = 0.0f; - inline float offset_y = 0.0f; - - inline FVector2D first_element_pos = FVector2D{0, 0}; - - inline FVector2D last_element_pos = FVector2D{0, 0}; - inline FVector2D last_element_size = FVector2D{0, 0}; - - inline int current_element = -1; - inline FVector2D current_element_pos = FVector2D{0, 0}; - inline FVector2D current_element_size = FVector2D{0, 0}; - inline int elements_count = 0; - - inline bool sameLine = false; - - inline bool pushY = false; - inline float pushYvalue = 0.0f; - - /// Point the canvas backend at the frame's UCanvas (Render::canvas draws through Engine::Canvas). - inline void SetupCanvas(UCanvas* _canvas) - { - Engine::Canvas = _canvas; - } - - inline FVector2D CursorPos() - { - POINT cursorPos; - GetCursorPos(&cursorPos); - - ScreenToClient(GetActiveWindow(), &cursorPos); - - return FVector2D{(float)cursorPos.x, (float)cursorPos.y}; - } - inline bool MouseInZone(FVector2D pos, FVector2D size) - { - FVector2D cursor_pos = CursorPos(); - - if (cursor_pos.X > pos.X && cursor_pos.Y > pos.Y) - if (cursor_pos.X < pos.X + size.X && cursor_pos.Y < pos.Y + size.Y) - return true; - - return false; - } - - /// Software arrow cursor built from line segments (the Canvas menu owns its cursor; it does not - /// borrow ImGui's software cursor, keeping the two backends fully decoupled). - inline void Draw_Cursor(bool toogle) - { - if (toogle) - { - FVector2D cursorPos = CursorPos(); - Render::canvas.Line(FVector2D{cursorPos.X, cursorPos.Y}, FVector2D{cursorPos.X + 35, cursorPos.Y + 10}, 1, FLinearColor{0.30f, 0.30f, 0.80f, 1.0f}); - - int x = 35; - int y = 10; - while (y != 30) // 20 steps - { - x -= 1; - if (x < 15) x = 15; - y += 1; - if (y > 30) y = 30; - - Render::canvas.Line(FVector2D{cursorPos.X, cursorPos.Y}, FVector2D{cursorPos.X + x, cursorPos.Y + y}, 1, FLinearColor{0.30f, 0.30f, 0.80f, 1.0f}); - } - - Render::canvas.Line(FVector2D{cursorPos.X, cursorPos.Y}, FVector2D{cursorPos.X + 15, cursorPos.Y + 30}, 1, FLinearColor{0.30f, 0.30f, 0.80f, 1.0f}); - Render::canvas.Line(FVector2D{cursorPos.X + 35, cursorPos.Y + 10}, FVector2D{cursorPos.X + 15, cursorPos.Y + 30}, 1, FLinearColor{0.30f, 0.30f, 0.80f, 1.0f}); - } - } - - inline void SameLine() - { - sameLine = true; - } - inline void PushNextElementY(float y, bool from_last_element = true) - { - pushY = true; - if (from_last_element) - pushYvalue = last_element_pos.Y + last_element_size.Y + y; - else - pushYvalue = y; - } - inline void NextColumn(float x) - { - offset_x = x; - PushNextElementY(first_element_pos.Y, false); - } - inline void ClearFirstPos() - { - first_element_pos = FVector2D{0, 0}; - } - - inline void TextLeft(const char* name, FVector2D pos, FLinearColor color, bool outline) - { - Render::canvas.Text(pos, std::string(name), 0.97f, color, false); - } - inline void TextCenter(const char* name, FVector2D pos, FLinearColor color, bool outline) - { - Render::canvas.Text(pos, std::string(name), 0.97f, color, true); - } - - inline void GetColor(FLinearColor* color, float* r, float* g, float* b, float* a) - { - *r = color->R; - *g = color->G; - *b = color->B; - *a = color->A; - } - inline UINT32 GetColorUINT(int r, int g, int b, int a) - { - UINT32 result = (BYTE(a) << 24) + (BYTE(r) << 16) + (BYTE(g) << 8) + BYTE(b); - return result; - } - - inline void Draw_Line(FVector2D from, FVector2D to, int thickness, FLinearColor color) - { - Render::canvas.Line(FVector2D{from.X, from.Y}, FVector2D{to.X, to.Y}, (float)thickness, color); - } - inline void drawFilledRect(FVector2D initial_pos, float w, float h, FLinearColor color) - { - Render::canvas.RectFilled(FVector2D{initial_pos.X, initial_pos.Y}, FVector2D{initial_pos.X + w, initial_pos.Y + h}, color); - } - inline void DrawFilledCircle(FVector2D pos, float r, FLinearColor color) - { - Render::canvas.CircleFilled(FVector2D{pos.X, pos.Y}, r, color); - } - inline void DrawCircle(FVector2D pos, int radius, int numSides, FLinearColor Color) - { - float P_I = 3.1415927f; - - float Step = P_I * 2.0f / numSides; - int Count = 0; - FVector2D V[128]; - for (float a = 0; a < P_I * 2.0f; a += Step) - { - float X1 = radius * cosf(a) + pos.X; - float Y1 = radius * sinf(a) + pos.Y; - float X2 = radius * cosf(a + Step) + pos.X; - float Y2 = radius * sinf(a + Step) + pos.Y; - V[Count].X = X1; - V[Count].Y = Y1; - V[Count + 1].X = X2; - V[Count + 1].Y = Y2; - Draw_Line(FVector2D{V[Count].X, V[Count].Y}, FVector2D{X2, Y2}, 1, Color); // Circle Around - } - } - - inline FVector2D dragPos; - inline bool Window(const char* name, FVector2D* pos, FVector2D size, bool isOpen) - { - elements_count = 0; - static HWND HWND = FindWindow((L"UnrealWindow"), (L"PortalWars ")); - - if (!isOpen || (GetActiveWindow() != HWND)) - { - return false; - }; - - bool isHovered = MouseInZone(FVector2D{pos->X, pos->Y}, size); - - // Drop last element - if (current_element != -1 && !GetAsyncKeyState(0x1)) - { - current_element = -1; - } - - // Drag - if (hover_element && GetAsyncKeyState(0x1)) - { - } - else if ((isHovered || dragPos.X != 0) && !hover_element) - { - if (Input::IsMouseClicked(0, elements_count, true)) - { - FVector2D cursorPos = CursorPos(); - - cursorPos.X -= size.X; - cursorPos.Y -= size.Y; - - if (dragPos.X == 0) - { - dragPos.X = (cursorPos.X - pos->X); - dragPos.Y = (cursorPos.Y - pos->Y); - } - pos->X = cursorPos.X - dragPos.X; - pos->Y = cursorPos.Y - dragPos.Y; - } - else - { - dragPos = FVector2D{0, 0}; - } - } - else - { - hover_element = false; - } - - offset_x = 0.0f; - offset_y = 0.0f; - menu_pos = FVector2D{pos->X, pos->Y}; - first_element_pos = FVector2D{0, 0}; - current_element_pos = FVector2D{0, 0}; - current_element_size = FVector2D{0, 0}; - - // Bg - drawFilledRect(FVector2D{pos->X, pos->Y}, size.X, size.Y, Colors::Window_Background); - drawFilledRect(FVector2D{pos->X, pos->Y}, 122, size.Y, Colors::Window_Tabs_Background); - - // Header - drawFilledRect(FVector2D{pos->X, pos->Y}, size.X, 25.0f, Colors::MainColor); - - offset_y += 25.0f; - - // Title - FVector2D titlePos = FVector2D{pos->X + size.X / 2, pos->Y + 25 / 2}; - TextCenter(name, titlePos, Colors::Text, false); - - return true; - } - - inline void Text(const char* text, bool center = false, bool outline = false) - { - elements_count++; - - float size = 25; - FVector2D padding = FVector2D{10, 10}; - FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; - if (sameLine) - { - pos.X = last_element_pos.X + last_element_size.X + padding.X; - pos.Y = last_element_pos.Y; - } - if (pushY) - { - pos.Y = pushYvalue; - pushY = false; - pushYvalue = 0.0f; - offset_y = pos.Y - menu_pos.Y; - } - - if (!sameLine) - offset_y += size + padding.Y; - - // Text - FVector2D textPos = FVector2D{pos.X + 5.0f, pos.Y + size / 2}; - if (center) - TextCenter(text, textPos, Colors::Text, outline); - else - TextLeft(text, textPos, Colors::Text, outline); - - sameLine = false; - last_element_pos = pos; - if (first_element_pos.X == 0.0f) - first_element_pos = pos; - } - inline bool ButtonTab(const char* name, FVector2D size, bool active) - { - elements_count++; - - FVector2D padding = FVector2D{5, 10}; - FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; - if (sameLine) - { - pos.X = last_element_pos.X + last_element_size.X + padding.X; - pos.Y = last_element_pos.Y; - } - if (pushY) - { - pos.Y = pushYvalue; - pushY = false; - pushYvalue = 0.0f; - offset_y = pos.Y - menu_pos.Y; - } - bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, size); - - // Bg - if (active) - { - drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::MainColor); - } - else if (isHovered) - { - drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::MainColor); - hover_element = true; - } - else - { - drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::MainColor); - } - - if (!sameLine) - offset_y += size.Y + padding.Y; - - // Text - FVector2D textPos = FVector2D{pos.X + size.X / 2, pos.Y + size.Y / 2}; - TextCenter(name, textPos, Colors::Text, false); - - sameLine = false; - last_element_pos = pos; - last_element_size = size; - if (first_element_pos.X == 0.0f) - first_element_pos = pos; - - if (isHovered && Input::IsMouseClicked(0, elements_count, false)) - return true; - - return false; - } - inline bool Button(const char* name, FVector2D size) - { - elements_count++; - - FVector2D padding = FVector2D{5, 10}; - FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; - if (sameLine) - { - pos.X = last_element_pos.X + last_element_size.X + padding.X; - pos.Y = last_element_pos.Y; - } - if (pushY) - { - pos.Y = pushYvalue; - pushY = false; - pushYvalue = 0.0f; - offset_y = pos.Y - menu_pos.Y; - } - bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, size); - - // Bg - if (isHovered) - { - drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::MainColor); - hover_element = true; - } - else - { - drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::MainColor); - } - - if (!sameLine) - offset_y += size.Y + padding.Y; - - // Text - FVector2D textPos = FVector2D{pos.X + size.X / 2, pos.Y + size.Y / 2}; - TextCenter(name, textPos, Colors::Text, false); - - sameLine = false; - last_element_pos = pos; - last_element_size = size; - if (first_element_pos.X == 0.0f) - first_element_pos = pos; - - if (isHovered && Input::IsMouseClicked(0, elements_count, false)) - return true; - - return false; - } - inline bool Checkbox(const char* name, bool* value) - { - elements_count++; - - float size = 18; - FVector2D padding = FVector2D{10, 10}; - FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; - if (sameLine) - { - pos.X = last_element_pos.X + last_element_size.X + padding.X; - pos.Y = last_element_pos.Y; - } - if (pushY) - { - pos.Y = pushYvalue; - pushY = false; - pushYvalue = 0.0f; - offset_y = pos.Y - menu_pos.Y; - } - bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, FVector2D{size, size}); - - // Bg - if (isHovered) - { - drawFilledRect(FVector2D{pos.X, pos.Y}, size, size, Colors::MainColor); - hover_element = true; - } - else - { - drawFilledRect(FVector2D{pos.X, pos.Y}, size, size, Colors::MainColor); - } - - if (!sameLine) - offset_y += size + padding.Y; - - if (*value) - { - drawFilledRect(FVector2D{pos.X + 3, pos.Y + 3}, size - 6, size - 6, Colors::Checkbox_Enabled); - } - - // Text - FVector2D textPos = FVector2D{pos.X + size + 5.0f, pos.Y + size / 2}; - TextLeft(name, textPos, Colors::Text, false); - - sameLine = false; - last_element_pos = pos; - if (first_element_pos.X == 0.0f) - first_element_pos = pos; - - if (isHovered && Input::IsMouseClicked(0, elements_count, false)) - { - *value = !*value; - return true; - } - return false; - } - inline void SliderInt(const char* name, int* value, int min, int max) - { - elements_count++; - - FVector2D size = FVector2D{240, 50}; - FVector2D slider_size = FVector2D{200, 10}; - FVector2D padding = FVector2D{10, 15}; - FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; - if (sameLine) - { - pos.X = last_element_pos.X + last_element_size.X + padding.X; - pos.Y = last_element_pos.Y; - } - if (pushY) - { - pos.Y = pushYvalue; - pushY = false; - pushYvalue = 0.0f; - offset_y = pos.Y - menu_pos.Y; - } - bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, slider_size); - - if (!sameLine) - offset_y += size.Y + padding.Y; - - // Bg - if (isHovered || current_element == elements_count) - { - // Drag - if (Input::IsMouseClicked(0, elements_count, true)) - { - current_element = elements_count; - - FVector2D cursorPos = CursorPos(); - *value = (int)(((cursorPos.X - pos.X) * ((max - min) / slider_size.X)) + min); - if (*value < min) *value = min; - if (*value > max) *value = max; - } - - drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, slider_size.X, slider_size.Y, Colors::Slider_Hovered); - drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y + 5.0f}, 5.0f, 5.0f, Colors::Slider_Progress); - - hover_element = true; - } - else - { - drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, slider_size.X, slider_size.Y, Colors::Slider_Idle); - drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y + 5.0f}, 5.0f, 5.0f, Colors::Slider_Progress); - } - - // Value - float oneP = slider_size.X / (max - min); - drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, oneP * (*value - min), slider_size.Y, Colors::Slider_Progress); - DrawFilledCircle(FVector2D{pos.X + oneP * (*value - min), pos.Y + slider_size.Y + 3.3f + padding.Y}, 10.0f, Colors::Slider_Button); - DrawFilledCircle(FVector2D{pos.X + oneP * (*value - min), pos.Y + slider_size.Y + 3.3f + padding.Y}, 5.0f, Colors::Slider_Progress); - - char buffer[32]; - sprintf_s(buffer, "%i", *value); - FVector2D valuePos = FVector2D{pos.X + oneP * (*value - min), pos.Y + slider_size.Y + 25 + padding.Y}; - TextCenter(buffer, valuePos, Colors::Text, false); - - // Text - FVector2D textPos = FVector2D{pos.X + 5, pos.Y + 10}; - TextLeft(name, textPos, Colors::Text, false); - - sameLine = false; - last_element_pos = pos; - last_element_size = size; - if (first_element_pos.X == 0.0f) - first_element_pos = pos; - } - inline void SliderFloat(const char* name, float* value, float min, float max, const char* format = "%.0f") - { - elements_count++; - - FVector2D size = FVector2D{210, 40}; - FVector2D slider_size = FVector2D{170, 7}; - FVector2D adjust_zone = FVector2D{0, 20}; - FVector2D padding = FVector2D{10, 15}; - FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; - if (sameLine) - { - pos.X = last_element_pos.X + last_element_size.X + padding.X; - pos.Y = last_element_pos.Y; - } - if (pushY) - { - pos.Y = pushYvalue; - pushY = false; - pushYvalue = 0.0f; - offset_y = pos.Y - menu_pos.Y; - } - bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y - adjust_zone.Y}, FVector2D{slider_size.X, slider_size.Y + adjust_zone.Y * 1.5f}); - - if (!sameLine) - offset_y += size.Y + padding.Y; - - // Bg - if (isHovered || current_element == elements_count) - { - // Drag - if (Input::IsMouseClicked(0, elements_count, true)) - { - current_element = elements_count; - - FVector2D cursorPos = CursorPos(); - *value = ((cursorPos.X - pos.X) * ((max - min) / slider_size.X)) + min; - if (*value < min) *value = min; - if (*value > max) *value = max; - } - - drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, slider_size.X, slider_size.Y, Colors::Slider_Hovered); - DrawFilledCircle(FVector2D{pos.X, pos.Y + padding.Y + 9.3f}, 3.1f, Colors::Slider_Progress); - DrawFilledCircle(FVector2D{pos.X + slider_size.X, pos.Y + padding.Y + 9.3f}, 3.1f, Colors::Slider_Hovered); - - hover_element = true; - } - else - { - drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, slider_size.X, slider_size.Y, Colors::Slider_Idle); - DrawFilledCircle(FVector2D{pos.X, pos.Y + padding.Y + 9.3f}, 3.1f, Colors::Slider_Progress); - DrawFilledCircle(FVector2D{pos.X + slider_size.X, pos.Y + padding.Y + 9.3f}, 3.1f, Colors::Slider_Idle); - } - - // Text - FVector2D textPos = FVector2D{pos.X, pos.Y + 5}; - TextLeft(name, textPos, Colors::Text, false); - - // Value - float oneP = slider_size.X / (max - min); - drawFilledRect(FVector2D{pos.X, pos.Y + slider_size.Y + padding.Y}, oneP * (*value - min), slider_size.Y, Colors::Slider_Progress); - DrawFilledCircle(FVector2D{pos.X + oneP * (*value - min), pos.Y + slider_size.Y + 2.66f + padding.Y}, 8.0f, Colors::Slider_Button); - DrawFilledCircle(FVector2D{pos.X + oneP * (*value - min), pos.Y + slider_size.Y + 2.66f + padding.Y}, 4.0f, Colors::Slider_Progress); - - char buffer[32]; - sprintf_s(buffer, format, *value); - FVector2D valuePos = FVector2D{pos.X + oneP * (*value - min), pos.Y + slider_size.Y + 20 + padding.Y}; - TextCenter(buffer, valuePos, Colors::Text, false); - - sameLine = false; - last_element_pos = pos; - last_element_size = size; - if (first_element_pos.X == 0.0f) - first_element_pos = pos; - } - - inline bool checkbox_enabled[256]; - /// Dropdown combo over an array of @p count option strings (array form, for the widget facade). - inline bool Combobox(const char* name, FVector2D size, int* value, const char* const* items, int count) - { - elements_count++; - bool changed = false; - - FVector2D padding = FVector2D{5, 10}; - FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; - if (sameLine) - { - pos.X = last_element_pos.X + last_element_size.X + padding.X; - pos.Y = last_element_pos.Y; - } - if (pushY) - { - pos.Y = pushYvalue; - pushY = false; - pushYvalue = 0.0f; - offset_y = pos.Y - menu_pos.Y; - } - bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, size); - - // Bg - if (isHovered || checkbox_enabled[elements_count]) - { - drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::Combobox_Hovered); - hover_element = true; - } - else - { - drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::Combobox_Idle); - } - - if (!sameLine) - offset_y += size.Y + padding.Y; - - // Text (label) - FVector2D textPos = FVector2D{pos.X + size.X + 5.0f, pos.Y + size.Y / 2}; - TextLeft(name, textPos, Colors::Text, false); - - // Elements - bool isHovered2 = false; - FVector2D element_pos = pos; - - for (int num = 0; num < count; num++) - { - const char* arg = items[num]; - - // Selected element (drawn on the closed combo) - if (num == *value) - { - FVector2D _textPos = FVector2D{pos.X + size.X / 2, pos.Y + size.Y / 2}; - TextCenter(arg, _textPos, Colors::Text, false); - } - - if (checkbox_enabled[elements_count]) - { - element_pos.Y += 25.0f; - - isHovered2 = MouseInZone(FVector2D{element_pos.X, element_pos.Y}, FVector2D{size.X, 25.0f}); - if (isHovered2) - { - hover_element = true; - PostRenderer::drawFilledRect(FVector2D{element_pos.X, element_pos.Y}, size.X, 25.0f, Colors::Combobox_Hovered); - - // Click - if (Input::IsMouseClicked(0, elements_count, false)) - { - *value = num; - changed = true; - checkbox_enabled[elements_count] = false; - } - } - else - { - PostRenderer::drawFilledRect(FVector2D{element_pos.X, element_pos.Y}, size.X, 25.0f, Colors::Combobox_Idle); - } - - PostRenderer::TextLeft(arg, FVector2D{element_pos.X + 5.0f, element_pos.Y + 15.0f}, Colors::Text, false); - } - } - - sameLine = false; - last_element_pos = pos; - last_element_size = size; - if (first_element_pos.X == 0.0f) - first_element_pos = pos; - - if (isHovered && Input::IsMouseClicked(0, elements_count, false)) - { - checkbox_enabled[elements_count] = !checkbox_enabled[elements_count]; - } - if (!isHovered && !isHovered2 && Input::IsMouseClicked(0, elements_count, false)) - { - checkbox_enabled[elements_count] = false; - } - - return changed; - } - - inline int active_hotkey = -1; - inline bool already_pressed = false; - inline std::string VirtualKeyCodeToString(UCHAR virtualKey) - { - UINT scanCode = MapVirtualKey(virtualKey, MAPVK_VK_TO_VSC); - - if (virtualKey == VK_LBUTTON) return "MOUSE0"; - if (virtualKey == VK_RBUTTON) return "MOUSE1"; - if (virtualKey == VK_MBUTTON) return "MBUTTON"; - if (virtualKey == VK_XBUTTON1) return "XBUTTON1"; - if (virtualKey == VK_XBUTTON2) return "XBUTTON2"; - - CHAR szName[128]; - int result = 0; - switch (virtualKey) - { - case VK_LEFT: - case VK_UP: - case VK_RIGHT: - case VK_DOWN: - case VK_RCONTROL: - case VK_RMENU: - case VK_LWIN: - case VK_RWIN: - case VK_APPS: - case VK_PRIOR: - case VK_NEXT: - case VK_END: - case VK_HOME: - case VK_INSERT: - case VK_DELETE: - case VK_DIVIDE: - case VK_NUMLOCK: - scanCode |= KF_EXTENDED; - default: - result = GetKeyNameTextA(scanCode << 16, szName, 128); - } - - return szName; - } - inline bool Hotkey(const char* name, FVector2D size, int* key) - { - elements_count++; - bool changed = false; - - FVector2D padding = FVector2D{5, 10}; - FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; - if (sameLine) - { - pos.X = last_element_pos.X + last_element_size.X + padding.X; - pos.Y = last_element_pos.Y + (last_element_size.Y / 2) - size.Y / 2; - } - if (pushY) - { - pos.Y = pushYvalue; - pushY = false; - pushYvalue = 0.0f; - offset_y = pos.Y - menu_pos.Y; - } - bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, size); - - // Bg - drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, Colors::MainColor); - if (isHovered) hover_element = true; - - if (!sameLine) - offset_y += size.Y + padding.Y; - - FVector2D textPos = FVector2D{pos.X + size.X / 2, pos.Y + size.Y / 2}; - if (active_hotkey == elements_count) - { - TextCenter("[Press Key]", textPos, Colors::Text, false); - - if (!ZeroGUI::Input::IsAnyMouseDown()) - { - already_pressed = false; - } - - if (!already_pressed) - { - for (int code = 0; code < 255; code++) - { - if (GetAsyncKeyState(code) & 0x8000) - { - *key = code; - changed = true; - active_hotkey = -1; - } - } - } - } - else - { - TextCenter(VirtualKeyCodeToString(*key).c_str(), textPos, Colors::Text, false); - - if (isHovered) - { - if (Input::IsMouseClicked(0, elements_count, false)) - { - already_pressed = true; - active_hotkey = elements_count; - - // Queue fix: drain the currently-pressed keys so the initiating click doesn't bind - for (int code = 0; code < 255; code++) - if (GetAsyncKeyState(code)) - { - } - } - } - else - { - if (Input::IsMouseClicked(0, elements_count, false)) - { - active_hotkey = -1; - } - } - } - - sameLine = false; - last_element_pos = pos; - last_element_size = size; - if (first_element_pos.X == 0.0f) - first_element_pos = pos; - - return changed; - } - - inline int active_picker = -1; - inline FLinearColor saved_color; - inline bool ColorPixel(FVector2D pos, FVector2D size, FLinearColor* original, FLinearColor color) - { - PostRenderer::drawFilledRect(FVector2D{pos.X, pos.Y}, size.X, size.Y, color); - - // Selected swatch outline - if (original->R == color.R && original->G == color.G && original->B == color.B) - { - PostRenderer::Draw_Line(FVector2D{pos.X, pos.Y}, FVector2D{pos.X + size.X - 1, pos.Y}, 1, FLinearColor{0.0f, 0.0f, 0.0f, 1.0f}); - PostRenderer::Draw_Line(FVector2D{pos.X, pos.Y + size.Y - 1}, FVector2D{pos.X + size.X - 1, pos.Y + size.Y - 1}, 1, FLinearColor{0.0f, 0.0f, 0.0f, 1.0f}); - PostRenderer::Draw_Line(FVector2D{pos.X, pos.Y}, FVector2D{pos.X, pos.Y + size.Y - 1}, 1, FLinearColor{0.0f, 0.0f, 0.0f, 1.0f}); - PostRenderer::Draw_Line(FVector2D{pos.X + size.X - 1, pos.Y}, FVector2D{pos.X + size.X - 1, pos.Y + size.Y - 1}, 1, FLinearColor{0.0f, 0.0f, 0.0f, 1.0f}); - } - - // Change color on click - bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, size); - if (isHovered) - { - if (Input::IsMouseClicked(0, elements_count, false)) - *original = color; - } - - return true; - } - inline bool ColorPicker(const char* name, FLinearColor* color) - { - elements_count++; - - float size = 25; - FVector2D padding = FVector2D{10, 10}; - FVector2D pos = FVector2D{menu_pos.X + padding.X + offset_x, menu_pos.Y + padding.Y + offset_y}; - if (sameLine) - { - pos.X = last_element_pos.X + last_element_size.X + padding.X; - pos.Y = last_element_pos.Y; - } - if (pushY) - { - pos.Y = pushYvalue; - pushY = false; - pushYvalue = 0.0f; - offset_y = pos.Y - menu_pos.Y; - } - bool isHovered = MouseInZone(FVector2D{pos.X, pos.Y}, FVector2D{size, size}); - - if (!sameLine) - offset_y += size + padding.Y; - - if (active_picker == elements_count) - { - hover_element = true; - - float sizePickerX = 250; - float sizePickerY = 250; - bool isHoveredPicker = MouseInZone(FVector2D{pos.X, pos.Y}, FVector2D{sizePickerX, sizePickerY - 60}); - - // Background - PostRenderer::drawFilledRect(FVector2D{pos.X, pos.Y}, sizePickerX, sizePickerY - 65, Colors::ColorPicker_Background); - - FVector2D pixelSize = FVector2D{sizePickerX / 12, sizePickerY / 12}; - - // 0 - { - ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{174 / 255.f, 235 / 255.f, 253 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{136 / 255.f, 225 / 255.f, 251 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{108 / 255.f, 213 / 255.f, 250 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{89 / 255.f, 175 / 255.f, 213 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{76 / 255.f, 151 / 255.f, 177 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{60 / 255.f, 118 / 255.f, 140 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{43 / 255.f, 85 / 255.f, 100 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{32 / 255.f, 62 / 255.f, 74 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{255 / 255.f, 255 / 255.f, 255 / 255.f, 1.0f}); - } - // 1 - { - ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{175 / 255.f, 205 / 255.f, 252 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{132 / 255.f, 179 / 255.f, 252 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{90 / 255.f, 152 / 255.f, 250 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{55 / 255.f, 120 / 255.f, 250 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{49 / 255.f, 105 / 255.f, 209 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{38 / 255.f, 83 / 255.f, 165 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{28 / 255.f, 61 / 255.f, 120 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{20 / 255.f, 43 / 255.f, 86 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{247 / 255.f, 247 / 255.f, 247 / 255.f, 1.0f}); - } - // 2 - { - ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{153 / 255.f, 139 / 255.f, 250 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{101 / 255.f, 79 / 255.f, 249 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{64 / 255.f, 50 / 255.f, 230 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{54 / 255.f, 38 / 255.f, 175 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{39 / 255.f, 31 / 255.f, 144 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{32 / 255.f, 25 / 255.f, 116 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{21 / 255.f, 18 / 255.f, 82 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{16 / 255.f, 13 / 255.f, 61 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{228 / 255.f, 228 / 255.f, 228 / 255.f, 1.0f}); - } - // 3 - { - ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{194 / 255.f, 144 / 255.f, 251 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{165 / 255.f, 87 / 255.f, 249 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{142 / 255.f, 57 / 255.f, 239 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{116 / 255.f, 45 / 255.f, 184 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{92 / 255.f, 37 / 255.f, 154 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{73 / 255.f, 29 / 255.f, 121 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{53 / 255.f, 21 / 255.f, 88 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{37 / 255.f, 15 / 255.f, 63 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{203 / 255.f, 203 / 255.f, 203 / 255.f, 1.0f}); - } - // 4 - { - ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{224 / 255.f, 162 / 255.f, 197 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{210 / 255.f, 112 / 255.f, 166 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{199 / 255.f, 62 / 255.f, 135 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{159 / 255.f, 49 / 255.f, 105 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{132 / 255.f, 41 / 255.f, 89 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{104 / 255.f, 32 / 255.f, 71 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{75 / 255.f, 24 / 255.f, 51 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{54 / 255.f, 14 / 255.f, 36 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{175 / 255.f, 175 / 255.f, 175 / 255.f, 1.0f}); - } - // 5 - { - ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{235 / 255.f, 175 / 255.f, 176 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{227 / 255.f, 133 / 255.f, 135 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{219 / 255.f, 87 / 255.f, 88 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{215 / 255.f, 50 / 255.f, 36 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{187 / 255.f, 25 / 255.f, 7 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{149 / 255.f, 20 / 255.f, 6 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{107 / 255.f, 14 / 255.f, 4 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{77 / 255.f, 9 / 255.f, 3 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{144 / 255.f, 144 / 255.f, 144 / 255.f, 1.0f}); - } - // 6 - { - ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{241 / 255.f, 187 / 255.f, 171 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{234 / 255.f, 151 / 255.f, 126 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{229 / 255.f, 115 / 255.f, 76 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{227 / 255.f, 82 / 255.f, 24 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{190 / 255.f, 61 / 255.f, 15 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{150 / 255.f, 48 / 255.f, 12 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{107 / 255.f, 34 / 255.f, 8 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{79 / 255.f, 25 / 255.f, 6 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{113 / 255.f, 113 / 255.f, 113 / 255.f, 1.0f}); - } - // 7 - { - ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{245 / 255.f, 207 / 255.f, 169 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{240 / 255.f, 183 / 255.f, 122 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{236 / 255.f, 159 / 255.f, 74 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{234 / 255.f, 146 / 255.f, 37 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{193 / 255.f, 111 / 255.f, 28 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{152 / 255.f, 89 / 255.f, 22 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{110 / 255.f, 64 / 255.f, 16 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{80 / 255.f, 47 / 255.f, 12 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{82 / 255.f, 82 / 255.f, 82 / 255.f, 1.0f}); - } - // 8 - { - ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{247 / 255.f, 218 / 255.f, 170 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{244 / 255.f, 200 / 255.f, 124 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{241 / 255.f, 182 / 255.f, 77 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{239 / 255.f, 174 / 255.f, 44 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{196 / 255.f, 137 / 255.f, 34 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{154 / 255.f, 108 / 255.f, 27 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{111 / 255.f, 77 / 255.f, 19 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{80 / 255.f, 56 / 255.f, 14 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{54 / 255.f, 54 / 255.f, 54 / 255.f, 1.0f}); - } - // 9 - { - ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{254 / 255.f, 243 / 255.f, 187 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{253 / 255.f, 237 / 255.f, 153 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{253 / 255.f, 231 / 255.f, 117 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{254 / 255.f, 232 / 255.f, 85 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{242 / 255.f, 212 / 255.f, 53 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{192 / 255.f, 169 / 255.f, 42 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{138 / 255.f, 120 / 255.f, 30 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{101 / 255.f, 87 / 255.f, 22 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{29 / 255.f, 29 / 255.f, 29 / 255.f, 1.0f}); - } - // 10 - { - ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{247 / 255.f, 243 / 255.f, 185 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{243 / 255.f, 239 / 255.f, 148 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{239 / 255.f, 232 / 255.f, 111 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{235 / 255.f, 229 / 255.f, 76 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{208 / 255.f, 200 / 255.f, 55 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{164 / 255.f, 157 / 255.f, 43 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{118 / 255.f, 114 / 255.f, 31 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{86 / 255.f, 82 / 255.f, 21 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{9 / 255.f, 9 / 255.f, 9 / 255.f, 1.0f}); - } - // 11 - { - ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{218 / 255.f, 232 / 255.f, 182 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{198 / 255.f, 221 / 255.f, 143 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{181 / 255.f, 210 / 255.f, 103 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{154 / 255.f, 186 / 255.f, 76 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{130 / 255.f, 155 / 255.f, 64 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{102 / 255.f, 121 / 255.f, 50 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{74 / 255.f, 88 / 255.f, 36 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{54 / 255.f, 64 / 255.f, 26 / 255.f, 1.0f}); - ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{0 / 255.f, 0 / 255.f, 0 / 255.f, 1.0f}); - } - - if (isHoveredPicker) - { - if (Input::IsMouseClicked(0, elements_count, false)) - { - } - } - else - { - if (Input::IsMouseClicked(0, elements_count, false)) - { - active_picker = -1; - } - } - } - else - { - // Bg - drawFilledRect(FVector2D{pos.X, pos.Y}, size, size, Colors::MainColor); - if (isHovered) hover_element = true; - - // Color - drawFilledRect(FVector2D{pos.X + 4, pos.Y + 4}, size - 8, size - 8, *color); - - // Text - FVector2D textPos = FVector2D{pos.X + size + 5.0f, pos.Y + size / 2}; - TextLeft(name, textPos, Colors::Text, false); - - if (isHovered && Input::IsMouseClicked(0, elements_count, false)) - { - saved_color = *color; - active_picker = elements_count; - } - } - - sameLine = false; - last_element_pos = pos; - if (first_element_pos.X == 0.0f) - first_element_pos = pos; - - // Report a change when the live color differs from the value saved when the picker opened. - return active_picker != elements_count && (color->R != saved_color.R || color->G != saved_color.G || color->B != saved_color.B); - } - - /// Drain the deferred draw queue (dropdowns / swatches) so they land on top. Call at frame end. - inline void Render() - { - for (int i = 0; i < 128; i++) - { - if (PostRenderer::drawlist[i].type != -1) - { - // Filled Rect - if (PostRenderer::drawlist[i].type == 1) - { - ZeroGUI::drawFilledRect(PostRenderer::drawlist[i].pos, PostRenderer::drawlist[i].size.X, PostRenderer::drawlist[i].size.Y, PostRenderer::drawlist[i].color); - } - // TextLeft - else if (PostRenderer::drawlist[i].type == 2) - { - ZeroGUI::TextLeft(PostRenderer::drawlist[i].name, PostRenderer::drawlist[i].pos, PostRenderer::drawlist[i].color, PostRenderer::drawlist[i].outline); - } - // TextCenter - else if (PostRenderer::drawlist[i].type == 3) - { - ZeroGUI::TextCenter(PostRenderer::drawlist[i].name, PostRenderer::drawlist[i].pos, PostRenderer::drawlist[i].color, PostRenderer::drawlist[i].outline); - } - // Draw_Line - else if (PostRenderer::drawlist[i].type == 4) - { - Draw_Line(PostRenderer::drawlist[i].from, PostRenderer::drawlist[i].to, PostRenderer::drawlist[i].thickness, PostRenderer::drawlist[i].color); - } - - PostRenderer::drawlist[i].type = -1; - } - } - } -} // namespace ZeroGUI diff --git a/Internal/menu/canvas/ZeroInput.h b/Internal/menu/canvas/ZeroInput.h deleted file mode 100644 index 6e7a69b..0000000 --- a/Internal/menu/canvas/ZeroInput.h +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -/// @file -/// @brief Win32 mouse/keyboard polling for the Canvas (ZeroGUI) menu backend. Sampled once per -/// frame by Handle(); the widgets read the per-button state and edge-detect clicks per widget id. -/// Everything is `inline` because this header is pulled into multiple translation units. - -#include - -namespace ZeroGUI -{ - namespace Input - { - inline bool mouseDown[5]; - inline bool mouseDownAlready[256]; - - inline bool keysDown[256]; - inline bool keysDownAlready[256]; - - inline bool IsAnyMouseDown() - { - if (mouseDown[0]) return true; - if (mouseDown[1]) return true; - if (mouseDown[2]) return true; - if (mouseDown[3]) return true; - if (mouseDown[4]) return true; - - return false; - } - - /// Rising-edge (or, with @p repeat, level) detection of button @p button for widget @p element_id. - inline bool IsMouseClicked(int button, int element_id, bool repeat) - { - if (mouseDown[button]) - { - if (!mouseDownAlready[element_id]) - { - mouseDownAlready[element_id] = true; - return true; - } - if (repeat) - return true; - } - else - { - mouseDownAlready[element_id] = false; - } - return false; - } - - inline bool IsKeyPressed(int key, bool repeat) - { - if (keysDown[key]) - { - if (!keysDownAlready[key]) - { - keysDownAlready[key] = true; - return true; - } - if (repeat) - return true; - } - else - { - keysDownAlready[key] = false; - } - return false; - } - - /// Sample every mouse button and key once per frame (high bit = currently down). - inline void Handle() - { - mouseDown[0] = (GetAsyncKeyState(VK_LBUTTON) & 0x8000) != 0; - mouseDown[1] = (GetAsyncKeyState(VK_RBUTTON) & 0x8000) != 0; - mouseDown[2] = (GetAsyncKeyState(VK_MBUTTON) & 0x8000) != 0; - mouseDown[3] = (GetAsyncKeyState(VK_XBUTTON1) & 0x8000) != 0; - mouseDown[4] = (GetAsyncKeyState(VK_XBUTTON2) & 0x8000) != 0; - - for (int i = 0; i < 256; i++) - keysDown[i] = (GetAsyncKeyState(i) & 0x8000) != 0; - } - } // namespace Input -} // namespace ZeroGUI diff --git a/Internal/menu/canvas/components/Button.h b/Internal/menu/canvas/components/Button.h new file mode 100644 index 0000000..7dcd98f --- /dev/null +++ b/Internal/menu/canvas/components/Button.h @@ -0,0 +1,26 @@ +#pragma once + +/// @file +/// @brief Button widget: a clickable labelled button; returns true on the click frame. + +#include "Core.h" + +namespace UCanvasGUI +{ + inline bool Button(const char* name, FVector2D size) + { + elementCount++; + + const FVector2D pos = NextPos({5, 10}, size.Y + 10.0f); + const bool isHovered = MouseInZone(pos, size); + + DrawRect(pos, size.X, size.Y, Colors::Accent); + if (isHovered) + elementHovered = true; + + TextCenter(name, FVector2D{pos.X + size.X / 2, pos.Y + size.Y / 2}, Colors::Text, false); + + EndElement(pos, size); + return isHovered && Input::IsMouseClicked(0, elementCount, false); + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/Checkbox.h b/Internal/menu/canvas/components/Checkbox.h new file mode 100644 index 0000000..9e28e20 --- /dev/null +++ b/Internal/menu/canvas/components/Checkbox.h @@ -0,0 +1,35 @@ +#pragma once + +/// @file +/// @brief Checkbox widget: a labelled boolean toggle with a filled mark when enabled. + +#include "Core.h" + +namespace UCanvasGUI +{ + inline bool Checkbox(const char* name, bool* value) + { + elementCount++; + + constexpr float box = 18.0f; + const FVector2D pos = NextPos({10, 10}, box + 10.0f); + const bool isHovered = MouseInZone(pos, FVector2D{box, box}); + + DrawRect(pos, box, box, Colors::Accent); + if (isHovered) + elementHovered = true; + if (*value) + DrawRect(FVector2D{pos.X + 3, pos.Y + 3}, box - 6, box - 6, Colors::Enabled); + + TextLeft(name, FVector2D{pos.X + box + 5.0f, pos.Y + box / 2}, Colors::Text, false); + + EndElement(pos); + + if (isHovered && Input::IsMouseClicked(0, elementCount, false)) + { + *value = !*value; + return true; + } + return false; + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/Child.h b/Internal/menu/canvas/components/Child.h new file mode 100644 index 0000000..cabe736 --- /dev/null +++ b/Internal/menu/canvas/components/Child.h @@ -0,0 +1,25 @@ +#pragma once + +/// @file +/// @brief Child rich widget: BeginChild / EndChild — a bordered region marker (no hard clip). + +#include "Core.h" + +namespace UCanvasGUI +{ + /// Bordered region marker: draws a frame of height @p h; content flows inside (no hard clip). + inline float childStartY = 0.0f; + inline bool BeginChild(const char* /*id*/, float w, float h) + { + const FVector2D pos{menuPos.X + 10.0f + offsetX, menuPos.Y + 10.0f + offsetY}; + const float width = w > 0.0f ? w : 320.0f; + DrawRect(pos, width, h, Colors::Background); + childStartY = offsetY; + return true; + } + inline void EndChild(float h) + { + const float end = childStartY + h + 10.0f; + if (offsetY < end) offsetY = end; + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/CollapsingHeader.h b/Internal/menu/canvas/components/CollapsingHeader.h new file mode 100644 index 0000000..d38f510 --- /dev/null +++ b/Internal/menu/canvas/components/CollapsingHeader.h @@ -0,0 +1,33 @@ +#pragma once + +/// @file +/// @brief CollapsingHeader rich widget: a persistent collapsible section header (also backs TreeNode). + +#include "Core.h" + +namespace UCanvasGUI +{ + inline bool headerOpen[256]; ///< per-id open state for CollapsingHeader / TreeNode + /// Collapsible section header; returns whether it is open (persistent per id). + inline bool CollapsingHeader(const char* name) + { + elementCount++; + const int id = elementCount; + + const FVector2D size{260.0f, 22.0f}; + const FVector2D pos = NextPos(FVector2D{10, 10}, size.Y + 8.0f); + const bool isHovered = MouseInZone(pos, size); + + DrawRect(pos, size.X, size.Y, Colors::Frame); + if (isHovered) elementHovered = true; + + std::string label = std::string(headerOpen[id] ? "- " : "+ ") + name; + TextLeft(label.c_str(), FVector2D{pos.X + 4.0f, pos.Y + size.Y / 2}, Colors::Text, false); + + if (isHovered && Input::IsMouseClicked(0, elementCount, false)) + headerOpen[id] = !headerOpen[id]; + + EndElement(pos, size); + return headerOpen[id]; + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/ColorPicker.h b/Internal/menu/canvas/components/ColorPicker.h new file mode 100644 index 0000000..4bd34e7 --- /dev/null +++ b/Internal/menu/canvas/components/ColorPicker.h @@ -0,0 +1,221 @@ +#pragma once + +/// @file +/// @brief ColorPicker widget: a swatch that opens a 12x9 palette grid (ColorPixel cells), drawn via PostRenderer. + +#include "Core.h" + +namespace UCanvasGUI +{ + inline int activePicker = -1; + inline FLinearColor savedColor; + /// One swatch of the open palette grid: draws @p color, outlines it while it's the current pick, + /// and writes it into @p original when clicked. + inline void ColorPixel(FVector2D pos, FVector2D size, FLinearColor* original, FLinearColor color) + { + PostRenderer::DrawRect(pos, size.X, size.Y, color); + + if (original->R == color.R && original->G == color.G && original->B == color.B) + { + constexpr FLinearColor outline{0.0f, 0.0f, 0.0f, 1.0f}; + PostRenderer::DrawLine(FVector2D{pos.X, pos.Y}, FVector2D{pos.X + size.X - 1, pos.Y}, 1, outline); + PostRenderer::DrawLine(FVector2D{pos.X, pos.Y + size.Y - 1}, FVector2D{pos.X + size.X - 1, pos.Y + size.Y - 1}, 1, outline); + PostRenderer::DrawLine(FVector2D{pos.X, pos.Y}, FVector2D{pos.X, pos.Y + size.Y - 1}, 1, outline); + PostRenderer::DrawLine(FVector2D{pos.X + size.X - 1, pos.Y}, FVector2D{pos.X + size.X - 1, pos.Y + size.Y - 1}, 1, outline); + } + + if (MouseInZone(pos, size) && Input::IsMouseClicked(0, elementCount, false)) + *original = color; + } + inline bool ColorPicker(const char* name, FLinearColor* color) + { + elementCount++; + + constexpr float box = 25.0f; + const FVector2D pos = NextPos({10, 10}, box + 10.0f); + const bool isHovered = MouseInZone(pos, FVector2D{box, box}); + + if (activePicker == elementCount) + { + elementHovered = true; + + float sizePickerX = 250; + float sizePickerY = 250; + bool isHoveredPicker = MouseInZone(FVector2D{pos.X, pos.Y}, FVector2D{sizePickerX, sizePickerY - 60}); + + // Background + PostRenderer::DrawRect(FVector2D{pos.X, pos.Y}, sizePickerX, sizePickerY - 65, Colors::Background); + + FVector2D pixelSize = FVector2D{sizePickerX / 12, sizePickerY / 12}; + + // 0 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{174 / 255.f, 235 / 255.f, 253 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{136 / 255.f, 225 / 255.f, 251 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{108 / 255.f, 213 / 255.f, 250 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{89 / 255.f, 175 / 255.f, 213 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{76 / 255.f, 151 / 255.f, 177 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{60 / 255.f, 118 / 255.f, 140 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{43 / 255.f, 85 / 255.f, 100 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{32 / 255.f, 62 / 255.f, 74 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 0, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{255 / 255.f, 255 / 255.f, 255 / 255.f, 1.0f}); + } + // 1 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{175 / 255.f, 205 / 255.f, 252 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{132 / 255.f, 179 / 255.f, 252 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{90 / 255.f, 152 / 255.f, 250 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{55 / 255.f, 120 / 255.f, 250 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{49 / 255.f, 105 / 255.f, 209 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{38 / 255.f, 83 / 255.f, 165 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{28 / 255.f, 61 / 255.f, 120 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{20 / 255.f, 43 / 255.f, 86 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 1, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{247 / 255.f, 247 / 255.f, 247 / 255.f, 1.0f}); + } + // 2 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{153 / 255.f, 139 / 255.f, 250 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{101 / 255.f, 79 / 255.f, 249 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{64 / 255.f, 50 / 255.f, 230 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{54 / 255.f, 38 / 255.f, 175 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{39 / 255.f, 31 / 255.f, 144 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{32 / 255.f, 25 / 255.f, 116 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{21 / 255.f, 18 / 255.f, 82 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{16 / 255.f, 13 / 255.f, 61 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 2, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{228 / 255.f, 228 / 255.f, 228 / 255.f, 1.0f}); + } + // 3 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{194 / 255.f, 144 / 255.f, 251 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{165 / 255.f, 87 / 255.f, 249 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{142 / 255.f, 57 / 255.f, 239 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{116 / 255.f, 45 / 255.f, 184 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{92 / 255.f, 37 / 255.f, 154 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{73 / 255.f, 29 / 255.f, 121 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{53 / 255.f, 21 / 255.f, 88 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{37 / 255.f, 15 / 255.f, 63 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 3, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{203 / 255.f, 203 / 255.f, 203 / 255.f, 1.0f}); + } + // 4 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{224 / 255.f, 162 / 255.f, 197 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{210 / 255.f, 112 / 255.f, 166 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{199 / 255.f, 62 / 255.f, 135 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{159 / 255.f, 49 / 255.f, 105 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{132 / 255.f, 41 / 255.f, 89 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{104 / 255.f, 32 / 255.f, 71 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{75 / 255.f, 24 / 255.f, 51 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{54 / 255.f, 14 / 255.f, 36 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 4, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{175 / 255.f, 175 / 255.f, 175 / 255.f, 1.0f}); + } + // 5 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{235 / 255.f, 175 / 255.f, 176 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{227 / 255.f, 133 / 255.f, 135 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{219 / 255.f, 87 / 255.f, 88 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{215 / 255.f, 50 / 255.f, 36 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{187 / 255.f, 25 / 255.f, 7 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{149 / 255.f, 20 / 255.f, 6 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{107 / 255.f, 14 / 255.f, 4 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{77 / 255.f, 9 / 255.f, 3 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 5, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{144 / 255.f, 144 / 255.f, 144 / 255.f, 1.0f}); + } + // 6 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{241 / 255.f, 187 / 255.f, 171 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{234 / 255.f, 151 / 255.f, 126 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{229 / 255.f, 115 / 255.f, 76 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{227 / 255.f, 82 / 255.f, 24 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{190 / 255.f, 61 / 255.f, 15 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{150 / 255.f, 48 / 255.f, 12 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{107 / 255.f, 34 / 255.f, 8 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{79 / 255.f, 25 / 255.f, 6 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 6, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{113 / 255.f, 113 / 255.f, 113 / 255.f, 1.0f}); + } + // 7 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{245 / 255.f, 207 / 255.f, 169 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{240 / 255.f, 183 / 255.f, 122 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{236 / 255.f, 159 / 255.f, 74 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{234 / 255.f, 146 / 255.f, 37 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{193 / 255.f, 111 / 255.f, 28 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{152 / 255.f, 89 / 255.f, 22 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{110 / 255.f, 64 / 255.f, 16 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{80 / 255.f, 47 / 255.f, 12 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 7, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{82 / 255.f, 82 / 255.f, 82 / 255.f, 1.0f}); + } + // 8 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{247 / 255.f, 218 / 255.f, 170 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{244 / 255.f, 200 / 255.f, 124 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{241 / 255.f, 182 / 255.f, 77 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{239 / 255.f, 174 / 255.f, 44 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{196 / 255.f, 137 / 255.f, 34 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{154 / 255.f, 108 / 255.f, 27 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{111 / 255.f, 77 / 255.f, 19 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{80 / 255.f, 56 / 255.f, 14 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 8, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{54 / 255.f, 54 / 255.f, 54 / 255.f, 1.0f}); + } + // 9 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{254 / 255.f, 243 / 255.f, 187 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{253 / 255.f, 237 / 255.f, 153 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{253 / 255.f, 231 / 255.f, 117 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{254 / 255.f, 232 / 255.f, 85 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{242 / 255.f, 212 / 255.f, 53 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{192 / 255.f, 169 / 255.f, 42 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{138 / 255.f, 120 / 255.f, 30 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{101 / 255.f, 87 / 255.f, 22 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 9, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{29 / 255.f, 29 / 255.f, 29 / 255.f, 1.0f}); + } + // 10 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{247 / 255.f, 243 / 255.f, 185 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{243 / 255.f, 239 / 255.f, 148 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{239 / 255.f, 232 / 255.f, 111 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{235 / 255.f, 229 / 255.f, 76 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{208 / 255.f, 200 / 255.f, 55 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{164 / 255.f, 157 / 255.f, 43 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{118 / 255.f, 114 / 255.f, 31 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{86 / 255.f, 82 / 255.f, 21 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 10, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{9 / 255.f, 9 / 255.f, 9 / 255.f, 1.0f}); + } + // 11 + { + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 0}, pixelSize, color, FLinearColor{218 / 255.f, 232 / 255.f, 182 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 1}, pixelSize, color, FLinearColor{198 / 255.f, 221 / 255.f, 143 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 2}, pixelSize, color, FLinearColor{181 / 255.f, 210 / 255.f, 103 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 3}, pixelSize, color, FLinearColor{154 / 255.f, 186 / 255.f, 76 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 4}, pixelSize, color, FLinearColor{130 / 255.f, 155 / 255.f, 64 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 5}, pixelSize, color, FLinearColor{102 / 255.f, 121 / 255.f, 50 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 6}, pixelSize, color, FLinearColor{74 / 255.f, 88 / 255.f, 36 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 7}, pixelSize, color, FLinearColor{54 / 255.f, 64 / 255.f, 26 / 255.f, 1.0f}); + ColorPixel(FVector2D{pos.X + pixelSize.X * 11, pos.Y + pixelSize.Y * 8}, pixelSize, color, FLinearColor{0 / 255.f, 0 / 255.f, 0 / 255.f, 1.0f}); + } + + // Consume the click; only one outside the grid closes the picker. + if (Input::IsMouseClicked(0, elementCount, false) && !isHoveredPicker) + activePicker = -1; + } + else + { + // Closed swatch: accent border, the current color inset, and the label. + DrawRect(pos, box, box, Colors::Accent); + if (isHovered) + elementHovered = true; + DrawRect(FVector2D{pos.X + 4, pos.Y + 4}, box - 8, box - 8, *color); + TextLeft(name, FVector2D{pos.X + box + 5.0f, pos.Y + box / 2}, Colors::Text, false); + + if (isHovered && Input::IsMouseClicked(0, elementCount, false)) + { + savedColor = *color; + activePicker = elementCount; + } + } + + EndElement(pos); + + // Report a change when the live color differs from the value saved when the picker opened. + return activePicker != elementCount && (color->R != savedColor.R || color->G != savedColor.G || color->B != savedColor.B); + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/Combo.h b/Internal/menu/canvas/components/Combo.h new file mode 100644 index 0000000..597cd24 --- /dev/null +++ b/Internal/menu/canvas/components/Combo.h @@ -0,0 +1,35 @@ +#pragma once + +/// @file +/// @brief Combo rich widgets: BeginCombo / EndCombo — a combo whose item list the caller draws inline. + +#include "Core.h" + +namespace UCanvasGUI +{ + inline bool comboOpen[256]; ///< per-id open state for BeginCombo + /// Combo box that opens an inline item list (drawn by the caller via Selectable); returns open. + inline bool BeginCombo(const char* name, const char* preview, float width) + { + elementCount++; + const int id = elementCount; + + const FVector2D size{width, 22.0f}; + const FVector2D pos = NextPos(FVector2D{10, 10}, size.Y + 10.0f); + const bool isHovered = MouseInZone(pos, size); + + DrawRect(pos, size.X, size.Y, Colors::Frame); + if (isHovered) elementHovered = true; + + if (preview && preview[0]) TextLeft(preview, FVector2D{pos.X + 4.0f, pos.Y + size.Y / 2}, Colors::Text, false); + if (name && name[0] && name[0] != '#') + TextLeft(name, FVector2D{pos.X + size.X + 5.0f, pos.Y + size.Y / 2}, Colors::Text, false); + + if (isHovered && Input::IsMouseClicked(0, elementCount, false)) + comboOpen[id] = !comboOpen[id]; + + EndElement(pos, size); + return comboOpen[id]; + } + inline void EndCombo() {} ///< items drew inline; nothing to close +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/Combobox.h b/Internal/menu/canvas/components/Combobox.h new file mode 100644 index 0000000..f16c533 --- /dev/null +++ b/Internal/menu/canvas/components/Combobox.h @@ -0,0 +1,73 @@ +#pragma once + +/// @file +/// @brief Combobox widget: a dropdown over a string array. The open item list is queued through +/// PostRenderer so it replays on top of the widgets drawn below it. + +#include "Core.h" + +namespace UCanvasGUI +{ + inline bool comboboxOpen[256]; ///< per-id open state for the array Combobox + + /// Dropdown combo over @p count option strings; writes the picked index to @p value. + /// @return true on the frame the selection changes. + inline bool Combobox(const char* name, FVector2D size, int* value, const char* const* items, int count) + { + elementCount++; + const int id = elementCount; + bool changed = false; + + const FVector2D pos = NextPos({5, 10}, size.Y + 10.0f); + const bool isHovered = MouseInZone(pos, size); + + DrawRect(pos, size.X, size.Y, Colors::Frame); + if (isHovered || comboboxOpen[id]) + elementHovered = true; + + TextLeft(name, FVector2D{pos.X + size.X + 5.0f, pos.Y + size.Y / 2}, Colors::Text, false); + + // The selected label sits on the closed box; the full list draws (on top) only when open. + bool overItem = false; + FVector2D itemPos = pos; + for (int num = 0; num < count; num++) + { + const char* item = items[num]; + if (num == *value) + TextCenter(item, FVector2D{pos.X + size.X / 2, pos.Y + size.Y / 2}, Colors::Text, false); + + if (!comboboxOpen[id]) + continue; + + itemPos.Y += 25.0f; + const bool overThis = MouseInZone(itemPos, FVector2D{size.X, 25.0f}); + overItem = overItem || overThis; + + PostRenderer::DrawRect(itemPos, size.X, 25.0f, Colors::Frame); + if (overThis) + { + elementHovered = true; + if (Input::IsMouseClicked(0, elementCount, false)) + { + *value = num; + changed = true; + comboboxOpen[id] = false; + } + } + PostRenderer::TextLeft(item, FVector2D{itemPos.X + 5.0f, itemPos.Y + 15.0f}, Colors::Text, false); + } + + EndElement(pos, size); + + // Toggle open on the box; close when clicking away from both the box and the list. + if (Input::IsMouseClicked(0, elementCount, false)) + { + if (isHovered) + comboboxOpen[id] = !comboboxOpen[id]; + else if (!overItem) + comboboxOpen[id] = false; + } + + return changed; + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/Core.h b/Internal/menu/canvas/components/Core.h new file mode 100644 index 0000000..edfbb5a --- /dev/null +++ b/Internal/menu/canvas/components/Core.h @@ -0,0 +1,245 @@ +#pragma once + +/// @file +/// @brief UCanvasGUI core: the shared immediate-mode foundation for the UE-canvas menu backend — the +/// deferred draw queue (PostRenderer), the single-window layout state, the Render:: draw primitives +/// (routed to whichever backend is active — canvas or the ImGui recorder), the software cursor, the +/// layout-cursor helpers (SameLine/PushNextElementY/NextPos/ +/// EndElement), and the frame-end Render() drain. Every widget component in this folder includes it. +/// Everything is `inline` — this header is pulled into multiple translation units. + +#include +#include +#include +#include +#include + +#include "../../../utils/Input.h" +#include "../../../ue/Engine.h" +#include "../../../render/Render.h" +#include "../Colors.h" + +namespace UCanvasGUI +{ + /// Deferred draw queue: pop-ups (combo dropdowns, color-picker swatches) enqueue here so they + /// replay last, on top of the widgets drawn earlier in the frame. Render() drains it. + namespace PostRenderer + { + /// One queued primitive. @ref kind selects which fields are meaningful. + struct Command + { + enum Kind + { + None = -1, + Rect, + TextLeft, + TextCenter, + Line + }; + + Kind kind = None; + FVector2D pos; + FVector2D size; + FLinearColor color; + const char* text; + bool outline; + + FVector2D from; + FVector2D to; + int thickness; + }; + + inline constexpr int Capacity = 128; + inline Command queue[Capacity]; + + /// Reserve the next free slot, or nullptr when the queue is full. + inline Command* Next() + { + for (Command& cmd : queue) + if (cmd.kind == Command::None) + return &cmd; + return nullptr; + } + + inline void DrawRect(FVector2D pos, float w, float h, FLinearColor color) + { + if (Command* cmd = Next()) + *cmd = {Command::Rect, pos, FVector2D{w, h}, color}; + } + inline void TextLeft(const char* text, FVector2D pos, FLinearColor color, bool outline) + { + if (Command* cmd = Next()) + { + cmd->kind = Command::TextLeft; + cmd->text = text; + cmd->pos = pos; + cmd->outline = outline; + cmd->color = color; + } + } + inline void TextCenter(const char* text, FVector2D pos, FLinearColor color, bool outline) + { + if (Command* cmd = Next()) + { + cmd->kind = Command::TextCenter; + cmd->text = text; + cmd->pos = pos; + cmd->outline = outline; + cmd->color = color; + } + } + inline void DrawLine(FVector2D from, FVector2D to, int thickness, FLinearColor color) + { + if (Command* cmd = Next()) + { + cmd->kind = Command::Line; + cmd->from = from; + cmd->to = to; + cmd->thickness = thickness; + cmd->color = color; + } + } + } // namespace PostRenderer + + // --- Immediate-mode layout state (single window, rebuilt every frame). --- + inline bool elementHovered = false; ///< set when the cursor is over a widget (suppresses window drag) + inline FVector2D menuPos = {0, 0}; ///< top-left of the window this frame + inline float offsetX = 0.0f; ///< running layout cursor, relative to menuPos + inline float offsetY = 0.0f; ///< running layout cursor, relative to menuPos + + inline FVector2D lastElementPos = {0, 0}; ///< last widget's position (for SameLine) + inline FVector2D lastElementSize = {0, 0}; ///< last widget's size (for SameLine) + + inline int activeElement = -1; ///< elementCount of the widget currently being dragged (-1 = none) + inline int elementCount = 0; ///< per-frame widget counter, doubles as each widget's id + + inline bool sameLine = false; ///< when set, the next widget is placed to the right of the last + + inline bool pushY = false; ///< when set, the next widget's Y is forced to pushYValue + inline float pushYValue = 0.0f; ///< absolute Y forced by PushNextElementY + + /// The cursor position in client (window) pixels. + inline FVector2D CursorPos() + { + POINT cursor; + GetCursorPos(&cursor); + ScreenToClient(GetActiveWindow(), &cursor); + return FVector2D{(float)cursor.x, (float)cursor.y}; + } + + /// Whether the cursor is inside the [pos, pos + size] rectangle. + inline bool MouseInZone(FVector2D pos, FVector2D size) + { + FVector2D cursor = CursorPos(); + return cursor.X > pos.X && cursor.Y > pos.Y && cursor.X < pos.X + size.X && cursor.Y < pos.Y + size.Y; + } + + // --- Draw primitives (draw through the active Render backend, not the canvas directly). --- + inline void DrawLine(FVector2D from, FVector2D to, int thickness, FLinearColor color) + { + Render::Line(from, to, (float)thickness, color); + } + inline void DrawRect(FVector2D pos, float w, float h, FLinearColor color) + { + Render::RectFilled(pos, FVector2D{pos.X + w, pos.Y + h}, color); + } + inline void DrawCircle(FVector2D pos, float radius, FLinearColor color) + { + Render::CircleFilled(pos, radius, color); + } + inline void TextLeft(const char* text, FVector2D pos, FLinearColor color, bool /*outline*/) + { + Render::Text(pos, std::string(text), 0.97f, color, false); + } + inline void TextCenter(const char* text, FVector2D pos, FLinearColor color, bool /*outline*/) + { + Render::Text(pos, std::string(text), 0.97f, color, true); + } + + /// Software arrow cursor built from line segments (the Canvas menu owns its cursor; it does not + /// borrow ImGui's software cursor, keeping the two backends fully decoupled). + inline void DrawCursor() + { + constexpr FLinearColor tint{0.30f, 0.30f, 0.80f, 1.0f}; + const FVector2D origin = CursorPos(); + + // Fan of segments from the tip, sweeping the arrow's inner edge. + for (int x = 35, y = 10; y <= 30; x = x > 15 ? x - 1 : 15, y++) + DrawLine(origin, FVector2D{origin.X + x, origin.Y + y}, 1, tint); + + DrawLine(FVector2D{origin.X + 35, origin.Y + 10}, FVector2D{origin.X + 15, origin.Y + 30}, 1, tint); + } + + // --- Layout cursor helpers. --- + inline void SameLine() + { + sameLine = true; + } + inline void PushNextElementY(float y, bool fromLastElement = true) + { + pushY = true; + pushYValue = fromLastElement ? lastElementPos.Y + lastElementSize.Y + y : y; + } + + /// Resolve the next widget's top-left, honoring SameLine / PushNextElementY, and (unless SameLine) + /// advance the vertical cursor by @p advance. @p sameLineY offsets the Y when placed on the same + /// line (used by widgets that vertically center against the previous element). + inline FVector2D NextPos(FVector2D padding, float advance, float sameLineY = 0.0f) + { + FVector2D pos{menuPos.X + padding.X + offsetX, menuPos.Y + padding.Y + offsetY}; + if (sameLine) + { + pos.X = lastElementPos.X + lastElementSize.X + padding.X; + pos.Y = lastElementPos.Y + sameLineY; + } + if (pushY) + { + pos.Y = pushYValue; + pushY = false; + pushYValue = 0.0f; + offsetY = pos.Y - menuPos.Y; + } + if (!sameLine) + offsetY += advance; + return pos; + } + + /// Close the current widget, recording its position (and, in the two-arg form, its size) for the + /// next SameLine, and clearing the SameLine request. + inline void EndElement(FVector2D pos) + { + sameLine = false; + lastElementPos = pos; + } + inline void EndElement(FVector2D pos, FVector2D size) + { + lastElementSize = size; + EndElement(pos); + } + + /// Drain the deferred draw queue (dropdowns / swatches) so they land on top. Call at frame end. + inline void Render() + { + for (PostRenderer::Command& cmd : PostRenderer::queue) + { + switch (cmd.kind) + { + case PostRenderer::Command::Rect: + DrawRect(cmd.pos, cmd.size.X, cmd.size.Y, cmd.color); + break; + case PostRenderer::Command::TextLeft: + TextLeft(cmd.text, cmd.pos, cmd.color, cmd.outline); + break; + case PostRenderer::Command::TextCenter: + TextCenter(cmd.text, cmd.pos, cmd.color, cmd.outline); + break; + case PostRenderer::Command::Line: + DrawLine(cmd.from, cmd.to, cmd.thickness, cmd.color); + break; + default: + continue; + } + cmd.kind = PostRenderer::Command::None; + } + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/Hotkey.h b/Internal/menu/canvas/components/Hotkey.h new file mode 100644 index 0000000..fa96dd3 --- /dev/null +++ b/Internal/menu/canvas/components/Hotkey.h @@ -0,0 +1,109 @@ +#pragma once + +/// @file +/// @brief Hotkey widget: a key-capture button that, once armed, binds the next pressed key. Includes +/// VirtualKeyCodeToString, which renders a virtual-key code as its display name. + +#include "Core.h" + +namespace UCanvasGUI +{ + inline int activeHotkey = -1; ///< elementCount of the hotkey currently capturing a key (-1 = none) + inline bool alreadyPressed = false; ///< guards against binding the mouse click that armed capture + + /// Human-readable name for a virtual-key code (mouse buttons and the extended-key set handled). + inline std::string VirtualKeyCodeToString(UCHAR virtualKey) + { + switch (virtualKey) + { + case VK_LBUTTON: return "MOUSE0"; + case VK_RBUTTON: return "MOUSE1"; + case VK_MBUTTON: return "MBUTTON"; + case VK_XBUTTON1: return "XBUTTON1"; + case VK_XBUTTON2: return "XBUTTON2"; + } + + UINT scanCode = MapVirtualKey(virtualKey, MAPVK_VK_TO_VSC); + switch (virtualKey) + { + case VK_LEFT: + case VK_UP: + case VK_RIGHT: + case VK_DOWN: + case VK_RCONTROL: + case VK_RMENU: + case VK_LWIN: + case VK_RWIN: + case VK_APPS: + case VK_PRIOR: + case VK_NEXT: + case VK_END: + case VK_HOME: + case VK_INSERT: + case VK_DELETE: + case VK_DIVIDE: + case VK_NUMLOCK: + scanCode |= KF_EXTENDED; + } + + char name[128]; + GetKeyNameTextA(scanCode << 16, name, sizeof(name)); + return name; + } + + inline bool Hotkey(const char* name, FVector2D size, int* key) + { + elementCount++; + bool changed = false; + + const FVector2D pos = NextPos({5, 10}, size.Y + 10.0f, lastElementSize.Y / 2 - size.Y / 2); + const bool isHovered = MouseInZone(pos, size); + + DrawRect(pos, size.X, size.Y, Colors::Accent); + if (isHovered) + elementHovered = true; + + const FVector2D textPos{pos.X + size.X / 2, pos.Y + size.Y / 2}; + if (activeHotkey == elementCount) + { + TextCenter("[Press Key]", textPos, Colors::Text, false); + + if (!Input::IsAnyMouseDown()) + alreadyPressed = false; + + // Bind the highest key currently held (once the arming click has been released). + if (!alreadyPressed) + for (int code = 0; code < 255; code++) + if (GetAsyncKeyState(code) & 0x8000) + { + *key = code; + changed = true; + activeHotkey = -1; + } + } + else + { + TextCenter(VirtualKeyCodeToString(*key).c_str(), textPos, Colors::Text, false); + + if (Input::IsMouseClicked(0, elementCount, false)) + { + if (isHovered) + { + alreadyPressed = true; + activeHotkey = elementCount; + + // Drain the keys down right now so the arming click/keys aren't captured as the bind. + for (int code = 0; code < 255; code++) + GetAsyncKeyState(code); + } + else + { + activeHotkey = -1; + } + } + } + + EndElement(pos, size); + return changed; + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/Selectable.h b/Internal/menu/canvas/components/Selectable.h new file mode 100644 index 0000000..57cc0a7 --- /dev/null +++ b/Internal/menu/canvas/components/Selectable.h @@ -0,0 +1,29 @@ +#pragma once + +/// @file +/// @brief Selectable rich widget: a clickable text row highlighted when hovered/selected. + +#include "Core.h" + +namespace UCanvasGUI +{ + /// Clickable text row (no background unless hovered/selected); returns true on click. + inline bool Selectable(const char* name, bool selected, float width) + { + elementCount++; + + const FVector2D size{width, 20.0f}; + const FVector2D pos = NextPos(FVector2D{10, 2}, size.Y + 2.0f); + const bool isHovered = MouseInZone(pos, size); + + if (selected || isHovered) + { + DrawRect(pos, size.X, size.Y, isHovered ? Colors::Frame : Colors::Selection); + if (isHovered) elementHovered = true; + } + TextLeft(name, FVector2D{pos.X + 4.0f, pos.Y + size.Y / 2}, Colors::Text, false); + + EndElement(pos, size); + return isHovered && Input::IsMouseClicked(0, elementCount, false); + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/Slider.h b/Internal/menu/canvas/components/Slider.h new file mode 100644 index 0000000..f59ee0a --- /dev/null +++ b/Internal/menu/canvas/components/Slider.h @@ -0,0 +1,96 @@ +#pragma once + +/// @file +/// @brief Slider widgets: SliderInt / SliderFloat — a draggable track with a knob and a live value +/// readout. Dragging is captured by widget id in `activeElement` so it continues off the track. + +#include "Core.h" + +namespace UCanvasGUI +{ + inline void SliderInt(const char* name, int* value, int min, int max) + { + elementCount++; + + const FVector2D size{240, 50}; + const FVector2D track{200, 10}; + const FVector2D pos = NextPos({10, 15}, size.Y + 15.0f); + + const FVector2D trackPos{pos.X, pos.Y + track.Y + 15.0f}; + const bool isHovered = MouseInZone(trackPos, track); + + if (isHovered || activeElement == elementCount) + { + elementHovered = true; + if (Input::IsMouseClicked(0, elementCount, true)) + { + activeElement = elementCount; + const FVector2D cursor = CursorPos(); + *value = (int)(((cursor.X - pos.X) * ((max - min) / track.X)) + min); + if (*value < min) *value = min; + if (*value > max) *value = max; + } + } + + // Track + progress fill + knob. + DrawRect(trackPos, track.X, track.Y, Colors::Frame); + DrawRect(FVector2D{trackPos.X, trackPos.Y + 5.0f}, 5.0f, 5.0f, Colors::Accent); + + const float step = track.X / (max - min); + DrawRect(trackPos, step * (*value - min), track.Y, Colors::Accent); + DrawCircle(FVector2D{pos.X + step * (*value - min), trackPos.Y + 3.3f}, 10.0f, Colors::Knob); + DrawCircle(FVector2D{pos.X + step * (*value - min), trackPos.Y + 3.3f}, 5.0f, Colors::Accent); + + char buffer[32]; + sprintf_s(buffer, "%i", *value); + TextCenter(buffer, FVector2D{pos.X + step * (*value - min), trackPos.Y + 25.0f}, Colors::Text, false); + TextLeft(name, FVector2D{pos.X + 5, pos.Y + 10}, Colors::Text, false); + + EndElement(pos, size); + } + + inline void SliderFloat(const char* name, float* value, float min, float max, const char* format = "%.0f") + { + elementCount++; + + const FVector2D size{210, 40}; + const FVector2D track{170, 7}; + const FVector2D pos = NextPos({10, 15}, size.Y + 15.0f); + + const FVector2D trackPos{pos.X, pos.Y + track.Y + 15.0f}; + // The hit zone is taller than the track so the thin bar is easy to grab. + const bool isHovered = MouseInZone(FVector2D{pos.X, trackPos.Y - 20.0f}, FVector2D{track.X, track.Y + 30.0f}); + + if (isHovered || activeElement == elementCount) + { + elementHovered = true; + if (Input::IsMouseClicked(0, elementCount, true)) + { + activeElement = elementCount; + const FVector2D cursor = CursorPos(); + *value = ((cursor.X - pos.X) * ((max - min) / track.X)) + min; + if (*value < min) *value = min; + if (*value > max) *value = max; + } + } + + // Track + end caps. + DrawRect(trackPos, track.X, track.Y, Colors::Frame); + DrawCircle(FVector2D{pos.X, pos.Y + 24.3f}, 3.1f, Colors::Accent); + DrawCircle(FVector2D{pos.X + track.X, pos.Y + 24.3f}, 3.1f, Colors::Frame); + + TextLeft(name, FVector2D{pos.X, pos.Y + 5}, Colors::Text, false); + + // Progress fill + knob. + const float step = track.X / (max - min); + DrawRect(trackPos, step * (*value - min), track.Y, Colors::Accent); + DrawCircle(FVector2D{pos.X + step * (*value - min), trackPos.Y + 2.66f}, 8.0f, Colors::Knob); + DrawCircle(FVector2D{pos.X + step * (*value - min), trackPos.Y + 2.66f}, 4.0f, Colors::Accent); + + char buffer[32]; + sprintf_s(buffer, format, *value); + TextCenter(buffer, FVector2D{pos.X + step * (*value - min), trackPos.Y + 20.0f}, Colors::Text, false); + + EndElement(pos, size); + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/Tab.h b/Internal/menu/canvas/components/Tab.h new file mode 100644 index 0000000..9e38f35 --- /dev/null +++ b/Internal/menu/canvas/components/Tab.h @@ -0,0 +1,26 @@ +#pragma once + +/// @file +/// @brief Tab button: a selectable tab-strip entry (ButtonTab), highlighted while active/hovered. + +#include "Core.h" + +namespace UCanvasGUI +{ + inline bool ButtonTab(const char* name, FVector2D size, bool active) + { + elementCount++; + + const FVector2D pos = NextPos({5, 10}, size.Y + 10.0f); + const bool isHovered = MouseInZone(pos, size); + + DrawRect(pos, size.X, size.Y, Colors::Accent); + if (isHovered && !active) + elementHovered = true; + + TextCenter(name, FVector2D{pos.X + size.X / 2, pos.Y + size.Y / 2}, Colors::Text, false); + + EndElement(pos, size); + return isHovered && Input::IsMouseClicked(0, elementCount, false); + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/Text.h b/Internal/menu/canvas/components/Text.h new file mode 100644 index 0000000..fae0123 --- /dev/null +++ b/Internal/menu/canvas/components/Text.h @@ -0,0 +1,25 @@ +#pragma once + +/// @file +/// @brief Text widget: a laid-out (left- or center-aligned) text row that advances the layout cursor. + +#include "Core.h" + +namespace UCanvasGUI +{ + inline void Text(const char* text, bool center = false, bool outline = false) + { + elementCount++; + + constexpr float height = 25.0f; + const FVector2D pos = NextPos({10, 10}, height + 10.0f); + const FVector2D textPos{pos.X + 5.0f, pos.Y + height / 2}; + + if (center) + TextCenter(text, textPos, Colors::Text, outline); + else + TextLeft(text, textPos, Colors::Text, outline); + + EndElement(pos); + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/TextField.h b/Internal/menu/canvas/components/TextField.h new file mode 100644 index 0000000..9fd88f1 --- /dev/null +++ b/Internal/menu/canvas/components/TextField.h @@ -0,0 +1,90 @@ +#pragma once + +/// @file +/// @brief TextField rich widget: an editable single-line field (plus CharFromVK) with hint and trailing label. + +#include "Core.h" + +namespace UCanvasGUI +{ + inline int activeField = -1; ///< elementCount id of the focused text field + /// Translate a virtual-key code to a printable character (basic US layout), honoring @p shift. + inline char CharFromVK(int vk, bool shift) + { + if (vk >= 'A' && vk <= 'Z') return shift ? (char)vk : (char)(vk - 'A' + 'a'); + if (vk >= '0' && vk <= '9') + { + if (!shift) return (char)vk; + static const char* sym = ")!@#$%^&*("; + return sym[vk - '0']; + } + switch (vk) + { + case VK_SPACE: + return ' '; + case VK_OEM_MINUS: + return shift ? '_' : '-'; + case VK_OEM_PLUS: + return shift ? '+' : '='; + case VK_OEM_PERIOD: + return shift ? '>' : '.'; + case VK_OEM_COMMA: + return shift ? '<' : ','; + case VK_OEM_1: + return shift ? ':' : ';'; + case VK_OEM_2: + return shift ? '?' : '/'; + case VK_OEM_5: + return shift ? '|' : '\\'; + } + return 0; + } + + /// Editable single-line text field with a trailing label; @p hint shows when empty and focused. + inline bool TextField(const char* name, char* buf, size_t size, float width, float height, const char* hint) + { + elementCount++; + bool changed = false; + + const FVector2D fieldSize{width, height}; + const FVector2D pos = NextPos(FVector2D{10, 10}, height + 10.0f); + const bool isHovered = MouseInZone(pos, fieldSize); + + DrawRect(pos, fieldSize.X, fieldSize.Y, Colors::Frame); + if (isHovered) elementHovered = true; + + if (Input::IsMouseClicked(0, elementCount, false)) + activeField = isHovered ? elementCount : (activeField == elementCount ? -1 : activeField); + + if (activeField == elementCount) + { + const bool shift = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0; + size_t len = strlen(buf); + if (Input::IsKeyPressed(VK_BACK, false) && len > 0) + { + buf[len - 1] = '\0'; + changed = true; + } + for (int vk = 0x20; vk < 256; vk++) + { + if (!Input::IsKeyPressed(vk, false)) continue; + char ch = CharFromVK(vk, shift); + if (ch && len < size - 1) + { + buf[len++] = ch; + buf[len] = '\0'; + changed = true; + } + } + } + + const bool empty = (buf[0] == '\0'); + const char* shown = empty ? (hint ? hint : "") : buf; + if (shown[0]) TextLeft(shown, FVector2D{pos.X + 4.0f, pos.Y + fieldSize.Y / 2}, Colors::Text, false); + if (name && name[0] && name[0] != '#') + TextLeft(name, FVector2D{pos.X + fieldSize.X + 5.0f, pos.Y + fieldSize.Y / 2}, Colors::Text, false); + + EndElement(pos, fieldSize); + return changed; + } +} // namespace UCanvasGUI diff --git a/Internal/menu/canvas/components/Window.h b/Internal/menu/canvas/components/Window.h new file mode 100644 index 0000000..e176c36 --- /dev/null +++ b/Internal/menu/canvas/components/Window.h @@ -0,0 +1,70 @@ +#pragma once + +/// @file +/// @brief The draggable menu window: chrome (background, tab column, header bar, title) and the +/// pointer-driven drag. Opens the frame — resets the layout cursor and widget counter — and draws the +/// chrome; returns false (drawing nothing) while closed or while the game window isn't focused. + +#include "Core.h" + +namespace UCanvasGUI +{ + inline FVector2D dragPos; ///< cursor-to-window offset captured at drag start (zero while not dragging) + + /// @param pos in/out window top-left; updated while dragged. + /// @param isOpen whether the menu is shown. + /// @return true if the window is drawn (callers should draw their widgets only then). + inline bool Window(const char* name, FVector2D* pos, FVector2D size, bool isOpen) + { + elementCount = 0; + static HWND window = FindWindow(L"UnrealWindow", L"PortalWars "); + if (!isOpen || GetActiveWindow() != window) + return false; + + const bool isHovered = MouseInZone(*pos, size); + const bool lmb = GetAsyncKeyState(VK_LBUTTON); + + // Release the dragged slider once the button is up. + if (activeElement != -1 && !lmb) + activeElement = -1; + + if (elementHovered && lmb) + { + // A widget is capturing this click — don't start a window drag. + } + else if ((isHovered || dragPos.X != 0) && !elementHovered) + { + if (Input::IsMouseClicked(0, elementCount, true)) + { + FVector2D cursor = CursorPos(); + cursor.X -= size.X; + cursor.Y -= size.Y; + if (dragPos.X == 0) + dragPos = FVector2D{cursor.X - pos->X, cursor.Y - pos->Y}; + pos->X = cursor.X - dragPos.X; + pos->Y = cursor.Y - dragPos.Y; + } + else + { + dragPos = FVector2D{0, 0}; + } + } + else + { + elementHovered = false; + } + + offsetX = 0.0f; + offsetY = 0.0f; + menuPos = *pos; + + // Chrome: window background, left tab column, header bar. + DrawRect(*pos, size.X, size.Y, Colors::Background); + DrawRect(*pos, 122, size.Y, Colors::Frame); + DrawRect(*pos, size.X, 25.0f, Colors::Accent); + offsetY += 25.0f; + + TextCenter(name, FVector2D{pos->X + size.X / 2, pos->Y + 25 / 2}, Colors::Text, false); + return true; + } +} // namespace UCanvasGUI From 79f84a0294edd4e3d3e7c8acef9cb164877cce6d Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:27 +0200 Subject: [PATCH 13/54] feat(menu): add the Menu::Backend interface and Phase enum Pure interface (no ImGui/UCanvasGUI) mirroring the Renderer strategy: lifecycle, window/tab chrome, core + rich widgets, clipboard. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/backend/Backend.h | 95 +++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 Internal/menu/backend/Backend.h diff --git a/Internal/menu/backend/Backend.h b/Internal/menu/backend/Backend.h new file mode 100644 index 0000000..01f5613 --- /dev/null +++ b/Internal/menu/backend/Backend.h @@ -0,0 +1,95 @@ +#pragma once + +/// @file +/// @brief Menu::Backend — the abstract menu GUI backend. The shared menu layer (Menu::UI facade, +/// the Menu::Frame driver, the tab sections) draws through this interface and never names a concrete +/// backend, exactly like the render layer's Renderer. Concrete backends (ImGuiBackend, CanvasBackend) +/// live in this folder and are the ONLY place ImGui / UCanvasGUI are named. Adding a third backend is a +/// new implementation + one registry entry — no shared-code change. +/// +/// This header is a pure interface: it depends only on the settings color type and the standard +/// library — no ImGui, no UCanvasGUI, no UE. + +#include +#include +#include +#include + +#include "../../settings/Settings.h" // ::Color + +namespace Menu +{ + /// Which per-frame hook drives a backend. ImGui draws in the Present hook; the UE canvas draws in + /// PostRender. The shared driver asks the active backend for its phase — that is the only place + /// backend selection happens, and it is a single comparison, not a per-widget branch. + enum class Phase + { + Present, + PostRender, + }; + + /// The menu GUI backend interface: window/tab chrome, every widget, and per-frame lifecycle. + class Backend + { + public: + virtual ~Backend() = default; + + // --- Lifecycle (each backend owns its input sampling, theming/accent, cursor, debug windows). --- + virtual Phase phase() const = 0; + virtual bool ExtraToggle() = 0; ///< extra "open menu" signal (e.g. ImGui gamepad Start); false if none + virtual void BeginFrame() = 0; ///< once per frame before the window (sample input, apply accent, ...) + virtual void EndFrame() = 0; ///< once per frame after the window + + // --- Window + tab chrome. --- + virtual bool BeginWindow(const char* title) = 0; ///< false => don't draw the menu this frame + virtual void EndWindow() = 0; + virtual bool BeginTab(const char* label) = 0; ///< true => this tab is active; draw its section + virtual void EndTab() = 0; + + // --- Core widgets (return `changed` where a value can change this frame). --- + virtual bool Toggle(const char* label, bool* v) = 0; + virtual bool Button(const char* label) = 0; + virtual bool SmallButton(const char* label) = 0; + virtual bool Checkbox(const char* label, bool* v) = 0; + virtual bool RadioBool(const char* label, bool active) = 0; + virtual bool RadioInt(const char* label, int* v, int value) = 0; + virtual bool SliderFloat(const char* label, float* v, float min, float max, const char* fmt) = 0; + virtual bool SliderInt(const char* label, int* v, int min, int max) = 0; + virtual bool Combo(const char* label, int* v, const char* const* items, int count) = 0; + virtual bool ColorEdit(const char* label, ::Color* c) = 0; + virtual bool HotKey(const char* label, int* key) = 0; + virtual void SameLine() = 0; + virtual void SeparatorText(const char* label) = 0; + virtual void Tooltip(const char* text) = 0; + virtual void TextV(const char* fmt, va_list args) = 0; + virtual void TextDisabledV(const char* fmt, va_list args) = 0; + virtual void BulletTextV(const char* fmt, va_list args) = 0; + + // --- Layout / scope helpers. --- + virtual void SetNextItemWidth(float w) = 0; + virtual void BeginDisabled(bool disabled) = 0; + virtual void EndDisabled() = 0; + virtual void PushID(const char* id) = 0; + virtual void PopID() = 0; + + // --- Rich widgets (the advanced panels use only these; both backends implement them). --- + virtual bool InputText(const char* label, char* buf, size_t size) = 0; + virtual bool InputTextHint(const char* label, const char* hint, char* buf, size_t size) = 0; + virtual bool InputTextMultiline(const char* label, char* buf, size_t size, float height) = 0; + virtual bool BeginChild(const char* id, float w, float h) = 0; + virtual void EndChild() = 0; + virtual bool BeginCombo(const char* label, const char* preview) = 0; + virtual void EndCombo() = 0; + virtual bool Selectable(const char* label, bool selected) = 0; + /// Draw a (potentially long) list of @p count rows; @p drawRow(i) draws row i. The backend + /// decides how to clip/scroll (ImGui uses ImGuiListClipper; the canvas culls to a visible window). + virtual void ClippedList(int count, const std::function& drawRow) = 0; + virtual bool CollapsingHeader(const char* label) = 0; + virtual bool TreeNode(const char* label) = 0; + virtual void TreePop() = 0; + + // --- Clipboard. --- + virtual void SetClipboardText(const char* text) = 0; + virtual const char* GetClipboardText() = 0; + }; +} // namespace Menu From 58e2a90e986590a0947ae6118c27f350ee8d4bf7 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:27 +0200 Subject: [PATCH 14/54] feat(menu): implement ImGuiBackend Native ImGui widgets/chrome, demo & style windows, gamepad toggle, software cursor. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/backend/ImGuiBackend.h | 172 +++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 Internal/menu/backend/ImGuiBackend.h diff --git a/Internal/menu/backend/ImGuiBackend.h b/Internal/menu/backend/ImGuiBackend.h new file mode 100644 index 0000000..42a5e57 --- /dev/null +++ b/Internal/menu/backend/ImGuiBackend.h @@ -0,0 +1,172 @@ +#pragma once + +/// @file +/// @brief The ImGui menu backend: draws in the Present hook. Owns everything ImGui — the window/tab +/// chrome, every widget (native ImGui / Custom.h), the accent tinting, the demo/style-editor windows, +/// the software cursor, and the gamepad open-toggle. Nothing here leaks into the shared menu layer. + +#include +#include + +#include + +#include "Backend.h" +#include "../gui/Custom.h" // ImGui::ToggleButton / HotKey / Tooltip +#include "../../settings/Settings.h" +#include "../../utils/Rgb.h" + +namespace Menu +{ + class ImGuiBackend : public Backend + { + public: + Phase phase() const override { return Phase::Present; } + + bool ExtraToggle() override { return ImGui::IsKeyPressed(ImGuiKey_GamepadStart); } + + void BeginFrame() override + { + // ImGui paints its own software cursor while the menu is open (the Canvas backend draws its + // own, so this is scoped to the ImGui backend). + ImGui::GetIO().MouseDrawCursor = Settings.MENU.ShowMenu; + + if (Settings.DEBUG.ShowDemoWindow) ImGui::ShowDemoWindow(&Settings.DEBUG.ShowDemoWindow); + if (Settings.DEBUG.ShowStyleEditor) ImGui::ShowStyleEditor(); + + ApplyAccent(); + } + + void EndFrame() override {} + + bool BeginWindow(const char* title) override + { + const ImGuiViewport* vp = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(ImVec2(vp->WorkPos.x + 550, vp->WorkPos.y + 20), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(550, 350), ImGuiCond_FirstUseEver); + + if (!ImGui::Begin(title, &Settings.MENU.ShowMenu, ImGuiWindowFlags_NoCollapse)) + { + ImGui::End(); + return false; + } + if (!ImGui::BeginTabBar("MainTabBar", ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_NoCloseWithMiddleMouseButton)) + { + ImGui::End(); + return false; + } + return true; + } + void EndWindow() override + { + ImGui::EndTabBar(); + ImGui::End(); + } + bool BeginTab(const char* label) override { return ImGui::BeginTabItem(label); } + void EndTab() override { ImGui::EndTabItem(); } + + // --- Core widgets. --- + bool Toggle(const char* label, bool* v) override { return ImGui::ToggleButton(label, v); } + bool Button(const char* label) override { return ImGui::Button(label); } + bool SmallButton(const char* label) override { return ImGui::SmallButton(label); } + bool Checkbox(const char* label, bool* v) override { return ImGui::Checkbox(label, v); } + bool RadioBool(const char* label, bool active) override { return ImGui::RadioButton(label, active); } + bool RadioInt(const char* label, int* v, int value) override { return ImGui::RadioButton(label, v, value); } + bool SliderFloat(const char* label, float* v, float min, float max, const char* fmt) override { return ImGui::SliderFloat(label, v, min, max, fmt); } + bool SliderInt(const char* label, int* v, int min, int max) override { return ImGui::SliderInt(label, v, min, max); } + bool Combo(const char* label, int* v, const char* const* items, int count) override { return ImGui::Combo(label, v, items, count); } + bool ColorEdit(const char* label, ::Color* c) override { return ImGui::ColorEdit4(label, &c->R); } + bool HotKey(const char* label, int* key) override + { + const int old = *key; + ImGui::HotKey(label, key); + return *key != old; + } + void SameLine() override { ImGui::SameLine(); } + void SeparatorText(const char* label) override { ImGui::SeparatorText(label); } + void Tooltip(const char* text) override { ImGui::Tooltip(text); } + void TextV(const char* fmt, va_list args) override + { + char buf[512]; + vsnprintf(buf, sizeof(buf), fmt, args); + ImGui::TextUnformatted(buf); + } + void TextDisabledV(const char* fmt, va_list args) override + { + char buf[512]; + vsnprintf(buf, sizeof(buf), fmt, args); + ImGui::TextDisabled("%s", buf); + } + void BulletTextV(const char* fmt, va_list args) override + { + char buf[512]; + vsnprintf(buf, sizeof(buf), fmt, args); + ImGui::BulletText("%s", buf); + } + + // --- Layout / scope. --- + void SetNextItemWidth(float w) override { ImGui::SetNextItemWidth(w); } + void BeginDisabled(bool disabled) override { ImGui::BeginDisabled(disabled); } + void EndDisabled() override { ImGui::EndDisabled(); } + void PushID(const char* id) override { ImGui::PushID(id); } + void PopID() override { ImGui::PopID(); } + + // --- Rich widgets. --- + bool InputText(const char* label, char* buf, size_t size) override { return ImGui::InputText(label, buf, size); } + bool InputTextHint(const char* label, const char* hint, char* buf, size_t size) override { return ImGui::InputTextWithHint(label, hint, buf, size); } + bool InputTextMultiline(const char* label, char* buf, size_t size, float height) override { return ImGui::InputTextMultiline(label, buf, size, ImVec2(0, height)); } + bool BeginChild(const char* id, float w, float h) override { return ImGui::BeginChild(id, ImVec2(w, h), true, ImGuiWindowFlags_HorizontalScrollbar); } + void EndChild() override { ImGui::EndChild(); } + bool BeginCombo(const char* label, const char* preview) override { return ImGui::BeginCombo(label, preview); } + void EndCombo() override { ImGui::EndCombo(); } + bool Selectable(const char* label, bool selected) override { return ImGui::Selectable(label, selected); } + void ClippedList(int count, const std::function& drawRow) override + { + ImGuiListClipper clipper; + clipper.Begin(count); + while (clipper.Step()) + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + drawRow(i); + } + bool CollapsingHeader(const char* label) override { return ImGui::CollapsingHeader(label); } + bool TreeNode(const char* label) override { return ImGui::TreeNode(label); } + void TreePop() override { ImGui::TreePop(); } + + void SetClipboardText(const char* text) override { ImGui::SetClipboardText(text); } + const char* GetClipboardText() override { return ImGui::GetClipboardText(); } + + private: + /// Tint the interactive accent slots each frame: the cycling RGB color when enabled, else the + /// theme's default reds. Defaults are snapshotted once (before the first override) so toggling RGB + /// off restores the original shades instead of freezing on the last rainbow frame. + static void ApplyAccent() + { + static constexpr ImGuiCol accentSlots[] = { + ImGuiCol_CheckMark, ImGuiCol_SliderGrab, ImGuiCol_SliderGrabActive, + ImGuiCol_Header, ImGuiCol_HeaderHovered, ImGuiCol_HeaderActive, + ImGuiCol_Tab, ImGuiCol_TabHovered, ImGuiCol_TabActive, ImGuiCol_TitleBgActive}; + + ImVec4* colors = ImGui::GetStyle().Colors; + + static const std::array defaults = [&] + { + std::array saved{}; + for (size_t i = 0; i < saved.size(); ++i) + saved[i] = colors[accentSlots[i]]; + return saved; + }(); + + if (Settings.MENU.Rgb) + { + const Color rgb = Rgb::Current(); + const ImVec4 accent(rgb.R, rgb.G, rgb.B, rgb.A); + for (ImGuiCol slot : accentSlots) + colors[slot] = accent; + } + else + { + for (size_t i = 0; i < defaults.size(); ++i) + colors[accentSlots[i]] = defaults[i]; + } + } + }; +} // namespace Menu From 8ef599a7dd39e9270fd8e7955580c5a5055dbafa Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:27 +0200 Subject: [PATCH 15/54] feat(menu): implement CanvasBackend over UCanvasGUI Window/tab chrome + every widget over UCanvasGUI, per-frame accent retint, input sampling, own cursor; clipboard via Shared::Utilities. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/backend/CanvasBackend.h | 175 ++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 Internal/menu/backend/CanvasBackend.h diff --git a/Internal/menu/backend/CanvasBackend.h b/Internal/menu/backend/CanvasBackend.h new file mode 100644 index 0000000..7654271 --- /dev/null +++ b/Internal/menu/backend/CanvasBackend.h @@ -0,0 +1,175 @@ +#pragma once + +/// @file +/// @brief The UE-canvas menu backend: draws in the PostRender hook through UCanvasGUI (which draws via +/// the active Render backend). Owns everything UCanvasGUI — the window/tab chrome (built over UCanvasGUI's Window + +/// tab-column primitives), every widget, the accent theming, per-frame input sampling, and its own +/// software cursor. Works everywhere the UE canvas is valid (main menu / loading included). Nothing +/// here leaks into the shared menu layer. + +#include +#include + +#include "Backend.h" +#include "../canvas/UCanvasGUI.h" +#include "../../../shared/Utilities.h" +#include "../../settings/Settings.h" +#include "../../utils/Rgb.h" + +namespace Menu +{ + class CanvasBackend : public Backend + { + public: + Phase phase() const override { return Phase::PostRender; } + bool ExtraToggle() override { return false; } + + void BeginFrame() override + { + Input::Handle(); // sample mouse/keyboard once per frame + + // Accent: retint from the rainbow while the RGB feature is on; otherwise reset to the palette. + UCanvasGUI::Colors::Accent = Settings.MENU.Rgb + ? [] { const Color c = Rgb::Current(); return FLinearColor{c.R, c.G, c.B, c.A}; }() + : Render::Palette::Primary.To(); + } + void EndFrame() override {} + + bool BeginWindow(const char* title) override + { + if (!UCanvasGUI::Window(title, &pos, FVector2D{700.f, 500.f}, Settings.MENU.ShowMenu)) return false; + tabIndex = 0; + return true; + } + void EndWindow() override + { + UCanvasGUI::Render(); // drain deferred pop-ups (combo/color swatches) on top + UCanvasGUI::DrawCursor(); // the Canvas menu's own cursor + } + bool BeginTab(const char* label) override + { + // Place this tab's button at an absolute slot in the left column (deterministic regardless of + // content drawn for other tabs), then, if it is the active tab, move the cursor to the content + // column so the section draws there. + const float headerH = 25.f, tabH = 32.f, contentX = 130.f; + UCanvasGUI::offsetX = 0.f; + UCanvasGUI::PushNextElementY(UCanvasGUI::menuPos.Y + headerH + tabIndex * tabH, false); + if (UCanvasGUI::ButtonTab(label, FVector2D{112.f, 30.f}, activeTab == tabIndex)) + activeTab = tabIndex; + + const bool isActive = (activeTab == tabIndex); + tabIndex++; + if (isActive) + { + UCanvasGUI::offsetX = contentX; + UCanvasGUI::PushNextElementY(UCanvasGUI::menuPos.Y + headerH + 5.f, false); + } + return isActive; + } + void EndTab() override {} + + // --- Core widgets. --- + bool Toggle(const char* label, bool* v) override { return UCanvasGUI::Checkbox(Vis(label), v); } + bool Button(const char* label) override { return UCanvasGUI::Button(Vis(label), FVector2D{150.f, 25.f}); } + bool SmallButton(const char* label) override { return UCanvasGUI::Button(Vis(label), FVector2D{90.f, 20.f}); } + bool Checkbox(const char* label, bool* v) override { return UCanvasGUI::Checkbox(Vis(label), v); } + bool RadioBool(const char* label, bool active) override { return UCanvasGUI::ButtonTab(Vis(label), FVector2D{110.f, 24.f}, active); } + bool RadioInt(const char* label, int* v, int value) override + { + if (UCanvasGUI::ButtonTab(Vis(label), FVector2D{110.f, 24.f}, *v == value)) + { + *v = value; + return true; + } + return false; + } + bool SliderFloat(const char* label, float* v, float min, float max, const char* fmt) override + { + const float old = *v; + UCanvasGUI::SliderFloat(Vis(label), v, min, max, fmt); + return *v != old; + } + bool SliderInt(const char* label, int* v, int min, int max) override + { + const int old = *v; + UCanvasGUI::SliderInt(Vis(label), v, min, max); + return *v != old; + } + bool Combo(const char* label, int* v, const char* const* items, int count) override { return UCanvasGUI::Combobox(Vis(label), FVector2D{150.f, 25.f}, v, items, count); } + bool ColorEdit(const char* label, ::Color* c) override { return UCanvasGUI::ColorPicker(Vis(label), reinterpret_cast(c)); } + bool HotKey(const char* label, int* key) override { return UCanvasGUI::Hotkey(Vis(label), FVector2D{90.f, 22.f}, key); } + void SameLine() override { UCanvasGUI::SameLine(); } + void SeparatorText(const char* label) override { UCanvasGUI::Text(Vis(label)); } + void Tooltip(const char*) override {} // no hover tooltip on the canvas backend + void TextV(const char* fmt, va_list args) override + { + char buf[512]; + vsnprintf(buf, sizeof(buf), fmt, args); + UCanvasGUI::Text(buf); + } + void TextDisabledV(const char* fmt, va_list args) override { TextV(fmt, args); } + void BulletTextV(const char* fmt, va_list args) override { TextV(fmt, args); } + + // --- Layout / scope (no-ops on the canvas: UCanvasGUI auto-sizes and ids are per-call). --- + void SetNextItemWidth(float) override {} + void BeginDisabled(bool) override {} + void EndDisabled() override {} + void PushID(const char*) override {} + void PopID() override {} + + // --- Rich widgets. --- + bool InputText(const char* label, char* buf, size_t size) override { return UCanvasGUI::TextField(Vis(label), buf, size, 220.f, 22.f, nullptr); } + bool InputTextHint(const char* label, const char* hint, char* buf, size_t size) override { return UCanvasGUI::TextField(Vis(label), buf, size, 220.f, 22.f, hint); } + bool InputTextMultiline(const char* label, char* buf, size_t size, float height) override { return UCanvasGUI::TextField(Vis(label), buf, size, 320.f, height, nullptr); } + bool BeginChild(const char* id, float w, float h) override + { + lastChildH = h; + return UCanvasGUI::BeginChild(id, w, h); + } + void EndChild() override { UCanvasGUI::EndChild(lastChildH); } + bool BeginCombo(const char* label, const char* preview) override { return UCanvasGUI::BeginCombo(Vis(label), preview, 220.f); } + void EndCombo() override { UCanvasGUI::EndCombo(); } + bool Selectable(const char* label, bool selected) override { return UCanvasGUI::Selectable(Vis(label), selected, 300.f); } + void ClippedList(int count, const std::function& drawRow) override + { + // No mouse-wheel on the canvas: draw a bounded window of rows and prompt to filter for more. + constexpr int maxVisible = 14; + const int shown = count < maxVisible ? count : maxVisible; + for (int i = 0; i < shown; i++) + drawRow(i); + if (count > shown) + { + char note[64]; + snprintf(note, sizeof(note), "... %d more (filter to narrow)", count - shown); + UCanvasGUI::Text(note); + } + } + bool CollapsingHeader(const char* label) override { return UCanvasGUI::CollapsingHeader(Vis(label)); } + bool TreeNode(const char* label) override { return UCanvasGUI::CollapsingHeader(Vis(label)); } + void TreePop() override {} + + void SetClipboardText(const char* text) override { Shared::Utilities::CopyToClipboard(text ? text : ""); } + const char* GetClipboardText() override + { + static std::string buffer; + buffer = Shared::Utilities::PasteFromClipboard(); + return buffer.c_str(); + } + + private: + FVector2D pos{400.f, 200.f}; ///< draggable window position + int tabIndex = 0; ///< reset each frame in BeginWindow + int activeTab = 0; ///< persists across frames + float lastChildH = 0.f; ///< height of the open BeginChild, for EndChild + + /// The visible part of a label: everything before an ImGui "##id" suffix (which the canvas would + /// otherwise draw literally). Returns a thread-local buffer valid for the widget call. + static const char* Vis(const char* label) + { + static std::string buf; + const char* p = std::strstr(label, "##"); + buf.assign(label, p ? static_cast(p - label) : std::strlen(label)); + return buf.c_str(); + } + }; +} // namespace Menu From b59bf94cc0cca676ac14a9510f2b2214b93e871a Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:28 +0200 Subject: [PATCH 16/54] feat(menu): add the backend registry (instances, table, Select) backends[] ordered by MenuBackend; Menu::active + Menu::Select mirror Render.h. A third backend is a new file + enum value + one entry. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/backend/Backends.h | 33 ++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 Internal/menu/backend/Backends.h diff --git a/Internal/menu/backend/Backends.h b/Internal/menu/backend/Backends.h new file mode 100644 index 0000000..1e3ef32 --- /dev/null +++ b/Internal/menu/backend/Backends.h @@ -0,0 +1,33 @@ +#pragma once + +/// @file +/// @brief The menu backend registry: the concrete backend instances, the table indexed by the +/// MenuBackend setting, the active pointer, and Select — mirroring render/Render.h. Adding a backend +/// is a new instance + one table entry here (and one MenuBackend enum value); no shared-code change. + +#include "Backend.h" +#include "ImGuiBackend.h" +#include "CanvasBackend.h" +#include "../../settings/Settings.h" + +namespace Menu +{ + inline ImGuiBackend imguiBackend; + inline CanvasBackend canvasBackend; + + /// Indexed by MenuBackend (order must match the enum). Adding a backend = a new entry, no branching. + inline Backend* const backends[] = { + &imguiBackend, // MenuBackend::ImGui + &canvasBackend, // MenuBackend::Canvas + }; + + /// The backend the shared menu layer draws through this frame. + inline Backend* active = backends[0]; + + /// Point @ref active at the backend the setting selects (called once per frame per hook). + inline void Select(MenuBackend backend) + { + const size_t index = static_cast(backend); + active = (index < (sizeof(backends) / sizeof(*backends))) ? backends[index] : backends[0]; + } +} // namespace Menu From fecacf5de120f7015bf7ba29bd055a3473f033fe Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:28 +0200 Subject: [PATCH 17/54] refactor(menu): rewrite Menu::UI as pure forwarders, add UI::Count Every UI:: call is a one-line forward to Menu::active->*; drop IsImGui/IsCanvas/Vis and the per-backend branches. UI::Count replaces ImGui's IM_ARRAYSIZE. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/ui/UI.h | 222 +++++++++++------------------------------- 1 file changed, 55 insertions(+), 167 deletions(-) diff --git a/Internal/menu/ui/UI.h b/Internal/menu/ui/UI.h index 9e04728..02ae3d7 100644 --- a/Internal/menu/ui/UI.h +++ b/Internal/menu/ui/UI.h @@ -1,210 +1,98 @@ #pragma once /// @file -/// @brief Menu::UI — the backend-neutral widget facade the tab sections call. Each function -/// dispatches on Settings.MENU.Backend: the ImGui backend forwards to the native ImGui / Custom.h -/// widgets (drawn in the Present frame), the Canvas backend forwards to the ZeroGUI widgets (drawn -/// through Render::canvas in the PostRender frame). Only one backend is active per frame and each -/// hook drives only its own backend's sections, so every call always lands in a valid context. -/// -/// Value widgets return `bool changed` (true on the change frame) so the sections keep the -/// `changed |= UI::Toggle(...); if (changed) Dispatch(SettingsChanged);` pattern across both backends. +/// @brief Menu::UI — the backend-neutral widget facade the tab sections call. Every function is a +/// one-line forward to the active backend (Menu::active->*); this layer names no concrete backend and +/// includes neither ImGui nor UCanvasGUI. Switching or adding a backend never touches this file or the +/// sections. Value widgets return `bool changed` (true on the change frame). #include -#include -#include -#include -#include - -#include "../../settings/Settings.h" +#include "../backend/Backend.h" +#include "../backend/Backends.h" #include "../../scripting/Events.h" -#include "../gui/Custom.h" // ImGui::ToggleButton / HotKey / Tooltip / ... -#include "../canvas/ZeroGUI.h" namespace Menu { namespace UI { - /// True when the ImGui menu backend is active (the only place sections may call ImGui:: directly, - /// inside an `if (UI::IsImGui())` guard — that branch only runs in the Present/ImGui path). - inline bool IsImGui() - { - return Settings.MENU.Backend == MenuBackend::ImGui; - } - inline bool IsCanvas() - { - return Settings.MENU.Backend == MenuBackend::Canvas; - } - - /// The visible part of an ImGui label: everything before a "##id" disambiguation suffix (ImGui - /// hides it; the Canvas backend would otherwise draw it literally). Kept alive by the caller for - /// the duration of the widget call. - inline std::string Vis(const char* label) + /// Element count of a C array (replaces ImGui's IM_ARRAYSIZE without depending on ImGui). + template + constexpr int Count(const T (&)[N]) { - const char* p = std::strstr(label, "##"); - return p ? std::string(label, static_cast(p - label)) : std::string(label); + return static_cast(N); } - // --- Default Canvas widget sizes (px). ImGui sizes itself from its layout. --- - inline constexpr float ButtonW = 150.f, ButtonH = 25.f; - inline constexpr float SmallButtonW = 90.f, SmallButtonH = 20.f; - inline constexpr float ComboW = 150.f, ComboH = 25.f; - inline constexpr float HotKeyW = 90.f, HotKeyH = 22.f; - - /// Animated on/off toggle. @return true on the frame it flipped. - inline bool Toggle(const char* label, bool* v) - { - if (IsImGui()) return ImGui::ToggleButton(label, v); - return ZeroGUI::Checkbox(Vis(label).c_str(), v); - } + // --- Core widgets. --- + inline bool Toggle(const char* label, bool* v) { return active->Toggle(label, v); } /// Toggle that dispatches SettingsChanged (payload name=label, value=0/1) when flipped. inline bool ToggleSetting(const char* label, bool* v) { - if (!Toggle(label, v)) return false; + if (!active->Toggle(label, v)) return false; Events::Dispatch(Events::Type::SettingsChanged, Events::Payload{.value = *v ? 1.f : 0.f, .name = label}); return true; } - inline bool Checkbox(const char* label, bool* v) - { - if (IsImGui()) return ImGui::Checkbox(label, v); - return ZeroGUI::Checkbox(Vis(label).c_str(), v); - } - - inline bool Button(const char* label) - { - if (IsImGui()) return ImGui::Button(label); - return ZeroGUI::Button(Vis(label).c_str(), FVector2D{ButtonW, ButtonH}); - } - - inline bool SmallButton(const char* label) - { - if (IsImGui()) return ImGui::SmallButton(label); - return ZeroGUI::Button(Vis(label).c_str(), FVector2D{SmallButtonW, SmallButtonH}); - } - - /// Clickable radio row. @return true on the frame it was clicked. - inline bool RadioButton(const char* label, bool active) - { - if (IsImGui()) return ImGui::RadioButton(label, active); - return ZeroGUI::Button(Vis(label).c_str(), FVector2D{ButtonW, ButtonH}); - } - - inline bool SliderFloat(const char* label, float* v, float min, float max, const char* fmt = "%.0f") - { - if (IsImGui()) return ImGui::SliderFloat(label, v, min, max, fmt); - const float old = *v; - ZeroGUI::SliderFloat(Vis(label).c_str(), v, min, max, fmt); - return *v != old; - } - - inline bool SliderInt(const char* label, int* v, int min, int max) - { - if (IsImGui()) return ImGui::SliderInt(label, v, min, max); - const int old = *v; - ZeroGUI::SliderInt(Vis(label).c_str(), v, min, max); - return *v != old; - } - - inline bool Combo(const char* label, int* v, const char* const* items, int count) - { - if (IsImGui()) return ImGui::Combo(label, v, items, count); - return ZeroGUI::Combobox(Vis(label).c_str(), FVector2D{ComboW, ComboH}, v, items, count); - } - - /// RGBA color editor over a settings ::Color (layout-compatible with FLinearColor). - /// @return true if the color changed this frame. - inline bool ColorEdit(const char* label, ::Color* c) - { - if (IsImGui()) return ImGui::ColorEdit4(label, &c->R); - return ZeroGUI::ColorPicker(Vis(label).c_str(), reinterpret_cast(c)); - } - - /// Rebindable hotkey row. @return true on the frame the key changed. - inline bool HotKey(const char* label, int* key) - { - if (IsImGui()) - { - const int old = *key; - ImGui::HotKey(label, key); - return *key != old; - } - return ZeroGUI::Hotkey(Vis(label).c_str(), FVector2D{HotKeyW, HotKeyH}, key); - } - - inline void SameLine() - { - if (IsImGui()) - ImGui::SameLine(); - else - ZeroGUI::SameLine(); - } - - /// Section header with a label. - inline void SeparatorText(const char* label) - { - if (IsImGui()) - ImGui::SeparatorText(label); - else - ZeroGUI::Text(Vis(label).c_str()); - } - - /// Hover tooltip on the previous widget (ImGui only; a no-op on the Canvas backend). - inline void Tooltip(const char* text) - { - if (IsImGui()) ImGui::Tooltip(text); - } - - // --- Text (printf-style). Formats once, then routes to the active backend. --- - inline void TextV(const char* fmt, va_list args) - { - char buf[512]; - vsnprintf(buf, sizeof(buf), fmt, args); - if (IsImGui()) - ImGui::TextUnformatted(buf); - else - ZeroGUI::Text(buf); - } + inline bool Checkbox(const char* label, bool* v) { return active->Checkbox(label, v); } + inline bool Button(const char* label) { return active->Button(label); } + inline bool SmallButton(const char* label) { return active->SmallButton(label); } + inline bool RadioButton(const char* label, int* v, int value) { return active->RadioInt(label, v, value); } + inline bool SliderFloat(const char* label, float* v, float min, float max, const char* fmt = "%.0f") { return active->SliderFloat(label, v, min, max, fmt); } + inline bool SliderInt(const char* label, int* v, int min, int max) { return active->SliderInt(label, v, min, max); } + inline bool Combo(const char* label, int* v, const char* const* items, int count) { return active->Combo(label, v, items, count); } + inline bool ColorEdit(const char* label, ::Color* c) { return active->ColorEdit(label, c); } + inline bool HotKey(const char* label, int* key) { return active->HotKey(label, key); } + inline void SameLine() { active->SameLine(); } + inline void SeparatorText(const char* label) { active->SeparatorText(label); } + inline void Tooltip(const char* text) { active->Tooltip(text); } + + // --- Text (printf-style; the va_list is packed here and lowered to the backend's TextV). --- inline void Text(const char* fmt, ...) { va_list args; va_start(args, fmt); - TextV(fmt, args); + active->TextV(fmt, args); va_end(args); } inline void TextDisabled(const char* fmt, ...) { va_list args; va_start(args, fmt); - if (IsImGui()) - { - char buf[512]; - vsnprintf(buf, sizeof(buf), fmt, args); - ImGui::TextDisabled("%s", buf); - } - else - { - TextV(fmt, args); - } + active->TextDisabledV(fmt, args); va_end(args); } inline void BulletText(const char* fmt, ...) { va_list args; va_start(args, fmt); - if (IsImGui()) - { - char buf[512]; - vsnprintf(buf, sizeof(buf), fmt, args); - ImGui::BulletText("%s", buf); - } - else - { - TextV(fmt, args); - } + active->BulletTextV(fmt, args); va_end(args); } + + // --- Layout / scope. --- + inline void SetNextItemWidth(float w) { active->SetNextItemWidth(w); } + inline void BeginDisabled(bool disabled) { active->BeginDisabled(disabled); } + inline void EndDisabled() { active->EndDisabled(); } + inline void PushID(const char* id) { active->PushID(id); } + inline void PopID() { active->PopID(); } + + // --- Rich widgets. --- + inline bool InputText(const char* label, char* buf, size_t size) { return active->InputText(label, buf, size); } + inline bool InputTextHint(const char* label, const char* hint, char* buf, size_t size) { return active->InputTextHint(label, hint, buf, size); } + inline bool InputTextMultiline(const char* label, char* buf, size_t size, float height) { return active->InputTextMultiline(label, buf, size, height); } + inline bool BeginChild(const char* id, float w, float h) { return active->BeginChild(id, w, h); } + inline void EndChild() { active->EndChild(); } + inline bool BeginCombo(const char* label, const char* preview) { return active->BeginCombo(label, preview); } + inline void EndCombo() { active->EndCombo(); } + inline bool Selectable(const char* label, bool selected = false) { return active->Selectable(label, selected); } + inline void ClippedList(int count, const std::function& drawRow) { active->ClippedList(count, drawRow); } + inline bool CollapsingHeader(const char* label) { return active->CollapsingHeader(label); } + inline bool TreeNode(const char* label) { return active->TreeNode(label); } + inline void TreePop() { active->TreePop(); } + + // --- Clipboard. --- + inline void SetClipboardText(const char* text) { active->SetClipboardText(text); } + inline const char* GetClipboardText() { return active->GetClipboardText(); } } // namespace UI } // namespace Menu From 47a46e36559353d8e6b5aa9f51de4486f98827e8 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:28 +0200 Subject: [PATCH 18/54] refactor(menu): drive the menu via Menu::Frame(Phase); drop Draw/Tick One backend-agnostic driver + shared Tabs[] list; each backend picks its own hook via phase(). Co-Authored-By: Claude Opus 4.8 --- Internal/menu/Menu.h | 262 ++++++++----------------------------------- 1 file changed, 47 insertions(+), 215 deletions(-) diff --git a/Internal/menu/Menu.h b/Internal/menu/Menu.h index 520f200..75e0126 100644 --- a/Internal/menu/Menu.h +++ b/Internal/menu/Menu.h @@ -1,14 +1,15 @@ #pragma once /// @file -/// @brief Top-level in-game GUI: owns the main ImGui window and dispatches to each tab section. +/// @brief Top-level menu driver: owns the tab list and the per-frame frame loop, and drives whichever +/// backend is active through the abstract Menu::Backend interface. It names no concrete backend and +/// includes neither ImGui nor UCanvasGUI — all backend-specific chrome/widgets live in menu/backend/. #include "../settings/Settings.h" #include "../scripting/Events.h" #include "../utils/Input.h" -#include "../utils/Rgb.h" -#include "ui/UI.h" -#include "canvas/ZeroGUI.h" +#include "backend/Backend.h" +#include "backend/Backends.h" #include "sections/Misc.h" #include "sections/Exploits.h" #include "sections/Visuals.h" @@ -20,25 +21,33 @@ #include "sections/Discord.h" #include "sections/Debug.h" -#include - -#include - -/// @brief Top-level menu: owns the main window and routes each frame to the tab sections. namespace Menu { - static ImGuiWindowFlags windowFlags = ImGuiWindowFlags_NoCollapse; - static int tab = 0; - static ImGuiTabBarFlags tabFlags = ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_NoCloseWithMiddleMouseButton; + /// One menu tab: its label and the section function that draws it (through Menu::UI). + struct Tab + { + const char* label; + void (*fn)(); + }; - /// Canvas-backend window position (draggable) and active tab index. ImGui keeps its own state. - inline FVector2D canvasPos = {400.f, 200.f}; - inline int canvasTab = 0; + /// The tab order, shared by every backend (each renders it through its own window/tab chrome). + inline const Tab Tabs[] = { + {"Misc", Sections::MiscTab}, + {"Exploits", Sections::ExploitsTab}, + {"Visuals", Sections::VisualsTab}, + {"Aim", Sections::AimTab}, + {"Network", Sections::NetworkTab}, + {"Config", Sections::ConfigTab}, + {"Scripts", Sections::ScriptsTab}, + {"SDK", Sections::SdkTab}, + {"Discord", Sections::DiscordTab}, + {"Debug", Sections::DebugTab}, + }; - /// Flip menu visibility on the show/hotkey (or @p extra, e.g. the gamepad Start button), dispatching - /// MenuOpened/MenuClosed. Factored out so both the ImGui (Present) and Canvas (PostRender) paths share - /// one toggle; the two backends are mutually exclusive per frame, so this never double-fires. - inline void HandleToggle(bool extra = false) + /// Flip menu visibility on the show/hotkey (or @p extra — a backend's own signal, e.g. gamepad + /// Start), dispatching MenuOpened/MenuClosed. The two backends are mutually exclusive per frame, so + /// this never double-fires. + inline void HandleToggle(bool extra) { if (Input::Pressed(Settings.MENU.ShowHotkey) || extra) { @@ -47,205 +56,28 @@ namespace Menu } } - /// @brief Renders one full frame of the ImGui menu (from the Present hook, ImGui backend only). - /// Toggles menu visibility on the configured hotkey / gamepad Start, and when visible draws the main - /// window with its tab bar plus the optional ImGui demo/style-editor windows. - void Draw() + /// Drive one frame of the menu for a given hook @p phase. Selects the configured backend and, if it + /// is driven by this phase, runs its frame: setup, toggle, then the window + tab loop. This is the + /// only shared code that touches a backend, and it does so purely through the interface. + inline void Frame(Phase phase) { - // The watermark is its own Watermark feature now (drawn through the Render backend), so it - // follows the active renderer — including the streamproof external window — and isn't drawn here. - HandleToggle(ImGui::IsKeyPressed(ImGuiKey_GamepadStart)); - if (!Settings.MENU.ShowMenu) return; - - if (Settings.DEBUG.ShowDemoWindow) - { - ImGui::ShowDemoWindow(&Settings.DEBUG.ShowDemoWindow); - } - if (Settings.DEBUG.ShowStyleEditor) - { - ImGui::ShowStyleEditor(); - } - - // Tint the interactive accent slots each frame: the cycling RGB color when enabled, else the - // theme's default reds. The defaults are snapshotted once (before the first override) so - // toggling RGB back off restores the original per-slot shades instead of freezing on the last - // rainbow frame. - { - static constexpr ImGuiCol accentSlots[] = { - ImGuiCol_CheckMark, ImGuiCol_SliderGrab, ImGuiCol_SliderGrabActive, - ImGuiCol_Header, ImGuiCol_HeaderHovered, ImGuiCol_HeaderActive, - ImGuiCol_Tab, ImGuiCol_TabHovered, ImGuiCol_TabActive, ImGuiCol_TitleBgActive}; - - ImVec4* colors = ImGui::GetStyle().Colors; - - static const std::array defaults = [&] - { - std::array saved{}; - for (size_t i = 0; i < saved.size(); ++i) - saved[i] = colors[accentSlots[i]]; - return saved; - }(); - - if (Settings.MENU.Rgb) - { - const Color rgb = Rgb::Current(); - const ImVec4 accent(rgb.R, rgb.G, rgb.B, rgb.A); - for (ImGuiCol slot : accentSlots) - colors[slot] = accent; - } - else - { - for (size_t i = 0; i < defaults.size(); ++i) - colors[accentSlots[i]] = defaults[i]; - } - } + Select(Settings.MENU.Backend); + if (active->phase() != phase) return; // the backend picks its own hook - if (!ImGui::Begin("Splitgate Internal", &Settings.MENU.ShowMenu, windowFlags)) - { - ImGui::End(); - return; - }; - - if (!ImGui::BeginTabBar("MainTabBar", tabFlags)) - { - ImGui::End(); - return; - }; - - if (ImGui::BeginTabItem("Misc")) - { - Menu::Sections::MiscTab(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Exploits")) - { - Menu::Sections::ExploitsTab(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Visuals")) - { - Menu::Sections::VisualsTab(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Aim")) - { - Menu::Sections::AimTab(); - ImGui::EndTabItem(); - } + active->BeginFrame(); + HandleToggle(active->ExtraToggle()); - if (ImGui::BeginTabItem("Network")) + if (Settings.MENU.ShowMenu && active->BeginWindow("Splitgate Internal")) { - Menu::Sections::NetworkTab(); - ImGui::EndTabItem(); + for (const Tab& tab : Tabs) + if (active->BeginTab(tab.label)) + { + tab.fn(); + active->EndTab(); + } + active->EndWindow(); } - if (ImGui::BeginTabItem("Config")) - { - Menu::Sections::ConfigTab(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Scripts")) - { - Menu::Sections::ScriptsTab(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("SDK")) - { - Menu::Sections::SdkTab(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Discord")) - { - Menu::Sections::DiscordTab(); - ImGui::EndTabItem(); - } - - if (ImGui::BeginTabItem("Debug")) - { - Menu::Sections::DebugTab(); - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); - ImGui::End(); - }; - - /// @brief Renders one full frame of the Canvas (ZeroGUI) menu, drawn through Render::canvas from the - /// PostRender hook (Canvas backend only). Works everywhere the UE canvas is valid — in a match and at - /// the main menu / loading. Owns its own software cursor, so it never depends on ImGui. - void Tick() - { - ZeroGUI::Input::Handle(); // sample mouse/keyboard once per frame - - HandleToggle(); - if (!Settings.MENU.ShowMenu) return; - - // RGB accent: retint the red accent group from the rainbow when enabled, else the theme red. - { - const FLinearColor accent = Settings.MENU.Rgb - ? [] - { const Color c = Rgb::Current(); return FLinearColor{c.R, c.G, c.B, c.A}; }() - : FLinearColor{1.f, 0.f, 0.f, 1.f}; - ZeroGUI::Colors::MainColor = accent; - ZeroGUI::Colors::Window_Header = accent; - ZeroGUI::Colors::Button_Idle = accent; - ZeroGUI::Colors::Button_Hovered = accent; - ZeroGUI::Colors::Button_Active = accent; - ZeroGUI::Colors::Slider_Progress = accent; - } - - const FVector2D winSize{700.f, 500.f}; - if (!ZeroGUI::Window("Splitgate Internal", &canvasPos, winSize, true)) return; - - // Left column: one tab button per section (stacks under the header). - static const char* const tabs[] = {"Misc", "Exploits", "Visuals", "Aim", "Network", "Config", "Scripts", "SDK", "Discord", "Debug"}; - for (int i = 0; i < IM_ARRAYSIZE(tabs); i++) - if (ZeroGUI::ButtonTab(tabs[i], FVector2D{112.f, 30.f}, canvasTab == i)) - canvasTab = i; - - // Content column: only the active tab's section (so inactive sections don't draw over it). - ZeroGUI::NextColumn(130.f); - switch (canvasTab) - { - case 0: - Sections::MiscTab(); - break; - case 1: - Sections::ExploitsTab(); - break; - case 2: - Sections::VisualsTab(); - break; - case 3: - Sections::AimTab(); - break; - case 4: - Sections::NetworkTab(); - break; - case 5: - Sections::ConfigTab(); - break; - case 6: - Sections::ScriptsTab(); - break; - case 7: - Sections::SdkTab(); - break; - case 8: - Sections::DiscordTab(); - break; - case 9: - Sections::DebugTab(); - break; - } - - ZeroGUI::Render(); // drain deferred pop-ups (combo dropdowns, color swatches) on top - ZeroGUI::Draw_Cursor(true); // the Canvas menu's own cursor - }; -}; // namespace Menu \ No newline at end of file + active->EndFrame(); + } +} // namespace Menu From 114a3c72fbb251598dd0df0b0eb6b6e4a63e759d Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:28 +0200 Subject: [PATCH 19/54] chore(menu): remove the obsolete Widgets.h Menu::ToggleSetting lives in the UI facade now; sections call UI:: directly. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/Widgets.h | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 Internal/menu/Widgets.h diff --git a/Internal/menu/Widgets.h b/Internal/menu/Widgets.h deleted file mode 100644 index 3238f72..0000000 --- a/Internal/menu/Widgets.h +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -/// @file -/// @brief Menu helpers that tie ImGui controls to the event bus. - -#include "gui/Custom.h" // ImGui::ToggleButton -#include "../scripting/Events.h" - -namespace Menu -{ - /// A toggle that, when flipped, dispatches SettingsChanged tagged with its label (so handlers - /// and scripts know which setting/feature changed — payload.name = label, payload.value = 0/1). - /// @return true if the value changed this frame. - inline bool ToggleSetting(const char* label, bool* value) - { - if (!ImGui::ToggleButton(label, value)) return false; - Events::Dispatch(Events::Type::SettingsChanged, Events::Payload{.value = *value ? 1.f : 0.f, .name = label}); - return true; - } -} // namespace Menu From 80abd034099795d27fdc2ecb5226d75f4d05d399 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:41 +0200 Subject: [PATCH 20/54] refactor(menu): draw the ImGui menu from Menu::Frame(Present) Drop the imguiMenu identity check; MouseDrawCursor defaults off (the ImGui backend re-enables it itself). Co-Authored-By: Claude Opus 4.8 --- Internal/menu/gui/Gui.h | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Internal/menu/gui/Gui.h b/Internal/menu/gui/Gui.h index 1a3394f..a844243 100644 --- a/Internal/menu/gui/Gui.h +++ b/Internal/menu/gui/Gui.h @@ -146,24 +146,18 @@ namespace GUI if (!external) Render::Flush(); - const ImGuiViewport* mainViewport = ImGui::GetMainViewport(); - ImGui::SetNextWindowPos(ImVec2(mainViewport->WorkPos.x + 550, mainViewport->WorkPos.y + 20), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSize(ImVec2(550, 350), ImGuiCond_FirstUseEver); - - // Only the ImGui menu backend draws here (and paints ImGui's software cursor). The Canvas menu - // draws in PostRender and owns its own cursor, so ImGui must not also show one. Game-input - // capture stays keyed on ShowMenu alone (backend-agnostic) so input is blocked whenever the - // menu is open, regardless of which backend is drawing it. - const bool imguiMenu = Settings.MENU.Backend == MenuBackend::ImGui; - + // Game-input capture is keyed on ShowMenu (backend-agnostic) so input is blocked whenever the + // menu is open, regardless of which backend draws it. The menu cursor is a backend concern: + // default it off; the ImGui backend re-enables ImGui's software cursor itself when it draws. ImGuiIO& io = ImGui::GetIO(); (void)io; - io.MouseDrawCursor = Settings.MENU.ShowMenu && imguiMenu; + io.MouseDrawCursor = false; io.WantCaptureMouse = Settings.MENU.ShowMenu; io.WantTextInput = Settings.MENU.ShowMenu; io.WantCaptureKeyboard = Settings.MENU.ShowMenu; - if (imguiMenu) Menu::Draw(); + // Draw the menu for the Present-driven backend (the ImGui backend); a no-op if Canvas is active. + Menu::Frame(Menu::Phase::Present); ImGui::EndFrame(); ImGui::Render(); From 05e6a1b3feb2934e636be5743cfe5f01a22a90f3 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:41 +0200 Subject: [PATCH 21/54] refactor(hook): drive the canvas menu via Menu::Frame, select renderer first Replace the Menu::Tick gate with Menu::Frame(Phase::PostRender); run Render::Select unconditionally before it so the canvas menu draws through the selected backend even at the main menu. Co-Authored-By: Claude Opus 4.8 --- Internal/hook/functions/PostRender.h | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Internal/hook/functions/PostRender.h b/Internal/hook/functions/PostRender.h index 29a6e13..69741e8 100644 --- a/Internal/hook/functions/PostRender.h +++ b/Internal/hook/functions/PostRender.h @@ -42,8 +42,13 @@ namespace PostRender Engine::World = World; Engine::Canvas = Canvas; - if (Settings.MENU.Backend == MenuBackend::Canvas) - Menu::Tick(); // ZeroGUI menu, drawn immediately through Render::canvas + // Point the drawing backend at the selected renderer for this frame — the menu and the features + // both draw through Render::*. Selected unconditionally so the Canvas menu renders correctly even + // at the main menu (no player controller). ImGui-recorded commands are replayed in the Present hook. + Render::Select(Settings.MENU.Renderer); + + // Draw the menu for the PostRender-driven backend (the Canvas backend); a no-op if ImGui is active. + Menu::Frame(Menu::Phase::PostRender); if (PlayerController) { @@ -53,10 +58,6 @@ namespace PostRender // One shared actor pass per frame, consumed by the visual features below. ActorCache::Update(); - // Point the drawing backend at the selected renderer for this frame (features draw - // through Render::*). ImGui-recorded commands are replayed in the Present hook. - Render::Select(Settings.MENU.Renderer); - Features::Execute(); } From bd2813c096dade34d23d121fdca34437f14f0f8e Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:42 +0200 Subject: [PATCH 22/54] docs(settings): rename ZeroGUI to UCanvasGUI in the MenuBackend comment Co-Authored-By: Claude Opus 4.8 --- Internal/settings/Settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Internal/settings/Settings.h b/Internal/settings/Settings.h index 4b6efca..2b626c3 100644 --- a/Internal/settings/Settings.h +++ b/Internal/settings/Settings.h @@ -53,7 +53,7 @@ NLOHMANN_JSON_SERIALIZE_ENUM(RendererMode, { }) /// Which GUI engine draws the menu itself (independent of RendererMode, which is for the ESP/overlay). -/// ImGui draws in the Present hook; Canvas draws through the UE canvas (ZeroGUI) in PostRender. +/// ImGui draws in the Present hook; Canvas draws through the UE canvas (UCanvasGUI) in PostRender. enum class MenuBackend { ImGui, From cacc3b99aede7b77a4eb6c1b92a4e587d8e13577 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:42 +0200 Subject: [PATCH 23/54] chore(build): register the new menu headers, drop ZeroGUI/ZeroInput Add canvas/Colors.h, UCanvasGUI.h, components/*, and backend/*; remove the ZeroInput/ZeroGUI entries. Co-Authored-By: Claude Opus 4.8 --- Internal/Internal.vcxproj | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Internal/Internal.vcxproj b/Internal/Internal.vcxproj index 95d0554..4f1294e 100644 --- a/Internal/Internal.vcxproj +++ b/Internal/Internal.vcxproj @@ -264,9 +264,28 @@ - - + + + + + + + + + + + + + + + + + + + + + From 530111b1697f217f45968ae7b3085d3f825eb7b4 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:58 +0200 Subject: [PATCH 24/54] chore(menu): use UI::Count instead of ImGui's IM_ARRAYSIZE Co-Authored-By: Claude Opus 4.8 --- Internal/menu/sections/Aim.h | 2 +- Internal/menu/sections/Exploits.h | 2 +- Internal/menu/sections/Visuals.h | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Internal/menu/sections/Aim.h b/Internal/menu/sections/Aim.h index 04a601e..5e4e4b6 100644 --- a/Internal/menu/sections/Aim.h +++ b/Internal/menu/sections/Aim.h @@ -24,7 +24,7 @@ namespace Menu changed |= UI::SliderFloat("Smoothing", &a.AimSmooth, 0.05f, 1.f, "%.2f"); UI::Tooltip("1.0 snaps instantly; lower is smoother."); const char* bones[] = {"Head", "Chest", "Pelvis"}; - changed |= UI::Combo("Bone", &a.AimBone, bones, IM_ARRAYSIZE(bones)); + changed |= UI::Combo("Bone", &a.AimBone, bones, UI::Count(bones)); changed |= UI::Toggle("Team check##aim", &a.AimTeamCheck); changed |= UI::Toggle("Ignore bots", &a.IgnoreBots); UI::Tooltip("Aimbot and triggerbot target only real players, never AI bots."); diff --git a/Internal/menu/sections/Exploits.h b/Internal/menu/sections/Exploits.h index 5533ac5..558b518 100644 --- a/Internal/menu/sections/Exploits.h +++ b/Internal/menu/sections/Exploits.h @@ -24,7 +24,7 @@ namespace Menu UI::SeparatorText("Camera"); const char* cameras[] = {"First person", "Third person", "Free cam"}; int camera = static_cast(Settings.EXPLOITS.Camera); - if (UI::Combo("Camera", &camera, cameras, IM_ARRAYSIZE(cameras))) + if (UI::Combo("Camera", &camera, cameras, UI::Count(cameras))) { Settings.EXPLOITS.Camera = static_cast(camera); Events::Dispatch(Events::Type::SettingsChanged); diff --git a/Internal/menu/sections/Visuals.h b/Internal/menu/sections/Visuals.h index 8fbe830..34bbc9c 100644 --- a/Internal/menu/sections/Visuals.h +++ b/Internal/menu/sections/Visuals.h @@ -23,7 +23,7 @@ namespace Menu UI::SeparatorText("Renderer"); const char* renderers[] = {"UE Canvas", "ImGui (faster)", "None", "External (streamproof)"}; int renderer = static_cast(Settings.MENU.Renderer); - if (UI::Combo("Draw with", &renderer, renderers, IM_ARRAYSIZE(renderers))) + if (UI::Combo("Draw with", &renderer, renderers, UI::Count(renderers))) { Settings.MENU.Renderer = static_cast(renderer); changed = true; @@ -34,7 +34,7 @@ namespace Menu const char* backends[] = {"ImGui", "UE Canvas"}; int backend = static_cast(Settings.MENU.Backend); - if (UI::Combo("Menu backend", &backend, backends, IM_ARRAYSIZE(backends))) + if (UI::Combo("Menu backend", &backend, backends, UI::Count(backends))) { Settings.MENU.Backend = static_cast(backend); changed = true; From 36351e5975545096c3c883ac8d74654ba3e4603b Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:58 +0200 Subject: [PATCH 25/54] refactor(menu): route the Discord tab through the UI facade Drop the IsImGui-gated BeginDisabled; use UI::BeginDisabled/EndDisabled (a no-op where a backend has no disabled scope). Co-Authored-By: Claude Opus 4.8 --- Internal/menu/sections/Discord.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Internal/menu/sections/Discord.h b/Internal/menu/sections/Discord.h index efea45c..ac6e46b 100644 --- a/Internal/menu/sections/Discord.h +++ b/Internal/menu/sections/Discord.h @@ -29,10 +29,9 @@ namespace Menu } UI::Tooltip("Show a Rich Presence on your Discord profile. Updates with the live game state\n(map + K/D) every few seconds."); - // BeginDisabled/EndDisabled greys out the block in ImGui; the Canvas backend has no equivalent, - // so it's only applied under the ImGui backend. + // Grey out the live/info block while RPC is off (a no-op on backends with no disabled scope). const bool disabled = !Settings.MISC.DiscordRPCEnabled; - if (UI::IsImGui() && disabled) ImGui::BeginDisabled(); + UI::BeginDisabled(disabled); UI::SeparatorText("Live"); const char* state = DiscordRPC::GetState(); @@ -46,7 +45,7 @@ namespace Menu UI::Text("Image: %s", "icon"); UI::TextDisabled("App ID / Steam app id are runtime-only and set at startup."); - if (UI::IsImGui() && disabled) ImGui::EndDisabled(); + UI::EndDisabled(); } } // namespace Sections } // namespace Menu From fdf47aacb69c8422eb8c899068aae2a2787d605c Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:58 +0200 Subject: [PATCH 26/54] refactor(menu): route the Scripts tab through the UI facade The loaded-script list now draws via UI::BeginChild + UI:: widgets on both backends. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/sections/Scripts.h | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/Internal/menu/sections/Scripts.h b/Internal/menu/sections/Scripts.h index c2aff5c..5f6b6ba 100644 --- a/Internal/menu/sections/Scripts.h +++ b/Internal/menu/sections/Scripts.h @@ -31,30 +31,23 @@ namespace Menu UI::SeparatorText("Loaded Scripts"); - // The scrollable list with per-script Run buttons uses ImGui child regions — ImGui-only. - if (!UI::IsImGui()) - { - UI::Text("The loaded-script list uses the ImGui menu backend."); - return; - } - if (Scripts::scriptList.empty()) { - ImGui::TextDisabled("No scripts found. Drop a .py with a main() into the UserScripts folder, then Reload."); + UI::TextDisabled("No scripts found. Drop a .py with a main() into the UserScripts folder, then Reload."); return; } std::string toRun; - ImGui::BeginChild("ScriptList", ImVec2(0, 200), true); + UI::BeginChild("ScriptList", 0, 200); for (const auto& script : Scripts::scriptList) { - ImGui::PushID(script.c_str()); - if (ImGui::SmallButton("Run")) toRun = script; - ImGui::SameLine(); - ImGui::TextUnformatted(script.c_str()); - ImGui::PopID(); + UI::PushID(script.c_str()); + if (UI::SmallButton("Run")) toRun = script; + UI::SameLine(); + UI::Text("%s", script.c_str()); + UI::PopID(); } - ImGui::EndChild(); + UI::EndChild(); if (!toRun.empty()) Scripts::ExecuteUnloaded(toRun); } } // namespace Sections From c46a7833155afc5b8e1f04e202319fb91ba72d5e Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:58 +0200 Subject: [PATCH 27/54] refactor(menu): route the Config tab through the UI facade Profiles + share-code panels use UI::InputText/InputTextMultiline/clipboard instead of raw ImGui. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/sections/Config.h | 49 ++++++++++++++------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/Internal/menu/sections/Config.h b/Internal/menu/sections/Config.h index 6a572c0..fa2634f 100644 --- a/Internal/menu/sections/Config.h +++ b/Internal/menu/sections/Config.h @@ -59,53 +59,46 @@ namespace Menu } UI::Tooltip("Reset all hotkeys (menu, no-clip, aim, trigger) to their defaults."); - // Named profiles and share codes use InputText / multiline / clipboard — ImGui-only. - if (!UI::IsImGui()) - { - UI::Text("Profiles and share codes use the ImGui menu backend."); - return; - } - - ImGui::SeparatorText("Save current config"); + UI::SeparatorText("Save current config"); static char nameBuffer[64] = ""; - ImGui::SetNextItemWidth(200.f); - ImGui::InputText("##name", nameBuffer, sizeof(nameBuffer)); - ImGui::SameLine(); - if (ImGui::Button("Save as") && nameBuffer[0]) + UI::SetNextItemWidth(200.f); + UI::InputText("##name", nameBuffer, sizeof(nameBuffer)); + UI::SameLine(); + if (UI::Button("Save as") && nameBuffer[0]) { Profiles::Save(nameBuffer); nameBuffer[0] = '\0'; } - ImGui::SeparatorText("Profiles"); + UI::SeparatorText("Profiles"); std::string toLoad, toDelete; for (const auto& name : Profiles::List()) { - ImGui::PushID(name.c_str()); - if (ImGui::SmallButton("Load")) toLoad = name; - ImGui::SameLine(); - if (ImGui::SmallButton("Delete")) toDelete = name; - ImGui::SameLine(); - ImGui::TextUnformatted(name.c_str()); - ImGui::PopID(); + UI::PushID(name.c_str()); + if (UI::SmallButton("Load")) toLoad = name; + UI::SameLine(); + if (UI::SmallButton("Delete")) toDelete = name; + UI::SameLine(); + UI::Text("%s", name.c_str()); + UI::PopID(); } if (!toLoad.empty()) Profiles::Load(toLoad); if (!toDelete.empty()) Profiles::Delete(toDelete); - ImGui::SeparatorText("Share code"); - ImGui::Tooltip("A portable code for the current config. Copy to share; paste + import to apply."); - if (ImGui::Button("Copy current config")) ImGui::SetClipboardText(Profiles::Export().c_str()); + UI::SeparatorText("Share code"); + UI::Tooltip("A portable code for the current config. Copy to share; paste + import to apply."); + if (UI::Button("Copy current config")) UI::SetClipboardText(Profiles::Export().c_str()); static char codeBuffer[8192] = ""; - ImGui::InputTextMultiline("##code", codeBuffer, sizeof(codeBuffer), ImVec2(0, 80)); + UI::InputTextMultiline("##code", codeBuffer, sizeof(codeBuffer), 80); - if (ImGui::Button("Paste")) + if (UI::Button("Paste")) { - const char* clip = ImGui::GetClipboardText(); + const char* clip = UI::GetClipboardText(); if (clip) strncpy_s(codeBuffer, clip, _TRUNCATE); } - ImGui::SameLine(); - if (ImGui::Button("Import") && codeBuffer[0]) + UI::SameLine(); + if (UI::Button("Import") && codeBuffer[0]) { if (Profiles::Import(codeBuffer)) codeBuffer[0] = '\0'; } From 6b488f229ce3f524bc4f7d55ac088f513522bce6 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:58 +0200 Subject: [PATCH 28/54] refactor(menu): route the Debug tab through the UI facade Console input, feature tree and log child go through UI::; the log's auto-scroll (no facade equivalent) is dropped. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/sections/Debug.h | 41 +++++++++++++--------------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/Internal/menu/sections/Debug.h b/Internal/menu/sections/Debug.h index 8af3232..93a55e5 100644 --- a/Internal/menu/sections/Debug.h +++ b/Internal/menu/sections/Debug.h @@ -51,51 +51,42 @@ namespace Menu Shared::Utilities::OpenFolder(Shared::AppDataPath(SettingsHelper::AppFolder)); UI::Tooltip("Open the SplitgateInternal data folder (settings, logs, dumps)."); - // The console-command input, feature tree and log child region use ImGui InputText / - // TreeNode / child regions — ImGui-only. The Canvas backend shows a note. - if (!UI::IsImGui()) - { - UI::Text("Console command, feature tree and logs use the ImGui menu backend."); - return; - } - - ImGui::SeparatorText("Console command"); + UI::SeparatorText("Console command"); static char consoleBuffer[256] = ""; - ImGui::SetNextItemWidth(260.f); - ImGui::InputText("##console", consoleBuffer, sizeof(consoleBuffer)); - ImGui::SameLine(); - if (ImGui::Button("Run") && consoleBuffer[0] && Engine::PlayerController) + UI::SetNextItemWidth(260.f); + UI::InputText("##console", consoleBuffer, sizeof(consoleBuffer)); + UI::SameLine(); + if (UI::Button("Run") && consoleBuffer[0] && Engine::PlayerController) { Engine::PlayerController->SendToConsole(FString(std::string(consoleBuffer))); } - if (ImGui::TreeNode("Loaded Features")) + if (UI::TreeNode("Loaded Features")) { for (const auto& Feature : Features::Features) { - ImGui::BulletText(Feature->Name.c_str()); - ImGui::Tooltip(std::format("Init [{}], Enabled [{}]", Feature->Initialized, Feature->Enabled).c_str()); + UI::BulletText("%s", Feature->Name.c_str()); + UI::Tooltip(std::format("Init [{}], Enabled [{}]", Feature->Initialized, Feature->Enabled).c_str()); } - ImGui::TreePop(); + UI::TreePop(); } - if (ImGui::CollapsingHeader("Recent logs")) + if (UI::CollapsingHeader("Recent logs")) { - if (ImGui::Button("Copy##logs")) + if (UI::Button("Copy##logs")) { std::string out; for (const auto& line : Logger::Recent()) out += line + "\n"; - ImGui::SetClipboardText(out.c_str()); + UI::SetClipboardText(out.c_str()); } - ImGui::Tooltip("Copy the recent log lines to the clipboard."); + UI::Tooltip("Copy the recent log lines to the clipboard."); - ImGui::BeginChild("Logs", ImVec2(0, 200), true, ImGuiWindowFlags_HorizontalScrollbar); + UI::BeginChild("Logs", 0, 200); for (const auto& line : Logger::Recent()) - ImGui::TextUnformatted(line.c_str()); - if (ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) ImGui::SetScrollHereY(1.0f); // stick to bottom - ImGui::EndChild(); + UI::Text("%s", line.c_str()); + UI::EndChild(); } } } // namespace Sections From 272d0e2e740fbd34ab8bc8ecd29c14557c4ca780 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:58 +0200 Subject: [PATCH 29/54] refactor(menu): route the Network tab through the UI facade Redirect editor + mitmproxy config + request flow use UI:: widgets (ToggleButton->UI::Toggle, clipper->UI::ClippedList). Co-Authored-By: Claude Opus 4.8 --- Internal/menu/sections/Network.h | 81 +++++++++++++++----------------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/Internal/menu/sections/Network.h b/Internal/menu/sections/Network.h index 9367a58..1da3abe 100644 --- a/Internal/menu/sections/Network.h +++ b/Internal/menu/sections/Network.h @@ -39,34 +39,27 @@ namespace Menu /// SettingsChanged on any change. void NetworkTab() { - // The redirect-map editor and mitmproxy config are InputText / multiline / child-based, so - // this whole tab is ImGui-only; the Canvas backend shows a note. - if (!UI::IsImGui()) - { - UI::Text("The backend-redirect editor uses the ImGui menu backend."); - return; - } bool changed = false; - ImGui::SeparatorText("Backend redirect"); - ImGui::TextUnformatted("Proxy mode"); - ImGui::Tooltip("Internal: the DLL redirects in-process. Mitmproxy: the launcher spawns\nmitmproxy at startup (takes effect next launch). Manual: do nothing."); + UI::SeparatorText("Backend redirect"); + UI::Text("Proxy mode"); + UI::Tooltip("Internal: the DLL redirects in-process. Mitmproxy: the launcher spawns\nmitmproxy at startup (takes effect next launch). Manual: do nothing."); int mode = static_cast(Settings.NETWORK.Proxy); - changed |= ImGui::RadioButton("Manual", &mode, static_cast(ProxyMode::Manual)); - ImGui::SameLine(); - changed |= ImGui::RadioButton("Internal", &mode, static_cast(ProxyMode::Internal)); - ImGui::SameLine(); - changed |= ImGui::RadioButton("Mitmproxy", &mode, static_cast(ProxyMode::Mitmproxy)); + changed |= UI::RadioButton("Manual", &mode, static_cast(ProxyMode::Manual)); + UI::SameLine(); + changed |= UI::RadioButton("Internal", &mode, static_cast(ProxyMode::Internal)); + UI::SameLine(); + changed |= UI::RadioButton("Mitmproxy", &mode, static_cast(ProxyMode::Mitmproxy)); Settings.NETWORK.Proxy = static_cast(mode); // Existing redirects, each with a remove button. std::string toRemove; for (const auto& [original, target] : Settings.NETWORK.Redirects) { - ImGui::BulletText("%s -> %s", original.c_str(), target.c_str()); - ImGui::SameLine(); - if (ImGui::SmallButton(("X##" + original).c_str())) toRemove = original; + UI::BulletText("%s -> %s", original.c_str(), target.c_str()); + UI::SameLine(); + if (UI::SmallButton(("X##" + original).c_str())) toRemove = original; } if (!toRemove.empty()) { @@ -77,9 +70,9 @@ namespace Menu // Add a new original -> target ("host" or "host:port") mapping. static char originalBuffer[128] = ""; static char targetBuffer[128] = ""; - ImGui::InputText("Original host", originalBuffer, sizeof(originalBuffer)); - ImGui::InputText("Target host[:port]", targetBuffer, sizeof(targetBuffer)); - if (ImGui::Button("Add redirect") && originalBuffer[0] && targetBuffer[0]) + UI::InputText("Original host", originalBuffer, sizeof(originalBuffer)); + UI::InputText("Target host[:port]", targetBuffer, sizeof(targetBuffer)); + if (UI::Button("Add redirect") && originalBuffer[0] && targetBuffer[0]) { Settings.NETWORK.Redirects[originalBuffer] = targetBuffer; originalBuffer[0] = '\0'; @@ -87,15 +80,15 @@ namespace Menu changed = true; } - changed |= ImGui::ToggleButton("Bypass SSL verification", &Settings.NETWORK.BypassSslVerify); - ImGui::Tooltip("Force curl's cert/host verification off so a redirected host can serve a self-signed cert.\nDisables TLS verification for ALL curl traffic while on."); + changed |= UI::Toggle("Bypass SSL verification", &Settings.NETWORK.BypassSslVerify); + UI::Tooltip("Force curl's cert/host verification off so a redirected host can serve a self-signed cert.\nDisables TLS verification for ALL curl traffic while on."); // Mitmproxy script — a launcher-only setting (launcher.settings), so it lives outside // the DLL's SETTINGS. Only relevant when the launcher will spawn mitmproxy. if (Settings.NETWORK.Proxy == ProxyMode::Mitmproxy) { - ImGui::SeparatorText("Mitmproxy script"); - ImGui::Tooltip("How the launcher starts mitmdump (saved to launcher.settings, applied next launch).\n" + UI::SeparatorText("Mitmproxy script"); + UI::Tooltip("How the launcher starts mitmdump (saved to launcher.settings, applied next launch).\n" "Default: a generated addon (redirects above + TLS passthrough).\n" "Path: mitmdump -s . Inline: your python, run as the addon."); @@ -122,16 +115,16 @@ namespace Menu bool launcherChanged = false; int scriptMode = static_cast(mitm.ScriptMode); - launcherChanged |= ImGui::RadioButton("Default##mitm", &scriptMode, static_cast(Shared::MitmScriptMode::Default)); - ImGui::SameLine(); - launcherChanged |= ImGui::RadioButton("Path##mitm", &scriptMode, static_cast(Shared::MitmScriptMode::Path)); - ImGui::SameLine(); - launcherChanged |= ImGui::RadioButton("Inline##mitm", &scriptMode, static_cast(Shared::MitmScriptMode::Inline)); + launcherChanged |= UI::RadioButton("Default##mitm", &scriptMode, static_cast(Shared::MitmScriptMode::Default)); + UI::SameLine(); + launcherChanged |= UI::RadioButton("Path##mitm", &scriptMode, static_cast(Shared::MitmScriptMode::Path)); + UI::SameLine(); + launcherChanged |= UI::RadioButton("Inline##mitm", &scriptMode, static_cast(Shared::MitmScriptMode::Inline)); mitm.ScriptMode = static_cast(scriptMode); if (mitm.ScriptMode == Shared::MitmScriptMode::Path) { - if (ImGui::InputText("Script path", pathBuffer, sizeof(pathBuffer))) + if (UI::InputText("Script path", pathBuffer, sizeof(pathBuffer))) { mitm.ScriptPath = pathBuffer; launcherChanged = true; @@ -139,7 +132,7 @@ namespace Menu } else if (mitm.ScriptMode == Shared::MitmScriptMode::Inline) { - if (ImGui::InputTextMultiline("Inline python", inlineBuffer, sizeof(inlineBuffer), ImVec2(0, 160))) + if (UI::InputTextMultiline("Inline python", inlineBuffer, sizeof(inlineBuffer), 160)) { mitm.InlineScript = inlineBuffer; launcherChanged = true; @@ -147,31 +140,31 @@ namespace Menu } else { - ImGui::TextDisabled("Runs the bundled scripts/default_proxy.py (redirects above + TLS passthrough)."); + UI::TextDisabled("Runs the bundled scripts/default_proxy.py (redirects above + TLS passthrough)."); } - launcherChanged |= ImGui::ToggleButton("Show mitmproxy window", &mitm.ShowConsole); + launcherChanged |= UI::Toggle("Show mitmproxy window", &mitm.ShowConsole); if (launcherChanged) LauncherConfigFile().Save(); } - ImGui::SeparatorText("HTTP logging"); - changed |= ImGui::ToggleButton("Log HTTP calls", &Settings.NETWORK.HttpLogging); - changed |= ImGui::ToggleButton("Also log to http.log", &Settings.NETWORK.HttpLogToFile); - changed |= ImGui::ToggleButton("Redirected hosts only", &Settings.NETWORK.HttpLogRedirectedOnly); + UI::SeparatorText("HTTP logging"); + changed |= UI::Toggle("Log HTTP calls", &Settings.NETWORK.HttpLogging); + changed |= UI::Toggle("Also log to http.log", &Settings.NETWORK.HttpLogToFile); + changed |= UI::Toggle("Redirected hosts only", &Settings.NETWORK.HttpLogRedirectedOnly); // Live request flow — populated while HTTP logging is on. - if (ImGui::CollapsingHeader("Request flow")) + if (UI::CollapsingHeader("Request flow")) { - if (ImGui::SmallButton("Clear")) Network::Http::Clear(); + if (UI::SmallButton("Clear")) Network::Http::Clear(); const auto requests = Network::Http::Recent(); - ImGui::BeginChild("RequestFlow", ImVec2(0, 200), true, ImGuiWindowFlags_HorizontalScrollbar); + UI::BeginChild("RequestFlow", 0, 200); if (requests.empty() && !Settings.NETWORK.HttpLogging) - ImGui::TextDisabled("Enable \"Log HTTP calls\" to capture requests."); + UI::TextDisabled("Enable \"Log HTTP calls\" to capture requests."); for (const auto& request : requests) - ImGui::TextUnformatted(request.c_str()); - ImGui::EndChild(); + UI::Text("%s", request.c_str()); + UI::EndChild(); } if (changed) Events::Dispatch(Events::Type::SettingsChanged); From 44726c3be27612cb9a7a49fbb4627f742c611509 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:59 +0200 Subject: [PATCH 30/54] refactor(menu): route the SDK tab through the UI facade Object/class/name-pool/instance explorers use UI::BeginChild + UI::ClippedList + UI::Selectable. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/sections/Sdk.h | 165 +++++++++++++++-------------------- 1 file changed, 72 insertions(+), 93 deletions(-) diff --git a/Internal/menu/sections/Sdk.h b/Internal/menu/sections/Sdk.h index 0281b0b..4bc4b30 100644 --- a/Internal/menu/sections/Sdk.h +++ b/Internal/menu/sections/Sdk.h @@ -41,24 +41,17 @@ namespace Menu /// @brief Renders the SDK tab. void SdkTab() { - // The object explorer is search boxes + clipped, scrollable result lists (InputText / child / - // clipper), so this whole tab is ImGui-only; the Canvas backend shows a note. - if (!UI::IsImGui()) - { - UI::Text("The SDK object explorer uses the ImGui menu backend."); - return; - } - ImGui::SeparatorText("Object search"); - ImGui::Tooltip("Scan every GObject and list those whose full name contains the text.\nOn demand (a full walk, like Dump GObjects)."); + UI::SeparatorText("Object search"); + UI::Tooltip("Scan every GObject and list those whose full name contains the text.\nOn demand (a full walk, like Dump GObjects)."); static char nameFilter[128] = ""; static std::vector nameResults; static int nameTotal = 0; - ImGui::SetNextItemWidth(260.f); - ImGui::InputText("##namefilter", nameFilter, sizeof(nameFilter)); - ImGui::SameLine(); - if (ImGui::Button("Search names") && nameFilter[0]) + UI::SetNextItemWidth(260.f); + UI::InputText("##namefilter", nameFilter, sizeof(nameFilter)); + UI::SameLine(); + if (UI::Button("Search names") && nameFilter[0]) { nameResults.clear(); nameTotal = 0; @@ -76,51 +69,46 @@ namespace Menu } Logger::Log("INFO", std::format("[SDK] {} objects match \"{}\" ({} shown)", nameTotal, needle, nameResults.size())); } - ImGui::SameLine(); - if (ImGui::Button("Copy##names")) + UI::SameLine(); + if (UI::Button("Copy##names")) { std::string out; for (const auto& row : nameResults) out += std::format("[{}] {}\n", row.index, row.name); - ImGui::SetClipboardText(out.c_str()); + UI::SetClipboardText(out.c_str()); } - ImGui::Tooltip("Copy the listed results to the clipboard."); + UI::Tooltip("Copy the listed results to the clipboard."); if (!nameResults.empty()) { - ImGui::Text("%d match(es)%s", nameTotal, nameTotal > (int)nameResults.size() ? " (first 1000)" : ""); - ImGui::BeginChild("NameResults", ImVec2(0, 180), true, ImGuiWindowFlags_HorizontalScrollbar); - // Only lay out the rows actually on screen (a fixed-height Text list), so a 1000-row - // result doesn't cost 1000 widgets every frame. - ImGuiListClipper clipper; - clipper.Begin(static_cast(nameResults.size())); - while (clipper.Step()) - for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) - ImGui::Text("[%d] %s", nameResults[i].index, nameResults[i].name.c_str()); - ImGui::EndChild(); + UI::Text("%d match(es)%s", nameTotal, nameTotal > (int)nameResults.size() ? " (first 1000)" : ""); + UI::BeginChild("NameResults", 0, 180); + UI::ClippedList(static_cast(nameResults.size()), [&](int i) + { UI::Text("[%d] %s", nameResults[i].index, nameResults[i].name.c_str()); }); + UI::EndChild(); } - ImGui::SeparatorText("Class search"); - ImGui::Tooltip("Filter the cached list of every class (shared with the Misc spawn picker; built\nonce, Refresh to rebuild). Click a row to copy the exact name."); + UI::SeparatorText("Class search"); + UI::Tooltip("Filter the cached list of every class (shared with the Misc spawn picker; built\nonce, Refresh to rebuild). Click a row to copy the exact name."); static char classFilter[128] = ""; static std::vector classFiltered; static std::string lastClassKey = "\x01"; // sentinel: forces the first filter build static size_t lastClassCacheSize = SIZE_MAX; // re-filter when the cache is rebuilt - ImGui::SetNextItemWidth(260.f); - ImGui::InputText("##classfilter", classFilter, sizeof(classFilter)); - ImGui::SameLine(); - if (ImGui::Button("Refresh##classes")) ClassCache::Rebuild(); - ImGui::SameLine(); - if (ImGui::Button("Copy##classes")) + UI::SetNextItemWidth(260.f); + UI::InputText("##classfilter", classFilter, sizeof(classFilter)); + UI::SameLine(); + if (UI::Button("Refresh##classes")) ClassCache::Rebuild(); + UI::SameLine(); + if (UI::Button("Copy##classes")) { std::string out; const std::string needle = classFilter; for (const auto& entry : ClassCache::Get()) if (needle.empty() || entry.name.find(needle) != std::string::npos) out += entry.name + "\n"; - ImGui::SetClipboardText(out.c_str()); + UI::SetClipboardText(out.c_str()); } - ImGui::Tooltip("Copy the filtered class names to the clipboard."); + UI::Tooltip("Copy the filtered class names to the clipboard."); { const auto& classes = ClassCache::Get(); if (classFilter != lastClassKey || classes.size() != lastClassCacheSize) @@ -134,21 +122,18 @@ namespace Menu classFiltered.push_back(i); } - ImGui::Text("%d / %d classes", static_cast(classFiltered.size()), static_cast(classes.size())); - ImGui::BeginChild("ClassResults", ImVec2(0, 160), true, ImGuiWindowFlags_HorizontalScrollbar); - ImGuiListClipper clipper; - clipper.Begin(static_cast(classFiltered.size())); - while (clipper.Step()) - for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) - { + UI::Text("%d / %d classes", static_cast(classFiltered.size()), static_cast(classes.size())); + UI::BeginChild("ClassResults", 0, 160); + UI::ClippedList(static_cast(classFiltered.size()), [&](int i) + { const std::string& name = classes[classFiltered[i]].name; - if (ImGui::Selectable(name.c_str())) ImGui::SetClipboardText(name.c_str()); - } - ImGui::EndChild(); + if (UI::Selectable(name.c_str())) UI::SetClipboardText(name.c_str()); + }); + UI::EndChild(); } - ImGui::SeparatorText("Name pool (FNames)"); - ImGui::Tooltip("Search every interned FName, including names for content that isn't loaded\n(e.g. map/level names to travel to). Built once, Refresh to rebuild. Click a row to copy."); + UI::SeparatorText("Name pool (FNames)"); + UI::Tooltip("Search every interned FName, including names for content that isn't loaded\n(e.g. map/level names to travel to). Built once, Refresh to rebuild. Click a row to copy."); { static char nameFilter2[128] = ""; static bool caseSensitive = false; // default: case-insensitive @@ -159,15 +144,15 @@ namespace Menu static size_t lastNameCacheSize = SIZE_MAX; // re-filter when the cache is rebuilt static bool lastCase = false, lastRegex = false; - ImGui::SetNextItemWidth(260.f); - ImGui::InputText("##namepoolfilter", nameFilter2, sizeof(nameFilter2)); - ImGui::SameLine(); - if (ImGui::Button("Refresh##names")) NameCache::Rebuild(); + UI::SetNextItemWidth(260.f); + UI::InputText("##namepoolfilter", nameFilter2, sizeof(nameFilter2)); + UI::SameLine(); + if (UI::Button("Refresh##names")) NameCache::Rebuild(); - ImGui::Checkbox("Case sensitive##names", &caseSensitive); - ImGui::SameLine(); - ImGui::Checkbox("Regex##names", &useRegex); - ImGui::Tooltip("ECMAScript regex, matched as a search (unanchored, so a bare pattern behaves like\n\"contains\"). Anchor with ^ and $ to constrain: ^/Game/Maps/[^/]+$ matches a map\npackage but not the assets nested under it."); + UI::Checkbox("Case sensitive##names", &caseSensitive); + UI::SameLine(); + UI::Checkbox("Regex##names", &useRegex); + UI::Tooltip("ECMAScript regex, matched as a search (unanchored, so a bare pattern behaves like\n\"contains\"). Anchor with ^ and $ to constrain: ^/Game/Maps/[^/]+$ matches a map\npackage but not the assets nested under it."); // Re-filter only when an input changes (text, cache, or a toggle), not every frame. const auto& allNames = NameCache::Get(); @@ -218,42 +203,39 @@ namespace Menu } if (useRegex && !regexError.empty()) - ImGui::TextColored(ImVec4(1.f, 0.4f, 0.4f, 1.f), "regex error: %s", regexError.c_str()); + UI::Text("regex error: %s", regexError.c_str()); - if (ImGui::Button("Copy##namepool")) + if (UI::Button("Copy##namepool")) { std::string out; for (int idx : nameFiltered) out += allNames[idx] + "\n"; - ImGui::SetClipboardText(out.c_str()); + UI::SetClipboardText(out.c_str()); } - ImGui::Tooltip("Copy the filtered names to the clipboard."); - ImGui::SameLine(); - ImGui::Text("%d / %d names", static_cast(nameFiltered.size()), static_cast(allNames.size())); + UI::Tooltip("Copy the filtered names to the clipboard."); + UI::SameLine(); + UI::Text("%d / %d names", static_cast(nameFiltered.size()), static_cast(allNames.size())); - ImGui::BeginChild("NamePoolResults", ImVec2(0, 160), true, ImGuiWindowFlags_HorizontalScrollbar); - ImGuiListClipper clipper; - clipper.Begin(static_cast(nameFiltered.size())); - while (clipper.Step()) - for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) - { + UI::BeginChild("NamePoolResults", 0, 160); + UI::ClippedList(static_cast(nameFiltered.size()), [&](int i) + { const std::string& name = allNames[nameFiltered[i]]; - if (ImGui::Selectable(name.c_str())) ImGui::SetClipboardText(name.c_str()); - } - ImGui::EndChild(); + if (UI::Selectable(name.c_str())) UI::SetClipboardText(name.c_str()); + }); + UI::EndChild(); } - ImGui::SeparatorText("Class instances"); - ImGui::Tooltip("Resolve a class by name, then list its live instances (IsA).\nAccepts a full name (\"Class PortalWars.PortalWarsCharacter\") or a bare class name."); + UI::SeparatorText("Class instances"); + UI::Tooltip("Resolve a class by name, then list its live instances (IsA).\nAccepts a full name (\"Class PortalWars.PortalWarsCharacter\") or a bare class name."); static char className[128] = ""; static std::vector instanceResults; static int instanceTotal = 0; static std::string classStatus; - ImGui::SetNextItemWidth(260.f); - ImGui::InputText("##classname", className, sizeof(className)); - ImGui::SameLine(); - if (ImGui::Button("List instances") && className[0]) + UI::SetNextItemWidth(260.f); + UI::InputText("##classname", className, sizeof(className)); + UI::SameLine(); + if (UI::Button("List instances") && className[0]) { instanceResults.clear(); instanceTotal = 0; @@ -282,29 +264,26 @@ namespace Menu } Logger::Log("INFO", "[SDK] " + classStatus); } - ImGui::SameLine(); - if (ImGui::Button("Copy##instances")) + UI::SameLine(); + if (UI::Button("Copy##instances")) { std::string out; for (const auto& row : instanceResults) out += std::format("[{}] 0x{:x} {}\n", row.index, row.address, row.name); - ImGui::SetClipboardText(out.c_str()); + UI::SetClipboardText(out.c_str()); } - ImGui::Tooltip("Copy the listed instances (index, address, name) to the clipboard."); - if (!classStatus.empty()) ImGui::TextUnformatted(classStatus.c_str()); + UI::Tooltip("Copy the listed instances (index, address, name) to the clipboard."); + if (!classStatus.empty()) UI::Text("%s", classStatus.c_str()); if (!instanceResults.empty()) { - ImGui::BeginChild("InstanceResults", ImVec2(0, 180), true, ImGuiWindowFlags_HorizontalScrollbar); - ImGuiListClipper clipper; - clipper.Begin(static_cast(instanceResults.size())); - while (clipper.Step()) - for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) - ImGui::Text("[%d] 0x%llx %s", instanceResults[i].index, static_cast(instanceResults[i].address), instanceResults[i].name.c_str()); - ImGui::EndChild(); + UI::BeginChild("InstanceResults", 0, 180); + UI::ClippedList(static_cast(instanceResults.size()), [&](int i) + { UI::Text("[%d] 0x%llx %s", instanceResults[i].index, static_cast(instanceResults[i].address), instanceResults[i].name.c_str()); }); + UI::EndChild(); } - ImGui::SeparatorText("Dump"); - if (ImGui::Button("Dump GObjects")) + UI::SeparatorText("Dump"); + if (UI::Button("Dump GObjects")) { fs::path dumpsDir = Shared::AppDataPath(SettingsHelper::AppFolder) / "Dumps"; if (!fs::exists(dumpsDir)) fs::create_directories(dumpsDir); @@ -333,7 +312,7 @@ namespace Menu Logger::Log("SUCCESS", msg); if (Engine::PlayerController) Engine::PlayerController->SendChatMessage(FString(msg)); } - ImGui::Tooltip("Write every GObject (index + full name) to Dumps/GObjects.txt."); + UI::Tooltip("Write every GObject (index + full name) to Dumps/GObjects.txt."); } } // namespace Sections } // namespace Menu From 0ffba3d69be9fc1ede116e0c756d6f8b43c72ed3 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:13:59 +0200 Subject: [PATCH 31/54] refactor(menu): route the Misc tab through the UI facade FOV/speed, and the searchable load-into-map / spawn / cosmetics dropdowns, use UI:: rich widgets on both backends. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/sections/Misc.h | 490 ++++++++++++++++------------------ 1 file changed, 234 insertions(+), 256 deletions(-) diff --git a/Internal/menu/sections/Misc.h b/Internal/menu/sections/Misc.h index d7e57b4..b94a7fd 100644 --- a/Internal/menu/sections/Misc.h +++ b/Internal/menu/sections/Misc.h @@ -39,7 +39,7 @@ namespace Menu UI::SeparatorText("Player"); - if (UI::IsImGui()) ImGui::SetNextItemWidth(180.f); + UI::SetNextItemWidth(180.f); UI::SliderFloat("##fov", &Settings.EXPLOITS.FOV, 80.0f, 160.0f, "FOV %.0f"); UI::SameLine(); if (UI::SmallButton("Reset##fov")) @@ -48,8 +48,8 @@ namespace Menu Events::Dispatch(Events::Type::SettingsChanged); } - if (UI::IsImGui() && !isInGame) ImGui::BeginDisabled(); - if (UI::IsImGui()) ImGui::SetNextItemWidth(180.f); + UI::BeginDisabled(!isInGame); + UI::SetNextItemWidth(180.f); UI::SliderFloat("##speed", &Settings.EXPLOITS.PlayerSpeed, 0.2f, 4.f, "Speed %.2f"); UI::SameLine(); if (UI::SmallButton("Reset##speed")) @@ -57,296 +57,274 @@ namespace Menu Settings.EXPLOITS.PlayerSpeed = ExploitsSettings{}.PlayerSpeed; Events::Dispatch(Events::Type::SettingsChanged); } - if (UI::IsImGui() && !isInGame) ImGui::EndDisabled(); + UI::EndDisabled(); UI::SeparatorText("Game"); - // Load-into-map, the spawn picker and the cosmetics pickers are all searchable dropdowns - // (BeginCombo + InputText + clipped Selectable list) — ImGui-only. In the Canvas backend the - // tab shows a short note instead. - if (!UI::IsImGui()) - { - UI::Text("Load into map, spawning and cosmetics use the ImGui menu backend."); - } - else + // "Load into map" is usable whenever you're out of a game (e.g. back in the menu after a + // match), disabled only while already in one. The dropdown beside it picks the target level + // SwitchLevel travels to. These are the game's Content/Maps package names (leaf, no path or + // .BuiltData); Simulation_Alpha (the firing range) is index 0 and the default. The selection + // rides in on the event payload. Enumerated from the FName pool via the SDK tab. + static const char* const levels[] = { + // Simulation / firing-range maps + "Simulation_Alpha", // default + "Simulation_Bravo", + "Simulation_Charlie", + "Simulation_Delta", + "Simulation_Echo", + "Simulation_Foxtrot", + "Simulation_Golf", + "Simulation_Hotel", + "Simulation_India", + "Simulation_Juliet", + // Arena maps + "Abyss", + "Atlantis", + "Crag", + "Foregone_Destruction", + "Helix", + "Highwind", + "Impact", + "Karman_Station", + "Lavawell", + "Oasis", + "Olympus", + "Pantheon", + "Silo", + "Stadium", + // Special / system maps + "MainMenu", + "Lobby", + "Tutorial", + "PracticeRange", + "TravelMap", + "Forge_Island", + "Forge_Flat_Earth", + "Abyss_Cinematics", + // Blockout / work-in-progress maps + "Maya_Blockout", + "Noboru_Temple_Blockout", + "Decay_Blockout_WIP", + "Drift_Blockout_WIP", + "Titan_Blockout_WIP", + "Toxic_Blockout_Wip", + "Vessel_Blockout_WIP", + "Vintage_Blockout_WIP", + }; + // Searchable dropdown (same pattern as the spawn picker): filter the static list, click a + // row to select. selectedLevel points into the static array above, so it stays valid to hand + // to the event payload. + static std::vector levelFiltered; + static char levelSearch[128] = ""; + static std::string levelLastKey = "\x01"; // sentinel: forces the first filter build + static const char* selectedLevel = levels[0]; + + UI::BeginDisabled(isInGame); + UI::SetNextItemWidth(220.f); + if (UI::BeginCombo("##level", selectedLevel)) { - // "Load into map" is usable whenever you're out of a game (e.g. back in the menu after a - // match), disabled only while already in one. The dropdown beside it picks the target level - // SwitchLevel travels to. These are the game's Content/Maps package names (leaf, no path or - // .BuiltData); Simulation_Alpha (the firing range) is index 0 and the default. The selection - // rides in on the event payload. Enumerated from the FName pool via the SDK tab. - static const char* const levels[] = { - // Simulation / firing-range maps - "Simulation_Alpha", // default - "Simulation_Bravo", - "Simulation_Charlie", - "Simulation_Delta", - "Simulation_Echo", - "Simulation_Foxtrot", - "Simulation_Golf", - "Simulation_Hotel", - "Simulation_India", - "Simulation_Juliet", - // Arena maps - "Abyss", - "Atlantis", - "Crag", - "Foregone_Destruction", - "Helix", - "Highwind", - "Impact", - "Karman_Station", - "Lavawell", - "Oasis", - "Olympus", - "Pantheon", - "Silo", - "Stadium", - // Special / system maps - "MainMenu", - "Lobby", - "Tutorial", - "PracticeRange", - "TravelMap", - "Forge_Island", - "Forge_Flat_Earth", - "Abyss_Cinematics", - // Blockout / work-in-progress maps - "Maya_Blockout", - "Noboru_Temple_Blockout", - "Decay_Blockout_WIP", - "Drift_Blockout_WIP", - "Titan_Blockout_WIP", - "Toxic_Blockout_Wip", - "Vessel_Blockout_WIP", - "Vintage_Blockout_WIP", - }; - // Searchable dropdown (same pattern as the spawn picker): filter the static list, click a - // row to select. selectedLevel points into the static array above, so it stays valid to hand - // to the event payload. - static std::vector levelFiltered; - static char levelSearch[128] = ""; - static std::string levelLastKey = "\x01"; // sentinel: forces the first filter build - static const char* selectedLevel = levels[0]; + UI::SetNextItemWidth(-1.f); + UI::InputTextHint("##levelsearch", "filter maps...", levelSearch, sizeof(levelSearch)); - if (isInGame) ImGui::BeginDisabled(); - ImGui::SetNextItemWidth(220.f); - if (ImGui::BeginCombo("##level", selectedLevel)) + // Rebuild the filtered index list only when the search text changes. + if (levelSearch != levelLastKey) { - ImGui::SetNextItemWidth(-1.f); - ImGui::InputTextWithHint("##levelsearch", "filter maps...", levelSearch, sizeof(levelSearch)); + levelLastKey = levelSearch; + levelFiltered.clear(); + const std::string needle = levelSearch; + for (int i = 0; i < UI::Count(levels); i++) + if (needle.empty() || std::string(levels[i]).find(needle) != std::string::npos) + levelFiltered.push_back(i); + } - // Rebuild the filtered index list only when the search text changes. - if (levelSearch != levelLastKey) - { - levelLastKey = levelSearch; - levelFiltered.clear(); - const std::string needle = levelSearch; - for (int i = 0; i < IM_ARRAYSIZE(levels); i++) - if (needle.empty() || std::string(levels[i]).find(needle) != std::string::npos) - levelFiltered.push_back(i); - } + UI::BeginChild("##levellist", 240, 200); + UI::ClippedList(static_cast(levelFiltered.size()), [&](int r) { + const char* name = levels[levelFiltered[r]]; + if (UI::Selectable(name, name == selectedLevel)) selectedLevel = name; + }); + UI::EndChild(); + UI::EndCombo(); + } + UI::SameLine(); + if (UI::Button("Load into map")) + { + Events::Payload payload; + payload.name = selectedLevel; + Events::Dispatch(Events::Type::LoadIntoMap, payload); + } + UI::EndDisabled(); - ImGui::BeginChild("##levellist", ImVec2(240, 200)); - ImGuiListClipper clipper; - clipper.Begin(static_cast(levelFiltered.size())); - while (clipper.Step()) - for (int r = clipper.DisplayStart; r < clipper.DisplayEnd; r++) - { - const char* name = levels[levelFiltered[r]]; - if (ImGui::Selectable(name, name == selectedLevel)) selectedLevel = name; - } - ImGui::EndChild(); - ImGui::EndCombo(); - } - ImGui::SameLine(); - if (ImGui::Button("Load into map")) - { - Events::Payload payload; - payload.name = selectedLevel; - Events::Dispatch(Events::Type::LoadIntoMap, payload); - } - if (isInGame) ImGui::EndDisabled(); + UI::SameLine(); + UI::BeginDisabled(!isInGame); + if (UI::Button("Respawn")) + if (auto* character = reinterpret_cast(Engine::PlayerController->Character)) + character->RequestSuicide(); + UI::EndDisabled(); + UI::Tooltip("Kill your character so it respawns (RequestSuicide)."); - ImGui::SameLine(); - if (!isInGame) ImGui::BeginDisabled(); - if (ImGui::Button("Respawn")) - if (auto* character = reinterpret_cast(Engine::PlayerController->Character)) - character->RequestSuicide(); - if (!isInGame) ImGui::EndDisabled(); - ImGui::Tooltip("Kill your character so it respawns (RequestSuicide)."); + // Spawn picker: a searchable dropdown of spawnable actor classes (bots, pawns, guns, ...) + // scanned from GObjects, plus a Spawn button that spawns the selection in front of you. + { + static std::vector filtered; // indices into the shared ClassCache + static char search[128] = ""; + static std::string lastKey = "\x01"; // sentinel: forces the first filter build + static size_t lastCacheSize = SIZE_MAX; // re-filter when the cache is rebuilt + static std::string selected; + static const char* keywords[] = {"Bot", "Pawn", "Gun", "Weapon", "Character", "Projectile", "Grenade", "Vehicle"}; - // Spawn picker: a searchable dropdown of spawnable actor classes (bots, pawns, guns, ...) - // scanned from GObjects, plus a Spawn button that spawns the selection in front of you. + UI::SetNextItemWidth(240.f); + if (UI::BeginCombo("##spawnclass", selected.empty() ? "Spawn class..." : selected.c_str())) { - static std::vector filtered; // indices into the shared ClassCache - static char search[128] = ""; - static std::string lastKey = "\x01"; // sentinel: forces the first filter build - static size_t lastCacheSize = SIZE_MAX; // re-filter when the cache is rebuilt - static std::string selected; - static const char* keywords[] = {"Bot", "Pawn", "Gun", "Weapon", "Character", "Projectile", "Grenade", "Vehicle"}; - - ImGui::SetNextItemWidth(240.f); - if (ImGui::BeginCombo("##spawnclass", selected.empty() ? "Spawn class..." : selected.c_str())) - { - const auto& classes = ClassCache::Get(); // shared, built once + const auto& classes = ClassCache::Get(); // shared, built once - ImGui::SetNextItemWidth(-1.f); - ImGui::InputTextWithHint("##spawnsearch", "filter: bot, gun, pawn...", search, sizeof(search)); + UI::SetNextItemWidth(-1.f); + UI::InputTextHint("##spawnsearch", "filter: bot, gun, pawn...", search, sizeof(search)); - // Rebuild the filtered index list only when the search or the underlying cache changes. - if (search != lastKey || classes.size() != lastCacheSize) + // Rebuild the filtered index list only when the search or the underlying cache changes. + if (search != lastKey || classes.size() != lastCacheSize) + { + lastKey = search; + lastCacheSize = classes.size(); + filtered.clear(); + const std::string needle = search; + for (int i = 0; i < static_cast(classes.size()); i++) { - lastKey = search; - lastCacheSize = classes.size(); - filtered.clear(); - const std::string needle = search; - for (int i = 0; i < static_cast(classes.size()); i++) - { - const std::string& name = classes[i].name; - bool spawnable = false; // narrow to bots/pawns/guns/... so it's a spawn list, not every class - for (const char* kw : keywords) - if (name.find(kw) != std::string::npos) - { - spawnable = true; - break; - } - if (!spawnable) continue; - if (!needle.empty() && name.find(needle) == std::string::npos) continue; - filtered.push_back(i); - } + const std::string& name = classes[i].name; + bool spawnable = false; // narrow to bots/pawns/guns/... so it's a spawn list, not every class + for (const char* kw : keywords) + if (name.find(kw) != std::string::npos) + { + spawnable = true; + break; + } + if (!spawnable) continue; + if (!needle.empty() && name.find(needle) == std::string::npos) continue; + filtered.push_back(i); } - - // Clip to the visible rows so a few-thousand-class list isn't laid out in full each frame. - ImGui::BeginChild("##spawnlist", ImVec2(340, 220)); - ImGuiListClipper clipper; - clipper.Begin(static_cast(filtered.size())); - while (clipper.Step()) - for (int r = clipper.DisplayStart; r < clipper.DisplayEnd; r++) - { - const std::string& name = classes[filtered[r]].name; - if (ImGui::Selectable(name.c_str(), name == selected)) selected = name; - } - ImGui::EndChild(); - ImGui::EndCombo(); } - ImGui::SameLine(); - if (ImGui::SmallButton("Refresh##spawn")) ClassCache::Rebuild(); - ImGui::SameLine(); - if (!isInGame || selected.empty()) ImGui::BeginDisabled(); - if (ImGui::Button("Spawn") && isInGame && !selected.empty() && Engine::PlayerController) + // Clip to the visible rows so a few-thousand-class list isn't laid out in full each frame. + UI::BeginChild("##spawnlist", 340, 220); + UI::ClippedList(static_cast(filtered.size()), [&](int r) { + const std::string& name = classes[filtered[r]].name; + if (UI::Selectable(name.c_str(), name == selected)) selected = name; + }); + UI::EndChild(); + UI::EndCombo(); + } + UI::SameLine(); + if (UI::SmallButton("Refresh##spawn")) ClassCache::Rebuild(); + + UI::SameLine(); + UI::BeginDisabled(!isInGame || selected.empty()); + if (UI::Button("Spawn") && isInGame && !selected.empty() && Engine::PlayerController) + { + UObject* cls = Engine::GObjects->FindObject(selected.c_str()); + auto* pawn = Engine::PlayerController->AcknowledgedPawn; + if (cls && pawn) { - UObject* cls = Engine::GObjects->FindObject(selected.c_str()); - auto* pawn = Engine::PlayerController->AcknowledgedPawn; - if (cls && pawn) - { - FVector loc = reinterpret_cast(pawn)->K2_GetActorLocation(); - loc.X += 200.f; // a bit in front of the player - AActor* actor = SpawnActor(reinterpret_cast(Engine::PlayerController), reinterpret_cast(cls), - loc, ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn, nullptr); - Logger::Log(actor ? "SUCCESS" : "ERROR", (actor ? "Spawned " : "Spawn failed: ") + selected); - } - else - Logger::Log("ERROR", "Spawn: class not found: " + selected); + FVector loc = reinterpret_cast(pawn)->K2_GetActorLocation(); + loc.X += 200.f; // a bit in front of the player + AActor* actor = SpawnActor(reinterpret_cast(Engine::PlayerController), reinterpret_cast(cls), + loc, ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn, nullptr); + Logger::Log(actor ? "SUCCESS" : "ERROR", (actor ? "Spawned " : "Spawn failed: ") + selected); } - if (!isInGame || selected.empty()) ImGui::EndDisabled(); - ImGui::Tooltip("Pick a spawnable class (searchable — bots, pawns, guns, ...) and Spawn it in front\nof you. Refresh rescans GObjects. Uses the deferred SpawnActor."); + else + Logger::Log("ERROR", "Spawn: class not found: " + selected); } + UI::EndDisabled(); + UI::Tooltip("Pick a spawnable class (searchable — bots, pawns, guns, ...) and Spawn it in front\nof you. Refresh rescans GObjects. Uses the deferred SpawnActor."); + } - ImGui::SeparatorText("Cosmetics"); - { - static std::vector skinFiltered; // indices into the shared ClassCache - static char skinSearch[128] = ""; - static std::string skinLastKey = "\x01"; - static size_t skinLastCacheSize = SIZE_MAX; - static std::string skinSelected; + UI::SeparatorText("Cosmetics"); + { + static std::vector skinFiltered; // indices into the shared ClassCache + static char skinSearch[128] = ""; + static std::string skinLastKey = "\x01"; + static size_t skinLastCacheSize = SIZE_MAX; + static std::string skinSelected; - ImGui::SetNextItemWidth(240.f); - if (ImGui::BeginCombo("##skinclass", skinSelected.empty() ? "Skin class..." : skinSelected.c_str())) - { - const auto& classes = ClassCache::Get(); + UI::SetNextItemWidth(240.f); + if (UI::BeginCombo("##skinclass", skinSelected.empty() ? "Skin class..." : skinSelected.c_str())) + { + const auto& classes = ClassCache::Get(); - ImGui::SetNextItemWidth(-1.f); - ImGui::InputTextWithHint("##skinsearch", "filter: skin name...", skinSearch, sizeof(skinSearch)); + UI::SetNextItemWidth(-1.f); + UI::InputTextHint("##skinsearch", "filter: skin name...", skinSearch, sizeof(skinSearch)); - if (skinSearch != skinLastKey || classes.size() != skinLastCacheSize) + if (skinSearch != skinLastKey || classes.size() != skinLastCacheSize) + { + skinLastKey = skinSearch; + skinLastCacheSize = classes.size(); + skinFiltered.clear(); + const std::string needle = skinSearch; + for (int i = 0; i < static_cast(classes.size()); i++) { - skinLastKey = skinSearch; - skinLastCacheSize = classes.size(); - skinFiltered.clear(); - const std::string needle = skinSearch; - for (int i = 0; i < static_cast(classes.size()); i++) - { - const std::string& name = classes[i].name; - if (name.find("Skin") == std::string::npos) continue; // skins only - if (!needle.empty() && name.find(needle) == std::string::npos) continue; - skinFiltered.push_back(i); - } + const std::string& name = classes[i].name; + if (name.find("Skin") == std::string::npos) continue; // skins only + if (!needle.empty() && name.find(needle) == std::string::npos) continue; + skinFiltered.push_back(i); } - - ImGui::BeginChild("##skinlist", ImVec2(340, 220)); - ImGuiListClipper clipper; - clipper.Begin(static_cast(skinFiltered.size())); - while (clipper.Step()) - for (int r = clipper.DisplayStart; r < clipper.DisplayEnd; r++) - { - const std::string& name = classes[skinFiltered[r]].name; - if (ImGui::Selectable(name.c_str(), name == skinSelected)) skinSelected = name; - } - ImGui::EndChild(); - ImGui::EndCombo(); } - ImGui::SameLine(); - if (ImGui::SmallButton("Refresh##skin")) ClassCache::Rebuild(); - ImGui::SameLine(); - if (!isInGame || skinSelected.empty()) ImGui::BeginDisabled(); - if (ImGui::Button("Apply skin") && isInGame && !skinSelected.empty() && Engine::PlayerController) + UI::BeginChild("##skinlist", 340, 220); + UI::ClippedList(static_cast(skinFiltered.size()), [&](int r) { + const std::string& name = classes[skinFiltered[r]].name; + if (UI::Selectable(name.c_str(), name == skinSelected)) skinSelected = name; + }); + UI::EndChild(); + UI::EndCombo(); + } + UI::SameLine(); + if (UI::SmallButton("Refresh##skin")) ClassCache::Rebuild(); + + UI::SameLine(); + UI::BeginDisabled(!isInGame || skinSelected.empty()); + if (UI::Button("Apply skin") && isInGame && !skinSelected.empty() && Engine::PlayerController) + { + UObject* cls = Engine::GObjects->FindObject(skinSelected.c_str()); + auto* character = reinterpret_cast(Engine::PlayerController->Character); + if (cls && character) { - UObject* cls = Engine::GObjects->FindObject(skinSelected.c_str()); - auto* character = reinterpret_cast(Engine::PlayerController->Character); - if (cls && character) - { - character->CharacterSkinClass = reinterpret_cast(cls); - character->UpdateSkins(); - Logger::Log("SUCCESS", "Applied skin: " + skinSelected); - } - else - Logger::Log("ERROR", "Apply skin: class not found: " + skinSelected); + character->CharacterSkinClass = reinterpret_cast(cls); + character->UpdateSkins(); + Logger::Log("SUCCESS", "Applied skin: " + skinSelected); } - if (!isInGame || skinSelected.empty()) ImGui::EndDisabled(); - ImGui::Tooltip("Pick a skin class, then apply it to your character, gun or jetpack.\nClient-side (sets the *SkinClass + UpdateSkins); the server may re-assert your real skins. Refresh rescans classes."); + else + Logger::Log("ERROR", "Apply skin: class not found: " + skinSelected); + } + UI::EndDisabled(); + UI::Tooltip("Pick a skin class, then apply it to your character, gun or jetpack.\nClient-side (sets the *SkinClass + UpdateSkins); the server may re-assert your real skins. Refresh rescans classes."); - // Apply the selected class to the gun / jetpack too (they use their own skin types). - if (!isInGame || skinSelected.empty()) ImGui::BeginDisabled(); - if (ImGui::Button("Apply gun skin") && isInGame && !skinSelected.empty() && Engine::PlayerController) + // Apply the selected class to the gun / jetpack too (they use their own skin types). + UI::BeginDisabled(!isInGame || skinSelected.empty()); + if (UI::Button("Apply gun skin") && isInGame && !skinSelected.empty() && Engine::PlayerController) + { + UObject* cls = Engine::GObjects->FindObject(skinSelected.c_str()); + auto* character = reinterpret_cast(Engine::PlayerController->Character); + if (cls && character && character->CurrentWeapon) { - UObject* cls = Engine::GObjects->FindObject(skinSelected.c_str()); - auto* character = reinterpret_cast(Engine::PlayerController->Character); - if (cls && character && character->CurrentWeapon) - { - character->CurrentWeapon->WeaponSkinClass = reinterpret_cast(cls); - character->CurrentWeapon->UpdateSkins(); - Logger::Log("SUCCESS", "Applied gun skin: " + skinSelected); - } + character->CurrentWeapon->WeaponSkinClass = reinterpret_cast(cls); + character->CurrentWeapon->UpdateSkins(); + Logger::Log("SUCCESS", "Applied gun skin: " + skinSelected); } - ImGui::SameLine(); - if (ImGui::Button("Apply jetpack skin") && isInGame && !skinSelected.empty() && Engine::PlayerController) + } + UI::SameLine(); + if (UI::Button("Apply jetpack skin") && isInGame && !skinSelected.empty() && Engine::PlayerController) + { + UObject* cls = Engine::GObjects->FindObject(skinSelected.c_str()); + auto* character = reinterpret_cast(Engine::PlayerController->Character); + if (cls && character) { - UObject* cls = Engine::GObjects->FindObject(skinSelected.c_str()); - auto* character = reinterpret_cast(Engine::PlayerController->Character); - if (cls && character) - { - character->JetpackSkinClass = reinterpret_cast(cls); - character->UpdateSkins(); - Logger::Log("SUCCESS", "Applied jetpack skin: " + skinSelected); - } + character->JetpackSkinClass = reinterpret_cast(cls); + character->UpdateSkins(); + Logger::Log("SUCCESS", "Applied jetpack skin: " + skinSelected); } - if (!isInGame || skinSelected.empty()) ImGui::EndDisabled(); } + UI::EndDisabled(); + } - } // else (ImGui-only Game / Cosmetics) UI::SeparatorText("Program"); if (UI::Button("Toggle Console")) From 726fe3d917b44aa3b5bf519e4bd3ace25885f7b9 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:15:16 +0200 Subject: [PATCH 32/54] fix(network): silence C4244 in WinHttp::Narrow with an explicit cast Build wchar->char narrowing explicitly (static_cast per char) instead of the std::string(begin,end) range ctor, which tripped C4244 in . Hosts are ASCII, so the narrowing is safe; the cast just makes it intentional. Co-Authored-By: Claude Opus 4.8 --- Internal/network/WinHttpHook.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Internal/network/WinHttpHook.h b/Internal/network/WinHttpHook.h index 4b41ac2..dc02974 100644 --- a/Internal/network/WinHttpHook.h +++ b/Internal/network/WinHttpHook.h @@ -32,7 +32,11 @@ namespace Network::WinHttp } inline std::string Narrow(const std::wstring& s) { - return std::string(s.begin(), s.end()); + std::string out; + out.reserve(s.size()); + for (wchar_t c : s) + out.push_back(static_cast(c)); // explicit ASCII narrowing (hosts are ASCII) + return out; } using Connect_t = HINTERNET(WINAPI*)(HINTERNET, LPCWSTR, INTERNET_PORT, DWORD); From 61053bcd5e4114e442ddb349ca92edbc80473dbf Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:31:06 +0200 Subject: [PATCH 33/54] feat(events): add EnteredGame/EnteredLobby, dispatched on the IsInGame flip PostRender compares IsInGame against the previous frame and dispatches the transition, so features can react to entering a match / returning to the lobby instead of polling every frame. Co-Authored-By: Claude Opus 4.8 --- Internal/hook/functions/PostRender.h | 8 +++++++- Internal/scripting/Events.h | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Internal/hook/functions/PostRender.h b/Internal/hook/functions/PostRender.h index 69741e8..3883029 100644 --- a/Internal/hook/functions/PostRender.h +++ b/Internal/hook/functions/PostRender.h @@ -37,7 +37,13 @@ namespace PostRender // off-thread fault (a null check can't detect a freed object). IsInGame is evaluated here on the // game thread, where the controller is valid. Engine::PlayerController = PlayerController; - Engine::IsInGame = PlayerController && PlayerController->IsInGame(); + + // Dispatch the in-game / in-lobby transition (event-driven features like DiscordPresence run + // off this instead of polling every frame). Compared against the previous frame's cached value. + const bool inGame = PlayerController && PlayerController->IsInGame(); + if (inGame != Engine::IsInGame) + Events::Dispatch(inGame ? Events::Type::EnteredGame : Events::Type::EnteredLobby); + Engine::IsInGame = inGame; Engine::World = World; Engine::Canvas = Canvas; diff --git a/Internal/scripting/Events.h b/Internal/scripting/Events.h index 5295cb7..d2dc417 100644 --- a/Internal/scripting/Events.h +++ b/Internal/scripting/Events.h @@ -31,6 +31,8 @@ namespace Events SettingsChanged, ///< a setting was changed in the menu MenuOpened, ///< the GUI was shown MenuClosed, ///< the GUI was hidden + EnteredGame, ///< the local player entered an active match (IsInGame false -> true) + EnteredLobby, ///< the local player left the match, back to lobby/menu (IsInGame true -> false) HotKeyPressed, ///< a key/mouse button went down while the game is focused; payload.value = vk code // Game events dispatched from ProcessEvent (payload.source = the calling UObject). From 4e4d16ab6ecbcf70fb87cc3afe4abffbce29f062 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:31:06 +0200 Subject: [PATCH 34/54] feat(features): allow multiple triggers per feature and forward the event/payload to Run() Feature::Event becomes Feature::Triggers (a vector, default {Render}); Execute runs a feature if Render is among its triggers, and Init subscribes each non-Render trigger to the bus. RunFeature forwards the triggering event + payload to Run(), which gains an overload chain (Run() / Run(event) / Run(event, payload)) so existing nullary overrides are untouched. Co-Authored-By: Claude Opus 4.8 --- Internal/features/AnnounceToggles.h | 2 +- Internal/features/Feature.h | 21 +++++++++++++++------ Internal/features/FeatureRunner.h | 19 +++++++++++-------- Internal/features/Features.h | 12 +++++++----- Tests/FeaturesTests.cpp | 6 +++--- 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/Internal/features/AnnounceToggles.h b/Internal/features/AnnounceToggles.h index c7d4970..7910a18 100644 --- a/Internal/features/AnnounceToggles.h +++ b/Internal/features/AnnounceToggles.h @@ -48,7 +48,7 @@ class AnnounceToggles : public Feature AnnounceToggles() { Name = "AnnounceToggles"; - Event = Events::Type::SettingsChanged; // driven by menu changes, not per-frame + Triggers = {Events::Type::SettingsChanged}; // driven by menu changes, not per-frame UpdateEnabled(); Log("Created"); }; diff --git a/Internal/features/Feature.h b/Internal/features/Feature.h index 54d21ab..2a2123d 100644 --- a/Internal/features/Feature.h +++ b/Internal/features/Feature.h @@ -6,6 +6,8 @@ /// drive it. See the class comment below for the full Init/Check/Run/Destroy /// contract. +#include + #include "../settings/Settings.h" #include "../utils/Logger.h" #include "../scripting/Events.h" @@ -36,10 +38,11 @@ class Feature bool OneTime = false; // Run() fires once per enable instead of every frame std::string Name = "BaseFeature"; ///< human-readable id, used in Log() output - // Which event drives this feature. Render runs every frame (the fast loop in - // Features::Execute); any other event subscribes it to the event bus and runs - // it when that event is dispatched (e.g. Events::Type::PlayerDeath). - Events::Type Event = Events::Type::Render; + // Which events drive this feature. Render runs every frame (the fast loop in + // Features::Execute); every other trigger subscribes it to the event bus and runs it when that + // event is dispatched (e.g. Events::Type::PlayerDeath). A feature may list several triggers, and + // may mix Render with bus events. + std::vector Triggers{Events::Type::Render}; // Bookkeeping owned by Features::Execute; subclasses should not touch these. bool applied = false; // Run() has been applied and not yet reverted by Destroy() @@ -58,8 +61,14 @@ class Feature virtual bool Check() = 0; /// Revert whatever Run() applied. Called once on the enabled -> disabled edge. virtual void Destroy() = 0; - /// Apply the effect. Called every frame while enabled (or once per enable if OneTime). - virtual void Run() = 0; + + /// Apply the effect. Called every frame while enabled (or once per enable if OneTime) for a + /// Render trigger, and on each dispatch of any other trigger event. The runner always calls the + /// (event, payload) form; the default chain lets a subclass override whichever arity it needs — + /// Run() to ignore the trigger, Run(event) for the event only, or Run(event, payload) for both. + virtual void Run() {} + virtual void Run(Events::Type) { Run(); } + virtual void Run(Events::Type event, const Events::Payload&) { Run(event); } /// Emit a "[Name] message" line, but only when Settings.DEBUG.FeaturesLogging is on. void Log(std::string message) diff --git a/Internal/features/FeatureRunner.h b/Internal/features/FeatureRunner.h index 7ba0878..abcb2a7 100644 --- a/Internal/features/FeatureRunner.h +++ b/Internal/features/FeatureRunner.h @@ -7,6 +7,7 @@ #include #include +#include #include "Feature.h" @@ -24,10 +25,11 @@ namespace Features // Drives a single feature once: init on first use, refresh Enabled, skip if // idle-disabled, then Run() while enabled (once, if OneTime) or Destroy() // exactly once on the enabled -> disabled edge. Used both by the per-frame - // render loop and by the event bus (for event-driven features). Features - // whose Check() still returns Enabled keep their previous behavior (they just - // skip on disable and never reach Destroy). - inline void RunFeature(Feature& feature) + // render loop and by the event bus (for event-driven features). The triggering + // @p event and @p payload are forwarded to Run() (defaulting to the Render loop's + // empty payload). Features whose Check() still returns Enabled keep their previous + // behavior (they just skip on disable and never reach Destroy). + inline void RunFeature(Feature& feature, Events::Type event = Events::Type::Render, const Events::Payload& payload = {}) { try { @@ -55,7 +57,7 @@ namespace Features { if (!feature.OneTime || !feature.hasRun) { - feature.Run(); + feature.Run(event, payload); feature.hasRun = true; } feature.applied = true; @@ -77,13 +79,14 @@ namespace Features } // Runs every render-driven feature. Called once per rendered frame from - // PostRender. Event-driven features (Event != "render") are skipped here and - // run from the event bus instead. + // PostRender. Features without a Render trigger are skipped here and run from + // the event bus instead (a feature may have both). inline void Execute() { for (const auto& feature : Features) { - if (feature->Event == Events::Type::Render) + const auto& triggers = feature->Triggers; + if (std::find(triggers.begin(), triggers.end(), Events::Type::Render) != triggers.end()) { RunFeature(*feature); } diff --git a/Internal/features/Features.h b/Internal/features/Features.h index 0c06c20..4641bb2 100644 --- a/Internal/features/Features.h +++ b/Internal/features/Features.h @@ -88,14 +88,16 @@ namespace Features // Autosave: persist on every change when enabled. if (Settings.MISC.AutoSave) SettingsHelper::File().Save(); }); - // Subscribe event-driven features (Event != "render") to the event bus; - // render features run from Features::Execute each frame instead. + // Subscribe each non-Render trigger to the event bus, forwarding the event + payload to the + // feature's Run(); Render triggers run from Features::Execute each frame instead. A feature + // may list several triggers (e.g. DiscordPresence on EnteredGame + EnteredLobby). for (auto& feature : Features) { - if (feature->Event != Events::Type::Render) + for (Events::Type trigger : feature->Triggers) { - Events::Register(feature->Event, [ptr = feature.get()] - { RunFeature(*ptr); }); + if (trigger == Events::Type::Render) continue; + Events::Register(trigger, [ptr = feature.get(), trigger](const Events::Payload& payload) + { RunFeature(*ptr, trigger, payload); }); } } diff --git a/Tests/FeaturesTests.cpp b/Tests/FeaturesTests.cpp index 5ccb771..2d5d905 100644 --- a/Tests/FeaturesTests.cpp +++ b/Tests/FeaturesTests.cpp @@ -218,7 +218,7 @@ namespace TEST_F(FeaturesTest, ExecuteSkipsEventDrivenFeatures) { FakeFeature* f = add(); - f->Event = Events::Type::Shutdown; + f->Triggers = {Events::Type::Shutdown}; f->Enabled = true; Features::Execute(); EXPECT_EQ(0, f->runCount); @@ -229,9 +229,9 @@ namespace TEST_F(FeaturesTest, EventFeatureRunsWhenDispatched) { FakeFeature f; - f.Event = Events::Type::Shutdown; + f.Triggers = {Events::Type::Shutdown}; f.Enabled = true; - Events::Register(f.Event, [&] + Events::Register(Events::Type::Shutdown, [&] { Features::RunFeature(f); }); Events::Dispatch(Events::Type::Render); From 2874f87a13095026799f2b0ad91289c23559376f Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:31:06 +0200 Subject: [PATCH 35/54] refactor(features): drive DiscordPresence off EnteredGame/EnteredLobby Subscribe to the two transition events instead of running every frame; drop the 5s throttle and refresh presence only when the in-game/in-lobby state changes (Run(event) logs which). Co-Authored-By: Claude Opus 4.8 --- Internal/features/DiscordPresence.h | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/Internal/features/DiscordPresence.h b/Internal/features/DiscordPresence.h index 5a56d44..e576c01 100644 --- a/Internal/features/DiscordPresence.h +++ b/Internal/features/DiscordPresence.h @@ -2,23 +2,19 @@ /// @file /// The DiscordPresence feature: refreshes the Discord Rich Presence with live game state (map + -/// K/D) on a throttle, so the presence stays current without spamming Discord every frame. +/// K/D) whenever the local player enters a match or returns to the lobby. Event-driven (EnteredGame +/// / EnteredLobby) rather than per-frame, so it only touches Discord on an actual state change. #include "Feature.h" #include "../discord/rpc.h" -#include - class DiscordPresence : public Feature { - private: - std::chrono::steady_clock::time_point lastUpdate{}; - static constexpr int IntervalSeconds = 5; - public: DiscordPresence() { Name = "DiscordPresence"; + Triggers = {Events::Type::EnteredGame, Events::Type::EnteredLobby}; UpdateEnabled(); Log("Created"); }; @@ -42,12 +38,10 @@ class DiscordPresence : public Feature void Destroy() { }; - void Run() + /// Push the current game state to Discord on the in-game / in-lobby transition that fired us. + void Run(Events::Type event) override { - const auto now = std::chrono::steady_clock::now(); - if (std::chrono::duration_cast(now - lastUpdate).count() < IntervalSeconds) return; - - lastUpdate = now; + Log(event == Events::Type::EnteredGame ? "entered game" : "entered lobby"); DiscordRPC::UpdateGameState(); }; }; From d12b18080a09933b9f730b3698055d0dd61ee163 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:31:06 +0200 Subject: [PATCH 36/54] docs(features): document multi-trigger features and the Run() overloads Co-Authored-By: Claude Opus 4.8 --- docs/features.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/features.md b/docs/features.md index 3992208..af8db45 100644 --- a/docs/features.md +++ b/docs/features.md @@ -74,12 +74,21 @@ rather than every frame. It re-arms when disabled. ### Event-driven features -Every feature has an `Event` (an `Events::Type`, default `Render`). `Render` -features run from the per-frame `Features::Execute` loop. A feature with any -other `Event` is instead subscribed to the [event bus](scripting.md#events) in +Every feature has `Triggers` — a `std::vector`, default +`{Render}`. `Render` triggers run from the per-frame `Features::Execute` loop. +Every other trigger is subscribed to the [event bus](scripting.md#events) in `Features::Init` and driven by `Features::RunFeature` when that event is -dispatched — e.g. set `Event = Events::Type::PlayerDeath` to run a feature when -the player dies. Same toggle/Check/Run/Destroy contract, just a different clock. +dispatched. A feature may list **several** triggers (and may mix `Render` with +bus events) — e.g. `DiscordPresence` sets +`Triggers = {Events::Type::EnteredGame, Events::Type::EnteredLobby}` to refresh +only when the in-game/in-lobby state flips. Same toggle/Check/Run/Destroy +contract, just a different clock. + +The triggering event and its `Events::Payload` are forwarded to `Run()`. Override +whichever arity you need: `Run()` to ignore the trigger (the common case), +`Run(Events::Type event)` for the event only, or +`Run(Events::Type event, const Events::Payload& payload)` for both — the base +delegates down the chain, so existing nullary `Run()` overrides keep working. One-shot **actions** that don't need a toggle (like the "Load into map" button) are better as a plain event handler than a feature — see the `LoadIntoMap` From 4aefbdcbf12d293c97bef18bead0a49bf522c265 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:38:06 +0200 Subject: [PATCH 37/54] feat(features): add a per-feature Run() throttle (ThrottleMs) Set Feature::ThrottleMs to cap how often Run() fires; the runner gates on ThrottleReady() (0 = every tick/event, so existing features are unchanged). Replaces the hand-rolled chrono throttle pattern. Co-Authored-By: Claude Opus 4.8 --- Internal/features/Feature.h | 18 ++++++++++++++++-- Internal/features/FeatureRunner.h | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Internal/features/Feature.h b/Internal/features/Feature.h index 2a2123d..095ad86 100644 --- a/Internal/features/Feature.h +++ b/Internal/features/Feature.h @@ -7,6 +7,7 @@ /// contract. #include +#include #include "../settings/Settings.h" #include "../utils/Logger.h" @@ -36,6 +37,7 @@ class Feature bool Enabled = false; // toggled from settings via UpdateEnabled() bool Initialized = false; ///< set true by Init() once setup succeeds; features guard on this bool OneTime = false; // Run() fires once per enable instead of every frame + int ThrottleMs = 0; ///< minimum gap between Run() calls in ms; 0 = every tick/event std::string Name = "BaseFeature"; ///< human-readable id, used in Log() output // Which events drive this feature. Render runs every frame (the fast loop in @@ -45,11 +47,23 @@ class Feature std::vector Triggers{Events::Type::Render}; // Bookkeeping owned by Features::Execute; subclasses should not touch these. - bool applied = false; // Run() has been applied and not yet reverted by Destroy() - bool hasRun = false; // a OneTime feature has already run this enable cycle + bool applied = false; // Run() has been applied and not yet reverted by Destroy() + bool hasRun = false; // a OneTime feature has already run this enable cycle + std::chrono::steady_clock::time_point lastRun{}; // when Run() last fired, for ThrottleMs Feature() {}; + /// Whether the throttle interval has elapsed since the last Run() (and, if so, stamps it now). + /// Always true when ThrottleMs <= 0. Owned by the runner; features don't call this. + bool ThrottleReady() + { + if (ThrottleMs <= 0) return true; + const auto now = std::chrono::steady_clock::now(); + if (now - lastRun < std::chrono::milliseconds(ThrottleMs)) return false; + lastRun = now; + return true; + } + /// One-time setup (resolve game objects, cache originals). Must set /// Initialized to reflect success; the runner calls it on first use. virtual void Init() = 0; diff --git a/Internal/features/FeatureRunner.h b/Internal/features/FeatureRunner.h index abcb2a7..6050814 100644 --- a/Internal/features/FeatureRunner.h +++ b/Internal/features/FeatureRunner.h @@ -55,7 +55,7 @@ namespace Features if (feature.Enabled) { - if (!feature.OneTime || !feature.hasRun) + if ((!feature.OneTime || !feature.hasRun) && feature.ThrottleReady()) { feature.Run(event, payload); feature.hasRun = true; From 91a6bf4e5f188e195a6be82c8d15083968b576bd Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:38:06 +0200 Subject: [PATCH 38/54] refactor(features): refresh DiscordPresence K/D on PlayerKilled Add PlayerKilled to the triggers so K/D updates on kills, not just the in-game/in-lobby transition; UpdateGameState's change-guard means it only pushes to Discord on the local player's own kills/deaths. Co-Authored-By: Claude Opus 4.8 --- Internal/features/DiscordPresence.h | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Internal/features/DiscordPresence.h b/Internal/features/DiscordPresence.h index e576c01..1142695 100644 --- a/Internal/features/DiscordPresence.h +++ b/Internal/features/DiscordPresence.h @@ -2,8 +2,11 @@ /// @file /// The DiscordPresence feature: refreshes the Discord Rich Presence with live game state (map + -/// K/D) whenever the local player enters a match or returns to the lobby. Event-driven (EnteredGame -/// / EnteredLobby) rather than per-frame, so it only touches Discord on an actual state change. +/// K/D). Event-driven rather than per-frame — it runs on the in-game/in-lobby transitions +/// (EnteredGame / EnteredLobby) and on each kill (PlayerKilled) so the K/D stays current. +/// UpdateGameState only pushes to Discord when the string actually changes, so PlayerKilled +/// (which fires for every kill in the match) effectively updates only on the local player's +/// own kills/deaths. #include "Feature.h" #include "../discord/rpc.h" @@ -14,7 +17,7 @@ class DiscordPresence : public Feature DiscordPresence() { Name = "DiscordPresence"; - Triggers = {Events::Type::EnteredGame, Events::Type::EnteredLobby}; + Triggers = {Events::Type::EnteredGame, Events::Type::EnteredLobby, Events::Type::PlayerKilled}; UpdateEnabled(); Log("Created"); }; @@ -38,10 +41,10 @@ class DiscordPresence : public Feature void Destroy() { }; - /// Push the current game state to Discord on the in-game / in-lobby transition that fired us. - void Run(Events::Type event) override + /// Push the current game state to Discord on the event that fired us (a state transition or a + /// kill). UpdateGameState no-ops when the resulting presence string is unchanged. + void Run() override { - Log(event == Events::Type::EnteredGame ? "entered game" : "entered lobby"); DiscordRPC::UpdateGameState(); }; }; From 39bb74118e89e5dcf6b3a2abe15d183376cf4eed Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:54:19 +0200 Subject: [PATCH 39/54] feat(native): add a stable ActorId accessor (UObject InternalIndex) A plain field read (no ProcessEvent) giving each actor a lifetime-stable key for per-actor state / a future spawn-despawn diff. Available but not yet wired to a consumer. Co-Authored-By: Claude Opus 4.8 --- Internal/native/ActorId.h | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Internal/native/ActorId.h diff --git a/Internal/native/ActorId.h b/Internal/native/ActorId.h new file mode 100644 index 0000000..dd124c1 --- /dev/null +++ b/Internal/native/ActorId.h @@ -0,0 +1,17 @@ +#pragma once + +/// @file +/// @brief Stable per-actor key: the UObject `InternalIndex` (its slot in the GObjects array), a plain +/// field read — no ProcessEvent. It stays constant for the lifetime of the object, so it keys +/// per-actor state across frames (a spawn/despawn diff, cached per-actor data, ...) more reliably +/// than a raw pointer, which can be reused once the actor is freed. + +#include + +#include "../ue/Engine.h" + +/// The actor's stable GObjects slot id, or 0 for a null actor. Accepts any UObject (actors upcast). +inline uint32_t ActorId(const UObject* actor) +{ + return actor ? actor->InternalIndex : 0; +} From 0aafd13775ca63b13360a71c5b57ae2152b34209 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:54:19 +0200 Subject: [PATCH 40/54] feat(hook): add the GuardHook guard-page hooking primitive Code-patch-free hook (PAGE_GUARD + a vectored exception handler that redirects RIP to the detour and single-steps to re-arm), an alternative to the MinHook trampoline for a code-integrity-sensitive, low-frequency target. Opt-in primitive; not wired to a live hook yet. Documents its one-exception-per-call cost and the re-arm race. Co-Authored-By: Claude Opus 4.8 --- Internal/hook/GuardHook.h | 144 ++++++++++++++++++++++++++++++++++++++ Internal/hook/Hook.h | 1 + 2 files changed, 145 insertions(+) create mode 100644 Internal/hook/GuardHook.h diff --git a/Internal/hook/GuardHook.h b/Internal/hook/GuardHook.h new file mode 100644 index 0000000..fcbd1a7 --- /dev/null +++ b/Internal/hook/GuardHook.h @@ -0,0 +1,144 @@ +#pragma once + +/// @file +/// @brief GuardHook — a code-patch-free hooking primitive (our own implementation), an alternative to +/// the MinHook trampoline used elsewhere. MinHook rewrites the target's prologue bytes, which a +/// code-integrity check can spot; a guard-page hook modifies **no** target bytes. It marks the page +/// containing the target `PAGE_GUARD` and installs a vectored exception handler: when execution reaches +/// the target the guard fault fires, the handler redirects RIP to the detour and single-steps once to +/// re-arm the guard (the CPU auto-clears PAGE_GUARD on the fault). +/// +/// Trade-offs (see docs/hooking.md): one exception per call — fine for a rarely-called function, far +/// too slow for a per-frame path like PostRender. It hooks by resolved address (no trampoline back to +/// the original — the detour is responsible for continuing execution), and there is an inherent race: +/// while the guard is cleared between the fault and the single-step re-arm, a concurrent call on +/// another thread runs the real target un-redirected. Ship it opt-in (e.g. behind a Debug toggle) for +/// a low-frequency target, not as a drop-in for the MinHook path. + +#include + +#include +#include + +namespace Hook +{ + /// Guard-page (PAGE_GUARD + VEH) hooking. Install(target, detour) / Remove(target). + namespace GuardHook + { + /// x86 EFlags trap flag: set it to single-step the next instruction (raises EXCEPTION_SINGLE_STEP). + inline constexpr DWORD TrapFlag = 0x100; + /// STATUS_GUARD_PAGE_VIOLATION — raised when code touches a PAGE_GUARD page (winnt has no macro). + inline constexpr DWORD GuardPageViolation = 0x80000001; + + /// One installed hook: redirect @ref target to @ref detour. + struct Entry + { + void* target; + void* detour; + }; + + inline std::vector entries; ///< installed hooks; small, mutated only by Install/Remove + inline PVOID vehHandle = nullptr; ///< the one registered vectored handler (installed lazily) + /// The target page to re-arm on this thread's next single-step (thread-local so concurrent + /// faults don't clobber each other's pending re-arm). + inline thread_local void* pendingRearm = nullptr; + + /// Whether @p a and @p b sit in the same 4 KB page. + inline bool SamePage(const void* a, const void* b) + { + return (reinterpret_cast(a) & ~static_cast(0xFFF)) == + (reinterpret_cast(b) & ~static_cast(0xFFF)); + } + + /// (Re)mark the page containing @p target as PAGE_GUARD, preserving its base protection. + inline bool Arm(void* target) + { + MEMORY_BASIC_INFORMATION mbi{}; + if (!VirtualQuery(target, &mbi, sizeof(mbi))) return false; + DWORD old = 0; + return VirtualProtect(target, 1, mbi.Protect | PAGE_GUARD, &old) != 0; + } + + /// The vectored handler: redirect target faults to the detour and re-arm the guard on the step. + inline LONG CALLBACK Handler(PEXCEPTION_POINTERS ex) + { + CONTEXT* ctx = ex->ContextRecord; + const DWORD code = ex->ExceptionRecord->ExceptionCode; + + if (code == GuardPageViolation) + { + // Execution fault → RIP is the faulting address. The OS has already cleared PAGE_GUARD + // on this page, so we must re-arm it (deferred to the single-step below). + const void* rip = reinterpret_cast(ctx->Rip); + for (const Entry& e : entries) + { + if (rip == e.target) // our target: redirect to the detour + { + ctx->Rip = reinterpret_cast(e.detour); + ctx->EFlags |= TrapFlag; + pendingRearm = e.target; + return EXCEPTION_CONTINUE_EXECUTION; + } + if (SamePage(rip, e.target)) // same page, different code: run it, just re-arm + { + ctx->EFlags |= TrapFlag; + pendingRearm = e.target; + return EXCEPTION_CONTINUE_EXECUTION; + } + } + return EXCEPTION_CONTINUE_SEARCH; // not one of ours (e.g. a stack guard page) + } + + if (code == EXCEPTION_SINGLE_STEP && pendingRearm) + { + Arm(pendingRearm); + pendingRearm = nullptr; + ctx->EFlags &= ~TrapFlag; // one step only (the CPU also clears TF after the trap) + return EXCEPTION_CONTINUE_EXECUTION; + } + + return EXCEPTION_CONTINUE_SEARCH; + } + + /// Redirect calls to @p target into @p detour without patching any bytes. + /// @return false on a null argument or if the guard/VEH couldn't be installed. + /// @note The detour receives control in place of the target; there is no trampoline back to the + /// original, so the detour owns continuing/emulating the target's work. + inline bool Install(void* target, void* detour) + { + if (!target || !detour) return false; + + if (!vehHandle) + { + // First in the chain so we intercept our own guard/step faults before the crash handler's + // last-chance SetUnhandledExceptionFilter sees them. + vehHandle = AddVectoredExceptionHandler(1, Handler); + if (!vehHandle) return false; + } + + entries.push_back({target, detour}); + if (Arm(target)) return true; + + entries.pop_back(); + return false; + } + + /// Remove a hook installed by Install: clear the page guard and drop the entry. Removes the + /// vectored handler once the last hook is gone. + inline void Remove(void* target) + { + MEMORY_BASIC_INFORMATION mbi{}; + DWORD old = 0; + if (VirtualQuery(target, &mbi, sizeof(mbi))) + VirtualProtect(target, 1, mbi.Protect & ~static_cast(PAGE_GUARD), &old); + + std::erase_if(entries, [&](const Entry& e) { return e.target == target; }); + + if (entries.empty() && vehHandle) + { + RemoveVectoredExceptionHandler(vehHandle); + vehHandle = nullptr; + } + } + } // namespace GuardHook +} // namespace Hook diff --git a/Internal/hook/Hook.h b/Internal/hook/Hook.h index ff65074..c2a3328 100644 --- a/Internal/hook/Hook.h +++ b/Internal/hook/Hook.h @@ -3,6 +3,7 @@ #include "../features/Features.h" #include "functions/ProcessEvent.h" #include "functions/PostRender.h" +#include "GuardHook.h" // code-patch-free hooking primitive (opt-in; not wired to a live hook yet) #include "../menu/gui/Gui.h" #include From 7af4db0a952253ed158360535a252ff3bd5af8c3 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:54:19 +0200 Subject: [PATCH 41/54] chore(build): register native/ActorId.h and hook/GuardHook.h Co-Authored-By: Claude Opus 4.8 --- Internal/Internal.vcxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Internal/Internal.vcxproj b/Internal/Internal.vcxproj index 4f1294e..d51f553 100644 --- a/Internal/Internal.vcxproj +++ b/Internal/Internal.vcxproj @@ -242,6 +242,7 @@ + @@ -258,6 +259,7 @@ + From 973599214b9556c2a341bc02078fe0de93d9928a Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 18:54:19 +0200 Subject: [PATCH 42/54] docs(roadmap): unblock trace-redirect silent aim (reflected hit RPCs in the dump) A dump search (FunctionsInfo.json) found reflected Server* hit/fire RPCs (ALineTraceGun::ServerNotifyHit, AShotgun::ServerProcessHits, AProjectileGun::ServerSpawnProjectile, ...); on the client these dispatch through ProcessEvent, so the existing funnel can rewrite the hit params without a native AOB hook. Correct the BLOCKED note and record the remaining work. Co-Authored-By: Claude Opus 4.8 --- docs/roadmap.md | 47 ++++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index f478635..2a2e7fe 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -357,27 +357,36 @@ ESP blending looks right; focus/alt-tab/resize edge cases. ## Aim -### True (trace-redirect) silent aim — BLOCKED (needs the native fire/trace function) +### True (trace-redirect) silent aim — UNBLOCKED (reflected hit RPCs found in the dump) The current silent aim snaps the view on fire. A truly invisible one redirects the *shot*, not the -view: hook the fire/trace path and rewrite the trace start/direction (or the hit result) toward the -selected target before the original runs, leaving `ControlRotation` untouched. - -**Why it isn't built yet.** The SDK dump exposes no clean hook point: the shot's trace is computed in -the `Gun`'s **native** fire code, and the plausible reflected sources (`APlayerController::StartFire`, -`APawn::GetBaseAimRotation`) are called *natively*, so the `ProcessEvent` hook — which only sees -reflected/Blueprint calls (as `EnableAllInput` does) — never observes them. (A dump search confirms -this: the only reflected `Gun` fire-path entry is `Function PortalWars.Gun.ServerGoToState`, no -`Fire`/`HitScan`/`ProcessHit` UFunction.) Redirecting the shot therefore needs a **MinHook on the -native function**, which can't be found/verified blind. - -**Next step (one in-game pass unblocks it).** Enable **Debug → Log ProcessEvent** and fire: if *any* -reflected fire/hit event appears (e.g. a `Server*Fire` / `ProcessHit` / weapon-fire UFunction), hook -it via the event bus / a `ProcessEvent` intercept and rewrite its trace params. If nothing reflected -shows, RE the `Gun` native fire (an AOB like the `curl_easy_setopt` one in -[ue4-cheatsheet.md](ue4-cheatsheet.md)) and MinHook it. Then a `bool TrueSilentAim` gates the rewrite. -**Files.** the fire hook, `settings/Settings.h`, `menu/sections/Aim.h`. **Size:** Medium; **depends -on:** identifying the fire/trace function in-game. +view: rewrite the hit result / shot direction toward the selected target before it reaches the server, +leaving `ControlRotation` untouched. + +**What the dump shows (`FunctionsInfo.json`, hash `d2a5bd8c`).** Contrary to the earlier note (which +saw only `Gun.ServerGoToState`), the hit path **is** reflected. The client reports its hits/shots to +the server through `Server*` UFunctions, and on the client a `Server*` RPC is dispatched through +`UObject::ProcessEvent` (that is how it serializes the params to send) — so the **existing ProcessEvent +funnel already sees them** and can rewrite their params before `Original` runs, exactly like the +`BroadcastDeath` / `ClientUpdateChat` decodes already do. No native AOB hook is needed: +- `ALineTraceGun::ServerNotifyHit` — the hitscan base's per-shot hit report. +- `AMultiKillGun::ServerNotifyHits` / `AShotgun::ServerProcessHits` — multi-pellet hit arrays. +- `AProjectileGun::ServerSpawnProjectile` — projectile weapons' spawn (redirect the direction). +- `APortalWarsCharacter::ServerApplyMeleeDamage` — melee damage. +- `AGunSkin::OnStartFire_BP` / `OnStopFire_BP` — reflected fire start/stop edges (a firing signal). + +**Remaining work.** (1) **Param layouts** — these classes/RPCs aren't in the SDK yet (only `AGun` / +`ABaseGun` are), so add `ALineTraceGun` / `AShotgun` / `AProjectileGun` and decode each RPC's param +block (an `FHitResult` / target + hit location/bone), from `ClassesInfo`/`StructsInfo` or one +`LogProcessEvent` pass while firing (the `BroadcastDeath` decode in +[ProcessEvent.h](../Internal/hook/functions/ProcessEvent.h) is the template). (2) A `bool TrueSilentAim` +gates the rewrite toward the aimbot's selected target (reuse its target pick). (3) Server-side +validation may still reject implausible hits (line-of-sight / angle / range) — best against the private +backend; verify in-game. This same `ServerNotifyHit`/`ServerProcessHits` rewrite also enables the +**wallbang** (Phasing Approach B) without disturbing world physics. +**Files.** `hook/functions/ProcessEvent.h` (the intercept), `ue/sdk/` (the gun classes + param +structs), `settings/Settings.h`, `menu/sections/Aim.h`. **Size:** Medium. **Depends on:** the hit-RPC +param layout + in-game validation (the reflected hook point itself is confirmed). ### Phasing bullets (wallbang) toggle — Approach A DONE From 104396456a5e48fef5e3c874b49e67d23b279986 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:10:36 +0200 Subject: [PATCH 43/54] revert: drop the ActorId accessor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InternalIndex is a reused slot index, so it aliases after free exactly like a pointer — it doesn't give the reuse-safe identity the doc claimed, and its intended consumer (the spawn/despawn-diff cache) was skipped. Reuse-safe identity would need a pointer+SerialNumber handle instead. Co-Authored-By: Claude Opus 4.8 --- Internal/Internal.vcxproj | 1 - Internal/native/ActorId.h | 17 ----------------- 2 files changed, 18 deletions(-) delete mode 100644 Internal/native/ActorId.h diff --git a/Internal/Internal.vcxproj b/Internal/Internal.vcxproj index d51f553..6793011 100644 --- a/Internal/Internal.vcxproj +++ b/Internal/Internal.vcxproj @@ -259,7 +259,6 @@ - diff --git a/Internal/native/ActorId.h b/Internal/native/ActorId.h deleted file mode 100644 index dd124c1..0000000 --- a/Internal/native/ActorId.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -/// @file -/// @brief Stable per-actor key: the UObject `InternalIndex` (its slot in the GObjects array), a plain -/// field read — no ProcessEvent. It stays constant for the lifetime of the object, so it keys -/// per-actor state across frames (a spawn/despawn diff, cached per-actor data, ...) more reliably -/// than a raw pointer, which can be reused once the actor is freed. - -#include - -#include "../ue/Engine.h" - -/// The actor's stable GObjects slot id, or 0 for a null actor. Accepts any UObject (actors upcast). -inline uint32_t ActorId(const UObject* actor) -{ - return actor ? actor->InternalIndex : 0; -} From ede5f282757a5407147949801bc07e8d16ff3a71 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:12:51 +0200 Subject: [PATCH 44/54] feat(settings): add VISUALS.CanvasFont (canvas renderer text font) Persisted font name for the UE-canvas renderer; empty = the engine default (Roboto). Co-Authored-By: Claude Opus 4.8 --- Internal/settings/Settings.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Internal/settings/Settings.h b/Internal/settings/Settings.h index 2b626c3..928e7d6 100644 --- a/Internal/settings/Settings.h +++ b/Internal/settings/Settings.h @@ -183,7 +183,8 @@ struct VisualsSettings bool RadarShowFriendly = false; ///< radar: also plot teammates, in FriendColor bool DrawAllNames = false; ///< debug: draw the object name of every actor (not just players) - float FontScale = 1.f; ///< text size for ESP names/distance (and the debug names) + float FontScale = 1.f; ///< text size for ESP names/distance (and the debug names) + std::string CanvasFont = ""; ///< UE-canvas renderer text font, by name (empty = the engine default / Roboto) Color NameColor{1.f, 1.f, 1.f, 1.f}; Color BoxColor{1.f, 0.f, 0.f, 1.f}; @@ -211,7 +212,7 @@ struct VisualsSettings Color GlowSelfColor{0.f, 1.f, 0.f, 1.f}; ///< own-pawn glow color (overridden by RGB when on) }; -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(VisualsSettings, Esp, Name, Box, Box3D, Bones, Snaplines, Health, Distance, KD, Rank, MaxDistance, EspVisibleCheck, BotTag, HideBots, Radar, ShowFriendly, RadarShowFriendly, DrawAllNames, FontScale, NameColor, BoxColor, BonesColor, SnaplineColor, FriendColor, VisibleColor, BotTagColor, Crosshair, CrosshairSize, CrosshairGap, CrosshairThickness, CrosshairColor, BulletTraces, BulletTraceDuration, BulletTraceColor, GlowEnemy, GlowFriendly, GlowSelf, GlowEnemyColor, GlowFriendlyColor, GlowSelfColor) +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(VisualsSettings, Esp, Name, Box, Box3D, Bones, Snaplines, Health, Distance, KD, Rank, MaxDistance, EspVisibleCheck, BotTag, HideBots, Radar, ShowFriendly, RadarShowFriendly, DrawAllNames, FontScale, CanvasFont, NameColor, BoxColor, BonesColor, SnaplineColor, FriendColor, VisibleColor, BotTagColor, Crosshair, CrosshairSize, CrosshairGap, CrosshairThickness, CrosshairColor, BulletTraces, BulletTraceDuration, BulletTraceColor, GlowEnemy, GlowFriendly, GlowSelf, GlowEnemyColor, GlowFriendlyColor, GlowSelfColor) /// Aimbot / triggerbot tunables (the Aim tab). struct AimSettings From 462d07c619382417229e28fd4404c19a3820e31b Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:12:51 +0200 Subject: [PATCH 45/54] feat(cache): add FontCache listing every UFont in GObjects Lazily-built name+pointer list of UFont objects (mirrors ClassCache), with Find(name) to resolve one; Rebuild() rescans for newly-loaded fonts. Co-Authored-By: Claude Opus 4.8 --- Internal/cache/FontCache.h | 60 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 Internal/cache/FontCache.h diff --git a/Internal/cache/FontCache.h b/Internal/cache/FontCache.h new file mode 100644 index 0000000..60b7ec6 --- /dev/null +++ b/Internal/cache/FontCache.h @@ -0,0 +1,60 @@ +#pragma once + +/// @file +/// @brief Lazily-built, shared list of every UFont in GObjects (short name + pointer), so the UE-canvas +/// renderer can draw text in a chosen game font instead of the default Roboto. Mirrors ClassCache: fonts +/// load once and rarely change, so the walk is done on demand and reused; Rebuild() (a Refresh button) +/// picks up newly-loaded fonts. + +#include +#include + +#include "../ue/Engine.h" + +namespace FontCache +{ + /// One cached font: its short name (for display / the setting) and the object pointer. + struct Entry + { + std::string name; + UFont* font; + }; + + inline std::vector fonts; + inline bool built = false; + + /// Walk GObjects once and collect every UFont instance (skipping the class default object). + inline void Rebuild() + { + fonts.clear(); + built = true; + if (!Engine::GObjects) return; + + UObject* fontClass = Engine::GObjects->FindObject("Class Engine.Font"); + if (!fontClass) return; + + const auto count = Engine::GObjects->NumElements; + for (auto i = 0u; i < count; i++) + { + auto* obj = Engine::GObjects->GetObjectPtr(i); + if (!obj || obj->IsDefaultObject() || !obj->IsA(fontClass)) continue; + fonts.push_back({obj->GetName(), reinterpret_cast(obj)}); + } + } + + /// The cached fonts, building them on first use. + inline const std::vector& Get() + { + if (!built) Rebuild(); + return fonts; + } + + /// Resolve a font by its short name; nullptr for an empty/unknown name (→ the renderer's default). + inline UFont* Find(const std::string& name) + { + if (name.empty()) return nullptr; + for (const Entry& e : Get()) + if (e.name == name) return e.font; + return nullptr; + } +} // namespace FontCache From 0fb6afc3771951ce826a9ea92d8cb8bbd0ac39e7 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:12:51 +0200 Subject: [PATCH 46/54] feat(render): draw canvas text in the configured font CanvasRenderer resolves Settings.VISUALS.CanvasFont via FontCache (re-resolving only when the setting changes) and passes it to K2_DrawText/K2_TextSize/K2_StrLen instead of the null (Roboto) font. Co-Authored-By: Claude Opus 4.8 --- Internal/render/CanvasRenderer.h | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/Internal/render/CanvasRenderer.h b/Internal/render/CanvasRenderer.h index 11072a1..7338d61 100644 --- a/Internal/render/CanvasRenderer.h +++ b/Internal/render/CanvasRenderer.h @@ -6,13 +6,31 @@ /// ImGuiRenderer exists. #include +#include #include "Renderer.h" #include "adapters/Ue.h" #include "../ue/Engine.h" +#include "../cache/FontCache.h" +#include "../settings/Settings.h" class CanvasRenderer : public Renderer { + UFont* font = nullptr; ///< resolved render font (null = the engine default / Roboto) + std::string fontName; ///< the Settings.VISUALS.CanvasFont value `font` was resolved from + + /// The UFont to draw/measure with, per Settings.VISUALS.CanvasFont. Re-resolves only when the + /// setting changes (so it's a cheap string compare per call, not a GObjects walk). Null = Roboto. + UFont* CurrentFont() + { + if (Settings.VISUALS.CanvasFont != fontName) + { + fontName = Settings.VISUALS.CanvasFont; + font = FontCache::Find(fontName); + } + return font; + } + public: void Line(const Render::Vec2& a, const Render::Vec2& b, float thickness, const Render::Color& color) override { @@ -22,19 +40,19 @@ class CanvasRenderer : public Renderer void Text(const Render::Vec2& pos, const std::string& text, float scale, const Render::Color& color, bool centered) override { if (Engine::Canvas) - Engine::Canvas->K2_DrawText(0, FString(text), pos.To(), {scale, scale}, color.To(), 1.f, {0.f, 0.f, 0.f, 0.f}, {0.f, 0.f}, centered, false, true, {0.f, 0.f, 0.f, 1.f}); + Engine::Canvas->K2_DrawText(CurrentFont(), FString(text), pos.To(), {scale, scale}, color.To(), 1.f, {0.f, 0.f, 0.f, 0.f}, {0.f, 0.f}, centered, false, true, {0.f, 0.f, 0.f, 1.f}); } Render::Vec2 TextSize(const std::string& text, float scale) override { if (!Engine::Canvas) return {0.f, 0.f}; - return Engine::Canvas->K2_TextSize(nullptr, FString(text), {scale, scale}); // null font -> Roboto fallback + return Engine::Canvas->K2_TextSize(CurrentFont(), FString(text), {scale, scale}); // null font -> Roboto fallback } Render::Vec2 StrLen(const std::string& text) override { if (!Engine::Canvas) return {0.f, 0.f}; - return Engine::Canvas->K2_StrLen(nullptr, FString(text)); + return Engine::Canvas->K2_StrLen(CurrentFont(), FString(text)); } void RectFilled(const Render::Vec2& min, const Render::Vec2& max, const Render::Color& color) override From 98636054ed805f7be7cb7f85789124b87236fd6f Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:12:51 +0200 Subject: [PATCH 47/54] feat(menu): add a canvas font picker to the Visuals tab When the renderer is UE Canvas, a font combo (Default/Roboto + every UFont from FontCache) and a Refresh button; ImGui mode uses its own atlas so the picker is hidden there. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/sections/Visuals.h | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Internal/menu/sections/Visuals.h b/Internal/menu/sections/Visuals.h index 34bbc9c..9096d54 100644 --- a/Internal/menu/sections/Visuals.h +++ b/Internal/menu/sections/Visuals.h @@ -3,8 +3,11 @@ /// @file /// @brief Visuals tab: player ESP element toggles, radar, and per-element colors. +#include + #include "../../settings/Settings.h" #include "../../scripting/Events.h" +#include "../../cache/FontCache.h" #include "../ui/UI.h" namespace Menu @@ -32,6 +35,30 @@ namespace Menu "External draws the ESP, watermark, and everything the renderer produces into a separate\n" "window hidden from screen capture (OBS, Game Bar); the menu stays on the game window."); + // Font picker — only the UE-canvas renderer draws text through a UFont (ImGui uses its own + // atlas), so this applies to canvas-mode text (ESP + the canvas menu). Index 0 = Roboto default. + if (Settings.MENU.Renderer == RendererMode::Canvas) + { + const auto& faces = FontCache::Get(); + std::vector items{"Default (Roboto)"}; + int fontIdx = 0; + for (int i = 0; i < static_cast(faces.size()); i++) + { + items.push_back(faces[i].name.c_str()); + if (faces[i].name == Settings.VISUALS.CanvasFont) fontIdx = i + 1; + } + + UI::SetNextItemWidth(220.f); + if (UI::Combo("Canvas font", &fontIdx, items.data(), static_cast(items.size()))) + { + Settings.VISUALS.CanvasFont = fontIdx == 0 ? "" : faces[fontIdx - 1].name; + changed = true; + } + UI::SameLine(); + if (UI::SmallButton("Refresh##fonts")) FontCache::Rebuild(); + UI::Tooltip("Text font for the UE-canvas renderer (ESP + the canvas menu). Default is the engine Roboto.\nRefresh rescans GObjects for newly-loaded fonts."); + } + const char* backends[] = {"ImGui", "UE Canvas"}; int backend = static_cast(Settings.MENU.Backend); if (UI::Combo("Menu backend", &backend, backends, UI::Count(backends))) From f21f54ce174803fce56dd712a81cb5908ec587f5 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:12:51 +0200 Subject: [PATCH 48/54] chore(build): register cache/FontCache.h Co-Authored-By: Claude Opus 4.8 --- Internal/Internal.vcxproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Internal/Internal.vcxproj b/Internal/Internal.vcxproj index 6793011..ef1aa2f 100644 --- a/Internal/Internal.vcxproj +++ b/Internal/Internal.vcxproj @@ -69,6 +69,7 @@ + From 9082aa22808b6d0453743470f9a3897bbc05dbf5 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:35:12 +0200 Subject: [PATCH 49/54] test(features): cover multi-trigger, throttle, and Run() overload dispatch New cases: Execute runs a feature when Render is among several triggers; ThrottleMs gates a repeated run and reopens after the interval (back-dating lastRun, no sleep); RunFeature forwards the event to Run(event) and the event+payload to Run(event, payload). Co-Authored-By: Claude Opus 4.8 --- Tests/FeaturesTests.cpp | 92 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/Tests/FeaturesTests.cpp b/Tests/FeaturesTests.cpp index 2d5d905..3ca5482 100644 --- a/Tests/FeaturesTests.cpp +++ b/Tests/FeaturesTests.cpp @@ -10,6 +10,7 @@ #include +#include #include #include #include @@ -51,6 +52,47 @@ namespace } }; + /// A feature that overrides the event-only Run(event) overload, to test the runner forwards the + /// triggering event and the base delegates the (event, payload) call down to it. + struct EventFake : public Feature + { + int runs = 0; + Events::Type lastEvent = Events::Type::Render; + + EventFake() { Name = "EventFake"; } + void Init() override { Initialized = true; } + void UpdateEnabled() override {} + bool Check() override { return true; } + void Destroy() override {} + void Run(Events::Type event) override + { + runs++; + lastEvent = event; + } + }; + + /// A feature that overrides the full Run(event, payload) overload, to test both are forwarded. + struct PayloadFake : public Feature + { + int runs = 0; + Events::Type lastEvent = Events::Type::Render; + float lastValue = -1.f; + std::string lastName; + + PayloadFake() { Name = "PayloadFake"; } + void Init() override { Initialized = true; } + void UpdateEnabled() override {} + bool Check() override { return true; } + void Destroy() override {} + void Run(Events::Type event, const Events::Payload& payload) override + { + runs++; + lastEvent = event; + lastValue = payload.value; + lastName = payload.name ? payload.name : ""; + } + }; + /// Registers a new FakeFeature in the global registry and returns a non-owning pointer to it. FakeFeature* add(std::string name = "Fake") { @@ -241,4 +283,54 @@ namespace EXPECT_EQ(1, f.runCount); // fires on its event } + // A feature can list several triggers; Execute() drives it as long as Render is one of them. + TEST_F(FeaturesTest, ExecuteRunsWhenRenderIsAmongTriggers) + { + FakeFeature* f = add(); + f->Triggers = {Events::Type::Shutdown, Events::Type::Render}; + f->Enabled = true; + Features::Execute(); + EXPECT_EQ(1, f->runCount); + } + + // ThrottleMs caps how often Run() fires: a second immediate tick is skipped, and it runs again + // once the interval has elapsed (simulated by back-dating lastRun rather than sleeping). + TEST_F(FeaturesTest, ThrottleGatesRepeatedRuns) + { + FakeFeature* f = add(); + f->Enabled = true; + f->ThrottleMs = 10000; + + Features::Execute(); // first run stamps lastRun + Features::Execute(); // within the interval -> throttled + EXPECT_EQ(1, f->runCount); + + f->lastRun = std::chrono::steady_clock::now() - std::chrono::milliseconds(20000); // interval elapsed + Features::Execute(); + EXPECT_EQ(2, f->runCount); + } + + // The runner forwards the triggering event to a feature that overrides Run(event) (the base + // delegates its Run(event, payload) down to it). + TEST_F(FeaturesTest, ForwardsEventToRunOverload) + { + EventFake f; + f.Enabled = true; + Features::RunFeature(f, Events::Type::PlayerDeath); + EXPECT_EQ(1, f.runs); + EXPECT_EQ(Events::Type::PlayerDeath, f.lastEvent); + } + + // The runner forwards both the event and the payload to a feature that overrides the full overload. + TEST_F(FeaturesTest, ForwardsEventAndPayloadToRunOverload) + { + PayloadFake f; + f.Enabled = true; + Features::RunFeature(f, Events::Type::HotKeyPressed, Events::Payload{.value = 3.f, .name = "hello"}); + EXPECT_EQ(1, f.runs); + EXPECT_EQ(Events::Type::HotKeyPressed, f.lastEvent); + EXPECT_EQ(3.f, f.lastValue); + EXPECT_EQ("hello", f.lastName); + } + } // namespace From e1ff4882b9172bd9f89a4f477ce139182d504e4a Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:35:12 +0200 Subject: [PATCH 50/54] refactor(menu): dispatch SettingsChanged per-toggle across the tabs Convert the batched 'changed |= UI::Toggle' toggles to UI::ToggleSetting (per-control dispatch tagged with the label), matching the Exploits tab; combos/sliders keep the tab's batched dispatch. Finishes the SettingsChanged-payload rollout for Aim, Visuals, and Network. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/sections/Aim.h | 20 +++++++-------- Internal/menu/sections/Network.h | 8 +++--- Internal/menu/sections/Visuals.h | 44 ++++++++++++++++---------------- 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Internal/menu/sections/Aim.h b/Internal/menu/sections/Aim.h index 5e4e4b6..4cbd950 100644 --- a/Internal/menu/sections/Aim.h +++ b/Internal/menu/sections/Aim.h @@ -18,39 +18,39 @@ namespace Menu auto& a = Settings.AIM; UI::SeparatorText("Aimbot"); - changed |= UI::Toggle("Enable##aim", &a.Aimbot); + UI::ToggleSetting("Enable##aim", &a.Aimbot); UI::HotKey("Aim key", &a.AimKey); changed |= UI::SliderFloat("FOV (px)", &a.AimFov, 10.f, 500.f, "%.0f"); changed |= UI::SliderFloat("Smoothing", &a.AimSmooth, 0.05f, 1.f, "%.2f"); UI::Tooltip("1.0 snaps instantly; lower is smoother."); const char* bones[] = {"Head", "Chest", "Pelvis"}; changed |= UI::Combo("Bone", &a.AimBone, bones, UI::Count(bones)); - changed |= UI::Toggle("Team check##aim", &a.AimTeamCheck); - changed |= UI::Toggle("Ignore bots", &a.IgnoreBots); + UI::ToggleSetting("Team check##aim", &a.AimTeamCheck); + UI::ToggleSetting("Ignore bots", &a.IgnoreBots); UI::Tooltip("Aimbot and triggerbot target only real players, never AI bots."); - changed |= UI::Toggle("Silent aim", &a.SilentAim); + UI::ToggleSetting("Silent aim", &a.SilentAim); UI::Tooltip("Snap to the target only while firing (left click), ignoring the aim key."); - changed |= UI::Toggle("Visibility check", &a.AimVisibleCheck); + UI::ToggleSetting("Visibility check", &a.AimVisibleCheck); UI::Tooltip("Only lock onto targets that were recently rendered (visible). Also applies to the triggerbot."); if (a.AimVisibleCheck) { - changed |= UI::Toggle("Per-bone visibility", &a.AimVisiblePerBone); + UI::ToggleSetting("Per-bone visibility", &a.AimVisiblePerBone); UI::Tooltip("Stricter: line-trace each bone and aim at the first one in line of sight\n(e.g. skip the head when only the legs are exposed). Skips targets with no visible bone."); } - changed |= UI::Toggle("Aim assist", &a.AimAssist); + UI::ToggleSetting("Aim assist", &a.AimAssist); UI::Tooltip("Amplify the weapon's built-in aim-assist/magnetism (soft aim, view isn't moved).\nMay only take effect on controller input - verify in-game."); if (a.AimAssist) changed |= UI::SliderFloat("Aim assist strength", &a.AimAssistStrength, 1.f, 8.f, "%.1fx"); - changed |= UI::Toggle("Draw FOV circle", &a.DrawAimFov); + UI::ToggleSetting("Draw FOV circle", &a.DrawAimFov); UI::ColorEdit("FOV circle color", &a.AimFovColor); UI::SeparatorText("Triggerbot"); - changed |= UI::Toggle("Enable##trig", &a.Triggerbot); + UI::ToggleSetting("Enable##trig", &a.Triggerbot); UI::HotKey("Trigger key", &a.TriggerKey); changed |= UI::SliderFloat("Trigger FOV (px)", &a.TriggerFov, 1.f, 30.f, "%.0f"); changed |= UI::SliderInt("Delay (ms)", &a.TriggerDelay, 0, 500); - changed |= UI::Toggle("Team check##trig", &a.TriggerTeamCheck); + UI::ToggleSetting("Team check##trig", &a.TriggerTeamCheck); if (changed) Events::Dispatch(Events::Type::SettingsChanged); } diff --git a/Internal/menu/sections/Network.h b/Internal/menu/sections/Network.h index 1da3abe..1166f44 100644 --- a/Internal/menu/sections/Network.h +++ b/Internal/menu/sections/Network.h @@ -80,7 +80,7 @@ namespace Menu changed = true; } - changed |= UI::Toggle("Bypass SSL verification", &Settings.NETWORK.BypassSslVerify); + UI::ToggleSetting("Bypass SSL verification", &Settings.NETWORK.BypassSslVerify); UI::Tooltip("Force curl's cert/host verification off so a redirected host can serve a self-signed cert.\nDisables TLS verification for ALL curl traffic while on."); // Mitmproxy script — a launcher-only setting (launcher.settings), so it lives outside @@ -149,9 +149,9 @@ namespace Menu } UI::SeparatorText("HTTP logging"); - changed |= UI::Toggle("Log HTTP calls", &Settings.NETWORK.HttpLogging); - changed |= UI::Toggle("Also log to http.log", &Settings.NETWORK.HttpLogToFile); - changed |= UI::Toggle("Redirected hosts only", &Settings.NETWORK.HttpLogRedirectedOnly); + UI::ToggleSetting("Log HTTP calls", &Settings.NETWORK.HttpLogging); + UI::ToggleSetting("Also log to http.log", &Settings.NETWORK.HttpLogToFile); + UI::ToggleSetting("Redirected hosts only", &Settings.NETWORK.HttpLogRedirectedOnly); // Live request flow — populated while HTTP logging is on. if (UI::CollapsingHeader("Request flow")) diff --git a/Internal/menu/sections/Visuals.h b/Internal/menu/sections/Visuals.h index 9096d54..3367421 100644 --- a/Internal/menu/sections/Visuals.h +++ b/Internal/menu/sections/Visuals.h @@ -70,19 +70,19 @@ namespace Menu "ImGui = the Present overlay; UE Canvas = drawn on the game canvas (works at the main menu too)."); UI::SeparatorText("Player ESP"); - changed |= UI::Toggle("Enable", &v.Esp); + UI::ToggleSetting("Enable", &v.Esp); UI::SeparatorText("Elements"); - changed |= UI::Toggle("Name", &v.Name); - changed |= UI::Toggle("Box", &v.Box); - changed |= UI::Toggle("3D Box", &v.Box3D); - changed |= UI::Toggle("Bones", &v.Bones); - changed |= UI::Toggle("Snaplines", &v.Snaplines); - changed |= UI::Toggle("Health", &v.Health); - changed |= UI::Toggle("Distance", &v.Distance); - changed |= UI::Toggle("K/D", &v.KD); + UI::ToggleSetting("Name", &v.Name); + UI::ToggleSetting("Box", &v.Box); + UI::ToggleSetting("3D Box", &v.Box3D); + UI::ToggleSetting("Bones", &v.Bones); + UI::ToggleSetting("Snaplines", &v.Snaplines); + UI::ToggleSetting("Health", &v.Health); + UI::ToggleSetting("Distance", &v.Distance); + UI::ToggleSetting("K/D", &v.KD); UI::Tooltip("Draw each player's kills/deaths, and [killstreak] for the current life."); - changed |= UI::Toggle("Rank", &v.Rank); + UI::ToggleSetting("Rank", &v.Rank); UI::Tooltip("Draw each player's rank/level (from their player state)."); UI::SeparatorText("Range"); @@ -90,32 +90,32 @@ namespace Menu UI::Tooltip("Only draw enemies within this many metres. 0 = unlimited."); UI::SeparatorText("Visibility"); - changed |= UI::Toggle("Visibility check", &v.EspVisibleCheck); + UI::ToggleSetting("Visibility check", &v.EspVisibleCheck); UI::Tooltip("Recolor visible (recently-rendered) enemies in the Visible color below;\noccluded enemies keep the normal box/bone/snapline colors."); - changed |= UI::Toggle("Hide bots", &v.HideBots); + UI::ToggleSetting("Hide bots", &v.HideBots); UI::Tooltip("Don't draw AI bots in the ESP at all."); - changed |= UI::Toggle("Bot tag", &v.BotTag); + UI::ToggleSetting("Bot tag", &v.BotTag); UI::Tooltip("Prefix an AI bot's name with a colored \"[BOT]\" tag."); if (v.BotTag) UI::ColorEdit("Bot tag color", &v.BotTagColor); UI::SeparatorText("Teams"); - changed |= UI::Toggle("Show teammates", &v.ShowFriendly); + UI::ToggleSetting("Show teammates", &v.ShowFriendly); UI::Tooltip("Also draw teammates (ESP + radar), in the friendly color below."); UI::SeparatorText("Radar"); - changed |= UI::Toggle("Enable Radar", &v.Radar); - changed |= UI::Toggle("Radar teammates", &v.RadarShowFriendly); + UI::ToggleSetting("Enable Radar", &v.Radar); + UI::ToggleSetting("Radar teammates", &v.RadarShowFriendly); UI::SeparatorText("Debug"); - changed |= UI::Toggle("Draw all object names", &v.DrawAllNames); + UI::ToggleSetting("Draw all object names", &v.DrawAllNames); UI::Tooltip("Draws the UObject name of every actor in the world (not just players)."); UI::SeparatorText("Text"); changed |= UI::SliderFloat("Font size", &v.FontScale, 0.5f, 3.f, "%.2f"); UI::SeparatorText("Crosshair"); - changed |= UI::Toggle("Crosshair", &v.Crosshair); + UI::ToggleSetting("Crosshair", &v.Crosshair); if (v.Crosshair) { changed |= UI::SliderFloat("Size", &v.CrosshairSize, 1.f, 30.f, "%.0f"); @@ -126,7 +126,7 @@ namespace Menu } UI::SeparatorText("Bullet traces"); - changed |= UI::Toggle("Bullet traces", &v.BulletTraces); + UI::ToggleSetting("Bullet traces", &v.BulletTraces); UI::Tooltip("Draw a fading trail behind each projectile (PortalWars.Projectile and subclasses)."); if (v.BulletTraces) { @@ -136,20 +136,20 @@ namespace Menu } UI::SeparatorText("Glow / chams"); - changed |= UI::Toggle("Glow enemies", &v.GlowEnemy); + UI::ToggleSetting("Glow enemies", &v.GlowEnemy); UI::Tooltip("Force a custom-depth outline on enemies, visible through walls.\nRides on the game's team-outline post-process (verify color mapping in-game)."); if (v.GlowEnemy) { UI::ColorEdit("Enemy glow", &v.GlowEnemyColor); UI::Tooltip("Overridden by the RGB rainbow when RGB is on."); } - changed |= UI::Toggle("Glow teammates", &v.GlowFriendly); + UI::ToggleSetting("Glow teammates", &v.GlowFriendly); if (v.GlowFriendly) { UI::ColorEdit("Teammate glow", &v.GlowFriendlyColor); UI::Tooltip("Overridden by the RGB rainbow when RGB is on."); } - changed |= UI::Toggle("Glow self", &v.GlowSelf); + UI::ToggleSetting("Glow self", &v.GlowSelf); UI::Tooltip("Outline your own pawn - only visible in third person."); if (v.GlowSelf) { From ca7bbe367ae6f522aeddaf2be028de70450210fe Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:35:12 +0200 Subject: [PATCH 51/54] feat(hook): add GuardHook::SelfTest and a Debug-tab button to run it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SelfTest guard-hooks a scratch VirtualAlloc'd page (a lone ret) and confirms the detour ran instead — an in-process proof that touches no game code. A 'Test GuardHook' button in Debug runs it and logs the result. Also routes the Debug tab's toggles through UI::ToggleSetting (the same SettingsChanged-payload rollout). Co-Authored-By: Claude Opus 4.8 --- Internal/hook/GuardHook.h | 29 +++++++++++++++++++++++++++++ Internal/menu/sections/Debug.h | 29 +++++++++++++++++------------ 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/Internal/hook/GuardHook.h b/Internal/hook/GuardHook.h index fcbd1a7..8d3be31 100644 --- a/Internal/hook/GuardHook.h +++ b/Internal/hook/GuardHook.h @@ -140,5 +140,34 @@ namespace Hook vehHandle = nullptr; } } + + inline bool demoDetourRan = false; ///< set by SelfTest's detour to prove it ran + inline void SelfTestDetour() { demoDetourRan = true; } ///< the SelfTest detour (a normal, un-guarded function) + + /// In-process proof that the primitive works, safe to run in the game (touches no game code): + /// guard-hook a scratch page holding a lone `ret`, call it, and confirm the detour ran instead. + /// The target lives on its own VirtualAlloc'd page so guarding it can never fault our own handler + /// or this function. @return true if the detour ran (hook worked). + inline bool SelfTest() + { + demoDetourRan = false; + + auto* page = static_cast(VirtualAlloc(nullptr, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); + if (!page) return false; + page[0] = 0xC3; // x64 `ret` — the "original" we expect to be redirected away from + + bool ok = false; + if (Install(page, reinterpret_cast(&SelfTestDetour))) + { + // Call through a volatile pointer so the compiler can't inline past the guarded page. + void(*volatile call)() = reinterpret_cast(page); + call(); + Remove(page); + ok = demoDetourRan; + } + + VirtualFree(page, 0, MEM_RELEASE); + return ok; + } } // namespace GuardHook } // namespace Hook diff --git a/Internal/menu/sections/Debug.h b/Internal/menu/sections/Debug.h index 93a55e5..07cdf74 100644 --- a/Internal/menu/sections/Debug.h +++ b/Internal/menu/sections/Debug.h @@ -6,6 +6,7 @@ #include "../../settings/Settings.h" #include "../../scripting/Events.h" #include "../../hook/Hook.h" +#include "../../hook/GuardHook.h" #include "../../../shared/Utilities.h" #include "../ui/UI.h" @@ -21,36 +22,40 @@ namespace Menu /// GObjects to Dumps/GObjects.txt, and a tree listing loaded features with their init/enabled state. void DebugTab() { - bool changed = false; - UI::SeparatorText("Logging"); - changed |= UI::Toggle("Log ProcessEvent", &Settings.DEBUG.LogProcessEvent); - changed |= UI::Toggle("Features Logging", &Settings.DEBUG.FeaturesLogging); + UI::ToggleSetting("Log ProcessEvent", &Settings.DEBUG.LogProcessEvent); + UI::ToggleSetting("Features Logging", &Settings.DEBUG.FeaturesLogging); UI::SeparatorText("GUI"); - changed |= UI::Toggle("Show demo window", &Settings.DEBUG.ShowDemoWindow); - changed |= UI::Toggle("Show style editor", &Settings.DEBUG.ShowStyleEditor); + UI::ToggleSetting("Show demo window", &Settings.DEBUG.ShowDemoWindow); + UI::ToggleSetting("Show style editor", &Settings.DEBUG.ShowStyleEditor); UI::SeparatorText("Performance"); - changed |= UI::Toggle("Native WorldToScreen", &Settings.DEBUG.NativeWorldToScreen); + UI::ToggleSetting("Native WorldToScreen", &Settings.DEBUG.NativeWorldToScreen); UI::Tooltip("Project overlays with math instead of the game's ProjectWorldLocationToScreen UFunction. Turn off if boxes/names are misplaced."); if (!Settings.DEBUG.NativeWorldToScreen) { - changed |= UI::Toggle("Custom projection", &Settings.DEBUG.CustomProjection); + UI::ToggleSetting("Custom projection", &Settings.DEBUG.CustomProjection); UI::Tooltip("With native off: use PortalWars' ProjectWorldLocationToScreenCustom instead of the stock UFunction."); } - changed |= UI::Toggle("Native bones", &Settings.DEBUG.NativeBones); + UI::ToggleSetting("Native bones", &Settings.DEBUG.NativeBones); UI::Tooltip("Project the ESP skeleton via native GetBoneMatrix + WorldToScreen. Off falls back to the game's bone projection."); - changed |= UI::Toggle("Native actor location", &Settings.DEBUG.NativeActorLocation); + UI::ToggleSetting("Native actor location", &Settings.DEBUG.NativeActorLocation); UI::Tooltip("Read actor location from RootComponent->RelativeLocation (no ProcessEvent). Off uses K2_GetActorLocation."); - if (changed) Events::Dispatch(Events::Type::SettingsChanged); - UI::SeparatorText("Files"); if (UI::Button("Open app folder")) Shared::Utilities::OpenFolder(Shared::AppDataPath(SettingsHelper::AppFolder)); UI::Tooltip("Open the SplitgateInternal data folder (settings, logs, dumps)."); + UI::SeparatorText("Hooking"); + if (UI::Button("Test GuardHook")) + { + const bool ok = Hook::GuardHook::SelfTest(); + Logger::Log(ok ? "SUCCESS" : "ERROR", ok ? "[GuardHook] self-test passed (detour ran)" : "[GuardHook] self-test failed"); + } + UI::Tooltip("Run an in-process self-test of the guard-page hook primitive: it hooks a scratch\nfunction and confirms the detour ran instead. Touches no game code; result goes to the log."); + UI::SeparatorText("Console command"); static char consoleBuffer[256] = ""; UI::SetNextItemWidth(260.f); From 9afdc4d84aeee5995c2f96fe24fbcee4df0c6789 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Sun, 6 Sep 2026 19:35:12 +0200 Subject: [PATCH 52/54] feat(menu): add a Dump FName pool button to the SDK tab Writes the whole cached FName pool (NameCache) to Dumps/FNames.txt, beside the existing GObjects dump. Co-Authored-By: Claude Opus 4.8 --- Internal/menu/sections/Sdk.h | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Internal/menu/sections/Sdk.h b/Internal/menu/sections/Sdk.h index 4bc4b30..d40ee68 100644 --- a/Internal/menu/sections/Sdk.h +++ b/Internal/menu/sections/Sdk.h @@ -313,6 +313,33 @@ namespace Menu if (Engine::PlayerController) Engine::PlayerController->SendChatMessage(FString(msg)); } UI::Tooltip("Write every GObject (index + full name) to Dumps/GObjects.txt."); + + UI::SameLine(); + if (UI::Button("Dump FName pool")) + { + fs::path dumpsDir = Shared::AppDataPath(SettingsHelper::AppFolder) / "Dumps"; + if (!fs::exists(dumpsDir)) fs::create_directories(dumpsDir); + + fs::path filePath = dumpsDir / "FNames.txt"; + std::ofstream file(filePath, std::ios::out | std::ios::trunc); + if (!file.is_open()) + { + char errorMsg[256]; + strerror_s(errorMsg, sizeof(errorMsg), errno); + Logger::Log("ERROR", std::format("Failed to open {} for writing: {}", filePath.string(), errorMsg)); + return; + } + + const auto& names = NameCache::Get(); + for (const auto& name : names) + file << name << '\n'; + file.close(); + + std::string msg = std::format("Dumped {} FNames to {}", names.size(), filePath.string()); + Logger::Log("SUCCESS", msg); + if (Engine::PlayerController) Engine::PlayerController->SendChatMessage(FString(msg)); + } + UI::Tooltip("Write the whole FName pool (every interned name, incl. not-yet-loaded content) to\nDumps/FNames.txt. Uses the cached pool — Refresh the Name pool list above to rescan first."); } } // namespace Sections } // namespace Menu From 2d2ae31c12c0f34a1e247a22ae1b642c899d23c9 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Mon, 7 Sep 2026 16:58:40 +0200 Subject: [PATCH 53/54] Update Hook.h --- Internal/hook/Hook.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Internal/hook/Hook.h b/Internal/hook/Hook.h index c2a3328..774742a 100644 --- a/Internal/hook/Hook.h +++ b/Internal/hook/Hook.h @@ -3,7 +3,7 @@ #include "../features/Features.h" #include "functions/ProcessEvent.h" #include "functions/PostRender.h" -#include "GuardHook.h" // code-patch-free hooking primitive (opt-in; not wired to a live hook yet) +#include "GuardHook.h" #include "../menu/gui/Gui.h" #include From 67ad90c753fd7168d519fdb336f3d471db689c31 Mon Sep 17 00:00:00 2001 From: 47PADO47 <62028267+47PADO47@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:04:23 +0200 Subject: [PATCH 54/54] ci: make MSBuild workflow dispatch-only and bump actions to latest Drop the push/pull_request triggers so the build only runs on manual workflow_dispatch. Update actions/checkout to v7, actions/setup-python to v7, microsoft/setup-msbuild to v3, and actions/upload-artifact to v7. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/msbuild.yml | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/.github/workflows/msbuild.yml b/.github/workflows/msbuild.yml index fe9bd3d..0fd7979 100644 --- a/.github/workflows/msbuild.yml +++ b/.github/workflows/msbuild.yml @@ -2,16 +2,6 @@ name: MSBuild on: workflow_dispatch: - push: - branches: [ "master" ] - paths: - - 'Internal/**' - - 'Launcher/**' - pull_request: - branches: [ "master" ] - paths: - - 'Internal/**' - - 'Launcher/**' env: SOLUTION_FILE_PATH: . @@ -24,16 +14,16 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: submodules: recursive - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v7 id: cpy with: python-version: '3.14' - name: Add MSBuild to PATH - uses: microsoft/setup-msbuild@v2 + uses: microsoft/setup-msbuild@v3 - name: Install vcpkg run: | @@ -60,7 +50,7 @@ jobs: run: Tools/build.bat - name: Upload Release Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: Release path: ${{env.SOLUTION_FILE_PATH}}/x64/Release