Expose window margins to tool HUDs

This commit is contained in:
Kevin Jacobson
2026-07-28 14:09:46 -07:00
parent d09cf80da5
commit fb58fa788b
4 changed files with 37 additions and 21 deletions
+5 -4
View File
@@ -164,10 +164,11 @@ for a tool to offer a fresh-session action before gameplay begins.
Ephemeral tools can wrap `save.write(next, game)` and return `false` to veto a Ephemeral tools can wrap `save.write(next, game)` and return `false` to veto a
progress write before world state is captured or any bytes reach disk. progress write before world state is captured or any bytes reach disk.
`render.hud` receives `(next, game, viewport)` after visible states draw and `render.hud` receives `(next, game, viewport)` after the finished game frame is
before the UI canvas is presented. `viewport.width` and `viewport.height` are composited and before touch controls draw. The window-space viewport contains
the active UI dimensions, so status indicators can stay visible over normal `width`, `height`, `gameX`, `gameY`, `gameWidth`, `gameHeight`, `scale`, `dpiX`,
and widescreen screens without pushing an updating game state. and `dpiY`, so a tool can use the letterbox margins without drawing over the
playfield or pushing an updating game state.
Developer mode also arms the mod loader's dev tripwire, which flags mods Developer mode also arms the mod loader's dev tripwire, which flags mods
that reach outside their permission set. that reach outside their permission set.
+7 -9
View File
@@ -255,14 +255,6 @@ function Game:draw()
end end
Renderer:beginFrame(worldBelow) Renderer:beginFrame(worldBelow)
self.stack:draw() self.stack:draw()
-- Persistent tool status belongs above every game state but inside the
-- active UI canvas, so it composes with palette/render mods and survives
-- menus, battles, and transitions without becoming an updating state.
if ModRuntime.wantsHook("render.hud") then
local width, height = Renderer:uiSize()
ModRuntime.call("render.hud", function() end, self,
{ width = width, height = height })
end
-- SGB colorization: the topmost state that knows its palette owns the -- SGB colorization: the topmost state that knows its palette owns the
-- screen (overlays like text boxes inherit from what's beneath them); -- screen (overlays like text boxes inherit from what's beneath them);
-- the overworld's world pass colors each visible map area separately -- the overworld's world pass colors each visible map area separately
@@ -282,7 +274,13 @@ function Game:draw()
if worldBelow and self.overworld.sgbWorldZones then if worldBelow and self.overworld.sgbWorldZones then
worldZones = self.overworld:sgbWorldZones() worldZones = self.overworld:sgbWorldZones()
end end
Renderer:endFrame(zones, worldZones) local viewport = Renderer:endFrame(zones, worldZones)
-- Persistent tool status is screen-space UI: draw it over the completed
-- render pipeline with exact playfield/margin geometry, but below mobile
-- controls. It never becomes an updating game state.
if ModRuntime.wantsHook("render.hud") then
ModRuntime.call("render.hud", function() end, self, viewport)
end
-- on-screen mobile controls: pure screen-space, over the finished frame -- on-screen mobile controls: pure screen-space, over the finished frame
TouchControls:draw() TouchControls:draw()
end end
+7
View File
@@ -706,6 +706,13 @@ function Renderer:endFrame(zones, worldZones)
self.uprightActive = false self.uprightActive = false
self.worldOverride = nil self.worldOverride = nil
PaletteFX.setPass(nil) PaletteFX.setPass(nil)
return {
width = ww, height = wh,
gameX = ox, gameY = oy,
gameWidth = vpw, gameHeight = vph,
scale = Sp,
dpiX = dpiX, dpiY = dpiY,
}
end end
return Renderer return Renderer
+18 -8
View File
@@ -86,9 +86,9 @@ do
hooks:removeOwner("tool_fixture") hooks:removeOwner("tool_fixture")
end end
-- A tool status indicator must draw after every visible game state but before -- A tool status indicator draws in window space after the renderer composites
-- the renderer presents that frame. This keeps the HUD visible over the -- the game. This gives it exact playfield/margin geometry and keeps it crisp
-- overworld, menus, battles, and compatible render pipelines. -- over compatible render pipelines without entering the game canvas.
do do
local Renderer = require("src.render.Renderer") local Renderer = require("src.render.Renderer")
local TouchControls = require("src.core.TouchControls") local TouchControls = require("src.core.TouchControls")
@@ -98,7 +98,15 @@ do
local order = {} local order = {}
Renderer.setUISize = function() end Renderer.setUISize = function() end
Renderer.beginFrame = function() end Renderer.beginFrame = function() end
Renderer.endFrame = function() order[#order + 1] = "present" end Renderer.endFrame = function()
order[#order + 1] = "present"
return {
width = 1024, height = 768,
gameX = 112, gameY = 24,
gameWidth = 800, gameHeight = 720,
scale = 5,
}
end
TouchControls.draw = function() order[#order + 1] = "touch" end TouchControls.draw = function() order[#order + 1] = "touch" end
local fake = { overworld = {}, stack = { states = {} } } local fake = { overworld = {}, stack = { states = {} } }
@@ -108,15 +116,17 @@ do
hooks:wrap("render.hud", function(nextFn, game, viewport) hooks:wrap("render.hud", function(nextFn, game, viewport)
T.check(game == fake, "render.hud receives the live Game object") T.check(game == fake, "render.hud receives the live Game object")
T.eq(viewport.width, 160, "render.hud receives the UI width") T.eq(viewport.width, 1024, "render.hud receives the window width")
T.eq(viewport.height, 144, "render.hud receives the UI height") T.eq(viewport.height, 768, "render.hud receives the window height")
T.eq(viewport.gameX, 112, "render.hud receives the playfield origin")
T.eq(viewport.gameWidth, 800, "render.hud receives the playfield width")
order[#order + 1] = "hud" order[#order + 1] = "hud"
return nextFn(game, viewport) return nextFn(game, viewport)
end, 0, "tool_fixture") end, 0, "tool_fixture")
require("src.core.Game").draw(fake) require("src.core.Game").draw(fake)
T.eq(table.concat(order, ","), "states,hud,present,touch", T.eq(table.concat(order, ","), "states,present,hud,touch",
"render.hud draws over states before frame presentation") "render.hud draws after frame composition and before touch controls")
hooks:removeOwner("tool_fixture") hooks:removeOwner("tool_fixture")
Renderer.setUISize, Renderer.beginFrame, Renderer.endFrame, Renderer.setUISize, Renderer.beginFrame, Renderer.endFrame,
TouchControls.draw = savedSetUISize, savedBegin, savedEnd, savedTouch TouchControls.draw = savedSetUISize, savedBegin, savedEnd, savedTouch