├── .github └── workflows │ └── check.yml ├── .luacheckrc ├── LICENSE ├── README.md └── imgui.lua /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Luacheck 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | luacheck: 7 | runs-on: ubuntu-latest 8 | env: 9 | ACTIONS_ALLOW_UNSECURE_COMMANDS: true 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: leafo/gh-actions-lua@v5 13 | - uses: leafo/gh-actions-luarocks@v2 14 | - name: Install luacheck 15 | run: luarocks install luacheck 16 | - name: Luacheck 17 | run: luacheck imgui.lua 18 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = { 2 | read_globals = { 3 | "Color", 4 | "GetConVar", 5 | "hook", 6 | "render", 7 | "vgui", 8 | "g_ContextMenu", 9 | "debug", 10 | "input", 11 | "MOUSE_LEFT", 12 | "KEY_E", 13 | "LocalPlayer", 14 | "util", 15 | "print", 16 | "math", 17 | "surface", 18 | "cam", 19 | "gui", 20 | "SysTime", 21 | "Vector", 22 | "IsValid", 23 | "unpack", 24 | "draw", 25 | "string", 26 | "TEXT_ALIGN_CENTER", 27 | "Angle", 28 | "tonumber", 29 | "vector_origin", 30 | "angle_zero", 31 | "ScrW", 32 | "ScrH", 33 | } 34 | } 35 | ignore = {"611"} 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 wyozi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imgui 2 | Immediate mode 3D2D UI for Garry's Mod. See some real-world uses [here](https://github.com/wyozi-gmod/imgui/issues/2). 3 | 4 | ### Features 5 | 6 | - Creating clickable buttons or tracking mouse in an area requires one line of code 7 | - Visibility optimizations to prevent UI from being rendered if it's not visible 8 | - Only a wrapper for `cam.Start3D2D` so all `surface` library functions are still usable 9 | - Error handling to prevent clients from crashing if a Lua error happens 10 | 11 | ### Installation 12 | 13 | Place `imgui.lua` somewhere in your addon or gamemode (eg. `myaddon/lua/myaddon/imgui.lua`) and make sure it is `AddCSLuaFile`d and available to your clientside rendering code. 14 | 15 | ### Example 16 | 17 | Using with entities: 18 | ```lua 19 | -- 3D2D UI should be rendered in translucent pass, so this should be either TRANSLUCENT or BOTH 20 | ENT.RenderGroup = RENDERGROUP_TRANSLUCENT 21 | 22 | function ENT:DrawTranslucent() 23 | -- While you can of course use the imgui.Start3D2D function for entities, IMGUI has some special syntax 24 | -- This function automatically calls LocalToWorld and LocalToWorldAngles respectively on position and angles 25 | if imgui.Entity3D2D(self, Vector(0, 0, 50), Angle(0, 90, 90), 0.1) then 26 | -- render things 27 | 28 | imgui.End3D2D() 29 | end 30 | end 31 | ``` 32 | 33 | Using with gamemode rendering hooks: 34 | ```lua 35 | local imgui = include("imgui.lua") -- imgui.lua should be in same folder and AddCSLuaFile'd 36 | 37 | hook.Add("PostDrawTranslucentRenderables", "PaintIMGUI", function(bDrawingSkybox, bDrawingDepth) 38 | -- Don't render during depth pass 39 | if bDrawingDepth then return end 40 | 41 | -- Starts the 3D2D context at given position, angle and scale. 42 | -- First 3 arguments are equivalent to cam.Start3D2D arguments. 43 | -- Fourth argument is the distance at which the UI panel won't be rendered anymore 44 | -- Fifth argument is the distance at which the UI will start fading away 45 | -- Function returns boolean indicating whether we should proceed with the rendering, hence the if statement 46 | -- These specific coordinates are for gm_construct at next to spawn 47 | if imgui.Start3D2D(Vector(980, -83, -79), Angle(0, 270, 90), 0.1, 200, 150) then 48 | -- This is a regular 3D2D context, so you can use normal surface functions to draw things 49 | surface.SetDrawColor(255, 127, 0) 50 | surface.DrawRect(0, 0, 100, 20) 51 | 52 | -- The main priority of the library is providing interactable panels 53 | -- This creates a clickable text button at x=0, y=30 with width=100, height=25 54 | -- The first argument is text to render inside button 55 | -- The second argument is special font syntax, that dynamically creates font "Roboto" at size 24 56 | -- The special syntax is just for convinience; you can use normal Garry's Mod font names in place 57 | -- The third, fourth, fith and sixth arguments are for x, y, width and height 58 | -- The seventh argument is the border width (optional) 59 | -- The last 3 arguments are for color, hover color, and press color (optional) 60 | if imgui.xTextButton("Foo bar", "!Roboto@24", 0, 30, 100, 25, 1, Color(255,255,255), Color(0,0,255), Color(255,0,0)) then 61 | -- the xTextButton function returns true, if user clicked on this area during this frame 62 | print("yay, we were clicked :D") 63 | end 64 | 65 | -- End the 3D2D context 66 | imgui.End3D2D() 67 | end 68 | end) 69 | ``` 70 | 71 | Prefer entities over global hooks, since they work better with [Source engine visiblity optimizations](https://developer.valvesoftware.com/wiki/Visibility_optimization). 72 | 73 | ### Debugging 74 | 75 | ![image](https://i.imgur.com/1KWwo57.png) 76 | 77 | Setting the `developer` convar to non-zero value draws a debugging panel on top of each IMGUI panel you draw. It shows few pieces of useful information: 78 | - The mouse coordinates or the reason why input is not enabled at the moment 79 | - World position of the TDUI panel, our distance from it and the distance at which the interface is hidden 80 | - World angles of the TDUI panel, the dot product between eye position and the panel and the angle between eye position and the panel 81 | - How many milliseconds did we spend on rendering this UI per frame (averaged over 100 frames/renders) 82 | 83 | If you wish to hide the developer panel even with `developer` cvar, set `imgui.DisableDeveloperMode = true` right after importing the library. 84 | 85 | ### API 86 | 87 | #### 3D2D Context 88 | Start 3D2D context (calls `cam.Start3D2D` internally) 89 | ```lua 90 | imgui.Start3D2D(pos, angles, scale, distanceHide, distanceFadeStart) 91 | ``` 92 | 93 | Start 3D2D context on an entity. Equivalent to `imgui.Start3D2D`, except a) `pos`/`angles` are automatically transformed from local entity coordinates into world coordinates and b) the entity will be ignored in mouse input collision checks. 94 | 95 | ```lua 96 | imgui.Entity3D2D(ent, lpos, lang, scale, distanceHide, distanceFadeStart) 97 | ``` 98 | 99 | Start 3D2D context on a weapon. Intended for use with weapon viewmodels. Automatically corrects for FOV differences between the player and viewmodel. Unlike `imgui.Entity3D2D`, this does **not** transform local coordinates to world coordinates, as it is assumed you are working with bone positions (which are already in world-space). 100 | 101 | ```lua 102 | imgui.Weapon3D2D(swep, pos, ang, scale, distanceHide, distanceFadeStart) 103 | ``` 104 | 105 | Ends 3D2D Context 106 | ```lua 107 | imgui.End3D2D() 108 | ``` 109 | 110 | #### Cursor 111 | Retrieves cursor position in 3D2D space. `mx`/`my` are null if player is not looking at the interface 112 | ```lua 113 | local mx, my = imgui.CursorPos() 114 | ``` 115 | 116 | Whether player's 3D2D cursor is within given bounds 117 | ```lua 118 | local hovering = imgui.IsHovering(x, y, w, h) 119 | ``` 120 | 121 | Whether player is currently pressing 122 | ```lua 123 | local pressing = imgui.IsPressing() 124 | ``` 125 | 126 | Whether player is pressed during this frame. This is guaranteed to only be called once per click 127 | ```lua 128 | local pressed = imgui.IsPressed() 129 | ``` 130 | 131 | #### UI functions (prefixed by x to separate them from core functionality) 132 | 133 | Draws a rectangle button without any text or content 134 | ```lua 135 | local wasPressed = imgui.xButton(x, y, w, h, borderWidth, borderClr, hoverClr, pressColor) 136 | ``` 137 | 138 | Draws a button with text inside. The `font` parameter is passed through `imgui.xFont`, so special font syntax is supported. 139 | The text is automatically centered within the button. 140 | ```lua 141 | local wasPressed = imgui.xTextButton(text, font, x, y, w, h, borderWidth, color, hoverClr, pressColor) 142 | ``` 143 | 144 | Draws a cursor IF the cursor is within given bounds. Note: `x`,`y`,`w`,`h` should be the bounds for your whole IMGUI interface, eg. `0, 0, 512, 512` if you draw into the 3d2d space within those bounds. 145 | ```lua 146 | imgui.xCursor(x, y, w, h) 147 | ``` 148 | 149 | #### Utility 150 | Retrieves font name usable for Garry's Mod functions based on parameter. See __Special font API__ section below 151 | ```lua 152 | local fontName = imgui.xFont("!Roboto@24") 153 | ``` 154 | 155 | Expands the entity's render bounds to cover the whole rectangle passed as 3D2D coordinates. Note: `x`,`y`,`w`,`h` should be the bounds for your whole IMGUI interface, eg. `0, 0, 512, 512` if you draw into the 3d2d space within those bounds. 156 | (only usable inside `imgui.Entity3D2D` block, before `imgui.End3D2D` call) 157 | ```lua 158 | imgui.ExpandRenderBoundsFromRect(x, y, w, h) 159 | ``` 160 | 161 | ### Error recovery 162 | 163 | One of the goals of the library is to not crash the client even if something throws an error during the 3D2D context. This is achieved by halting all rendering library-wide if we detect attempt at starting a new 3D2D context without never having called `End3D2D` in between. 164 | 165 | The recovery protocol triggers automatically if you try to nest 3D2D contexts due to error or any other reason. You'll be notified with a `[IMGUI] Starting a new IMGUI context when previous one is still rendering. Shutting down rendering pipeline to prevent crashes..` message in console if that happens. The only way to recover from this error state is to re-initialize the whole library to reset the internal global state of IMGUI. There is no programmatic way to do this at the moment, but you can re-save the `imgui.lua` to trigger Lua autorefresh on it, or just reimport it. 166 | 167 | ### Special font API 168 | 169 | IMGUI comes with a simplified method for creating fonts. If you use the built-in functions, such as `imgui.xTextButton`, the passed font argument will automatically go through the font syntax parser, but you can also access it directly with `imgui.xFont`. 170 | 171 | Here's an example of using `imgui.xFont` for drawing normal text: 172 | ```lua 173 | -- Draw 'Foo bar' using Roboto font at font size 30 at 0, 0 174 | draw.SimpleText("Foo bar", imgui.xFont("!Roboto@30"), 0, 0) 175 | ``` 176 | 177 | ### Networking data 178 | 179 | You'll probably want to network data from client to server on button press or other clientside action at some point. IMGUI doesn't support this by itself, but [netdata](https://github.com/wyozi-gmod/netdata) is a pretty good fit for entity-based UIs. 180 | 181 | Here's an example entity with IMGUI + netdata: 182 | ```lua 183 | ENT.Type = "anim" 184 | 185 | if SERVER then 186 | function ENT:Initialize() 187 | self:SetModel("models/props_phx/construct/glass/glass_plate1x1.mdl") 188 | self:PhysicsInit(SOLID_VPHYSICS) 189 | self.Greeters = {} 190 | end 191 | 192 | function ENT:ReceiveNetAction(cl) 193 | table.insert(self.Greeters, cl:Nick()) 194 | self:NetDataUpdate() 195 | end 196 | 197 | function ENT:NetDataWrite() 198 | net.WriteTable(self.Greeters) 199 | end 200 | end 201 | if CLIENT then 202 | ENT.RenderGroup = RENDERGROUP_BOTH 203 | 204 | function ENT:NetDataRead() 205 | self.Greeters = net.ReadTable() 206 | end 207 | 208 | function ENT:DrawTranslucent() 209 | local imgui = resort.bar.imgui 210 | 211 | if imgui.Entity3D2D(self, Vector(0, 0, 3.5), Angle(0, 0, 0), 0.1) then 212 | if imgui.xTextButton("Say hello", "!Roboto@24", -100, -200, 200, 50, 1) then 213 | self:StartNetAction() 214 | net.SendToServer() 215 | end 216 | 217 | if self.Greeters then 218 | for i, nick in pairs(self.Greeters) do 219 | draw.SimpleText(nick, "DermaLarge", 0, -160 + i * 25, nil, TEXT_ALIGN_CENTER) 220 | end 221 | end 222 | 223 | imgui.End3D2D() 224 | end 225 | end 226 | end 227 | ``` 228 | -------------------------------------------------------------------------------- /imgui.lua: -------------------------------------------------------------------------------- 1 | local imgui = {} 2 | 3 | imgui.skin = { 4 | background = Color(0, 0, 0, 0), 5 | backgroundHover = Color(0, 0, 0, 0), 6 | 7 | border = Color(255, 255, 255), 8 | borderHover = Color(255, 127, 0), 9 | borderPress = Color(255, 80, 0), 10 | 11 | foreground = Color(255, 255, 255), 12 | foregroundHover = Color(255, 127, 0), 13 | foregroundPress = Color(255, 80, 0), 14 | } 15 | 16 | local devCvar = GetConVar("developer") 17 | function imgui.IsDeveloperMode() 18 | return not imgui.DisableDeveloperMode and devCvar:GetInt() > 0 19 | end 20 | 21 | local _devMode = false -- cached local variable updated once in a while 22 | 23 | function imgui.Hook(name, id, callback) 24 | local hookUniqifier = debug.getinfo(4).short_src 25 | hook.Add(name, "IMGUI / " .. id .. " / " .. hookUniqifier, callback) 26 | end 27 | 28 | local localPlayer 29 | local gState = {} 30 | 31 | local function shouldAcceptInput() 32 | -- don't process input during non-main renderpass 33 | if render.GetRenderTarget() ~= nil then 34 | return false 35 | end 36 | 37 | -- don't process input if we're doing VGUI stuff (and not in context menu) 38 | if vgui.CursorVisible() and vgui.GetHoveredPanel() ~= g_ContextMenu and not vgui.IsHoveringWorld() then 39 | return false 40 | end 41 | 42 | return true 43 | end 44 | 45 | imgui.Hook("PreRender", "Input", function() 46 | -- calculate mouse state 47 | if shouldAcceptInput() then 48 | local useBind = input.LookupBinding("+use", true) 49 | local attackBind = input.LookupBinding("+attack", true) 50 | local USE = useBind and input.GetKeyCode(useBind) 51 | local ATTACK = attackBind and input.GetKeyCode(attackBind) 52 | 53 | local wasPressing = gState.pressing 54 | gState.pressing = (USE and input.IsButtonDown(USE)) or (ATTACK and input.IsButtonDown(ATTACK)) 55 | gState.pressed = not wasPressing and gState.pressing 56 | end 57 | end) 58 | 59 | hook.Add("NotifyShouldTransmit", "IMGUI / ClearRenderBounds", function(ent, shouldTransmit) 60 | if shouldTransmit and ent._imguiRBExpansion then 61 | ent._imguiRBExpansion = nil 62 | end 63 | end) 64 | 65 | local traceResultTable = {} 66 | local traceQueryTable = { output = traceResultTable, filter = {} } 67 | local function isObstructed(eyePos, hitPos, ignoredEntity, ignoreParent) 68 | local q = traceQueryTable 69 | q.start = eyePos 70 | q.endpos = hitPos 71 | q.filter[1] = localPlayer 72 | q.filter[2] = ignoredEntity 73 | if ignoreParent == true then 74 | local parent = ignoredEntity:GetParent() 75 | if IsValid(parent) then 76 | q.filter[3] = parent 77 | else 78 | q.filter[3] = nil 79 | end 80 | else 81 | q.filter[3] = nil 82 | end 83 | 84 | local tr = util.TraceLine(q) 85 | if tr.Hit then 86 | return true, tr.Entity 87 | else 88 | return false 89 | end 90 | end 91 | 92 | function imgui.Start3D2D(pos, angles, scale, distanceHide, distanceFadeStart) 93 | if not IsValid(localPlayer) then 94 | localPlayer = LocalPlayer() 95 | end 96 | 97 | if gState.shutdown == true then 98 | return 99 | end 100 | 101 | if gState.rendering == true then 102 | print( 103 | "[IMGUI] Starting a new IMGUI context when previous one is still rendering" .. 104 | "Shutting down rendering pipeline to prevent crashes.." 105 | ) 106 | gState.shutdown = true 107 | return false 108 | end 109 | 110 | _devMode = imgui.IsDeveloperMode() 111 | 112 | local eyePos = localPlayer:EyePos() 113 | local eyePosToPos = pos - eyePos 114 | 115 | -- OPTIMIZATION: Test that we are in front of the UI 116 | do 117 | local normal = angles:Up() 118 | local dot = eyePosToPos:Dot(normal) 119 | 120 | if _devMode then gState._devDot = dot end 121 | 122 | -- since normal is pointing away from surface towards viewer, dot<0 is visible 123 | if dot >= 0 then 124 | return false 125 | end 126 | end 127 | 128 | -- OPTIMIZATION: Distance based fade/hide 129 | if distanceHide then 130 | local distance = eyePosToPos:Length() 131 | if distance > distanceHide then 132 | return false 133 | end 134 | 135 | if _devMode then 136 | gState._devDist = distance 137 | gState._devHideDist = distanceHide 138 | end 139 | 140 | if distanceHide and distanceFadeStart and distance > distanceFadeStart then 141 | local blend = math.min(math.Remap(distance, distanceFadeStart, distanceHide, 1, 0), 1) 142 | render.SetBlend(blend) 143 | surface.SetAlphaMultiplier(blend) 144 | end 145 | end 146 | 147 | gState.rendering = true 148 | gState.pos = pos 149 | gState.angles = angles 150 | gState.scale = scale 151 | 152 | cam.Start3D2D(pos, angles, scale) 153 | 154 | -- calculate mousepos 155 | if not vgui.CursorVisible() or vgui.IsHoveringWorld() then 156 | local tr = localPlayer:GetEyeTrace() 157 | local eyepos = tr.StartPos 158 | local eyenormal 159 | 160 | if vgui.CursorVisible() and vgui.IsHoveringWorld() then 161 | eyenormal = imgui.ScreenToVector(input.GetCursorPos()) 162 | else 163 | eyenormal = tr.Normal 164 | end 165 | 166 | local planeNormal = angles:Up() 167 | 168 | local hitPos = util.IntersectRayWithPlane(eyepos, eyenormal, pos, planeNormal) 169 | if hitPos then 170 | local obstructed, obstructer = isObstructed(eyepos, hitPos, gState.entity, true) 171 | if obstructed then 172 | gState.mx = nil 173 | gState.my = nil 174 | 175 | if _devMode then gState._devInputBlocker = "collision " .. obstructer:GetClass() .. "/" .. obstructer:EntIndex() end 176 | else 177 | local diff = pos - hitPos 178 | 179 | -- This cool code is from Willox's keypad CalculateCursorPos 180 | local x = diff:Dot(-angles:Forward()) / scale 181 | local y = diff:Dot(-angles:Right()) / scale 182 | 183 | gState.mx = x 184 | gState.my = y 185 | end 186 | else 187 | gState.mx = nil 188 | gState.my = nil 189 | 190 | if _devMode then gState._devInputBlocker = "not looking at plane" end 191 | end 192 | else 193 | gState.mx = nil 194 | gState.my = nil 195 | 196 | if _devMode then gState._devInputBlocker = "not hovering world" end 197 | end 198 | 199 | if _devMode then gState._renderStarted = SysTime() end 200 | 201 | return true 202 | end 203 | 204 | function imgui.ScreenToVector(x, y) 205 | local view = render.GetViewSetup() 206 | local w, h = ScrW(), ScrH() 207 | local fov = view.fov 208 | if gState.weapon then 209 | fov = view.fovviewmodel 210 | end 211 | 212 | return util.AimVector(view.angles, fov, x, y, w, h) 213 | end 214 | 215 | function imgui.Weapon3D2D(weapon, pos, ang, scale, ...) 216 | gState.weapon = weapon 217 | local ret = imgui.Start3D2D(pos, ang, scale, ...) 218 | if not ret then 219 | gState.weapon = nil 220 | end 221 | return ret 222 | end 223 | 224 | function imgui.Entity3D2D(ent, lpos, lang, scale, ...) 225 | gState.entity = ent 226 | local ret = imgui.Start3D2D(ent:LocalToWorld(lpos), ent:LocalToWorldAngles(lang), scale, ...) 227 | if not ret then 228 | gState.entity = nil 229 | end 230 | return ret 231 | end 232 | 233 | local function calculateRenderBounds(x, y, w, h) 234 | local pos = gState.pos 235 | local fwd, right = gState.angles:Forward(), gState.angles:Right() 236 | local scale = gState.scale 237 | local firstCorner, secondCorner = 238 | pos + fwd * x * scale + right * y * scale, 239 | pos + fwd * (x + w) * scale + right * (y + h) * scale 240 | 241 | local minrb, maxrb = Vector(math.huge, math.huge, math.huge), Vector(-math.huge, -math.huge, -math.huge) 242 | 243 | minrb.x = math.min(minrb.x, firstCorner.x, secondCorner.x) 244 | minrb.y = math.min(minrb.y, firstCorner.y, secondCorner.y) 245 | minrb.z = math.min(minrb.z, firstCorner.z, secondCorner.z) 246 | maxrb.x = math.max(maxrb.x, firstCorner.x, secondCorner.x) 247 | maxrb.y = math.max(maxrb.y, firstCorner.y, secondCorner.y) 248 | maxrb.z = math.max(maxrb.z, firstCorner.z, secondCorner.z) 249 | 250 | return minrb, maxrb 251 | end 252 | 253 | function imgui.ExpandRenderBoundsFromRect(x, y, w, h) 254 | local ent = gState.entity 255 | if IsValid(ent) then 256 | -- make sure we're not applying same expansion twice 257 | local expansion = ent._imguiRBExpansion 258 | if expansion then 259 | local ex, ey, ew, eh = unpack(expansion) 260 | if ex == x and ey == y and ew == w and eh == h then 261 | return 262 | end 263 | end 264 | 265 | local minrb, maxrb = calculateRenderBounds(x, y, w, h) 266 | 267 | ent:SetRenderBoundsWS(minrb, maxrb) 268 | if _devMode then 269 | print("[IMGUI] Updated renderbounds of ", ent, " to ", minrb, "x", maxrb) 270 | end 271 | 272 | ent._imguiRBExpansion = {x, y, w, h} 273 | else 274 | if _devMode then 275 | print("[IMGUI] Attempted to update renderbounds when entity is not valid!! ", debug.traceback()) 276 | end 277 | end 278 | end 279 | 280 | local devOffset = Vector(0, 0, 30) 281 | local devColours = { 282 | background = Color(0, 0, 0, 200), 283 | title = Color(78, 205, 196), 284 | mouseHovered = Color(0, 255, 0), 285 | mouseUnhovered = Color(255, 0, 0), 286 | pos = Color(255, 255, 255), 287 | distance = Color(200, 200, 200, 200), 288 | ang = Color(255, 255, 255), 289 | dot = Color(200, 200, 200, 200), 290 | angleToEye = Color(200, 200, 200, 200), 291 | renderTime = Color(255, 255, 255), 292 | renderBounds = Color(0, 0, 255) 293 | } 294 | 295 | local function developerText(str, x, y, clr) 296 | draw.SimpleText( 297 | str, "DefaultFixedDropShadow", x, y, clr, TEXT_ALIGN_CENTER, nil 298 | ) 299 | end 300 | 301 | local function drawDeveloperInfo() 302 | local camAng = localPlayer:EyeAngles() 303 | camAng:RotateAroundAxis(camAng:Right(), 90) 304 | camAng:RotateAroundAxis(camAng:Up(), -90) 305 | 306 | cam.IgnoreZ(true) 307 | cam.Start3D2D(gState.pos + devOffset, camAng, 0.15) 308 | 309 | local bgCol = devColours["background"] 310 | surface.SetDrawColor(bgCol.r, bgCol.g, bgCol.b, bgCol.a) 311 | surface.DrawRect(-100, 0, 200, 140) 312 | 313 | local titleCol = devColours["title"] 314 | developerText("imgui developer", 0, 5, titleCol) 315 | 316 | surface.SetDrawColor(titleCol.r, titleCol.g, titleCol.b) 317 | surface.DrawLine(-50, 16, 50, 16) 318 | 319 | local mx, my = gState.mx, gState.my 320 | if mx and my then 321 | developerText( 322 | string.format("mouse: hovering %d x %d", mx, my), 323 | 0, 20, devColours["mouseHovered"] 324 | ) 325 | else 326 | developerText( 327 | string.format("mouse: %s", gState._devInputBlocker or ""), 328 | 0, 20, devColours["mouseUnhovered"] 329 | ) 330 | end 331 | 332 | local pos = gState.pos 333 | developerText( 334 | string.format("pos: %.2f %.2f %.2f", pos.x, pos.y, pos.z), 335 | 0, 40, devColours["pos"] 336 | ) 337 | 338 | developerText( 339 | string.format("distance %.2f / %.2f", gState._devDist or 0, gState._devHideDist or 0), 340 | 0, 53, devColours["distance"] 341 | ) 342 | 343 | local ang = gState.angles 344 | developerText(string.format("ang: %.2f %.2f %.2f", ang.p, ang.y, ang.r), 0, 75, devColours["ang"]) 345 | developerText(string.format("dot %d", gState._devDot or 0), 0, 88, devColours["dot"]) 346 | 347 | local angToEye = (pos - localPlayer:EyePos()):Angle() 348 | angToEye:RotateAroundAxis(ang:Up(), -90) 349 | angToEye:RotateAroundAxis(ang:Right(), 90) 350 | 351 | developerText( 352 | string.format("angle to eye (%d,%d,%d)", angToEye.p, angToEye.y, angToEye.r), 353 | 0, 100, devColours["angleToEye"] 354 | ) 355 | 356 | developerText( 357 | string.format("rendertime avg: %.2fms", (gState._devBenchAveraged or 0) * 1000), 358 | 0, 120, devColours["renderTime"] 359 | ) 360 | 361 | cam.End3D2D() 362 | cam.IgnoreZ(false) 363 | 364 | local ent = gState.entity 365 | if IsValid(ent) and ent._imguiRBExpansion then 366 | local ex, ey, ew, eh = unpack(ent._imguiRBExpansion) 367 | local minrb, maxrb = calculateRenderBounds(ex, ey, ew, eh) 368 | render.DrawWireframeBox(vector_origin, angle_zero, minrb, maxrb, devColours["renderBounds"]) 369 | end 370 | end 371 | 372 | function imgui.End3D2D() 373 | if gState then 374 | if _devMode then 375 | local renderTook = SysTime() - gState._renderStarted 376 | gState._devBenchTests = (gState._devBenchTests or 0) + 1 377 | gState._devBenchTaken = (gState._devBenchTaken or 0) + renderTook 378 | if gState._devBenchTests == 100 then 379 | gState._devBenchAveraged = gState._devBenchTaken / 100 380 | gState._devBenchTests = 0 381 | gState._devBenchTaken = 0 382 | end 383 | end 384 | 385 | gState.rendering = false 386 | cam.End3D2D() 387 | render.SetBlend(1) 388 | surface.SetAlphaMultiplier(1) 389 | 390 | if _devMode then 391 | drawDeveloperInfo() 392 | end 393 | 394 | gState.entity = nil 395 | gState.weapon = nil 396 | end 397 | end 398 | 399 | function imgui.CursorPos() 400 | local mx, my = gState.mx, gState.my 401 | return mx, my 402 | end 403 | 404 | function imgui.IsHovering(x, y, w, h) 405 | local mx, my = gState.mx, gState.my 406 | return mx and my and mx >= x and mx <= (x + w) and my >= y and my <= (y + h) 407 | end 408 | function imgui.IsPressing() 409 | return shouldAcceptInput() and gState.pressing 410 | end 411 | function imgui.IsPressed() 412 | return shouldAcceptInput() and gState.pressed 413 | end 414 | 415 | -- String->Bool mappings for whether font has been created 416 | local _createdFonts = {} 417 | 418 | -- Cached IMGUIFontNamd->GModFontName 419 | local _imguiFontToGmodFont = {} 420 | 421 | local EXCLAMATION_BYTE = string.byte("!") 422 | function imgui.xFont(font, defaultSize) 423 | -- special font 424 | if string.byte(font, 1) == EXCLAMATION_BYTE then 425 | 426 | local existingGFont = _imguiFontToGmodFont[font] 427 | if existingGFont then 428 | return existingGFont 429 | end 430 | 431 | -- Font not cached; parse the font 432 | local name, size = font:match("!([^@]+)@(.+)") 433 | if size then size = tonumber(size) end 434 | 435 | if not size and defaultSize then 436 | name = font:match("^!([^@]+)$") 437 | size = defaultSize 438 | end 439 | 440 | local fontName = string.format("IMGUI_%s_%d", name, size) 441 | _imguiFontToGmodFont[font] = fontName 442 | if not _createdFonts[fontName] then 443 | surface.CreateFont(fontName, { 444 | font = name, 445 | size = size 446 | }) 447 | _createdFonts[fontName] = true 448 | end 449 | 450 | return fontName 451 | end 452 | return font 453 | end 454 | 455 | function imgui.xButton(x, y, w, h, borderWidth, borderClr, hoverClr, pressColor) 456 | local bw = borderWidth or 1 457 | 458 | local bgColor = imgui.IsHovering(x, y, w, h) and imgui.skin.backgroundHover or imgui.skin.background 459 | local borderColor = 460 | ((imgui.IsPressing() and imgui.IsHovering(x, y, w, h)) and (pressColor or imgui.skin.borderPress)) 461 | or (imgui.IsHovering(x, y, w, h) and (hoverClr or imgui.skin.borderHover)) 462 | or (borderClr or imgui.skin.border) 463 | 464 | surface.SetDrawColor(bgColor) 465 | surface.DrawRect(x, y, w, h) 466 | 467 | if bw > 0 then 468 | surface.SetDrawColor(borderColor) 469 | 470 | surface.DrawRect(x, y, w, bw) 471 | surface.DrawRect(x, y + bw, bw, h - bw * 2) 472 | surface.DrawRect(x, y + h-bw, w, bw) 473 | surface.DrawRect(x + w - bw + 1, y, bw, h) 474 | end 475 | 476 | return shouldAcceptInput() and imgui.IsHovering(x, y, w, h) and gState.pressed 477 | end 478 | 479 | function imgui.xCursor(x, y, w, h) 480 | local fgColor = imgui.IsPressing() and imgui.skin.foregroundPress or imgui.skin.foreground 481 | local mx, my = gState.mx, gState.my 482 | 483 | if not mx or not my then return end 484 | 485 | if x and w and (mx < x or mx > x + w) then return end 486 | if y and h and (my < y or my > y + h) then return end 487 | 488 | local cursorSize = math.ceil(0.3 / gState.scale) 489 | surface.SetDrawColor(fgColor) 490 | surface.DrawLine(mx - cursorSize, my, mx + cursorSize, my) 491 | surface.DrawLine(mx, my - cursorSize, mx, my + cursorSize) 492 | end 493 | 494 | function imgui.xTextButton(text, font, x, y, w, h, borderWidth, color, hoverClr, pressColor) 495 | local fgColor = 496 | ((imgui.IsPressing() and imgui.IsHovering(x, y, w, h)) and (pressColor or imgui.skin.foregroundPress)) 497 | or (imgui.IsHovering(x, y, w, h) and (hoverClr or imgui.skin.foregroundHover)) 498 | or (color or imgui.skin.foreground) 499 | 500 | local clicked = imgui.xButton(x, y, w, h, borderWidth, color, hoverClr, pressColor) 501 | 502 | font = imgui.xFont(font, math.floor(h * 0.618)) 503 | draw.SimpleText(text, font, x + w / 2, y + h / 2, fgColor, TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER) 504 | 505 | return clicked 506 | end 507 | 508 | return imgui 509 | --------------------------------------------------------------------------------