mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 13:09:54 +02:00
Respect iOS/Android safe areas in launcher and touch chrome.
Layout interactive UI against love.window.getSafeArea so notch, Dynamic Island, and home-indicator insets no longer clip controls, while keeping the game framebuffer edge-to-edge. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
-- Usable window rect for mobile chrome (notch / Dynamic Island / home
|
||||||
|
-- indicator / Android display cutouts). Wraps love.window.getSafeArea when
|
||||||
|
-- the engine provides it; otherwise the full graphics window.
|
||||||
|
--
|
||||||
|
-- Desktop and headless stubs return the full window, so callers can always
|
||||||
|
-- layout against this rect without platform branches. Interactive chrome
|
||||||
|
-- (touch overlay, launcher) should prefer this over getDimensions; the game
|
||||||
|
-- canvas may still letterbox into the full framebuffer for immersion.
|
||||||
|
|
||||||
|
local SafeArea = {}
|
||||||
|
|
||||||
|
function SafeArea.rect()
|
||||||
|
local ww, wh = 0, 0
|
||||||
|
if love and love.graphics and love.graphics.getDimensions then
|
||||||
|
ww, wh = love.graphics.getDimensions()
|
||||||
|
end
|
||||||
|
if ww <= 0 then ww = 1 end
|
||||||
|
if wh <= 0 then wh = 1 end
|
||||||
|
|
||||||
|
if not (love and love.window and love.window.getSafeArea) then
|
||||||
|
return 0, 0, ww, wh
|
||||||
|
end
|
||||||
|
|
||||||
|
local x, y, w, h = love.window.getSafeArea()
|
||||||
|
if type(x) ~= "number" or type(y) ~= "number"
|
||||||
|
or type(w) ~= "number" or type(h) ~= "number"
|
||||||
|
or w <= 0 or h <= 0 then
|
||||||
|
return 0, 0, ww, wh
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Clamp to the drawable window so a bad / mid-rotation backend cannot
|
||||||
|
-- push layout outside the surface.
|
||||||
|
x = math.max(0, math.min(x, ww))
|
||||||
|
y = math.max(0, math.min(y, wh))
|
||||||
|
w = math.max(1, math.min(w, ww - x))
|
||||||
|
h = math.max(1, math.min(h, wh - y))
|
||||||
|
return x, y, w, h
|
||||||
|
end
|
||||||
|
|
||||||
|
return SafeArea
|
||||||
+42
-26
@@ -23,6 +23,7 @@
|
|||||||
-- and a player rebind can never detach the overlay.
|
-- and a player rebind can never detach the overlay.
|
||||||
|
|
||||||
local Input = require("src.core.Input")
|
local Input = require("src.core.Input")
|
||||||
|
local SafeArea = require("src.core.SafeArea")
|
||||||
|
|
||||||
local TouchControls = {}
|
local TouchControls = {}
|
||||||
|
|
||||||
@@ -92,20 +93,23 @@ function TouchControls.normalizeConfig(tc)
|
|||||||
return out
|
return out
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Pure default layout in LOVE units for a given window size. Shared by
|
-- Pure default layout in LOVE units for a usable rect of size ww x wh at
|
||||||
-- layout() and the editor's Reset path so defaults stay in one place.
|
-- origin (ox, oy). Shared by layout() and the editor's Reset path so
|
||||||
function TouchControls.defaultLayout(ww, wh)
|
-- defaults stay in one place. ox/oy default to 0 for the headless tests
|
||||||
|
-- and for callers that already pass a full-window size.
|
||||||
|
function TouchControls.defaultLayout(ww, wh, ox, oy)
|
||||||
|
ox, oy = ox or 0, oy or 0
|
||||||
local short = math.min(ww, wh)
|
local short = math.min(ww, wh)
|
||||||
local dpadW = math.min(180, short * 0.34)
|
local dpadW = math.min(180, short * 0.34)
|
||||||
local abW = dpadW * 0.46
|
local abW = dpadW * 0.46
|
||||||
local ssW = dpadW * 0.30
|
local ssW = dpadW * 0.30
|
||||||
local margin = dpadW * 0.12
|
local margin = dpadW * 0.12
|
||||||
return {
|
return {
|
||||||
dpad = { cx = margin + dpadW / 2, cy = wh - margin - dpadW / 2, w = dpadW },
|
dpad = { cx = ox + margin + dpadW / 2, cy = oy + wh - margin - dpadW / 2, w = dpadW },
|
||||||
a = { cx = ww - margin - abW * 0.55, cy = wh - margin - abW * 1.75, w = abW },
|
a = { cx = ox + ww - margin - abW * 0.55, cy = oy + wh - margin - abW * 1.75, w = abW },
|
||||||
b = { cx = ww - margin - abW * 1.60, cy = wh - margin - abW * 0.55, w = abW },
|
b = { cx = ox + ww - margin - abW * 1.60, cy = oy + wh - margin - abW * 0.55, w = abW },
|
||||||
start = { cx = ww / 2 + ssW * 0.60, cy = wh - margin - ssW * 0.95, w = ssW },
|
start = { cx = ox + ww / 2 + ssW * 0.60, cy = oy + wh - margin - ssW * 0.95, w = ssW },
|
||||||
select = { cx = ww / 2 - ssW * 0.60, cy = wh - margin - ssW * 0.95, w = ssW },
|
select = { cx = ox + ww / 2 - ssW * 0.60, cy = oy + wh - margin - ssW * 0.95, w = ssW },
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -155,6 +159,7 @@ function TouchControls:applyOptions(opts)
|
|||||||
self.enabled = cfg.enabled
|
self.enabled = cfg.enabled
|
||||||
self.positions = cfg.positions
|
self.positions = cfg.positions
|
||||||
self.layoutW, self.layoutH = nil, nil
|
self.layoutW, self.layoutH = nil, nil
|
||||||
|
self.layoutOx, self.layoutOy = nil, nil
|
||||||
if not self.enabled then
|
if not self.enabled then
|
||||||
self.controllerHidden = false
|
self.controllerHidden = false
|
||||||
self:reset()
|
self:reset()
|
||||||
@@ -185,30 +190,37 @@ function TouchControls:visible()
|
|||||||
and not self.controllerHidden
|
and not self.controllerHidden
|
||||||
end
|
end
|
||||||
|
|
||||||
local function clampZone(zone, ww, wh)
|
-- Keep a control fully inside the usable rect [x0,y0]..[x1,y1].
|
||||||
|
local function clampZone(zone, x0, y0, x1, y1)
|
||||||
local half = zone.w * 0.5
|
local half = zone.w * 0.5
|
||||||
zone.cx = math.max(half, math.min(ww - half, zone.cx))
|
zone.cx = math.max(x0 + half, math.min(x1 - half, zone.cx))
|
||||||
zone.cy = math.max(half, math.min(wh - half, zone.cy))
|
zone.cy = math.max(y0 + half, math.min(y1 - half, zone.cy))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Layout in LOVE units (density-independent on mobile), recomputed when
|
-- Layout in LOVE units (density-independent on mobile), recomputed when
|
||||||
-- the window size changes (rotation, resize). Default: d-pad bottom-left,
|
-- the window or safe area changes (rotation, resize, notch insets).
|
||||||
-- B/A bottom-right with A above B (the Game Boy diagonal), START/SELECT
|
-- Default: d-pad bottom-left, B/A bottom-right with A above B (the Game Boy
|
||||||
-- flanking the bottom center. Custom positions (normalized 0..1) override
|
-- diagonal), START/SELECT flanking the bottom center -- all inside the
|
||||||
-- centers while sizes stay derived from the short edge.
|
-- device safe area so thumbs clear the home indicator / cutouts.
|
||||||
|
-- Custom positions (normalized 0..1 within the safe rect) override centers
|
||||||
|
-- while sizes stay derived from the short edge.
|
||||||
function TouchControls:layout()
|
function TouchControls:layout()
|
||||||
local ww, wh = love.graphics.getDimensions()
|
local ox, oy, sw, sh = SafeArea.rect()
|
||||||
if self.layoutW == ww and self.layoutH == wh and self.L then return self.L end
|
if self.layoutW == sw and self.layoutH == sh
|
||||||
self.layoutW, self.layoutH = ww, wh
|
and self.layoutOx == ox and self.layoutOy == oy and self.L then
|
||||||
self.L = TouchControls.defaultLayout(ww, wh)
|
return self.L
|
||||||
|
end
|
||||||
|
self.layoutW, self.layoutH = sw, sh
|
||||||
|
self.layoutOx, self.layoutOy = ox, oy
|
||||||
|
self.L = TouchControls.defaultLayout(sw, sh, ox, oy)
|
||||||
if self.positions then
|
if self.positions then
|
||||||
for _, name in ipairs(CONTROLS) do
|
for _, name in ipairs(CONTROLS) do
|
||||||
local p = self.positions[name]
|
local p = self.positions[name]
|
||||||
local zone = self.L[name]
|
local zone = self.L[name]
|
||||||
if p and zone then
|
if p and zone then
|
||||||
zone.cx = p.x * ww
|
zone.cx = ox + p.x * sw
|
||||||
zone.cy = p.y * wh
|
zone.cy = oy + p.y * sh
|
||||||
clampZone(zone, ww, wh)
|
clampZone(zone, ox, oy, ox + sw, oy + sh)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -222,21 +234,25 @@ function TouchControls:layout()
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Move one control to a screen-space point and persist its normalized
|
-- Move one control to a screen-space point and persist its normalized
|
||||||
-- position. Used by the layout editor while dragging.
|
-- position within the safe rect. Used by the layout editor while dragging.
|
||||||
function TouchControls:setControlCenter(name, cx, cy)
|
function TouchControls:setControlCenter(name, cx, cy)
|
||||||
local ww, wh = love.graphics.getDimensions()
|
local ox, oy, sw, sh = SafeArea.rect()
|
||||||
local L = self:layout()
|
local L = self:layout()
|
||||||
local zone = L[name]
|
local zone = L[name]
|
||||||
if not zone then return end
|
if not zone then return end
|
||||||
zone.cx, zone.cy = cx, cy
|
zone.cx, zone.cy = cx, cy
|
||||||
clampZone(zone, ww, wh)
|
clampZone(zone, ox, oy, ox + sw, oy + sh)
|
||||||
self.positions = self.positions or {}
|
self.positions = self.positions or {}
|
||||||
self.positions[name] = { x = zone.cx / ww, y = zone.cy / wh }
|
self.positions[name] = {
|
||||||
|
x = sw > 0 and (zone.cx - ox) / sw or 0,
|
||||||
|
y = sh > 0 and (zone.cy - oy) / sh or 0,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
function TouchControls:clearPositions()
|
function TouchControls:clearPositions()
|
||||||
self.positions = nil
|
self.positions = nil
|
||||||
self.layoutW, self.layoutH = nil, nil
|
self.layoutW, self.layoutH = nil, nil
|
||||||
|
self.layoutOx, self.layoutOy = nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function inCircle(zone, x, y, slop)
|
local function inCircle(zone, x, y, slop)
|
||||||
|
|||||||
+42
-35
@@ -1,6 +1,7 @@
|
|||||||
local GameVersion = require("src.core.GameVersion")
|
local GameVersion = require("src.core.GameVersion")
|
||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
local HostShell = require("src.core.HostShell")
|
local HostShell = require("src.core.HostShell")
|
||||||
|
local SafeArea = require("src.core.SafeArea")
|
||||||
|
|
||||||
local RomImporter = {}
|
local RomImporter = {}
|
||||||
RomImporter.__index = RomImporter
|
RomImporter.__index = RomImporter
|
||||||
@@ -1295,10 +1296,10 @@ local PAD_DPAD_SPEED = 420
|
|||||||
|
|
||||||
function RomImporter:_activatePadCursor()
|
function RomImporter:_activatePadCursor()
|
||||||
if self._padCursorActive then return end
|
if self._padCursorActive then return end
|
||||||
local w, h = love.graphics.getDimensions()
|
local ox, oy, w, h = SafeArea.rect()
|
||||||
if not self._padInited then
|
if not self._padInited then
|
||||||
self._padCursor.x = w * 0.5
|
self._padCursor.x = ox + w * 0.5
|
||||||
self._padCursor.y = h * 0.45
|
self._padCursor.y = oy + h * 0.45
|
||||||
self._padInited = true
|
self._padInited = true
|
||||||
end
|
end
|
||||||
self._padCursorActive = true
|
self._padCursorActive = true
|
||||||
@@ -1344,11 +1345,11 @@ function RomImporter:_updatePadCursor(dt)
|
|||||||
if mag > 1 then dx, dy = dx / mag, dy / mag end
|
if mag > 1 then dx, dy = dx / mag, dy / mag end
|
||||||
local speed = (math.abs(ax) > PAD_DEAD or math.abs(ay) > PAD_DEAD)
|
local speed = (math.abs(ax) > PAD_DEAD or math.abs(ay) > PAD_DEAD)
|
||||||
and PAD_SPEED or PAD_DPAD_SPEED
|
and PAD_SPEED or PAD_DPAD_SPEED
|
||||||
local w, h = love.graphics.getDimensions()
|
local ox, oy, w, h = SafeArea.rect()
|
||||||
local nx = self._padCursor.x + dx * speed * dt
|
local nx = self._padCursor.x + dx * speed * dt
|
||||||
local ny = self._padCursor.y + dy * speed * dt
|
local ny = self._padCursor.y + dy * speed * dt
|
||||||
self._padCursor.x = math.max(0, math.min(w, nx))
|
self._padCursor.x = math.max(ox, math.min(ox + w, nx))
|
||||||
self._padCursor.y = math.max(0, math.min(h, ny))
|
self._padCursor.y = math.max(oy, math.min(oy + h, ny))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Right stick scrolls the active list (save slots or mods), or the whole page
|
-- Right stick scrolls the active list (save slots or mods), or the whole page
|
||||||
@@ -1726,7 +1727,10 @@ function RomImporter:_resetFrameRects()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function RomImporter:draw()
|
function RomImporter:draw()
|
||||||
local width, height = love.graphics.getDimensions()
|
-- Full window for immersive backdrop; safe rect for interactive chrome so
|
||||||
|
-- notch / Dynamic Island / home indicator / Android cutouts are respected.
|
||||||
|
local fullW, fullH = love.graphics.getDimensions()
|
||||||
|
local ox, oy, width, height = SafeArea.rect()
|
||||||
local s = clamp(height / 768, 0.7, 1.6)
|
local s = clamp(height / 768, 0.7, 1.6)
|
||||||
local pulse = self.pulse
|
local pulse = self.pulse
|
||||||
self._s = s
|
self._s = s
|
||||||
@@ -1745,8 +1749,9 @@ function RomImporter:draw()
|
|||||||
self._anyHover = false
|
self._anyHover = false
|
||||||
self:_resetFrameRects()
|
self:_resetFrameRects()
|
||||||
|
|
||||||
-- Fonts + size-dependent scenery, rebuilt only when the window size changes.
|
-- Fonts + size-dependent scenery, rebuilt only when the window / safe
|
||||||
local fontKey = ("%dx%d"):format(width, height)
|
-- area changes (rotation, resize, inset changes).
|
||||||
|
local fontKey = ("%dx%d@%d,%d"):format(fullW, fullH, ox, oy)
|
||||||
if self.fontKey ~= fontKey then
|
if self.fontKey ~= fontKey then
|
||||||
self.fontKey = fontKey
|
self.fontKey = fontKey
|
||||||
local function f(px) return love.graphics.newFont(math.max(8, math.floor(px + 0.5))) end
|
local function f(px) return love.graphics.newFont(math.max(8, math.floor(px + 0.5))) end
|
||||||
@@ -1770,10 +1775,10 @@ function RomImporter:draw()
|
|||||||
-- Background: a radial gradient (bright navy at top-centre -> near black).
|
-- Background: a radial gradient (bright navy at top-centre -> near black).
|
||||||
-- A triangle fan from the top-centre gives the radial falloff; the screen
|
-- A triangle fan from the top-centre gives the radial falloff; the screen
|
||||||
-- is cleared to the outer colour first so the corners it does not reach
|
-- is cleared to the outer colour first so the corners it does not reach
|
||||||
-- match seamlessly.
|
-- match seamlessly. Sized to the full window so unsafe edges stay filled.
|
||||||
do
|
do
|
||||||
local cx, cy = width / 2, 0
|
local cx, cy = fullW / 2, 0
|
||||||
local rx, ry = width * 1.3, height * 1.08
|
local rx, ry = fullW * 1.3, fullH * 1.08
|
||||||
local n = 72
|
local n = 72
|
||||||
local verts = { { cx, cy, 0, 0,
|
local verts = { { cx, cy, 0, 0,
|
||||||
PAL.bgTop[1] / 255, PAL.bgTop[2] / 255, PAL.bgTop[3] / 255, 1 } }
|
PAL.bgTop[1] / 255, PAL.bgTop[2] / 255, PAL.bgTop[3] / 255, 1 } }
|
||||||
@@ -1787,8 +1792,8 @@ function RomImporter:draw()
|
|||||||
|
|
||||||
-- CRT vignette: a gentle edge darkening, centred slightly above the middle.
|
-- CRT vignette: a gentle edge darkening, centred slightly above the middle.
|
||||||
do
|
do
|
||||||
local cx, cy = width / 2, height * 0.45
|
local cx, cy = fullW / 2, fullH * 0.45
|
||||||
local rx, ry = width * 0.78, height * 0.78
|
local rx, ry = fullW * 0.78, fullH * 0.78
|
||||||
local n = 72
|
local n = 72
|
||||||
local verts = { { cx, cy, 0, 0, 0, 0, 0, 0 } }
|
local verts = { { cx, cy, 0, 0, 0, 0, 0, 0 } }
|
||||||
for i = 0, n do
|
for i = 0, n do
|
||||||
@@ -1810,7 +1815,7 @@ function RomImporter:draw()
|
|||||||
self.scanlineImage:setWrap("repeat", "repeat")
|
self.scanlineImage:setWrap("repeat", "repeat")
|
||||||
self.scanlineImage:setFilter("nearest", "nearest")
|
self.scanlineImage:setFilter("nearest", "nearest")
|
||||||
end
|
end
|
||||||
self.scanlineQuad = love.graphics.newQuad(0, 0, width, height, 1, 3)
|
self.scanlineQuad = love.graphics.newQuad(0, 0, fullW, fullH, 1, 3)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Invert shader: the Boi's Club Games mark is dark ink; on this dark panel it
|
-- Invert shader: the Boi's Club Games mark is dark ink; on this dark panel it
|
||||||
@@ -1836,21 +1841,23 @@ function RomImporter:draw()
|
|||||||
}
|
}
|
||||||
]])
|
]])
|
||||||
|
|
||||||
-- background
|
-- background (full window — unsafe edges stay painted)
|
||||||
col(PAL.bgBot)
|
col(PAL.bgBot)
|
||||||
love.graphics.rectangle("fill", 0, 0, width, height)
|
love.graphics.rectangle("fill", 0, 0, fullW, fullH)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.draw(self.bgMesh)
|
love.graphics.draw(self.bgMesh)
|
||||||
|
|
||||||
-- Centered content container (max ~1440 scaled units on very wide windows)
|
-- Centered content container (max ~1440 scaled units on very wide windows)
|
||||||
-- with a responsive side gutter; every column below derives from these.
|
-- with a responsive side gutter; every column below derives from these.
|
||||||
|
-- Origin is the safe-area top-left so chrome clears device insets.
|
||||||
local appW = math.min(width, 1440 * s)
|
local appW = math.min(width, 1440 * s)
|
||||||
local appX = (width - appW) / 2
|
local appX = ox + (width - appW) / 2
|
||||||
local padH = clamp(appW * 0.03, 12 * s, 26 * s)
|
local padH = clamp(appW * 0.03, 12 * s, 26 * s)
|
||||||
local third = appW / 3
|
local third = appW / 3
|
||||||
|
|
||||||
-- tricolor strip (Red | Blue | Yellow), 6px tall, with a soft downward bloom
|
-- tricolor strip (Red | Blue | Yellow), 6px tall, with a soft downward bloom
|
||||||
local stripH = math.max(4, 6 * s)
|
local stripH = math.max(4, 6 * s)
|
||||||
|
local stripY = oy
|
||||||
local segs = {
|
local segs = {
|
||||||
{ PAL.red, appX, third },
|
{ PAL.red, appX, third },
|
||||||
{ PAL.blue, appX + third, third },
|
{ PAL.blue, appX + third, third },
|
||||||
@@ -1858,11 +1865,11 @@ function RomImporter:draw()
|
|||||||
}
|
}
|
||||||
love.graphics.setBlendMode("add")
|
love.graphics.setBlendMode("add")
|
||||||
for _, seg in ipairs(segs) do
|
for _, seg in ipairs(segs) do
|
||||||
fillGrad(seg[2], stripH, seg[3], stripH * 3.6, seg[1], seg[1], 0.30, 0.0)
|
fillGrad(seg[2], stripY + stripH, seg[3], stripH * 3.6, seg[1], seg[1], 0.30, 0.0)
|
||||||
end
|
end
|
||||||
love.graphics.setBlendMode("alpha")
|
love.graphics.setBlendMode("alpha")
|
||||||
for _, seg in ipairs(segs) do
|
for _, seg in ipairs(segs) do
|
||||||
col(seg[1]); love.graphics.rectangle("fill", seg[2], 0, seg[3], stripH)
|
col(seg[1]); love.graphics.rectangle("fill", seg[2], stripY, seg[3], stripH)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Footer (Boi's Club Games logo + trust warning), measured first so the
|
-- Footer (Boi's Club Games logo + trust warning), measured first so the
|
||||||
@@ -1884,7 +1891,7 @@ function RomImporter:draw()
|
|||||||
math.min(330 * s, appW - 32 * s))
|
math.min(330 * s, appW - 32 * s))
|
||||||
local logoScale = math.min(logoTargetW / logoW, height * 0.15 / logoH)
|
local logoScale = math.min(logoTargetW / logoW, height * 0.15 / logoH)
|
||||||
local logoDW, logoDH = logoW * logoScale, logoH * logoScale
|
local logoDW, logoDH = logoW * logoScale, logoH * logoScale
|
||||||
local logoY = stripH + 14 * s
|
local logoY = stripY + stripH + 14 * s
|
||||||
|
|
||||||
-- Tab bar: R/B/Y/divider/MODS chips (label + underline on the active one),
|
-- Tab bar: R/B/Y/divider/MODS chips (label + underline on the active one),
|
||||||
-- with "N of 3 ready" right-aligned.
|
-- with "N of 3 ready" right-aligned.
|
||||||
@@ -1914,7 +1921,7 @@ function RomImporter:draw()
|
|||||||
local bannerBand = bannerActive and (bannerH + 20 * s) or 6 * s
|
local bannerBand = bannerActive and (bannerH + 20 * s) or 6 * s
|
||||||
local cX = appX + padH
|
local cX = appX + padH
|
||||||
local cW = appW - 2 * padH
|
local cW = appW - 2 * padH
|
||||||
local contentBottom = height - footerH - bannerBand
|
local contentBottom = oy + height - footerH - bannerBand
|
||||||
local cH = math.max(0, contentBottom - contentTop)
|
local cH = math.max(0, contentBottom - contentTop)
|
||||||
|
|
||||||
-- Page scroll. Everything under the tab bar -- panel, updater banner and
|
-- Page scroll. Everything under the tab bar -- panel, updater banner and
|
||||||
@@ -1925,14 +1932,14 @@ function RomImporter:draw()
|
|||||||
-- the previous frame's measurement, the same one-frame settle the slot and
|
-- the previous frame's measurement, the same one-frame settle the slot and
|
||||||
-- mod lists already rely on. While the page fits, `paged` is false and every
|
-- mod lists already rely on. While the page fits, `paged` is false and every
|
||||||
-- measurement below is what it always was.
|
-- measurement below is what it always was.
|
||||||
local viewportH = math.max(0, height - contentTop)
|
local viewportH = math.max(0, oy + height - contentTop)
|
||||||
self._panelNaturalH = self._panelNaturalH or {}
|
self._panelNaturalH = self._panelNaturalH or {}
|
||||||
local naturalH = (self._panelNaturalH[self.tab] or 0) + bannerBand + footerH
|
local naturalH = (self._panelNaturalH[self.tab] or 0) + bannerBand + footerH
|
||||||
local paged, pageScroll, maxPage =
|
local paged, pageScroll, maxPage =
|
||||||
RomImporter.pageScrollFor(naturalH, viewportH, self.pageScroll)
|
RomImporter.pageScrollFor(naturalH, viewportH, self.pageScroll)
|
||||||
self.pageScroll, self._pageMax = pageScroll, maxPage
|
self.pageScroll, self._pageMax = pageScroll, maxPage
|
||||||
-- read by the hit tests; a scrolled control is live only inside the viewport
|
-- read by the hit tests; a scrolled control is live only inside the viewport
|
||||||
pageBand = paged and { contentTop, height } or nil
|
pageBand = paged and { contentTop, oy + height } or nil
|
||||||
|
|
||||||
-- tab bar (rebuilds self.tabRects). Pinned: it is the launcher's navigation,
|
-- tab bar (rebuilds self.tabRects). Pinned: it is the launcher's navigation,
|
||||||
-- and it sits above the scrolling viewport.
|
-- and it sits above the scrolling viewport.
|
||||||
@@ -2103,10 +2110,10 @@ function RomImporter:draw()
|
|||||||
|
|
||||||
-- logo, over the split, with a gentle bob + gold glow + sweeping shine
|
-- logo, over the split, with a gentle bob + gold glow + sweeping shine
|
||||||
local bob = math.sin(pulse * (2 * math.pi / 4)) * 6 * s
|
local bob = math.sin(pulse * (2 * math.pi / 4)) * 6 * s
|
||||||
local lx, ly = (width - logoDW) / 2, logoY + bob
|
local lx, ly = ox + (width - logoDW) / 2, logoY + bob
|
||||||
love.graphics.setBlendMode("add")
|
love.graphics.setBlendMode("add")
|
||||||
love.graphics.setColor(1, 0.85, 0.2, 0.16 + 0.12 * (0.5 + 0.5 * math.sin(pulse * 1.6)))
|
love.graphics.setColor(1, 0.85, 0.2, 0.16 + 0.12 * (0.5 + 0.5 * math.sin(pulse * 1.6)))
|
||||||
love.graphics.draw(self.logo, (width - logoDW * 1.05) / 2, ly - logoDH * 0.025, 0,
|
love.graphics.draw(self.logo, ox + (width - logoDW * 1.05) / 2, ly - logoDH * 0.025, 0,
|
||||||
logoScale * 1.05, logoScale * 1.05)
|
logoScale * 1.05, logoScale * 1.05)
|
||||||
love.graphics.setBlendMode("alpha")
|
love.graphics.setBlendMode("alpha")
|
||||||
local shineW = 0.16
|
local shineW = 0.16
|
||||||
@@ -2139,11 +2146,11 @@ function RomImporter:draw()
|
|||||||
-- save-slot rename modal (#205), drawn over everything
|
-- save-slot rename modal (#205), drawn over everything
|
||||||
if self._rename then
|
if self._rename then
|
||||||
col(PAL.bgBot, 0.72)
|
col(PAL.bgBot, 0.72)
|
||||||
love.graphics.rectangle("fill", 0, 0, width, height)
|
love.graphics.rectangle("fill", 0, 0, fullW, fullH)
|
||||||
local dw = math.min(appW - 32 * s, 420 * s)
|
local dw = math.min(appW - 32 * s, 420 * s)
|
||||||
local dh = 128 * s
|
local dh = 128 * s
|
||||||
local dx = appX + (appW - dw) / 2
|
local dx = appX + (appW - dw) / 2
|
||||||
local dy = (height - dh) / 2
|
local dy = oy + (height - dh) / 2
|
||||||
local rr = 12 * s
|
local rr = 12 * s
|
||||||
neonGlow(dx, dy, dw, dh, rr, PAL.green, 0.4)
|
neonGlow(dx, dy, dw, dh, rr, PAL.green, 0.4)
|
||||||
fillGradRounded(dx, dy, dw, dh, rr, PAL.slotBg, PAL.slotBg, 0.85, 0.85)
|
fillGradRounded(dx, dy, dw, dh, rr, PAL.slotBg, PAL.slotBg, 0.85, 0.85)
|
||||||
@@ -2185,11 +2192,11 @@ function RomImporter:draw()
|
|||||||
-- it is typed.
|
-- it is typed.
|
||||||
if self._indexPrompt then
|
if self._indexPrompt then
|
||||||
col(PAL.bgBot, 0.72)
|
col(PAL.bgBot, 0.72)
|
||||||
love.graphics.rectangle("fill", 0, 0, width, height)
|
love.graphics.rectangle("fill", 0, 0, fullW, fullH)
|
||||||
local dw = math.min(appW - 32 * s, 520 * s)
|
local dw = math.min(appW - 32 * s, 520 * s)
|
||||||
local dh = 168 * s
|
local dh = 168 * s
|
||||||
local dx = appX + (appW - dw) / 2
|
local dx = appX + (appW - dw) / 2
|
||||||
local dy = (height - dh) / 2
|
local dy = oy + (height - dh) / 2
|
||||||
local rr = 12 * s
|
local rr = 12 * s
|
||||||
neonGlow(dx, dy, dw, dh, rr, PAL.modDot, 0.4)
|
neonGlow(dx, dy, dw, dh, rr, PAL.modDot, 0.4)
|
||||||
fillGradRounded(dx, dy, dw, dh, rr, PAL.slotBg, PAL.slotBg, 0.9, 0.9)
|
fillGradRounded(dx, dy, dw, dh, rr, PAL.slotBg, PAL.slotBg, 0.9, 0.9)
|
||||||
@@ -2241,7 +2248,7 @@ function RomImporter:draw()
|
|||||||
if self._modConfirm or self._modVersions or self._modReleaseNotes
|
if self._modConfirm or self._modVersions or self._modReleaseNotes
|
||||||
or self._findDetails then
|
or self._findDetails then
|
||||||
col(PAL.bgBot, 0.72)
|
col(PAL.bgBot, 0.72)
|
||||||
love.graphics.rectangle("fill", 0, 0, width, height)
|
love.graphics.rectangle("fill", 0, 0, fullW, fullH)
|
||||||
end
|
end
|
||||||
if self._modConfirm then
|
if self._modConfirm then
|
||||||
local c = self._modConfirm
|
local c = self._modConfirm
|
||||||
@@ -2249,7 +2256,7 @@ function RomImporter:draw()
|
|||||||
local lineH = self.hintFont:getHeight() + 4 * s
|
local lineH = self.hintFont:getHeight() + 4 * s
|
||||||
local dh = 36 * s + (#c.lines) * lineH + 56 * s
|
local dh = 36 * s + (#c.lines) * lineH + 56 * s
|
||||||
local dx = appX + (appW - dw) / 2
|
local dx = appX + (appW - dw) / 2
|
||||||
local dy = (height - dh) / 2
|
local dy = oy + (height - dh) / 2
|
||||||
local rr = 12 * s
|
local rr = 12 * s
|
||||||
fillGradRounded(dx, dy, dw, dh, rr, PAL.slotBg, PAL.slotBg, 0.92, 0.92)
|
fillGradRounded(dx, dy, dw, dh, rr, PAL.slotBg, PAL.slotBg, 0.92, 0.92)
|
||||||
love.graphics.setLineWidth(math.max(1, 1.2 * s))
|
love.graphics.setLineWidth(math.max(1, 1.2 * s))
|
||||||
@@ -2291,7 +2298,7 @@ function RomImporter:draw()
|
|||||||
local dw = math.min(appW - 32 * s, 480 * s)
|
local dw = math.min(appW - 32 * s, 480 * s)
|
||||||
local dh = math.min(height - 48 * s, 360 * s)
|
local dh = math.min(height - 48 * s, 360 * s)
|
||||||
local dx = appX + (appW - dw) / 2
|
local dx = appX + (appW - dw) / 2
|
||||||
local dy = (height - dh) / 2
|
local dy = oy + (height - dh) / 2
|
||||||
local rr = 12 * s
|
local rr = 12 * s
|
||||||
fillGradRounded(dx, dy, dw, dh, rr, PAL.slotBg, PAL.slotBg, 0.92, 0.92)
|
fillGradRounded(dx, dy, dw, dh, rr, PAL.slotBg, PAL.slotBg, 0.92, 0.92)
|
||||||
love.graphics.setLineWidth(math.max(1, 1.2 * s))
|
love.graphics.setLineWidth(math.max(1, 1.2 * s))
|
||||||
@@ -2336,7 +2343,7 @@ function RomImporter:draw()
|
|||||||
local dw = math.min(appW - 32 * s, 520 * s)
|
local dw = math.min(appW - 32 * s, 520 * s)
|
||||||
local dh = math.min(height - 48 * s, 420 * s)
|
local dh = math.min(height - 48 * s, 420 * s)
|
||||||
local dx = appX + (appW - dw) / 2
|
local dx = appX + (appW - dw) / 2
|
||||||
local dy = (height - dh) / 2
|
local dy = oy + (height - dh) / 2
|
||||||
local rr = 12 * s
|
local rr = 12 * s
|
||||||
fillGradRounded(dx, dy, dw, dh, rr, PAL.slotBg, PAL.slotBg, 0.94, 0.94)
|
fillGradRounded(dx, dy, dw, dh, rr, PAL.slotBg, PAL.slotBg, 0.94, 0.94)
|
||||||
love.graphics.setLineWidth(math.max(1, 1.2 * s))
|
love.graphics.setLineWidth(math.max(1, 1.2 * s))
|
||||||
@@ -2393,7 +2400,7 @@ function RomImporter:draw()
|
|||||||
listH = listN * rowH
|
listH = listN * rowH
|
||||||
dh = headerH + listH + footerH
|
dh = headerH + listH + footerH
|
||||||
local dx = appX + (appW - dw) / 2
|
local dx = appX + (appW - dw) / 2
|
||||||
local dy = (height - dh) / 2
|
local dy = oy + (height - dh) / 2
|
||||||
local rr = 12 * s
|
local rr = 12 * s
|
||||||
fillGradRounded(dx, dy, dw, dh, rr, PAL.slotBg, PAL.slotBg, 0.96, 0.96)
|
fillGradRounded(dx, dy, dw, dh, rr, PAL.slotBg, PAL.slotBg, 0.96, 0.96)
|
||||||
love.graphics.setLineWidth(math.max(1, 1.2 * s))
|
love.graphics.setLineWidth(math.max(1, 1.2 * s))
|
||||||
|
|||||||
@@ -109,34 +109,36 @@ function Editor.update(_dt)
|
|||||||
end
|
end
|
||||||
|
|
||||||
function Editor.draw()
|
function Editor.draw()
|
||||||
local ww, wh = love.graphics.getDimensions()
|
local SafeArea = require("src.core.SafeArea")
|
||||||
|
local fullW, fullH = love.graphics.getDimensions()
|
||||||
|
local ox, oy, ww, wh = SafeArea.rect()
|
||||||
local s = math.max(0.75, math.min(1.4, wh / 768))
|
local s = math.max(0.75, math.min(1.4, wh / 768))
|
||||||
Editor.rects = {}
|
Editor.rects = {}
|
||||||
|
|
||||||
-- radial-ish navy field (two stacked fills; matches launcher atmosphere)
|
-- radial-ish navy field (two stacked fills; matches launcher atmosphere)
|
||||||
col(PAL.bgBot)
|
col(PAL.bgBot)
|
||||||
love.graphics.rectangle("fill", 0, 0, ww, wh)
|
love.graphics.rectangle("fill", 0, 0, fullW, fullH)
|
||||||
col(PAL.bgTop, 0.85)
|
col(PAL.bgTop, 0.85)
|
||||||
love.graphics.circle("fill", ww * 0.5, wh * 0.15, math.max(ww, wh) * 0.55)
|
love.graphics.circle("fill", ox + ww * 0.5, oy + wh * 0.15, math.max(ww, wh) * 0.55)
|
||||||
|
|
||||||
local pad = 18 * s
|
local pad = 18 * s
|
||||||
local barH = 56 * s
|
local barH = 56 * s
|
||||||
local btnH = 40 * s
|
local btnH = 40 * s
|
||||||
local btnW = 100 * s
|
local btnW = 100 * s
|
||||||
|
|
||||||
-- top bar
|
-- top bar (inside the safe area so it clears the notch / status bar)
|
||||||
col(PAL.card, 0.92)
|
col(PAL.card, 0.92)
|
||||||
love.graphics.rectangle("fill", 0, 0, ww, barH + pad)
|
love.graphics.rectangle("fill", 0, 0, fullW, oy + barH + pad)
|
||||||
col(PAL.stroke, 0.35)
|
col(PAL.stroke, 0.35)
|
||||||
love.graphics.setLineWidth(1)
|
love.graphics.setLineWidth(1)
|
||||||
love.graphics.line(0, barH + pad, ww, barH + pad)
|
love.graphics.line(0, oy + barH + pad, fullW, oy + barH + pad)
|
||||||
|
|
||||||
love.graphics.setFont(Editor.fonts.title)
|
love.graphics.setFont(Editor.fonts.title)
|
||||||
col(PAL.white)
|
col(PAL.white)
|
||||||
love.graphics.print("Touch Controls", pad, pad + 4 * s)
|
love.graphics.print("Touch Controls", ox + pad, oy + pad + 4 * s)
|
||||||
|
|
||||||
-- Done / Reset
|
-- Done / Reset
|
||||||
local done = { x = ww - pad - btnW, y = pad + (barH - btnH) / 2,
|
local done = { x = ox + ww - pad - btnW, y = oy + pad + (barH - btnH) / 2,
|
||||||
w = btnW, h = btnH }
|
w = btnW, h = btnH }
|
||||||
local reset = { x = done.x - 10 * s - btnW, y = done.y, w = btnW, h = btnH }
|
local reset = { x = done.x - 10 * s - btnW, y = done.y, w = btnW, h = btnH }
|
||||||
Editor.rects.done, Editor.rects.reset = done, reset
|
Editor.rects.done, Editor.rects.reset = done, reset
|
||||||
@@ -156,9 +158,9 @@ function Editor.draw()
|
|||||||
chromeBtn(done, "Done", PAL.green)
|
chromeBtn(done, "Done", PAL.green)
|
||||||
|
|
||||||
-- enable toggle card
|
-- enable toggle card
|
||||||
local cardY = barH + pad + 14 * s
|
local cardY = oy + barH + pad + 14 * s
|
||||||
local cardH = 64 * s
|
local cardH = 64 * s
|
||||||
local cardX, cardW = pad, ww - 2 * pad
|
local cardX, cardW = ox + pad, ww - 2 * pad
|
||||||
col(PAL.card, 0.88)
|
col(PAL.card, 0.88)
|
||||||
roundRect("fill", cardX, cardY, cardW, cardH, 12 * s)
|
roundRect("fill", cardX, cardY, cardW, cardH, 12 * s)
|
||||||
col(PAL.stroke, 0.4)
|
col(PAL.stroke, 0.4)
|
||||||
@@ -190,7 +192,7 @@ function Editor.draw()
|
|||||||
local hint = on
|
local hint = on
|
||||||
and "Drag each button to reposition. Layout is saved when you tap Done."
|
and "Drag each button to reposition. Layout is saved when you tap Done."
|
||||||
or "Controls are hidden in-game. Enable them to show and edit the layout."
|
or "Controls are hidden in-game. Enable them to show and edit the layout."
|
||||||
love.graphics.printf(hint, pad, cardY + cardH + 12 * s, ww - 2 * pad, "left")
|
love.graphics.printf(hint, ox + pad, cardY + cardH + 12 * s, ww - 2 * pad, "left")
|
||||||
|
|
||||||
-- the overlay itself (preview mode; dimmed when disabled)
|
-- the overlay itself (preview mode; dimmed when disabled)
|
||||||
TouchControls:draw()
|
TouchControls:draw()
|
||||||
|
|||||||
@@ -203,4 +203,12 @@ stub.mouse = {
|
|||||||
|
|
||||||
stub.timer = { getTime = function() return 0 end }
|
stub.timer = { getTime = function() return 0 end }
|
||||||
|
|
||||||
|
-- Desktop / headless: full-window safe area (matches LÖVE's fallback).
|
||||||
|
stub.window = {
|
||||||
|
getSafeArea = function()
|
||||||
|
local ww, wh = stub.graphics.getDimensions()
|
||||||
|
return 0, 0, ww, wh
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
return stub
|
return stub
|
||||||
|
|||||||
+29
-1
@@ -1696,6 +1696,7 @@ end
|
|||||||
-- ---------------------------------------------------------------- touch controls layout (#327)
|
-- ---------------------------------------------------------------- touch controls layout (#327)
|
||||||
do
|
do
|
||||||
local TC = require("src.core.TouchControls")
|
local TC = require("src.core.TouchControls")
|
||||||
|
local SafeArea = require("src.core.SafeArea")
|
||||||
local cfg = TC.normalizeConfig(nil)
|
local cfg = TC.normalizeConfig(nil)
|
||||||
eq(cfg.enabled, true, "touchControls default enabled")
|
eq(cfg.enabled, true, "touchControls default enabled")
|
||||||
check(cfg.positions == nil, "touchControls default positions nil")
|
check(cfg.positions == nil, "touchControls default positions nil")
|
||||||
@@ -1721,6 +1722,12 @@ do
|
|||||||
check(L.a.cx > 200, "default A on right half")
|
check(L.a.cx > 200, "default A on right half")
|
||||||
check(L.dpad.cy > 400, "default d-pad in bottom half")
|
check(L.dpad.cy > 400, "default d-pad in bottom half")
|
||||||
|
|
||||||
|
-- safe-area origin shifts defaults without changing relative layout
|
||||||
|
local Ls = TC.defaultLayout(400, 800, 20, 30)
|
||||||
|
eq(Ls.dpad.cx, L.dpad.cx + 20, "defaultLayout ox shifts controls")
|
||||||
|
eq(Ls.dpad.cy, L.dpad.cy + 30, "defaultLayout oy shifts controls")
|
||||||
|
eq(Ls.a.cx, L.a.cx + 20, "defaultLayout ox shifts A")
|
||||||
|
|
||||||
-- applyOptions + visible gate (no real images needed for the gate)
|
-- applyOptions + visible gate (no real images needed for the gate)
|
||||||
TC.enabled = true
|
TC.enabled = true
|
||||||
TC.active = true
|
TC.active = true
|
||||||
@@ -1743,16 +1750,37 @@ do
|
|||||||
-- custom position applied through layout()
|
-- custom position applied through layout()
|
||||||
local g = love.graphics
|
local g = love.graphics
|
||||||
local oldDim, oldFont = g.getDimensions, g.newFont
|
local oldDim, oldFont = g.getDimensions, g.newFont
|
||||||
|
local oldSafe = love.window and love.window.getSafeArea
|
||||||
g.getDimensions = function() return 400, 800 end
|
g.getDimensions = function() return 400, 800 end
|
||||||
g.newFont = function() return { getWidth = function() return 10 end,
|
g.newFont = function() return { getWidth = function() return 10 end,
|
||||||
getHeight = function() return 10 end } end
|
getHeight = function() return 10 end } end
|
||||||
TC.layoutW, TC.layoutH, TC.L = nil, nil, nil
|
love.window = love.window or {}
|
||||||
|
love.window.getSafeArea = function() return 0, 0, 400, 800 end
|
||||||
|
TC.layoutW, TC.layoutH, TC.layoutOx, TC.layoutOy, TC.L = nil, nil, nil, nil, nil
|
||||||
local lay = TC:layout()
|
local lay = TC:layout()
|
||||||
eq(lay.dpad.cx, 100, "custom dpad cx = nx * ww")
|
eq(lay.dpad.cx, 100, "custom dpad cx = nx * ww")
|
||||||
eq(lay.dpad.cy, 600, "custom dpad cy = ny * wh")
|
eq(lay.dpad.cy, 600, "custom dpad cy = ny * wh")
|
||||||
|
|
||||||
|
-- inset safe area: custom positions stay inside the usable rect
|
||||||
|
love.window.getSafeArea = function() return 10, 40, 380, 720 end
|
||||||
|
TC.layoutW, TC.layoutH, TC.layoutOx, TC.layoutOy, TC.L = nil, nil, nil, nil, nil
|
||||||
|
lay = TC:layout()
|
||||||
|
eq(lay.dpad.cx, 10 + 0.25 * 380, "safe-area custom dpad cx")
|
||||||
|
eq(lay.dpad.cy, 40 + 0.75 * 720, "safe-area custom dpad cy")
|
||||||
|
check(lay.dpad.cy <= 40 + 720 - lay.dpad.w * 0.5 + 1e-6,
|
||||||
|
"safe-area dpad clears bottom inset")
|
||||||
|
|
||||||
|
local x, y, w, h = SafeArea.rect()
|
||||||
|
eq(x, 10, "SafeArea.rect x")
|
||||||
|
eq(y, 40, "SafeArea.rect y")
|
||||||
|
eq(w, 380, "SafeArea.rect w")
|
||||||
|
eq(h, 720, "SafeArea.rect h")
|
||||||
|
|
||||||
TC:clearPositions()
|
TC:clearPositions()
|
||||||
check(TC.positions == nil, "clearPositions wipes overrides")
|
check(TC.positions == nil, "clearPositions wipes overrides")
|
||||||
g.getDimensions, g.newFont = oldDim, oldFont
|
g.getDimensions, g.newFont = oldDim, oldFont
|
||||||
|
if oldSafe then love.window.getSafeArea = oldSafe
|
||||||
|
else love.window.getSafeArea = nil end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ---------------------------------------------------------------- crit thresholds (CriticalHitTest)
|
-- ---------------------------------------------------------------- crit thresholds (CriticalHitTest)
|
||||||
|
|||||||
Reference in New Issue
Block a user