From 85058f39c68f56cd16a0ce371b8641a06e69ea24 Mon Sep 17 00:00:00 2001 From: Can Uysal Date: Sun, 23 Aug 2026 21:53:00 +0300 Subject: [PATCH 1/3] Restyle the base controls with a shared visual language Buttons, drop downs, edits, check boxes, sliders, scroll bars, lists, sections and popups each hardcoded their own draw colours, repeating the same 1px light grey outline over a black fill throughout. Add Modules/UI, which holds the colour tokens they now share along with rounded rectangle primitives. SimpleGraphic can only draw axis aligned quads, so rounding uses Assets/ui_round.png, an antialiased disc whose four quadrants serve as corners, and the check mark comes from a sprite of its own for the same reason. Anything holding text stays near black and leans on its border for definition, since the game's text palette is mixed for contrast against black; the lift is spent on the hover and pressed states instead. Replace the scroll bar's stepper buttons with a full length track and a pill knob that fattens while the bar is in use, and hide the bar entirely when there is nothing to scroll. Lay the side bar stat list out as label and value pinned to opposite edges rather than meeting in the middle, group each run of rows between spacers into a card with hairlines between its rows, and clip a label that would otherwise run underneath its own value. Frame the point display and the build name like disabled buttons, since both are read-only, and close the top bar off with a hairline rather than a four pixel rule. Drop the vertical tick at the centre of the top bar, which marked where its two anchor groups meet and meant nothing to the reader. --- src/Assets/ui_check.png | Bin 0 -> 630 bytes src/Assets/ui_round.png | Bin 0 -> 278 bytes src/Classes/ButtonControl.lua | 35 ++---- src/Classes/CheckBoxControl.lua | 70 ++++-------- src/Classes/DropDownControl.lua | 59 ++++------ src/Classes/EditControl.lua | 29 ++--- src/Classes/ListControl.lua | 89 +++++---------- src/Classes/PopupDialog.lua | 13 +-- src/Classes/ScrollBarControl.lua | 184 ++++++++----------------------- src/Classes/SectionControl.lua | 17 ++- src/Classes/SliderControl.lua | 48 ++++---- src/Classes/TextListControl.lua | 87 +++++++++++++-- src/Modules/Build.lua | 40 ++++--- src/Modules/Main.lua | 10 +- src/Modules/UI.lua | 170 ++++++++++++++++++++++++++++ 15 files changed, 451 insertions(+), 400 deletions(-) create mode 100644 src/Assets/ui_check.png create mode 100644 src/Assets/ui_round.png create mode 100644 src/Modules/UI.lua diff --git a/src/Assets/ui_check.png b/src/Assets/ui_check.png new file mode 100644 index 0000000000000000000000000000000000000000..a04708a7467f80be3c0f0945c068f3c345ceb52f GIT binary patch literal 630 zcmV-+0*U>JP)35E_+J9fdGO305C8%|00;m9AOHlwCr^dv!neZXX9BPhe&YYF6MhgL`3@lG z_pJvL`U)T&p7fHHJ_2|Ve&y@z<{N-$;+tPG<_m!Sdv3VAc>|yV1m8_1LKlPo;4nM* zkOQ{B;IDH{kxRj|mE~^g)n~3ZLa{`-8(ZdjD;xsK-Jcac%mNKC_{xWAumlF*%=P{* z0VsgMpYBb6t^mk@8NW}IiR*$08wM9hxqEe85$em`1^DXcn37mBJed%=+_k{qcUYQW zpNP(**X%gV;kp4Fb@uo=0MrY~hG`7`6T95W0w`8yTpbYJ70O+#djGjUtW880#UHUz zD0f=kz&V}G#FJpa0jC?lp)E!j;77HR-(svV;JjgPh9cyI)c?;n@E&_pt1;*VRrlKH)o}-Cqxgm5l%bh4Lo7zOog7%1CRx++nK1P*G#``pR|y(7B;8o$;~v0y1-h)cVR505W1~ z!Yx_GU|e+n$m!0O)mS!?y>5dNl-Lx!OGjh~00AHX1b_e#00KY&2!L+?1y}_1%P^Td Qt^fc407*qoM6N<$g7tSFBme*a literal 0 HcmV?d00001 diff --git a/src/Assets/ui_round.png b/src/Assets/ui_round.png new file mode 100644 index 0000000000000000000000000000000000000000..c8ae7645b5559cbff7583c09bb90121b4646fd86 GIT binary patch literal 278 zcmV+x0qOpUP)S69V^r;3 z1M#NK*CygW1FrJ1?}fwtGLX0(($+w@c-zmsj<7b6xjpjRfIpaIYT>n~CrzCwbg1M1 zgn_h(pwQE%st<8 literal 0 HcmV?d00001 diff --git a/src/Classes/ButtonControl.lua b/src/Classes/ButtonControl.lua index a37c6fc658..97ee73409b 100644 --- a/src/Classes/ButtonControl.lua +++ b/src/Classes/ButtonControl.lua @@ -40,41 +40,26 @@ function ButtonClass:Draw(viewPort, noTooltip) local enabled = self:IsEnabled() local mOver = self:IsMouseOver() local locked = self:GetProperty("locked") - if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif mOver or locked then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.5, 0.5, 0.5) - end - DrawImage(nil, x, y, width, height) - if not enabled then - SetDrawColor(0, 0, 0) - elseif self.clicked and mOver then - SetDrawColor(0.5, 0.5, 0.5) - elseif mOver or locked then - SetDrawColor(0.33, 0.33, 0.33) - else - SetDrawColor(0, 0, 0) + local colors = ui.colors + local radius = ui.FitRadius(ui.radius, width, height) + local border, fill = ui.SurfaceColors(enabled, mOver or locked, self.clicked and mOver) + if locked and enabled then + border = colors.borderActive end - DrawImage(nil, x + 1, y + 1, width - 2, height - 2) + ui.DrawBox(x, y, width, height, radius, border, fill) if self.image then if enabled then SetDrawColor(1, 1, 1) else - SetDrawColor(0.33, 0.33, 0.33) + SetDrawColor(0.4, 0.4, 0.4) end DrawImage(self.image, x + 2, y + 2, width - 4, height - 4) if self.clicked and mOver then - SetDrawColor(1, 1, 1, 0.5) - DrawImage(nil, x + 1, y + 1, width - 2, height - 2) + SetDrawColor(1, 1, 1, 0.25) + ui.DrawRect(x + 1, y + 1, width - 2, height - 2, radius - 1) end end - if enabled then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.33, 0.33, 0.33) - end + ui.SetColor(enabled and colors.text or colors.textDisabled) local label = self:GetProperty("label") if label == "+" then DrawImage(nil, x + width * 0.2, y + height * 0.45, width * 0.6, height * 0.1) diff --git a/src/Classes/CheckBoxControl.lua b/src/Classes/CheckBoxControl.lua index 1648399425..84f0e3b6cf 100644 --- a/src/Classes/CheckBoxControl.lua +++ b/src/Classes/CheckBoxControl.lua @@ -36,59 +36,37 @@ function CheckBoxClass:Draw(viewPort, noTooltip) local size = self.width local enabled = self:IsEnabled() local mOver = self:IsMouseOver() - if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif mOver then - SetDrawColor(1, 1, 1) - elseif self.borderFunc then + local colors = ui.colors + local radius = ui.FitRadius(ui.radiusSmall, size, size) + local border, fill = ui.SurfaceColors(enabled, mOver, self.clicked and mOver) + if self.borderFunc and enabled and not mOver then local r, g, b = self.borderFunc() - SetDrawColor(r, g, b) - elseif self.checkImage and self.state then - SetDrawColor(0.75, 0.75, 0.75) - else - SetDrawColor(0.5, 0.5, 0.5) + border = { r, g, b } end - DrawImage(nil, x, y, size, size) - if not enabled then - SetDrawColor(0, 0, 0) - elseif self.clicked and mOver then - SetDrawColor(0.5, 0.5, 0.5) - elseif mOver then - SetDrawColor(0.33, 0.33, 0.33) - else - SetDrawColor(0, 0, 0) + -- A ticked box is filled with the emphasis colour and carries a dark check mark, like the + -- unticked box inverted. Boxes carrying their own image keep a dark body, since the image is + -- drawn light. + if self.state and enabled and not self.checkImage then + border = mOver and colors.primaryHover or colors.primary + fill = border end - DrawImage(nil, x + 1, y + 1, size - 2, size - 2) + ui.DrawBox(x, y, size, size, radius, border, fill) if self.checkImage then - if self.state then - if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif mOver then - SetDrawColor(2, 2, 2) - else - SetDrawColor(1, 1, 1) - end + if not enabled then + ui.SetColor(colors.textDisabled) + elseif not self.state then + ui.SetColor(colors.textMuted) + elseif mOver then + SetDrawColor(2, 2, 2) else - SetDrawColor(0.5, 0.5, 0.5) - end - DrawImage(self.checkImage.handle, x + 1, y + 1, size - 2, size - 2, self.checkImage[1]) - else - if self.state then - if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif mOver then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.75, 0.75, 0.75) - end - main:DrawCheckMark(x + size/2, y + size/2, size * 0.8) + ui.SetColor(colors.text) end + DrawImage(self.checkImage.handle, x + 2, y + 2, size - 4, size - 4, self.checkImage[1]) + elseif self.state then + ui.SetColor(enabled and colors.onPrimary or colors.textDisabled) + main:DrawCheckMark(x + size/2, y + size/2, size * 0.7) end - if enabled then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.33, 0.33, 0.33) - end + ui.SetColor(enabled and colors.text or colors.textDisabled) local label = self:GetProperty("label") if label then DrawString(x - 5, y + 2, "RIGHT_X", size - 4, "VAR", label) diff --git a/src/Classes/DropDownControl.lua b/src/Classes/DropDownControl.lua index cc7d47161f..3088f1bb81 100644 --- a/src/Classes/DropDownControl.lua +++ b/src/Classes/DropDownControl.lua @@ -243,47 +243,30 @@ function DropDownClass:Draw(viewPort, noTooltip) local dropExtra = self.dropHeight + 4 scrollBar:SetContentDimension(lineHeight * self:GetDropCount(), self.dropHeight) local dropY = self.dropUp and y - dropExtra or y + height - if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif mOver or self.dropped then - SetDrawColor(1, 1, 1) - elseif self.borderFunc then + local colors = ui.colors + local radius = ui.FitRadius(ui.radius, width, height) + local border, fill = ui.SurfaceColors(enabled, mOver, self.dropped) + if self.borderFunc and enabled and not (mOver or self.dropped) then local r, g, b = self.borderFunc() - SetDrawColor(r, g, b) - else - SetDrawColor(0.5, 0.5, 0.5) + border = { r, g, b } end - DrawImage(nil, x, y, width, height) + ui.DrawBox(x, y, width, height, radius, border, fill) if self.dropped then SetDrawLayer(nil, 5) - DrawImage(nil, x, dropY, self.droppedWidth, dropExtra) + ui.DrawBox(x, dropY, self.droppedWidth, dropExtra, ui.radiusLarge, colors.border, colors.popover) SetDrawLayer(nil, 0) end - if not enabled or self.dropped then - SetDrawColor(0, 0, 0) - elseif mOver then - SetDrawColor(0.33, 0.33, 0.33) - else - SetDrawColor(0, 0, 0) - end - DrawImage(nil, x + 1, y + 1, width - 2, height - 2) if not enabled then - SetDrawColor(0.33, 0.33, 0.33) + ui.SetColor(colors.textDisabled) elseif mOver or self.dropped then - SetDrawColor(1, 1, 1) + ui.SetColor(colors.text) else - SetDrawColor(0.5, 0.5, 0.5) - end - main:DrawArrow(x + width - height/2, y + height/2, height/2, height/2, "DOWN") - if self.dropped then - SetDrawLayer(nil, 5) - SetDrawColor(0, 0, 0) - DrawImage(nil, x + 1, dropY + 1, self.droppedWidth - 2, dropExtra - 2) - SetDrawLayer(nil, 0) + ui.SetColor(colors.textMuted) end + main:DrawArrow(x + width - height/2, y + height/2, height/2 - 2, height/2 - 4, "DOWN") if self.otherDragSource then - SetDrawColor(0, 1, 0, 0.25) - DrawImage(nil, x, y, width, height) + ui.SetColor(colors.dropTarget, 0.25) + ui.DrawRect(x, y, width, height, radius) end -- draw dropdown bar @@ -297,9 +280,9 @@ function DropDownClass:Draw(viewPort, noTooltip) mOver and "BODY" or "OUT", self.selIndex, self.list[self.selIndex]) SetDrawLayer(nil, 0) end - SetDrawColor(1, 1, 1) + ui.SetColor(colors.text) else - SetDrawColor(0.66, 0.66, 0.66) + ui.SetColor(colors.textDisabled) end -- draw selected label or search term local selLabel = nil @@ -319,6 +302,7 @@ function DropDownClass:Draw(viewPort, noTooltip) DrawString(0, 0, "LEFT", lineHeight, "VAR", selLabel or "") if selDetail ~= nil then local dx = DrawStringWidth(lineHeight, "VAR", selDetail) + ui.SetColor(enabled and colors.textMuted or colors.textDisabled) DrawString(width - dx - 22, 0, "LEFT", lineHeight, "VAR", selDetail) end SetViewport() @@ -357,14 +341,17 @@ function DropDownClass:Draw(viewPort, noTooltip) local y = (dropIndex - 1) * lineHeight - scrollBar.offset -- highlight background if hovered if index == self.hoverSel then - SetDrawColor(0.33, 0.33, 0.33) - DrawImage(nil, 0, y, width - 4, lineHeight) + ui.SetColor(colors.accent) + ui.DrawRect(0, y, width - 4, lineHeight, ui.radiusSmall) + elseif index == self.selIndex then + ui.SetColor(colors.accentSubtle) + ui.DrawRect(0, y, width - 4, lineHeight, ui.radiusSmall) end -- highlight font color if hovered or selected if index == self.hoverSel or index == self.selIndex then - SetDrawColor(1, 1, 1) + ui.SetColor(colors.text) else - SetDrawColor(0.66, 0.66, 0.66) + ui.SetColor(colors.textMuted) end -- draw actual item label with search match highlight if available local label = nil diff --git a/src/Classes/EditControl.lua b/src/Classes/EditControl.lua index dd81738884..fc943eb8f7 100644 --- a/src/Classes/EditControl.lua +++ b/src/Classes/EditControl.lua @@ -243,29 +243,24 @@ function EditClass:Draw(viewPort, noTooltip) local width, height = self:GetSize() local enabled = self:IsEnabled() local mOver = self:IsMouseOver() + local colors = ui.colors + local radius = ui.FitRadius(ui.radius, width, height) + local border = colors.border + local fill = colors.input if not enabled then - SetDrawColor(0.33, 0.33, 0.33) + border, fill = colors.borderDisabled, colors.surfaceDisabled + elseif self.hasFocus then + border, fill = colors.borderActive, colors.inputHover elseif mOver then - SetDrawColor(1, 1, 1) + border, fill = colors.borderHover, colors.inputHover elseif self.borderFunc then local r, g, b = self.borderFunc() - SetDrawColor(r, g, b) - else - SetDrawColor(0.5, 0.5, 0.5) + border = { r, g, b } end - DrawImage(nil, x, y, width, height) - if not enabled then - SetDrawColor(0, 0, 0) - elseif self.hasFocus or mOver then - if self.lineHeight then - SetDrawColor(0.1, 0.1, 0.1) - else - SetDrawColor(0.15, 0.15, 0.15) - end - else - SetDrawColor(0, 0, 0) + if self.hasFocus and enabled then + ui.DrawFocusRing(x, y, width, height, radius) end - DrawImage(nil, x + 1, y + 1, width - 2, height - 2) + ui.DrawBox(x, y, width, height, radius, border, fill) local textX = x + 2 local textY = y + 2 local textHeight = self.lineHeight or (height - 4) diff --git a/src/Classes/ListControl.lua b/src/Classes/ListControl.lua index 34dd0e7f41..f6346950a7 100644 --- a/src/Classes/ListControl.lua +++ b/src/Classes/ListControl.lua @@ -179,20 +179,14 @@ function ListClass:Draw(viewPort, noTooltip) if label then DrawString(x + self.labelPositionOffset[1], y - 20 + self.labelPositionOffset[2], "LEFT", 16, self.font, label) end + local colors = ui.colors + local border, fill = colors.border, colors.input if self.otherDragSource and not self.CanDragToValue then - SetDrawColor(0.2, 0.6, 0.2) + border, fill = colors.dropTarget, colors.dropTargetSurface elseif self.hasFocus then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.5, 0.5, 0.5) - end - DrawImage(nil, x, y, width, height) - if self.otherDragSource and not self.CanDragToValue then - SetDrawColor(0, 0.05, 0) - else - SetDrawColor(0, 0, 0) + border = colors.borderActive end - DrawImage(nil, x + 1, y + 1, width - 2, height - 2) + ui.DrawBox(x, y, width, height, ui.radius, border, fill) self:DrawControls(viewPort, (noTooltip and not self.forceTooltip) and self) SetViewport(x + 2, y + 2, self.scroll and width - 20 or width, height - 4 - (self.scroll and self.scrollH and 16 or 0)) @@ -231,40 +225,22 @@ function ListClass:Draw(viewPort, noTooltip) ttWidth = m_max(textWidth + 8, relX - colOffset) end end - if self.showRowSeparators then - if self.hasFocus and value == self.selValue then - SetDrawColor(1, 1, 1) - elseif value == ttValue then - SetDrawColor(0.8, 0.8, 0.8) - else - SetDrawColor(0.5, 0.5, 0.5) - end - DrawImage(nil, colOffset, lineY, not self.scroll and colWidth - 4 or colWidth, rowHeight) - if (value == self.selValue or value == ttValue) then - SetDrawColor(0.33, 0.33, 0.33) - elseif self.otherDragSource and self.CanDragToValue and self:CanDragToValue(index, value, self.otherDragSource) then - SetDrawColor(0, 0.2, 0) - elseif index % 2 == 0 then - SetDrawColor(0.05, 0.05, 0.05) - else - SetDrawColor(0, 0, 0) - end - DrawImage(nil, colOffset, lineY + 1, not self.scroll and colWidth - 4 or colWidth, rowHeight - 2) - elseif value == self.selValue or value == ttValue then - if self.hasFocus and value == self.selValue then - SetDrawColor(1, 1, 1) - elseif value == ttValue then - SetDrawColor(0.8, 0.8, 0.8) - else - SetDrawColor(0.5, 0.5, 0.5) - end - DrawImage(nil, colOffset, lineY, not self.scroll and colWidth - 4 or colWidth, rowHeight) - if self.otherDragSource and self.CanDragToValue and self:CanDragToValue(index, value, self.otherDragSource) then - SetDrawColor(0, 0.2, 0) - else - SetDrawColor(0.15, 0.15, 0.15) - end - DrawImage(nil, colOffset, lineY + 1, not self.scroll and colWidth - 4 or colWidth, rowHeight - 2) + -- Rows are filled rather than outlined; columns are drawn one after another, so any + -- corner rounding here would show up as notches between them + local rowWidth = not self.scroll and colWidth - 4 or colWidth + local rowFill + if value == self.selValue then + rowFill = self.hasFocus and colors.accent or colors.accentSubtle + elseif value == ttValue then + rowFill = colors.accentSubtle + elseif self.otherDragSource and self.CanDragToValue and self:CanDragToValue(index, value, self.otherDragSource) then + rowFill = colors.dropTargetSurface + elseif self.showRowSeparators and index % 2 == 0 then + rowFill = colors.surface + end + if rowFill then + ui.SetColor(rowFill) + DrawImage(nil, colOffset, lineY + 1, rowWidth, rowHeight - 2) end if not self.SetHighlightColor or not self:SetHighlightColor(index, value) then SetDrawColor(1, 1, 1) @@ -280,33 +256,28 @@ function ListClass:Draw(viewPort, noTooltip) if self.colLabels then local mOver = relX >= colOffset and relX <= colOffset + colWidth and relY >= 0 and relY <= 18 if mOver and self:GetColumnProperty(column, "sortable") then - SetDrawColor(1, 1, 1) - DrawImage(nil, colOffset, 1, colWidth, 18) - SetDrawColor(0.33, 0.33, 0.33) - DrawImage(nil, colOffset + 1, 2, colWidth - 2, 16) + ui.SetColor(colors.accent) else - SetDrawColor(0.5, 0.5, 0.5) - DrawImage(nil, colOffset, 1, colWidth, 18) - SetDrawColor(0.15, 0.15, 0.15) - DrawImage(nil, colOffset + 1, 2, colWidth - 2, 16) + ui.SetColor(colors.surface) end + DrawImage(nil, colOffset, 1, colWidth - 1, 18) + ui.SetColor(colors.border) + DrawImage(nil, colOffset, 19, colWidth - 1, 1) local label = self:GetColumnProperty(column, "label") if label and #label > 0 then - SetDrawColor(1, 1, 1) + ui.SetColor(colors.textMuted) DrawString(colOffset + colWidth/2, 4, "CENTER_X", 12, "VAR", label) end end end if #self.list == 0 and self.defaultText then - SetDrawColor(1, 1, 1) + ui.SetColor(colors.textMuted) DrawString(2, 2, "LEFT", 14, self.font, self.defaultText) end if self.selDragIndex then local lineY = rowHeight * (self.selDragIndex - 1) - scrollOffsetV - SetDrawColor(1, 1, 1) - DrawImage(nil, 0, lineY - 1, width - 20, 3) - SetDrawColor(0, 0, 0) - DrawImage(nil, 0, lineY, width - 20, 1) + ui.SetColor(colors.primary) + ui.DrawRect(0, lineY - 1, width - 20, 2, 1) end SetViewport() diff --git a/src/Classes/PopupDialog.lua b/src/Classes/PopupDialog.lua index 223059def9..f0225d25db 100644 --- a/src/Classes/PopupDialog.lua +++ b/src/Classes/PopupDialog.lua @@ -40,20 +40,15 @@ end) function PopupDialogClass:Draw(viewPort) local x, y = self:GetPos() local width, height = self:GetSize() + local colors = ui.colors -- Draw dialog background - SetDrawColor(0.8, 0.8, 0.8) - DrawImage(nil, x, y, width, height) - SetDrawColor(0.1, 0.1, 0.1) - DrawImage(nil, x + 2, y + 2, width - 4, height - 4) + ui.DrawBox(x, y, width, height, ui.radiusLarge, colors.borderHover, colors.popover) -- Draw dialog title box local title = self:GetProperty("title") local titleWidth = DrawStringWidth(16, "VAR", title) local titleX = x + m_floor((width - titleWidth - 8) / 2) - SetDrawColor(1, 1, 1) - DrawImage(nil, titleX, y - 10, titleWidth + 8, 24) - SetDrawColor(0, 0, 0) - DrawImage(nil, titleX + 2, y - 8, titleWidth + 4, 20) - SetDrawColor(1, 1, 1) + ui.DrawBox(titleX - 4, y - 10, titleWidth + 16, 24, ui.radius, colors.borderHover, colors.popover) + ui.SetColor(colors.text) DrawString(titleX + 4, y - 7, "LEFT", 16, "VAR", title) if self.scrollBarFunc then self.scrollBarFunc() diff --git a/src/Classes/ScrollBarControl.lua b/src/Classes/ScrollBarControl.lua index 7dfc416e9e..37bc9365ca 100644 --- a/src/Classes/ScrollBarControl.lua +++ b/src/Classes/ScrollBarControl.lua @@ -30,14 +30,11 @@ function ScrollBarClass:SetContentDimension(conDim, viewDim) self.offset = 0 else local width, height = self:GetSize() + local length = self.dir == "HORIZONTAL" and width or height self.enabled = true - if self.dir == "HORIZONTAL" then - self.knobDim = m_max(height, (width - height * 2) * viewDim / conDim) - self.knobTravel = (width - height * 2) - self.knobDim - else - self.knobDim = m_max(width, (height - width * 2) * viewDim / conDim) - self.knobTravel = (height - width * 2) - self.knobDim - end + -- The knob runs the whole length of the bar; there are no stepper buttons to make room for + self.knobDim = m_max(m_min(length, 24), length * viewDim / conDim) + self.knobTravel = length - self.knobDim self.offsetMax = conDim - viewDim self.offset = m_min(self.offset, self.offsetMax) end @@ -77,30 +74,14 @@ function ScrollBarClass:IsMouseOver() local mOver = cursorX >= x and cursorY >= y and cursorX < x + width and cursorY < y + height local mOverComp if mOver and self.enabled then - local relDim - local shortDim, longDim - if self.dir == "HORIZONTAL" then - relDim = cursorX - x - shortDim = height - longDim = width - else - relDim = cursorY - y - shortDim = width - longDim = height - end - if relDim < shortDim then - mOverComp = "UP" - elseif relDim >= longDim - shortDim then - mOverComp = "DOWN" + local relDim = self.dir == "HORIZONTAL" and cursorX - x or cursorY - y + local knobPos = self:GetKnobPosForOffset() + if relDim < knobPos then + mOverComp = "SLIDEUP" + elseif relDim >= knobPos + self.knobDim then + mOverComp = "SLIDEDOWN" else - local knobPos = self:GetKnobPosForOffset() - if relDim < shortDim + knobPos then - mOverComp = "SLIDEUP" - elseif relDim >= shortDim + knobPos + self.knobDim then - mOverComp = "SLIDEDOWN" - else - mOverComp = "KNOB" - end + mOverComp = "KNOB" end end return mOver, mOverComp @@ -137,9 +118,9 @@ function ScrollBarClass:Draw() self.holdPauseTime = nil end local time = now - self.holdTime - if self.holdComp == "UP" then + if self.holdComp == "SLIDEUP" then self:SetOffset(self.holdBase - m_ceil(time / 50) * self.step) - elseif self.holdComp == "DOWN" then + elseif self.holdComp == "SLIDEDOWN" then self:SetOffset(self.holdBase + m_ceil(time / 50) * self.step) end end @@ -147,108 +128,39 @@ function ScrollBarClass:Draw() self.holdPauseTime = GetTime() end end - -- Draw up/left button background - if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif mOverComp == "UP" then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.5, 0.5, 0.5) - end - if dir == "HORIZONTAL" then - DrawImage(nil, x, y, height, height) - else - DrawImage(nil, x, y, width, width) - end - if enabled and mOverComp == "UP" then - SetDrawColor(0.33, 0.33, 0.33) - else - SetDrawColor(0, 0, 0) - end - if dir == "HORIZONTAL" then - DrawImage(nil, x + 1, y + 1, height - 2, height - 2) - else - DrawImage(nil, x + 1, y + 1, width - 2, width - 2) - end - -- Draw up/left arrow + -- Nothing to scroll, so nothing to show if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif mOverComp == "UP" then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.5, 0.5, 0.5) - end - if dir == "HORIZONTAL" then - main:DrawArrow(x + height/2, y + height/2, height/2, height/2, "LEFT") - else - main:DrawArrow(x + width/2, y + width/2, width/2, width/2, "UP") - end - -- Draw down/right button background - if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif mOverComp == "DOWN" then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.5, 0.5, 0.5) - end - if dir == "HORIZONTAL" then - DrawImage(nil, x + width - height, y, height, height) - else - DrawImage(nil, x, y + height - width, width, width) - end - if enabled and mOverComp == "DOWN" then - SetDrawColor(0.33, 0.33, 0.33) - else - SetDrawColor(0, 0, 0) - end - if dir == "HORIZONTAL" then - DrawImage(nil, x + width - height + 1, y + 1, height - 2, height - 2) - else - DrawImage(nil, x + 1, y + height - width + 1, width - 2, width - 2) - end - -- Draw down/right arrow - if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif mOverComp == "DOWN" then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.5, 0.5, 0.5) + return end - if dir == "HORIZONTAL" then - main:DrawArrow(x + width - height/2, y + height/2, height/2, height/2, "RIGHT") + local colors = ui.colors + local horizontal = dir == "HORIZONTAL" + -- Thickness of the bar across its short axis; the track is a thin rail down the middle of it, + -- while the full width stays clickable + local shortDim = horizontal and height or width + local active = self.dragging or mOver + local trackDim = m_max(4, shortDim - 8) + local trackOff = m_floor((shortDim - trackDim) / 2) + ui.SetColor(colors.accentSubtle, active and 0.9 or 0.55) + if horizontal then + ui.DrawRect(x, y + trackOff, width, trackDim, trackDim / 2) else - main:DrawArrow(x + width/2, y + height - width/2, width/2, width/2, "DOWN") + ui.DrawRect(x + trackOff, y, trackDim, height, trackDim / 2) end - -- Draw slide background - if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif self.dragging or mOverComp == "KNOB" or mOverComp == "SLIDEUP" or mOverComp == "SLIDEDOWN" then - SetDrawColor(1, 1, 1) + -- Knob, which fattens up while the bar is being used + if self.dragging or mOverComp == "KNOB" then + ui.SetColor(colors.borderActive) + elseif mOver then + ui.SetColor(colors.borderHover) else - SetDrawColor(0.5, 0.5, 0.5) + ui.SetColor(colors.border) end - if dir == "HORIZONTAL" then - DrawImage(nil, x + height, y, width - height * 2, height) - SetDrawColor(0, 0, 0) - DrawImage(nil, x + height, y + 1, width - height * 2, height - 2) + local knobDim = active and m_max(6, shortDim - 3) or trackDim + local knobOff = m_floor((shortDim - knobDim) / 2) + local knobPos = m_floor(self:GetKnobPosForOffset()) + if horizontal then + ui.DrawRect(x + knobPos, y + knobOff, self.knobDim, knobDim, knobDim / 2) else - DrawImage(nil, x, y + width, width, height - width * 2) - SetDrawColor(0, 0, 0) - DrawImage(nil, x + 1, y + width, width - 2, height - width * 2) - end - -- Draw knob - if enabled then - if self.dragging or mOverComp == "KNOB" then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.5, 0.5, 0.5) - end - local knobPos = self:GetKnobPosForOffset() - if dir == "HORIZONTAL" then - DrawImage(nil, x + height + knobPos + 1, y + 2, self.knobDim - 2, height - 4) - else - DrawImage(nil, x + 2, y + width + knobPos + 1, width - 4, self.knobDim - 2) - end + ui.DrawRect(x + knobOff, y + knobPos, knobDim, self.knobDim, knobDim / 2) end end @@ -269,20 +181,14 @@ function ScrollBarClass:OnKeyDown(key) self.dragCY = cursorY self.dragKnobPos = self:GetKnobPosForOffset() end - elseif mOverComp == "UP" then - self:Scroll(-1) - self.holdComp = "UP" - self.holdTime = GetTime() - self.holdBase = self.offset - elseif mOverComp == "DOWN" then - self:Scroll(1) - self.holdComp = "DOWN" + elseif mOverComp == "SLIDEUP" or mOverComp == "SLIDEDOWN" then + -- Clicking the track pages towards the cursor, and holding keeps scrolling that way + -- until the knob catches up + local step = mOverComp == "SLIDEUP" and -self.knobDim or self.knobDim + self:SetOffsetFromKnobPos(self:GetKnobPosForOffset() + step) + self.holdComp = mOverComp self.holdTime = GetTime() self.holdBase = self.offset - elseif mOverComp == "SLIDEUP" then - self:SetOffsetFromKnobPos(self:GetKnobPosForOffset() - self.knobDim) - elseif mOverComp == "SLIDEDOWN" then - self:SetOffsetFromKnobPos(self:GetKnobPosForOffset() + self.knobDim) end end return self diff --git a/src/Classes/SectionControl.lua b/src/Classes/SectionControl.lua index 45e1498d2e..0c338bb163 100644 --- a/src/Classes/SectionControl.lua +++ b/src/Classes/SectionControl.lua @@ -12,18 +12,15 @@ end) function SectionClass:Draw() local x, y = self:GetPos() local width, height = self:GetSize() + local colors = ui.colors SetDrawLayer(nil, -10) - SetDrawColor(0.66, 0.66, 0.66) - DrawImage(nil, x, y, width, height) - SetDrawColor(0.1, 0.1, 0.1) - DrawImage(nil, x + 2, y + 2, width - 4, height - 4) + ui.DrawBox(x, y, width, height, ui.radiusLarge, colors.border, colors.panel) SetDrawLayer(nil, 0) + -- The label straddles the top edge, so it needs to punch a hole in the border it sits on local label = self:GetProperty("label") local labelWidth = DrawStringWidth(14, "VAR", label) - SetDrawColor(0.66, 0.66, 0.66) - DrawImage(nil, x + 6, y - 8, labelWidth + 6, 18) - SetDrawColor(0, 0, 0) - DrawImage(nil, x + 7, y - 7, labelWidth + 4, 16) - SetDrawColor(1, 1, 1) - DrawString(x + 9, y - 6, "LEFT", 14, "VAR", label) + ui.SetColor(colors.panel) + ui.DrawRect(x + 8, y - 8, labelWidth + 10, 18, ui.radiusSmall) + ui.SetColor(colors.text) + DrawString(x + 13, y - 6, "LEFT", 14, "VAR", label) end \ No newline at end of file diff --git a/src/Classes/SliderControl.lua b/src/Classes/SliderControl.lua index 2c3048de48..35664431a2 100644 --- a/src/Classes/SliderControl.lua +++ b/src/Classes/SliderControl.lua @@ -84,36 +84,34 @@ function SliderClass:Draw(viewPort) self:SetValFromKnobX((cursorX - self.dragCX) + self.dragKnobX) end local mOver, mOverComp = self:IsMouseOver() - if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif self.dragging or mOver then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.5, 0.5, 0.5) - end - DrawImage(nil, x, y, width, height) - SetDrawColor(0, 0, 0) - DrawImage(nil, x + 1, y + 1, width - 2, height - 2) + local colors = ui.colors + local knobX = self:GetKnobXForVal() + -- Thin track running the length of the control, with the travelled part filled in + local trackHeight = m_max(4, m_ceil(height / 4)) + local trackY = y + m_ceil((height - trackHeight) / 2) + local trackRadius = trackHeight / 2 + ui.SetColor(enabled and colors.accent or colors.surfaceDisabled) + ui.DrawRect(x, trackY, width, trackHeight, trackRadius) if enabled then - if self.divCount then - SetDrawColor(0.33, 0.33, 0.33) - for d = 0, knobTravel + 0.5, knobTravel / self.divCount do - DrawImage(nil, x + self.knobSize/2 + d, y + 1, 2, height - 2) - end - end - if self.dragging or mOverComp == "KNOB" then - SetDrawColor(1, 1, 1) + if self.dragging or mOver then + ui.SetColor(colors.primaryHover) else - SetDrawColor(0.5, 0.5, 0.5) + ui.SetColor(colors.primary) end - local knobX = self:GetKnobXForVal() + ui.DrawRect(x, trackY, m_max(trackHeight, knobX + self.knobSize / 2), trackHeight, trackRadius) if self.divCount then - local arrowHeight = self.knobSize/2 - main:DrawArrow(x + 1 + knobX + self.knobSize/2, y + height/2 - arrowHeight/2, self.knobSize, arrowHeight, "UP") - main:DrawArrow(x + 1 + knobX + self.knobSize/2, y + height/2 + arrowHeight/2, self.knobSize, arrowHeight, "DOWN") - else - DrawImage(nil, x + 2 + knobX, y + 2, self.knobSize - 2, self.knobSize - 2) + ui.SetColor(colors.surface) + for d = 0, knobTravel + 0.5, knobTravel / self.divCount do + DrawImage(nil, x + self.knobSize/2 + d, trackY, 2, trackHeight) + end end + -- Round knob, outlined so it stays readable over the filled part of the track + local knobSize = self.knobSize + local knobY = y + (height - knobSize) / 2 + ui.SetColor((self.dragging or mOverComp == "KNOB") and colors.borderActive or colors.border) + ui.DrawCircle(x + 1 + knobX, knobY, knobSize) + ui.SetColor((self.dragging or mOverComp == "KNOB") and colors.primaryHover or colors.primary) + ui.DrawCircle(x + 2 + knobX, knobY + 1, knobSize - 2) end if enabled and (mOver or self.dragging) then SetDrawLayer(nil, 100) diff --git a/src/Classes/TextListControl.lua b/src/Classes/TextListControl.lua index 7302a153b9..591d96cca0 100644 --- a/src/Classes/TextListControl.lua +++ b/src/Classes/TextListControl.lua @@ -3,6 +3,10 @@ -- Class: Text List -- Simple list control for displaying a block of text -- +-- A line's height doubles as its font size, so grouped lists pad every row to buy the text some +-- breathing room without inflating it +local rowPad = 6 + local TextListClass = newClass("TextListControl", "Control", "ControlHost", function(self, anchor, rect, columns, list, sectionHeights) self.Control(anchor, rect) self.ControlHost() @@ -16,6 +20,59 @@ local TextListClass = newClass("TextListControl", "Control", "ControlHost", func self.sectionHeights = sectionHeights end) +---Height a line occupies, which is its font size plus the padding grouped lists add. Headers take +---double, to hold the rule under them clear of the card that follows. +function TextListClass:GetLineHeight(lineInfo) + if not self.grouped then + return lineInfo.height + end + return lineInfo.height + (lineInfo.header and rowPad * 2 or rowPad) +end + +-- Banding for lists that arrive as one long run of lines broken up by blank spacers, which is +-- hard to read at the length the side bar stat list reaches. Each run of lines between spacers +-- becomes a card, its rows divided by hairlines, and any line flagged as a header gets a rule +-- under it. Opt in with control.grouped, since the changelog and help lists want the plain +-- treatment. +function TextListClass:DrawGroups(contentWidth, offset) + local colors = ui.colors + -- Cards first, so the dividers land on top of them + local lineY = -offset + local groupStart = nil + local function endGroup(endY) + if groupStart and endY > groupStart then + ui.SetColor(colors.surface) + ui.DrawRect(0, groupStart - 2, contentWidth, endY - groupStart + 4, ui.radiusLarge) + end + groupStart = nil + end + for _, lineInfo in ipairs(self.list) do + if lineInfo.header or not lineInfo[1] then + endGroup(lineY) + else + groupStart = groupStart or lineY + end + lineY = lineY + self:GetLineHeight(lineInfo) + end + endGroup(lineY) + -- Rule under each header, hairline between the rows sharing a card + lineY = -offset + for index, lineInfo in ipairs(self.list) do + local lineHeight = self:GetLineHeight(lineInfo) + if lineInfo.header then + ui.SetColor(colors.border) + DrawImage(nil, 0, lineY + lineInfo.height + rowPad, contentWidth, 1) + elseif lineInfo[1] then + local nextLine = self.list[index + 1] + if nextLine and nextLine[1] and not nextLine.header then + ui.SetColor(colors.border, 0.45) + DrawImage(nil, 8, lineY + lineHeight - 1, contentWidth - 16, 1) + end + end + lineY = lineY + lineHeight + end +end + function TextListClass:IsMouseOver() if not self:IsShown() then return @@ -29,22 +86,36 @@ function TextListClass:Draw(viewPort) local scrollBar = self.controls.scrollBar local contentHeight = 0 for _, lineInfo in pairs(self.list) do - contentHeight = contentHeight + lineInfo.height + contentHeight = contentHeight + self:GetLineHeight(lineInfo) end scrollBar:SetContentDimension(contentHeight, height - 4) - SetDrawColor(0.66, 0.66, 0.66) - DrawImage(nil, x, y, width, height) - SetDrawColor(0.05, 0.05, 0.05) - DrawImage(nil, x + 1, y + 1, width - 2, height - 2) + ui.DrawBox(x, y, width, height, ui.radius, ui.colors.border, ui.colors.input) self:DrawControls(viewPort) SetViewport(x + 2, y + 2, width - 20, height - 4) + if self.grouped then + self:DrawGroups(width - 26, scrollBar.offset) + end + local textPad = self.grouped and rowPad / 2 or 0 + local valueCol = self.columns[2] for colIndex, colInfo in pairs(self.columns) do local lineY = -scrollBar.offset for _, lineInfo in ipairs(self.list) do - if lineInfo[colIndex] then - DrawString(lineInfo.x or colInfo.x, lineY, lineInfo.align or colInfo.align, lineInfo.height, lineInfo.font or "VAR", lineInfo[colIndex]) + local text = lineInfo[colIndex] + if text then + local font = lineInfo.font or "VAR" + local drawX = lineInfo.x or colInfo.x + -- Clip a label that would otherwise run underneath its own value + if self.grouped and colIndex == 1 and valueCol and lineInfo[2] and not lineInfo.x then + local space = valueCol.x - drawX - DrawStringWidth(lineInfo.height, font, lineInfo[2]) - 8 + if DrawStringWidth(lineInfo.height, font, text) > space then + local clipWidth = DrawStringWidth(lineInfo.height, font, "..") + local clipIndex = DrawStringCursorIndex(lineInfo.height, font, text, space - clipWidth, 0) + text = text:sub(1, clipIndex - 1) .. ".." + end + end + DrawString(drawX, lineY + textPad, lineInfo.align or colInfo.align, lineInfo.height, font, text) end - lineY = lineY + lineInfo.height + lineY = lineY + self:GetLineHeight(lineInfo) end end SetViewport() diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index e8b9a48140..dc91f6299e 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -141,10 +141,8 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.controls.buildName.Draw = function(control) local x, y = control:GetPos() local width, height = control:GetSize() - SetDrawColor(0.5, 0.5, 0.5) - DrawImage(nil, x + 91, y, self.strWidth + 6, 20) - SetDrawColor(0, 0, 0) - DrawImage(nil, x + 92, y + 1, self.strWidth + 4, 18) + -- Read-only, so it is framed like a disabled button to match the rest of the top bar + ui.DrawBox(x + 91, y, self.strWidth + 6, 20, ui.radius, ui.colors.borderDisabled, ui.colors.surfaceDisabled) SetDrawColor(1, 1, 1) SetViewport(x, y + 2, self.strWidth + 94, 16) DrawString(0, 0, "LEFT", 16, "VAR", "Current build: "..self.buildName) @@ -181,10 +179,8 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.controls.pointDisplay.Draw = function(control) local x, y = control:GetPos() local width, height = control:GetSize() - SetDrawColor(1, 1, 1) - DrawImage(nil, x, y, width, height) - SetDrawColor(0, 0, 0) - DrawImage(nil, x + 1, y + 1, width - 2, height - 2) + -- Read-only, so it is framed like a disabled button to match the rest of the top bar + ui.DrawBox(x, y, width + 2, height, ui.radius, ui.colors.borderDisabled, ui.colors.surfaceDisabled) SetDrawColor(1, 1, 1) DrawString(x + 4, y + 2, "LEFT", 16, "FIXED", control.str) if control:IsMouseInBounds() then @@ -475,7 +471,10 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.buildFlag = true end) self.controls.statBoxAnchor = new("Control", {"TOPLEFT",self.controls.mainSkillMinionSkillStatSet,"BOTTOMLEFT",true}, {0, 2, 0, 0}) - self.controls.statBox = new("TextListControl", {"TOPLEFT",self.controls.statBoxAnchor,"BOTTOMLEFT"}, {0, 2, 300, 0}, {{x=170,align="RIGHT_X"},{x=174,align="LEFT"}}) + -- Label and value are pinned to opposite edges rather than meeting in the middle, so both read + -- as columns and long labels have the whole width to run into + self.controls.statBox = new("TextListControl", {"TOPLEFT",self.controls.statBoxAnchor,"BOTTOMLEFT"}, {0, 2, 300, 0}, {{x=6,align="LEFT"},{x=268,align="RIGHT_X"}}) + self.controls.statBox.grouped = true self.controls.statBox.height = function(control) local x, y = control:GetPos() local warnHeight = main.showWarnings and #self.controls.warnings.lines > 0 and 18 or 0 @@ -1375,18 +1374,17 @@ function buildMode:OnFrame(inputEvents) SetDrawLayer(5) - -- Draw top bar background - SetDrawColor(0.2, 0.2, 0.2) - DrawImage(nil, 0, 0, main.screenW, 28) - SetDrawColor(0.85, 0.85, 0.85) - DrawImage(nil, 0, 28, main.screenW, 4) - DrawImage(nil, main.screenW/2 - 2, 0, 4, 28) + -- Draw top bar background, closed off by a hairline rather than the old 4px rule + ui.SetColor(ui.colors.chrome) + DrawImage(nil, 0, 0, main.screenW, 31) + ui.SetColor(ui.colors.chromeBorder) + DrawImage(nil, 0, 31, main.screenW, 1) -- Draw side bar background - SetDrawColor(0.1, 0.1, 0.1) - DrawImage(nil, 0, 32, sideBarWidth - 4, main.screenH - 32) - SetDrawColor(0.85, 0.85, 0.85) - DrawImage(nil, sideBarWidth - 4, 32, 4, main.screenH - 32) + ui.SetColor(ui.colors.chrome) + DrawImage(nil, 0, 32, sideBarWidth - 1, main.screenH - 32) + ui.SetColor(ui.colors.chromeBorder) + DrawImage(nil, sideBarWidth - 1, 32, 1, main.screenH - 32) self:DrawControls(main.viewPort) @@ -2241,7 +2239,7 @@ function buildMode:RefreshStatList() end end if self.calcsTab.mainEnv.minion then - t_insert(statBoxList, { height = 18, "^7Minion:" }) + t_insert(statBoxList, { height = 20, header = true, "^7Minion" }) if self.calcsTab.mainEnv.minion.mainSkill.infoMessage then -- Split the line if too long if #self.calcsTab.mainEnv.minion.mainSkill.infoMessage > 40 then @@ -2257,7 +2255,7 @@ function buildMode:RefreshStatList() end self:AddDisplayStatList(self.minionDisplayStats, self.calcsTab.mainEnv.minion) t_insert(statBoxList, { height = 10 }) - t_insert(statBoxList, { height = 18, "^7Player:" }) + t_insert(statBoxList, { height = 20, header = true, "^7Player" }) end if self.calcsTab.mainEnv.player.mainSkill.activeEffect.statSet.skillFlags.disable then t_insert(statBoxList, { height = 16, "^7Skill disabled:" }) diff --git a/src/Modules/Main.lua b/src/Modules/Main.lua index 98d4a786c0..08b830dbc0 100644 --- a/src/Modules/Main.lua +++ b/src/Modules/Main.lua @@ -16,6 +16,10 @@ local m_pi = math.pi LoadModule("GameVersions") LoadModule("Modules/Common") + +-- Load as global so the control classes can share one set of colours and drawing primitives +ui = LoadModule("Modules/UI") + LoadModule("Modules/CalcFormat") LoadModule("Modules/Data") LoadModule("Modules/ModTools") @@ -1503,11 +1507,7 @@ function main:DrawArrow(x, y, width, height, dir) end function main:DrawCheckMark(x, y, size) - size = size / 0.8 - x = x - size / 2 - y = y - size / 2 - DrawImageQuad(nil, x + size * 0.15, y + size * 0.50, x + size * 0.30, y + size * 0.45, x + size * 0.50, y + size * 0.80, x + size * 0.40, y + size * 0.90) - DrawImageQuad(nil, x + size * 0.40, y + size * 0.90, x + size * 0.35, y + size * 0.75, x + size * 0.80, y + size * 0.10, x + size * 0.90, y + size * 0.20) + ui.DrawCheckMark(x, y, size / 0.8) end do diff --git a/src/Modules/UI.lua b/src/Modules/UI.lua new file mode 100644 index 0000000000..0a86acbf78 --- /dev/null +++ b/src/Modules/UI.lua @@ -0,0 +1,170 @@ +-- Path of Building +-- +-- Module: UI +-- Shared visual language for the base controls (buttons, drop downs, edits, check boxes, ...). +-- +-- Everything here is purely cosmetic: it provides the colour tokens the controls draw with, +-- plus a handful of rounded rectangle primitives, since SimpleGraphic only knows how to draw +-- axis aligned quads. +-- +local m_min = math.min +local m_max = math.max +local m_floor = math.floor + +local ui = { } + +-- Corner rounding used by the base controls +ui.radiusSmall = 3 +ui.radius = 5 +ui.radiusLarge = 8 + +-- Colour tokens. Each entry is { red, green, blue } in the 0-1 range that SetDrawColor expects. +ui.colors = { + -- Outlines + border = { 0.24, 0.24, 0.27 }, + borderHover = { 0.45, 0.45, 0.50 }, + borderActive = { 0.63, 0.63, 0.69 }, + borderDisabled = { 0.16, 0.16, 0.18 }, + + -- Control bodies. Item names, "(Unused)" notes and other body text use the game's own + -- palette, which is mixed for contrast against black, so anything holding text stays near + -- black and leans on the border for definition. Only the hover and pressed states lift far + -- enough to be felt. + surface = { 0.055, 0.055, 0.065 }, + surfaceHover = { 0.115, 0.115, 0.135 }, + surfaceActive = { 0.17, 0.17, 0.20 }, + surfaceDisabled = { 0.035, 0.035, 0.04 }, + + -- Containers that float above the rest of the interface + popover = { 0.03, 0.03, 0.038 }, + panel = { 0.042, 0.042, 0.05 }, + + -- Window chrome, i.e. the top bar and the side bar the tab buttons sit on. It reads as raised + -- rather than recessed, so it is the one surface that sits above the controls it holds. + chrome = { 0.13, 0.13, 0.15 }, + chromeBorder = { 0.30, 0.30, 0.34 }, + + -- Text entry bodies, which sit a little deeper than a button + input = { 0.022, 0.022, 0.028 }, + inputHover = { 0.045, 0.045, 0.055 }, + + -- Row highlights inside lists and drop downs + accent = { 0.17, 0.17, 0.20 }, + accentSubtle = { 0.095, 0.095, 0.11 }, + + -- Filled emphasis, e.g. a ticked check box or a slider knob + primary = { 0.88, 0.88, 0.91 }, + primaryHover = { 1, 1, 1 }, + onPrimary = { 0.05, 0.05, 0.06 }, + + -- Text + text = { 0.98, 0.98, 0.98 }, + textMuted = { 0.72, 0.72, 0.75 }, + textDisabled = { 0.42, 0.42, 0.45 }, + + -- Focus ring drawn just outside a focused control + ring = { 0.55, 0.55, 0.62 }, + + -- Drag and drop feedback + dropTarget = { 0.35, 0.75, 0.45 }, + dropTargetSurface = { 0.05, 0.09, 0.06 }, +} + +-- A white disc; each quadrant is used as a rounded corner, so a rounded rectangle is +-- four corner draws plus three plain quads. +local roundImage = NewImageHandle() +roundImage:Load("Assets/ui_round.png", "CLAMP", "MIPMAP") + +-- Untextured geometry has no antialiasing, which a diagonal shape like a check mark shows badly, +-- so it comes from a sprite as well +local checkImage = NewImageHandle() +checkImage:Load("Assets/ui_check.png", "CLAMP", "MIPMAP") + +---Set the current draw colour from a token +---@param color number[] +---@param alpha? number +function ui.SetColor(color, alpha) + SetDrawColor(color[1], color[2], color[3], alpha or color[4] or 1) +end + +---Draw a filled rectangle with rounded corners, using the current draw colour +---@param radius? number defaults to ui.radius +function ui.DrawRect(x, y, width, height, radius) + if width <= 0 or height <= 0 then + return + end + radius = m_floor(m_min(radius or ui.radius, width / 2, height / 2)) + if radius < 1 then + DrawImage(nil, x, y, width, height) + return + end + local right = x + width - radius + local bottom = y + height - radius + DrawImage(roundImage, x, y, radius, radius, 0, 0, 0.5, 0.5) + DrawImage(roundImage, right, y, radius, radius, 0.5, 0, 1, 0.5) + DrawImage(roundImage, x, bottom, radius, radius, 0, 0.5, 0.5, 1) + DrawImage(roundImage, right, bottom, radius, radius, 0.5, 0.5, 1, 1) + DrawImage(nil, x + radius, y, width - radius * 2, radius) + DrawImage(nil, x, y + radius, width, height - radius * 2) + DrawImage(nil, x + radius, bottom, width - radius * 2, radius) +end + +---Draw a filled circle of the given diameter, using the current draw colour +function ui.DrawCircle(x, y, diameter) + DrawImage(roundImage, x, y, diameter, diameter) +end + +---Draw a check mark centred on the given point, using the current draw colour +function ui.DrawCheckMark(x, y, size) + DrawImage(checkImage, x - size / 2, y - size / 2, size, size) +end + +---Draw an outlined, filled rounded rectangle +---@param border? number[] outline colour, or nil for no outline +---@param fill? number[] body colour, or nil to leave the body untouched +---@param thickness? number outline thickness, defaults to 1 +function ui.DrawBox(x, y, width, height, radius, border, fill, thickness) + radius = radius or ui.radius + thickness = thickness or 1 + if border then + ui.SetColor(border) + ui.DrawRect(x, y, width, height, radius) + elseif not fill then + return + end + if fill then + ui.SetColor(fill) + local inset = border and thickness or 0 + ui.DrawRect(x + inset, y + inset, width - inset * 2, height - inset * 2, radius - inset) + end +end + +---Draw a focus ring around a control. Must be drawn before the control body, which covers its +---inner half. +function ui.DrawFocusRing(x, y, width, height, radius, color, spread) + spread = spread or 2 + ui.SetColor(color or ui.colors.ring, 0.55) + ui.DrawRect(x - spread, y - spread, width + spread * 2, height + spread * 2, (radius or ui.radius) + spread) +end + +---Pick the outline and body colours for an interactive control +---@return number[] border +---@return number[] fill +function ui.SurfaceColors(enabled, hover, active) + local colors = ui.colors + if not enabled then + return colors.borderDisabled, colors.surfaceDisabled + elseif active then + return colors.borderActive, colors.surfaceActive + elseif hover then + return colors.borderHover, colors.surfaceHover + end + return colors.border, colors.surface +end + +---Clamp a corner radius so it never exceeds half of the smaller side +function ui.FitRadius(radius, width, height) + return m_max(0, m_min(radius, m_floor(m_min(width, height) / 2))) +end + +return ui From 1e27d845b39a41d317eb65339a5a6a08d7d42294 Mon Sep 17 00:00:00 2001 From: Can Uysal Date: Tue, 25 Aug 2026 11:39:01 +0300 Subject: [PATCH 2/3] Loosen the main skill controls and inset their text Drop down and edit text was drawn hard against the border at two pixels in. Inset it, and grow the dropped panel and auto sized box widths by the same amount so long labels do not clip by exactly what was added. The edit control works out its text origin separately in the draw path and in the click to place caret path, so both move together; changing only the former would leave the caret landing about a character off. The main skill section was built from eighteen pixel controls with two pixel gaps, and two sixteen pixel outliers. A drop down draws its text at its height less four, so that meant fourteen pixel text against the sixteen pixel label above it and the sixteen pixel stat rows below. Every control in the section is now twenty pixels with wider gaps, which puts its text at the same size as its surroundings. The stat box takes its height from the screen, so it absorbs the difference. --- src/Classes/DropDownControl.lua | 15 +++++++++------ src/Classes/EditControl.lua | 7 +++++-- src/Modules/Build.lua | 28 ++++++++++++++-------------- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/Classes/DropDownControl.lua b/src/Classes/DropDownControl.lua index 3088f1bb81..c7e2ea8370 100644 --- a/src/Classes/DropDownControl.lua +++ b/src/Classes/DropDownControl.lua @@ -8,6 +8,9 @@ local m_min = math.min local m_max = math.max local m_floor = math.floor +-- Text sits in from the left edge rather than against it, so the label has room to breathe +local textInset = 6 + local DropDownClass = newClass("DropDownControl", "Control", "ControlHost", "TooltipHost", "SearchHost", function(self, anchor, rect, list, selFunc, tooltipText) self.Control(anchor, rect) self.ControlHost() @@ -299,7 +302,7 @@ function DropDownClass:Draw(viewPort, noTooltip) end end SetViewport(x + 2, y + 2, width - height, lineHeight) - DrawString(0, 0, "LEFT", lineHeight, "VAR", selLabel or "") + DrawString(textInset, 0, "LEFT", lineHeight, "VAR", selLabel or "") if selDetail ~= nil then local dx = DrawStringWidth(lineHeight, "VAR", selDetail) ui.SetColor(enabled and colors.textMuted or colors.textDisabled) @@ -362,18 +365,18 @@ function DropDownClass:Draw(viewPort, noTooltip) else label = listVal end - DrawString(0, y, "LEFT", lineHeight, "VAR", label) + DrawString(textInset, y, "LEFT", lineHeight, "VAR", label) if detail ~= nil then local detail = listVal.detail dx = DrawStringWidth(lineHeight, "VAR", detail) DrawString(width - dx - 4 - 22, y, "LEFT", lineHeight, "VAR", detail) end - self:DrawSearchHighlights(label, searchInfo, 0, y, width - 4, lineHeight) + self:DrawSearchHighlights(label, searchInfo, textInset, y, width - 4, lineHeight) end end SetDrawColor(1, 1, 1) if self:IsSearchActive() and self:GetMatchCount() == 0 then - DrawString(0, 0 , "LEFT", lineHeight, "VAR", "") + DrawString(textInset, 0 , "LEFT", lineHeight, "VAR", "") end SetViewport() SetDrawLayer(nil, 0) @@ -506,7 +509,7 @@ function DropDownClass:CheckDroppedWidth(enable) line = line.label or "" end -- +10 to stop clipping - dWidth = m_max(dWidth, DrawStringWidth(lineHeight, "VAR", line or "") + 10) + dWidth = m_max(dWidth, DrawStringWidth(lineHeight, "VAR", line or "") + 10 + textInset) end -- no greater than self.maxDroppedWidth self.droppedWidth = m_min(dWidth + scrollWidth, self.maxDroppedWidth) @@ -517,7 +520,7 @@ function DropDownClass:CheckDroppedWidth(enable) end -- add 20 to account for the 'down arrow' in the box local boxWidth - boxWidth = DrawStringWidth(lineHeight, "VAR", line or "") + 20 + boxWidth = DrawStringWidth(lineHeight, "VAR", line or "") + 20 + textInset self.width = m_max(m_min(boxWidth, 390), 190) end diff --git a/src/Classes/EditControl.lua b/src/Classes/EditControl.lua index fc943eb8f7..859c600534 100644 --- a/src/Classes/EditControl.lua +++ b/src/Classes/EditControl.lua @@ -9,6 +9,9 @@ local m_floor = math.floor local protected_replace = "*" local utf8 = require('lua-utf8') +-- Matches the drop down inset so fields and selects sitting side by side line up +local textInset = 6 + local function lastLine(str) local lastLineIndex = 1 while true do @@ -261,7 +264,7 @@ function EditClass:Draw(viewPort, noTooltip) ui.DrawFocusRing(x, y, width, height, radius) end ui.DrawBox(x, y, width, height, radius, border, fill) - local textX = x + 2 + local textX = x + 2 + textInset local textY = y + 2 local textHeight = self.lineHeight or (height - 4) if self.prompt then @@ -480,7 +483,7 @@ function EditClass:OnKeyDown(key, doubleClick) self.drag = true local x, y = self:GetPos() local width, height = self:GetSize() - local textX = x + 2 + local textX = x + 2 + textInset local textY = y + 2 local textHeight = self.lineHeight or (height - 4) if self.prompt then diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index dc91f6299e..a9c6577f47 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -350,7 +350,7 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.controls.modeCompare.locked = function() return self.viewMode == "COMPARE" end -- Skills self.controls.mainSkillLabel = new("LabelControl", {"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 80, 300, 16}, "^7Main Skill:") - self.controls.mainSocketGroup = new("DropDownControl", {"TOPLEFT",self.controls.mainSkillLabel,"BOTTOMLEFT"}, {0, 2, 300, 18}, nil, function(index, value) + self.controls.mainSocketGroup = new("DropDownControl", {"TOPLEFT",self.controls.mainSkillLabel,"BOTTOMLEFT"}, {0, 4, 300, 20}, nil, function(index, value) self.mainSocketGroup = index self.modFlag = true self.buildFlag = true @@ -362,13 +362,13 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.skillsTab:AddSocketGroupTooltip(tooltip, socketGroup) end end - self.controls.mainSkill = new("DropDownControl", {"TOPLEFT",self.controls.mainSocketGroup,"BOTTOMLEFT"}, {0, 2, 300, 18}, nil, function(index, value) + self.controls.mainSkill = new("DropDownControl", {"TOPLEFT",self.controls.mainSocketGroup,"BOTTOMLEFT"}, {0, 4, 300, 20}, nil, function(index, value) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] mainSocketGroup.mainActiveSkill = index self.modFlag = true self.buildFlag = true end) - self.controls.statSet = new("DropDownControl", {"TOPLEFT",self.controls.mainSkill,"BOTTOMLEFT"}, {0, 2, 300, 18}, nil, function(index, value) + self.controls.statSet = new("DropDownControl", {"TOPLEFT",self.controls.mainSkill,"BOTTOMLEFT"}, {0, 4, 300, 20}, nil, function(index, value) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance srcInstance.statSet = srcInstance.statSet or { } @@ -376,38 +376,38 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.modFlag = true self.buildFlag = true end) - self.controls.mainSkillPart = new("DropDownControl", {"TOPLEFT",self.controls.statSet,"BOTTOMLEFT",true}, {0, 2, 300, 18}, nil, function(index, value) + self.controls.mainSkillPart = new("DropDownControl", {"TOPLEFT",self.controls.statSet,"BOTTOMLEFT",true}, {0, 4, 300, 20}, nil, function(index, value) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance srcInstance.skillPart = index self.modFlag = true self.buildFlag = true end) - self.controls.mainSkillStageCountLabel = new("LabelControl", {"TOPLEFT",self.controls.mainSkillPart,"BOTTOMLEFT",true}, {0, 3, 0, 16}, "^7Stages:") { + self.controls.mainSkillStageCountLabel = new("LabelControl", {"TOPLEFT",self.controls.mainSkillPart,"BOTTOMLEFT",true}, {0, 5, 0, 16}, "^7Stages:") { shown = function() return self.controls.mainSkillStageCount:IsShown() end, } - self.controls.mainSkillStageCount = new("EditControl", {"LEFT",self.controls.mainSkillStageCountLabel,"RIGHT",true}, {2, 0, 60, 18}, nil, nil, "%D", nil, function(buf) + self.controls.mainSkillStageCount = new("EditControl", {"LEFT",self.controls.mainSkillStageCountLabel,"RIGHT",true}, {2, 0, 60, 20}, nil, nil, "%D", nil, function(buf) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance srcInstance.skillStageCount = tonumber(buf) self.modFlag = true self.buildFlag = true end) - self.controls.mainSkillMineCountLabel = new("LabelControl", {"TOPLEFT",self.controls.mainSkillStageCountLabel,"BOTTOMLEFT",true}, {0, 3, 0, 16}, "^7Active Mines:") { + self.controls.mainSkillMineCountLabel = new("LabelControl", {"TOPLEFT",self.controls.mainSkillStageCountLabel,"BOTTOMLEFT",true}, {0, 5, 0, 16}, "^7Active Mines:") { shown = function() return self.controls.mainSkillMineCount:IsShown() end, } - self.controls.mainSkillMineCount = new("EditControl", {"LEFT",self.controls.mainSkillMineCountLabel,"RIGHT",true}, {2, 0, 60, 18}, nil, nil, "%D", nil, function(buf) + self.controls.mainSkillMineCount = new("EditControl", {"LEFT",self.controls.mainSkillMineCountLabel,"RIGHT",true}, {2, 0, 60, 20}, nil, nil, "%D", nil, function(buf) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance srcInstance.skillMineCount = tonumber(buf) self.modFlag = true self.buildFlag = true end) - self.controls.mainSkillMinion = new("DropDownControl", {"TOPLEFT",self.controls.mainSkillMineCountLabel,"BOTTOMLEFT",true}, {0, 3, 178, 18}, nil, function(index, value) + self.controls.mainSkillMinion = new("DropDownControl", {"TOPLEFT",self.controls.mainSkillMineCountLabel,"BOTTOMLEFT",true}, {0, 4, 178, 20}, nil, function(index, value) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance if value.itemSetId then @@ -448,20 +448,20 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin tooltip:AddLine(14, colorCodes.TIP.."Tip: You can drag items from the Items tab onto this dropdown to equip them onto the minion.") end end - self.controls.mainSkillMinionLibrary = new("ButtonControl", {"LEFT",self.controls.mainSkillMinion,"RIGHT"}, {2, 0, 120, 18}, "Manage Spectres...", function() + self.controls.mainSkillMinionLibrary = new("ButtonControl", {"LEFT",self.controls.mainSkillMinion,"RIGHT"}, {2, 0, 120, 20}, "Manage Spectres...", function() self:OpenSpectreLibrary("spectre") end) - self.controls.mainSkillBeastLibrary = new("ButtonControl", {"LEFT",self.controls.mainSkillMinion,"RIGHT"}, {2, 0, 120, 18}, "Manage Beasts...", function() + self.controls.mainSkillBeastLibrary = new("ButtonControl", {"LEFT",self.controls.mainSkillMinion,"RIGHT"}, {2, 0, 120, 20}, "Manage Beasts...", function() self:OpenSpectreLibrary("beast") end) - self.controls.mainSkillMinionSkill = new("DropDownControl", {"TOPLEFT",self.controls.mainSkillMinion,"BOTTOMLEFT",true}, {0, 2, 200, 16}, nil, function(index, value) + self.controls.mainSkillMinionSkill = new("DropDownControl", {"TOPLEFT",self.controls.mainSkillMinion,"BOTTOMLEFT",true}, {0, 4, 200, 20}, nil, function(index, value) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance srcInstance.skillMinionSkill = index self.modFlag = true self.buildFlag = true end) - self.controls.mainSkillMinionSkillStatSet = new("DropDownControl", {"TOPLEFT",self.controls.mainSkillMinionSkill,"BOTTOMLEFT",true}, {0, 2, 200, 16}, nil, function(index, value) + self.controls.mainSkillMinionSkillStatSet = new("DropDownControl", {"TOPLEFT",self.controls.mainSkillMinionSkill,"BOTTOMLEFT",true}, {0, 4, 200, 20}, nil, function(index, value) local mainSocketGroup = self.skillsTab.socketGroupList[self.mainSocketGroup] local srcInstance = mainSocketGroup.displaySkillList[mainSocketGroup.mainActiveSkill].activeEffect.srcInstance srcInstance.skillMinionSkillStatSetIndexLookup = srcInstance.skillMinionSkillStatSetIndexLookup or { } @@ -470,7 +470,7 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.modFlag = true self.buildFlag = true end) - self.controls.statBoxAnchor = new("Control", {"TOPLEFT",self.controls.mainSkillMinionSkillStatSet,"BOTTOMLEFT",true}, {0, 2, 0, 0}) + self.controls.statBoxAnchor = new("Control", {"TOPLEFT",self.controls.mainSkillMinionSkillStatSet,"BOTTOMLEFT",true}, {0, 4, 0, 0}) -- Label and value are pinned to opposite edges rather than meeting in the middle, so both read -- as columns and long labels have the whole width to run into self.controls.statBox = new("TextListControl", {"TOPLEFT",self.controls.statBoxAnchor,"BOTTOMLEFT"}, {0, 2, 300, 0}, {{x=6,align="LEFT"},{x=268,align="RIGHT_X"}}) From 98035276958950280e2bed175a0228c059d9bfd2 Mon Sep 17 00:00:00 2001 From: Can Uysal Date: Tue, 25 Aug 2026 16:24:09 +0300 Subject: [PATCH 3/3] Pack the colour tokens into numbers instead of tables Review raised the risk of building tables for RGB values on the draw path. The token table itself is built once at load, but three controls were creating a fresh {r, g, b} every frame: the check box, drop down and edit each wrapped whatever their borderFunc returned. Those run for every such control on every frame, and the config tab alone has a lot of them, so the collector had a steady trickle of short lived tables to trace. Store each token as a packed 0xRRGGBB number and unpack it in SetColor, and add PackColor for colours that only exist at draw time. Numbers are values in Lua, so nothing on the draw path allocates now, and a token costs one hash lookup rather than a lookup plus three array reads. Colours are unchanged to within half a step out of 255, which is not visible. --- src/Classes/CheckBoxControl.lua | 2 +- src/Classes/DropDownControl.lua | 2 +- src/Classes/EditControl.lua | 2 +- src/Modules/UI.lua | 80 ++++++++++++++++++++------------- 4 files changed, 51 insertions(+), 35 deletions(-) diff --git a/src/Classes/CheckBoxControl.lua b/src/Classes/CheckBoxControl.lua index 84f0e3b6cf..b3ffcc5590 100644 --- a/src/Classes/CheckBoxControl.lua +++ b/src/Classes/CheckBoxControl.lua @@ -41,7 +41,7 @@ function CheckBoxClass:Draw(viewPort, noTooltip) local border, fill = ui.SurfaceColors(enabled, mOver, self.clicked and mOver) if self.borderFunc and enabled and not mOver then local r, g, b = self.borderFunc() - border = { r, g, b } + border = ui.PackColor(r, g, b) end -- A ticked box is filled with the emphasis colour and carries a dark check mark, like the -- unticked box inverted. Boxes carrying their own image keep a dark body, since the image is diff --git a/src/Classes/DropDownControl.lua b/src/Classes/DropDownControl.lua index c7e2ea8370..e76bb30472 100644 --- a/src/Classes/DropDownControl.lua +++ b/src/Classes/DropDownControl.lua @@ -251,7 +251,7 @@ function DropDownClass:Draw(viewPort, noTooltip) local border, fill = ui.SurfaceColors(enabled, mOver, self.dropped) if self.borderFunc and enabled and not (mOver or self.dropped) then local r, g, b = self.borderFunc() - border = { r, g, b } + border = ui.PackColor(r, g, b) end ui.DrawBox(x, y, width, height, radius, border, fill) if self.dropped then diff --git a/src/Classes/EditControl.lua b/src/Classes/EditControl.lua index 859c600534..2ccc3581b2 100644 --- a/src/Classes/EditControl.lua +++ b/src/Classes/EditControl.lua @@ -258,7 +258,7 @@ function EditClass:Draw(viewPort, noTooltip) border, fill = colors.borderHover, colors.inputHover elseif self.borderFunc then local r, g, b = self.borderFunc() - border = { r, g, b } + border = ui.PackColor(r, g, b) end if self.hasFocus and enabled then ui.DrawFocusRing(x, y, width, height, radius) diff --git a/src/Modules/UI.lua b/src/Modules/UI.lua index 0a86acbf78..58d57cfaf2 100644 --- a/src/Modules/UI.lua +++ b/src/Modules/UI.lua @@ -19,55 +19,59 @@ ui.radius = 5 ui.radiusLarge = 8 -- Colour tokens. Each entry is { red, green, blue } in the 0-1 range that SetDrawColor expects. +-- Colour tokens, packed as 0xRRGGBB. They are plain numbers rather than {r, g, b} tables: the draw +-- path runs every frame for every control, so a colour that has to be built there, or a table that +-- has to be traced by the collector, adds up. Numbers are values in Lua, so passing one around or +-- deriving one at draw time allocates nothing. ui.colors = { -- Outlines - border = { 0.24, 0.24, 0.27 }, - borderHover = { 0.45, 0.45, 0.50 }, - borderActive = { 0.63, 0.63, 0.69 }, - borderDisabled = { 0.16, 0.16, 0.18 }, + border = 0x3D3D45, + borderHover = 0x737380, + borderActive = 0xA1A1B0, + borderDisabled = 0x29292E, -- Control bodies. Item names, "(Unused)" notes and other body text use the game's own -- palette, which is mixed for contrast against black, so anything holding text stays near -- black and leans on the border for definition. Only the hover and pressed states lift far -- enough to be felt. - surface = { 0.055, 0.055, 0.065 }, - surfaceHover = { 0.115, 0.115, 0.135 }, - surfaceActive = { 0.17, 0.17, 0.20 }, - surfaceDisabled = { 0.035, 0.035, 0.04 }, + surface = 0x0E0E11, + surfaceHover = 0x1D1D22, + surfaceActive = 0x2B2B33, + surfaceDisabled = 0x09090A, -- Containers that float above the rest of the interface - popover = { 0.03, 0.03, 0.038 }, - panel = { 0.042, 0.042, 0.05 }, + popover = 0x08080A, + panel = 0x0B0B0D, -- Window chrome, i.e. the top bar and the side bar the tab buttons sit on. It reads as raised -- rather than recessed, so it is the one surface that sits above the controls it holds. - chrome = { 0.13, 0.13, 0.15 }, - chromeBorder = { 0.30, 0.30, 0.34 }, + chrome = 0x212126, + chromeBorder = 0x4C4C57, -- Text entry bodies, which sit a little deeper than a button - input = { 0.022, 0.022, 0.028 }, - inputHover = { 0.045, 0.045, 0.055 }, + input = 0x060607, + inputHover = 0x0B0B0E, -- Row highlights inside lists and drop downs - accent = { 0.17, 0.17, 0.20 }, - accentSubtle = { 0.095, 0.095, 0.11 }, + accent = 0x2B2B33, + accentSubtle = 0x18181C, -- Filled emphasis, e.g. a ticked check box or a slider knob - primary = { 0.88, 0.88, 0.91 }, - primaryHover = { 1, 1, 1 }, - onPrimary = { 0.05, 0.05, 0.06 }, + primary = 0xE0E0E8, + primaryHover = 0xFFFFFF, + onPrimary = 0x0D0D0F, -- Text - text = { 0.98, 0.98, 0.98 }, - textMuted = { 0.72, 0.72, 0.75 }, - textDisabled = { 0.42, 0.42, 0.45 }, + text = 0xFAFAFA, + textMuted = 0xB8B8BF, + textDisabled = 0x6B6B73, -- Focus ring drawn just outside a focused control - ring = { 0.55, 0.55, 0.62 }, + ring = 0x8C8C9E, -- Drag and drop feedback - dropTarget = { 0.35, 0.75, 0.45 }, - dropTargetSurface = { 0.05, 0.09, 0.06 }, + dropTarget = 0x59BF73, + dropTargetSurface = 0x0D170F, } -- A white disc; each quadrant is used as a rounded corner, so a rounded rectangle is @@ -80,11 +84,23 @@ roundImage:Load("Assets/ui_round.png", "CLAMP", "MIPMAP") local checkImage = NewImageHandle() checkImage:Load("Assets/ui_check.png", "CLAMP", "MIPMAP") ----Set the current draw colour from a token ----@param color number[] +---Set the current draw colour from a packed 0xRRGGBB token +---@param color integer ---@param alpha? number function ui.SetColor(color, alpha) - SetDrawColor(color[1], color[2], color[3], alpha or color[4] or 1) + SetDrawColor( + m_floor(color / 0x10000) / 255, + m_floor(color / 0x100) % 0x100 / 255, + color % 0x100 / 255, + alpha or 1 + ) +end + +---Pack a colour that only exists at draw time, e.g. one a control's borderFunc returns, into the +---same form the tokens use. Returns a number, so it costs no allocation. +---@return integer +function ui.PackColor(r, g, b) + return m_floor(r * 255 + 0.5) * 0x10000 + m_floor(g * 255 + 0.5) * 0x100 + m_floor(b * 255 + 0.5) end ---Draw a filled rectangle with rounded corners, using the current draw colour @@ -120,8 +136,8 @@ function ui.DrawCheckMark(x, y, size) end ---Draw an outlined, filled rounded rectangle ----@param border? number[] outline colour, or nil for no outline ----@param fill? number[] body colour, or nil to leave the body untouched +---@param border? integer outline colour, or nil for no outline +---@param fill? integer body colour, or nil to leave the body untouched ---@param thickness? number outline thickness, defaults to 1 function ui.DrawBox(x, y, width, height, radius, border, fill, thickness) radius = radius or ui.radius @@ -148,8 +164,8 @@ function ui.DrawFocusRing(x, y, width, height, radius, color, spread) end ---Pick the outline and body colours for an interactive control ----@return number[] border ----@return number[] fill +---@return integer border +---@return integer fill function ui.SurfaceColors(enabled, hover, active) local colors = ui.colors if not enabled then