mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 01:34:06 -04:00
349 lines
12 KiB
Lua
349 lines
12 KiB
Lua
-- Shared button faces for launcher and save-editor input adapters.
|
|
-- The owner supplies fonts, pointer state and text drawing; this module only paints.
|
|
local Theme = require("src.ui.kit.Theme")
|
|
local PAL = Theme.PAL
|
|
local Icons = require("src.ui.kit.Icons")
|
|
local Button = {}
|
|
local KINDS = {
|
|
primary = { fill = PAL.green, ink = PAL.inverse },
|
|
good = { fill = PAL.green, ink = PAL.inverse },
|
|
accent = { fill = PAL.buttonBlue, ink = PAL.heading },
|
|
warn = { fill = PAL.yellow, ink = PAL.inverse },
|
|
danger = { fill = PAL.surface, ink = PAL.red, stroke = PAL.red },
|
|
ghost = { fill = PAL.surface, ink = PAL.text, stroke = PAL.line },
|
|
disabled = { fill = PAL.surface, ink = PAL.muted, stroke = PAL.line },
|
|
}
|
|
Button.KINDS = KINDS
|
|
|
|
-- Complete labels wrap between words. The input adapter sizes the control
|
|
-- from these same metrics; the painter never substitutes an ellipsis.
|
|
function Button.labelLayout(Kit, w, h, label, opts)
|
|
local name = opts.font or "button"
|
|
local lineH = Kit.textHeight(name)
|
|
local pad = Theme.BUTTON.labelInset * Kit.scale / 2
|
|
local trailing = opts.trailingIcon and h * 0.65 or 0
|
|
local available = w - 2 * pad - trailing
|
|
local function wrap(width)
|
|
local lines, line, widest = {}, "", 0
|
|
for word in tostring(label):gmatch("%S+") do
|
|
local nextLine = line == "" and word or (line .. " " .. word)
|
|
if line ~= "" and Kit.textWidth(name, nextLine) > width then
|
|
lines[#lines + 1] = line
|
|
widest = math.max(widest, Kit.textWidth(name, line))
|
|
line = word
|
|
else
|
|
line = nextLine
|
|
end
|
|
end
|
|
if line ~= "" then
|
|
lines[#lines + 1] = line
|
|
widest = math.max(widest, Kit.textWidth(name, line))
|
|
end
|
|
return lines, widest
|
|
end
|
|
local lines, widest = wrap(available)
|
|
local mode, size, gap
|
|
if opts.icon then
|
|
size = math.floor(math.min(w, h) * 0.42)
|
|
gap = math.floor(7 * Kit.scale)
|
|
local sideW = available - size - gap
|
|
local sideLines, sideWidth = wrap(sideW)
|
|
if not opts.iconStack and sideWidth <= sideW and #sideLines * lineH <= h - 4 * Kit.scale then
|
|
lines, widest, available, mode = sideLines, sideWidth, sideW, "side"
|
|
elseif size + math.max(2, 3 * Kit.scale) + #lines * lineH <= h - 4 * Kit.scale then
|
|
mode, gap = "stack", math.max(2, 3 * Kit.scale)
|
|
end
|
|
end
|
|
return {
|
|
font = name,
|
|
lines = lines,
|
|
width = widest,
|
|
available = available,
|
|
pad = pad,
|
|
trailing = trailing,
|
|
lineH = lineH,
|
|
height = #lines * lineH + (mode == "stack" and size + gap or 0),
|
|
mode = mode,
|
|
iconSize = size,
|
|
gap = gap,
|
|
}
|
|
end
|
|
|
|
function Button.draw(Kit, x, y, w, h, label, opts, hot, focused)
|
|
local G = love and love.graphics
|
|
local enabled = opts.enabled ~= false
|
|
local face = opts.face or "fill"
|
|
local B = Theme.BUTTON
|
|
local radius = opts.radius or B.radius
|
|
local segments = opts.segments
|
|
local active = opts.active or opts.on
|
|
local invert = false
|
|
local fill, ink, stroke, strokeA, strokeWidth, doEmboss, doRing, glowA
|
|
if face == "bare" then
|
|
ink = opts.ink or PAL.heading
|
|
doRing = focused
|
|
elseif face == "selection" then
|
|
-- A selected control keeps its geometry, dark face and label layout.
|
|
-- Selection is an inset accent outline, never a filled keycap.
|
|
fill = PAL.surface
|
|
ink = PAL.text
|
|
stroke = (active or focused) and PAL.blue or PAL.line
|
|
strokeA = (active or focused) and Theme.A.focus or (hot and Theme.A.hover or Theme.A.hairline)
|
|
strokeWidth = (active or focused) and 2 or 1
|
|
doRing = focused or hot
|
|
elseif face == "invert" then
|
|
invert = false
|
|
fill = (hot or focused) and PAL.raised or (opts.fill or PAL.surface)
|
|
ink = opts.ink or PAL.heading
|
|
stroke = opts.stroke or PAL.line
|
|
strokeA = (hot or focused) and Theme.A.focus or Theme.A.hairline
|
|
doRing = focused or hot
|
|
elseif face == "tab" then
|
|
invert = active and true or false
|
|
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 = (focused or hot) and Theme.A.focus
|
|
or (opts.color and Theme.A.hover or Theme.A.hairline)
|
|
end
|
|
doRing = focused or hot
|
|
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
|
|
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 = (opts.kind == "primary" or opts.kind == "good" or opts.kind == "accent")
|
|
stroke = kind.stroke
|
|
strokeA = stroke and Theme.A.hover or nil
|
|
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
|
|
if face ~= "bare" then
|
|
Theme.fillRounded(x, y, w, h, fill, enabled and 1 or B.disabledA, radius, segments)
|
|
end
|
|
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, strokeWidth or 1, radius, segments)
|
|
end
|
|
if doRing then
|
|
Theme.fillRounded(
|
|
x,
|
|
y,
|
|
w,
|
|
h,
|
|
PAL.ink,
|
|
Kit.mouseDown and hot and 0.04 or 0.09,
|
|
radius,
|
|
segments
|
|
)
|
|
if face ~= "selection" then
|
|
Theme.strokeRounded(
|
|
x,
|
|
y,
|
|
w,
|
|
h,
|
|
PAL.ink,
|
|
focused and 1 or 0.7,
|
|
focused and 2 or 1,
|
|
radius,
|
|
segments
|
|
)
|
|
end
|
|
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,
|
|
segments
|
|
)
|
|
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" and face ~= "selection"
|
|
end
|
|
if opts.fullLabel and hasLabel then
|
|
local layout = opts.labelLayout or Button.labelLayout(Kit, w, h, label, opts)
|
|
local lx, ly = x + layout.pad, y + (h - #layout.lines * layout.lineH) / 2
|
|
if layout.mode == "side" then
|
|
local groupW = layout.iconSize + layout.gap + layout.width
|
|
local ix = opts.align == "left" and lx or x + (w - layout.trailing - groupW) / 2
|
|
Icons.draw(
|
|
opts.icon,
|
|
ix,
|
|
y + (h - layout.iconSize) / 2,
|
|
layout.iconSize,
|
|
ink,
|
|
enabled and 1 or B.disabledA
|
|
)
|
|
lx = ix + layout.iconSize + layout.gap
|
|
elseif layout.mode == "stack" then
|
|
local top = y + (h - layout.height) / 2
|
|
Icons.draw(
|
|
opts.icon,
|
|
x + (w - layout.trailing - layout.iconSize) / 2,
|
|
top,
|
|
layout.iconSize,
|
|
ink,
|
|
enabled and 1 or B.disabledA
|
|
)
|
|
ly = top + layout.iconSize + layout.gap
|
|
end
|
|
for i, line in ipairs(layout.lines) do
|
|
local lineY = ly + (i - 1) * layout.lineH
|
|
if layout.mode == "side" or opts.align == "left" then
|
|
if bold then
|
|
Kit.textBold(layout.font, line, lx, lineY, ink)
|
|
else
|
|
Kit.text(layout.font, line, lx, lineY, ink)
|
|
end
|
|
elseif bold then
|
|
Kit.textCenterBold(layout.font, line, lx, lineY, layout.available, ink)
|
|
else
|
|
Kit.textCenter(layout.font, line, lx, lineY, layout.available, ink)
|
|
end
|
|
end
|
|
elseif opts.icon and opts.iconStack then
|
|
local size = math.floor(math.min(w * 0.5, h * 0.4))
|
|
local labelH = Kit.textHeight(fname)
|
|
local gap = math.max(2, math.floor(3 * Kit.scale))
|
|
local top = y + (h - size - gap - labelH) / 2
|
|
Icons.draw(opts.icon, x + (w - size) / 2, top, size, ink, enabled and 1 or B.disabledA)
|
|
local shown = Kit.ellipsize(fname, label or "", w - B.labelInset * Kit.scale)
|
|
Kit.textCenter(fname, shown, x, top + size + gap, w, ink)
|
|
elseif opts.icon then
|
|
local box = math.min(w, h)
|
|
local size = math.floor(box * (hasLabel and 0.42 or 0.5) * (opts.iconScale or 1))
|
|
local gap = math.floor(7 * Kit.scale)
|
|
local shown = hasLabel
|
|
and Kit.ellipsize(
|
|
fname,
|
|
label,
|
|
w - size - gap - 2 * B.labelPad * Kit.scale - (opts.trailingIcon and h * 0.65 or 0)
|
|
)
|
|
or ""
|
|
local groupW = size + (hasLabel and gap + Kit.textWidth(fname, shown) or 0)
|
|
local ix = opts.align == "left" and x + B.labelPad * Kit.scale or x + (w - groupW) / 2
|
|
Icons.draw(opts.icon, ix, y + (h - size) / 2, size, ink, enabled and 1 or B.disabledA)
|
|
if hasLabel then
|
|
if bold then
|
|
Kit.textBold(fname, shown, ix + size + gap, ty, ink)
|
|
else
|
|
Kit.text(fname, shown, ix + size + gap, ty, ink)
|
|
end
|
|
end
|
|
elseif 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)
|
|
Theme.col(ink, enabled and 1 or B.disabledA)
|
|
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)
|
|
elseif letter then
|
|
if opts.letterBold then
|
|
Kit.textCenterBold(fname, letter, x, ty, h, ink)
|
|
else
|
|
Kit.textCenter(fname, letter, x, ty, h, ink)
|
|
end
|
|
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 - (opts.trailingIcon and h * 0.65 or 0)
|
|
)
|
|
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 opts.trailingIcon and G then
|
|
local size = math.floor(h * 0.4)
|
|
Icons.draw(
|
|
opts.trailingIcon,
|
|
x + w - size - B.labelPad * Kit.scale,
|
|
y + (h - size) / 2,
|
|
size,
|
|
ink,
|
|
enabled and 1 or B.disabledA
|
|
)
|
|
end
|
|
end
|
|
return Button
|