From a635c1bc348a4597511613f7a514cd6066a76ca3 Mon Sep 17 00:00:00 2001 From: Floris Date: Sat, 12 Sep 2026 00:22:21 +0200 Subject: [PATCH 1/2] top bar buttons: extracted into separate widget (#9211) --- luaui/Widgets/gui_ecostats.lua | 19 +- luaui/Widgets/gui_options.lua | 6 +- luaui/Widgets/gui_spectator_hud.lua | 13 +- luaui/Widgets/gui_top_bar.lua | 1116 ++------------------ luaui/Widgets/gui_top_bar_buttons.lua | 1351 +++++++++++++++++++++++++ 5 files changed, 1471 insertions(+), 1034 deletions(-) create mode 100644 luaui/Widgets/gui_top_bar_buttons.lua diff --git a/luaui/Widgets/gui_ecostats.lua b/luaui/Widgets/gui_ecostats.lua index d04a1820cd1..e985a9d12f1 100644 --- a/luaui/Widgets/gui_ecostats.lua +++ b/luaui/Widgets/gui_ecostats.lua @@ -365,12 +365,21 @@ local function updateButtons() end if cfgSticktotopbar and WG.topbar ~= nil then - local topbarArea = WG.topbar.GetPosition() - if not topbarShowButtons then - topbarArea[2] = topbarArea[4] + -- Hang under the menu button strip, right-aligned with it. Its widget owns that + -- rect; without it fall back to the top bar, and to the screen top without that. + local buttonsArea = WG.topbar.GetButtonsPosition and WG.topbar.GetButtonsPosition() + local topbarArea = WG.topbar.GetPosition and WG.topbar.GetPosition() + if buttonsArea and topbarShowButtons then + widgetPosX = buttonsArea[3] - widgetWidth + widgetPosY = buttonsArea[2] - widgetHeight + elseif topbarArea then + -- no buttons in the way, so sit right under the bar itself + widgetPosX = topbarArea[3] - widgetWidth + widgetPosY = topbarArea[2] - widgetHeight + elseif buttonsArea then + widgetPosX = buttonsArea[3] - widgetWidth + widgetPosY = buttonsArea[2] - widgetHeight end - widgetPosX = topbarArea[3] - widgetWidth - widgetPosY = (topbarArea[6] or topbarArea[2]) - widgetHeight end if widgetPosX + widgetWidth / 2 > vsx / 2 then diff --git a/luaui/Widgets/gui_options.lua b/luaui/Widgets/gui_options.lua index bcf129ace67..099b7db6d3c 100644 --- a/luaui/Widgets/gui_options.lua +++ b/luaui/Widgets/gui_options.lua @@ -6955,12 +6955,12 @@ function init() "ui.settings.option.topbar_hidebuttons" ), type = "bool", - value = (WG.topbar ~= nil and WG.topbar.getAutoHideButtons() or 0), + value = (WG.topbar ~= nil and WG.topbar.getAutoHideButtons ~= nil and WG.topbar.getAutoHideButtons()) or false, onload = function(i) - loadWidgetData("Top Bar", "topbar_hidebuttons", { "autoHideButtons" }) + loadWidgetData("Top Bar Buttons", "topbar_hidebuttons", { "autoHideButtons" }) end, onchange = function(i, value) - saveOptionValue("Top Bar", "topbar", "setAutoHideButtons", { "autoHideButtons" }, value) + saveOptionValue("Top Bar Buttons", "topbar", "setAutoHideButtons", { "autoHideButtons" }, value) end, }, diff --git a/luaui/Widgets/gui_spectator_hud.lua b/luaui/Widgets/gui_spectator_hud.lua index 5d79984b841..f604e78242e 100644 --- a/luaui/Widgets/gui_spectator_hud.lua +++ b/luaui/Widgets/gui_spectator_hud.lua @@ -752,9 +752,16 @@ local function calculateWidgetDimensions() widgetDimensions.left = viewScreenWidth - widgetDimensions.width widgetDimensions.distanceFromTopBar = mathfloor(defaults.widgetDimensions.distanceFromTopBar * scaleMultiplier) - if WG.topbar and WG.topbar.getShowButtons() then - local topBarPosition = WG.topbar.GetPosition() - widgetDimensions.top = (topBarPosition[6] or topBarPosition[2]) -- widgetDimensions.distanceFromTopBar + -- Sit under whatever occupies the top right corner: the menu button strip when it + -- is shown (its own widget owns that rect), otherwise the top bar, otherwise the + -- screen edge. + local buttonsArea = WG.topbar and WG.topbar.GetButtonsPosition and WG.topbar.GetButtonsPosition() + local topBarPosition = WG.topbar and WG.topbar.GetPosition and WG.topbar.GetPosition() + local buttonsShown = WG.topbar and WG.topbar.getShowButtons and WG.topbar.getShowButtons() + if buttonsArea and buttonsShown then + widgetDimensions.top = buttonsArea[2] + elseif topBarPosition then + widgetDimensions.top = topBarPosition[2] else widgetDimensions.top = viewScreenHeight end diff --git a/luaui/Widgets/gui_top_bar.lua b/luaui/Widgets/gui_top_bar.lua index 59c6931b729..25973bab0f2 100644 --- a/luaui/Widgets/gui_top_bar.lua +++ b/luaui/Widgets/gui_top_bar.lua @@ -3,14 +3,13 @@ local widget = widget ---@type Widget function widget:GetInfo() return { name = "Top Bar", - desc = "Shows Resources, wind speed, commander counter, and various options.", + desc = "Shows resources, wind speed and the commander counter.", author = "Floris", date = "Feb, 2017", license = "GNU GPL, v2 or later", layer = -95000, enabled = true, handler = true, --can use widgetHandler:x() - modalExempt = true, -- its menu buttons are how the player switches/closes windows } end @@ -60,7 +59,6 @@ local skewTan = math.tan(math.rad(cfg.skewAngleDeg)) -- System local guishaderEnabled = false -local gameinfoEnabled = false local gaiaTeamID = Spring.GetGaiaTeamID() local spec = sp.GetSpectatingState() local myAllyTeamID = sp.GetMyAllyTeamID() @@ -70,16 +68,8 @@ local myAllyTeamList = sp.GetTeamList(myAllyTeamID) local numTeamsInAllyTeam = #myAllyTeamList -- Game mode / state -local numPlayers = BAR.Utilities.GetPlayerCount() -local isSinglePlayer = BAR.Utilities.Gametype.IsSinglePlayer() -local _modOpts = Spring.GetModOptions() -local isScenario = _modOpts ~= nil and _modOpts.scenariooptions ~= nil -local chobbyLoaded = false local isSingle = false -local gameStarted = (sp.GetGameFrame() > 0) local gameFrame = sp.GetGameFrame() -local gameIsOver = false -local graphsWindowVisible = false -- Resources local r = { metal = { sp.GetTeamResources(myTeamID, "metal") }, energy = { sp.GetTeamResources(myTeamID, "energy") } } @@ -166,7 +156,6 @@ local shareIndicatorArea = { metal = {}, energy = {} } local windArea = {} local tidalarea = {} local comsArea = {} -local buttonsArea = {} -- UI State local orgHeight = 46 @@ -178,11 +167,8 @@ local mx = -1 local my = -1 local widgetScale = (0.80 + (vsx * vsy / 6000000)) local xPos = mathFloor(vsx * cfg.relXpos) -local showButtons = true -local autoHideButtons = false local showResourceBars = true -local widgetSpaceMargin, bgpadding, RectRound, RectRoundOutline, TexturedRectRound, UiElement, UiButton, UiSliderKnob -local RectRoundCircle +local widgetSpaceMargin, bgpadding, RectRound, RectRoundOutline, TexturedRectRound, UiElement, UiSliderKnob local updateRes = { metal = { false, false, false, false }, energy = { false, false, false, false } } -- Display Lists (consolidated into table to save local slots) @@ -210,12 +196,7 @@ local cache = { lastResbarValueWidth = { metal = 1, energy = 1 }, lastDrawnValue = { metal = "", energy = "" }, warningCleared = { metal = false, energy = false }, - prevShowButtons = showButtons, showIndicators = true, - -- A window (or the quit dialog) is open and the handler is hiding the rest of the - -- interface: draw the menu buttons only, they are how the player leaves it again. - ---@type boolean - modalActive = false, } -- Reused scratch tables for DrawScreen to avoid per-frame allocations. @@ -246,12 +227,11 @@ end -- Interactions local draggingShareIndicatorValue = {} local draggingConversionIndicatorValue, draggingShareIndicator, draggingConversionIndicator -local conversionIndicatorArea, quitscreenArea, quitscreenStayArea, quitscreenQuitArea, quitscreenResignArea, quitscreenTeamResignArea, hoveringTopbar, hideQuitWindow -local font, font2, firstButton, fontSize, comcountChanged, showQuitscreen, resbarHover, teamResign +local conversionIndicatorArea, hoveringTopbar +local font2, fontSize, comcountChanged, resbarHover -- Audio local playSounds = true -local leftclick = "LuaUI/Sounds/tock.wav" local resourceclick = "LuaUI/Sounds/buildbar_click.wav" -- Timers + intervals (consolidated into table to save local slots) @@ -275,18 +255,6 @@ local blinkDirection = true local blinkProgress = 0 -------------------------------------------------------------------------------- -local function getPlayerLiveAllyCount() - local nAllies = 0 - for _, teamID in ipairs(myAllyTeamList) do - if teamID ~= myTeamID then - local _, _, isDead, hasAI = Spring.GetTeamInfo(teamID, false) - if not isDead and not hasAI then - nAllies = nAllies + 1 - end - end - end - return nAllies -end local function RectQuad(px, py, sx, sy, offset) gl.TexCoord(offset, 1 - offset) @@ -314,11 +282,8 @@ function widget:ViewResize() RectRoundOutline = WG.FlowUI.Draw.RectRoundOutline TexturedRectRound = WG.FlowUI.Draw.TexturedRectRound UiElement = WG.FlowUI.Draw.Element - UiButton = WG.FlowUI.Draw.Button UiSliderKnob = WG.FlowUI.Draw.SliderKnob - RectRoundCircle = WG.FlowUI.Draw.RectRoundCircle - font = WG.fonts.getFont() font2 = WG.fonts.getFont(2) for n, _ in pairs(dlist.windText) do @@ -367,147 +332,6 @@ local function short(n, f) return result end -local function updateButtons() - local fontsize = (height * widgetScale) / 3 - local prevButtonsArea = buttonsArea - - -- if not buttonsArea['buttons'] then -- With this condition it doesn't actually update buttons if they were already added - buttonsArea.buttons = {} - - local margin = bgpadding - local textPadding = mathFloor(fontsize * 0.8) - local sidePadding = textPadding - local offset = sidePadding - local lastbutton - local badgeMinRadius = fontsize * 0.47 - local badgeFontsize = fontsize * 0.69 - - -- badge: optional number shown in a circle at the bottom right of the button text - local function addButton(name, text, badge) - local textWidth = font2:GetTextWidth(text) * fontsize - -- the circle grows along with the amount of characters the number has - local badgeRadius = 0 - if badge then - local badgeTextWidth = font2:GetTextWidth(badge) * badgeFontsize - badgeRadius = mathMax(badgeMinRadius, (badgeTextWidth / 2) + (fontsize * 0.25)) - end - local badgeWidth = badgeRadius * 2 - local width = mathFloor(textWidth + badgeWidth + textPadding) - local textCenter = buttonsArea[3] - offset - (width / 2) - (badgeWidth / 2) - buttonsArea.buttons[name] = { - buttonsArea[3] - offset - width, - buttonsArea[2] + margin, - buttonsArea[3] - offset, - buttonsArea[4], - text, - textCenter, - } - if badge then - local button = buttonsArea.buttons[name] - button[7] = { - text = badge, - x = textCenter + (textWidth / 2) + badgeRadius + (1.5 * widgetScale), - -- slightly below the center of the button text, so it sits at its bottom right - y = button[2] + ((button[4] - button[2]) * 0.5) - (fontsize / 5) + (fontsize * 0.15), - radius = badgeRadius, - fontsize = badgeFontsize, - } - end - if not lastbutton then - buttonsArea.buttons[name][3] = buttonsArea[3] - end - offset = mathFloor(offset + width + 0.5) - lastbutton = name - end - - if not gameIsOver and chobbyLoaded then - addButton("quit", BAR.I18N("ui.topbar.button.lobby")) - else - addButton("quit", BAR.I18N("ui.topbar.button.quit")) - end - if not gameIsOver and not spec and gameStarted and not isSinglePlayer then - addButton("resign", BAR.I18N("ui.topbar.button.resign")) - end - - if WG.options then - addButton("options", BAR.I18N("ui.topbar.button.settings")) - end - if WG.keybinds then - addButton("keybinds", BAR.I18N("ui.topbar.button.keys")) - end - if WG.changelog and not isScenario then - addButton("changelog", BAR.I18N("ui.topbar.button.changes")) - end - if WG.teamstats and not isScenario then - addButton("stats", BAR.I18N("ui.topbar.button.stats")) - end - -- only shown when settings differ from their default, the amount of them is put in the badge - if WG.gameinfo and (not isSinglePlayer or BAR.Utilities.ShowDevUI()) then - local changedCount = WG.gameinfo.getChangedModoptionsCount and WG.gameinfo.getChangedModoptionsCount() or 0 - if changedCount > 0 then - addButton("info", BAR.I18N("ui.topbar.button.info"), tostring(changedCount)) - end - end - if gameIsOver then - addButton("graphs", BAR.I18N("ui.topbar.button.graphs")) - end - if WG.scavengerinfo then - addButton("scavengers", BAR.I18N("ui.topbar.button.scavengers")) - end - if isScenario and WG.missioninfo then - addButton("mission", BAR.I18N("ui.topbar.button.mission")) - end - if isSinglePlayer and cfg.allowSavegame and WG.savegame then - addButton("save", BAR.I18N("ui.topbar.button.save")) - end - - buttonsArea.buttons[lastbutton][1] = buttonsArea.buttons[lastbutton][1] - sidePadding - offset = offset + sidePadding - buttonsArea[1] = buttonsArea[3] - offset - margin - - -- sometimes its gets wider when (stats) button gets added - if prevButtonsArea[1] and buttonsArea[1] ~= prevButtonsArea[1] then - refreshUi = true - end - prevButtonsArea = buttonsArea - - if dlist.buttons then - glDeleteList(dlist.buttons) - end - dlist.buttons = glCreateList(function() - for _, params in pairs(buttonsArea.buttons) do - local badge = params[7] - if badge then - -- corner size 0.586 x radius makes it a regular octagon, which reads as a circle at this size - -- the dark circle below the smaller colored one acts as its outline - local outlineColor = { 0.18, 0.18, 0.18, 1 } - local badgeColor = { 0.66, 0.66, 0.66, 1 } - local innerRadius = badge.radius - mathMax(1, badge.radius * 0.15) - RectRoundCircle(badge.x, badge.y, badge.radius, badge.radius * 0.586, 0, outlineColor, outlineColor) - RectRoundCircle(badge.x, badge.y, innerRadius, innerRadius * 0.586, 0, badgeColor, badgeColor) - end - end - font2:Begin(true) - font2:SetTextColor(0.92, 0.92, 0.92, 1) - font2:SetOutlineColor(0, 0, 0, 1) - for name, params in pairs(buttonsArea.buttons) do - font2:Print( - params[5], - params[6], - params[2] + ((params[4] - params[2]) * 0.5) - (fontsize / 5), - fontsize, - "co" - ) - local badge = params[7] - if badge then - font2:SetTextColor(0.08, 0.08, 0.08, 1) - font2:Print(badge.text, badge.x, badge.y - (badge.fontsize * 0.32), badge.fontsize, "c") - font2:SetTextColor(0.92, 0.92, 0.92, 1) - end - end - font2:End() - end) -end local function updateComs(forceText) local area = comsArea @@ -727,6 +551,12 @@ local function drawResbarStorage(res) font2:End() end +-- The quit dialog lives in the Top Bar Buttons widget; the resource bars stop +-- reacting to hover while it is up. +local function showingQuitscreen() + return WG.topbar ~= nil and WG.topbar.showingQuit ~= nil and WG.topbar.showingQuit() ~= nil +end + local function updateResbarText(res, force) if not showResourceBars then return @@ -1055,7 +885,7 @@ local function updateResbar(res) local glowSize = barHeight * 7 local edgeWidth = mathMax(1, mathFloor(vsy / 1100)) - if not showQuitscreen and resbarHover and resbarHover == res then + if not showingQuitscreen() and resbarHover and resbarHover == res then sliderHeightAdd = barHeight / 0.75 shareSliderWidth = barHeight + sliderHeightAdd + sliderHeightAdd end @@ -1669,10 +1499,10 @@ function init() updateComs() end - -- buttons + -- the button strip is drawn by the Top Bar Buttons widget; this is the width it + -- reserves on the right, which GetFreeArea and GetPosition below report width = mathFloor(totalWidth / 4) - buttonsArea = { topbarArea[3] - width, topbarArea[2] + smallVPad, topbarArea[3], topbarArea[4] } - updateButtons() + local buttonsBottom = topbarArea[2] + smallVPad if WG.topbar then WG.topbar.GetPosition = function() @@ -1683,7 +1513,7 @@ function init() topbarArea[3], topbarArea[4], widgetScale, - buttonsArea[2], + buttonsBottom, } end @@ -1758,7 +1588,6 @@ local function countComs(forceUpdate) end function widget:GameStart() - gameStarted = true checkSelfStatus() if displayComCounter then countComs(true) @@ -1835,13 +1664,6 @@ local function updateAllyTeamOverflowing() end local function hoveringElement(x, y) - -- only the buttons are drawn while a window is open, so only they can be hovered - if cache.modalActive then - if buttonsArea[1] and mathIsInRect(x, y, buttonsArea[1], buttonsArea[2], buttonsArea[3], buttonsArea[4]) then - return "menu" - end - return false - end if resbarArea.metal[1] and mathIsInRect(x, y, resbarArea.metal[1], resbarArea.metal[2], resbarArea.metal[3], resbarArea.metal[4]) @@ -1867,15 +1689,58 @@ local function hoveringElement(x, y) if displayComCounter and comsArea[1] and mathIsInRect(x, y, comsArea[1], comsArea[2], comsArea[3], comsArea[4]) then return "com" end - if buttonsArea[1] and mathIsInRect(x, y, buttonsArea[1], buttonsArea[2], buttonsArea[3], buttonsArea[4]) then - return "menu" + return false +end + +-- WG.topbar is shared with the Top Bar Buttons widget. Consumers guard on the table, +-- not on the individual functions, so when that widget is not running its half of the +-- API has to answer "there are no buttons" rather than be missing. Checked from Update, +-- which runs after every widget's Initialize and again if it is disabled mid-game. +local fallback = {} + +local function provide(name, fn) + local current = WG.topbar[name] + if current == nil then + fallback[name] = fn + WG.topbar[name] = fn + elseif fallback[name] ~= nil and current ~= fallback[name] then + fallback[name] = nil -- the buttons widget took over end +end +local function returnNil() end +local function returnFalse() return false end +local function provideButtonFallbacks() + if not WG.topbar then + return + end + provide("showingQuit", returnNil) + provide("hideWindows", returnNil) + provide("buttonAt", returnNil) + provide("GetButtonsPosition", returnNil) + provide("setAutoHideButtons", returnNil) + provide("getAutoHideButtons", returnFalse) + provide("getShowButtons", returnFalse) +end + +local function removeButtonFallbacks() + if not WG.topbar then + return + end + for name, fn in pairs(fallback) do + if WG.topbar[name] == fn then + WG.topbar[name] = nil + end + end +end + function widget:Update(dt) now = osClock() + + provideButtonFallbacks() timers.deferResourceUpdate = timers.gameFrameHappened and now - timers.lastUpdateTime < (1 / 30) timers.gameFrameHappened = false timers.lastUpdateTime = now @@ -1929,19 +1794,11 @@ function widget:Update(dt) elseif not guishaderActive and guishaderEnabled then guishaderEnabled = guishaderActive end - - -- the gameinfo widget loads after the topbar, so its button gets added once it is there - local gameinfoActive = WG.gameinfo ~= nil and not isSinglePlayer - if gameinfoActive ~= gameinfoEnabled then - gameinfoEnabled = gameinfoActive - refreshUi = true - updateButtons() - end end if now > timers.nextResBarUpdate then timers.nextResBarUpdate = now + 0.05 - if not spec and not showQuitscreen then + if not spec and not showingQuitscreen() then if hoveringTopbar == "energy" then if not resbarHover then resbarHover = "energy" @@ -2252,341 +2109,9 @@ local function drawResBars() end end -local function drawQuitScreen() - local fadeTime = 0.2 - local fadeProgress = (now - showQuitscreen) / fadeTime - if fadeProgress > 1 then - fadeProgress = 1 - end - - Spring.SetMouseCursor("cursornormal") - - dlist.quit = glCreateList(function() - if WG.guishader then - glColor(0, 0, 0, (0.18 * fadeProgress)) - else - glColor(0, 0, 0, (0.35 * fadeProgress)) - end - - gl.Rect(0, 0, vsx, vsy) - - if not hideQuitWindow then - -- when terminating spring, keep the faded screen - - local w = mathFloor(320 * widgetScale) - local h = mathFloor(w / 3.5) - - local fontSize = h / 6 - local text = BAR.I18N("ui.topbar.quit.reallyQuit") - teamResign = false - - if not spec then - text = BAR.I18N("ui.topbar.quit.reallyQuitResign") - if not gameIsOver and chobbyLoaded then - if numPlayers < 3 then - text = BAR.I18N("ui.topbar.quit.reallyResign") - else - if getPlayerLiveAllyCount() >= 1 then - teamResign = true - end - text = BAR.I18N("ui.topbar.quit.reallyResignSpectate") - end - end - end - - local padding = mathFloor(w / 90) - local textTopPadding = padding + padding + padding + padding + padding + fontSize - local txtWidth = font:GetTextWidth(text) * fontSize - w = mathMax(w, txtWidth + textTopPadding + textTopPadding) - - local x = mathFloor((vsx / 2) - (w / 2)) - local y = mathFloor((vsy / 1.8) - (h / 2)) - local maxButtons = teamResign and 5 or 4 - local buttonMargin = mathFloor(h / 9) - local buttonWidth = mathFloor((w - buttonMargin * maxButtons) / (maxButtons - 1)) -- maxButtons+1 margins for maxButtons buttons - local buttonHeight = mathFloor(h * 0.30) - - quitscreenArea = { x, y, x + w, y + h } - - if teamResign then - quitscreenArea[2] = quitscreenArea[2] - mathFloor(fontSize * 1.7) - end - - quitscreenStayArea = { - x + buttonMargin + 0 * (buttonWidth + buttonMargin), - y + buttonMargin, - x + buttonMargin + 0 * (buttonWidth + buttonMargin) + buttonWidth, - y + buttonMargin + buttonHeight, - } - quitscreenResignArea = { - x + buttonMargin + 1 * (buttonWidth + buttonMargin), - y + buttonMargin, - x + buttonMargin + 1 * (buttonWidth + buttonMargin) + buttonWidth, - y + buttonMargin + buttonHeight, - } - local nextButton = 2 - if teamResign then - quitscreenTeamResignArea = { - x + buttonMargin + nextButton * (buttonWidth + buttonMargin), - y + buttonMargin, - x + buttonMargin + nextButton * (buttonWidth + buttonMargin) + buttonWidth, - y + buttonMargin + buttonHeight, - } - nextButton = nextButton + 1 - end - quitscreenQuitArea = { - x + buttonMargin + nextButton * (buttonWidth + buttonMargin), - y + buttonMargin, - x + buttonMargin + nextButton * (buttonWidth + buttonMargin) + buttonWidth, - y + buttonMargin + buttonHeight, - } - - -- window - UiElement( - quitscreenArea[1], - quitscreenArea[2], - quitscreenArea[3], - quitscreenArea[4], - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - nil, - { 1, 1, 1, 0.6 + (0.34 * fadeProgress) }, - { 0.45, 0.45, 0.4, 0.025 + (0.025 * fadeProgress) }, - nil - ) - local color1, color2 - - font:Begin(true) - font:SetTextColor(0, 0, 0, 1) - font:Print( - text, - quitscreenArea[1] + ((quitscreenArea[3] - quitscreenArea[1]) / 2), - quitscreenArea[4] - textTopPadding, - fontSize, - "cn" - ) - font:End() - - font2:Begin(true) - font2:SetTextColor(1, 1, 1, 1) - font2:SetOutlineColor(0, 0, 0, 0.23) - - fontSize = fontSize * 0.92 - - -- stay button - if gameIsOver or not chobbyLoaded then - if - mathIsInRect( - mx, - my, - quitscreenStayArea[1], - quitscreenStayArea[2], - quitscreenStayArea[3], - quitscreenStayArea[4] - ) - then - color1 = { 0, 0.4, 0, 0.4 + (0.5 * fadeProgress) } - color2 = { 0.05, 0.6, 0.05, 0.4 + (0.5 * fadeProgress) } - else - color1 = { 0, 0.25, 0, 0.35 + (0.5 * fadeProgress) } - color2 = { 0, 0.5, 0, 0.35 + (0.5 * fadeProgress) } - end - UiButton( - quitscreenStayArea[1], - quitscreenStayArea[2], - quitscreenStayArea[3], - quitscreenStayArea[4], - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - nil, - color1, - color2, - padding * 0.5 - ) - font2:Print( - BAR.I18N("ui.topbar.quit.stay"), - quitscreenStayArea[1] + ((quitscreenStayArea[3] - quitscreenStayArea[1]) / 2), - quitscreenStayArea[2] + ((quitscreenStayArea[4] - quitscreenStayArea[2]) / 2) - (fontSize / 3), - fontSize, - "con" - ) - end - - -- resign button - if not spec and not gameIsOver then - local mouseOver = false - if - mathIsInRect( - mx, - my, - quitscreenResignArea[1], - quitscreenResignArea[2], - quitscreenResignArea[3], - quitscreenResignArea[4] - ) - then - color1 = { 0.4, 0, 0, 0.4 + (0.5 * fadeProgress) } - color2 = { 0.6, 0.05, 0.05, 0.4 + (0.5 * fadeProgress) } - mouseOver = "resign" - else - color1 = { 0.25, 0, 0, 0.35 + (0.5 * fadeProgress) } - color2 = { 0.5, 0, 0, 0.35 + (0.5 * fadeProgress) } - end - UiButton( - quitscreenResignArea[1], - quitscreenResignArea[2], - quitscreenResignArea[3], - quitscreenResignArea[4], - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - nil, - color1, - color2, - padding * 0.5 - ) - font2:Print( - BAR.I18N("ui.topbar.quit.resign"), - quitscreenResignArea[1] + ((quitscreenResignArea[3] - quitscreenResignArea[1]) / 2), - quitscreenResignArea[2] + ((quitscreenResignArea[4] - quitscreenResignArea[2]) / 2) - (fontSize / 3), - fontSize, - "con" - ) - - if teamResign then - if - mathIsInRect( - mx, - my, - quitscreenTeamResignArea[1], - quitscreenTeamResignArea[2], - quitscreenTeamResignArea[3], - quitscreenTeamResignArea[4] - ) - then - color1 = { 0.28, 0.28, 0.28, 0.4 + (0.5 * fadeProgress) } - color2 = { 0.45, 0.45, 0.45, 0.4 + (0.5 * fadeProgress) } - mouseOver = "teamResign" - else - color1 = { 0.18, 0.18, 0.18, 0.4 + (0.5 * fadeProgress) } - color2 = { 0.33, 0.33, 0.33, 0.4 + (0.5 * fadeProgress) } - end - UiButton( - quitscreenTeamResignArea[1], - quitscreenTeamResignArea[2], - quitscreenTeamResignArea[3], - quitscreenTeamResignArea[4], - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - nil, - color1, - color2, - padding * 0.5 - ) - font2:Print( - BAR.I18N("ui.topbar.quit.teamResign"), - quitscreenTeamResignArea[1] + ((quitscreenTeamResignArea[3] - quitscreenTeamResignArea[1]) / 2), - quitscreenTeamResignArea[2] - + ((quitscreenTeamResignArea[4] - quitscreenTeamResignArea[2]) / 2) - - (fontSize / 3), - fontSize, - "con" - ) - end - if mouseOver and teamResign then - font:Print( - BAR.I18N("ui.topbar.hint." .. mouseOver), - quitscreenTeamResignArea[1] - buttonMargin, - quitscreenArea[2] + (2.5 * fontSize / 3), - fontSize * 0.9, - "cn" - ) - end - end - - -- quit button - if gameIsOver or not chobbyLoaded then - if - mathIsInRect( - mx, - my, - quitscreenQuitArea[1], - quitscreenQuitArea[2], - quitscreenQuitArea[3], - quitscreenQuitArea[4] - ) - then - color1 = { 0.4, 0, 0, 0.4 + (0.5 * fadeProgress) } - color2 = { 0.6, 0.05, 0.05, 0.4 + (0.5 * fadeProgress) } - else - color1 = { 0.25, 0, 0, 0.35 + (0.5 * fadeProgress) } - color2 = { 0.5, 0, 0, 0.35 + (0.5 * fadeProgress) } - end - UiButton( - quitscreenQuitArea[1], - quitscreenQuitArea[2], - quitscreenQuitArea[3], - quitscreenQuitArea[4], - 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, - nil, - color1, - color2, - padding * 0.5 - ) - font2:Print( - BAR.I18N("ui.topbar.quit.quit"), - quitscreenQuitArea[1] + ((quitscreenQuitArea[3] - quitscreenQuitArea[1]) / 2), - quitscreenQuitArea[2] + ((quitscreenQuitArea[4] - quitscreenQuitArea[2]) / 2) - (fontSize / 3), - fontSize, - "con" - ) - end - - font2:End() - end - end) - - -- background - if WG.guishader then - WG.guishader.setScreenBlur(true) - WG.guishader.insertRenderDlist(dlist.quit) - else - glCallList(dlist.quit) - end -end local function drawUiBackground() - if showResourceBars and not cache.modalActive then + if showResourceBars then if resbarArea.energy[1] then local energySkew = cfg.useSkew and { brx = -((resbarArea.energy[4] - resbarArea.energy[2]) * skewTan) } or nil @@ -2635,7 +2160,7 @@ local function drawUiBackground() ) end end - if cache.showIndicators and not cache.modalActive and comsArea[1] then + if cache.showIndicators and comsArea[1] then local H = comsArea[4] - comsArea[2] local smallSkew = cfg.useSkew and { blx = -(H * skewTan), brx = -(H * skewTan) } or nil UiElement( @@ -2659,7 +2184,7 @@ local function drawUiBackground() smallSkew ) end - if cache.showIndicators and not cache.modalActive and windArea[1] then + if cache.showIndicators and windArea[1] then local H = windArea[4] - windArea[2] local smallSkew = cfg.useSkew and { blx = -(H * skewTan), brx = -(H * skewTan) } or nil UiElement( @@ -2683,7 +2208,7 @@ local function drawUiBackground() smallSkew ) end - if cache.showIndicators and not cache.modalActive and displayTidalSpeed and tidalarea[1] then + if cache.showIndicators and displayTidalSpeed and tidalarea[1] then local H = tidalarea[4] - tidalarea[2] local smallSkew = cfg.useSkew and { blx = -(H * skewTan), brx = -(H * skewTan) } or nil UiElement( @@ -2707,33 +2232,10 @@ local function drawUiBackground() smallSkew ) end - if showButtons and buttonsArea[1] then - UiElement( - buttonsArea[1], - buttonsArea[2], - buttonsArea[3], - buttonsArea[4], - 0, - 0, - 0, - 1, - nil, - nil, - nil, - nil, - nil, - nil, - nil, - nil - ) - end end local function drawUi() - if showButtons and dlist.buttons then - glCallList(dlist.buttons) - end - if showResourceBars and not cache.modalActive and dlist.resbar.energy and dlist.resbar.energy[1] then + if showResourceBars and dlist.resbar.energy and dlist.resbar.energy[1] then glCallList(dlist.resbar.energy[1]) glCallList(dlist.resbar.metal[1]) end @@ -2742,7 +2244,7 @@ local function drawUi() local windH = windArea[4] - windArea[2] local fontsize = windH / 3.2 local windSkewCX = windArea[1] + ((windArea[3] - windArea[1]) / 2) - (cfg.useSkew and windH * skewTan * 0.5 or 0) - if cache.showIndicators and not cache.modalActive and noWind then + if cache.showIndicators and noWind then font2:Begin(true) --font2:Print("\255\200\200\200no wind", windSkewCX, windArea[2] + ((windArea[4] - windArea[2]) / 2.05) - (fontsize / 5), fontsize, 'oc') -- Wind speed text font2:Print( @@ -2763,7 +2265,7 @@ local function drawUi() end -- tidal speed - if cache.showIndicators and not cache.modalActive and displayTidalSpeed then + if cache.showIndicators and displayTidalSpeed then local fontSize = (tidalarea[4] - tidalarea[2]) / 2.3 local skewCenterOffset = cfg.useSkew and (tidalarea[4] - tidalarea[2]) * skewTan * 0.5 or 0 font2:Begin(true) @@ -2850,19 +2352,6 @@ function widget:DrawScreen() sp.SetMouseCursor("cursornormal") end - if showButtons ~= cache.prevShowButtons then - cache.prevShowButtons = showButtons - refreshUi = true - end - - -- Read here rather than in Update: the handler recomputes the modal state at the top - -- of its own DrawScreen, so this sees it in the same frame and rebakes right away. - local modal = widgetHandler:IsModalActive() - if modal ~= cache.modalActive then - cache.modalActive = modal - refreshUi = true - end - if refreshUi then if uiBgTex then gl.DeleteTexture(uiBgTex) @@ -2938,7 +2427,7 @@ function widget:DrawScreen() glCallList(dlist.blendBg) end - if cache.showIndicators and not cache.modalActive and dlist.wind1 then + if cache.showIndicators and dlist.wind1 then glPushMatrix() glCallList(dlist.wind1) glRotate(windRotation, 0, 0, 1) @@ -2946,7 +2435,7 @@ function widget:DrawScreen() glPopMatrix() end - if cache.showIndicators and not cache.modalActive and displayTidalSpeed and dlist.tidal2 then + if cache.showIndicators and displayTidalSpeed and dlist.tidal2 then local tidalSkewCX = tidalarea[1] + ((tidalarea[3] - tidalarea[1]) / 2) - (cfg.useSkew and (tidalarea[4] - tidalarea[2]) * skewTan * 0.5 or 0) @@ -2969,7 +2458,6 @@ function widget:DrawScreen() -- cleared since drawResbarStorage skips drawing while the warning is showing. if uiTex - and not cache.modalActive and ( (showingWarning.metal and not cache.warningCleared.metal) or (showingWarning.energy and not cache.warningCleared.energy) @@ -3008,7 +2496,7 @@ function widget:DrawScreen() end -- current wind - if cache.showIndicators and not cache.modalActive and not noWind then + if cache.showIndicators and not noWind then if currentWind ~= prevWind or refreshUi then prevWind = currentWind windTextScissor[1] = windArea[1] - topbarArea[1] @@ -3020,12 +2508,10 @@ function widget:DrawScreen() end end - if not cache.modalActive then - drawResBars() - end + drawResBars() glPushMatrix() - if cache.showIndicators and not cache.modalActive and displayComCounter and dlist.coms then + if cache.showIndicators and displayComCounter and dlist.coms then -- commander counter if refreshUi @@ -3044,116 +2530,7 @@ function widget:DrawScreen() end end - if autoHideButtons then - if buttonsArea[1] and hoveringTopbar == "menu" then - if not showButtons then - showButtons = true - end - elseif showButtons then - showButtons = false - end - end - if showButtons and dlist.buttons and buttonsArea.buttons then - -- changelog changes highlight - if WG.changelog and WG.changelog.haschanges() then - local button = "changelog" - if buttonsArea.buttons[button] then - local paddingsize = 1 - RectRound( - buttonsArea.buttons[button][1] + paddingsize, - buttonsArea.buttons[button][2] + paddingsize, - buttonsArea.buttons[button][3] - paddingsize, - buttonsArea.buttons[button][4] - paddingsize, - 3.5 * widgetScale, - 0, - 0, - 0, - button == firstButton and 1 or 0, - { 1, 1, 1, 0.1 * blinkProgress } - ) - end - end - - -- hovered? - if not showQuitscreen and buttonsArea.buttons and hoveringTopbar == "menu" then - for button, pos in pairs(buttonsArea.buttons) do - if mathIsInRect(mx, my, pos[1], pos[2], pos[3], pos[4]) then - local paddingsize = 1 - RectRound( - buttonsArea.buttons[button][1] + paddingsize, - buttonsArea.buttons[button][2] + paddingsize, - buttonsArea.buttons[button][3] - paddingsize, - buttonsArea.buttons[button][4] - paddingsize, - 3.5 * widgetScale, - 0, - 0, - 0, - button == firstButton and 1 or 0, - { 0, 0, 0, 0.06 } - ) - glBlending(GL.SRC_ALPHA, GL.ONE) - local mb = buttonsArea.buttons[button][9] - RectRound( - buttonsArea.buttons[button][1], - buttonsArea.buttons[button][2], - buttonsArea.buttons[button][3], - buttonsArea.buttons[button][4], - 3.5 * widgetScale, - 0, - 0, - 0, - button == firstButton and 1 or 0, - { 1, 1, 1, mb and 0.13 or 0.03 }, - { 0.44, 0.44, 0.44, mb and 0.4 or 0.2 } - ) - local mult = 1 - RectRound( - buttonsArea.buttons[button][1], - buttonsArea.buttons[button][4] - - ((buttonsArea.buttons[button][4] - buttonsArea.buttons[button][2]) * 0.4), - buttonsArea.buttons[button][3], - buttonsArea.buttons[button][4], - 3.3 * widgetScale, - 0, - 0, - 0, - 0, - { 1, 1, 1, 0 }, - { 1, 1, 1, 0.18 * mult } - ) - RectRound( - buttonsArea.buttons[button][1], - buttonsArea.buttons[button][2], - buttonsArea.buttons[button][3], - buttonsArea.buttons[button][2] - + ((buttonsArea.buttons[button][4] - buttonsArea.buttons[button][2]) * 0.25), - 3.3 * widgetScale, - 0, - 0, - 0, - button == firstButton and 1 or 0, - { 1, 1, 1, 0.045 * mult }, - { 1, 1, 1, 0 } - ) - glBlending(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA) - break - end - end - end - end - - if dlist.quit then - if WG.guishader then - WG.guishader.removeRenderDlist(dlist.quit) - end - glDeleteList(dlist.quit) - dlist.quit = nil - end - - if showQuitscreen then - drawQuitScreen() - end glColor(1, 1, 1, 1) glPopMatrix() @@ -3198,272 +2575,13 @@ function widget:MouseMove(x, y) adjustSliders(x, y) end --- Second return value: the window refused to close (the keybind editor raises a guard --- when there are unsaved edits) and is still up. -local function closeWindow(name) - if WG[name] ~= nil and WG[name].isvisible() then - WG[name].toggle(false) - return true, WG[name].isvisible() == true - end - return false, false -end - -local function hideWindows() - local closedWindow = false - local stillOpen = false - - local function hide(name) - local closed, blocked = closeWindow(name) - closedWindow = closed or closedWindow - stillOpen = blocked or stillOpen - end - - hide("options") - hide("scavengerinfo") - hide("missioninfo") - hide("keybinds") - hide("changelog") - hide("gameinfo") - hide("teamstats") - hide("widgetselector") - if showQuitscreen then - closedWindow = true - end - - showQuitscreen = nil - - if WG.guishader then - WG.guishader.setScreenBlur(false) - end - - if gameIsOver then -- Graphs window can only be open after game end - -- Closing Graphs window if open, no way to tell if it was open or not - Spring.SendCommands("endgraph 0") - graphsWindowVisible = false - end - - return closedWindow, stillOpen -end - -local function toggleWindow(name) - local isvisible = false - if WG[name] ~= nil then - isvisible = WG[name].isvisible() - end - local _, stillOpen = hideWindows() - -- Opening another window on top of a window that refused to close would bury its guard. - if stillOpen then - return isvisible - end - if WG[name] ~= nil and isvisible ~= true then - WG[name].toggle() - end - return isvisible -end - --- Which menu button, if any, sits under these screen coords. --- Exposed as WG.topbar.buttonAt so the window widgets (which sit on a lower layer and --- therefore see the click first) can tell a click on the top bar apart from a click that --- dismisses them: closing themselves there would eat the click meant to open another window. -local function buttonAt(x, y) - if not buttonsArea.buttons then - return nil - end - for name, pos in pairs(buttonsArea.buttons) do - if mathIsInRect(x, y, pos[1], pos[2], pos[3], pos[4]) then - return name - end - end - return nil -end - -local function applyButtonAction(button) - if playSounds then - Spring.PlaySoundFile(leftclick, 0.8, "ui") - end - - local isvisible = false - if button == "quit" or button == "resign" then - if not gameIsOver and chobbyLoaded and button == "quit" then - Spring.SendLuaMenuMsg("showLobby") - else - local oldShowQuitscreen - if showQuitscreen then - oldShowQuitscreen = showQuitscreen - isvisible = true - end - - hideWindows() - - if oldShowQuitscreen then - if isvisible ~= true then - showQuitscreen = oldShowQuitscreen - if WG.guishader then - WG.guishader.setScreenBlur(true) - end - end - else - showQuitscreen = now - end - end - elseif button == "options" then - toggleWindow("options") - elseif button == "save" then - hideWindows() - if isSinglePlayer and cfg.allowSavegame and WG.savegame then - local time = os.date("%Y%m%d_%H%M%S") - Spring.SendCommands("savegame " .. time) - end - elseif button == "scavengers" then - toggleWindow("scavengerinfo") - elseif button == "mission" then - toggleWindow("missioninfo") - elseif button == "keybinds" then - toggleWindow("keybinds") - elseif button == "changelog" then - toggleWindow("changelog") - elseif button == "stats" then - toggleWindow("teamstats") - elseif button == "info" then - toggleWindow("gameinfo") - elseif button == "graphs" then - isvisible = graphsWindowVisible - hideWindows() - if gameIsOver and not isvisible then - Spring.SendCommands("endgraph 2") - graphsWindowVisible = true - end - end -end - -function widget:GameOver() - refreshUi = true - gameIsOver = true - updateButtons() -end -function widget:MouseWheel(up, value) -- up = true/false , value = -1/1 - if showQuitscreen and quitscreenArea then - return true - end -end -function widget:KeyPress(key) - if key == 27 then -- ESC - if not WG.options or (WG.options.disallowEsc and not WG.options.disallowEsc()) then - local escDidSomething = hideWindows() - if cfg.escapeKeyPressesQuit and not escDidSomething then - applyButtonAction("quit") - end - end - end - if showQuitscreen and quitscreenArea then - return true - end -end function widget:MousePress(x, y, button) if button == 1 then - if showQuitscreen and quitscreenArea then - if mathIsInRect(x, y, quitscreenArea[1], quitscreenArea[2], quitscreenArea[3], quitscreenArea[4]) then - if - (gameIsOver or not chobbyLoaded or not spec) - and mathIsInRect( - x, - y, - quitscreenStayArea[1], - quitscreenStayArea[2], - quitscreenStayArea[3], - quitscreenStayArea[4] - ) - then - if playSounds then - Spring.PlaySoundFile(leftclick, 0.75, "ui") - end - - showQuitscreen = nil - if WG.guishader then - WG.guishader.setScreenBlur(false) - end - end - if - (gameIsOver or not chobbyLoaded) - and mathIsInRect( - x, - y, - quitscreenQuitArea[1], - quitscreenQuitArea[2], - quitscreenQuitArea[3], - quitscreenQuitArea[4] - ) - then - if playSounds then - Spring.PlaySoundFile(leftclick, 0.75, "ui") - end - - if not chobbyLoaded then - Spring.SendCommands("QuitForce") -- Exit the game completely - else - Spring.SendCommands("ReloadForce") -- Exit to the lobby - end - showQuitscreen = nil - hideQuitWindow = now - end - if - not spec - and not gameIsOver - and mathIsInRect( - x, - y, - quitscreenResignArea[1], - quitscreenResignArea[2], - quitscreenResignArea[3], - quitscreenResignArea[4] - ) - then - if playSounds then - Spring.PlaySoundFile(leftclick, 0.75, "ui") - end - Spring.SendCommands("spectator") - showQuitscreen = nil - if WG.guishader then - WG.guishader.setScreenBlur(false) - end - end - if - not spec - and not gameIsOver - and teamResign - and mathIsInRect( - x, - y, - quitscreenTeamResignArea[1], - quitscreenTeamResignArea[2], - quitscreenTeamResignArea[3], - quitscreenTeamResignArea[4] - ) - then - if playSounds then - Spring.PlaySoundFile(leftclick, 0.75, "ui") - end - Spring.SendCommands("say !cv resign") - showQuitscreen = nil - if WG.guishader then - WG.guishader.setScreenBlur(false) - end - end - else - showQuitscreen = nil - if WG.guishader then - WG.guishader.setScreenBlur(false) - end - end - return true - end - - -- the bars aren't drawn while a window is open, so their sliders aren't there to grab - if not spec and not cache.modalActive then + if not spec then if not isSingle then if mathIsInRect( @@ -3514,26 +2632,12 @@ function widget:MousePress(x, y, button) end end - local clickedButton = buttonAt(x, y) - if clickedButton then - applyButtonAction(clickedButton) - return true - end - else - if showQuitscreen and quitscreenArea then - return true - end - end if hoveringTopbar then return true end end -function widget:MouseRelease(x, y, button) - if showQuitscreen and quitscreenArea then - return true - end if draggingShareIndicator then adjustSliders(x, y) @@ -3624,11 +2728,6 @@ function widget:Initialize() end end - if Spring.GetMenuName and string.find(string.lower(Spring.GetMenuName()), "chobby") then - chobbyLoaded = true - Spring.SendLuaMenuMsg("disableLobbyButton") - end - if not spec then local teamList = Spring.GetTeamList(myAllyTeamID) or {} isSingle = #teamList == 1 @@ -3641,41 +2740,9 @@ function widget:Initialize() end end - -- The quit/resign dialog counts as a window: while it is up the handler hides the - -- rest of the interface, and this widget draws only its menu buttons. - -- (this widget holds the real widgetHandler, so it passes itself) - widgetHandler:RegisterModalWindow(widget, function() - return showQuitscreen ~= nil - end) - - WG.topbar = {} - - WG.topbar.showingQuit = function() - return showQuitscreen - end - - WG.topbar.hideWindows = function() - hideWindows() - end - - WG.topbar.buttonAt = function(x, y) - return buttonAt(x, y) - end - - WG.topbar.setAutoHideButtons = function(value) - refreshUi = true - autoHideButtons = value - showButtons = not value - updateButtons() - end - - WG.topbar.getAutoHideButtons = function() - return autoHideButtons - end - - WG.topbar.getShowButtons = function() - return showButtons - end + -- Shared with the Top Bar Buttons widget, which fills in the button and quit + -- dialog half of this API. Either widget can run without the other. + WG.topbar = WG.topbar or {} WG.topbar.updateTopBarEnergy = function(value) draggingConversionIndicatorValue = value @@ -3746,13 +2813,11 @@ end function widget:Shutdown() --Spring.SendCommands("resbar 1") - if dlist.buttons then + if dlist.blendBg then dlist.wind1 = glDeleteList(dlist.wind1) dlist.wind2 = glDeleteList(dlist.wind2) dlist.tidal2 = glDeleteList(dlist.tidal2) dlist.coms = glDeleteList(dlist.coms) - dlist.buttons = glDeleteList(dlist.buttons) - dlist.quit = glDeleteList(dlist.quit) dlist.blendBg = glDeleteList(dlist.blendBg) dlist.blendUi = glDeleteList(dlist.blendUi) @@ -3813,15 +2878,20 @@ function widget:Shutdown() WG.tooltip.RemoveTooltip(res .. "_current") end - WG.topbar = nil -end - -function widget:GetConfigData() - return { autoHideButtons = autoHideButtons } -end - -function widget:SetConfigData(data) - if data.autoHideButtons then - autoHideButtons = data.autoHideButtons + removeButtonFallbacks() + if WG.topbar then + WG.topbar.GetPosition = nil + WG.topbar.GetFreeArea = nil + WG.topbar.GetSkewConfig = nil + WG.topbar.updateTopBarEnergy = nil + WG.topbar.setResourceBarsVisible = nil + WG.topbar.getResourceBarsVisible = nil + WG.topbar.setIndicatorsVisible = nil + WG.topbar.getIndicatorsVisible = nil + -- the Top Bar Buttons widget may still be using the table for its own half + if next(WG.topbar) == nil then + WG.topbar = nil + end end end + diff --git a/luaui/Widgets/gui_top_bar_buttons.lua b/luaui/Widgets/gui_top_bar_buttons.lua new file mode 100644 index 00000000000..7fb3c41a48c --- /dev/null +++ b/luaui/Widgets/gui_top_bar_buttons.lua @@ -0,0 +1,1351 @@ +local widget = widget ---@type Widget + +function widget:GetInfo() + return { + name = "Top Bar Buttons", + desc = "The menu buttons at the top right, and the quit/resign dialog.", + author = "Floris", + date = "Feb, 2017", + license = "GNU GPL, v2 or later", + layer = -95000, + enabled = true, + handler = true, --can use widgetHandler:x() + modalExempt = true, -- these buttons are how the player switches and closes windows + } +end + +-- Split out of the Top Bar widget: that one owns the resource bars, wind, tidal and +-- commander counter, this one owns the button strip in the top right corner and the +-- quit/resign dialog behind its quit button. The two share the WG.topbar table, each +-- filling in its own half of the API, so either can run without the other. + +-- Localized functions for performance +local mathFloor = math.floor +local mathMax = math.max +local mathIsInRect = math.isInRect + +-- System +local spec = Spring.GetSpectatingState() +local myTeamID = Spring.GetLocalTeamID() +local myAllyTeamID = Spring.GetLocalAllyTeamID() +local myAllyTeamList = Spring.GetTeamList(myAllyTeamID) + +-- Game mode / state +local numPlayers = BAR.Utilities.GetPlayerCount() +local isSinglePlayer = BAR.Utilities.Gametype.IsSinglePlayer() +local _modOpts = Spring.GetModOptions() +local isScenario = _modOpts ~= nil and _modOpts.scenariooptions ~= nil +local chobbyLoaded = false +local gameStarted = (Spring.GetGameFrame() > 0) +local gameIsOver = false +local graphsWindowVisible = false + +-- Configuration. The geometry values mirror the Top Bar's, so the strip lines up with +-- the bar whether or not that widget is running. +local cfg = { + relXpos = 0.3, + borderPadding = 5, + escapeKeyPressesQuit = false, + allowSavegame = true, + useSkew = true, + smallElementHeightFraction = 0.85, -- buttons height as fraction of the top bar +} + +-- OpenGL +local glCreateList = gl.CreateList +local glCallList = gl.CallList +local glDeleteList = gl.DeleteList +local glBlending = gl.Blending +local glColor = gl.Color + +-- UI elements +local topbarArea = {} +local buttonsArea = {} + +-- UI state +local orgHeight = 46 +local ui_scale = tonumber(Spring.GetConfigFloat("ui_scale", 1)) or 1 +local height = orgHeight * (1 + (ui_scale - 1) / 1.7) +local vsx, vsy = Spring.GetViewGeometry() +local widgetScale = (0.80 + (vsx * vsy / 6000000)) +local xPos = mathFloor(vsx * cfg.relXpos) +local mx, my = -1, -1 +local showButtons = true +local autoHideButtons = false +local hoveringButtons = false +local configLoaded = false +local refreshButtons = true +---@type number +local bgpadding +---@type fun(...) +local RectRound +---@type fun(...) +local UiElement +---@type fun(...) +local UiButton +---@type fun(...) +local RectRoundCircle +---@type any +local font +---@type any +local font2 + +-- Display lists +-- buttons: the baked button labels and badges +-- background: the strip's panel chrome, also handed to the blur +-- quit: the quit/resign dialog, rebuilt every frame while it is up +---@type table +local dlist = {} + +-- Interactions +---@type any +local quitscreenArea +---@type any +local quitscreenStayArea +---@type any +local quitscreenQuitArea +---@type any +local quitscreenResignArea +---@type any +local quitscreenTeamResignArea +---@type any +local showQuitscreen +---@type any +local hideQuitWindow +---@type boolean +local teamResign = false + +-- Audio +local playSounds = true +local leftclick = "LuaUI/Sounds/tock.wav" + +-- Timers +local osClock = os.clock +local now = osClock() +local nextStateCheck = 0 +local nextButtonCheck = 0 +local blinkDirection = true +local blinkProgress = 0 + +-------------------------------------------------------------------------------- +local function getPlayerLiveAllyCount() + local nAllies = 0 + for _, teamID in ipairs(myAllyTeamList) do + if teamID ~= myTeamID then + local _, _, isDead, hasAI = Spring.GetTeamInfo(teamID, false) + if not isDead and not hasAI then + nAllies = nAllies + 1 + end + end + end + return nAllies +end +local function updateButtons() + local fontsize = (height * widgetScale) / 3 + + -- if not buttonsArea['buttons'] then -- With this condition it doesn't actually update buttons if they were already added + buttonsArea.buttons = {} + + local margin = bgpadding + local textPadding = mathFloor(fontsize * 0.8) + local sidePadding = textPadding + local offset = sidePadding + local lastbutton + local badgeMinRadius = fontsize * 0.47 + local badgeFontsize = fontsize * 0.69 + + -- badge: optional number shown in a circle at the bottom right of the button text + local function addButton(name, text, badge) + local textWidth = font2:GetTextWidth(text) * fontsize + -- the circle grows along with the amount of characters the number has + local badgeRadius = 0 + if badge then + local badgeTextWidth = font2:GetTextWidth(badge) * badgeFontsize + badgeRadius = mathMax(badgeMinRadius, (badgeTextWidth / 2) + (fontsize * 0.25)) + end + local badgeWidth = badgeRadius * 2 + local width = mathFloor(textWidth + badgeWidth + textPadding) + local textCenter = buttonsArea[3] - offset - (width / 2) - (badgeWidth / 2) + buttonsArea.buttons[name] = { + buttonsArea[3] - offset - width, + buttonsArea[2] + margin, + buttonsArea[3] - offset, + buttonsArea[4], + text, + textCenter, + } + if badge then + local button = buttonsArea.buttons[name] + button[7] = { + text = badge, + x = textCenter + (textWidth / 2) + badgeRadius + (1.5 * widgetScale), + -- slightly below the center of the button text, so it sits at its bottom right + y = button[2] + ((button[4] - button[2]) * 0.5) - (fontsize / 5) + (fontsize * 0.15), + radius = badgeRadius, + fontsize = badgeFontsize, + } + end + if not lastbutton then + buttonsArea.buttons[name][3] = buttonsArea[3] + end + offset = mathFloor(offset + width + 0.5) + lastbutton = name + end + + if not gameIsOver and chobbyLoaded then + addButton("quit", BAR.I18N("ui.topbar.button.lobby")) + else + addButton("quit", BAR.I18N("ui.topbar.button.quit")) + end + if not gameIsOver and not spec and gameStarted and not isSinglePlayer then + addButton("resign", BAR.I18N("ui.topbar.button.resign")) + end + + if WG.options then + addButton("options", BAR.I18N("ui.topbar.button.settings")) + end + if WG.keybinds then + addButton("keybinds", BAR.I18N("ui.topbar.button.keys")) + end + if WG.changelog and not isScenario then + addButton("changelog", BAR.I18N("ui.topbar.button.changes")) + end + if WG.teamstats and not isScenario then + addButton("stats", BAR.I18N("ui.topbar.button.stats")) + end + -- only shown when settings differ from their default, the amount of them is put in the badge + if WG.gameinfo and (not isSinglePlayer or BAR.Utilities.ShowDevUI()) then + local changedCount = WG.gameinfo.getChangedModoptionsCount and WG.gameinfo.getChangedModoptionsCount() or 0 + if changedCount > 0 then + addButton("info", BAR.I18N("ui.topbar.button.info"), tostring(changedCount)) + end + end + if gameIsOver then + addButton("graphs", BAR.I18N("ui.topbar.button.graphs")) + end + if WG.scavengerinfo then + addButton("scavengers", BAR.I18N("ui.topbar.button.scavengers")) + end + if isScenario and WG.missioninfo then + addButton("mission", BAR.I18N("ui.topbar.button.mission")) + end + if isSinglePlayer and cfg.allowSavegame and WG.savegame then + addButton("save", BAR.I18N("ui.topbar.button.save")) + end + + buttonsArea.buttons[lastbutton][1] = buttonsArea.buttons[lastbutton][1] - sidePadding + offset = offset + sidePadding + buttonsArea[1] = buttonsArea[3] - offset - margin + + if dlist.buttons then + glDeleteList(dlist.buttons) + end + dlist.buttons = glCreateList(function() + for _, params in pairs(buttonsArea.buttons) do + local badge = params[7] + if badge then + -- corner size 0.586 x radius makes it a regular octagon, which reads as a circle at this size + -- the dark circle below the smaller colored one acts as its outline + local outlineColor = { 0.18, 0.18, 0.18, 1 } + local badgeColor = { 0.66, 0.66, 0.66, 1 } + local innerRadius = badge.radius - mathMax(1, badge.radius * 0.15) + RectRoundCircle(badge.x, badge.y, badge.radius, badge.radius * 0.586, 0, outlineColor, outlineColor) + RectRoundCircle(badge.x, badge.y, innerRadius, innerRadius * 0.586, 0, badgeColor, badgeColor) + end + end + font2:Begin(true) + font2:SetTextColor(0.92, 0.92, 0.92, 1) + font2:SetOutlineColor(0, 0, 0, 1) + for name, params in pairs(buttonsArea.buttons) do + font2:Print( + params[5], + params[6], + params[2] + ((params[4] - params[2]) * 0.5) - (fontsize / 5), + fontsize, + "co" + ) + local badge = params[7] + if badge then + font2:SetTextColor(0.08, 0.08, 0.08, 1) + font2:Print(badge.text, badge.x, badge.y - (badge.fontsize * 0.32), badge.fontsize, "c") + font2:SetTextColor(0.92, 0.92, 0.92, 1) + end + end + font2:End() + end) +end + +-- The strip's panel chrome. Baked once per layout change and also handed to the blur, +-- which only reads its coverage. +local function rebuildBackground() + if dlist.background then + if WG.guishader then + WG.guishader.RemoveDlist("topbar_buttons") + end + dlist.background = glDeleteList(dlist.background) + end + if not buttonsArea[1] then + return + end + dlist.background = glCreateList(function() + -- only the bottom left corner is rounded: the other three sit against the screen edge + UiElement( + buttonsArea[1], + buttonsArea[2], + buttonsArea[3], + buttonsArea[4], + 0, + 0, + 0, + 1, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil + ) + end) + if WG.guishader then + WG.guishader.InsertDlist(dlist.background, "topbar_buttons", nil, widget) + end +end + +-- Mirrors the Top Bar's own geometry so the strip lines up with the bar, without +-- depending on that widget being loaded. +local function updateLayout() + widgetScale = (vsy / height) * 0.0425 * ui_scale + xPos = mathFloor(vsx * cfg.relXpos) + topbarArea = { mathFloor(xPos + (cfg.borderPadding * widgetScale)), mathFloor(vsy - (height * widgetScale)), vsx, vsy } + + -- Small elements (wind, tidal, coms, buttons) are top-aligned to a fraction of the + -- bar height when skew is on. + local smallVPad = cfg.useSkew + and mathFloor((topbarArea[4] - topbarArea[2]) * (1 - cfg.smallElementHeightFraction)) + or 0 + local width = mathFloor((topbarArea[3] - topbarArea[1]) / 4) + buttonsArea = { topbarArea[3] - width, topbarArea[2] + smallVPad, topbarArea[3], topbarArea[4] } + + updateButtons() + rebuildBackground() +end + +function widget:ViewResize() + vsx, vsy = gl.GetViewSizes() + + bgpadding = WG.FlowUI.elementPadding + RectRound = WG.FlowUI.Draw.RectRound + UiElement = WG.FlowUI.Draw.Element + UiButton = WG.FlowUI.Draw.Button + RectRoundCircle = WG.FlowUI.Draw.RectRoundCircle + + font = WG.fonts.getFont() + font2 = WG.fonts.getFont(2) + + updateLayout() +end + +local function drawQuitScreen() + local fadeTime = 0.2 + local fadeProgress = (now - showQuitscreen) / fadeTime + if fadeProgress > 1 then + fadeProgress = 1 + end + + Spring.SetMouseCursor("cursornormal") + + dlist.quit = glCreateList(function() + if WG.guishader then + glColor(0, 0, 0, (0.18 * fadeProgress)) + else + glColor(0, 0, 0, (0.35 * fadeProgress)) + end + + gl.Rect(0, 0, vsx, vsy) + + if not hideQuitWindow then + -- when terminating spring, keep the faded screen + + local w = mathFloor(320 * widgetScale) + local h = mathFloor(w / 3.5) + + local fontSize = h / 6 + local text = BAR.I18N("ui.topbar.quit.reallyQuit") + teamResign = false + + if not spec then + text = BAR.I18N("ui.topbar.quit.reallyQuitResign") + if not gameIsOver and chobbyLoaded then + if numPlayers < 3 then + text = BAR.I18N("ui.topbar.quit.reallyResign") + else + if getPlayerLiveAllyCount() >= 1 then + teamResign = true + end + text = BAR.I18N("ui.topbar.quit.reallyResignSpectate") + end + end + end + + local padding = mathFloor(w / 90) + local textTopPadding = padding + padding + padding + padding + padding + fontSize + local txtWidth = font:GetTextWidth(text) * fontSize + w = mathMax(w, txtWidth + textTopPadding + textTopPadding) + + local x = mathFloor((vsx / 2) - (w / 2)) + local y = mathFloor((vsy / 1.8) - (h / 2)) + local maxButtons = teamResign and 5 or 4 + local buttonMargin = mathFloor(h / 9) + local buttonWidth = mathFloor((w - buttonMargin * maxButtons) / (maxButtons - 1)) -- maxButtons+1 margins for maxButtons buttons + local buttonHeight = mathFloor(h * 0.30) + + quitscreenArea = { x, y, x + w, y + h } + + if teamResign then + quitscreenArea[2] = quitscreenArea[2] - mathFloor(fontSize * 1.7) + end + + quitscreenStayArea = { + x + buttonMargin + 0 * (buttonWidth + buttonMargin), + y + buttonMargin, + x + buttonMargin + 0 * (buttonWidth + buttonMargin) + buttonWidth, + y + buttonMargin + buttonHeight, + } + quitscreenResignArea = { + x + buttonMargin + 1 * (buttonWidth + buttonMargin), + y + buttonMargin, + x + buttonMargin + 1 * (buttonWidth + buttonMargin) + buttonWidth, + y + buttonMargin + buttonHeight, + } + local nextButton = 2 + if teamResign then + quitscreenTeamResignArea = { + x + buttonMargin + nextButton * (buttonWidth + buttonMargin), + y + buttonMargin, + x + buttonMargin + nextButton * (buttonWidth + buttonMargin) + buttonWidth, + y + buttonMargin + buttonHeight, + } + nextButton = nextButton + 1 + end + quitscreenQuitArea = { + x + buttonMargin + nextButton * (buttonWidth + buttonMargin), + y + buttonMargin, + x + buttonMargin + nextButton * (buttonWidth + buttonMargin) + buttonWidth, + y + buttonMargin + buttonHeight, + } + + -- window + UiElement( + quitscreenArea[1], + quitscreenArea[2], + quitscreenArea[3], + quitscreenArea[4], + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + nil, + { 1, 1, 1, 0.6 + (0.34 * fadeProgress) }, + { 0.45, 0.45, 0.4, 0.025 + (0.025 * fadeProgress) }, + nil + ) + local color1, color2 + + font:Begin(true) + font:SetTextColor(0, 0, 0, 1) + font:Print( + text, + quitscreenArea[1] + ((quitscreenArea[3] - quitscreenArea[1]) / 2), + quitscreenArea[4] - textTopPadding, + fontSize, + "cn" + ) + font:End() + + font2:Begin(true) + font2:SetTextColor(1, 1, 1, 1) + font2:SetOutlineColor(0, 0, 0, 0.23) + + fontSize = fontSize * 0.92 + + -- stay button + if gameIsOver or not chobbyLoaded then + if + mathIsInRect( + mx, + my, + quitscreenStayArea[1], + quitscreenStayArea[2], + quitscreenStayArea[3], + quitscreenStayArea[4] + ) + then + color1 = { 0, 0.4, 0, 0.4 + (0.5 * fadeProgress) } + color2 = { 0.05, 0.6, 0.05, 0.4 + (0.5 * fadeProgress) } + else + color1 = { 0, 0.25, 0, 0.35 + (0.5 * fadeProgress) } + color2 = { 0, 0.5, 0, 0.35 + (0.5 * fadeProgress) } + end + UiButton( + quitscreenStayArea[1], + quitscreenStayArea[2], + quitscreenStayArea[3], + quitscreenStayArea[4], + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + nil, + color1, + color2, + padding * 0.5 + ) + font2:Print( + BAR.I18N("ui.topbar.quit.stay"), + quitscreenStayArea[1] + ((quitscreenStayArea[3] - quitscreenStayArea[1]) / 2), + quitscreenStayArea[2] + ((quitscreenStayArea[4] - quitscreenStayArea[2]) / 2) - (fontSize / 3), + fontSize, + "con" + ) + end + + -- resign button + if not spec and not gameIsOver then + local mouseOver = false + if + mathIsInRect( + mx, + my, + quitscreenResignArea[1], + quitscreenResignArea[2], + quitscreenResignArea[3], + quitscreenResignArea[4] + ) + then + color1 = { 0.4, 0, 0, 0.4 + (0.5 * fadeProgress) } + color2 = { 0.6, 0.05, 0.05, 0.4 + (0.5 * fadeProgress) } + mouseOver = "resign" + else + color1 = { 0.25, 0, 0, 0.35 + (0.5 * fadeProgress) } + color2 = { 0.5, 0, 0, 0.35 + (0.5 * fadeProgress) } + end + UiButton( + quitscreenResignArea[1], + quitscreenResignArea[2], + quitscreenResignArea[3], + quitscreenResignArea[4], + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + nil, + color1, + color2, + padding * 0.5 + ) + font2:Print( + BAR.I18N("ui.topbar.quit.resign"), + quitscreenResignArea[1] + ((quitscreenResignArea[3] - quitscreenResignArea[1]) / 2), + quitscreenResignArea[2] + ((quitscreenResignArea[4] - quitscreenResignArea[2]) / 2) - (fontSize / 3), + fontSize, + "con" + ) + + if teamResign then + if + mathIsInRect( + mx, + my, + quitscreenTeamResignArea[1], + quitscreenTeamResignArea[2], + quitscreenTeamResignArea[3], + quitscreenTeamResignArea[4] + ) + then + color1 = { 0.28, 0.28, 0.28, 0.4 + (0.5 * fadeProgress) } + color2 = { 0.45, 0.45, 0.45, 0.4 + (0.5 * fadeProgress) } + mouseOver = "teamResign" + else + color1 = { 0.18, 0.18, 0.18, 0.4 + (0.5 * fadeProgress) } + color2 = { 0.33, 0.33, 0.33, 0.4 + (0.5 * fadeProgress) } + end + UiButton( + quitscreenTeamResignArea[1], + quitscreenTeamResignArea[2], + quitscreenTeamResignArea[3], + quitscreenTeamResignArea[4], + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + nil, + color1, + color2, + padding * 0.5 + ) + font2:Print( + BAR.I18N("ui.topbar.quit.teamResign"), + quitscreenTeamResignArea[1] + ((quitscreenTeamResignArea[3] - quitscreenTeamResignArea[1]) / 2), + quitscreenTeamResignArea[2] + + ((quitscreenTeamResignArea[4] - quitscreenTeamResignArea[2]) / 2) + - (fontSize / 3), + fontSize, + "con" + ) + end + if mouseOver and teamResign then + font:Print( + BAR.I18N("ui.topbar.hint." .. mouseOver), + quitscreenTeamResignArea[1] - buttonMargin, + quitscreenArea[2] + (2.5 * fontSize / 3), + fontSize * 0.9, + "cn" + ) + end + end + + -- quit button + if gameIsOver or not chobbyLoaded then + if + mathIsInRect( + mx, + my, + quitscreenQuitArea[1], + quitscreenQuitArea[2], + quitscreenQuitArea[3], + quitscreenQuitArea[4] + ) + then + color1 = { 0.4, 0, 0, 0.4 + (0.5 * fadeProgress) } + color2 = { 0.6, 0.05, 0.05, 0.4 + (0.5 * fadeProgress) } + else + color1 = { 0.25, 0, 0, 0.35 + (0.5 * fadeProgress) } + color2 = { 0.5, 0, 0, 0.35 + (0.5 * fadeProgress) } + end + UiButton( + quitscreenQuitArea[1], + quitscreenQuitArea[2], + quitscreenQuitArea[3], + quitscreenQuitArea[4], + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + nil, + color1, + color2, + padding * 0.5 + ) + font2:Print( + BAR.I18N("ui.topbar.quit.quit"), + quitscreenQuitArea[1] + ((quitscreenQuitArea[3] - quitscreenQuitArea[1]) / 2), + quitscreenQuitArea[2] + ((quitscreenQuitArea[4] - quitscreenQuitArea[2]) / 2) - (fontSize / 3), + fontSize, + "con" + ) + end + + font2:End() + end + end) + + -- background + if WG.guishader then + WG.guishader.setScreenBlur(true) + WG.guishader.insertRenderDlist(dlist.quit) + else + glCallList(dlist.quit) + end +end +-- Second return value: the window refused to close (the keybind editor raises a guard +-- when there are unsaved edits) and is still up. +local function closeWindow(name) + if WG[name] ~= nil and WG[name].isvisible() then + WG[name].toggle(false) + return true, WG[name].isvisible() == true + end + return false, false +end + +local function hideWindows() + local closedWindow = false + local stillOpen = false + + local function hide(name) + local closed, blocked = closeWindow(name) + closedWindow = closed or closedWindow + stillOpen = blocked or stillOpen + end + + hide("options") + hide("scavengerinfo") + hide("missioninfo") + hide("keybinds") + hide("changelog") + hide("gameinfo") + hide("teamstats") + hide("widgetselector") + if showQuitscreen then + closedWindow = true + end + + showQuitscreen = nil + + if WG.guishader then + WG.guishader.setScreenBlur(false) + end + + if gameIsOver then -- Graphs window can only be open after game end + -- Closing Graphs window if open, no way to tell if it was open or not + Spring.SendCommands("endgraph 0") + graphsWindowVisible = false + end + + return closedWindow, stillOpen +end + +local function toggleWindow(name) + local isvisible = false + if WG[name] ~= nil then + isvisible = WG[name].isvisible() + end + local _, stillOpen = hideWindows() + -- Opening another window on top of a window that refused to close would bury its guard. + if stillOpen then + return isvisible + end + if WG[name] ~= nil and isvisible ~= true then + WG[name].toggle() + end + return isvisible +end +-- Which menu button, if any, sits under these screen coords. +-- Exposed as WG.topbar.buttonAt so the window widgets (which sit on a lower layer and +-- therefore see the click first) can tell a click on the top bar apart from a click that +-- dismisses them: closing themselves there would eat the click meant to open another window. +local function buttonAt(x, y) + if not buttonsArea.buttons then + return nil + end + for name, pos in pairs(buttonsArea.buttons) do + if mathIsInRect(x, y, pos[1], pos[2], pos[3], pos[4]) then + return name + end + end + return nil +end + +local function applyButtonAction(button) + if playSounds then + Spring.PlaySoundFile(leftclick, 0.8, "ui") + end + + local isvisible = false + if button == "quit" or button == "resign" then + if not gameIsOver and chobbyLoaded and button == "quit" then + Spring.SendLuaMenuMsg("showLobby") + else + local oldShowQuitscreen + if showQuitscreen then + oldShowQuitscreen = showQuitscreen + isvisible = true + end + + hideWindows() + + if oldShowQuitscreen then + if isvisible ~= true then + showQuitscreen = oldShowQuitscreen + if WG.guishader then + WG.guishader.setScreenBlur(true) + end + end + else + showQuitscreen = now + end + end + elseif button == "options" then + toggleWindow("options") + elseif button == "save" then + hideWindows() + if isSinglePlayer and cfg.allowSavegame and WG.savegame then + local time = os.date("%Y%m%d_%H%M%S") + Spring.SendCommands("savegame " .. time) + end + elseif button == "scavengers" then + toggleWindow("scavengerinfo") + elseif button == "mission" then + toggleWindow("missioninfo") + elseif button == "keybinds" then + toggleWindow("keybinds") + elseif button == "changelog" then + toggleWindow("changelog") + elseif button == "stats" then + toggleWindow("teamstats") + elseif button == "info" then + toggleWindow("gameinfo") + elseif button == "graphs" then + isvisible = graphsWindowVisible + hideWindows() + if gameIsOver and not isvisible then + Spring.SendCommands("endgraph 2") + graphsWindowVisible = true + end + end +end +function widget:DrawScreen() + now = osClock() + + if hoveringButtons then + Spring.SetMouseCursor("cursornormal") + end + + if refreshButtons then + refreshButtons = false + updateButtons() + rebuildBackground() + end + + if autoHideButtons then + if buttonsArea[1] and hoveringButtons then + if not showButtons then + showButtons = true + end + elseif showButtons then + showButtons = false + end + end + + if showButtons then + if dlist.background then + glCallList(dlist.background) + end + if dlist.buttons then + glCallList(dlist.buttons) + end + end + + if showButtons and dlist.buttons and buttonsArea.buttons then + -- changelog changes highlight + if WG.changelog and WG.changelog.haschanges() then + local button = "changelog" + if buttonsArea.buttons[button] then + local paddingsize = 1 + RectRound( + buttonsArea.buttons[button][1] + paddingsize, + buttonsArea.buttons[button][2] + paddingsize, + buttonsArea.buttons[button][3] - paddingsize, + buttonsArea.buttons[button][4] - paddingsize, + 3.5 * widgetScale, + 0, + 0, + 0, + 0, + { 1, 1, 1, 0.1 * blinkProgress } + ) + end + end + + -- hovered? + if not showQuitscreen and buttonsArea.buttons and hoveringButtons then + for button, pos in pairs(buttonsArea.buttons) do + if mathIsInRect(mx, my, pos[1], pos[2], pos[3], pos[4]) then + local paddingsize = 1 + RectRound( + buttonsArea.buttons[button][1] + paddingsize, + buttonsArea.buttons[button][2] + paddingsize, + buttonsArea.buttons[button][3] - paddingsize, + buttonsArea.buttons[button][4] - paddingsize, + 3.5 * widgetScale, + 0, + 0, + 0, + 0, + { 0, 0, 0, 0.06 } + ) + glBlending(GL.SRC_ALPHA, GL.ONE) + RectRound( + buttonsArea.buttons[button][1], + buttonsArea.buttons[button][2], + buttonsArea.buttons[button][3], + buttonsArea.buttons[button][4], + 3.5 * widgetScale, + 0, + 0, + 0, + 0, + { 1, 1, 1, 0.03 }, + { 0.44, 0.44, 0.44, 0.2 } + ) + local mult = 1 + RectRound( + buttonsArea.buttons[button][1], + buttonsArea.buttons[button][4] + - ((buttonsArea.buttons[button][4] - buttonsArea.buttons[button][2]) * 0.4), + buttonsArea.buttons[button][3], + buttonsArea.buttons[button][4], + 3.3 * widgetScale, + 0, + 0, + 0, + 0, + { 1, 1, 1, 0 }, + { 1, 1, 1, 0.18 * mult } + ) + RectRound( + buttonsArea.buttons[button][1], + buttonsArea.buttons[button][2], + buttonsArea.buttons[button][3], + buttonsArea.buttons[button][2] + + ((buttonsArea.buttons[button][4] - buttonsArea.buttons[button][2]) * 0.25), + 3.3 * widgetScale, + 0, + 0, + 0, + 0, + { 1, 1, 1, 0.045 * mult }, + { 1, 1, 1, 0 } + ) + glBlending(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA) + break + end + end + end + end + if dlist.quit then + if WG.guishader then + WG.guishader.removeRenderDlist(dlist.quit) + end + glDeleteList(dlist.quit) + dlist.quit = nil + end + + if showQuitscreen then + drawQuitScreen() + end + + glColor(1, 1, 1, 1) +end + +function widget:MousePress(x, y, button) + if button == 1 then + if showQuitscreen and quitscreenArea then + if mathIsInRect(x, y, quitscreenArea[1], quitscreenArea[2], quitscreenArea[3], quitscreenArea[4]) then + if + (gameIsOver or not chobbyLoaded or not spec) + and mathIsInRect( + x, + y, + quitscreenStayArea[1], + quitscreenStayArea[2], + quitscreenStayArea[3], + quitscreenStayArea[4] + ) + then + if playSounds then + Spring.PlaySoundFile(leftclick, 0.75, "ui") + end + + showQuitscreen = nil + if WG.guishader then + WG.guishader.setScreenBlur(false) + end + end + if + (gameIsOver or not chobbyLoaded) + and mathIsInRect( + x, + y, + quitscreenQuitArea[1], + quitscreenQuitArea[2], + quitscreenQuitArea[3], + quitscreenQuitArea[4] + ) + then + if playSounds then + Spring.PlaySoundFile(leftclick, 0.75, "ui") + end + + if not chobbyLoaded then + Spring.SendCommands("QuitForce") -- Exit the game completely + else + Spring.SendCommands("ReloadForce") -- Exit to the lobby + end + + showQuitscreen = nil + hideQuitWindow = now + end + if + not spec + and not gameIsOver + and mathIsInRect( + x, + y, + quitscreenResignArea[1], + quitscreenResignArea[2], + quitscreenResignArea[3], + quitscreenResignArea[4] + ) + then + if playSounds then + Spring.PlaySoundFile(leftclick, 0.75, "ui") + end + Spring.SendCommands("spectator") + showQuitscreen = nil + if WG.guishader then + WG.guishader.setScreenBlur(false) + end + end + if + not spec + and not gameIsOver + and teamResign + and mathIsInRect( + x, + y, + quitscreenTeamResignArea[1], + quitscreenTeamResignArea[2], + quitscreenTeamResignArea[3], + quitscreenTeamResignArea[4] + ) + then + if playSounds then + Spring.PlaySoundFile(leftclick, 0.75, "ui") + end + Spring.SendCommands("say !cv resign") + showQuitscreen = nil + if WG.guishader then + WG.guishader.setScreenBlur(false) + end + end + else + showQuitscreen = nil + if WG.guishader then + WG.guishader.setScreenBlur(false) + end + end + return true + end + + local clickedButton = buttonAt(x, y) + if clickedButton then + applyButtonAction(clickedButton) + return true + end + else + if showQuitscreen and quitscreenArea then + return true + end + end + + -- a click on the strip itself never falls through to the world + if hoveringButtons then + return true + end +end + +function widget:MouseWheel(up, value) -- up = true/false , value = -1/1 + if showQuitscreen and quitscreenArea then + return true + end +end + +function widget:KeyPress(key) + if key == 27 then -- ESC + if not WG.options or (WG.options.disallowEsc and not WG.options.disallowEsc()) then + local escDidSomething = hideWindows() + if cfg.escapeKeyPressesQuit and not escDidSomething then + applyButtonAction("quit") + end + end + end + if showQuitscreen and quitscreenArea then + return true + end +end +-- WG.topbar is shared with the Top Bar widget. Consumers guard on the table, not on the +-- individual functions, so when that widget is not running its half of the API has to be +-- filled in rather than left missing. Checked from Update, which runs after every +-- widget's Initialize and again if the Top Bar is disabled mid-game. +local fallback = {} + +local function provide(name, fn) + local current = WG.topbar[name] + if current == nil then + fallback[name] = fn + WG.topbar[name] = fn + elseif fallback[name] ~= nil and current ~= fallback[name] then + fallback[name] = nil -- the Top Bar widget took over + end +end + +local function noop() end +local function returnFalse() + return false +end + +local function provideTopBarFallbacks() + if not WG.topbar then + return + end + provide("GetPosition", function() + return { topbarArea[1], topbarArea[2], topbarArea[3], topbarArea[4], widgetScale, buttonsArea[2] } + end) + provide("GetFreeArea", function() + -- no resource bars to make room for, so everything left of the strip is free + return { topbarArea[1], topbarArea[2], buttonsArea[1], topbarArea[4], widgetScale } + end) + provide("GetSkewConfig", function() + return { + useSkew = cfg.useSkew, + skewTan = math.tan(math.rad(20)), + smallElementHeightFraction = cfg.smallElementHeightFraction, + } + end) + -- there are no resource bars or indicators to show, hide or update + provide("updateTopBarEnergy", noop) + provide("setResourceBarsVisible", noop) + provide("setIndicatorsVisible", noop) + provide("getResourceBarsVisible", returnFalse) + provide("getIndicatorsVisible", returnFalse) +end + +local function removeFallbacks() + if not WG.topbar then + return + end + for name, fn in pairs(fallback) do + if WG.topbar[name] == fn then + WG.topbar[name] = nil + end + end +end + +-- Which buttons exist depends on widgets that may load after this one (game info, save +-- game, the scenario panels) and on game state, so the set is re-checked periodically +-- instead of only at startup. +local buttonSignature = "" +local function currentButtonSignature() + local changedModoptions = 0 + if WG.gameinfo and WG.gameinfo.getChangedModoptionsCount then + changedModoptions = WG.gameinfo.getChangedModoptionsCount() or 0 + end + return table.concat({ + WG.options and 1 or 0, + WG.keybinds and 1 or 0, + WG.changelog and 1 or 0, + WG.teamstats and 1 or 0, + WG.scavengerinfo and 1 or 0, + WG.missioninfo and 1 or 0, + WG.savegame and 1 or 0, + changedModoptions, + gameIsOver and 1 or 0, + gameStarted and 1 or 0, + spec and 1 or 0, + chobbyLoaded and 1 or 0, + }, ",") +end + +function widget:Update(dt) + now = osClock() + + provideTopBarFallbacks() + + if now > nextStateCheck then + nextStateCheck = now + 0.0333 + + mx, my = Spring.GetMouseState() + -- deliberately not gated on showButtons: with auto-hide on, hovering the empty + -- strip is what brings the buttons back + hoveringButtons = buttonsArea[1] ~= nil + and mathIsInRect(mx, my, buttonsArea[1], buttonsArea[2], buttonsArea[3], buttonsArea[4]) + + local _, _, isPaused = Spring.GetGameSpeed() + if not isPaused then + if blinkDirection then + blinkProgress = blinkProgress + (dt * 9) + if blinkProgress > 1 then + blinkProgress = 1 + blinkDirection = false + end + else + blinkProgress = blinkProgress - (dt / (blinkProgress * 1.5)) + if blinkProgress < 0 then + blinkProgress = 0 + blinkDirection = true + end + end + end + end + + if now > nextButtonCheck then + nextButtonCheck = now + 0.5 + local signature = currentButtonSignature() + if signature ~= buttonSignature then + buttonSignature = signature + refreshButtons = true + end + end +end + +function widget:GameStart() + gameStarted = true + refreshButtons = true +end + +function widget:GameOver() + gameIsOver = true + refreshButtons = true +end + +function widget:PlayerChanged() + spec = Spring.GetSpectatingState() + myAllyTeamID = Spring.GetLocalAllyTeamID() + myTeamID = Spring.GetLocalTeamID() + myAllyTeamList = Spring.GetTeamList(myAllyTeamID) + refreshButtons = true +end + +function widget:LanguageChanged() + refreshButtons = true +end + +function widget:MouseRelease(x, y, button) + if showQuitscreen and quitscreenArea then + return true + end +end + +function widget:Initialize() + if Spring.GetMenuName and string.find(string.lower(Spring.GetMenuName()), "chobby") then + chobbyLoaded = true + Spring.SendLuaMenuMsg("disableLobbyButton") + end + + -- The quit/resign dialog counts as a window: while it is up, the handler can hide the + -- rest of the interface (springsetting WindowsHideInterface), and this widget stays. + widgetHandler:RegisterModalWindow(widget, function() + return showQuitscreen ~= nil + end) + + -- This widget was split out of "Top Bar", which used to persist the auto-hide setting. + -- Adopt it once so the setting survives the split. + if not configLoaded and widgetHandler.configData then + local previous = widgetHandler.configData["Top Bar"] + if previous and previous.autoHideButtons ~= nil then + autoHideButtons = previous.autoHideButtons + showButtons = not autoHideButtons + end + end + + -- Shared with the Top Bar widget: each fills in its own half, so either can run alone. + WG.topbar = WG.topbar or {} + + WG.topbar.showingQuit = function() + return showQuitscreen + end + + WG.topbar.hideWindows = function() + hideWindows() + end + + WG.topbar.buttonAt = function(x, y) + return buttonAt(x, y) + end + + -- The strip's rect, for widgets that place themselves under or beside it (ecostats, + -- the spectator HUD). Reports the geometry whether or not the buttons are currently + -- drawn; ask getShowButtons() for that. + WG.topbar.GetButtonsPosition = function() + if not buttonsArea[1] then + return nil + end + return { buttonsArea[1], buttonsArea[2], buttonsArea[3], buttonsArea[4], widgetScale } + end + + WG.topbar.setAutoHideButtons = function(value) + autoHideButtons = value + showButtons = not value + refreshButtons = true + end + + WG.topbar.getAutoHideButtons = function() + return autoHideButtons + end + + WG.topbar.getShowButtons = function() + return showButtons + end + + widget:ViewResize() + buttonSignature = currentButtonSignature() +end + +function widget:Shutdown() + removeFallbacks() + + if dlist.buttons then + dlist.buttons = glDeleteList(dlist.buttons) + end + if dlist.background then + if WG.guishader then + WG.guishader.RemoveDlist("topbar_buttons") + end + dlist.background = glDeleteList(dlist.background) + end + if dlist.quit then + if WG.guishader then + WG.guishader.removeRenderDlist(dlist.quit) + WG.guishader.setScreenBlur(false) + end + dlist.quit = glDeleteList(dlist.quit) + end + + if WG.topbar then + WG.topbar.showingQuit = nil + WG.topbar.hideWindows = nil + WG.topbar.buttonAt = nil + WG.topbar.GetButtonsPosition = nil + WG.topbar.setAutoHideButtons = nil + WG.topbar.getAutoHideButtons = nil + WG.topbar.getShowButtons = nil + -- the Top Bar widget may still be using the table for its own half + if next(WG.topbar) == nil then + WG.topbar = nil + end + end +end + +function widget:GetConfigData() + return { autoHideButtons = autoHideButtons } +end + +function widget:SetConfigData(data) + configLoaded = true + if data.autoHideButtons ~= nil then + autoHideButtons = data.autoHideButtons + showButtons = not autoHideButtons + end +end From 85751f8cd11a9debaf46557367ed6d931fb8d056 Mon Sep 17 00:00:00 2001 From: Floris Date: Sat, 12 Sep 2026 01:05:09 +0200 Subject: [PATCH 2/2] widgethandler: add show/hide interface (#9212) --- luaui/Widgets/gui_top_bar_buttons.lua | 30 ++++++- luaui/barwidgets.lua | 110 +++++++++++++++++++++++--- 2 files changed, 127 insertions(+), 13 deletions(-) diff --git a/luaui/Widgets/gui_top_bar_buttons.lua b/luaui/Widgets/gui_top_bar_buttons.lua index 7fb3c41a48c..6c6bc49505e 100644 --- a/luaui/Widgets/gui_top_bar_buttons.lua +++ b/luaui/Widgets/gui_top_bar_buttons.lua @@ -96,6 +96,8 @@ local font2 -- quit: the quit/resign dialog, rebuilt every frame while it is up ---@type table local dlist = {} +-- whether the strip's shape is currently registered with the blur +local guishaderShown = false -- Interactions ---@type any @@ -275,6 +277,27 @@ local function updateButtons() end) end +-- The blur follows the strip rather than its geometry: with auto hide on, a region +-- left registered would keep blurring a rectangle of map where the buttons no longer +-- are. Re-checked every frame, which also picks the blur back up if the guishader +-- widget is enabled mid-game. +local function applyGuishader() + if not WG.guishader then + guishaderShown = false + return + end + local wanted = showButtons and dlist.background ~= nil + if wanted == guishaderShown then + return + end + if wanted then + WG.guishader.InsertDlist(dlist.background, "topbar_buttons", nil, widget) + else + WG.guishader.RemoveDlist("topbar_buttons") + end + guishaderShown = wanted +end + -- The strip's panel chrome. Baked once per layout change and also handed to the blur, -- which only reads its coverage. local function rebuildBackground() @@ -282,6 +305,7 @@ local function rebuildBackground() if WG.guishader then WG.guishader.RemoveDlist("topbar_buttons") end + guishaderShown = false dlist.background = glDeleteList(dlist.background) end if not buttonsArea[1] then @@ -308,9 +332,7 @@ local function rebuildBackground() nil ) end) - if WG.guishader then - WG.guishader.InsertDlist(dlist.background, "topbar_buttons", nil, widget) - end + applyGuishader() end -- Mirrors the Top Bar's own geometry so the strip lines up with the bar, without @@ -838,6 +860,8 @@ function widget:DrawScreen() end end + applyGuishader() + if showButtons then if dlist.background then glCallList(dlist.background) diff --git a/luaui/barwidgets.lua b/luaui/barwidgets.lua index 4ca6c3b0479..103d66be49f 100644 --- a/luaui/barwidgets.lua +++ b/luaui/barwidgets.lua @@ -805,6 +805,15 @@ function widgetHandler:NewWidget(enableLocalsAccess, fromZip, filename) wh.SetWindowsHideInterface = function(_, enabled) return self:SetWindowsHideInterface(enabled) end + wh.HideInterface = function(_, reason, keep) + return self:HideInterface(reason, keep) + end + wh.ShowInterface = function(_, reason) + return self:ShowInterface(reason) + end + wh.IsInterfaceHidden = function(_) + return self:IsInterfaceHidden() + end tracy.ZoneEnd() return widget end @@ -1499,6 +1508,15 @@ end -- Registered windows stay in the allowed set while closed, so a window that closes -- while another is open can still run its own cleanup frame. -- +-- The same hiding can also be asked for outright, with no window involved and +-- regardless of the springsetting, for a cutscene or a tool: +-- widgetHandler:HideInterface("cutscene") -- keeps the menu buttons +-- widgetHandler:HideInterface("cutscene", {"Chat"}) -- keeps only chat +-- widgetHandler:HideInterface("cutscene", {}) -- keeps nothing +-- widgetHandler:ShowInterface("cutscene") +-- Requests are named so two callers cannot switch each other off, and the +-- interface returns once the last one is released. +-- local modalWindows = {} -- widget -> is-open predicate local modalExempt = {} -- widget -> true local modalAllowed = {} -- widget -> true (exempt widgets and registered windows) @@ -1508,6 +1526,26 @@ local modalEnabled = false local modalConfigTimer = 0 local MODAL_CONFIG_INTERVAL = 1 -- seconds between config re-reads (picks up /set) +-- The same hiding, asked for outright rather than driven by an open window: for a +-- cutscene, a screenshot mode, an editor. Requests are named so two callers cannot +-- switch each other off, and each names the widgets it wants kept. Unlike the window +-- case this ignores the springsetting: a caller that asks for it means it. +local interfaceHiddenReasons = {} -- reason -> set of widget names to keep +local interfaceHidden = false +local interfaceAllowedNames = {} -- union of the names every active reason keeps +-- what a caller gets when it names nothing: the menu buttons, so there is always a +-- way back out. Pass an explicit (possibly empty) list to keep something else. +local DEFAULT_INTERFACE_ALLOW = { "Top Bar Buttons" } + +-- The single question every filtered call-in asks. Kept cheap: two upvalue reads +-- when nothing is hiding anything. +local function allowedWidget(widget) + if interfaceHidden then + return interfaceAllowedNames[widget.whInfo.name] == true + end + return (not modalActive) or (modalAllowed[widget] == true) +end + local function ModalAllowWidget(widget, allowed) if (modalAllowed[widget] or false) ~= allowed then modalAllowed[widget] = allowed or nil @@ -1549,16 +1587,68 @@ function widgetHandler:IsModalActive() return modalActive end +local function rebuildInterfaceAllowed() + local hidden = false + local names = {} + for _, allow in pairs(interfaceHiddenReasons) do + hidden = true + for name in pairs(allow) do + names[name] = true + end + end + interfaceAllowedNames = names + interfaceHidden = hidden + -- cached visibility elsewhere (the guishader stencil) has to be rebuilt + modalRevision = modalRevision + 1 +end + +---Hide every widget except the ones named, until the same reason is released. +---@param reason string caller-chosen key; pass the same one to ShowInterface +---@param keep string[]? widget names to keep drawing and clickable. +---Defaults to the menu buttons; pass {} to keep nothing at all. +function widgetHandler:HideInterface(reason, keep) + if type(reason) ~= "string" then + Spring.Log("barwidgets.lua", LOG.ERROR, "HideInterface: expected a reason name") + return false + end + local names = {} + for _, name in ipairs(keep or DEFAULT_INTERFACE_ALLOW) do + names[name] = true + end + interfaceHiddenReasons[reason] = names + rebuildInterfaceAllowed() + return true +end + +---Release one reason. The interface comes back once no reason is left. +---@param reason string +function widgetHandler:ShowInterface(reason) + if interfaceHiddenReasons[reason] == nil then + return false + end + interfaceHiddenReasons[reason] = nil + rebuildInterfaceAllowed() + return true +end + +function widgetHandler:IsInterfaceHidden() + return interfaceHidden +end + -- Bumped whenever the allowed set changes, so cached state elsewhere (the guishader's -- stencil) can tell it needs rebuilding. function widgetHandler:GetModalRevision() return modalRevision end --- Widgets the modal window lets through. A nil widget (e.g. an unowned guishader +-- Whether a widget is drawing and taking input right now, whether it is a window +-- or an outright hide that is covering it. A nil widget (e.g. an unowned guishader -- region) counts as not allowed, which is the wanted default. function widgetHandler:ModalAllows(widget) - return (not modalActive) or (modalAllowed[widget] == true) + if widget == nil then + return (not modalActive) and (not interfaceHidden) + end + return allowedWidget(widget) end function widgetHandler:SetWindowsHideInterface(enabled) @@ -1786,7 +1876,7 @@ function widgetHandler:DrawScreen() local list = self.DrawScreenList for i = #list, 1, -1 do local w = list[i] - if not modalActive or modalAllowed[w] then + if allowedWidget(w) then tracy.ZoneBeginN(w._tracyDrawScreenName) w:DrawScreen() tracy.ZoneEnd() @@ -2136,7 +2226,7 @@ function widgetHandler:KeyPress(key, mods, isRepeat, label, unicode, scanCode, a end for _, w in ipairs(self.KeyPressList) do - if not modalActive or modalAllowed[w] then + if allowedWidget(w) then if w:KeyPress(key, mods, isRepeat, label, unicode, scanCode, actions) then tracy.ZoneEnd() return true @@ -2188,7 +2278,7 @@ function widgetHandler:TextInput(utf8, ...) local list = self.TextInputList for i = #list, 1, -1 do - if not modalActive or modalAllowed[list[i]] then + if allowedWidget(list[i]) then if list[i]:TextInput(utf8, ...) then tracy.ZoneEnd() return true @@ -2208,7 +2298,7 @@ end function widgetHandler:WidgetAt(x, y) tracy.ZoneBeginN("W:WidgetAt") for _, w in ipairs(self.IsAboveList) do - if (not modalActive or modalAllowed[w]) and w:IsAbove(x, y) then + if allowedWidget(w) and w:IsAbove(x, y) then tracy.ZoneEnd() return w end @@ -2224,7 +2314,7 @@ function widgetHandler:MousePress(x, y, button) self.mouseOwner:MousePress(x, y, button) else for _, w in ipairs(self.MousePressList) do - if not modalActive or modalAllowed[w] then + if allowedWidget(w) then if w:MousePress(x, y, button) then self.mouseOwner = w break @@ -2236,7 +2326,7 @@ function widgetHandler:MousePress(x, y, button) -- While a window is open the click is consumed even when nothing claimed it, so it -- never reaches the engine and orders no units. No mouseOwner is taken for that: -- there is nothing to route the follow-up move/release to. - local consumed = (self.mouseOwner ~= nil) or modalActive + local consumed = (self.mouseOwner ~= nil) or modalActive or interfaceHidden if widgetHandler.WG.SmartSelect_MousePress2 then widgetHandler.WG.SmartSelect_MousePress2(x, y, button, consumed) end @@ -2277,7 +2367,7 @@ function widgetHandler:MouseWheel(up, value) end tracy.ZoneBeginN("W:MouseWheel") for _, w in ipairs(self.MouseWheelList) do - if (not modalActive or modalAllowed[w]) and w:MouseWheel(up, value) then + if allowedWidget(w) and w:MouseWheel(up, value) then tracy.ZoneEnd() return true end @@ -2390,7 +2480,7 @@ end function widgetHandler:GetTooltip(x, y) tracy.ZoneBeginN("W:GetTooltip") for _, w in ipairs(self.GetTooltipList) do - if (not modalActive or modalAllowed[w]) and w:IsAbove(x, y) then + if allowedWidget(w) and w:IsAbove(x, y) then local tip = w:GetTooltip(x, y) if type(tip) == "string" and #tip > 0 then tracy.ZoneEnd()