mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-27 08:51:27 +02:00
feat(ui): 4-layer vertical controller navigation, bright white glowing focus highlights, and handheld updater support
This commit is contained in:
@@ -377,12 +377,19 @@ end
|
|||||||
-- visible while typing, and blinks a caret on the importer's pulse clock.
|
-- visible while typing, and blinks a caret on the importer's pulse clock.
|
||||||
local function textField(imp, x, y, w, h, key, rawText, placeholder, focused, action)
|
local function textField(imp, x, y, w, h, key, rawText, placeholder, focused, action)
|
||||||
Kit._audit("control", x, y, w, h, key)
|
Kit._audit("control", x, y, w, h, key)
|
||||||
Kit.focusable(key, x, y, w, h)
|
local isFocused = Kit.focusable(key, x, y, w, h)
|
||||||
Theme.fill(x, y, w, h, PAL.bg, 1)
|
Theme.fill(x, y, w, h, PAL.bg, 1)
|
||||||
Theme.stroke(x, y, w, h, PAL.line,
|
if isFocused then
|
||||||
focused and Theme.A.focus or
|
local glowPulse = 0.5 + 0.5 * math.sin(Kit.time * 5)
|
||||||
(Kit.hover(x, y, w, h) and Theme.A.hover or Theme.A.hairline),
|
Theme.strokeRounded(x - 3, y - 3, w + 6, h + 6, PAL.ink, 0.35 + 0.25 * glowPulse, 2.5, 4)
|
||||||
focused and 2 or 1)
|
Theme.strokeRounded(x, y, w, h, PAL.ink, 0.95 + 0.05 * glowPulse, 2, 2)
|
||||||
|
Theme.fillRounded(x, y, w, h, PAL.ink, 0.12 + 0.06 * glowPulse, 2)
|
||||||
|
else
|
||||||
|
Theme.stroke(x, y, w, h, PAL.line,
|
||||||
|
focused and Theme.A.focus or
|
||||||
|
(Kit.hover(x, y, w, h) and Theme.A.hover or Theme.A.hairline),
|
||||||
|
focused and 2 or 1)
|
||||||
|
end
|
||||||
local pad = math.floor(10 * Kit.scale)
|
local pad = math.floor(10 * Kit.scale)
|
||||||
local ty = y + (h - Kit.textHeight("button")) / 2
|
local ty = y + (h - Kit.textHeight("button")) / 2
|
||||||
local text = rawText or ""
|
local text = rawText or ""
|
||||||
@@ -3433,13 +3440,19 @@ local function buildFooter(imp, m, y)
|
|||||||
local bx = m.x + math.floor((m.w - topW) / 2)
|
local bx = m.x + math.floor((m.w - topW) / 2)
|
||||||
local my = cy + math.floor((rowH - dh) / 2)
|
local my = cy + math.floor((rowH - dh) / 2)
|
||||||
local chipY = cy + math.floor((rowH - chipH) / 2)
|
local chipY = cy + math.floor((rowH - chipH) / 2)
|
||||||
|
local isFocused = Kit.focusable("bcg", bx, my, dw, dh)
|
||||||
local hot = Kit.hover(bx, my, dw, dh)
|
local hot = Kit.hover(bx, my, dw, dh)
|
||||||
|
if isFocused then
|
||||||
|
local glowPulse = 0.5 + 0.5 * math.sin(Kit.time * 5)
|
||||||
|
Theme.strokeRounded(bx - 3, my - 3, dw + 6, dh + 6, PAL.ink, 0.35 + 0.25 * glowPulse, 2.5, 4)
|
||||||
|
Theme.strokeRounded(bx, my, dw, dh, PAL.ink, 0.95 + 0.05 * glowPulse, 2, 2)
|
||||||
|
end
|
||||||
love.graphics.setShader(imp.invertShader)
|
love.graphics.setShader(imp.invertShader)
|
||||||
love.graphics.setColor(1, 1, 1, hot and 1 or 0.85)
|
love.graphics.setColor(1, 1, 1, (hot or isFocused) and 1 or 0.85)
|
||||||
love.graphics.draw(imp.bcg, Theme.snap(bx), Theme.snap(my), 0, scale, scale)
|
love.graphics.draw(imp.bcg, Theme.snap(bx), Theme.snap(my), 0, scale, scale)
|
||||||
love.graphics.setShader()
|
love.graphics.setShader()
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
if Kit.press(bx, my, dw, dh) then
|
if Kit.press(bx, my, dw, dh) or Kit._activateId == "bcg" then
|
||||||
queueAction(imp, "bcg", function() love.system.openURL(COMMUNITY_URL) end)
|
queueAction(imp, "bcg", function() love.system.openURL(COMMUNITY_URL) end)
|
||||||
end
|
end
|
||||||
local cx = bx + dw
|
local cx = bx + dw
|
||||||
|
|||||||
@@ -1263,7 +1263,12 @@ end
|
|||||||
-- up the background worker or reach out to the network.
|
-- up the background worker or reach out to the network.
|
||||||
local function updaterAllowed()
|
local function updaterAllowed()
|
||||||
if not Platform.networkValidated() then return false end
|
if not Platform.networkValidated() then return false end
|
||||||
if not (love.filesystem.isFused and love.filesystem.isFused()) then return false end
|
local isHandheld = os.getenv("HANDHELD") == "1" or os.getenv("PORTMASTER") == "1"
|
||||||
|
or os.getenv("POKEPORT_HANDHELD") == "1" or os.getenv("TRIMUI") == "1"
|
||||||
|
or os.getenv("MUOS") == "1" or os.getenv("KNULLI") == "1"
|
||||||
|
if not (love.filesystem.isFused and love.filesystem.isFused()) and not isHandheld then
|
||||||
|
return false
|
||||||
|
end
|
||||||
if os.getenv("POKEPORT_AUTOPILOT") or os.getenv("POKEPORT_DRIVER") then return false end
|
if os.getenv("POKEPORT_AUTOPILOT") or os.getenv("POKEPORT_DRIVER") then return false end
|
||||||
if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then return false end
|
if os.getenv("POKEPORT_IMPORT_ONLY") == "1" then return false end
|
||||||
return true
|
return true
|
||||||
|
|||||||
+96
-35
@@ -407,9 +407,31 @@ function Kit.setFocus(id)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- Pick the nearest focusable in `dir` from the current one. Candidates must
|
-- Pick the nearest focusable in `dir` from the current one. Candidates must
|
||||||
-- lie in the half-plane of the direction; the score prefers a small step
|
local function getNavLayer(slot)
|
||||||
-- along the axis of travel and penalises drift across it, which keeps a
|
local id = tostring(slot.id or "")
|
||||||
-- column walk inside its column.
|
local y = slot.y or 0
|
||||||
|
|
||||||
|
-- Layer 1: Top Bar (Settings / Gear, Close / Quit)
|
||||||
|
if id == "gear" or id == "settings" or id == "close" or id == "quit" or (y < 45 * Kit.scale and not id:match("^tab%-")) then
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Layer 2: ROM Select & Feature Tabs (Red, Blue, Yellow, Gold, Silver, Crystal, Mods, Find, Skins, Bug)
|
||||||
|
if id:match("^tab%-") or (y >= 45 * Kit.scale and y < 105 * Kit.scale and (slot.h < 50 * Kit.scale)) then
|
||||||
|
return 2
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Layer 4: Footer (Patch notes, Updater, BCG, Export, etc.)
|
||||||
|
local screenH = (love and love.graphics and love.graphics.getHeight and love.graphics.getHeight()) or 480
|
||||||
|
if id == "patch-notes" or id == "updater" or id == "bcg"
|
||||||
|
or id:match("^footer") or (y > screenH - 55 * Kit.scale) then
|
||||||
|
return 4
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Layer 3: Main content panel (Import ROM / Play, Manage, Save slots, Mods list, Find mods, etc.)
|
||||||
|
return 3
|
||||||
|
end
|
||||||
|
|
||||||
function Kit._resolveNav()
|
function Kit._resolveNav()
|
||||||
local dir = Kit._navQueue
|
local dir = Kit._navQueue
|
||||||
Kit._navQueue = nil
|
Kit._navQueue = nil
|
||||||
@@ -417,34 +439,68 @@ function Kit._resolveNav()
|
|||||||
if not dir or n == 0 then return end
|
if not dir or n == 0 then return end
|
||||||
local cur
|
local cur
|
||||||
for i = 1, n do
|
for i = 1, n do
|
||||||
if Kit._nav[i].id == Kit.focusId then cur = Kit._nav[i] break end
|
if Kit._nav[i].id == Kit.focusId then cur = Kit._nav[i]; break end
|
||||||
end
|
end
|
||||||
if not cur then
|
if not cur then
|
||||||
Kit.focusId = Kit._nav[1].id
|
Kit.focusId = Kit._nav[1].id
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local curLayer = getNavLayer(cur)
|
||||||
local cx, cy = cur.x + cur.w / 2, cur.y + cur.h / 2
|
local cx, cy = cur.x + cur.w / 2, cur.y + cur.h / 2
|
||||||
local best, bestScore
|
|
||||||
for i = 1, n do
|
if dir == "left" or dir == "right" then
|
||||||
local c = Kit._nav[i]
|
-- STRICT SAME-LAYER HORIZONTAL NAVIGATION (Left/Right NEVER jumps between layers)
|
||||||
if c.id ~= cur.id then
|
local best, bestDx
|
||||||
local dx = (c.x + c.w / 2) - cx
|
for i = 1, n do
|
||||||
local dy = (c.y + c.h / 2) - cy
|
local c = Kit._nav[i]
|
||||||
local along, across
|
if c.id ~= cur.id and getNavLayer(c) == curLayer then
|
||||||
if dir == "left" then along, across = -dx, math.abs(dy)
|
local cMidX = c.x + c.w / 2
|
||||||
elseif dir == "right" then along, across = dx, math.abs(dy)
|
local dx = cMidX - cx
|
||||||
elseif dir == "up" then along, across = -dy, math.abs(dx)
|
if dir == "left" and dx < -1 then
|
||||||
else along, across = dy, math.abs(dx) end
|
local dist = -dx
|
||||||
-- A control merely overlapping on the travel axis is not "in that
|
if not bestDx or dist < bestDx then
|
||||||
-- direction"; require real separation so a tall row's neighbours do
|
best, bestDx = c, dist
|
||||||
-- not all qualify.
|
end
|
||||||
if along > 1 then
|
elseif dir == "right" and dx > 1 then
|
||||||
local score = along + across * 2
|
local dist = dx
|
||||||
if not bestScore or score < bestScore then best, bestScore = c, score end
|
if not bestDx or dist < bestDx then
|
||||||
|
best, bestDx = c, dist
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if best then
|
||||||
|
Kit.focusId = best.id
|
||||||
|
return
|
||||||
|
end
|
||||||
|
return
|
||||||
|
elseif dir == "up" or dir == "down" then
|
||||||
|
-- VERTICAL LAYER NAVIGATION (Up/Down steps between layers: 1 <-> 2 <-> 3 <-> 4)
|
||||||
|
local targetLayer = dir == "up" and (curLayer - 1) or (curLayer + 1)
|
||||||
|
targetLayer = math.max(1, math.min(4, targetLayer))
|
||||||
|
|
||||||
|
local best, bestScore
|
||||||
|
for i = 1, n do
|
||||||
|
local c = Kit._nav[i]
|
||||||
|
if c.id ~= cur.id then
|
||||||
|
local l = getNavLayer(c)
|
||||||
|
if (dir == "up" and l < curLayer) or (dir == "down" and l > curLayer) then
|
||||||
|
local layerDiff = math.abs(l - targetLayer)
|
||||||
|
local cMidX = c.x + c.w / 2
|
||||||
|
local xDist = math.abs(cMidX - cx)
|
||||||
|
local score = layerDiff * 10000 + xDist
|
||||||
|
if not bestScore or score < bestScore then
|
||||||
|
best, bestScore = c, score
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if best then
|
||||||
|
Kit.focusId = best.id
|
||||||
|
return
|
||||||
|
end
|
||||||
end
|
end
|
||||||
if best then Kit.focusId = best.id end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ------------------------------------------------------------ input plumbing
|
-- ------------------------------------------------------------ input plumbing
|
||||||
@@ -568,12 +624,15 @@ function Kit.row(x, y, w, h, selected, id)
|
|||||||
-- The focus ring is a second inset outline, so it reads on both a black
|
-- The focus ring is a second inset outline, so it reads on both a black
|
||||||
-- row and a white selected one.
|
-- row and a white selected one.
|
||||||
if focused then
|
if focused then
|
||||||
local glowPulse = 0.5 + 0.5 * math.sin(Kit.time * 4)
|
local glowPulse = 0.5 + 0.5 * math.sin(Kit.time * 5)
|
||||||
Theme.strokeRounded(x - 2, y - 2, w + 4, h + 4,
|
-- Outer white soft aura
|
||||||
PAL.railBlue, 0.35 + 0.25 * glowPulse, 2, Theme.radius() + 2)
|
Theme.strokeRounded(x - 3, y - 3, w + 6, h + 6,
|
||||||
|
PAL.ink, 0.35 + 0.25 * glowPulse, 2.5, Theme.radius() + 3)
|
||||||
|
-- Bright white inner stroke
|
||||||
Theme.strokeRounded(x, y, w, h,
|
Theme.strokeRounded(x, y, w, h,
|
||||||
selected and PAL.inverse or PAL.lineStrong, 0.90 + 0.10 * glowPulse, 1.5,
|
PAL.ink, 0.95 + 0.05 * glowPulse, 2, Theme.radius())
|
||||||
Theme.radius())
|
-- Luminous white fill overlay
|
||||||
|
Theme.fillRounded(x, y, w, h, PAL.ink, 0.12 + 0.06 * glowPulse, Theme.radius())
|
||||||
end
|
end
|
||||||
local clicked = Kit.press(x, y, w, h)
|
local clicked = Kit.press(x, y, w, h)
|
||||||
or (id ~= nil and Kit._activateId == id)
|
or (id ~= nil and Kit._activateId == id)
|
||||||
@@ -691,15 +750,17 @@ function Kit.button(x, y, w, h, label, opts)
|
|||||||
Theme.strokeRounded(x, y, w, h, stroke, strokeA, 1, radius)
|
Theme.strokeRounded(x, y, w, h, stroke, strokeA, 1, radius)
|
||||||
end
|
end
|
||||||
if doRing then
|
if doRing then
|
||||||
local glowPulse = 0.5 + 0.5 * math.sin(Kit.time * 4)
|
local glowPulse = 0.5 + 0.5 * math.sin(Kit.time * 5)
|
||||||
-- Outer soft-glow highlight aura
|
-- 1. Outer bright white soft glow aura
|
||||||
Theme.strokeRounded(x - B.ringPad - 2, y - B.ringPad - 2,
|
Theme.strokeRounded(x - B.ringPad - 3, y - B.ringPad - 3,
|
||||||
w + 2 * (B.ringPad + 2), h + 2 * (B.ringPad + 2), PAL.railBlue,
|
w + 2 * (B.ringPad + 3), h + 2 * (B.ringPad + 3), PAL.ink,
|
||||||
0.30 + 0.25 * glowPulse, 2, radius + B.ringPad + 2)
|
0.35 + 0.25 * glowPulse, 2.5, radius + B.ringPad + 3)
|
||||||
-- Primary crisp focus ring
|
-- 2. Inner solid white focus border
|
||||||
Theme.strokeRounded(x - B.ringPad, y - B.ringPad,
|
Theme.strokeRounded(x - B.ringPad, y - B.ringPad,
|
||||||
w + 2 * B.ringPad, h + 2 * B.ringPad, PAL.lineStrong,
|
w + 2 * B.ringPad, h + 2 * B.ringPad, PAL.ink,
|
||||||
0.90 + 0.10 * glowPulse, B.ringWidth, radius + B.ringPad)
|
0.95 + 0.05 * glowPulse, 2.5, radius + B.ringPad)
|
||||||
|
-- 3. Subtle luminous white fill overlay so the entire button body shines
|
||||||
|
Theme.fillRounded(x, y, w, h, PAL.ink, 0.12 + 0.06 * glowPulse, radius)
|
||||||
elseif glowA then
|
elseif glowA then
|
||||||
Theme.strokeRounded(x - B.ringPad, y - B.ringPad,
|
Theme.strokeRounded(x - B.ringPad, y - B.ringPad,
|
||||||
w + 2 * B.ringPad, h + 2 * B.ringPad, PAL.lineStrong,
|
w + 2 * B.ringPad, h + 2 * B.ringPad, PAL.lineStrong,
|
||||||
|
|||||||
Reference in New Issue
Block a user