├── README.md └── flex.lua /README.md: -------------------------------------------------------------------------------- 1 | Flex layout panel for Garry's mod 2 | 3 | Use it to your liking, I don't care 4 | 5 | Provided as is, any bug reports or fixes are not required but appreciated 6 | 7 | Hold down CTRL+SHIFT+C to open the inspector (a thing similar to one you'll find in Chrome dev tools), it will allow you to view/change any properties of any flex panels on the screen. Hold down shift to enable panel selection. Left click to select currently highlighted panel. Right click to deselect. This thing is meant to be useful during debugging and prototyping of your UI 8 | 9 | The inspector itself serves as an example of how approximately to work with this shit, take a look at the code basically, I'm too lazy to document it atm 10 | 11 | Here's approximately what it looks like: https://youtu.be/NXE3vxgcleA 12 | 13 | ![](https://i.imgur.com/LrSvyxd.png) 14 | -------------------------------------------------------------------------------- /flex.lua: -------------------------------------------------------------------------------- 1 | AddCSLuaFile() 2 | if SERVER then return end 3 | 4 | local FLEX_DIR_ROW = 0 _G.FLEX_DIR_ROW = FLEX_DIR_ROW 5 | local FLEX_DIR_COL = 1 _G.FLEX_DIR_COL = FLEX_DIR_COL 6 | 7 | local FLEX_FLOW_START = 0 _G.FLEX_FLOW_START = FLEX_FLOW_START 8 | local FLEX_FLOW_CENTER = 1 _G.FLEX_FLOW_CENTER = FLEX_FLOW_CENTER 9 | local FLEX_FLOW_END = 2 _G.FLEX_FLOW_END = FLEX_FLOW_END 10 | local FLEX_FLOW_STRETCH = 3 _G.FLEX_FLOW_STRETCH = FLEX_FLOW_STRETCH 11 | 12 | local FLEX_WRAP_NONE = 0 _G.FLEX_WRAP_NONE = FLEX_WRAP_NONE 13 | local FLEX_WRAP_BEFORE_SHRINK = 1 _G.FLEX_WRAP_BEFORE_SHRINK = FLEX_WRAP_BEFORE_SHRINK 14 | local FLEX_WRAP_AFTER_SHRINK = 2 _G.FLEX_WRAP_AFTER_SHRINK = FLEX_WRAP_AFTER_SHRINK 15 | 16 | 17 | local FLEX_INTERP_LINEAR = 0 _G.FLEX_INTERP_LINEAR = FLEX_INTERP_LINEAR 18 | local FLEX_INTERP_SMOOTH = 1 _G.FLEX_INTERP_SMOOTH = FLEX_INTERP_SMOOTH 19 | local FLEX_INTERP_FADEIN = 2 _G.FLEX_INTERP_FADEIN = FLEX_INTERP_FADEIN 20 | local FLEX_INTERP_FADEOUT = 3 _G.FLEX_INTERP_FADEOUT = FLEX_INTERP_FADEOUT 21 | 22 | local interp = { 23 | [FLEX_INTERP_LINEAR] = function(x) return x end, 24 | [FLEX_INTERP_SMOOTH] = function(x) return 0.5 + math.sin((x - 0.5) * math.pi) / 2 end, 25 | [FLEX_INTERP_FADEIN] = function(x) return 1 + math.sin((x - 1) * math.pi * 0.5) end, 26 | [FLEX_INTERP_FADEOUT] = function(x) return math.sin(x * math.pi * 0.5) end, 27 | } 28 | 29 | local inf = 1 / 0 30 | 31 | local function calcLine(items, crossFlow) 32 | local main = 0 33 | local mainSpace = 0 34 | local mainGap = 0 35 | local mainMB, mainMF = 0, 0 36 | local crossStart, crossEnd, crossTotal = inf, 0, 0 37 | local stretchCrossMB, stretchCrossMF = 0, 0 38 | for i, item in pairs(items) do 39 | item.targetMain = item:DesiredMain() 40 | item.targetCross = item:DesiredCross() 41 | mainGap = math.max(mainGap, item.mainMB) 42 | if i == 1 then 43 | mainGap = 0 44 | mainMB = item.mainMB 45 | end 46 | main = main + mainGap + item.targetMain 47 | mainSpace = mainSpace + mainGap 48 | mainGap = item.mainMF 49 | local flow = item.selfCrossFlow or crossFlow 50 | crossTotal = math.max(crossTotal, item.crossMB + item.targetCross + item.crossMF) 51 | if flow == FLEX_FLOW_START or flow == FLEX_FLOW_STRETCH then 52 | crossStart = math.min(crossStart, item.crossMB) 53 | crossEnd = math.max(crossEnd, item.crossMB + item.targetCross) 54 | if flow == FLEX_FLOW_STRETCH then 55 | stretchCrossMB = math.max(stretchCrossMB, item.crossMB) 56 | stretchCrossMF = math.max(stretchCrossMF, item.crossMF) 57 | end 58 | elseif flow == FLEX_FLOW_END then 59 | crossStart = math.min(crossStart, crossTotal - item.crossMF - item.targetCross) 60 | crossEnd = math.max(crossEnd, crossTotal - item.crossMF) 61 | elseif flow == FLEX_FLOW_CENTER then 62 | crossStart = math.min(crossStart, crossTotal / 2 - item.targetCross / 2) 63 | crossEnd = math.max(crossEnd, crossTotal / 2 + item.targetCross / 2) 64 | end 65 | end 66 | mainMF = mainGap 67 | local cross = crossEnd - crossStart 68 | local crossMB = math.max(crossStart, stretchCrossMB) 69 | local crossMF = math.max(crossTotal - crossEnd, stretchCrossMF) 70 | return 71 | main, 72 | mainSpace, 73 | mainMB, 74 | mainMF, 75 | cross, 76 | crossMB, 77 | crossMF 78 | end 79 | 80 | local function calcLines(items, dir, wrap, mainSize, mainPB, mainPF) 81 | if wrap == FLEX_WRAP_NONE then 82 | for i, item in pairs(items) do 83 | item:SetFlowDir(dir) 84 | end 85 | return {items} 86 | end 87 | local curLine 88 | local lines = {} 89 | local lineLen = 0 90 | local prevGap = mainPB 91 | for i, item in pairs(items) do 92 | if item.flex then 93 | item:SetFlowDir(dir) 94 | prevGap = math.max(prevGap, item.mainMB) 95 | local itemMainMin = 96 | wrap == FLEX_WRAP_AFTER_SHRINK and item.shrink > 0 and item.mainMin or 97 | item.main 98 | lineLen = lineLen + prevGap + itemMainMin 99 | prevGap = item.mainMF 100 | if not curLine or lineLen + math.max(prevGap, mainPF) > mainSize then 101 | curLine = {item} 102 | table.insert(lines, curLine) 103 | lineLen = math.max(mainPB, item.mainMB) + itemMainMin 104 | prevGap = item.mainMF 105 | else 106 | table.insert(curLine, item) 107 | end 108 | end 109 | end 110 | return lines 111 | end 112 | 113 | local function getGrowable(items) 114 | local growable = {} 115 | local totalGrow = 0 116 | for i, item in ipairs(items) do 117 | if item.targetMain < item.mainMax then 118 | table.insert(growable, item) 119 | totalGrow = totalGrow + item.grow 120 | end 121 | end 122 | return growable, totalGrow 123 | end 124 | 125 | local function getShrinkable(items) 126 | local shrinkable = {} 127 | local totalShrink = 0 128 | for i, item in ipairs(items) do 129 | if item.targetMain > item.mainMin then 130 | table.insert(shrinkable, item) 131 | totalShrink = totalShrink + item.shrink 132 | end 133 | end 134 | return shrinkable, totalShrink 135 | end 136 | 137 | local SCROLLX = vgui.RegisterTable({}, 'Panel') 138 | 139 | function SCROLLX:Init() 140 | self:SetSize(8, 8) 141 | end 142 | 143 | function SCROLLX:Paint(w, h) 144 | local flex = self:GetParent() 145 | local pw = flex:GetWide() 146 | surface.SetDrawColor(0, 0, 0, 200) 147 | surface.DrawRect(0, 0, w, h) 148 | if self.dragging then 149 | local x, _ = flex:CursorPos() 150 | local scrollProgress = (x - self.clickOffset) / (pw - w) 151 | local clampedOffsetX = math.Clamp( 152 | Lerp(scrollProgress, flex.overflowXB, flex.overflowXF), 153 | flex.overflowXB, 154 | flex.overflowXF 155 | ) 156 | if clampedOffsetX ~= flex.offsetX then 157 | flex.offsetX = clampedOffsetX 158 | flex:InvalidateLayout(true) 159 | end 160 | end 161 | end 162 | 163 | function SCROLLX:OnMousePressed(mcode) 164 | if mcode == MOUSE_LEFT then 165 | self.dragging = true 166 | self:MouseCapture(true) 167 | local x, _ = self:CursorPos() 168 | self.clickOffset = x 169 | end 170 | end 171 | 172 | function SCROLLX:OnMouseReleased(mcode) 173 | if self.dragging then 174 | self:MouseCapture(false) 175 | self.dragging = false 176 | end 177 | end 178 | 179 | local SCROLLY = vgui.RegisterTable({}, 'Panel') 180 | 181 | function SCROLLY:Init() 182 | self:SetSize(8, 8) 183 | end 184 | 185 | function SCROLLY:Paint(w, h) 186 | local flex = self:GetParent() 187 | local ph = flex:GetTall() 188 | surface.SetDrawColor(0, 0, 0, 200) 189 | surface.DrawRect(0, 0, w, h) 190 | if self.dragging then 191 | local _, y = flex:CursorPos() 192 | local scrollProgress = (y - self.clickOffset) / (ph - h) 193 | local clampedOffsetY = math.Clamp( 194 | Lerp(scrollProgress, flex.overflowYB, flex.overflowYF), 195 | flex.overflowYB, 196 | flex.overflowYF 197 | ) 198 | if clampedOffsetY ~= flex.offsetY then 199 | flex.offsetY = clampedOffsetY 200 | flex:InvalidateLayout(true) 201 | end 202 | end 203 | end 204 | 205 | function SCROLLY:OnMousePressed(mcode) 206 | if mcode == MOUSE_LEFT then 207 | self.dragging = true 208 | self:MouseCapture(true) 209 | local _, y = self:CursorPos() 210 | self.clickOffset = y 211 | end 212 | end 213 | 214 | function SCROLLY:OnMouseReleased(mcode) 215 | if self.dragging then 216 | self:MouseCapture(false) 217 | self.dragging = false 218 | end 219 | end 220 | 221 | local FLEX = vgui.Register('Flex', { 222 | bgColor = nil, 223 | w = 0, wMin = 0, wMax = inf, 224 | h = 0, hMin = 0, hMax = inf, 225 | wAuto = false, hAuto = false, 226 | dir = FLEX_DIR_ROW, 227 | mainFlow = FLEX_FLOW_START, 228 | crossFlow = FLEX_FLOW_START, 229 | selfCrossFlow = nil, 230 | lineFlow = FLEX_FLOW_START, 231 | wrap = FLEX_WRAP_NONE, 232 | grow = 0, shrink = 0, 233 | scrollX = true, scrollY = true, 234 | scrollSpeedX = 16, scrollSpeedY = 16, 235 | }, 'Panel') 236 | 237 | function FLEX:Init() 238 | self.flex = true 239 | self.offsetX, self.offsetY = 0, 0 240 | self.overflowXB, self.overflowXF = 0, 0 241 | self.overflowYB, self.overflowYF = 0, 0 242 | self:SetFlowDir(self.dir) 243 | self.scrollbarX = self:Add(SCROLLX) 244 | self.scrollbarY = self:Add(SCROLLY) 245 | end 246 | 247 | function FLEX:DesiredMain() 248 | return math.Clamp(self.main, self.mainMin, self.mainMax) 249 | end 250 | 251 | function FLEX:DesiredCross() 252 | return math.Clamp(self.cross, self.crossMin, self.crossMax) 253 | end 254 | 255 | function FLEX:GetItems() 256 | local items = {} 257 | for i, child in pairs(self:GetChildren()) do 258 | if child.flex and child:IsVisible() then 259 | table.insert(items, child) 260 | end 261 | end 262 | return items 263 | end 264 | 265 | function FLEX:Anim(duration, interpMethod, animTick, animEnd) 266 | local animId = tostring(self:GetTable()) .. tostring(animTick) 267 | local animStart = CurTime() 268 | local interpFunc = interp[interpMethod] 269 | hook.Add('Think', animId, function() 270 | local progress = math.Clamp((CurTime() - animStart) / duration, 0, 1) 271 | if IsValid(self) then 272 | animTick(interpFunc(progress)) 273 | end 274 | if progress >= 1 then 275 | hook.Remove('Think', animId) 276 | if animEnd then 277 | animEnd() 278 | end 279 | end 280 | if IsValid(self) then 281 | self:InvalidateChildren(true) 282 | end 283 | end) 284 | end 285 | 286 | function FLEX:SetFlowDir(dir) 287 | local ml, mt, mr, mb = self:GetDockMargin() 288 | local pl, pt, pr, pb = self:GetDockPadding() 289 | self.ml, self.mt, self.mr, self.mb = ml, mt, mr, mb 290 | self.pl, self.pt, self.pr, self.pb = pl, pt, pr, pb 291 | if dir == FLEX_DIR_ROW then 292 | self.main, self.mainMin, self.mainMax = self.w, self.wMin, self.wMax 293 | self.cross, self.crossMin, self.crossMax = self.h, self.hMin, self.hMax 294 | self.mainMB, self.crossMB, self.mainMF, self.crossMF = ml or 0, mt or 0, mr or 0, mb or 0 295 | self.mainPB, self.crossPB, self.mainPF, self.crossPF = pl or 0, pt or 0, pr or 0, pb or 0 296 | elseif dir == FLEX_DIR_COL then 297 | self.main, self.mainMin, self.mainMax = self.h, self.hMin, self.hMax 298 | self.cross, self.crossMin, self.crossMax = self.w, self.wMin, self.wMax 299 | self.crossMB, self.mainMB, self.crossMF, self.mainMF = ml or 0, mt or 0, mr or 0, mb or 0 300 | self.crossPB, self.mainPB, self.crossPF, self.mainPF = pl or 0, pt or 0, pr or 0, pb or 0 301 | end 302 | end 303 | 304 | function FLEX:UpdateScrollbars() 305 | local w, h = self:GetSize() 306 | local clampedOffsetX = math.Clamp(self.offsetX, self.overflowXB, self.overflowXF) 307 | local clampedOffsetY = math.Clamp(self.offsetY, self.overflowYB, self.overflowYF) 308 | if clampedOffsetX ~= self.offsetX or clampedOffsetY ~= self.offsetY then 309 | self.offsetX = clampedOffsetX 310 | self.offsetY = clampedOffsetY 311 | self:InvalidateLayout(true) 312 | return 313 | end 314 | local overflowX = self.overflowXF - self.overflowXB 315 | if self.scrollX and overflowX > 1 then 316 | local scrollProgress = (self.offsetX - self.overflowXB) / overflowX 317 | local thumbSize = math.max(w * w / (w + overflowX), math.min(w / 2, 32)) 318 | local maxThumbPos = w - thumbSize 319 | self.scrollbarX:SetSize(thumbSize, 8) 320 | self.scrollbarX:SetPos(maxThumbPos * scrollProgress, h - 8) 321 | self.scrollbarX:Show() 322 | self.scrollbarX:MoveToFront() 323 | else 324 | self.scrollbarX:Hide() 325 | end 326 | local overflowY = self.overflowYF - self.overflowYB 327 | if self.scrollY and overflowY > 1 then 328 | local scrollProgress = (self.offsetY - self.overflowYB) / overflowY 329 | local thumbSize = math.max(h * h / (h + overflowY), math.min(h / 2, 32)) 330 | local maxThumbPos = h - thumbSize 331 | self.scrollbarY:SetSize(8, thumbSize) 332 | self.scrollbarY:SetPos(w - 8, maxThumbPos * scrollProgress) 333 | self.scrollbarY:Show() 334 | self.scrollbarY:MoveToFront() 335 | else 336 | self.scrollbarY:Hide() 337 | end 338 | end 339 | 340 | function FLEX:AutoSize() 341 | if self.wAuto then self.w = math.Clamp(self.cw, self.wMin, self.wMax) end 342 | if self.hAuto then self.h = math.Clamp(self.ch, self.hMin, self.hMax) end 343 | end 344 | 345 | function FLEX:PerformLayout(w, h) 346 | if not self:GetParent().flex then 347 | self:SetFlowDir(self.dir) 348 | end 349 | local isHorizontal = self.dir < FLEX_DIR_COL 350 | local main = isHorizontal and w or h 351 | local cross = isHorizontal and h or w 352 | local items = self:GetItems() 353 | local lines = calcLines(items, self.dir, self.wrap, main, self.mainPB, self.mainPF) 354 | local linesThickness = 0 355 | local lineGap 356 | for lineNumber, line in pairs(lines) do 357 | local lineMain, lineMainSpace 358 | lineMain, 359 | lineMainSpace, 360 | lineMainMB, 361 | lineMainMF, 362 | line.cross, 363 | line.crossMB, 364 | line.crossMF = calcLine(line, self.crossFlow) 365 | lineGap = lineGap and math.max(lineGap, line.crossMB) or 0 366 | linesThickness = linesThickness + lineGap + line.cross 367 | local mainSB, mainSF = math.max(self.mainPB, lineMainMB), math.max(self.mainPF, lineMainMF) 368 | local contentMainFill = lineMain - lineMainSpace 369 | local availableMainFill = main - mainSB - mainSF - lineMainSpace 370 | local remainder = availableMainFill - contentMainFill 371 | local flexResizeStart = CurTime() 372 | while math.floor(math.abs(remainder)) ~= 0 and CurTime() - flexResizeStart < 0.5 do 373 | local growing = remainder > 0 374 | local sizableItems, totalFactor 375 | if growing then 376 | sizableItems, totalFactor = getGrowable(line) 377 | else 378 | sizableItems, totalFactor = getShrinkable(line) 379 | end 380 | if totalFactor == 0 then break end 381 | local extent = remainder / totalFactor 382 | for i, item in pairs(sizableItems) do 383 | oldTargetMain = item.targetMain 384 | item.targetMain = math.Clamp( 385 | item.targetMain + extent * (growing and item.grow or item.shrink), 386 | item.mainMin, 387 | item.mainMax 388 | ) 389 | remainder = remainder - (item.targetMain - oldTargetMain) 390 | end 391 | end 392 | line.remainder = remainder 393 | end 394 | local lineCount = #lines 395 | local contentCrossSB, contentCrossSF = 396 | math.max(self.crossPB, lineCount > 0 and lines[1].crossMB or 0), 397 | math.max(self.crossPF, lineCount > 0 and lines[lineCount].crossMF or 0) 398 | local linePos = 399 | self.lineFlow == FLEX_FLOW_START and contentCrossSB or 400 | self.lineFlow == FLEX_FLOW_CENTER and 401 | (contentCrossSB + cross / 2 - linesThickness / 2 - contentCrossSB) or 402 | self.lineFlow == FLEX_FLOW_END and (cross - linesThickness - contentCrossSF) 403 | local crossGap = self.crossPB 404 | local m1, m2, c1, c2 = inf, -inf, inf, -inf 405 | for lineNumber, line in pairs(lines) do 406 | crossGap = math.max(crossGap, line.crossMB) 407 | if lineNumber > 1 then 408 | linePos = linePos + crossGap 409 | end 410 | local mainPos = 411 | self.mainFlow == FLEX_FLOW_START and 0 or 412 | self.mainFlow == FLEX_FLOW_CENTER and line.remainder / 2 or 413 | self.mainFlow == FLEX_FLOW_END and line.remainder 414 | local mainGap = self.mainPB 415 | for i, item in ipairs(line) do 416 | mainGap = math.max(mainGap, item.mainMB) 417 | mainPos = mainPos + mainGap 418 | local crossFlow = item.selfCrossFlow or self.crossFlow 419 | local crossSize = 420 | crossFlow == FLEX_FLOW_STRETCH and line.cross or 421 | item:DesiredCross() 422 | local crossPos = 423 | crossFlow == FLEX_FLOW_CENTER and line.cross / 2 - crossSize / 2 or 424 | crossFlow == FLEX_FLOW_END and 425 | line.cross - crossSize - math.max(0, item.crossMF - line.crossMF) or 426 | math.max(0, item.crossMB - crossGap) 427 | local ms, cs = item.targetMain, crossSize 428 | local mp, cp = mainPos, linePos + crossPos 429 | m1 = math.min(m1, mp - math.max(item.mainMB, self.mainPB)) 430 | c1 = math.min(c1, cp - math.max(item.crossMB, self.crossPB)) 431 | m2 = math.max(m2, mp + ms + math.max(item.mainMF, self.mainPF)) 432 | c2 = math.max(c2, cp + cs + math.max(item.crossMF, self.crossPF)) 433 | local ix, iy, iw, ih = ms, cs, mp, cp 434 | if not isHorizontal then 435 | ix, iy = iy, ix 436 | iw, ih = ih, iw 437 | end 438 | item:SetSize(ix, iy) 439 | item:SetPos(iw - self.offsetX, ih - self.offsetY) 440 | mainPos = mainPos + ms 441 | item.targetMain = nil 442 | mainGap = item.mainMF 443 | end 444 | linePos = linePos + line.cross 445 | crossGap = line.crossMF 446 | end 447 | local cw, ch = m2 - m1, c2 - c1 448 | self.overflowXB, self.overflowXF = math.min(m1, 0), -math.min(main - m2, 0) 449 | self.overflowYB, self.overflowYF = math.min(c1, 0), -math.min(cross - c2, 0) 450 | if not isHorizontal then 451 | cw, ch = ch, cw 452 | self.overflowXB, self.overflowXF, self.overflowYB, self.overflowYF = 453 | self.overflowYB, self.overflowYF, self.overflowXB, self.overflowXF 454 | end 455 | self.cw, self.ch = cw, ch 456 | self:AutoSize() 457 | self:UpdateScrollbars() 458 | end 459 | 460 | function FLEX:Paint(w, h) 461 | if self.bgColor then 462 | surface.SetDrawColor(self.bgColor) 463 | surface.DrawRect(0, 0, w, h) 464 | end 465 | end 466 | 467 | function FLEX:OnMouseWheeled(delta) 468 | if input.IsKeyDown(KEY_LSHIFT) then 469 | if self.scrollX and (self.overflowXB < 0 or self.overflowXF > 0) then 470 | local newOffset = math.Clamp(self.offsetX - delta * self.scrollSpeedX, self.overflowXB, self.overflowXF) 471 | if self.offsetX ~= newOffset then 472 | self.offsetX = newOffset 473 | self:InvalidateLayout(true) 474 | return true 475 | end 476 | end 477 | else 478 | if self.scrollY and (self.overflowYB < 0 or self.overflowYF > 0) then 479 | local newOffset = math.Clamp(self.offsetY - delta * self.scrollSpeedY, self.overflowYB, self.overflowYF) 480 | if self.offsetY ~= newOffset then 481 | self.offsetY = newOffset 482 | self:InvalidateLayout(true) 483 | return true 484 | end 485 | end 486 | end 487 | end 488 | 489 | local FLEXTEXT = vgui.Register('FlexText', {}, 'Flex') 490 | 491 | function FLEXTEXT:Init() 492 | local label = self:Add('DLabel') 493 | self.label = label 494 | label:SetAutoStretchVertical(true) 495 | end 496 | 497 | function FLEXTEXT:SetText(text) 498 | self.label:SetText(text) 499 | end 500 | 501 | function FLEXTEXT:PerformLayout(w, h) 502 | if not self:GetParent().flex then 503 | self:SetFlowDir(self.dir) 504 | end 505 | self.label:SetPos(self.pl, self.pt) 506 | self.label:SetWide(w - self.pr - self.pl) 507 | self.label:SetWrap(self.wrap ~= FLEX_WRAP_NONE) 508 | local tw, th = self.label:GetTextSize() 509 | self.cw, self.ch = self.pl + tw + self.pr, self.pt + th + self.pb 510 | self:UpdateScrollbars() 511 | self:AutoSize() 512 | end 513 | 514 | local inspector 515 | 516 | local PROPWANG = vgui.RegisterTable({}, 'DNumberWang') 517 | 518 | function PROPWANG:SetProp(propData) 519 | self.propData = propData 520 | self:SetMinMax(propData.min or 0, propData.max or 10000) 521 | self:SetDecimals(propData.decimals or 0) 522 | end 523 | 524 | function PROPWANG:OnValueChanged(val) 525 | local target = inspector and inspector.target 526 | if IsValid(target) then 527 | if self.propData.setValue then 528 | self.propData.setValue(target, val) 529 | else 530 | target[self.propData.prop] = tonumber(val) 531 | end 532 | target:GetParent():InvalidateChildren(true) 533 | end 534 | end 535 | 536 | function PROPWANG:Think() 537 | local target = inspector and inspector.target 538 | if IsValid(target) then 539 | self:SetEnabled(true) 540 | local paramVal = target[self.propData.prop] 541 | if self.oldValue ~= paramVal then 542 | self.oldValue = paramVal 543 | self:SetValue(paramVal) 544 | end 545 | else 546 | self:SetEnabled(false) 547 | end 548 | end 549 | 550 | local PROPCOMBO = vgui.RegisterTable({}, 'DComboBox') 551 | 552 | function PROPCOMBO:SetProp(propData) 553 | self.propData = propData 554 | self.optionCache = {} 555 | for i, option in pairs(propData.options) do 556 | self.optionCache[option.val] = self:AddChoice(option.text, option.val) 557 | end 558 | self.optionCache['UNSET'] = self:AddChoice('unset') 559 | self:ChooseOptionID(self.optionCache['UNSET']) 560 | end 561 | 562 | function PROPCOMBO:Think() 563 | local target = inspector and inspector.target 564 | if IsValid(target) then 565 | self:SetEnabled(true) 566 | local paramVal = target[self.propData.prop] 567 | if self.oldValue ~= paramVal then 568 | self.oldValue = paramVal 569 | local opt = self.optionCache[paramVal] or self.optionCache['UNSET'] 570 | self:ChooseOptionID(opt) 571 | end 572 | else 573 | self:SetEnabled(false) 574 | end 575 | end 576 | 577 | function PROPCOMBO:OnSelect(id, text, val) 578 | local target = inspector and inspector.target 579 | if IsValid(target) then 580 | if self.propData.setValue then 581 | self.propData.setValue(target, val) 582 | else 583 | target[self.propData.prop] = tonumber(val) 584 | end 585 | target:GetParent():InvalidateChildren(true) 586 | end 587 | end 588 | 589 | local PROPBOOL = vgui.RegisterTable({ controlWidth = 16 }, 'DCheckBox') 590 | 591 | function PROPBOOL:SetProp(propData) 592 | self.propData = propData 593 | end 594 | 595 | function PROPBOOL:Think() 596 | local target = inspector and inspector.target 597 | if IsValid(target) then 598 | self:SetEnabled(true) 599 | local paramVal = target[self.propData.prop] 600 | if self.oldValue ~= paramVal then 601 | self.oldValue = paramVal 602 | self:SetChecked(paramVal) 603 | end 604 | else 605 | self:SetEnabled(false) 606 | end 607 | end 608 | 609 | function PROPBOOL:OnChange(val) 610 | local target = inspector and inspector.target 611 | if IsValid(target) then 612 | if self.propData.setValue then 613 | self.propData.setValue(target, val) 614 | else 615 | target[self.propData.prop] = val 616 | end 617 | target:GetParent():InvalidateChildren(true) 618 | end 619 | end 620 | 621 | local INSPECTOR = vgui.RegisterTable({}, 'DFrame') 622 | 623 | local propGroups = { 624 | { 625 | 'Size', 626 | { prop = 'w', control = PROPWANG, label = 'width', desc = 'Width of the panel' }, 627 | { prop = 'wMin', control = PROPWANG, label = 'min-width', desc = 'Minimum width' }, 628 | { prop = 'wMax', control = PROPWANG, label = 'max-width', desc = 'Maximum width' }, 629 | { prop = 'wAuto', control = PROPBOOL, label = 'auto-width', 630 | desc = 'Automatically set width based on content width' }, 631 | { prop = 'h', control = PROPWANG, label = 'height', 'Height of the panel' }, 632 | { prop = 'hMin', control = PROPWANG, label = 'min-height', desc = 'Minimum height' }, 633 | { prop = 'hMax', control = PROPWANG, label = 'max-height', desc = 'Maximum height' }, 634 | { prop = 'hAuto', control = PROPBOOL, label = 'auto-height', 635 | desc = 'Automatically set height based on content height' }, 636 | }, 637 | { 638 | 'Spacing', 639 | { prop = 'ml', control = PROPWANG, label = 'margin-left', setValue = function(target, val) 640 | local _, t, r, b = target:GetDockMargin() 641 | target:DockMargin(val, t, r, b) 642 | end }, 643 | { prop = 'mt', control = PROPWANG, label = 'margin-top', setValue = function(target, val) 644 | local l, _, r, b = target:GetDockMargin() 645 | target:DockMargin(l, val, r, b) 646 | end }, 647 | { prop = 'mr', control = PROPWANG, label = 'margin-right', setValue = function(target, val) 648 | local l, t, _, b = target:GetDockMargin() 649 | target:DockMargin(l, t, val, b) 650 | end }, 651 | { prop = 'mb', control = PROPWANG, label = 'margin-bottom', setValue = function(target, val) 652 | local l, t, r, _ = target:GetDockMargin() 653 | target:DockMargin(l, t, r, val) 654 | end }, 655 | { prop = 'pl', control = PROPWANG, label = 'padding-left', setValue = function(target, val) 656 | local _, t, r, b = target:GetDockPadding() 657 | target:DockPadding(val, t, r, b) 658 | end }, 659 | { prop = 'pt', control = PROPWANG, label = 'padding-top', setValue = function(target, val) 660 | local l, _, r, b = target:GetDockPadding() 661 | target:DockPadding(l, val, r, b) 662 | end }, 663 | { prop = 'pr', control = PROPWANG, label = 'padding-right', setValue = function(target, val) 664 | local l, t, _, b = target:GetDockPadding() 665 | target:DockPadding(l, t, val, b) 666 | end }, 667 | { prop = 'pb', control = PROPWANG, label = 'padding-bottom', setValue = function(target, val) 668 | local l, t, r, _ = target:GetDockPadding() 669 | target:DockPadding(l, t, r, val) 670 | end }, 671 | }, 672 | { 673 | 'Flex parameters', 674 | { prop = 'dir', control = PROPCOMBO, label = 'direction', options = { 675 | { text = 'row', val = FLEX_DIR_ROW }, 676 | { text = 'column', val = FLEX_DIR_COL }, 677 | } }, 678 | { prop = 'wrap', control = PROPCOMBO, label = 'wrap', options = { 679 | { text = 'nowrap', val = FLEX_WRAP_NONE }, 680 | { text = 'before-shrink', val = FLEX_WRAP_BEFORE_SHRINK }, 681 | { text = 'after-shrink', val = FLEX_WRAP_AFTER_SHRINK }, 682 | } }, 683 | { prop = 'mainFlow', control = PROPCOMBO, label = 'main-flow', options = { 684 | { text = 'start', val = FLEX_FLOW_START }, 685 | { text = 'center', val = FLEX_FLOW_CENTER }, 686 | { text = 'end', val = FLEX_FLOW_END }, 687 | } }, 688 | { prop = 'lineFlow', control = PROPCOMBO, label = 'line-flow', options = { 689 | { text = 'start', val = FLEX_FLOW_START }, 690 | { text = 'center', val = FLEX_FLOW_CENTER }, 691 | { text = 'end', val = FLEX_FLOW_END }, 692 | } }, 693 | { prop = 'crossFlow', control = PROPCOMBO, label = 'cross-flow', options = { 694 | { text = 'start', val = FLEX_FLOW_START }, 695 | { text = 'center', val = FLEX_FLOW_CENTER }, 696 | { text = 'end', val = FLEX_FLOW_END }, 697 | { text = 'stretch', val = FLEX_FLOW_STRETCH }, 698 | } }, 699 | { prop = 'selfCrossFlow', control = PROPCOMBO, label = 'self-cross-flow', options = { 700 | { text = 'start', val = FLEX_FLOW_START }, 701 | { text = 'center', val = FLEX_FLOW_CENTER }, 702 | { text = 'end', val = FLEX_FLOW_END }, 703 | { text = 'stretch', val = FLEX_FLOW_STRETCH }, 704 | } }, 705 | { prop = 'grow', control = PROPWANG, label = 'grow' }, 706 | { prop = 'shrink', control = PROPWANG, label = 'shrink' }, 707 | }, 708 | } 709 | 710 | local function addPropDesc(root, param, paramName, desc) 711 | local paramDesc = param:Add('FlexText') 712 | paramDesc.wrap = FLEX_WRAP_BEFORE_SHRINK 713 | paramDesc:SetText(desc) 714 | paramDesc.w = 1000 715 | paramDesc.hAuto = true 716 | paramDesc:DockMargin(4, 4, 4, 4) 717 | paramDesc:DockPadding(4, 4, 4, 4) 718 | paramDesc.shrink = 1 719 | paramDesc:Hide() 720 | paramName.DoClick = function(p) 721 | if paramDesc.animating then return end 722 | paramDesc.animating = true 723 | local expanded = paramDesc:IsVisible() 724 | paramDesc.hAuto = false 725 | root:Anim(0.25, FLEX_INTERP_SMOOTH, expanded and function(progress) 726 | paramDesc.h = (1 - progress) * paramDesc.ch 727 | end or function(progress) 728 | if not paramDesc:IsVisible() then 729 | paramDesc:Show() 730 | paramDesc:InvalidateLayout(true) 731 | end 732 | paramDesc.h = progress * paramDesc.ch 733 | end, function() 734 | paramDesc.animating = false 735 | paramDesc.hAuto = true 736 | if expanded then paramDesc:Hide() end 737 | end) 738 | end 739 | end 740 | 741 | function INSPECTOR:Init() 742 | self:SetTitle('Flex inspector') 743 | self:SetWide(300) 744 | self:SetTall(ScrW() / 2) 745 | self:AlignRight(8) 746 | self:CenterVertical() 747 | self:MakePopup() 748 | self:SetSizable(true) 749 | local root = self:Add('Flex') 750 | root:Dock(FILL) 751 | root.wrap = FLEX_WRAP_BEFORE_SHRINK 752 | for groupName, props in pairs(propGroups) do 753 | local group = root:Add('Flex') 754 | group:DockMargin(8, 8, 8, 8) 755 | group:DockPadding(8, 8, 8, 8) 756 | group.wrap = FLEX_WRAP_BEFORE_SHRINK 757 | group.grow = 1 758 | group.shrink = 1 759 | group.w = 200 760 | group.wMin = 200 761 | group.hAuto = true 762 | group.bgColor = Color(0, 0, 0, 64) 763 | for prop, propData in pairs(props) do 764 | local param = group:Add('Flex') 765 | param:DockMargin(4, 4, 4, 4) 766 | param:DockPadding(4, 4, 4, 4) 767 | param.bgColor = Color(0, 0, 0, 64) 768 | param.w = 200 769 | param.hAuto = true 770 | param.grow = 1 771 | param.shrink = 1 772 | param.wrap = FLEX_WRAP_BEFORE_SHRINK 773 | param.crossFlow = FLEX_FLOW_STRETCH 774 | local paramNameContainer = param:Add('Flex') 775 | paramNameContainer:DockPadding(4, 0, 0, 0) 776 | paramNameContainer.h = 16 777 | paramNameContainer.grow = 1 778 | paramNameContainer.shrink = 1 779 | local paramName = paramNameContainer:Add('DLabel') 780 | paramName:Dock(FILL) 781 | if isstring(propData) then 782 | param.w = 10000 783 | paramName:SetContentAlignment(5) 784 | paramName:SetText(propData) 785 | else 786 | paramName:SetText(propData.label) 787 | paramName:SetContentAlignment(4) 788 | paramName:SetMouseInputEnabled(true) 789 | local controlContainer = param:Add('Flex') 790 | local control = controlContainer:Add(propData.control) 791 | control:SetProp(propData) 792 | control:SetWide(propData.control.controlWidth or 100) 793 | controlContainer.w, controlContainer.h = control:GetSize() 794 | if propData.desc then 795 | addPropDesc(root, param, paramName, propData.desc) 796 | end 797 | end 798 | paramName:SizeToContents() 799 | paramNameContainer.wMin = paramName:GetWide() 800 | end 801 | end 802 | end 803 | 804 | local lastInspectorToggle = 0 805 | 806 | hook.Add('CreateMove', 'flex.inspect', function() 807 | if input.WasKeyPressed(KEY_C) and 808 | input.IsKeyDown(KEY_LCONTROL) and 809 | input.IsKeyDown(KEY_LSHIFT) and 810 | CurTime() - lastInspectorToggle > 0.1 811 | then 812 | lastInspectorToggle = CurTime() 813 | if IsValid(inspector) then 814 | inspector:Remove() 815 | else 816 | inspector = vgui.CreateFromTable(INSPECTOR) 817 | end 818 | end 819 | end) 820 | 821 | local function drawHollowRect(x1, y1, w1, h1, x2, y2, w2, h2) 822 | surface.DrawRect(x1, y1, x2 - x1, h1) 823 | surface.DrawRect(x2 + w2, y1, w1 - w2 - (x2 - x1), h1) 824 | surface.DrawRect(x2, y1, w2, y2 - y1) 825 | surface.DrawRect(x2, y2 + h2, w2, h1 - h2 - (y2 - y1)) 826 | end 827 | 828 | local marginColor = Color(255, 255, 100, 64) 829 | local paddingColor = Color(100, 255, 100, 64) 830 | local contentColor = Color(200, 200, 255, 32) 831 | local colorGrow = Color(0, 255, 0, 255) 832 | local colorShrink = Color(255, 0, 0, 255) 833 | local colorDbgText = Color(255, 255, 255, 255) 834 | local colorDbgTextBg = Color(0, 0, 0, 255) 835 | 836 | local function drawFlexBounds(flex, verbose) 837 | local x, y = vgui.GetWorldPanel():GetChildPosition(flex) 838 | local w, h = flex:GetSize() 839 | local marX, marY = x - flex.mainMB, y - flex.crossMB 840 | local marW, marH = w + flex.mainMB + flex.mainMF, h + flex.crossMB + flex.crossMF 841 | local conX, conY = x + flex.mainPB, y + flex.crossPB 842 | local conW, conH = w - flex.mainPB - flex.mainPF, h - flex.crossPB - flex.crossPF 843 | surface.SetDrawColor(marginColor) 844 | drawHollowRect(marX, marY, marW, marH, x, y, w, h) 845 | surface.SetDrawColor(paddingColor) 846 | drawHollowRect(x, y, w, h, conX, conY, conW, conH) 847 | surface.SetDrawColor(contentColor) 848 | surface.DrawRect(conX, conY, conW, conH) 849 | if verbose then 850 | draw.SimpleTextOutlined( 851 | w, 852 | 'Default', 853 | x + w / 2, 854 | y - 8, 855 | w > flex.w and colorGrow or w < flex.w and colorShrink or colorDbgText, 856 | TEXT_ALIGN_CENTER, 857 | TEXT_ALIGN_BOTTOM, 858 | 1, 859 | colorDbgTextBg 860 | ) 861 | draw.SimpleTextOutlined( 862 | h, 863 | 'Default', 864 | x - 8, 865 | y + h / 2, 866 | h > flex.h and colorGrow or h < flex.h and colorShrink or colorDbgText, 867 | TEXT_ALIGN_RIGHT, 868 | TEXT_ALIGN_CENTER, 869 | 1, 870 | colorDbgTextBg 871 | ) 872 | local cx, cy = flex:GetPos() 873 | draw.SimpleTextOutlined( 874 | cx .. ', ' .. cy, 875 | 'Default', 876 | x - 8, 877 | y - 8, 878 | colorDbgText, 879 | TEXT_ALIGN_RIGHT, 880 | TEXT_ALIGN_BOTTOM, 881 | 1, 882 | colorDbgTextBg 883 | ) 884 | end 885 | end 886 | 887 | local function getPanelFlex(pnl) 888 | while pnl and not pnl.flex do 889 | pnl = pnl:GetParent() 890 | end 891 | return pnl 892 | end 893 | 894 | hook.Add('PostRenderVGUI', 'flex.inspect', function() 895 | if not IsValid(inspector) then return end 896 | if IsValid(inspector.target) then 897 | drawFlexBounds(inspector.target, true) 898 | end 899 | if input.IsKeyDown(KEY_LSHIFT) then 900 | local flex = getPanelFlex(vgui.GetHoveredPanel()) 901 | if flex and flex ~= inspector.target then 902 | drawFlexBounds(flex) 903 | end 904 | end 905 | end) 906 | 907 | hook.Add('VGUIMousePressed', 'flex.inspect', function(pnl, mcode) 908 | if not IsValid(inspector) or not input.IsKeyDown(KEY_LSHIFT) then return end 909 | if mcode == MOUSE_LEFT then 910 | local flex = getPanelFlex(pnl) 911 | if flex then 912 | inspector.target = flex 913 | end 914 | else 915 | inspector.target = nil 916 | end 917 | end) 918 | --------------------------------------------------------------------------------