CLOSES #455, CLOSES #487, CLOSES #501, CLOSES #540, CLOSES #585, CLOSES #591, CLOSES #593, CLOSES #595, CLOSES #597, CLOSES #599, CLOSES #600, CLOSES #606, CLOSES #607, CLOSES #610, CLOSES #613, CLOSES #616, CLOSES #620, CLOSES #626, CLOSES #632, CLOSES #633, CLOSES #647

This commit is contained in:
bryanthaboi
2026-08-02 08:17:16 -04:00
parent ebbc55c4d0
commit 35b3fa6d9c
87 changed files with 6349 additions and 285 deletions
+11 -2
View File
@@ -41,6 +41,10 @@ local S
-- vanilla records over an already-merged Data
local mods
local mouseClicked = false
-- Wheel notches queued by App.wheelmoved since the last draw, handed to Kit
-- there like mouseClicked is: LOVE delivers events before love.draw, so a
-- notch is always spent by the frame that follows it (#595).
local wheelY = 0
-- Which game's cache Data was loaded from. main.lua checks this before
-- opening the editor on a save from the other version, because the two
@@ -602,7 +606,7 @@ local function drawStatusBar(x, y, w, h)
and (ctrl .. "+S save . " .. ctrl ..
"+R reload . Esc clear selection . Close returns to the launcher")
or (ctrl .. "+S save . " .. ctrl ..
"+R reload . Esc clear selection . arrows pan map . wheel zoom")
"+R reload . Esc clear selection . arrows pan map . wheel scrolls lists")
local hintW = Kit.textWidth("tiny", hint)
Kit.textRight("tiny", hint, x + w - pad, y + (h - Kit.textHeight("tiny")) / 2, PAL.faint)
local avail = w - 2 * pad - hintW - 14 * s
@@ -620,8 +624,9 @@ function App.draw()
local s = Kit.scale
local mx, my = love.mouse.getPosition()
Kit.beginFrame(mx, my, mouseClicked)
Kit.beginFrame(mx, my, mouseClicked, wheelY)
mouseClicked = false
wheelY = 0
-- Modal shield. Kit has no z-order, so the picker cannot simply be drawn
-- last: the chrome and the panel underneath would take the same tap. The
-- shield goes up before anything dispatches and comes down only for the
@@ -699,9 +704,13 @@ end
function App.wheelmoved(x, y)
if not S then return end
-- The map tab spends the wheel on zoom; every other tab routes it through
-- Kit so whichever list the pointer is over takes it next draw (#595).
if S.tab == "map" and MapBrowser.wheelmoved then
MapBrowser.wheelmoved(S, y)
return
end
wheelY = wheelY + (y or 0)
end
function App.quit()
+34 -5
View File
@@ -1,10 +1,12 @@
-- Immediate-mode widget kit for the save editor, drawn in the launcher's
-- visual language (see Theme.lua and SaveEditor.dc.html).
--
-- Call Kit.beginFrame(mx, my, clicked) once per love.draw() before any widget
-- and Kit.endFrame() after the last one; widgets read the frame's mouse state
-- to decide hover / click, and endFrame retires the text-input queue so a
-- keystroke is never applied twice.
-- Call Kit.beginFrame(mx, my, clicked, wheel) once per love.draw() before any
-- widget and Kit.endFrame() after the last one; widgets read the frame's mouse
-- state to decide hover / click, and endFrame retires the text-input queue so a
-- keystroke is never applied twice. The wheel notches accumulated since the
-- last frame arrive the same way and are retired the same way: an unclaimed
-- notch dies with the frame rather than scrolling something later (#595).
--
-- Hit testing is a plain rect with no z-order, so panels must draw
-- overlapping controls in dispatch order and every target is >= 26px tall
@@ -17,6 +19,7 @@ local PAL = Theme.PAL
local Kit = {}
Kit.mouseX, Kit.mouseY = 0, 0
Kit.mouseClicked = false -- left button pressed this frame
Kit.wheelY = 0 -- wheel notches queued since the last frame (#595)
Kit.focus = nil -- id of the text field receiving keystrokes
Kit.time = 0
Kit.fonts = {}
@@ -58,9 +61,10 @@ local function canPrintf()
return G and type(G.printf) == "function"
end
function Kit.beginFrame(mx, my, clicked)
function Kit.beginFrame(mx, my, clicked, wheel)
Kit.mouseX, Kit.mouseY = mx, my
Kit.mouseClicked = clicked
Kit.wheelY = wheel or 0
if love and love.timer and love.timer.getTime then
Kit.time = love.timer.getTime()
end
@@ -68,8 +72,10 @@ end
-- Retire this frame's keystrokes. Anything typed while no field had focus is
-- dropped here rather than replayed into the next field that gets clicked.
-- A wheel notch no list claimed retires with them, for the same reason.
function Kit.endFrame()
for i = #edits, 1, -1 do edits[i] = nil end
Kit.wheelY = 0
end
-- Rebuild the font set when the window size changes. `s` matched the
@@ -427,6 +433,29 @@ function Kit.pager(x, y, w, offset, total, perPage)
return offset, h
end
-- ----------------------------------------------------------------- scroll
-- Mouse wheel over a list body (#595): same offset contract as Kit.pager, so
-- a list can carry both and stay on one page counter. Three rules, all of
-- them consequences of Kit having no z-order:
-- * only the list the pointer is inside takes the notch,
-- * the notch is consumed, so two stacked lists cannot both eat it,
-- * Kit.blockClicks shields it exactly as it shields Kit.press, or the
-- 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)
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 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
Kit.wheelY = 0
return Theme.clamp(offset + step, 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.
+1
View File
@@ -55,6 +55,7 @@ function State.new()
selectedItemId = nil,
selectedBagId = nil,
selectedPcId = nil,
itemPickOffset = 0, -- scroll position in the ADD ITEM list (#595)
bagOffset = 0,
pcOffset = 0,
+23 -7
View File
@@ -4,9 +4,10 @@
--
-- The picker is a searchable list rather than the old pair of arrows that
-- cycled one id at a time through ~250 items, which was the single worst
-- interaction in the editor. Badges sit in the wallet column as toggle
-- chips because they are boolean inventory flags, not stackable items, and
-- must not look like quantity rows.
-- interaction in the editor. It scrolls under the mouse wheel too (#595):
-- typing used to be the only way to reach an id past the first screenful.
-- Badges sit in the wallet column as toggle chips because they are boolean
-- inventory flags, not stackable items, and must not look like quantity rows.
local Bag = require("src.inventory.Bag")
local Theme = require("Theme")
@@ -97,8 +98,12 @@ function M.draw(S, Kit, x, y, w, h)
Kit.card(x, pickY, leftW, pickH)
Kit.caption(x + pad, pickY + pad, "ADD ITEM")
local qy = pickY + pad + Kit.textHeight("caption") + 8 * s
local prevQuery = S.itemQuery or ""
S.itemQuery = Kit.textfield("item-query", x + pad, qy, leftW - 2 * pad, 32 * s,
S.itemQuery or "", "search item ids...")
-- a new query is a new list: keep the first hit on screen rather than
-- leaving the view parked wherever the old result set had scrolled to
if S.itemQuery ~= prevQuery then S.itemPickOffset = 0 end
local choices = {}
for _, id in ipairs(S.cat.items) do
@@ -117,9 +122,14 @@ function M.draw(S, Kit, x, y, w, h)
local cRowH = 28 * s
local cGap = 5 * s
local visible = math.max(1, math.floor((listBottom - listTop) / (cRowH + cGap)))
-- #595: the wheel drives the same offset a pager would, so the whole
-- catalog is reachable with the mouse alone. Kit.scroll clamps, which is
-- also what pulls the view back when a narrower query shortens the list.
S.itemPickOffset = Kit.scroll(x + pad, listTop, leftW - 2 * pad,
listBottom - listTop, S.itemPickOffset or 0, #choices, visible)
Kit.pushClip(x + pad, listTop, leftW - 2 * pad, listBottom - listTop)
for i = 1, math.min(visible, #choices) do
local id = choices[i]
for i = 1, math.min(visible, #choices - S.itemPickOffset) do
local id = choices[S.itemPickOffset + i]
local ry = listTop + (i - 1) * (cRowH + cGap)
if Kit.row(x + pad, ry, leftW - 2 * pad, cRowH, id == S.selectedItemId,
PAL.green, 8 * s) then
@@ -130,10 +140,11 @@ function M.draw(S, Kit, x, y, w, h)
x + pad + 10 * s, ry + (cRowH - Kit.textHeight("mono")) / 2, PAL.text)
end
Kit.popClip()
-- the overflow count rides the caption line, where it can never collide
-- the position counter rides the caption line, where it can never collide
-- with the list body or the two add buttons below it
if #choices > visible then
Kit.textRight("micro", ("+%d more"):format(#choices - visible),
Kit.textRight("micro", ("%d-%d of %d"):format(S.itemPickOffset + 1,
math.min(S.itemPickOffset + visible, #choices), #choices),
x + leftW - pad, pickY + pad, PAL.faint)
elseif #choices == 0 then
Kit.text("mono", "no item matches", x + pad + 10 * s, listTop + 8 * s, PAL.faint)
@@ -195,6 +206,9 @@ function M.draw(S, Kit, x, y, w, h)
local rowGap = 6 * s
local perPage = math.max(1, math.floor((pagerY - 12 * s - rowsTop) / (rowH + rowGap)))
S.bagOffset = Ops.clamp(S.bagOffset or 0, 0, math.max(0, #order - perPage))
-- the wheel moves the same offset the pager below does (#595)
S.bagOffset = Kit.scroll(bagX + pad, rowsTop, listW - 2 * pad,
pagerY - 12 * s - rowsTop, S.bagOffset, #order, perPage)
if #order == 0 then
Kit.emptyBox(bagX + pad, rowsTop, listW - 2 * pad, 70 * s, "Bag is empty.")
@@ -221,6 +235,8 @@ function M.draw(S, Kit, x, y, w, h)
Kit.textRight("mono", ("%d kinds"):format(#pcOrder), pcX + listW - pad,
y + pad, PAL.caption)
S.pcOffset = Ops.clamp(S.pcOffset or 0, 0, math.max(0, #pcOrder - perPage))
S.pcOffset = Kit.scroll(pcX + pad, rowsTop, listW - 2 * pad,
pagerY - 12 * s - rowsTop, S.pcOffset, #pcOrder, perPage)
if #pcOrder == 0 then
Kit.emptyBox(pcX + pad, rowsTop, listW - 2 * pad, 70 * s,
"PC storage is empty. Items sent here have no slot cap.")