mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 03:35:56 +02:00
This commit is contained in:
+128
-78
@@ -368,6 +368,7 @@ Kit._navPrevN = 0
|
||||
Kit._navSeen = {}
|
||||
Kit._navQueue = nil
|
||||
Kit._activateId = nil
|
||||
Kit._ringShown = false
|
||||
|
||||
-- Register a focusable. Returns true when it currently holds the ring.
|
||||
-- Shielded widgets do not register: while a modal owns the frame the ring
|
||||
@@ -383,11 +384,12 @@ function Kit.focusable(id, x, y, w, h)
|
||||
-- First focusable ever drawn adopts the ring, so keyboard users start
|
||||
-- somewhere rather than nowhere.
|
||||
if Kit.focusId == nil then Kit.focusId = id end
|
||||
return Kit.focusId == id
|
||||
return Kit._ringShown and Kit.focusId == id
|
||||
end
|
||||
|
||||
function Kit.navigate(dir)
|
||||
Kit._navQueue = dir
|
||||
Kit._ringShown = true
|
||||
end
|
||||
|
||||
function Kit.activateFocused()
|
||||
@@ -396,6 +398,7 @@ end
|
||||
|
||||
function Kit.setFocus(id)
|
||||
Kit.focusId = id
|
||||
Kit._ringShown = id ~= nil
|
||||
end
|
||||
|
||||
-- Pick the nearest focusable in `dir` from the current one. Candidates must
|
||||
@@ -530,13 +533,10 @@ Kit._audit = audit
|
||||
function Kit.tapMin() return math.floor(30 * Kit.scale) end
|
||||
|
||||
-- ---------------------------------------------------------------- surfaces
|
||||
function Kit.card(x, y, w, h, emphasis)
|
||||
Theme.card(x, y, w, h, emphasis)
|
||||
function Kit.card(x, y, w, h, variant)
|
||||
Theme.card(x, y, w, h, variant)
|
||||
end
|
||||
|
||||
-- A list row. `id` opts it into the focus ring; pass nil for decorative
|
||||
-- rows. Returns (clicked, inkColor) -- a selected row fills white, so the
|
||||
-- caller must print with the returned ink or it will draw white on white.
|
||||
function Kit.row(x, y, w, h, selected, id)
|
||||
audit("row", x, y, w, h, id or "row")
|
||||
local focused = id and Kit.focusable(id, x, y, w, h) or false
|
||||
@@ -560,7 +560,7 @@ end
|
||||
-- hairline says the same thing for one rect.)
|
||||
function Kit.emptyBox(x, y, w, h, message)
|
||||
if not G then return end
|
||||
Theme.strokeRounded(x, y, w, h, PAL.line, 0.22, 1, Theme.radius())
|
||||
Theme.card(x, y, w, h, "empty")
|
||||
Kit.textCenter("button", Kit.ellipsize("button", message, w - 24 * Kit.scale),
|
||||
x, y + (h - Kit.textHeight("button")) / 2, w, PAL.muted)
|
||||
end
|
||||
@@ -598,61 +598,130 @@ local KINDS = {
|
||||
disabled = { fill = PAL.steel, ink = PAL.inverse, flat = true },
|
||||
}
|
||||
Kit.KINDS = KINDS
|
||||
local NO_OPTS = {}
|
||||
|
||||
-- opts: { kind, font, enabled, align, id, glow, fill, ink }
|
||||
-- id -- opts into the focus ring (give every real control one)
|
||||
-- glow -- a pulsing outline for "something is waiting for you" (the
|
||||
-- update button). No blend-mode change: the alpha of the
|
||||
-- existing outline is animated instead.
|
||||
-- fill/ink -- override the kind's colours. The ONE caller is the
|
||||
-- launcher's Play button, which wears its cartridge colour
|
||||
-- (red/blue/gold) rather than a semantic one: on that screen
|
||||
-- "which game am I launching" outranks "what kind of verb is
|
||||
-- this", and the colour is already the tab's identity.
|
||||
-- Returns true when activated, by click OR by the focus ring's Enter/A.
|
||||
function Kit.button(x, y, w, h, label, opts)
|
||||
opts = opts or {}
|
||||
opts = opts or NO_OPTS
|
||||
local enabled = opts.enabled ~= false
|
||||
-- Disabled buttons audit too: they stay visible, so they still must not
|
||||
-- paint over a neighbour.
|
||||
audit("control", x, y, w, h, label)
|
||||
local focused = enabled and opts.id
|
||||
and Kit.focusable(opts.id, x, y, w, h) or false
|
||||
local kind = KINDS[enabled and (opts.kind or "ghost") or "disabled"]
|
||||
if enabled and opts.fill then
|
||||
kind = { fill = opts.fill, ink = opts.ink or PAL.inverse }
|
||||
end
|
||||
local hot = enabled and Kit.hover(x, y, w, h)
|
||||
|
||||
if G then
|
||||
-- The fill IS the control: a rounded, embossed, colour-coded key. A
|
||||
-- disabled button keeps its shape in a dead grey rather than
|
||||
-- disappearing, so a layout never reflows on state.
|
||||
Theme.fillRounded(x, y, w, h, kind.fill, enabled and 1 or 0.45)
|
||||
Theme.emboss(x, y, w, h, enabled and (hot and 1.3 or 1) or 0.4)
|
||||
if hot or focused then
|
||||
-- White ring outside the fill: legible on green, blue, yellow, red and
|
||||
-- white alike, which one darker/lighter shade per colour would not be.
|
||||
Theme.strokeRounded(x - 2, y - 2, w + 4, h + 4, PAL.lineStrong,
|
||||
Theme.A.focus, 2, Theme.radius() + 2)
|
||||
elseif opts.glow and enabled then
|
||||
-- "Something is waiting for you" (the update button): a pulsing ring.
|
||||
-- Pure alpha on one existing stroke -- no extra draw calls, no blend
|
||||
-- mode change.
|
||||
local a = 0.25 + 0.75 * (0.5 + 0.5 * math.sin(Kit.time * 3))
|
||||
Theme.strokeRounded(x - 2, y - 2, w + 4, h + 4, PAL.lineStrong, a, 2,
|
||||
Theme.radius() + 2)
|
||||
local face = opts.face or "fill"
|
||||
local B = Theme.BUTTON
|
||||
local radius = opts.radius or B.radius
|
||||
local active = opts.active or opts.on
|
||||
local invert = false
|
||||
local fill, ink, stroke, strokeA, doEmboss, doRing, glowA
|
||||
if face == "invert" then
|
||||
invert = hot
|
||||
fill = invert and (opts.hotFill or PAL.ink) or (opts.fill or PAL.surface)
|
||||
ink = invert and (opts.hotInk or PAL.inverse) or (opts.ink or PAL.heading)
|
||||
stroke = opts.stroke or PAL.line
|
||||
strokeA = invert and Theme.A.focus or Theme.A.hairline
|
||||
doRing = focused and not hot
|
||||
elseif face == "tab" then
|
||||
invert = active or focused or hot
|
||||
local tint = opts.color or opts.fill or PAL.ink
|
||||
fill = invert and tint or PAL.surface
|
||||
ink = invert and PAL.inverse or (opts.color or PAL.text)
|
||||
if not invert then
|
||||
stroke = tint
|
||||
strokeA = opts.color and Theme.A.hover or Theme.A.hairline
|
||||
end
|
||||
local fname = opts.font or "button"
|
||||
local ink = enabled and kind.ink or PAL.inverse
|
||||
local ty = y + (h - Kit.textHeight(fname)) / 2
|
||||
local shown = Kit.ellipsize(fname, label, w - 16 * Kit.scale)
|
||||
-- Button labels are bold: they are the shortest, most-scanned text on
|
||||
-- screen and sit on a saturated fill.
|
||||
if opts.align == "left" then
|
||||
Kit.textBold(fname, shown, x + 10 * Kit.scale, ty, ink)
|
||||
elseif face == "chip" then
|
||||
local c = opts.color or PAL.line
|
||||
invert = active and true or false
|
||||
if active then
|
||||
fill = c
|
||||
ink = PAL.inverse
|
||||
doEmboss = true
|
||||
else
|
||||
Kit.textCenterBold(fname, shown, x, ty, w, ink)
|
||||
fill = PAL.bg
|
||||
ink = c
|
||||
stroke = c
|
||||
strokeA = (focused or hot) and Theme.A.focus or Theme.A.hover
|
||||
end
|
||||
doRing = focused or hot
|
||||
else
|
||||
local kind = KINDS[enabled and (opts.kind or "ghost") or "disabled"]
|
||||
fill = (enabled and opts.fill) or kind.fill
|
||||
ink = (enabled and opts.ink) or kind.ink
|
||||
doEmboss = true
|
||||
doRing = hot or focused
|
||||
if opts.glow and enabled and not doRing then
|
||||
glowA = B.glowBase + B.glowAmp * (0.5 + 0.5 * math.sin(Kit.time * B.glowHz))
|
||||
end
|
||||
end
|
||||
if opts.emboss ~= nil then doEmboss = opts.emboss end
|
||||
if opts.ring ~= nil then doRing = opts.ring end
|
||||
if G then
|
||||
Theme.fillRounded(x, y, w, h, fill, enabled and 1 or B.disabledA, radius)
|
||||
if doEmboss then
|
||||
local es = enabled and ((hot or focused) and B.embossHot or B.embossRest)
|
||||
or B.embossDisabled
|
||||
Theme.emboss(x, y, w, h, es)
|
||||
end
|
||||
if strokeA then
|
||||
Theme.strokeRounded(x, y, w, h, stroke, strokeA, 1, radius)
|
||||
end
|
||||
if doRing then
|
||||
Theme.strokeRounded(x - B.ringPad, y - B.ringPad,
|
||||
w + 2 * B.ringPad, h + 2 * B.ringPad, PAL.lineStrong,
|
||||
Theme.A.focus, B.ringWidth, radius + B.ringPad)
|
||||
elseif glowA then
|
||||
Theme.strokeRounded(x - B.ringPad, y - B.ringPad,
|
||||
w + 2 * B.ringPad, h + 2 * B.ringPad, PAL.lineStrong,
|
||||
glowA, B.ringWidth, radius + B.ringPad)
|
||||
end
|
||||
local fname = opts.font or ((face == "chip") and "micro" or "button")
|
||||
local ty = y + (h - Kit.textHeight(fname)) / 2
|
||||
local image = opts.image
|
||||
local drawFn = opts.drawFn
|
||||
local letter = opts.letter
|
||||
local hasLabel = label and label ~= ""
|
||||
local bold = opts.bold
|
||||
if bold == nil then bold = face ~= "tab" end
|
||||
if image then
|
||||
local box = h
|
||||
local boxX, boxY = x, y
|
||||
if not hasLabel then
|
||||
box = math.min(w, h)
|
||||
boxX = x + (w - box) / 2
|
||||
boxY = y + (h - box) / 2
|
||||
end
|
||||
local iw, ih = image:getDimensions()
|
||||
local pad = math.floor(box * (opts.iconPad or B.iconPad))
|
||||
local s = math.min((box - 2 * pad) / iw, (box - 2 * pad) / ih)
|
||||
if invert then Theme.col(PAL.inverse, 1)
|
||||
else Theme.col(PAL.ink, B.iconRestA) end
|
||||
G.draw(image, Theme.snap(boxX + (box - iw * s) / 2),
|
||||
Theme.snap(boxY + (box - ih * s) / 2), 0, s, s)
|
||||
if hasLabel then
|
||||
local lx = x + h + B.letterGap * Kit.scale
|
||||
if bold then Kit.textBold(fname, label, lx, ty, ink)
|
||||
else Kit.text(fname, label, lx, ty, ink) end
|
||||
end
|
||||
elseif drawFn then
|
||||
drawFn(x, y, w, h, invert or hot or focused)
|
||||
elseif letter then
|
||||
Kit.textCenter(fname, letter, x, ty, h, ink)
|
||||
if hasLabel then
|
||||
local lx = x + h + B.letterGap * Kit.scale
|
||||
if bold then Kit.textBold(fname, label, lx, ty, ink)
|
||||
else Kit.text(fname, label, lx, ty, ink) end
|
||||
end
|
||||
elseif hasLabel then
|
||||
local shown = Kit.ellipsize(fname, label, w - B.labelInset * Kit.scale)
|
||||
if opts.align == "left" then
|
||||
local lx = x + B.labelPad * Kit.scale
|
||||
if bold then Kit.textBold(fname, shown, lx, ty, ink)
|
||||
else Kit.text(fname, shown, lx, ty, ink) end
|
||||
elseif bold then
|
||||
Kit.textCenterBold(fname, shown, x, ty, w, ink)
|
||||
else
|
||||
Kit.textCenter(fname, shown, x, ty, w, ink)
|
||||
end
|
||||
end
|
||||
end
|
||||
if not enabled then return false end
|
||||
@@ -661,7 +730,6 @@ function Kit.button(x, y, w, h, label, opts)
|
||||
and Kit._activateId == opts.id)
|
||||
end
|
||||
|
||||
-- A small square control: +/- steppers, arrow cyclers, the row X.
|
||||
function Kit.stepper(x, y, w, h, glyph, opts)
|
||||
opts = opts or {}
|
||||
opts.kind = opts.kind or "ghost"
|
||||
@@ -669,31 +737,13 @@ function Kit.stepper(x, y, w, h, glyph, opts)
|
||||
return Kit.button(x, y, w, h, glyph, opts)
|
||||
end
|
||||
|
||||
-- A pill toggle (badges, dex SEEN/OWN, sub-tabs). `on` inverts it.
|
||||
local CHIP_OPTS = { face = "chip", font = "micro" }
|
||||
|
||||
function Kit.chip(x, y, w, h, label, on, color, id)
|
||||
audit("control", x, y, w, h, label)
|
||||
local focused = id and Kit.focusable(id, x, y, w, h) or false
|
||||
local c = color or PAL.line
|
||||
if G then
|
||||
local hot = focused or Kit.hover(x, y, w, h)
|
||||
if on then
|
||||
Theme.fillRounded(x, y, w, h, c, 1)
|
||||
Theme.emboss(x, y, w, h, 1)
|
||||
Kit.textCenterBold("micro", label, x,
|
||||
y + (h - Kit.textHeight("micro")) / 2, w, PAL.inverse)
|
||||
else
|
||||
Theme.fillRounded(x, y, w, h, PAL.bg, 1)
|
||||
Theme.strokeRounded(x, y, w, h, c,
|
||||
hot and Theme.A.focus or Theme.A.hover, 1)
|
||||
Kit.textCenterBold("micro", label, x,
|
||||
y + (h - Kit.textHeight("micro")) / 2, w, c)
|
||||
end
|
||||
if hot then
|
||||
Theme.strokeRounded(x - 2, y - 2, w + 4, h + 4, PAL.lineStrong,
|
||||
Theme.A.focus, 2, Theme.radius() + 2)
|
||||
end
|
||||
end
|
||||
return Kit.press(x, y, w, h) or (id ~= nil and Kit._activateId == id)
|
||||
CHIP_OPTS.active = on
|
||||
CHIP_OPTS.color = color
|
||||
CHIP_OPTS.id = id
|
||||
return Kit.button(x, y, w, h, label, CHIP_OPTS)
|
||||
end
|
||||
|
||||
-- A status label with no interaction: outlined text, the "INSTALLED"/"UPDATE"
|
||||
|
||||
Reference in New Issue
Block a user