launcher updates

This commit is contained in:
bryanthaboi
2026-08-06 09:57:31 -04:00
parent 6f75a64c46
commit 984cefdc7b
14 changed files with 1366 additions and 1086 deletions
+28 -14
View File
@@ -29,8 +29,10 @@
-- 4. Lists PAGINATE. Row count is bounded by the page size, so a 500-mod
-- index costs exactly what a 10-mod one does. There is no virtualised
-- scroller and no momentum integrator to run.
-- 5. Draw flat. No stencil, no mesh, no blend-mode change, no rounded
-- corners (see Theme.lua) -- every one is a pipeline flush.
-- 5. Draw flat. No stencil, no mesh, no canvas, no shader, no blend-mode
-- change -- every one is a pipeline flush. Rounded corners, the emboss
-- and a card's drop shadow are allowed because they only add VERTICES at
-- the same pipeline state (see Theme.lua's header for the full rule).
--
-- ACCESSIBILITY / INPUT: every control is reachable four ways -- mouse,
-- touch (>= 30px targets), keyboard (spatial focus ring, arrows + Enter),
@@ -544,8 +546,9 @@ function Kit.row(x, y, w, h, selected, id)
-- The focus ring is a second inset outline, so it reads on both a black
-- row and a white selected one.
if focused then
Theme.stroke(x + 2, y + 2, w - 4, h - 4,
selected and PAL.inverse or PAL.lineStrong, Theme.A.focus, 1)
Theme.strokeRounded(x + 2, y + 2, w - 4, h - 4,
selected and PAL.inverse or PAL.lineStrong, Theme.A.focus, 1,
Theme.radius())
end
local clicked = Kit.press(x, y, w, h)
or (id ~= nil and Kit._activateId == id)
@@ -557,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.stroke(x, y, w, h, PAL.line, 0.22, 1)
Theme.strokeRounded(x, y, w, h, PAL.line, 0.22, 1, Theme.radius())
Kit.textCenter("button", Kit.ellipsize("button", message, w - 24 * Kit.scale),
x, y + (h - Kit.textHeight("button")) / 2, w, PAL.muted)
end
@@ -596,11 +599,16 @@ local KINDS = {
}
Kit.KINDS = KINDS
-- opts: { kind, font, enabled, align, id, glow }
-- 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 {}
@@ -611,6 +619,9 @@ function Kit.button(x, y, w, h, label, opts)
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
@@ -700,12 +711,13 @@ function Kit.checkbox(x, y, w, h, checked, label, id, labelColor)
local box = 20 * Kit.scale
local bx, by = x + 12 * Kit.scale, y + (h - box) / 2
if G then
local br = math.min(Theme.radius(), box / 3)
if checked then
Theme.fill(bx, by, box, box, PAL.ink, 1)
Theme.fillRounded(bx, by, box, box, PAL.ink, 1, br)
Kit.textCenter("small", "X", bx,
by + (box - Kit.textHeight("small")) / 2, box, PAL.inverse)
else
Theme.stroke(bx, by, box, box, PAL.line, Theme.A.hover, 1)
Theme.strokeRounded(bx, by, box, box, PAL.line, Theme.A.hover, 1, br)
end
local lx = bx + box + 12 * Kit.scale
Kit.text("mono", Kit.ellipsize("mono", label, x + w - lx - 10 * Kit.scale),
@@ -724,12 +736,14 @@ function Kit.toggle(x, y, w, h, on, id)
-- Track, then a knob inset inside it, so the control reads as a switch
-- rather than as a white square with a word next to it. The label sits
-- in the empty half, which is the half that says what pressing does.
Theme.stroke(x, y, w, h, PAL.line,
(focused or Kit.hover(x, y, w, h)) and Theme.A.focus or Theme.A.hover, 1)
local r = math.min(Theme.radius(), h / 2)
Theme.fillRounded(x, y, w, h, PAL.rowBg, 1, r)
Theme.strokeRounded(x, y, w, h, PAL.line,
(focused or Kit.hover(x, y, w, h)) and Theme.A.focus or Theme.A.hover, 1, r)
local inset = 3
local knob = w / 2 - inset
Theme.fill(on and (x + w / 2) or (x + inset), y + inset, knob, h - 2 * inset,
PAL.ink, 1)
Theme.fillRounded(on and (x + w / 2) or (x + inset), y + inset, knob,
h - 2 * inset, PAL.ink, 1, math.min(r, (h - 2 * inset) / 2))
Kit.textCenter("micro", on and "ON" or "OFF",
on and x or (x + w / 2), y + (h - Kit.textHeight("micro")) / 2, w / 2,
PAL.text)
@@ -768,8 +782,8 @@ function Kit.textfield(id, x, y, w, h, value, placeholder)
end
end
if G then
Theme.fill(x, y, w, h, PAL.bg, 1)
Theme.stroke(x, y, w, h, PAL.line,
Theme.fillRounded(x, y, w, h, PAL.bg, 1)
Theme.strokeRounded(x, y, w, h, PAL.line,
(focused or focusRing) and Theme.A.focus or Theme.A.hairline,
focused and 2 or 1)
local pad = 10 * Kit.scale
+84 -34
View File
@@ -1,7 +1,9 @@
-- High-contrast theme shared by the launcher (src/import/LauncherView.lua)
-- and the save editor (tools/save-editor/). This replaces the old navy
-- gradient look wholesale: black field, white hairline outlines, flat fills,
-- no gradients and no glows anywhere.
-- High-contrast theme for the launcher (src/import/LauncherView.lua). The
-- save editor keeps its own tools/save-editor/Theme.lua, whose primitives
-- take a radius where these take a colour -- do not cross-wire them. This
-- replaces the old navy gradient look wholesale: a near-black field with the
-- faintest red cast, cards a few values above it, white hairline outlines,
-- flat fills, no gradients and no glows anywhere.
--
-- That is not only a visual choice. Every effect this theme drops was a GPU
-- pipeline flush in the old renderer:
@@ -9,10 +11,13 @@
-- (G.stencil / setStencilTest / draw(mesh) = 3 state changes per card),
-- * glows set blend mode "add", drew 7 stacked rects, then set it back.
-- Flat fills with a 1px outline all share one pipeline state, so LOVE batches
-- an entire panel into a couple of draw calls. Controls do carry a small
-- corner radius and a two-rect emboss, which cost extra vertices but no state
-- change -- that is the tier of expense this theme is willing to pay, and the
-- tier above it (stencils, meshes, blend modes) is the one it will not.
-- an entire panel into a couple of draw calls. What this theme DOES pay for
-- is extra vertices at the same pipeline state: rounded corners, the
-- two-rect emboss on a control, and the three stacked rounded rects that make
-- a card's drop shadow (Theme.shadow). Vertices are the tier of expense this
-- theme is willing to pay; the tier above it -- stencils, meshes, blend-mode
-- changes, canvases, shaders -- is the one it will not, and a shadow drawn as
-- a blurred canvas would land squarely in it.
--
-- Emphasis is carried by INVERSION, not by colour weight: a selected or
-- focused control fills white and prints black. That keeps contrast at
@@ -25,10 +30,16 @@
local Theme = {}
local PAL = {
-- field + surfaces. Only three fills exist in the whole UI.
bg = { 0, 0, 0 }, -- the page, and every card interior
surface = { 0, 0, 0 }, -- cards/rows: same black, told apart by outline
raised = { 20, 20, 20 }, -- the one non-black fill: hover feedback
-- field + surfaces. The field carries a FAINT red cast (a few points of
-- red over an otherwise neutral near-black) and cards sit a few steps above
-- it in the same hue, so a card reads as a raised object rather than as an
-- outline drawn on the page. These are still flat fills -- the depth comes
-- from the value step plus Theme.shadow, not from a gradient.
field = { 16, 8, 10 }, -- the page BEHIND the cards
bg = { 0, 0, 0 }, -- true black: button rests, field interiors
surface = { 28, 21, 24 }, -- card interiors
rowBg = { 20, 14, 17 }, -- rows inside a card, one step below it
raised = { 44, 34, 38 }, -- hover feedback
ink = { 255, 255, 255 }, -- the selected/focused fill
-- outlines. Two weights only: a hairline for structure, solid for focus.
line = { 255, 255, 255 }, -- hairline, drawn at alpha 0.35
@@ -55,7 +66,6 @@ local PAL = {
}
-- Semantic aliases kept so ported call sites read the same as before.
PAL.cardBorder = PAL.line
PAL.rowBg = PAL.surface
PAL.greenInk = PAL.inverse
PAL.blueInk = PAL.blue
PAL.redSoft = PAL.red
@@ -106,11 +116,34 @@ function Theme.fill(x, y, w, h, c, a)
G.rectangle("fill", snap(x), snap(y), snap(w), snap(h))
end
-- Corner radius for controls. Small and fixed: enough to read as a physical
-- key rather than a painted rectangle, small enough that the extra
-- tessellation is noise next to the rest of the frame.
-- Corner radius for controls. Fixed rather than scaled: LOVE tessellates a
-- rounded rect by radius, so a scale-driven radius would change the vertex
-- count with the window size, and these are the two tiers the design needs.
-- Controls get the smaller one, containers the larger, so a button never
-- looks like a card and a card never looks like a button.
function Theme.radius()
return 4
return 8
end
function Theme.cardRadius()
return 14
end
-- DROP SHADOW. Three stacked rounded rects at low alpha, each one step wider
-- and one step lower than the last -- a cheap falloff that needs no blur, no
-- canvas and no blend-mode change, so it stays inside the pipeline budget the
-- rest of this file is written to. Drawn BEFORE the surface it belongs to,
-- and never for a control (only containers cast one, or the whole screen
-- reads as floating debris).
function Theme.shadow(x, y, w, h, r)
if not G or w <= 0 or h <= 0 then return end
r = r or Theme.cardRadius()
for i = 1, 3 do
local spread = i * 2
col(PAL.bg, 0.13)
G.rectangle("fill", snap(x - spread), snap(y - spread + i * 3),
snap(w + 2 * spread), snap(h + 2 * spread), r + spread, r + spread)
end
end
function Theme.fillRounded(x, y, w, h, c, a, r)
@@ -139,7 +172,10 @@ function Theme.emboss(x, y, w, h, strength)
if not G or w <= 2 or h <= 2 then return end
strength = strength or 1
local t = math.max(1, math.floor(h * 0.10))
local r = Theme.radius()
-- The inset must clear the corner arc, but a narrow control (a stepper, a
-- row chip) is thinner than two radii -- clamp or the highlight rect goes
-- negative-width and vanishes.
local r = math.min(Theme.radius(), math.floor(w / 3))
-- highlight along the top
col(PAL.ink, 0.28 * strength)
G.rectangle("fill", snap(x) + r, snap(y) + 1, snap(w) - 2 * r, t)
@@ -166,35 +202,46 @@ function Theme.stroke(x, y, w, h, c, a, lw)
if probe("setLineWidth") then G.setLineWidth(1) end
end
-- The design's only container: black interior, white hairline. `emphasis`
-- raises the outline to full white (used for the focused/active card).
-- The design's only container: a rounded surface a few values above the
-- field, its own drop shadow, and a white hairline. `emphasis` raises the
-- outline to full white (used for the focused/active card).
function Theme.card(x, y, w, h, emphasis)
Theme.fill(x, y, w, h, PAL.bg, 1)
Theme.stroke(x, y, w, h, PAL.line, emphasis and Theme.A.focus or Theme.A.hairline, 1)
local r = Theme.cardRadius()
Theme.shadow(x, y, w, h, r)
Theme.fillRounded(x, y, w, h, PAL.surface, 1, r)
Theme.strokeRounded(x, y, w, h, PAL.line,
emphasis and Theme.A.focus or Theme.A.hairline, 1, r)
end
-- A list row. Three states, each one rect plus one outline:
-- normal black fill, hairline
-- hover near-black fill, brighter hairline
-- normal one value below the card it sits in, hairline
-- hover lifted fill, brighter hairline
-- selected WHITE fill (callers print ink = PAL.inverse over it)
function Theme.row(x, y, w, h, state)
local r = Theme.radius()
if state == "selected" then
Theme.fill(x, y, w, h, PAL.ink, 1)
Theme.fillRounded(x, y, w, h, PAL.ink, 1, r)
return PAL.inverse
end
Theme.fill(x, y, w, h, state == "hover" and PAL.raised or PAL.surface, 1)
Theme.stroke(x, y, w, h, PAL.line,
state == "hover" and Theme.A.hover or Theme.A.hairline, 1)
Theme.fillRounded(x, y, w, h,
state == "hover" and PAL.raised or PAL.rowBg, 1, r)
Theme.strokeRounded(x, y, w, h, PAL.line,
state == "hover" and Theme.A.hover or Theme.A.hairline, 1, r)
return PAL.text
end
-- A percentage meter (HP, box fill, dex completion, import progress).
-- pct is 0-100. Outline + solid white fill, no rounding.
-- pct is 0-100. Outline + solid fill, rounded to the track's own half-height
-- so a thin bar reads as a capsule instead of a clipped rectangle.
function Theme.meter(x, y, w, h, pct, c)
if not G then return end
Theme.stroke(x, y, w, h, PAL.line, Theme.A.hairline, 1)
local r = math.min(Theme.radius(), h / 2)
Theme.strokeRounded(x, y, w, h, PAL.line, Theme.A.hairline, 1, r)
local fill = (w - 2) * clamp((pct or 0) / 100, 0, 1)
if fill > 0 then Theme.fill(x + 1, y + 1, fill, h - 2, c or PAL.ink, 1) end
if fill > 0 then
Theme.fillRounded(x + 1, y + 1, fill, h - 2, c or PAL.ink, 1,
math.min(r, fill / 2))
end
end
-- The 4px tri-colour rail across the top of both windows: the only brand
@@ -325,11 +372,14 @@ function Theme.ellipsizeLeft(font, text, maxW)
return ell
end
-- The background: a flat black clear. One call, no mesh, no fan, no
-- allocation -- the old radial field built a 66-vertex mesh EVERY frame.
-- The background: one flat clear to the faintly red-cast field colour. One
-- call, no mesh, no fan, no allocation -- the old radial field built a
-- 66-vertex mesh EVERY frame. The tint is deliberately small (a handful of
-- points of red at near-black): enough that the cards read as sitting ON
-- something, not enough to compete with the tri-colour rail for brand duty.
function Theme.field()
if not G then return end
G.clear(0, 0, 0, 1)
G.clear(PAL.field[1] / 255, PAL.field[2] / 255, PAL.field[3] / 255, 1)
end
-- ------------------------------------------------------------------- fonts