From 546ce61136712b99191ab3ef45acd071a8fabb6f Mon Sep 17 00:00:00 2001 From: Floris Date: Fri, 11 Sep 2026 19:18:16 +0200 Subject: [PATCH 1/3] added configsetting WindowsHideInterface to hide the GUI exept the center windows + chat+voteinterface+top bar buttons (#9206) --- language/en/interface.json | 2 + luaui/Widgets/gfx_guishader.lua | 75 ++++++++-- luaui/Widgets/gui_changelog_info.lua | 7 +- luaui/Widgets/gui_chat.lua | 13 +- luaui/Widgets/gui_gameinfo.lua | 7 +- luaui/Widgets/gui_keybind_info.lua | 7 +- luaui/Widgets/gui_mission_info.lua | 8 +- luaui/Widgets/gui_options.lua | 33 ++++- luaui/Widgets/gui_pausescreen.lua | 1 + luaui/Widgets/gui_scavenger_info.lua | 7 +- luaui/Widgets/gui_teamstats.lua | 7 +- luaui/Widgets/gui_tooltip.lua | 12 +- luaui/Widgets/gui_top_bar.lua | 60 ++++++-- luaui/Widgets/gui_vote_interface.lua | 3 +- luaui/Widgets/widget_selector.lua | 13 +- luaui/barwidgets.lua | 205 ++++++++++++++++++++++++--- types/Addon.lua | 3 + 17 files changed, 395 insertions(+), 68 deletions(-) diff --git a/language/en/interface.json b/language/en/interface.json index 2ff8fae345a..68af9a1b2f5 100644 --- a/language/en/interface.json +++ b/language/en/interface.json @@ -1618,6 +1618,8 @@ "catchupminfps_descr": " ", "widgetselector": "Widget selector interface", "widgetselector_descr": "Allow the toggling of the widget selector interface (via F11)", + "windows_hideinterface": "Windows hide the interface", + "windows_hideinterface_descr": "While a window such as Settings, Keys or Stats is open, hide the rest of the interface and block its input. The top bar menu buttons, chat, votes and the pause overlay stay.", "devmode": "Developer UI", "devmode_descr": "Toggle between how a developer or player see the UI", "customwidgets": "Allow custom widgets", diff --git a/luaui/Widgets/gfx_guishader.lua b/luaui/Widgets/gfx_guishader.lua index 843f24a4142..91c5a9e0d46 100644 --- a/luaui/Widgets/gfx_guishader.lua +++ b/luaui/Widgets/gfx_guishader.lua @@ -13,6 +13,7 @@ function widget:GetInfo() license = "GNU GPL, v2 or later", layer = -990000, -- other widgets can be run earlier (lower layer) and thus guishader blur are will lag behind a frame, (like tooltip screenblur) enabled = true, + modalExempt = true, -- the blur behind an open window is this widget's work } end @@ -70,6 +71,16 @@ local guishaderScreenDlists = {} local updateStencilTexture = false local updateStencilTextureScreen = false +-- Which widget registered each region, when it said so (name -> widget). Used only while +-- a modal window hides the interface: a hidden widget's region would otherwise stay on +-- screen as a blurred patch of map. A region with no owner is treated as hidden then. +local rectOwners = {} +local dlistOwners = {} +local screenRectOwners = {} +local screenDlistOwners = {} +local lastModalActive = false +local lastModalRevision = -1 + local oldvs = 0 local vsx, vsy, vpx, vpy = spGetViewGeometry() local blurScale = 1 @@ -152,22 +163,30 @@ local function DrawStencilTexture(world, fullscreen) glTranslate(-1, -1, 0) glScale(2 / vsx, 2 / vsy, 0) if world then - for _, rect in pairs(guishaderRects) do - glRect(rect[1], rect[2], rect[3], rect[4]) + for name, rect in pairs(guishaderRects) do + if widgetHandler:ModalAllows(rectOwners[name]) then + glRect(rect[1], rect[2], rect[3], rect[4]) + end end - for _, dlist in pairs(guishaderDlists) do - glColor(1, 1, 1, 1) - glCallList(dlist) + for name, dlist in pairs(guishaderDlists) do + if widgetHandler:ModalAllows(dlistOwners[name]) then + glColor(1, 1, 1, 1) + glCallList(dlist) + end end elseif fullscreen then glRect(0, 0, vsx, vsy) else - for _, rect in pairs(guishaderScreenRects) do - glRect(rect[1], rect[2], rect[3], rect[4]) + for name, rect in pairs(guishaderScreenRects) do + if widgetHandler:ModalAllows(screenRectOwners[name]) then + glRect(rect[1], rect[2], rect[3], rect[4]) + end end - for _, dlist in pairs(guishaderScreenDlists) do - glColor(1, 1, 1, 1) - glCallList(dlist) + for name, dlist in pairs(guishaderScreenDlists) do + if widgetHandler:ModalAllows(screenDlistOwners[name]) then + glColor(1, 1, 1, 1) + glCallList(dlist) + end end end glPopMatrix() @@ -370,6 +389,18 @@ function widget:Shutdown() end function widget:DrawScreenEffects() -- This blurs the world underneath UI elements + -- Before the early returns on purpose: when a modal window starts or stops hiding the + -- interface, which regions belong in the stencil changes even though no widget + -- registered or removed one, and the quit dialog's fullscreen blur skips the rest. + local modalActive = widgetHandler:IsModalActive() + local modalRevision = widgetHandler:GetModalRevision() + if modalActive ~= lastModalActive or modalRevision ~= lastModalRevision then + lastModalActive = modalActive + lastModalRevision = modalRevision + updateStencilTexture = true + updateStencilTextureScreen = true + end + if spIsGUIHidden() or uiOpacity > 0.99 then return end @@ -509,9 +540,14 @@ function widget:Initialize() self:UpdateCallIns() WG.guishader = {} - WG.guishader.InsertDlist = function(dlist, name, force) - if force or guishaderDlists[name] ~= dlist then + -- The trailing `owner` argument of the Insert functions is optional and only matters + -- when a modal window hides the interface: pass the registering `widget` and the + -- region follows that widget's visibility, otherwise it is dropped while a window is + -- open. See the "Modal windows" block in barwidgets.lua. + WG.guishader.InsertDlist = function(dlist, name, force, owner) + if force or guishaderDlists[name] ~= dlist or dlistOwners[name] ~= owner then guishaderDlists[name] = dlist + dlistOwners[name] = owner updateStencilTexture = true end end @@ -519,6 +555,7 @@ function widget:Initialize() local found = guishaderDlists[name] ~= nil if found then guishaderDlists[name] = nil + dlistOwners[name] = nil updateStencilTexture = true end return found @@ -528,30 +565,35 @@ function widget:Initialize() if found then deleteDlistQueue[#deleteDlistQueue + 1] = guishaderDlists[name] guishaderDlists[name] = nil + dlistOwners[name] = nil updateStencilTexture = true end return found end - WG.guishader.InsertRect = function(left, top, right, bottom, name) + WG.guishader.InsertRect = function(left, top, right, bottom, name, owner) guishaderRects[name] = { left, top, right, bottom } + rectOwners[name] = owner updateStencilTexture = true end WG.guishader.RemoveRect = function(name) local found = guishaderRects[name] ~= nil if found then guishaderRects[name] = nil + rectOwners[name] = nil updateStencilTexture = true end return found end - WG.guishader.InsertScreenDlist = function(dlist, name) + WG.guishader.InsertScreenDlist = function(dlist, name, owner) guishaderScreenDlists[name] = dlist + screenDlistOwners[name] = owner updateStencilTextureScreen = true end WG.guishader.RemoveScreenDlist = function(name) local found = guishaderScreenDlists[name] ~= nil if found then guishaderScreenDlists[name] = nil + screenDlistOwners[name] = nil updateStencilTextureScreen = true end return found @@ -561,17 +603,20 @@ function widget:Initialize() if found then deleteDlistQueue[#deleteDlistQueue + 1] = guishaderScreenDlists[name] guishaderScreenDlists[name] = nil + screenDlistOwners[name] = nil end return found end - WG.guishader.InsertScreenRect = function(left, top, right, bottom, name) + WG.guishader.InsertScreenRect = function(left, top, right, bottom, name, owner) guishaderScreenRects[name] = { left, top, right, bottom } + screenRectOwners[name] = owner updateStencilTextureScreen = true end WG.guishader.RemoveScreenRect = function(name) local found = guishaderScreenRects[name] ~= nil if found then guishaderScreenRects[name] = nil + screenRectOwners[name] = nil updateStencilTextureScreen = true end return found diff --git a/luaui/Widgets/gui_changelog_info.lua b/luaui/Widgets/gui_changelog_info.lua index ef18ebb3b93..ec3e4891027 100644 --- a/luaui/Widgets/gui_changelog_info.lua +++ b/luaui/Widgets/gui_changelog_info.lua @@ -516,7 +516,7 @@ function widget:DrawScreen() backgroundGuishader = glCreateList(function() RectRound(screenX, screenY - screenHeight, screenX + screenWidth, screenY, elementCorner, 1, 1, 1, 1) end) - WG.guishader.InsertDlist(backgroundGuishader, "changelog") + WG.guishader.InsertDlist(backgroundGuishader, "changelog", nil, widget) end showOnceMore = false @@ -604,6 +604,11 @@ function widget:Initialize() return end + -- lets the handler hide the rest of the interface while the panel is open + widgetHandler:RegisterModalWindow(function() + return show == true + end) + WG.changelog = {} WG.changelog.toggle = function(state) if state ~= nil then diff --git a/luaui/Widgets/gui_chat.lua b/luaui/Widgets/gui_chat.lua index 51cb7233384..4acbbf195a4 100644 --- a/luaui/Widgets/gui_chat.lua +++ b/luaui/Widgets/gui_chat.lua @@ -10,6 +10,7 @@ function widget:GetInfo() layer = -95000, enabled = true, handler = true, + modalExempt = true, -- chat stays readable and usable while a window is open } end @@ -1186,7 +1187,7 @@ function state.updateChatInputGuishader(left, bottom, right, top) RectRound(left, bottom, right, top, elementCorner) end) WG.guishader.RemoveDlist("chatinput") - WG.guishader.InsertDlist(state.chatInputGuishaderDlist, "chatinput") + WG.guishader.InsertDlist(state.chatInputGuishaderDlist, "chatinput", nil, widget) end function state.drawEmojiPickerButton(rect, iconSize) @@ -1245,7 +1246,7 @@ function state.drawEmojiPickerGrid(inputAlpha, inputFontSize) glColor(0, 0, 0, inputAlpha * 1.12) RectRound(pickerLeft, pickerBottom, pickerRight, pickerTop, elementCorner * 0.7, 0, 0, 1, 1) if WG.guishader then - WG.guishader.InsertRect(pickerLeft, pickerBottom, pickerRight, pickerTop, "chatinputemojipicker") + WG.guishader.InsertRect(pickerLeft, pickerBottom, pickerRight, pickerTop, "chatinputemojipicker", widget) end for i = 1, #emojiAutocompleteAliases do local col = (i - 1) % pickerColumns @@ -2534,7 +2535,8 @@ drawChatInput = function() yPos - height, x2 - elementPadding, yPos, - "chatinputautocomplete" + "chatinputautocomplete", + widget ) end local addHeight = floor((inputFontSize * scale) * 1.35) - autocLineHeight @@ -2587,7 +2589,7 @@ drawChatInput = function() "o" ) if WG.guishader then - WG.guishader.InsertRect(infoLeft, infoBottom, infoRight, infoTop, "chatinputinfo") + WG.guishader.InsertRect(infoLeft, infoBottom, infoRight, infoTop, "chatinputinfo", widget) end else if WG.guishader then @@ -2985,7 +2987,8 @@ function widget:DrawScreen() activationArea[2] + chatlogHeightDiff, activationArea[3], activationArea[4], - "chat" + "chat", + widget ) end diff --git a/luaui/Widgets/gui_gameinfo.lua b/luaui/Widgets/gui_gameinfo.lua index b48efab9b6f..f4cd3dee2cb 100644 --- a/luaui/Widgets/gui_gameinfo.lua +++ b/luaui/Widgets/gui_gameinfo.lua @@ -1931,7 +1931,7 @@ function widget:DrawScreen() backgroundGuishader = glCreateList(function() RectRound(screenX, screenY - screenHeight, screenX + screenWidth, screenY, elementCorner, 1, 1, 1, 1) end) - WG.guishader.InsertDlist(backgroundGuishader, "gameinfo") + WG.guishader.InsertDlist(backgroundGuishader, "gameinfo", nil, widget) end showOnceMore = false @@ -2239,6 +2239,11 @@ function widget:Initialize() end end, nil, "p") + -- lets the handler hide the rest of the interface while the panel is open + widgetHandler:RegisterModalWindow(function() + return show == true + end) + WG.gameinfo = {} WG.gameinfo.toggle = function(state) if state == nil then diff --git a/luaui/Widgets/gui_keybind_info.lua b/luaui/Widgets/gui_keybind_info.lua index 4dffe22de4d..9d75f08f800 100644 --- a/luaui/Widgets/gui_keybind_info.lua +++ b/luaui/Widgets/gui_keybind_info.lua @@ -147,7 +147,7 @@ function widget:DrawScreen() backgroundGuishader = glCreateList(function() RectRound(screenX, screenY - screenHeight, screenX + screenWidth, screenY, elementCorner, 0, 1, 1, 1) end) - WG.guishader.InsertDlist(backgroundGuishader, "keybindinfo") + WG.guishader.InsertDlist(backgroundGuishader, "keybindinfo", nil, widget) end showOnceMore = false @@ -359,6 +359,11 @@ function widget:Initialize() end end) + -- lets the handler hide the rest of the interface while the panel is open + widgetHandler:RegisterModalWindow(function() + return show == true + end) + WG.keybinds = {} WG.keybinds.toggle = function(state) local wanted = state diff --git a/luaui/Widgets/gui_mission_info.lua b/luaui/Widgets/gui_mission_info.lua index 3ba0b08905b..d24798d6c0d 100644 --- a/luaui/Widgets/gui_mission_info.lua +++ b/luaui/Widgets/gui_mission_info.lua @@ -343,7 +343,7 @@ function widget:DrawScreen() RectRound(titleRect[1], titleRect[2], titleRect[3], titleRect[4], elementCorner, 1, 1, 0, 0) end) dlistcreated = true - WG.guishader.InsertDlist(backgroundGuishader, "missiontext") + WG.guishader.InsertDlist(backgroundGuishader, "missiontext", nil, widget) end showOnceMore = false @@ -468,6 +468,12 @@ function widget:Initialize() totalTextLines = #textLines + -- lets the handler hide the rest of the interface while the panel is open. + -- Reads `show` alone: justClosedFromPress is a one-frame lie told to the top bar. + widgetHandler:RegisterModalWindow(function() + return show == true + end) + WG.missioninfo = {} WG.missioninfo.toggle = function(state) local wasVisible = show diff --git a/luaui/Widgets/gui_options.lua b/luaui/Widgets/gui_options.lua index e72d8037f93..bcf129ace67 100644 --- a/luaui/Widgets/gui_options.lua +++ b/luaui/Widgets/gui_options.lua @@ -553,7 +553,8 @@ function updateInputDlist() activationArea[2] + chatlogHeightDiff - distance - inputHeight, x2, activationArea[2] + chatlogHeightDiff - distance, - "optionsinput" + "optionsinput", + widget ) end @@ -1831,7 +1832,8 @@ function widget:DrawScreen() guishaderedTabs = false end end) - WG.guishader.InsertDlist(backgroundGuishader, "options") + -- 'self' rather than 'widget': this function sits at the Lua 5.1 upvalue cap + WG.guishader.InsertDlist(backgroundGuishader, "options", nil, self) end end showOnceMore = false @@ -2229,14 +2231,16 @@ function widget:DrawScreen() optionButtons[showSelectOptions][2], optionButtons[showSelectOptions][3], optionButtons[showSelectOptions][4], - "options_select" + "options_select", + self ) WG.guishader.InsertScreenRect( optionButtons[showSelectOptions][1], yPos - oHeight - oPadding, optionButtons[showSelectOptions][1] + maxWidth, optionButtons[showSelectOptions][2], - "options_select_options" + "options_select_options", + self ) WG.guishader.insertRenderDlist(selectOptionsList) else @@ -7031,6 +7035,21 @@ function init() end, }, + { + id = "windows_hideinterface", + group = "ui", + category = types.basic, + name = BAR.I18N("ui.settings.option.windows_hideinterface"), + type = "bool", + value = Spring.GetConfigInt("WindowsHideInterface", 0) == 1, + description = BAR.I18N("ui.settings.option.windows_hideinterface_descr"), + onchange = function(i, value) + Spring.SetConfigInt("WindowsHideInterface", (value and 1 or 0)) + -- pushed through as well so it takes effect now instead of at the next poll + widgetHandler:SetWindowsHideInterface(value) + end, + }, + { id = "label_ui_visuals", group = "ui", @@ -12672,6 +12691,12 @@ function widget:Initialize() Spring.SendCommands("minimap unitsize " .. (Spring.GetConfigFloat("MinimapIconScale", 3.5))) -- spring won't remember what you set with '/minimap iconssize #' + -- lets the handler hide the rest of the interface while the window is open + -- (this widget holds the real widgetHandler, so it passes itself) + widgetHandler:RegisterModalWindow(widget, function() + return show == true + end) + WG.options = {} WG.options.toggle = function(state) local newShow = state diff --git a/luaui/Widgets/gui_pausescreen.lua b/luaui/Widgets/gui_pausescreen.lua index f6f1c6c76d6..c4eb012cc96 100644 --- a/luaui/Widgets/gui_pausescreen.lua +++ b/luaui/Widgets/gui_pausescreen.lua @@ -12,6 +12,7 @@ function widget:GetInfo() version = "1.34", layer = 999999, enabled = true, + modalExempt = true, -- some windows pause the game, so this has to stay visible } end diff --git a/luaui/Widgets/gui_scavenger_info.lua b/luaui/Widgets/gui_scavenger_info.lua index f5f5847678a..ee6f9d8fd25 100644 --- a/luaui/Widgets/gui_scavenger_info.lua +++ b/luaui/Widgets/gui_scavenger_info.lua @@ -232,7 +232,7 @@ function widget:DrawScreen() RectRound(titleRect[1], titleRect[2], titleRect[3], titleRect[4], elementCorner, 1, 1, 0, 0) end) dlistcreated = true - WG.guishader.InsertDlist(backgroundGuishader, "text") + WG.guishader.InsertDlist(backgroundGuishader, "text", nil, widget) end showOnceMore = false @@ -313,6 +313,11 @@ end function widget:Initialize() if textFile then + -- lets the handler hide the rest of the interface while the panel is open + widgetHandler:RegisterModalWindow(function() + return show == true + end) + WG.scavengerinfo = {} WG.scavengerinfo.toggle = function(state) if state ~= nil then diff --git a/luaui/Widgets/gui_teamstats.lua b/luaui/Widgets/gui_teamstats.lua index cbea9bf4e55..06583a7b805 100644 --- a/luaui/Widgets/gui_teamstats.lua +++ b/luaui/Widgets/gui_teamstats.lua @@ -281,6 +281,11 @@ function widget:Initialize() widget:GameFrame(GetGameFrame(), true) end + -- lets the handler hide the rest of the interface while the panel is open + widgetHandler:RegisterModalWindow(function() + return guiData.mainPanel.visible == true + end) + WG.teamstats = {} WG.teamstats.toggle = function(state) if state ~= nil then @@ -614,7 +619,7 @@ local function DrawBackground() backgroundGuishader = glCreateList(function() RectRound(x1 - bgpadding, y1 - bgpadding, x2 + bgpadding, y2 + bgpadding, elementCorner) end) - WG.guishader.InsertDlist(backgroundGuishader, "teamstats_window") + WG.guishader.InsertDlist(backgroundGuishader, "teamstats_window", nil, widget) end if backgroundDisplayList then diff --git a/luaui/Widgets/gui_tooltip.lua b/luaui/Widgets/gui_tooltip.lua index 362d7fad02f..040fe7b3522 100644 --- a/luaui/Widgets/gui_tooltip.lua +++ b/luaui/Widgets/gui_tooltip.lua @@ -9,6 +9,7 @@ function widget:GetInfo() license = "GNU GPL, v2 or later", layer = -1200000, enabled = true, + modalExempt = true, -- the windows show their own tooltips through this widget } end @@ -424,14 +425,16 @@ local function drawTooltip(name, x, y) posY - maxHeight - paddingH, posX + maxWidth + paddingW - bgpadding, posY + paddingH, - "tooltip_" .. name + "tooltip_" .. name, + widget ) WG.guishader.InsertScreenRect( posX - paddingW, posY - maxHeight - paddingH + bgpadding, posX + maxWidth + paddingW, posY + paddingH - bgpadding, - "2tooltip_" .. name + "2tooltip_" .. name, + widget ) end @@ -467,6 +470,10 @@ function widget:DrawScreen() local x, y = spGetMouseState() local now = os.clock() + -- Hover tooltips belong to elements that a modal window is hiding, so they must not + -- pop up over it. Tooltips shown outright (option descriptions and the like) stay. + local modalActive = widgetHandler:IsModalActive() + if WG.guishader then for name, _ in pairs(cleanupGuishaderAreas) do WG.guishader.RemoveScreenRect("tooltip_" .. name) @@ -479,6 +486,7 @@ function widget:DrawScreen() (tooltip.area == nil and not tooltip.disabled) or ( tooltip.area + and not modalActive and tooltip.area[4] ~= nil and math_isInRect(x, y, tooltip.area[1], tooltip.area[2], tooltip.area[3], tooltip.area[4]) ) diff --git a/luaui/Widgets/gui_top_bar.lua b/luaui/Widgets/gui_top_bar.lua index 765766976f1..59c6931b729 100644 --- a/luaui/Widgets/gui_top_bar.lua +++ b/luaui/Widgets/gui_top_bar.lua @@ -10,6 +10,7 @@ function widget:GetInfo() layer = -95000, enabled = true, handler = true, --can use widgetHandler:x() + modalExempt = true, -- its menu buttons are how the player switches/closes windows } end @@ -211,6 +212,10 @@ local cache = { 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. @@ -1830,6 +1835,13 @@ 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]) @@ -2574,7 +2586,7 @@ local function drawQuitScreen() end local function drawUiBackground() - if showResourceBars then + if showResourceBars and not cache.modalActive then if resbarArea.energy[1] then local energySkew = cfg.useSkew and { brx = -((resbarArea.energy[4] - resbarArea.energy[2]) * skewTan) } or nil @@ -2623,7 +2635,7 @@ local function drawUiBackground() ) end end - if cache.showIndicators and comsArea[1] then + if cache.showIndicators and not cache.modalActive and comsArea[1] then local H = comsArea[4] - comsArea[2] local smallSkew = cfg.useSkew and { blx = -(H * skewTan), brx = -(H * skewTan) } or nil UiElement( @@ -2647,7 +2659,7 @@ local function drawUiBackground() smallSkew ) end - if cache.showIndicators and windArea[1] then + if cache.showIndicators and not cache.modalActive and windArea[1] then local H = windArea[4] - windArea[2] local smallSkew = cfg.useSkew and { blx = -(H * skewTan), brx = -(H * skewTan) } or nil UiElement( @@ -2671,7 +2683,7 @@ local function drawUiBackground() smallSkew ) end - if cache.showIndicators and displayTidalSpeed and tidalarea[1] then + if cache.showIndicators and not cache.modalActive 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( @@ -2721,7 +2733,7 @@ local function drawUi() if showButtons and dlist.buttons then glCallList(dlist.buttons) end - if showResourceBars and dlist.resbar.energy and dlist.resbar.energy[1] then + if showResourceBars and not cache.modalActive and dlist.resbar.energy and dlist.resbar.energy[1] then glCallList(dlist.resbar.energy[1]) glCallList(dlist.resbar.metal[1]) end @@ -2730,7 +2742,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 noWind then + if cache.showIndicators and not cache.modalActive 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( @@ -2751,7 +2763,7 @@ local function drawUi() end -- tidal speed - if cache.showIndicators and displayTidalSpeed then + if cache.showIndicators and not cache.modalActive 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) @@ -2843,6 +2855,14 @@ function widget:DrawScreen() 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) @@ -2909,7 +2929,8 @@ function widget:DrawScreen() gl.TexRect(topbarArea[1], topbarArea[2], topbarArea[3], topbarArea[4], false, true) gl.Texture(false) end) - WG.guishader.InsertDlist(uiBgList, "topbar_background") + -- 'self' rather than 'widget': this function sits at the Lua 5.1 upvalue cap + WG.guishader.InsertDlist(uiBgList, "topbar_background", nil, self) end end @@ -2917,7 +2938,7 @@ function widget:DrawScreen() glCallList(dlist.blendBg) end - if cache.showIndicators and dlist.wind1 then + if cache.showIndicators and not cache.modalActive and dlist.wind1 then glPushMatrix() glCallList(dlist.wind1) glRotate(windRotation, 0, 0, 1) @@ -2925,7 +2946,7 @@ function widget:DrawScreen() glPopMatrix() end - if cache.showIndicators and displayTidalSpeed and dlist.tidal2 then + if cache.showIndicators and not cache.modalActive 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) @@ -2948,6 +2969,7 @@ 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) @@ -2986,7 +3008,7 @@ function widget:DrawScreen() end -- current wind - if cache.showIndicators and not noWind then + if cache.showIndicators and not cache.modalActive and not noWind then if currentWind ~= prevWind or refreshUi then prevWind = currentWind windTextScissor[1] = windArea[1] - topbarArea[1] @@ -2998,10 +3020,12 @@ function widget:DrawScreen() end end - drawResBars() + if not cache.modalActive then + drawResBars() + end glPushMatrix() - if cache.showIndicators and displayComCounter and dlist.coms then + if cache.showIndicators and not cache.modalActive and displayComCounter and dlist.coms then -- commander counter if refreshUi @@ -3438,7 +3462,8 @@ function widget:MousePress(x, y, button) return true end - if not spec then + -- 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 isSingle then if mathIsInRect( @@ -3616,6 +3641,13 @@ 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() diff --git a/luaui/Widgets/gui_vote_interface.lua b/luaui/Widgets/gui_vote_interface.lua index 3d4e37dbb16..ab335ef46b7 100644 --- a/luaui/Widgets/gui_vote_interface.lua +++ b/luaui/Widgets/gui_vote_interface.lua @@ -9,6 +9,7 @@ function widget:GetInfo() license = "GNU GPL, v2 or later", layer = -985000, enabled = true, + modalExempt = true, -- a vote must not be missed because a window is open } end @@ -531,7 +532,7 @@ local function StartVote(name) -- when called without params its just to refresh dlistGuishader = gl.CreateList(function() RectRound(windowArea[1], windowArea[2], windowArea[3], windowArea[4], elementCorner) end) - WG.guishader.InsertDlist(dlistGuishader, "voteinterface") + WG.guishader.InsertDlist(dlistGuishader, "voteinterface", nil, widget) end end diff --git a/luaui/Widgets/widget_selector.lua b/luaui/Widgets/widget_selector.lua index 19857464aad..6b998374ead 100644 --- a/luaui/Widgets/widget_selector.lua +++ b/luaui/Widgets/widget_selector.lua @@ -276,7 +276,8 @@ function drawChatInput() activationArea[2] + chatlogHeightDiff - distance - inputHeight, x2, activationArea[2] + chatlogHeightDiff - distance, - "selectorinput" + "selectorinput", + widget ) end @@ -430,6 +431,12 @@ function widget:Initialize() widgetHandler.knownChanged = true Spring.SendCommands("unbindkeyset f11") + -- lets the handler hide the rest of the interface while the list is open + -- (this widget holds the real widgetHandler, so it passes itself) + widgetHandler:RegisterModalWindow(widget, function() + return show == true + end) + WG.widgetselector = {} WG.widgetselector.toggle = function(state) local newShow = state @@ -968,8 +975,8 @@ function widget:DrawScreen() if WG.guishader and not activeGuishader then activeGuishader = true if dlistGuishader then - WG.guishader.InsertDlist(dlistGuishader, "widgetselector") - WG.guishader.InsertDlist(dlistGuishader2, "widgetselector2") + WG.guishader.InsertDlist(dlistGuishader, "widgetselector", nil, widget) + WG.guishader.InsertDlist(dlistGuishader2, "widgetselector2", nil, widget) end end diff --git a/luaui/barwidgets.lua b/luaui/barwidgets.lua index 7d8fa545f34..4ca6c3b0479 100644 --- a/luaui/barwidgets.lua +++ b/luaui/barwidgets.lua @@ -429,6 +429,7 @@ function widgetHandler:Initialize() widgetHandler:CreateQueuedReorderFuncs() widgetHandler:HookReorderSpecialFuncs() self:LoadConfigData() + self:SetWindowsHideInterface(Spring.GetConfigInt("WindowsHideInterface", 0) == 1) if self.allowUserWidgets == nil then self.allowUserWidgets = true @@ -784,6 +785,26 @@ function widgetHandler:NewWidget(enableLocalsAccess, fromZip, filename) wh.SetGlobal = function(_, name, value) return self:SetGlobal(widget, name, value) end + + -- Modal windows (see the "Modal windows" block further down) + wh.RegisterModalWindow = function(_, isOpen) + return self:RegisterModalWindow(widget, isOpen) + end + wh.DeregisterModalWindow = function(_) + return self:DeregisterModalWindow(widget) + end + wh.IsModalActive = function(_) + return self:IsModalActive() + end + wh.ModalAllows = function(_, owner) + return self:ModalAllows(owner) + end + wh.GetModalRevision = function(_) + return self:GetModalRevision() + end + wh.SetWindowsHideInterface = function(_, enabled) + return self:SetWindowsHideInterface(enabled) + end tracy.ZoneEnd() return widget end @@ -805,6 +826,7 @@ function widgetHandler:FinalizeWidget(widget, filename, basename) wi.license = info.license or "" wi.enabled = info.enabled or false wi.hidden = info.hidden or false + wi.modalExempt = info.modalExempt or false end widget.whInfo = {} -- a proxy table @@ -1078,6 +1100,8 @@ function widgetHandler:InsertWidgetRaw(widget) SafeWrapWidget(widget) + self:ApplyModalExempt(widget) + ArrayInsert(self.widgets, true, widget) for _, listname in ipairs(callInLists) do local func = widget[listname] @@ -1113,6 +1137,7 @@ function widgetHandler:RemoveWidgetRaw(widget) widget:Shutdown() end ArrayRemove(self.widgets, widget) + self:ForgetModalWidget(widget) self:RemoveWidgetGlobals(widget) self.actionHandler:RemoveWidgetActions(widget) for _, listname in ipairs(callInLists) do @@ -1448,6 +1473,130 @@ function widgetHandler:BlankOut() end end +-------------------------------------------------------------------------------- +-- +-- Modal windows +-- +-- Optional behaviour (springsetting "WindowsHideInterface", default off): while one +-- of the big central windows is open, the rest of the screen interface is neither +-- drawn nor clickable, so the window is the only thing the player can interact with. +-- +-- A window widget opts in from its Initialize with an is-open predicate: +-- widgetHandler:RegisterModalWindow(function() return show end) +-- (widgets with handler = true get the real handler, so they pass themselves first: +-- widgetHandler:RegisterModalWindow(widget, function() return show end)) +-- +-- A widget that must stay visible and usable regardless declares it in GetInfo: +-- modalExempt = true, +-- +-- Filtered while a window is open: DrawScreen, KeyPress, TextInput, MousePress, +-- MouseWheel, IsAbove and GetTooltip, and a MousePress nothing claimed is swallowed +-- so clicks never reach the engine (no unit orders under an open window). +-- Deliberately not filtered: KeyRelease/MouseRelease (a hidden widget must still see +-- the release of a press it got before the window opened), Update, the world and +-- effects draw passes, and the action handler, so keybinds keep working. +-- +-- 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. +-- +local modalWindows = {} -- widget -> is-open predicate +local modalExempt = {} -- widget -> true +local modalAllowed = {} -- widget -> true (exempt widgets and registered windows) +local modalActive = false +local modalRevision = 0 +local modalEnabled = false +local modalConfigTimer = 0 +local MODAL_CONFIG_INTERVAL = 1 -- seconds between config re-reads (picks up /set) + +local function ModalAllowWidget(widget, allowed) + if (modalAllowed[widget] or false) ~= allowed then + modalAllowed[widget] = allowed or nil + modalRevision = modalRevision + 1 + end +end + +function widgetHandler:RegisterModalWindow(widget, isOpen) + if type(isOpen) ~= "function" then + Spring.Log("barwidgets.lua", LOG.ERROR, "RegisterModalWindow: expected an is-open function") + return false + end + modalWindows[widget] = isOpen + ModalAllowWidget(widget, true) + return true +end + +function widgetHandler:DeregisterModalWindow(widget) + modalWindows[widget] = nil + ModalAllowWidget(widget, modalExempt[widget] == true) +end + +-- Called from InsertWidgetRaw/RemoveWidgetRaw, which are defined above this block and +-- so reach the state through the handler rather than as upvalues. +function widgetHandler:ApplyModalExempt(widget) + if widget.whInfo and widget.whInfo.modalExempt then + modalExempt[widget] = true + ModalAllowWidget(widget, true) + end +end + +function widgetHandler:ForgetModalWidget(widget) + modalWindows[widget] = nil + modalExempt[widget] = nil + ModalAllowWidget(widget, false) +end + +function widgetHandler:IsModalActive() + return modalActive +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 +-- region) counts as not allowed, which is the wanted default. +function widgetHandler:ModalAllows(widget) + return (not modalActive) or (modalAllowed[widget] == true) +end + +function widgetHandler:SetWindowsHideInterface(enabled) + modalEnabled = enabled and true or false + modalConfigTimer = 0 + self:UpdateModalState() +end + +function widgetHandler:UpdateModalState(deltaTime) + if deltaTime then + modalConfigTimer = modalConfigTimer + deltaTime + if modalConfigTimer >= MODAL_CONFIG_INTERVAL then + modalConfigTimer = 0 + modalEnabled = (Spring.GetConfigInt("WindowsHideInterface", 0) == 1) + end + end + + local active = false + if modalEnabled then + for w, isOpen in pairs(modalWindows) do + local ok, open = pcall(isOpen) + if not ok then + Spring.Log( + "barwidgets.lua", + LOG.ERROR, + "modal window check failed for " .. tostring(w.whInfo and w.whInfo.name) .. ": " .. tostring(open) + ) + modalWindows[w] = nil -- removing the current key mid-traversal is allowed + ModalAllowWidget(w, modalExempt[w] == true) + elseif open then + active = true + break + end + end + end + modalActive = active +end + local gcCheckCounter = 0 function widgetHandler:Update() @@ -1463,6 +1612,11 @@ function widgetHandler:Update() local deltaTime = Spring.GetLastUpdateSeconds() -- update the hour timer hourTimer = (hourTimer + deltaTime) % 3600.0 + + -- before the widgets run, so a widget asking IsModalActive this frame sees the + -- window that was opened by the input events just handled + self:UpdateModalState(deltaTime) + tracy.ZoneBeginN("W:Update") for _, w in ipairs(self.UpdateList) do tracy.ZoneBeginN("W:Update:" .. w.whInfo.name) @@ -1624,14 +1778,19 @@ end function widgetHandler:DrawScreen() tracy.ZoneBeginN("W:DrawScreen") + -- catches a window opened from another widget's Update, which ran after the + -- recompute at the top of widgetHandler:Update + self:UpdateModalState() if not Spring.IsGUIHidden() then if not self.chobbyInterface then local list = self.DrawScreenList for i = #list, 1, -1 do local w = list[i] - tracy.ZoneBeginN(w._tracyDrawScreenName) - w:DrawScreen() - tracy.ZoneEnd() + if not modalActive or modalAllowed[w] then + tracy.ZoneBeginN(w._tracyDrawScreenName) + w:DrawScreen() + tracy.ZoneEnd() + end end elseif widgetHandler.WG.guishader and widgetHandler.WG.guishader.DrawScreen then tracy.ZoneBeginN("W:DrawScreen:guishader") @@ -1977,9 +2136,11 @@ function widgetHandler:KeyPress(key, mods, isRepeat, label, unicode, scanCode, a end for _, w in ipairs(self.KeyPressList) do - if w:KeyPress(key, mods, isRepeat, label, unicode, scanCode, actions) then - tracy.ZoneEnd() - return true + if not modalActive or modalAllowed[w] then + if w:KeyPress(key, mods, isRepeat, label, unicode, scanCode, actions) then + tracy.ZoneEnd() + return true + end end end tracy.ZoneEnd() @@ -2027,9 +2188,11 @@ function widgetHandler:TextInput(utf8, ...) local list = self.TextInputList for i = #list, 1, -1 do - if list[i]:TextInput(utf8, ...) then - tracy.ZoneEnd() - return true + if not modalActive or modalAllowed[list[i]] then + if list[i]:TextInput(utf8, ...) then + tracy.ZoneEnd() + return true + end end end tracy.ZoneEnd() @@ -2045,7 +2208,7 @@ end function widgetHandler:WidgetAt(x, y) tracy.ZoneBeginN("W:WidgetAt") for _, w in ipairs(self.IsAboveList) do - if w:IsAbove(x, y) then + if (not modalActive or modalAllowed[w]) and w:IsAbove(x, y) then tracy.ZoneEnd() return w end @@ -2057,23 +2220,29 @@ end function widgetHandler:MousePress(x, y, button) tracy.ZoneBeginN("W:MousePress") if self.mouseOwner then + -- unfiltered: a drag that started before the window opened has to finish self.mouseOwner:MousePress(x, y, button) else for _, w in ipairs(self.MousePressList) do - if w:MousePress(x, y, button) then - self.mouseOwner = w - break + if not modalActive or modalAllowed[w] then + if w:MousePress(x, y, button) then + self.mouseOwner = w + break + end end end end - local hasMouseOwner = self.mouseOwner ~= nil + -- 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 if widgetHandler.WG.SmartSelect_MousePress2 then - widgetHandler.WG.SmartSelect_MousePress2(x, y, button, hasMouseOwner) + widgetHandler.WG.SmartSelect_MousePress2(x, y, button, consumed) end tracy.ZoneEnd() - return hasMouseOwner + return consumed end function widgetHandler:MouseMove(x, y, dx, dy, button) @@ -2108,7 +2277,7 @@ function widgetHandler:MouseWheel(up, value) end tracy.ZoneBeginN("W:MouseWheel") for _, w in ipairs(self.MouseWheelList) do - if w:MouseWheel(up, value) then + if (not modalActive or modalAllowed[w]) and w:MouseWheel(up, value) then tracy.ZoneEnd() return true end @@ -2221,7 +2390,7 @@ end function widgetHandler:GetTooltip(x, y) tracy.ZoneBeginN("W:GetTooltip") for _, w in ipairs(self.GetTooltipList) do - if w:IsAbove(x, y) then + if (not modalActive or modalAllowed[w]) and w:IsAbove(x, y) then local tip = w:GetTooltip(x, y) if type(tip) == "string" and #tip > 0 then tracy.ZoneEnd() diff --git a/types/Addon.lua b/types/Addon.lua index ab7e5a155a8..7301f6160ad 100644 --- a/types/Addon.lua +++ b/types/Addon.lua @@ -16,6 +16,9 @@ function Addon:GetInfo() end ---@field license string? ---@field layer number? ---@field enabled boolean? +---Keep drawing and receiving input while an open modal window hides the rest of the +---interface (LuaUI only, see the "Modal windows" block in barwidgets.lua). +---@field modalExempt boolean? ---@type Addon ---@diagnostic disable-next-line: lowercase-global From 9067b493ac6fdf4b16ba1677f8133a430ac1c7cd Mon Sep 17 00:00:00 2001 From: PrivacyIsARight Date: Fri, 11 Sep 2026 16:17:02 -0400 Subject: [PATCH 2/3] Fix console color wrapping (#9199) Resolves funky line-wrapping shenanigans that do not carry line-level font coloring to newly wrapped lines. --- luaui/Widgets/gui_chat.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/luaui/Widgets/gui_chat.lua b/luaui/Widgets/gui_chat.lua index 4acbbf195a4..c5200c54be6 100644 --- a/luaui/Widgets/gui_chat.lua +++ b/luaui/Widgets/gui_chat.lua @@ -1729,7 +1729,9 @@ local function processAddConsoleLine(gameFrame, line, orgLineID, reprocessID) end end - line = colorConsoleStr .. lineColor .. line + if string.byte(line, 1) ~= 255 then + line = (lineColor ~= "" and lineColor or colorConsoleStr) .. line + end end if not bypassThisMessage then From 0d73feb936fee2ad8d20ab4c86eefc1e993803bd Mon Sep 17 00:00:00 2001 From: Floris Date: Fri, 11 Sep 2026 22:19:18 +0200 Subject: [PATCH 3/3] pip: fix minimized pip resolution change position/sizing issue (#9209) --- luaui/Widgets/gui_pip.lua | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/luaui/Widgets/gui_pip.lua b/luaui/Widgets/gui_pip.lua index 4b24b0e54b4..edcdd59c9ab 100644 --- a/luaui/Widgets/gui_pip.lua +++ b/luaui/Widgets/gui_pip.lua @@ -9841,8 +9841,17 @@ function widget:ViewResize() font = WG.fonts.getFont(2) local oldVsx, oldVsy = render.vsx, render.vsy + local oldWidgetScale = render.widgetScale render.vsx, render.vsy = Spring.GetViewGeometry() + -- Update the UI scale before any dimension validation below: helpers like + -- AreExpandedDimensionsValid/BuildDefaultExpandedDimensions derive the minimum + -- panel size from it, and they are applied to dimensions already rescaled to the + -- new resolution. Keeping the old scale here made a shrink falsely reject valid + -- saved dimensions (and build oversized defaults). + render.widgetScale = (render.vsy / 2000) * render.uiScale + render.usedButtonSize = math.floor(config.buttonSize * render.widgetScale * render.uiScale) + -- In minimap mode, calculate position and size like the minimap widget does if isMinimapMode then -- Use mapEdgeMargin = 0 in minimap mode @@ -9971,7 +9980,9 @@ function widget:ViewResize() else -- Normal PIP mode: scale dimensions with screen size -- When in minMode, render.dim is the tiny button — use savedDimensions as the real dimensions - local minSize = math.floor(config.minPanelSize * render.widgetScale) + -- render.dim is still in old-resolution pixels here, so validate it against the + -- minimum size of the *old* scale. + local minSize = math.floor(config.minPanelSize * (oldWidgetScale or render.widgetScale)) -- Capture old PIP width before rescaling so we can adjust zoom proportionally local oldPipWidth @@ -10000,8 +10011,20 @@ function widget:ViewResize() if uiState.inMinMode then -- In min mode, render.dim is the tiny button — don't validate it as expanded dims. - -- Just ensure we have valid savedDimensions (or build defaults). - if not AreExpandedDimensionsValid(uiState.savedDimensions) then + -- savedDimensions was just rescaled into the new resolution; repair it in place + -- (grow to the new minimum size, keep the user's position) the same way + -- CorrectScreenPosition repairs render.dim when not minimized. Only genuinely + -- corrupt dimensions fall back to defaults, so a resize never teleports the PIP. + if AreDimensionsValid(uiState.savedDimensions, 1, 1) then + local newMinSize = math.floor(config.minPanelSize * render.widgetScale) + if uiState.savedDimensions.r - uiState.savedDimensions.l < newMinSize then + uiState.savedDimensions.r = uiState.savedDimensions.l + newMinSize + end + if uiState.savedDimensions.t - uiState.savedDimensions.b < newMinSize then + uiState.savedDimensions.t = uiState.savedDimensions.b + newMinSize + end + ClampDimensionsToScreen(uiState.savedDimensions) + else uiState.savedDimensions = BuildDefaultExpandedDimensions() end -- render.dim will be overwritten to the button position below @@ -10064,8 +10087,7 @@ function widget:ViewResize() end end - render.widgetScale = (render.vsy / 2000) * render.uiScale - render.usedButtonSize = math.floor(config.buttonSize * render.widgetScale * render.uiScale) + -- (render.widgetScale / render.usedButtonSize are updated at the top of ViewResize) render.elementPadding = WG.FlowUI.elementPadding render.elementCorner = WG.FlowUI.elementCorner