mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
big ui moment
This commit is contained in:
+189
-24
@@ -65,6 +65,18 @@ function Kit.beginFrame(mx, my, clicked, wheel)
|
||||
Kit.mouseX, Kit.mouseY = mx, my
|
||||
Kit.mouseClicked = clicked
|
||||
Kit.wheelY = wheel or 0
|
||||
-- Held-button state is polled, not evented: the editor is hosted both
|
||||
-- standalone and inside the launcher, and neither routes mousereleased
|
||||
-- here. Touch drag scrolling (#715) rides this poll, so it works in both
|
||||
-- hosts without new plumbing. The stub has no love.mouse.isDown; a frame
|
||||
-- without it simply has no drags.
|
||||
local down = false
|
||||
if love and love.mouse and love.mouse.isDown then
|
||||
down = love.mouse.isDown(1) and true or false
|
||||
end
|
||||
Kit.mouseDown = down
|
||||
if not down then Kit._drag = nil end
|
||||
Kit.resetClip()
|
||||
if love and love.timer and love.timer.getTime then
|
||||
Kit.time = love.timer.getTime()
|
||||
end
|
||||
@@ -83,11 +95,23 @@ end
|
||||
-- (720x1560) is TALLER than the desktop reference and barely half as wide, so
|
||||
-- a height-only scale drew a 1.6x desktop layout into a 720px window and every
|
||||
-- right-aligned cluster in the chrome landed on top of the block to its left.
|
||||
-- The layout needs roughly 1000 logical px of width, so the window now pays
|
||||
-- for both axes. Every desktop and landscape size still lands on the height
|
||||
-- term, which is why they stay pixel-identical to before.
|
||||
--
|
||||
-- The #497 answer was to shrink the whole layout down to fit the width
|
||||
-- (floor 0.62), which #715 showed is its own failure: a small window got a
|
||||
-- complete but unreadably tiny desktop layout, and the panels still assumed
|
||||
-- their columns fit. Shrink-to-fit is gone. The scale now never dips below
|
||||
-- 0.9, so text and the 26px tap targets stay readable everywhere, and a
|
||||
-- narrow window is answered by REFLOW instead: every panel compares its real
|
||||
-- pixel width against what its columns need (Party/Boxes/Items stack their
|
||||
-- cards, Dex/Events drop grid columns, the chrome wraps its button row) and
|
||||
-- whatever no longer fits vertically scrolls through Kit.scroll /
|
||||
-- Kit.scrollPixels. The width term survives only to keep a portrait phone
|
||||
-- from inflating to the 1.6 cap its height alone would buy: 640 real px is
|
||||
-- the narrowest the single-row chrome fits at scale 1. Desktop sizes
|
||||
-- (width >= 640 * height / 768) still land on the height term, so they stay
|
||||
-- pixel-identical to before.
|
||||
function Kit.layout(width, height)
|
||||
local s = Theme.clamp(math.min(width / 1000, height / 768), 0.62, 1.6)
|
||||
local s = Theme.clamp(math.min(width / 640, height / 768), 0.9, 1.6)
|
||||
local key = ("%dx%d"):format(width, height)
|
||||
if Kit._fontKey ~= key then
|
||||
Kit._fontKey = key
|
||||
@@ -128,11 +152,36 @@ function Kit.blur()
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------- hit testing
|
||||
-- A widget inside a scrolled clip region can sit at coordinates outside the
|
||||
-- visible rect (#715: stacked panels scroll in pixels), so the active clip
|
||||
-- bounds the hit: what the user cannot see cannot take the tap.
|
||||
function Kit.hit(x, y, w, h)
|
||||
local c = Kit._clipRect
|
||||
if c and not (Kit.mouseX >= c.x and Kit.mouseX <= c.x + c.w
|
||||
and Kit.mouseY >= c.y and Kit.mouseY <= c.y + c.h) then
|
||||
return false
|
||||
end
|
||||
return Kit.mouseX >= x and Kit.mouseX <= x + w
|
||||
and Kit.mouseY >= y and Kit.mouseY <= y + h
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------ layout audit
|
||||
-- #715 reflow tests: when a test sets Kit.audit to a table, every control
|
||||
-- that could take a click this frame appends its rect (plus the clip that
|
||||
-- bounds it), so a window-size sweep can assert that no two controls
|
||||
-- overlap and none escapes the window. Shielded widgets are skipped: under
|
||||
-- a modal they cannot take the tap, and the modal legitimately covers them.
|
||||
Kit.audit = nil
|
||||
|
||||
local function audit(class, x, y, w, h, label)
|
||||
local a = Kit.audit
|
||||
if not a or Kit.blockClicks then return end
|
||||
local c = Kit._clipRect
|
||||
a[#a + 1] = { class = class, x = x, y = y, w = w, h = h,
|
||||
label = tostring(label or ""),
|
||||
clip = c and { x = c.x, y = c.y, w = c.w, h = c.h } or nil }
|
||||
end
|
||||
|
||||
function Kit.hover(x, y, w, h)
|
||||
return Kit.hit(x, y, w, h)
|
||||
end
|
||||
@@ -224,6 +273,7 @@ end
|
||||
-- true when the row was clicked this frame.
|
||||
function Kit.row(x, y, w, h, selected, accent, r)
|
||||
r = r or 12 * Kit.scale
|
||||
audit("row", x, y, w, h, "row")
|
||||
if not G then return Kit.press(x, y, w, h) end
|
||||
accent = accent or PAL.green
|
||||
if selected then Theme.glow(x, y, w, h, r, accent, 0.45) end
|
||||
@@ -280,6 +330,9 @@ local KINDS = {
|
||||
function Kit.button(x, y, w, h, label, opts)
|
||||
opts = opts or {}
|
||||
local enabled = opts.enabled ~= false
|
||||
-- disabled buttons audit too: rule 3 keeps them visible, so they still
|
||||
-- must not paint over a neighbour (#715)
|
||||
audit("control", x, y, w, h, label)
|
||||
local kind = KINDS[enabled and (opts.kind or "ghost") or "disabled"]
|
||||
local r = opts.radius or 10 * Kit.scale
|
||||
local hot = enabled and Kit.hover(x, y, w, h)
|
||||
@@ -327,6 +380,7 @@ end
|
||||
-- A pill toggle (badges, dex SEEN/OWN, event sub-tabs). `on` colours it;
|
||||
-- returns true when clicked.
|
||||
function Kit.chip(x, y, w, h, label, on, onColor, offColor)
|
||||
audit("control", x, y, w, h, label)
|
||||
local c = on and (onColor or PAL.green) or (offColor or PAL.steel)
|
||||
if G then
|
||||
local r = 6 * Kit.scale
|
||||
@@ -367,6 +421,7 @@ end
|
||||
-- routes love.textinput / love.keypressed in through Kit.textinput /
|
||||
-- Kit.keypressed. Returns the (possibly edited) value; the caller stores it.
|
||||
function Kit.textfield(id, x, y, w, h, value, placeholder)
|
||||
audit("control", x, y, w, h, id)
|
||||
value = tostring(value or "")
|
||||
if Kit.press(x, y, w, h) then Kit.focus = id end
|
||||
local focused = (Kit.focus == id)
|
||||
@@ -428,8 +483,12 @@ function Kit.pager(x, y, w, offset, total, perPage)
|
||||
local shown = math.min(perPage, math.max(0, total - offset))
|
||||
local label = ("%d-%d of %d"):format(total > 0 and offset + 1 or 0,
|
||||
offset + shown, total)
|
||||
Kit.text("mono", label, x + 2 * bw + 20 * Kit.scale,
|
||||
y + (h - Kit.textHeight("mono")) / 2, PAL.caption)
|
||||
-- the counter clips to the width the caller granted: a panel parking a
|
||||
-- button on the pager line passes a reduced w and the text yields instead
|
||||
-- of running underneath it (#715)
|
||||
local labelX = x + 2 * bw + 20 * Kit.scale
|
||||
Kit.text("mono", Kit.ellipsize("mono", label, math.max(0, x + w - labelX)),
|
||||
labelX, y + (h - Kit.textHeight("mono")) / 2, PAL.caption)
|
||||
return offset, h
|
||||
end
|
||||
|
||||
@@ -443,37 +502,143 @@ end
|
||||
-- panel under an open species picker would scroll through the modal.
|
||||
local SCROLL_ROWS = 3
|
||||
|
||||
function Kit.scroll(x, y, w, h, offset, total, perPage)
|
||||
-- `step` is optional and exists for grids: a 4-column dex page must move in
|
||||
-- multiples of 4 or the columns shear. Lists leave it nil and keep the old
|
||||
-- behaviour bit for bit (wheel notch = 3 rows, drag = 1 row per row height).
|
||||
function Kit.scroll(x, y, w, h, offset, total, perPage, step)
|
||||
local maxOffset = math.max(0, (total or 0) - (perPage or 0))
|
||||
offset = Theme.clamp(offset or 0, 0, maxOffset)
|
||||
if Kit.blockClicks or (Kit.wheelY or 0) == 0 then return offset end
|
||||
if Kit.blockClicks then return offset end
|
||||
|
||||
-- Touch drag (#715): a phone has no wheel and the pagers are small
|
||||
-- targets, so a held pointer dragging vertically over the list body
|
||||
-- scrolls it. The drag is keyed to the rect it started in and follows the
|
||||
-- pointer even once it leaves, like every native scroll view; the press
|
||||
-- frame itself still dispatches as a click, which is the pre-existing
|
||||
-- press-on-down contract, so a tap keeps selecting rows.
|
||||
local dragStep = math.max(1, step or 1)
|
||||
if Kit.mouseDown and maxOffset > 0 and h > 0 and (perPage or 0) > 0 then
|
||||
local key = math.floor(x) .. ":" .. math.floor(y)
|
||||
local d = Kit._drag
|
||||
if not d and Kit.hit(x, y, w, h) then
|
||||
Kit._drag = { key = key, startY = Kit.mouseY, base = offset }
|
||||
elseif d and d.key == key then
|
||||
local visRows = math.max(1, math.floor(perPage / dragStep))
|
||||
local rowPx = math.max(1, h / visRows)
|
||||
local moved = math.floor((d.startY - Kit.mouseY) / rowPx + 0.5) * dragStep
|
||||
offset = Theme.clamp(d.base + moved, 0, maxOffset)
|
||||
end
|
||||
end
|
||||
|
||||
if (Kit.wheelY or 0) == 0 then return offset end
|
||||
if not Kit.hit(x, y, w, h) then return offset end
|
||||
-- LOVE reports wheel-up as positive y; up moves the window toward the top
|
||||
-- of the list, which is a smaller offset.
|
||||
local rows = math.max(1, math.min(SCROLL_ROWS, perPage or SCROLL_ROWS))
|
||||
local step = (Kit.wheelY > 0) and -rows or rows
|
||||
local rows = step or math.max(1, math.min(SCROLL_ROWS, perPage or SCROLL_ROWS))
|
||||
local notch = (Kit.wheelY > 0) and -rows or rows
|
||||
Kit.wheelY = 0
|
||||
return Theme.clamp(offset + step, 0, maxOffset)
|
||||
return Theme.clamp(offset + notch, 0, maxOffset)
|
||||
end
|
||||
|
||||
-- Clip drawing to a rect (list bodies). No-ops under the headless stub.
|
||||
function Kit.pushClip(x, y, w, h)
|
||||
-- A compact mobile viewport can leave a panel with no room for a list.
|
||||
-- LÖVE rejects negative scissor dimensions, so treat an exhausted clip
|
||||
-- region as empty instead of passing invalid geometry through to it.
|
||||
Kit._clipActive = G and G.setScissor ~= nil
|
||||
if Kit._clipActive then
|
||||
if w <= 0 or h <= 0 then
|
||||
G.setScissor(0, 0, 0, 0)
|
||||
else
|
||||
G.setScissor(math.floor(x), math.floor(y), math.ceil(w), math.ceil(h))
|
||||
-- Pixel-unit sibling of Kit.scroll for a whole stacked card column (#715
|
||||
-- reflow): `offset` is a pixel offset into `contentH` pixels of laid-out
|
||||
-- content shown through an `h`-pixel viewport. Same three rules as
|
||||
-- Kit.scroll (pointer-inside only, notch consumed, shielded by
|
||||
-- Kit.blockClicks), same drag contract (a tap still dispatches as a click).
|
||||
-- Call it AFTER the content so any inner Kit.scroll list gets first claim on
|
||||
-- a wheel notch or drag that lands over it.
|
||||
function Kit.scrollPixels(x, y, w, h, offset, contentH)
|
||||
local maxOffset = math.max(0, (contentH or 0) - math.max(0, h))
|
||||
offset = Theme.clamp(offset or 0, 0, maxOffset)
|
||||
if Kit.blockClicks then return offset end
|
||||
|
||||
if Kit.mouseDown and maxOffset > 0 and h > 0 then
|
||||
local key = "px:" .. math.floor(x) .. ":" .. math.floor(y)
|
||||
local d = Kit._drag
|
||||
if not d and Kit.hit(x, y, w, h) then
|
||||
Kit._drag = { key = key, startY = Kit.mouseY, base = offset }
|
||||
elseif d and d.key == key then
|
||||
offset = Theme.clamp(d.base + (d.startY - Kit.mouseY), 0, maxOffset)
|
||||
end
|
||||
end
|
||||
|
||||
if (Kit.wheelY or 0) == 0 then return offset end
|
||||
if not Kit.hit(x, y, w, h) then return offset end
|
||||
local notch = 48 * Kit.scale
|
||||
local delta = (Kit.wheelY > 0) and -notch or notch
|
||||
Kit.wheelY = 0
|
||||
return Theme.clamp(offset + delta, 0, maxOffset)
|
||||
end
|
||||
|
||||
-- Thin overlay scrollbar along the right edge of a list body, drawn after
|
||||
-- the rows so it stays visible. Pure indicator (the drag above and the
|
||||
-- pager are the controls): on a phone the old layout looked "stuck" because
|
||||
-- nothing said the list continued past the fold (#715).
|
||||
function Kit.scrollbar(x, y, w, h, offset, total, perPage)
|
||||
if not G then return end
|
||||
total, perPage = total or 0, perPage or 0
|
||||
if total <= perPage or h <= 0 or perPage <= 0 then return end
|
||||
local bw = 3 * Kit.scale
|
||||
local bx = x + w - bw
|
||||
Theme.col(PAL.cardBorder, 0.22)
|
||||
G.rectangle("fill", bx, y, bw, h, bw / 2, bw / 2)
|
||||
local maxOffset = total - perPage
|
||||
local th = math.max(18 * Kit.scale, h * perPage / total)
|
||||
local ty = y + (h - th) * (Theme.clamp(offset or 0, 0, maxOffset) / maxOffset)
|
||||
Theme.col(PAL.blue, 0.55)
|
||||
G.rectangle("fill", bx, ty, bw, th, bw / 2, bw / 2)
|
||||
end
|
||||
|
||||
-- Clip drawing to a rect (list bodies, scrolled cards). A stack since #715:
|
||||
-- a stacked panel scrolls its whole column inside one clip and the lists
|
||||
-- inside it push their own, so pushes nest by intersecting with the rect
|
||||
-- above and a pop restores that rect rather than clearing the scissor. The
|
||||
-- tracked rect also bounds Kit.hit, so a widget scrolled out of view is
|
||||
-- inert instead of taking taps aimed at whatever is drawn where it left.
|
||||
-- Under the headless stub the scissor is a no-op but the rect tracking (and
|
||||
-- so the hit fencing) still runs.
|
||||
local clipStack = {}
|
||||
|
||||
local function applyClip(rect)
|
||||
Kit._clipRect = rect
|
||||
if not (G and G.setScissor) then return end
|
||||
if not rect then
|
||||
G.setScissor()
|
||||
elseif rect.w <= 0 or rect.h <= 0 then
|
||||
-- A compact mobile viewport can leave a panel with no room for a list.
|
||||
-- LÖVE rejects negative scissor dimensions, so treat an exhausted clip
|
||||
-- region as empty instead of passing invalid geometry through to it.
|
||||
G.setScissor(0, 0, 0, 0)
|
||||
else
|
||||
G.setScissor(math.floor(rect.x), math.floor(rect.y),
|
||||
math.ceil(rect.w), math.ceil(rect.h))
|
||||
end
|
||||
end
|
||||
|
||||
function Kit.pushClip(x, y, w, h)
|
||||
local prev = clipStack[#clipStack]
|
||||
local x2, y2 = x + math.max(0, w), y + math.max(0, h)
|
||||
if prev then
|
||||
x, y = math.max(x, prev.x), math.max(y, prev.y)
|
||||
x2 = math.min(x2, prev.x + prev.w)
|
||||
y2 = math.min(y2, prev.y + prev.h)
|
||||
end
|
||||
local rect = { x = x, y = y, w = math.max(0, x2 - x), h = math.max(0, y2 - y) }
|
||||
clipStack[#clipStack + 1] = rect
|
||||
applyClip(rect)
|
||||
end
|
||||
|
||||
function Kit.popClip()
|
||||
if Kit._clipActive and G and G.setScissor then G.setScissor() end
|
||||
Kit._clipActive = false
|
||||
clipStack[#clipStack] = nil
|
||||
applyClip(clipStack[#clipStack])
|
||||
end
|
||||
|
||||
-- A pcall-ed draw that raised mid-clip must not leak the stack into later
|
||||
-- frames (every hit test would stay fenced to the dead rect), so the frame
|
||||
-- boundary clears it.
|
||||
function Kit.resetClip()
|
||||
for i = #clipStack, 1, -1 do clipStack[i] = nil end
|
||||
applyClip(nil)
|
||||
end
|
||||
|
||||
return Kit
|
||||
|
||||
Reference in New Issue
Block a user