From c1d0c542c4c31cb0e8923b5db6e2e695d6fee2d7 Mon Sep 17 00:00:00 2001 From: Can Uysal Date: Sat, 15 Aug 2026 12:32:03 +0300 Subject: [PATCH 1/5] 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. --- src/Assets/ui_round.png | Bin 0 -> 278 bytes src/Classes/ButtonControl.lua | 35 ++----- src/Classes/CheckBoxControl.lua | 43 +++------ src/Classes/DropDownControl.lua | 80 +++++++--------- src/Classes/EditControl.lua | 29 +++--- src/Classes/ListControl.lua | 94 ++++++++----------- src/Classes/PopupDialog.lua | 13 +-- src/Classes/ScrollBarControl.lua | 97 ++++---------------- src/Classes/SectionControl.lua | 17 ++-- src/Classes/SliderControl.lua | 48 +++++----- src/Modules/Main.lua | 4 + src/Modules/UI.lua | 152 +++++++++++++++++++++++++++++++ 12 files changed, 314 insertions(+), 298 deletions(-) create mode 100644 src/Assets/ui_round.png create mode 100644 src/Modules/UI.lua 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 e231280f5ec..9d151dfffd0 100644 --- a/src/Classes/ButtonControl.lua +++ b/src/Classes/ButtonControl.lua @@ -44,41 +44,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 048d2582a2b..60e356f9afc 100644 --- a/src/Classes/CheckBoxControl.lua +++ b/src/Classes/CheckBoxControl.lua @@ -42,42 +42,29 @@ 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) - 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 + if self.state and enabled 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.state then if not enabled then - SetDrawColor(0.33, 0.33, 0.33) - elseif mOver then - SetDrawColor(1, 1, 1) + ui.SetColor(colors.textDisabled) else - SetDrawColor(0.75, 0.75, 0.75) + ui.SetColor(colors.onPrimary) end - main:DrawCheckMark(x + size/2, y + size/2, size * 0.8) - end - if enabled then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.33, 0.33, 0.33) + main:DrawCheckMark(x + size/2, y + size/2, size * 0.7) end + ui.SetColor(enabled and colors.text or colors.textDisabled) local label = self:GetProperty("label") if label and self.labelRight then DrawString(x + self.width + 5, y + 2, nil, size - 4, "VAR", label) diff --git a/src/Classes/DropDownControl.lua b/src/Classes/DropDownControl.lua index bb999c49a48..e49b72f677d 100644 --- a/src/Classes/DropDownControl.lua +++ b/src/Classes/DropDownControl.lua @@ -249,47 +249,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 @@ -303,9 +286,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 @@ -325,18 +308,12 @@ function DropDownClass:Draw(viewPort, noTooltip) DrawString(0, 0, "LEFT", lineHeight, "VAR", selLabel or "") if selDetail ~= nil then local dx = DrawStringWidth(lineHeight, "VAR", selDetail) - 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 + ui.SetColor(fill) DrawImage(nil, width - dx - 4 - 22, 0, width - 4, lineHeight) if enabled then - SetDrawColor(1, 1, 1) + ui.SetColor(colors.textMuted) else - SetDrawColor(0.66, 0.66, 0.66) + ui.SetColor(colors.textDisabled) end DrawString(width - dx - 22, 0, "LEFT", lineHeight, "VAR", selDetail) end @@ -376,14 +353,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 @@ -399,16 +379,18 @@ function DropDownClass:Draw(viewPort, noTooltip) local detail = listVal.detail local dx = DrawStringWidth(lineHeight, "VAR", detail) if index == self.hoverSel then - SetDrawColor(0.33, 0.33, 0.33) + ui.SetColor(colors.accent) + elseif index == self.selIndex then + ui.SetColor(colors.accentSubtle) else - SetDrawColor(0, 0, 0) + ui.SetColor(colors.popover) end DrawImage(nil, width - dx - 8 - 22, y, width - 4, lineHeight) -- 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 DrawString(width - dx - 4 - 22, y, "LEFT", lineHeight, "VAR", detail) end diff --git a/src/Classes/EditControl.lua b/src/Classes/EditControl.lua index 899f5b0bcfe..80d2efbe3b8 100644 --- a/src/Classes/EditControl.lua +++ b/src/Classes/EditControl.lua @@ -248,29 +248,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 47dd9a8f908..d14294337c4 100644 --- a/src/Classes/ListControl.lua +++ b/src/Classes/ListControl.lua @@ -184,20 +184,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)) @@ -236,40 +230,29 @@ 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) + -- 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 value == self.selValue and self.hasFocus then + -- Accent bar marking the focused selection, drawn once at the left edge + if colIndex == 1 then + ui.SetColor(colors.primary) + DrawImage(nil, colOffset, lineY + 2, 2, rowHeight - 4) end - DrawImage(nil, colOffset, lineY + 1, not self.scroll and colWidth - 4 or colWidth, rowHeight - 2) end if not self.SetHighlightColor or not self:SetHighlightColor(index, value) then SetDrawColor(1, 1, 1) @@ -285,33 +268,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 1e54e8c695b..d5825c2e5f0 100644 --- a/src/Classes/PopupDialog.lua +++ b/src/Classes/PopupDialog.lua @@ -44,20 +44,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 145fb94bfdf..c4867cf575b 100644 --- a/src/Classes/ScrollBarControl.lua +++ b/src/Classes/ScrollBarControl.lua @@ -151,107 +151,50 @@ 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 + local colors = ui.colors + -- Draw the track behind the whole bar, including the stepper buttons + ui.SetColor(colors.accentSubtle, 0.6) + ui.DrawRect(x, y, width, height, ui.radiusSmall) -- Draw up/left arrow if not enabled then - SetDrawColor(0.33, 0.33, 0.33) + ui.SetColor(colors.textDisabled) 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") + ui.SetColor(colors.text) 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) + ui.SetColor(colors.textMuted) end if dir == "HORIZONTAL" then - DrawImage(nil, x + width - height, y, height, height) + main:DrawArrow(x + height/2, y + height/2, height/2 - 2, height/2 - 2, "LEFT") 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) + main:DrawArrow(x + width/2, y + width/2, width/2 - 2, width/2 - 2, "UP") end -- Draw down/right arrow if not enabled then - SetDrawColor(0.33, 0.33, 0.33) + ui.SetColor(colors.textDisabled) elseif mOverComp == "DOWN" then - SetDrawColor(1, 1, 1) - else - SetDrawColor(0.5, 0.5, 0.5) - end - if dir == "HORIZONTAL" then - main:DrawArrow(x + width - height/2, y + height/2, height/2, height/2, "RIGHT") - else - main:DrawArrow(x + width/2, y + height - width/2, width/2, width/2, "DOWN") - 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) + ui.SetColor(colors.text) else - SetDrawColor(0.5, 0.5, 0.5) + ui.SetColor(colors.textMuted) 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) + main:DrawArrow(x + width - height/2, y + height/2, height/2 - 2, height/2 - 2, "RIGHT") 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) + main:DrawArrow(x + width/2, y + height - width/2, width/2 - 2, width/2 - 2, "DOWN") end -- Draw knob if enabled then if self.dragging or mOverComp == "KNOB" then - SetDrawColor(1, 1, 1) + ui.SetColor(colors.borderActive) + elseif mOverComp == "SLIDEUP" or mOverComp == "SLIDEDOWN" then + ui.SetColor(colors.borderHover) else - SetDrawColor(0.5, 0.5, 0.5) + ui.SetColor(colors.border) end local knobPos = self:GetKnobPosForOffset() if dir == "HORIZONTAL" then - DrawImage(nil, x + height + knobPos + 1, y + 2, self.knobDim - 2, height - 4) + ui.DrawRect(x + height + knobPos + 1, y + 3, self.knobDim - 2, height - 6, (height - 6) / 2) else - DrawImage(nil, x + 2, y + width + knobPos + 1, width - 4, self.knobDim - 2) + ui.DrawRect(x + 3, y + width + knobPos + 1, width - 6, self.knobDim - 2, (width - 6) / 2) end end end diff --git a/src/Classes/SectionControl.lua b/src/Classes/SectionControl.lua index e0acb6fd213..8fc1ec202fc 100644 --- a/src/Classes/SectionControl.lua +++ b/src/Classes/SectionControl.lua @@ -16,18 +16,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 89dba3e1fc0..d8ee625d9d7 100644 --- a/src/Classes/SliderControl.lua +++ b/src/Classes/SliderControl.lua @@ -88,36 +88,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/Modules/Main.lua b/src/Modules/Main.lua index a7451fe269e..b459bb0d2c2 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") diff --git a/src/Modules/UI.lua b/src/Modules/UI.lua new file mode 100644 index 00000000000..1bc0cb4ac80 --- /dev/null +++ b/src/Modules/UI.lua @@ -0,0 +1,152 @@ +-- 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 + surface = { 0.10, 0.10, 0.12 }, + surfaceHover = { 0.16, 0.16, 0.19 }, + surfaceActive = { 0.22, 0.22, 0.25 }, + surfaceDisabled = { 0.08, 0.08, 0.09 }, + + -- Containers that float above the rest of the interface + popover = { 0.06, 0.06, 0.07 }, + panel = { 0.09, 0.09, 0.10 }, + + -- Text entry bodies, which sit a little deeper than a button + input = { 0.05, 0.05, 0.06 }, + inputHover = { 0.08, 0.08, 0.09 }, + + -- Row highlights inside lists and drop downs + accent = { 0.20, 0.20, 0.23 }, + accentSubtle = { 0.14, 0.14, 0.16 }, + + -- 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.64, 0.64, 0.68 }, + textDisabled = { 0.38, 0.38, 0.41 }, + + -- 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") + +---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 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 0e6111ae88fd29069c18040ef4d51a4146996b18 Mon Sep 17 00:00:00 2001 From: Can Uysal Date: Sun, 23 Aug 2026 21:38:19 +0300 Subject: [PATCH 2/5] Darken the control interiors and rebuild the scroll bar The first pass lifted every control body to around ten percent grey, which flattened the game's own text palette; item rarity colours and the dimmer notes are mixed for contrast against black. Anything holding text now sits near black again and leans on its border for definition, with the lift spent on the hover and pressed states instead. Draw the check mark from a supersampled sprite as well. Untextured geometry has no antialiasing, which barely showed on the old low contrast tick but was obvious once the box became a filled shape. 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. 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/Classes/ListControl.lua | 7 -- src/Classes/ScrollBarControl.lua | 127 +++++++++++-------------------- src/Classes/TextListControl.lua | 106 +++++++++++++++++++++----- src/Modules/Build.lua | 40 +++++----- src/Modules/Main.lua | 6 +- src/Modules/UI.lua | 44 +++++++---- 7 files changed, 184 insertions(+), 146 deletions(-) create mode 100644 src/Assets/ui_check.png 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/Classes/ListControl.lua b/src/Classes/ListControl.lua index d14294337c4..170662ea4b4 100644 --- a/src/Classes/ListControl.lua +++ b/src/Classes/ListControl.lua @@ -247,13 +247,6 @@ function ListClass:Draw(viewPort, noTooltip) ui.SetColor(rowFill) DrawImage(nil, colOffset, lineY + 1, rowWidth, rowHeight - 2) end - if value == self.selValue and self.hasFocus then - -- Accent bar marking the focused selection, drawn once at the left edge - if colIndex == 1 then - ui.SetColor(colors.primary) - DrawImage(nil, colOffset, lineY + 2, 2, rowHeight - 4) - end - end if not self.SetHighlightColor or not self:SetHighlightColor(index, value) then SetDrawColor(1, 1, 1) end diff --git a/src/Classes/ScrollBarControl.lua b/src/Classes/ScrollBarControl.lua index c4867cf575b..e9757d85fb0 100644 --- a/src/Classes/ScrollBarControl.lua +++ b/src/Classes/ScrollBarControl.lua @@ -34,14 +34,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 @@ -81,30 +78,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 @@ -141,9 +122,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 @@ -151,51 +132,39 @@ function ScrollBarClass:Draw() self.holdPauseTime = GetTime() end end - local colors = ui.colors - -- Draw the track behind the whole bar, including the stepper buttons - ui.SetColor(colors.accentSubtle, 0.6) - ui.DrawRect(x, y, width, height, ui.radiusSmall) - -- Draw up/left arrow + -- Nothing to scroll, so nothing to show if not enabled then - ui.SetColor(colors.textDisabled) - elseif mOverComp == "UP" then - ui.SetColor(colors.text) - else - ui.SetColor(colors.textMuted) + return end - if dir == "HORIZONTAL" then - main:DrawArrow(x + height/2, y + height/2, height/2 - 2, height/2 - 2, "LEFT") + 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 + width/2, width/2 - 2, width/2 - 2, "UP") + ui.DrawRect(x + trackOff, y, trackDim, height, trackDim / 2) end - -- Draw down/right arrow - if not enabled then - ui.SetColor(colors.textDisabled) - elseif mOverComp == "DOWN" then - ui.SetColor(colors.text) + -- 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 - ui.SetColor(colors.textMuted) + ui.SetColor(colors.border) end - if dir == "HORIZONTAL" then - main:DrawArrow(x + width - height/2, y + height/2, height/2 - 2, height/2 - 2, "RIGHT") + 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 - main:DrawArrow(x + width/2, y + height - width/2, width/2 - 2, width/2 - 2, "DOWN") - end - -- Draw knob - if enabled then - if self.dragging or mOverComp == "KNOB" then - ui.SetColor(colors.borderActive) - elseif mOverComp == "SLIDEUP" or mOverComp == "SLIDEDOWN" then - ui.SetColor(colors.borderHover) - else - ui.SetColor(colors.border) - end - local knobPos = self:GetKnobPosForOffset() - if dir == "HORIZONTAL" then - ui.DrawRect(x + height + knobPos + 1, y + 3, self.knobDim - 2, height - 6, (height - 6) / 2) - else - ui.DrawRect(x + 3, y + width + knobPos + 1, width - 6, self.knobDim - 2, (width - 6) / 2) - end + ui.DrawRect(x + knobOff, y + knobPos, knobDim, self.knobDim, knobDim / 2) end end @@ -216,20 +185,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/TextListControl.lua b/src/Classes/TextListControl.lua index 50e58a2d168..87ef5dc5fe8 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 + ---@class TextListControl: Control, ControlHost local TextListClass = newClass("TextListControl", "Control", "ControlHost") @@ -20,6 +24,59 @@ function TextListClass:TextListControl(anchor, rect, columns, list, sectionHeigh return self 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 @@ -33,34 +90,46 @@ 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 - local textX = lineInfo.x or colInfo.x + local text = lineInfo[colIndex] + if text then + local font = lineInfo.font or "VAR" + local drawX = lineInfo.x or colInfo.x local align = lineInfo.align or colInfo.align - DrawString(textX, lineY, align, lineInfo.height, lineInfo.font or "VAR", lineInfo[colIndex]) + -- 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, align, lineInfo.height, font, text) if lineInfo.underline and lineInfo.underline[colIndex] then - local width = DrawStringWidth(lineInfo.height, "VAR", StripEscapes(lineInfo[colIndex])) + -- measured off the text as drawn, so a clipped label keeps its rule the same length + local textWidth = DrawStringWidth(lineInfo.height, font, StripEscapes(text)) -- note: not fully handled. this is currently only used for -- the side bar stats - if align == "RIGHT_X" then - textX = textX - width - end - SetDrawColor(0.5, 0.5, 0.5) - DrawImage(nil, textX, lineY + lineInfo.height, width, 1) + local underlineX = align == "RIGHT_X" and drawX - textWidth or drawX + ui.SetColor(ui.colors.textMuted) + DrawImage(nil, underlineX, lineY + textPad + lineInfo.height, textWidth, 1) end end - lineY = lineY + lineInfo.height + lineY = lineY + self:GetLineHeight(lineInfo) end end -- determine which line the user is hovering over @@ -70,11 +139,12 @@ function TextListClass:Draw(viewPort) local rowY = y - scrollBar.offset + 2 -- suboptimal. should do binary search if this causes performance problems for _, lineInfo in ipairs(self.list) do - if cursorY >= rowY and cursorY < rowY + lineInfo.height then - self.hoveredLine = { line = lineInfo, x = x, y = rowY, width = width } + local lineHeight = self:GetLineHeight(lineInfo) + if cursorY >= rowY and cursorY < rowY + lineHeight then + self.hoveredLine = { line = lineInfo, x = x, y = rowY, width = width, height = lineHeight } break end - rowY = rowY + lineInfo.height + rowY = rowY + lineHeight end end SetViewport() diff --git a/src/Modules/Build.lua b/src/Modules/Build.lua index d080d84c90e..c0734877e8f 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -157,10 +157,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) @@ -197,10 +195,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 + 2, height) - SetDrawColor(0, 0, 0) - DrawImage(nil, x + 1, y + 1, width, 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 @@ -566,7 +562,10 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.buildFlag = true end) self.controls.statBoxAnchor = new("Control"):Control({"TOPLEFT",self.controls.mainSkillMinionSkill,"BOTTOMLEFT",true}, {0, 2, 0, 0}) - self.controls.statBox = new("TextListControl"):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"):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 @@ -1325,18 +1324,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) local hovered = self.controls.statBox and self.controls.statBox.hoveredLine @@ -2081,7 +2079,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 @@ -2097,7 +2095,7 @@ function buildMode:RefreshStatList() end self:AddDisplayStatList(self.minionDisplayStats, self.calcsTab.mainEnv.minion, "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.skillFlags.disable then t_insert(statBoxList, { height = 16, "^7Skill disabled:" }) diff --git a/src/Modules/Main.lua b/src/Modules/Main.lua index b459bb0d2c2..5a106217f37 100644 --- a/src/Modules/Main.lua +++ b/src/Modules/Main.lua @@ -1515,11 +1515,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 index 1bc0cb4ac80..0a86acbf78c 100644 --- a/src/Modules/UI.lua +++ b/src/Modules/UI.lua @@ -26,23 +26,31 @@ ui.colors = { borderActive = { 0.63, 0.63, 0.69 }, borderDisabled = { 0.16, 0.16, 0.18 }, - -- Control bodies - surface = { 0.10, 0.10, 0.12 }, - surfaceHover = { 0.16, 0.16, 0.19 }, - surfaceActive = { 0.22, 0.22, 0.25 }, - surfaceDisabled = { 0.08, 0.08, 0.09 }, + -- 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.06, 0.06, 0.07 }, - panel = { 0.09, 0.09, 0.10 }, + 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.05, 0.05, 0.06 }, - inputHover = { 0.08, 0.08, 0.09 }, + input = { 0.022, 0.022, 0.028 }, + inputHover = { 0.045, 0.045, 0.055 }, -- Row highlights inside lists and drop downs - accent = { 0.20, 0.20, 0.23 }, - accentSubtle = { 0.14, 0.14, 0.16 }, + 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 }, @@ -51,8 +59,8 @@ ui.colors = { -- Text text = { 0.98, 0.98, 0.98 }, - textMuted = { 0.64, 0.64, 0.68 }, - textDisabled = { 0.38, 0.38, 0.41 }, + 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 }, @@ -67,6 +75,11 @@ ui.colors = { 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 @@ -101,6 +114,11 @@ 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 From 4c2ea0f14a4bbd90e9d664ca8b774dafd943236f Mon Sep 17 00:00:00 2001 From: Can Uysal Date: Tue, 25 Aug 2026 11:36:23 +0300 Subject: [PATCH 3/5] 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 one sixteen pixel outlier. 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 | 22 +++++++++++----------- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/Classes/DropDownControl.lua b/src/Classes/DropDownControl.lua index e49b72f677d..407d34a237b 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 + ---@class DropDownControl: Control, ControlHost, TooltipHost, SearchHost local DropDownClass = newClass("DropDownControl", "Control", "ControlHost", "TooltipHost", "SearchHost") @@ -305,7 +308,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(fill) @@ -374,7 +377,7 @@ 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 local dx = DrawStringWidth(lineHeight, "VAR", detail) @@ -394,12 +397,12 @@ function DropDownClass:Draw(viewPort, noTooltip) end 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) @@ -532,7 +535,7 @@ function DropDownClass:CheckDroppedWidth(enable) line = line.label or "" end -- +10 to stop clipping - dWidth = m_max(dWidth, DrawStringWidth(lineHeight, "VAR", line) + 10) + dWidth = m_max(dWidth, DrawStringWidth(lineHeight, "VAR", line) + 10 + textInset) end -- no greater than self.maxDroppedWidth self.droppedWidth = m_min(dWidth + scrollWidth, self.maxDroppedWidth) @@ -543,7 +546,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 80d2efbe3b8..f12e9eb3dbe 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 @@ -266,7 +269,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 @@ -490,7 +493,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 c0734877e8f..5dc70e046b1 100644 --- a/src/Modules/Build.lua +++ b/src/Modules/Build.lua @@ -473,7 +473,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"):LabelControl({"TOPLEFT",self.anchorSideBar,"TOPLEFT"}, {0, 80, 300, 16}, "^7Main Skill:") - self.controls.mainSocketGroup = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkillLabel,"BOTTOMLEFT"}, {0, 2, 300, 18}, nil, function(index, value) + self.controls.mainSocketGroup = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkillLabel,"BOTTOMLEFT"}, {0, 4, 300, 20}, nil, function(index, value) self.mainSocketGroup = index self.modFlag = true self.buildFlag = true @@ -485,44 +485,44 @@ function buildMode:Init(dbFileName, buildName, buildXML, convertBuild, importLin self.skillsTab:AddSocketGroupTooltip(tooltip, socketGroup) end end - self.controls.mainSkill = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSocketGroup,"BOTTOMLEFT"}, {0, 2, 300, 18}, nil, function(index, value) + self.controls.mainSkill = new("DropDownControl"):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.mainSkillPart = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkill,"BOTTOMLEFT",true}, {0, 2, 300, 18}, nil, function(index, value) + self.controls.mainSkillPart = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkill,"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"):LabelControl({"TOPLEFT",self.controls.mainSkillPart,"BOTTOMLEFT",true}, {0, 3, 0, 16}, "^7Stages:") { + self.controls.mainSkillStageCountLabel = new("LabelControl"):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"):EditControl({"LEFT",self.controls.mainSkillStageCountLabel,"RIGHT",true}, {2, 0, 60, 18}, nil, nil, "%D", nil, function(buf) + self.controls.mainSkillStageCount = new("EditControl"):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"):LabelControl({"TOPLEFT",self.controls.mainSkillStageCountLabel,"BOTTOMLEFT",true}, {0, 3, 0, 16}, "^7Active Mines:") { + self.controls.mainSkillMineCountLabel = new("LabelControl"):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"):EditControl({"LEFT",self.controls.mainSkillMineCountLabel,"RIGHT",true}, {2, 0, 60, 18}, nil, nil, "%D", nil, function(buf) + self.controls.mainSkillMineCount = new("EditControl"):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"):DropDownControl({"TOPLEFT",self.controls.mainSkillMineCountLabel,"BOTTOMLEFT",true}, {0, 3, 178, 18}, nil, function(index, value) + self.controls.mainSkillMinion = new("DropDownControl"):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 @@ -551,17 +551,17 @@ 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"):ButtonControl({"LEFT",self.controls.mainSkillMinion,"RIGHT"}, {2, 0, 120, 18}, "Manage Spectres...", function() + self.controls.mainSkillMinionLibrary = new("ButtonControl"):ButtonControl({"LEFT",self.controls.mainSkillMinion,"RIGHT"}, {2, 0, 120, 20}, "Manage Spectres...", function() self:OpenSpectreLibrary() end) - self.controls.mainSkillMinionSkill = new("DropDownControl"):DropDownControl({"TOPLEFT",self.controls.mainSkillMinion,"BOTTOMLEFT",true}, {0, 2, 200, 16}, nil, function(index, value) + self.controls.mainSkillMinionSkill = new("DropDownControl"):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.statBoxAnchor = new("Control"):Control({"TOPLEFT",self.controls.mainSkillMinionSkill,"BOTTOMLEFT",true}, {0, 2, 0, 0}) + self.controls.statBoxAnchor = new("Control"):Control({"TOPLEFT",self.controls.mainSkillMinionSkill,"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"):TextListControl({"TOPLEFT",self.controls.statBoxAnchor,"BOTTOMLEFT"}, {0, 2, 300, 0}, {{x=6,align="LEFT"},{x=268,align="RIGHT_X"}}) From c957e9c50062e0c654bb0966a37a106113113379 Mon Sep 17 00:00:00 2001 From: Can Uysal Date: Tue, 25 Aug 2026 12:53:29 +0300 Subject: [PATCH 4/5] Add a temporary switch for squaring off the rounded corners Not everyone wants rounded corners, so ui.rounded lets the whole refresh be viewed with sharp edges without touching anything else: same colours, same spacing, same borders, only the corners change. It gates the drawing primitives rather than the radius tokens, because the slider knob, the scroll bar thumb and the list drag marker work out their own radius instead of reading a token, and zeroing the tokens would leave those three still round. Defaults to rounded, and is marked to come out before this reaches upstream. --- src/Modules/UI.lua | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Modules/UI.lua b/src/Modules/UI.lua index 0a86acbf78c..692913ee8bf 100644 --- a/src/Modules/UI.lua +++ b/src/Modules/UI.lua @@ -13,6 +13,13 @@ local m_floor = math.floor local ui = { } +-- TODO: remove this switch before merging upstream. It is here for the review only: not everyone +-- wants rounded corners, and some exiles have said they prefer the sharp edges, so setting this +-- to false squares everything off and makes the two looks easy to compare side by side. +-- Honoured by the primitives rather than the tokens, so it also catches the places that derive +-- their own radius, like the slider knob and the scroll bar thumb. +ui.rounded = true + -- Corner rounding used by the base controls ui.radiusSmall = 3 ui.radius = 5 @@ -94,7 +101,7 @@ function ui.DrawRect(x, y, width, height, radius) return end radius = m_floor(m_min(radius or ui.radius, width / 2, height / 2)) - if radius < 1 then + if not ui.rounded or radius < 1 then DrawImage(nil, x, y, width, height) return end @@ -111,6 +118,10 @@ end ---Draw a filled circle of the given diameter, using the current draw colour function ui.DrawCircle(x, y, diameter) + if not ui.rounded then + DrawImage(nil, x, y, diameter, diameter) + return + end DrawImage(roundImage, x, y, diameter, diameter) end From 9e017c315cc1de2bb99af65fed745c9ecfa292e0 Mon Sep 17 00:00:00 2001 From: Can Uysal Date: Tue, 25 Aug 2026 16:24:08 +0300 Subject: [PATCH 5/5] 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 60e356f9afc..c0967e4f71c 100644 --- a/src/Classes/CheckBoxControl.lua +++ b/src/Classes/CheckBoxControl.lua @@ -47,7 +47,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 diff --git a/src/Classes/DropDownControl.lua b/src/Classes/DropDownControl.lua index 407d34a237b..fdd2e41e11e 100644 --- a/src/Classes/DropDownControl.lua +++ b/src/Classes/DropDownControl.lua @@ -257,7 +257,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 f12e9eb3dbe..53d33a74b26 100644 --- a/src/Classes/EditControl.lua +++ b/src/Classes/EditControl.lua @@ -263,7 +263,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 692913ee8bf..cae5a6fe98c 100644 --- a/src/Modules/UI.lua +++ b/src/Modules/UI.lua @@ -26,55 +26,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 @@ -87,11 +91,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 @@ -131,8 +147,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 @@ -159,8 +175,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