From 5638ca80e20354122d1e9ed279de0758ea932559 Mon Sep 17 00:00:00 2001 From: Floris Date: Thu, 10 Sep 2026 00:24:48 +0200 Subject: [PATCH 1/2] keybind editor: more style tweaks (#9175) --- luaui/Include/keybind_dropdown.lua | 8 ++- luaui/Include/keybind_editbox.lua | 57 ++++++++++++++++++++-- luaui/Include/keybind_editor_view.lua | 70 +++++++++++++++++++++------ luaui/Widgets/gui_keybind_info.lua | 4 +- 4 files changed, 119 insertions(+), 20 deletions(-) diff --git a/luaui/Include/keybind_dropdown.lua b/luaui/Include/keybind_dropdown.lua index 002ed3ddbcb..5fdb220e10a 100644 --- a/luaui/Include/keybind_dropdown.lua +++ b/luaui/Include/keybind_dropdown.lua @@ -147,7 +147,13 @@ function Dropdown:draw() -- A profile name is free text and can outrun the control, which is fixed width so the -- header does not reflow every time the selection changes. local labelW = (arrowX - arrowH) - (x1 + inset) - inset * 2 - font:Print(fittedLabel(fitted, 0, font, label, labelW, self.fontSize), x1 + inset, (y1 + y2) * 0.5, self.fontSize, "ov") + font:Print( + fittedLabel(fitted, 0, font, label, labelW, self.fontSize), + x1 + inset, + (y1 + y2) * 0.5, + self.fontSize, + "ov" + ) font:End() if self.open and #self.optRects > 0 then diff --git a/luaui/Include/keybind_editbox.lua b/luaui/Include/keybind_editbox.lua index fa3d2993d01..59ffceb7a4b 100644 --- a/luaui/Include/keybind_editbox.lua +++ b/luaui/Include/keybind_editbox.lua @@ -13,11 +13,24 @@ local floor = math.floor local colorText = "\255\235\235\235" local colorDim = "\255\160\160\160" +-- Caret look and blink taken from gui_chat's input, so the two fields read as the same +-- control: a sharp bar that starts bright on a keystroke and fades over a second before +-- snapping back, rather than a hard on/off blink. +local cursorBlinkDuration = 1 +local cursorGrey = 0.7 + -- Font is fetched per draw; it does not exist when this file is included. local function getFont() return WG["fonts"].getFont() end +-- Restarts the fade, so the caret is at its brightest right after an edit. +local function resetBlink(self) + self.blinkStart = Spring.GetTimer() + self.blinkText = self.text + self.blinkCaret = self.caret +end + -- Single-line text field with a caret, selection and word motion. function Editbox.new(opts) opts = opts or {} @@ -62,6 +75,11 @@ end -- SDL text input is owned by the panel, not by this field: blurring the search box to -- click a keybind must not stop text events while the editor is still open. function Editbox:focus() + -- A field that just took focus shows a bright caret, not whatever phase the fade + -- happened to be in when it was last used. + if not self.focused then + resetBlink(self) + end self.focused = true end @@ -270,11 +288,37 @@ local function update(self) self.dragging = false end end + + -- Watched here rather than reset from each editing path: every way the caret can move + -- (typing, deleting, arrows, a click, a drag, setText) shows up as one of these two + -- changing, so none of them can be missed. + if not self.blinkStart or self.text ~= self.blinkText or self.caret ~= self.blinkCaret then + resetBlink(self) + end +end + +-- Alpha of the caret this frame: full brightness at the last edit, fading to 0.15 over +-- the blink duration, then starting over. Matches gui_chat's sawtooth exactly. +local function caretAlpha(self) + local elapsed = Spring.DiffTimers(Spring.GetTimer(), self.blinkStart) % cursorBlinkDuration + + return 1 - (elapsed * (1 / cursorBlinkDuration)) + 0.15 +end + +-- How far into the text the caret sits, in pixels. Measured only when the text, the caret +-- or the size moved: the field is drawn live every frame so the blink can animate, and +-- measuring the leading substring each of those frames is the one real cost in here. +local function caretOffset(self, font) + if self.caretPxAt ~= self.caret or self.caretPxText ~= self.text or self.caretPxFs ~= self.fontSize then + self.caretPxAt, self.caretPxText, self.caretPxFs = self.caret, self.text, self.fontSize + self.caretPx = font:GetTextWidth(utf8.sub(self.text, 1, self.caret)) * self.fontSize + end + + return self.caretPx end -- Held rather than built per draw: a colour table a frame is an allocation a frame. local fieldFill = { 0, 0, 0, 0.35 } -local caretFill = { 1, 1, 1, 0.85 } function Editbox:draw() update(self) @@ -321,8 +365,15 @@ function Editbox:draw() font:End() if self.focused then - local cw = font:GetTextWidth(utf8.sub(self.text, 1, self.caret)) * self.fontSize - R(tx + cw, y1 + inset, tx + cw + math.max(1, floor(inset * 0.5)), y2 - inset, 0, 0, 0, 0, 0, caretFill) + -- Sharp bar rather than a rounded one, sized and placed off the font like chat's: + -- a fixed span around the text's middle, so it does not stretch with the field. + local cx = tx + caretOffset(self, font) + local cWidth = 1 + floor(self.fontSize / 14) + local cy1 = math.max(y1 + 1, ty - self.fontSize * 0.6) + local cy2 = math.min(y2 - 1, ty + self.fontSize * 0.64) + gl.Color(cursorGrey, cursorGrey, cursorGrey, caretAlpha(self)) + gl.Rect(cx, cy1, cx + cWidth, cy2) + gl.Color(1, 1, 1, 1) end end diff --git a/luaui/Include/keybind_editor_view.lua b/luaui/Include/keybind_editor_view.lua index e95bd8f0149..30f495db49c 100644 --- a/luaui/Include/keybind_editor_view.lua +++ b/luaui/Include/keybind_editor_view.lua @@ -107,6 +107,8 @@ local Scroller local UiElement ---@type function local Highlight +---@type function +local UiButton local colorAction = "\255\210\210\205" local colorKey = "\255\235\185\070" @@ -127,14 +129,16 @@ local confirmFillHover = { 0.24, 0.52, 0.29, 1 } local confirmFillMuted = { 0.11, 0.20, 0.13, 1 } local pillFill = { 0.22, 0.22, 0.22, 1 } local sheenTop = { 1, 1, 1, 0.05 } -local sheenNone = { 1, 1, 1, 0 } -- Fills and captions the list is painted with, in one table for the same reason as -- metrics above. local look = { chipFill = { 0, 0, 0, 0.35 }, chipFillHover = { 0, 0, 0, 0.45 }, addFill = { 0.2, 0.45, 0.25, 0.4 }, - addFillHover = { 0.2, 0.45, 0.25, 0.55 }, + -- Lit rather than nudged: hovering used to lift the alpha alone, which on a green this + -- soft was hard to tell from resting. A tinted element brightens its own fill instead + -- of taking the white overlay, which would wash the green out to grey. + addFillHover = { 0.32, 0.74, 0.4, 0.6 }, selectedFill = { 1, 1, 1, 0.13 }, -- The category column sits on its own darker card, so it reads apart from the list. sidebarFill = { 0, 0, 0, 0.24 }, @@ -146,9 +150,28 @@ local look = { removeHot = colorDanger .. "x", removeCold = colorDim .. "x", plusText = colorText .. "+", + -- The glyph goes to full white with it, the way a chip's key does under the cursor. + plusTextHover = "\255\255\255\255" .. "+", arrow = colorKey .. string.char(226, 128, 186), } +-- FlowUI's Button gradients from a bottom stop to a top one. Left to its defaults it +-- fades black up to near-transparent white, which washes a tinted button out to grey, so +-- each fill becomes a darker bottom and itself on top - the same shape gui_pregameui +-- gives its ready button. Derived once per fill and kept, since the pair is passed every +-- draw and a table per button per frame is what the rest of this file avoids. +look.gradients = setmetatable({}, { + __index = function(self, fill) + local pair = { + { fill[1] * 0.55, fill[2] * 0.55, fill[3] * 0.55, fill[4] or 1 }, + { fill[1], fill[2], fill[3], fill[4] or 1 }, + } + self[fill] = pair + + return pair + end, +}) + ---@type table local searchBox ---@type table @@ -897,6 +920,9 @@ local function startReset() openDialog({ title = L.reset, message = L.resetConfirm, + -- Named for what it does. Without this it falls back to the generic "Accept", which + -- says nothing about the edits being thrown away. + acceptLabel = L.discard, accept = function() discardStaged() end, @@ -1119,6 +1145,7 @@ function view.init() Scroller = WG.FlowUI.Draw.Scroller UiElement = WG.FlowUI.Draw.Element Highlight = WG.FlowUI.Draw.SelectHighlight + UiButton = WG.FlowUI.Draw.Button ensureControls() end @@ -1156,7 +1183,7 @@ function view.setArea(x1, y1, x2, y2, s) local pad = floor(6 * scale) - sidebarW = floor(260 * scale) + sidebarW = floor(240 * scale) listX1 = area.x1 + sidebarW + floor(12 * scale) layoutHeader() @@ -1771,7 +1798,8 @@ local function rowLayout(row) lay.arrow = look.arrow lay.arrowX = listX1 + metrics.rowPad * 5 + floor(font:GetTextWidth(row.label) * metrics.rowFs) + metrics.rowPad * 2 else - lay.text = colorAction .. text.fit(font, row.label, keyAreaX1 - (listX1 + metrics.rowPad) - metrics.rowPad, metrics.rowFs) + local labelW = keyAreaX1 - (listX1 + metrics.rowPad) - metrics.rowPad + lay.text = colorAction .. text.fit(font, row.label, labelW, metrics.rowFs) local mets, cx, addW, rightGap = rowChipBand(row.action, metrics.rowFs, metrics.rowPad) for i = 1, #mets do local m = mets[i] @@ -1941,15 +1969,32 @@ local function gridCellRect(row, col, x1, gridBottom, cell) return cx, cy, cx + cell, cy + cell end +-- Through FlowUI's Button so these carry the same border, gloss and corner as every other +-- button in the UI. It serves a repeated draw from a display-list cache; the cached form +-- was checked against the immediate one and is identical, so a button does not change as +-- the cache takes over. local function drawButtonFace(r, base) - RectRound(r[1], r[2], r[3], r[4], metrics.csButton, 1, 1, 1, 1, base, base) - RectRound(r[1], r[2], r[3], (r[2] + r[4]) * 0.5, metrics.csButton, 0, 0, 1, 1, sheenTop, sheenNone) + local pair = look.gradients[base] + + UiButton(r[1], r[2], r[3], r[4], 1, 1, 1, 1, 1, 1, 1, 1, nil, pair[1], pair[2]) end -- The category column: its own card under the title, then one entry per category, with -- hoverIdx the entry under the cursor. local function drawSidebar(hoverIdx) - RectRound(area.x1, area.y1, area.x1 + sidebarW, area.y2 - floor(33 * scale), metrics.csPanel, 1, 1, 1, 1, look.sidebarFill, look.sidebarFillTop) + RectRound( + area.x1, + area.y1, + area.x1 + sidebarW, + area.y2 - floor(33 * scale), + metrics.csPanel, + 1, + 1, + 1, + 1, + look.sidebarFill, + look.sidebarFillTop + ) queueText(L.titleText, area.x1 + metrics.sidePad, area.y2 - floor(17 * scale), floor(rowHeight * 0.85), "ov") -- Laid out before the font existed, so the labels are still waiting to be fitted. @@ -1968,7 +2013,7 @@ local function drawSidebar(hoverIdx) elseif i == hoverIdx then Highlight(x1 + metrics.catInset, y1, x2 - metrics.catInset, y2, metrics.csSmall, look.rowHoverOpacity, look.white) end - queueText((selected and c.textSel or c.textDim) or c.label, x1 + metrics.sidePad, (y1 + y2) * 0.5, fs, "ov") + queueText((selected and c.textSel or c.textDim) or c.label, x1 + metrics.sidePad, (y1 + y2) * 0.5, fs*0.85, "ov") end end end @@ -2212,8 +2257,9 @@ local function drawRow(row, top, bottom, hovered, zone, zoneIdx) if lay.showAdd then local cx = lay.cx - RectRound(cx, c1, cx + lay.addW, c2, metrics.csSmall, 1, 1, 1, 1, zone == "add" and look.addFillHover or look.addFill) - queueText(look.plusText, (cx + cx + lay.addW) * 0.5, cyc, fs, "cov") + local overAdd = zone == "add" + RectRound(cx, c1, cx + lay.addW, c2, metrics.csSmall, 1, 1, 1, 1, overAdd and look.addFillHover or look.addFill) + queueText(overAdd and look.plusTextHover or look.plusText, (cx + cx + lay.addW) * 0.5, cyc, fs, "cov") end end @@ -2417,10 +2463,6 @@ local function drawButtons(hotId) -- A tinted button loses its colour under the usual white hover overlay, so it -- brightens its own fill instead. local fill = b.fill and ((not enabled and b.fillMuted) or (hovered and b.fillHover) or b.fill) - -- Drawn here rather than through Draw.Button: that caches each distinct button - -- into a display list compiled mid-frame on a budget, and the immediate and - -- replayed forms do not match, so a button sized to its own label visibly - -- alternates between them. drawButtonFace(r, fill or buttonFill) if b.icon then diff --git a/luaui/Widgets/gui_keybind_info.lua b/luaui/Widgets/gui_keybind_info.lua index 5e3cad12d1d..e04b8d305fe 100644 --- a/luaui/Widgets/gui_keybind_info.lua +++ b/luaui/Widgets/gui_keybind_info.lua @@ -24,8 +24,8 @@ local doUpdate local vsx, vsy = spGetViewGeometry() -local screenHeightOrg = 760 -local screenWidthOrg = 1180 +local screenHeightOrg = 640 +local screenWidthOrg = 1100 local screenHeight = screenHeightOrg local screenWidth = screenWidthOrg From 8a54f124e3c23b472e1f1070002eb36f7c3a8de1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:18:56 +0000 Subject: [PATCH 2/2] Update Lua libary submodule --- recoil-lua-library | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recoil-lua-library b/recoil-lua-library index 73fe3148a5e..e81c769a8f0 160000 --- a/recoil-lua-library +++ b/recoil-lua-library @@ -1 +1 @@ -Subproject commit 73fe3148a5e9989001da5aa990832a9a9bd8f285 +Subproject commit e81c769a8f0a2ccae1c1508e1cec131f788c0aef