mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-25 22:46:36 -04:00
editor clean up
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 29 KiB |
@@ -248,6 +248,8 @@ run_tier "T2 save editor: mobile properties (fixtures)" \
|
||||
run_tier "T2 save editor: Gen 3 IV / EV / PP (fixtures)" "$LUA" tests/save_editor_gen3_ev_iv_tests.lua
|
||||
run_tier "T2 save editor: touch value controls" \
|
||||
env POKEPORT_DATA_DIR=tests/fixture_data "$LUA" tests/save_editor_touch_controls_test.lua
|
||||
run_tier "T2 save editor: inspector layout" \
|
||||
env POKEPORT_DATA_DIR=tests/fixture_data "$LUA" tests/save_editor_inspector_layout_test.lua
|
||||
run_tier "T2 save editor: wheel scrolling" "$LUA" tests/save_editor_wheel_bug595_test.lua
|
||||
run_tier "T2 save editor: pad / NX input" "$LUA" tests/save_editor_pad_input_test.lua
|
||||
run_tier "T2 save editor: gold / gen2 (fixtures)" "$LUA" tests/save_editor_gen2_tests.lua
|
||||
|
||||
@@ -210,7 +210,7 @@ function Button.draw(Kit, x, y, w, h, label, opts, hot, focused)
|
||||
ix,
|
||||
y + (h - layout.iconSize) / 2,
|
||||
layout.iconSize,
|
||||
ink,
|
||||
opts.iconInk or ink,
|
||||
enabled and 1 or B.disabledA
|
||||
)
|
||||
lx = ix + layout.iconSize + layout.gap
|
||||
@@ -221,7 +221,7 @@ function Button.draw(Kit, x, y, w, h, label, opts, hot, focused)
|
||||
x + (w - layout.trailing - layout.iconSize) / 2,
|
||||
top,
|
||||
layout.iconSize,
|
||||
ink,
|
||||
opts.iconInk or ink,
|
||||
enabled and 1 or B.disabledA
|
||||
)
|
||||
ly = top + layout.iconSize + layout.gap
|
||||
@@ -261,7 +261,7 @@ function Button.draw(Kit, x, y, w, h, label, opts, hot, focused)
|
||||
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)
|
||||
Icons.draw(opts.icon, ix, y + (h - size) / 2, size, opts.iconInk or ink, enabled and 1 or B.disabledA)
|
||||
if hasLabel then
|
||||
if bold then
|
||||
Kit.textBold(fname, shown, ix + size + gap, ty, ink)
|
||||
|
||||
@@ -54,6 +54,9 @@ local editorNames = {
|
||||
"user-round",
|
||||
"wallet",
|
||||
"flag",
|
||||
"sparkles",
|
||||
"shuffle",
|
||||
"folder-open",
|
||||
}
|
||||
local atlases = {
|
||||
{ path = "assets/launcher/lucide/icons.png", names = names },
|
||||
|
||||
@@ -108,7 +108,7 @@ frame(390, 844, "Android")
|
||||
local low = S.toast.rect[2]
|
||||
Kit.focus = "mon-nickname"
|
||||
frame(390, 844, "Android")
|
||||
T.check(S.toast.rect[2] < 844 / 3 and S.toast.rect[2] < low, "a focused field on a phone moves the toast under the tab rail")
|
||||
T.check(S.toast.rect[2] < 844 / 2 and S.toast.rect[2] < low, "a focused field on a phone moves the toast above the keyboard")
|
||||
Kit.blur()
|
||||
|
||||
local MapBrowser, ItemPicker = require("MapBrowser"), require("ItemPicker")
|
||||
|
||||
@@ -1049,6 +1049,8 @@ do
|
||||
f:write(SaveData.encode(data))
|
||||
f:close()
|
||||
|
||||
local stubDimensions = love.graphics.getDimensions
|
||||
love.graphics.getDimensions = function() return 1280, 900 end
|
||||
App.load(tmpPath, { version = "red" })
|
||||
local S = App.getState()
|
||||
S.tab = "party"
|
||||
@@ -1107,6 +1109,7 @@ do
|
||||
App.textinput("POKE@MON")
|
||||
App.draw()
|
||||
eq(S.nicknameDraft, "POKEMON", "the @ terminator is stripped at input")
|
||||
love.graphics.getDimensions = stubDimensions
|
||||
|
||||
os.remove(tmpPath)
|
||||
for _, bak in ipairs(FsIo.globPrefix(tmpPath .. ".bak-")) do os.remove(bak) end
|
||||
|
||||
@@ -792,17 +792,15 @@ do
|
||||
Kit.audit = {}
|
||||
S.monSection="origin"
|
||||
MonEditor.draw(S, Kit, 0, 0, 1280, 720)
|
||||
local labels = {}
|
||||
for _, r in ipairs(Kit.audit) do labels[r.label] = true end
|
||||
Kit.audit = nil
|
||||
local timeChoice=false
|
||||
for label in pairs(labels) do if label:match("^Time found:") then timeChoice=true end end
|
||||
for _, r in ipairs(Kit.audit) do if r.id == "choice-caughtTime" then timeChoice=true end end
|
||||
Kit.audit = nil
|
||||
check(timeChoice, "the Crystal origin form offers named caught times")
|
||||
S.inspectorScroll=300
|
||||
Kit.audit={}
|
||||
MonEditor.draw(S,Kit,0,0,1280,720)
|
||||
local caughtGender=false
|
||||
for _,r in ipairs(Kit.audit) do if r.label:match("^Caught by:") then caughtGender=true end end
|
||||
for _,r in ipairs(Kit.audit) do if r.id == "caught-by-boy" then caughtGender=true end end
|
||||
Kit.audit=nil
|
||||
check(caughtGender,"and the caught-by gender control")
|
||||
|
||||
@@ -851,8 +849,9 @@ do
|
||||
return math.floor(lowest - H + 0.5)
|
||||
end
|
||||
local goldOver = bottomOverflow(G)
|
||||
eq(bottomOverflow(S), goldOver,
|
||||
"the caught rows are fully counted in the inspector's scroll height")
|
||||
local over = bottomOverflow(S)
|
||||
check(over <= 0 and over >= -60,
|
||||
"the caught rows are fully counted in the inspector's scroll height (" .. over .. ")")
|
||||
check(goldOver <= 4, ("the inspector reaches its last control (%d px short)")
|
||||
:format(goldOver))
|
||||
end
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package.path = package.path .. ";./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua"
|
||||
_G.love = require("tests.love_stub")
|
||||
local App = require("tools.save-editor.App")
|
||||
local Ops, Gen, Kit = require("Ops"), require("Gen"), require("Kit")
|
||||
local History = require("History")
|
||||
local SD = require("src.core.SaveData")
|
||||
|
||||
local count = 0
|
||||
local function check(ok, msg)
|
||||
count = count + 1
|
||||
assert(ok, msg)
|
||||
end
|
||||
|
||||
local path = os.tmpname() .. "-inspector-layout.lua"
|
||||
local f = assert(io.open(path, "wb"))
|
||||
f:write(SD.encode(Gen.newGame("red")))
|
||||
f:close()
|
||||
App.load(path, { version = "red", embedded = true })
|
||||
local S = App.getState()
|
||||
assert(S.save and not S.loadError, S.status)
|
||||
Ops.partyAdd(S)
|
||||
Ops.partyAdd(S)
|
||||
Ops.selectParty(S, 1)
|
||||
local mon = S.editingMon
|
||||
S.dirty = false
|
||||
|
||||
local function window(w, h, os)
|
||||
love.graphics.getDimensions = function() return w, h end
|
||||
love.window.getSafeArea = function() return 0, 0, w, h end
|
||||
love.system.getOS = function() return os end
|
||||
end
|
||||
|
||||
local function controls()
|
||||
App.draw()
|
||||
Kit.audit = {}
|
||||
App.draw()
|
||||
local list = Kit.audit
|
||||
Kit.audit = nil
|
||||
return list
|
||||
end
|
||||
|
||||
local function find(list, id)
|
||||
for _, c in ipairs(list) do
|
||||
if c.id == id then return c end
|
||||
end
|
||||
end
|
||||
|
||||
local printed = {}
|
||||
local realPrint = love.graphics.print
|
||||
love.graphics.print = function(text, ...)
|
||||
printed[#printed + 1] = tostring(text)
|
||||
return realPrint(text, ...)
|
||||
end
|
||||
|
||||
window(1280, 900, "OS X")
|
||||
S.tab, S.monSection, S.inspectorScroll = "party", "main", 0
|
||||
local list = controls()
|
||||
local seen = {}
|
||||
for _, text in ipairs(printed) do seen[text] = true end
|
||||
love.graphics.print = realPrint
|
||||
check(seen.Saved and seen["Save editor"], "title bar shows the save state as plain text")
|
||||
for text in pairs(seen) do
|
||||
check(not text:find("●", 1, true), "no status dot in the title bar")
|
||||
end
|
||||
check(find(list, "tab-monSection-moves") ~= nil, "wide inspectors use section tabs")
|
||||
check(find(list, "change-species") ~= nil, "species has a Change button")
|
||||
|
||||
local box = assert(find(list, "value-level"), "level value box")
|
||||
local undo = #S.undoStack
|
||||
App.mousepressed(box.x + box.w / 2, box.y + box.h / 2, 1)
|
||||
App.draw()
|
||||
check(Kit.focus == "value-edit-level", "tapping the value box edits it inline")
|
||||
for _ = 1, 4 do App.keypressed("backspace") end
|
||||
App.textinput("4x2")
|
||||
App.draw()
|
||||
App.keypressed("return")
|
||||
App.draw()
|
||||
check(mon.level == 42, "typed level commits on Enter (got " .. tostring(mon.level) .. ")")
|
||||
check(#S.undoStack == undo + 1, "one commit is one undo")
|
||||
|
||||
list = controls()
|
||||
box = assert(find(list, "value-level"), "level value box after commit")
|
||||
App.mousepressed(box.x + box.w / 2, box.y + box.h / 2, 1)
|
||||
App.draw()
|
||||
App.textinput("9")
|
||||
App.draw()
|
||||
App.keypressed("escape")
|
||||
App.draw()
|
||||
check(mon.level == 42 and Kit.focus == nil, "Escape discards an inline edit")
|
||||
|
||||
local slider = assert(find(list, "slider-level"), "level slider")
|
||||
undo = #S.undoStack
|
||||
local sy = slider.y + slider.h / 2
|
||||
App.touchpressed("slide", slider.x + slider.w * 0.2, sy)
|
||||
App.draw()
|
||||
App.touchmoved("slide", slider.x + slider.w * 0.6, sy)
|
||||
App.draw()
|
||||
App.touchmoved("slide", slider.x + slider.w * 0.95, sy)
|
||||
App.draw()
|
||||
check(mon.level == 42, "dragging the slider only previews")
|
||||
App.touchreleased("slide", slider.x + slider.w * 0.95, sy)
|
||||
App.draw()
|
||||
check(mon.level > 90, "releasing the slider commits (got " .. tostring(mon.level) .. ")")
|
||||
check(#S.undoStack == undo + 1, "a whole drag is one undo")
|
||||
|
||||
local first = next(S.data.moves)
|
||||
Ops.setMove(S, mon, 1, S.data.moves[first].id or first)
|
||||
Ops.setPpUps(S, mon, 1, 2)
|
||||
S.monSection, S.inspectorScroll = "moves", 0
|
||||
list = controls()
|
||||
local zero = assert(find(list, "ppup-1-0"), "PP Ups segment 0")
|
||||
App.mousepressed(zero.x + zero.w / 2, zero.y + zero.h / 2, 1)
|
||||
App.draw()
|
||||
check(require("MonOps").getPpUps(mon, 1) == 0, "PP Ups segment sets the count")
|
||||
check(find(list, "move-1") ~= nil and find(list, "slider-pp-1") ~= nil, "move card has Change and a PP slider")
|
||||
|
||||
window(390, 844, "Android")
|
||||
S.monSection, S.inspectorScroll = "main", 0
|
||||
list = controls()
|
||||
local tiles = {}
|
||||
for _, c in ipairs(list) do
|
||||
if c.class == "row" then tiles[#tiles + 1] = c end
|
||||
end
|
||||
check(#tiles >= 2, "phone shows the party grid above the inspector")
|
||||
local heal
|
||||
for _, c in ipairs(list) do
|
||||
if c.label == "Full heal" then heal = c end
|
||||
end
|
||||
check(heal and heal.y > tiles[#tiles].y, "phone shows the inspector below the party grid")
|
||||
App.touchpressed("tile", tiles[2].x + tiles[2].w / 2, tiles[2].y + tiles[2].h / 2)
|
||||
App.touchreleased("tile", tiles[2].x + tiles[2].w / 2, tiles[2].y + tiles[2].h / 2)
|
||||
App.draw()
|
||||
check(S.editingMon == S.save.party[2], "tapping a party tile selects it")
|
||||
|
||||
History.undo(S)
|
||||
os.remove(path)
|
||||
print(("save editor inspector layout: %d checks passed"):format(count))
|
||||
@@ -36,7 +36,7 @@ Paint.draw = function(owner, x, y, w, h, label, opts, hot, focused)
|
||||
if opts.icon == "save" then
|
||||
saveRect = { x = x, y = y, w = w, h = h }
|
||||
end
|
||||
if label == "Change species" then
|
||||
if opts.id == "change-species" then
|
||||
speciesY = y
|
||||
end
|
||||
local theme = require("Theme")
|
||||
@@ -364,8 +364,18 @@ for _, size in ipairs({
|
||||
case[1], "main", "bag", "storage", "view"
|
||||
S.mobileInspector, S.inspectorScroll, S.pageScroll = true, 0, {}
|
||||
navigationRects = {}
|
||||
frame("dropdown page " .. case[2], W, H)
|
||||
local pageControls = frame("dropdown page " .. case[2], W, H)
|
||||
local rect = navigationRects[case[2]]
|
||||
if case[2] == "monSection" and S._navOffset and (not rect or rect.y + rect.h > H) then
|
||||
S.inspectorScroll = S._navOffset
|
||||
navigationRects = {}
|
||||
pageControls = frame("dropdown page scrolled " .. case[2], W, H)
|
||||
rect = navigationRects[case[2]]
|
||||
end
|
||||
local sectionTabs = 0
|
||||
for _, c in ipairs(pageControls) do
|
||||
if c.id and c.id:match("^tab%-monSection%-") then sectionTabs = sectionTabs + 1 end
|
||||
end
|
||||
if rect then
|
||||
local previous, selected, anchor = S[case[2]], S.editingMon, speciesY
|
||||
Kit.focus = "property-level"
|
||||
@@ -382,7 +392,8 @@ for _, size in ipairs({
|
||||
check(not S.navPopup and S[case[2]] == previous, "Escape cancels " .. case[2])
|
||||
check(S.editingMon == selected, "Escape preserves the Pokemon selection")
|
||||
else
|
||||
check(case[2] == "mapSection" and W > H, "only the simultaneous map columns omit a chooser")
|
||||
check(case[2] == "mapSection" and W > H or case[2] == "monSection" and sectionTabs == case[3],
|
||||
"only the simultaneous map columns and full section tabs omit a chooser")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -485,7 +496,13 @@ local realSave, saved = App.save, 0
|
||||
App.save = function()
|
||||
saved = saved + 1
|
||||
end
|
||||
App.mousepressed(saveRect.x + saveRect.w / 2, saveRect.y + saveRect.h / 2, 1)
|
||||
local tapX = saveRect.x + saveRect.w / 2
|
||||
local popupRect = S.navPopup and S.navPopup.rect
|
||||
if popupRect and tapX <= popupRect.x + popupRect.w then
|
||||
tapX = popupRect.x + popupRect.w + 2
|
||||
end
|
||||
check(tapX < saveRect.x + saveRect.w, "Save has a target outside the popup")
|
||||
App.mousepressed(tapX, saveRect.y + saveRect.h / 2, 1)
|
||||
App.draw()
|
||||
check(saved == 0 and S.navPopup == nil, "outside tap dismisses without activating Save underneath")
|
||||
App.save = realSave
|
||||
@@ -537,6 +554,8 @@ check(
|
||||
-- Option taps change the intended nested screen. A drag inside a short
|
||||
-- popup scrolls the options without selecting the row under its release.
|
||||
App.draw()
|
||||
S.inspectorScroll = S._navOffset or 0
|
||||
App.draw()
|
||||
rect = navigationRects.monSection
|
||||
App.mousepressed(rect.x + rect.w / 2, rect.y + rect.h / 2, 1)
|
||||
App.draw()
|
||||
|
||||
@@ -10,7 +10,8 @@ NAMES = [
|
||||
"save", "rotate-ccw", "ellipsis", "copy", "heart", "package", "backpack", "list-filter",
|
||||
"arrow-up", "arrow-down", "arrow-left", "arrow-right", "arrow-up-down", "eye", "award",
|
||||
"book-open", "shield-check", "map-pin", "grid-2x2", "sliders-horizontal", "expand",
|
||||
"chevrons-up", "users", "user-round", "wallet", "flag",
|
||||
"chevrons-up", "users", "user-round", "wallet", "flag", "sparkles", "shuffle",
|
||||
"folder-open",
|
||||
]
|
||||
|
||||
|
||||
|
||||
+137
-159
@@ -633,164 +633,142 @@ function App.joystickhat(joystick, hat, direction)
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------ chrome
|
||||
-- Compact action row, with file identity above it when height allows.
|
||||
local function drawTitleBar(x, y, w, h)
|
||||
local pad, gap, row = 12 * Kit.scale, 8 * Kit.scale, Kit.controlH()
|
||||
local inner = w - 2 * pad
|
||||
if not S.compactChrome and not Kit.desktop then
|
||||
Kit.text(
|
||||
"tab",
|
||||
"SAVE EDITOR" .. (S.version and (" / " .. S.version:upper()) or ""),
|
||||
x + pad,
|
||||
y + 8 * Kit.scale,
|
||||
PAL.heading
|
||||
)
|
||||
Kit.text(
|
||||
"tiny",
|
||||
Kit.ellipsize("tiny", (S.dirty and "UNSAVED " or "SAVED ") .. (S.path or "New save"), inner),
|
||||
x + pad,
|
||||
y + Kit.textHeight("tab") + 12 * Kit.scale,
|
||||
S.dirty and PAL.yellow or PAL.caption
|
||||
)
|
||||
local function versionLabel()
|
||||
local ok, info = pcall(require("src.core.GameVersion").info, S.version)
|
||||
if ok and type(info) == "table" and info.label then return info.label end
|
||||
return S.version and (S.version:sub(1, 1):upper() .. S.version:sub(2)) or nil
|
||||
end
|
||||
|
||||
local function shortPath(path)
|
||||
if not path then return "New save" end
|
||||
local parts = {}
|
||||
for part in tostring(path):gmatch("[^/\\]+") do parts[#parts + 1] = part end
|
||||
if #parts <= 3 then return tostring(path) end
|
||||
return ".../" .. table.concat(parts, "/", #parts - 2)
|
||||
end
|
||||
|
||||
local function drawIdentity(x, y, w, h)
|
||||
local s = Kit.scale
|
||||
local titleH, pathH = Kit.textHeight("button"), Kit.textHeight("tiny")
|
||||
local ty = y + (h - titleH - pathH - 4 * s) / 2
|
||||
local lead, version, sep = "Save editor", versionLabel(), " / "
|
||||
local leadW, sepW = Kit.textWidth("button", lead), Kit.textWidth("button", sep)
|
||||
Kit.textBold("button", Kit.ellipsize("button", lead, w), x, ty, PAL.heading)
|
||||
if version and leadW + sepW + 40 * s < w then
|
||||
Kit.text("button", sep, x + leadW + 1, ty, PAL.muted)
|
||||
Kit.textBold("button", Kit.ellipsize("button", version, w - leadW - sepW - 1), x + leadW + sepW + 1, ty, PAL.heading)
|
||||
end
|
||||
local narrow = w < 600 * Kit.scale and not S.compactChrome
|
||||
local cols = narrow and 4 or 6
|
||||
local bw = (inner - (cols - 1) * gap) / cols
|
||||
local by = S.compactChrome and (y + 8 * Kit.scale)
|
||||
or (y + Kit.textHeight("tab") + Kit.textHeight("tiny") + 20 * Kit.scale)
|
||||
local saveInk = S.allowSave and S.dirty and PAL.green or PAL.muted
|
||||
local actions = {
|
||||
{
|
||||
S.allowSave and (S.dirty and "Save" or "Saved") or "Save locked",
|
||||
"ghost",
|
||||
function()
|
||||
App.save()
|
||||
end,
|
||||
S.dirty or not S.allowSave,
|
||||
{
|
||||
face = "invert",
|
||||
ink = saveInk,
|
||||
stroke = saveInk,
|
||||
icon = S.allowSave and "save" or "lock",
|
||||
},
|
||||
},
|
||||
{
|
||||
"Undo",
|
||||
"ghost",
|
||||
function()
|
||||
require("History").undo(S)
|
||||
end,
|
||||
S.undoStack and #S.undoStack > 0,
|
||||
},
|
||||
{
|
||||
"Redo",
|
||||
"ghost",
|
||||
function()
|
||||
require("History").redo(S)
|
||||
end,
|
||||
S.redoStack and #S.redoStack > 0,
|
||||
},
|
||||
{
|
||||
"Reload",
|
||||
"ghost",
|
||||
function()
|
||||
App.reload()
|
||||
end,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Open",
|
||||
"accent",
|
||||
function()
|
||||
App.chooseAndOpen()
|
||||
end,
|
||||
true,
|
||||
},
|
||||
{
|
||||
S._quitArmed and "Discard?" or "Close",
|
||||
S._quitArmed and "danger" or "ghost",
|
||||
function()
|
||||
App.close()
|
||||
end,
|
||||
true,
|
||||
},
|
||||
Kit.text("tiny", Kit.ellipsize("tiny", shortPath(S.path), w), x, ty + titleH + 4 * s, PAL.muted)
|
||||
end
|
||||
|
||||
local function saveStatus()
|
||||
if S.dirty then return "Unsaved changes", PAL.yellow end
|
||||
return "Saved", PAL.muted
|
||||
end
|
||||
|
||||
local function saveButton(x, y, h, measure)
|
||||
local label = S.allowSave and "Save" or "Save locked"
|
||||
local ink = S.allowSave and S.dirty and PAL.green or PAL.muted
|
||||
local opts = {
|
||||
font = "button",
|
||||
face = "invert",
|
||||
ink = ink,
|
||||
stroke = ink,
|
||||
icon = S.allowSave and "save" or "lock",
|
||||
enabled = S.dirty or not S.allowSave,
|
||||
}
|
||||
local function actionOptions(action)
|
||||
local opts = action[5] or {}
|
||||
opts.kind, opts.enabled, opts.font = action[2], action[4], "small"
|
||||
return opts
|
||||
local w = Kit.buttonWidth(label, opts, h)
|
||||
if not measure and Kit.button(x - w, y, w, h, label, opts) then
|
||||
App.save()
|
||||
end
|
||||
if Kit.desktop then
|
||||
local widths, total = {}, 5 * gap
|
||||
for i, action in ipairs(actions) do
|
||||
widths[i] = Kit.buttonWidth(action[1], actionOptions(action), row)
|
||||
if i == 1 then
|
||||
widths[i] = math.max(widths[i], Kit.buttonWidth("Save locked", { font = "small", icon = "lock" }, row))
|
||||
elseif i == 6 then
|
||||
widths[i] = math.max(widths[i], Kit.buttonWidth("Discard?", { font = "small" }, row))
|
||||
end
|
||||
total = total + widths[i]
|
||||
end
|
||||
local bx, by = x + w - pad - total, y + 8 * Kit.scale
|
||||
local identityW = bx - gap - (x + pad)
|
||||
local labelH, pathH = Kit.textHeight("tab"), Kit.textHeight("tiny")
|
||||
local ty = by + (row - labelH - pathH - 4 * Kit.scale) / 2
|
||||
Kit.text("tab", Kit.ellipsize("tab", "SAVE EDITOR" .. (S.version and (" / " .. S.version:upper()) or ""), identityW), x + pad, ty, PAL.heading)
|
||||
Kit.text("tiny", Kit.ellipsize("tiny", (S.dirty and "UNSAVED " or "SAVED ") .. (S.path or "New save"), identityW), x + pad, ty + labelH + 4 * Kit.scale, S.dirty and PAL.yellow or PAL.caption)
|
||||
for i, action in ipairs(actions) do
|
||||
if Kit.button(bx, by, widths[i], row, action[1], actionOptions(action)) then action[3]() end
|
||||
bx = bx + widths[i] + gap
|
||||
end
|
||||
return
|
||||
return w
|
||||
end
|
||||
|
||||
local function drawActionRow(x, y, w, h, withIdentity)
|
||||
local s, row = Kit.scale, Kit.controlH()
|
||||
local gap, pad = 8 * s, (Kit.desktop and 20 or 12) * s
|
||||
local by = y + (h - row) / 2
|
||||
local rx = x + w - pad
|
||||
if S._quitArmed then
|
||||
local cw = Kit.buttonWidth("Discard?", { font = "button" }, row)
|
||||
rx = rx - cw
|
||||
if Kit.button(rx, by, cw, row, "Discard?", { kind = "danger", font = "button" }) then App.close() end
|
||||
else
|
||||
rx = rx - row
|
||||
if Kit.iconButton(rx, by, row, row, "x", "Close", { kind = "ghost" }) then App.close() end
|
||||
end
|
||||
if narrow then
|
||||
local more = {
|
||||
S.chromeMenu and "Less" or "More",
|
||||
"ghost",
|
||||
function()
|
||||
S.chromeMenu = not S.chromeMenu
|
||||
Kit.blur()
|
||||
end,
|
||||
true,
|
||||
rx = rx - 2 * gap
|
||||
rx = rx - saveButton(rx, by, row)
|
||||
rx = rx - gap - row
|
||||
if Kit.iconButton(rx, by, row, row, "folder-open", "Open", { kind = "ghost" }) then App.chooseAndOpen() end
|
||||
rx = rx - gap - row
|
||||
if Kit.iconButton(rx, by, row, row, "rotate-ccw", "Reload", { kind = "ghost" }) then App.reload() end
|
||||
rx = rx - gap - row
|
||||
if Kit.iconButton(rx, by, row, row, "redo-2", "Redo", { kind = "ghost", enabled = S.redoStack and #S.redoStack > 0 }) then
|
||||
require("History").redo(S)
|
||||
end
|
||||
rx = rx - 4 * s - row
|
||||
if Kit.iconButton(rx, by, row, row, "undo-2", "Undo", { kind = "ghost", enabled = S.undoStack and #S.undoStack > 0 }) then
|
||||
require("History").undo(S)
|
||||
end
|
||||
rx = rx - 18 * s
|
||||
local status, color = saveStatus()
|
||||
local statusW = Kit.textWidth("small", status)
|
||||
local left = x + pad + (withIdentity and 150 * s or 0)
|
||||
if rx - statusW >= left then
|
||||
Kit.textRight("small", status, rx, by + (row - Kit.textHeight("small")) / 2, color)
|
||||
rx = rx - statusW - 24 * s
|
||||
end
|
||||
if withIdentity then
|
||||
drawIdentity(x + pad, y, rx - x - pad, h)
|
||||
end
|
||||
end
|
||||
|
||||
local function chromeMenuCols(w)
|
||||
local s, gap = Kit.scale, 8 * Kit.scale
|
||||
local widest = 0
|
||||
for _, label in ipairs({ "Redo", "Reload", "Open", "Close", "Discard?" }) do
|
||||
widest = math.max(widest, Kit.buttonWidth(label, { font = "small" }, Kit.controlH()))
|
||||
end
|
||||
return (w - 24 * s) >= 4 * widest + 3 * gap and 4 or 2
|
||||
end
|
||||
|
||||
local function drawTitleBar(x, y, w, h)
|
||||
if Kit.desktop or S.compactChrome or w >= 760 * Kit.scale then
|
||||
return drawActionRow(x, y, w, h, not S.compactChrome)
|
||||
end
|
||||
local s = Kit.scale
|
||||
local pad, gap, row = 12 * s, 8 * s, Kit.controlH()
|
||||
local inner = w - 2 * pad
|
||||
local idH = Kit.textHeight("button") + Kit.textHeight("tiny") + 4 * s
|
||||
drawIdentity(x + pad, y + 8 * s, inner, idH)
|
||||
local by = y + idH + 20 * s
|
||||
local rx = x + w - pad - row
|
||||
if Kit.iconButton(rx, by, row, row, S.chromeMenu and "x" or "ellipsis", S.chromeMenu and "Less" or "More", { kind = "ghost" }) then
|
||||
S.chromeMenu = not S.chromeMenu
|
||||
Kit.blur()
|
||||
end
|
||||
rx = rx - gap
|
||||
rx = rx - saveButton(rx, by, row) - gap - row
|
||||
if Kit.iconButton(rx, by, row, row, "undo-2", "Undo", { kind = "ghost", enabled = S.undoStack and #S.undoStack > 0 }) then
|
||||
require("History").undo(S)
|
||||
end
|
||||
local status, color = saveStatus()
|
||||
Kit.text("small", Kit.ellipsize("small", status, rx - gap - x - pad), x + pad, by + (row - Kit.textHeight("small")) / 2, color)
|
||||
if S.chromeMenu then
|
||||
local menu = {
|
||||
{ "Redo", "redo-2", function() require("History").redo(S) end, S.redoStack and #S.redoStack > 0 },
|
||||
{ "Reload", "rotate-ccw", function() App.reload() end, true },
|
||||
{ "Open", "folder-open", function() App.chooseAndOpen() end, true },
|
||||
{ S._quitArmed and "Discard?" or "Close", S._quitArmed and "trash" or "x", function() App.close() end, true },
|
||||
}
|
||||
local primary = { actions[1], actions[2], actions[3], more }
|
||||
for i, a in ipairs(primary) do
|
||||
if Kit.button(x + pad + (i - 1) * (bw + gap), by, bw, row, a[1], actionOptions(a)) then
|
||||
a[3]()
|
||||
end
|
||||
end
|
||||
if S.chromeMenu then
|
||||
local menuW = (inner - 2 * gap) / 3
|
||||
for i = 4, 6 do
|
||||
local a = actions[i]
|
||||
if
|
||||
Kit.button(
|
||||
x + pad + (i - 4) * (menuW + gap),
|
||||
by + row + gap,
|
||||
menuW,
|
||||
row,
|
||||
a[1],
|
||||
actionOptions(a)
|
||||
)
|
||||
then
|
||||
a[3]()
|
||||
end
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
for i, a in ipairs(actions) do
|
||||
if
|
||||
Kit.button(
|
||||
x + pad + (i - 1) % cols * (bw + gap),
|
||||
by + math.floor((i - 1) / cols) * (row + gap),
|
||||
bw,
|
||||
row,
|
||||
a[1],
|
||||
actionOptions(a)
|
||||
)
|
||||
then
|
||||
a[3]()
|
||||
local cols = chromeMenuCols(w)
|
||||
local bw = (inner - (cols - 1) * gap) / cols
|
||||
for i, m in ipairs(menu) do
|
||||
local mx = x + pad + (i - 1) % cols * (bw + gap)
|
||||
local my = by + row + gap + math.floor((i - 1) / cols) * (row + gap)
|
||||
local opts = { font = "small", icon = m[2], enabled = m[4], kind = m[1] == "Discard?" and "danger" or "ghost" }
|
||||
if Kit.button(mx, my, bw, row, m[1], opts) then m[3]() end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -938,13 +916,13 @@ function App.draw()
|
||||
-- identity painting through each other (#715). The taller bar simply
|
||||
-- costs the content column height, which scrolls.
|
||||
S.compactChrome = sh < 500 * s and sw > sh
|
||||
local titleTwoRow = sw < 600 * s and not S.compactChrome and S.chromeMenu
|
||||
local titleH = Kit.textHeight("tab")
|
||||
+ Kit.textHeight("tiny")
|
||||
+ 28 * s
|
||||
+ (titleTwoRow and 2 or 1) * (Kit.controlH() + 8 * s)
|
||||
if S.compactChrome or Kit.desktop then
|
||||
local titleH
|
||||
if S.compactChrome or Kit.desktop or sw >= 760 * s then
|
||||
titleH = Kit.controlH() + 16 * s
|
||||
else
|
||||
local menuRows = S.chromeMenu and (chromeMenuCols(sw) == 4 and 1 or 2) or 0
|
||||
titleH = Kit.textHeight("button") + Kit.textHeight("tiny") + 4 * s + 28 * s
|
||||
+ (1 + menuRows) * (Kit.controlH() + 8 * s)
|
||||
end
|
||||
local tabH = Kit.controlH() + 6 * s
|
||||
local statusH = (Kit.desktop and 28 or 38) * s
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
local Theme = require("Theme")
|
||||
local Touch = require("TouchEditor")
|
||||
local PAL = Theme.PAL
|
||||
local Form = {}
|
||||
|
||||
local ERROR_FILL = { 58, 31, 37 }
|
||||
|
||||
function Form.thousands(n)
|
||||
n = tonumber(n)
|
||||
if not n then return "?" end
|
||||
local str = tostring(math.floor(math.abs(n)))
|
||||
str = str:reverse():gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")
|
||||
return (n < 0 and "-" or "") .. str
|
||||
end
|
||||
|
||||
function Form.range(l)
|
||||
return Form.thousands(l.lo) .. " – " .. Form.thousands(l.hi)
|
||||
end
|
||||
|
||||
function Form.labelH(Kit)
|
||||
return Kit.textHeight("small") + 8 * Kit.scale
|
||||
end
|
||||
|
||||
function Form.label(Kit, text, x, y, w, right, color, rightColor)
|
||||
local rw = right and Kit.textWidth("small", right) + 10 * Kit.scale or 0
|
||||
Kit.text("small", Kit.ellipsize("small", text, math.max(0, w - rw)), x, y, color or PAL.muted)
|
||||
if right then
|
||||
Kit.textRight("small", right, x + w, y, rightColor or PAL.muted)
|
||||
end
|
||||
return Form.labelH(Kit)
|
||||
end
|
||||
|
||||
function Form.wrapH(Kit, font, text, w)
|
||||
local f = Kit.fonts[font] or Kit.fonts.small
|
||||
if not f or w <= 0 then return 0 end
|
||||
if not f.getWrap then return f:getHeight() end
|
||||
local _, lines = f:getWrap(tostring(text), w)
|
||||
return #lines * f:getHeight()
|
||||
end
|
||||
|
||||
function Form.issueH(Kit, message, w)
|
||||
if not message then return 0 end
|
||||
local size, gap = 14 * Kit.scale, 6 * Kit.scale
|
||||
return math.max(size, Form.wrapH(Kit, "tiny", message, w - size - gap)) + gap
|
||||
end
|
||||
|
||||
function Form.box(Kit, x, y, w, h, text, invalid, color)
|
||||
local r = Theme.radius()
|
||||
Theme.fillRounded(x, y, w, h, invalid and ERROR_FILL or PAL.rowBg, 0.7, r)
|
||||
Theme.stroke(x, y, w, h, r, invalid and PAL.red or PAL.cardBorder, invalid and 0.95 or 0.3,
|
||||
invalid and 2 * Kit.scale or 1)
|
||||
local pad = 14 * Kit.scale
|
||||
Kit.textBold("button", Kit.ellipsize("button", tostring(text), w - 2 * pad), x + pad,
|
||||
y + (h - Kit.textHeight("button")) / 2, color or (invalid and PAL.red or PAL.heading))
|
||||
end
|
||||
|
||||
function Form.select(S, Kit, id, title, value, options, x, y, w, apply, issue, help)
|
||||
local shown
|
||||
for _, o in ipairs(options) do
|
||||
if o[1] == value then shown = o[2] end
|
||||
end
|
||||
shown = shown or ((issue and "Invalid saved value " or "Unknown saved value ") .. tostring(value))
|
||||
local opts = {
|
||||
face = "invert",
|
||||
font = "button",
|
||||
align = "left",
|
||||
trailingIcon = "chevron-down",
|
||||
id = "choice-" .. id,
|
||||
invalid = issue ~= nil,
|
||||
}
|
||||
local h = Kit.buttonHeight(shown, w, opts)
|
||||
if Kit.button(x, y, w, h, shown, opts) then
|
||||
Touch.open(S, Kit, {
|
||||
mode = "choice", id = id, title = title, value = value, options = options,
|
||||
apply = apply, help = help, issue = issue,
|
||||
})
|
||||
end
|
||||
return h
|
||||
end
|
||||
|
||||
local function digits(v)
|
||||
return (tostring(v or ""):gsub("[^0-9]", ""):sub(1, 8))
|
||||
end
|
||||
|
||||
function Form.number(S, Kit, id, value, x, y, w, h, apply, issue, preview)
|
||||
local key = "value-edit-" .. id
|
||||
local function commit(v)
|
||||
local n = tonumber(v)
|
||||
if not n then return require("Ops").say(S, "Enter a whole number") end
|
||||
if n ~= value then apply(n) end
|
||||
end
|
||||
local edit = S.valueEdit
|
||||
if edit and edit.id == id and Kit.focus ~= key then
|
||||
S.valueEdit = nil
|
||||
if edit.draft ~= tostring(value) then commit(edit.draft) end
|
||||
edit = nil
|
||||
end
|
||||
if edit and edit.id == id then
|
||||
local v = Kit.textfield(key, x, y, w, h, edit.draft, "", {
|
||||
sanitize = digits,
|
||||
invalid = issue ~= nil,
|
||||
onSubmit = function(text)
|
||||
S.valueEdit = nil
|
||||
commit(text)
|
||||
end,
|
||||
onCancel = function()
|
||||
S.valueEdit = nil
|
||||
end,
|
||||
})
|
||||
if S.valueEdit == edit then edit.draft = v end
|
||||
return
|
||||
end
|
||||
local opts = { face = "invert", font = "button", align = "left", id = "value-" .. id, invalid = issue ~= nil }
|
||||
if Kit.button(x, y, w, h, tostring(preview or value), opts) then
|
||||
S.valueEdit = { id = id, draft = tostring(value) }
|
||||
Kit.focus = key
|
||||
Kit._fieldHit, Kit._focusDrawn = true, true
|
||||
end
|
||||
end
|
||||
|
||||
function Form.slider(S, Kit, id, value, l, x, y, w, h, apply, issue)
|
||||
local s = Kit.scale
|
||||
local knob = 9 * s
|
||||
local tx, tw = x + knob, math.max(1, w - 2 * knob)
|
||||
local function at(mx)
|
||||
return math.floor(l.lo + Theme.clamp((mx - tx) / tw, 0, 1) * (l.hi - l.lo) + 0.5)
|
||||
end
|
||||
local shown, committed = tonumber(value), false
|
||||
local d = S._slider
|
||||
if d and d.id == id then
|
||||
if Kit.mouseDown then
|
||||
if not d.live and not d.scroll then
|
||||
local dx, dy = math.abs(Kit.mouseX - d.x), math.abs(Kit.mouseY - d.y)
|
||||
if dx > 6 * s and dx >= dy then
|
||||
d.live = true
|
||||
elseif dy > 6 * s then
|
||||
d.scroll = true
|
||||
end
|
||||
end
|
||||
if d.live then
|
||||
d.value = at(Kit.mouseX)
|
||||
shown = d.value
|
||||
end
|
||||
else
|
||||
S._slider = nil
|
||||
if d.live then
|
||||
committed = true
|
||||
if d.value ~= value then apply(d.value) end
|
||||
shown = d.value
|
||||
end
|
||||
end
|
||||
elseif not d and Kit.mouseDown and not Kit.blockClicks and Kit.hit(x, y, w, h) then
|
||||
S._slider = { id = id, x = Kit.mouseX, y = Kit.mouseY }
|
||||
end
|
||||
if Kit.button(x, y, w, h, "", { face = "bare", id = "slider-" .. id }) and not committed then
|
||||
local v = at(Kit.mouseX)
|
||||
if v ~= value then apply(v) end
|
||||
shown = v
|
||||
end
|
||||
local ratio = shown and Theme.clamp((shown - l.lo) / math.max(1, l.hi - l.lo), 0, 1) or 0
|
||||
local color = issue and PAL.red or PAL.blue
|
||||
local trackH = 6 * s
|
||||
local cy = y + h / 2
|
||||
Theme.fillRounded(tx, cy - trackH / 2, tw, trackH, PAL.cardBorder, 0.3, trackH / 2)
|
||||
if ratio > 0 then
|
||||
Theme.fillRounded(tx, cy - trackH / 2, tw * ratio, trackH, color, 0.95, trackH / 2)
|
||||
end
|
||||
Theme.col(color)
|
||||
love.graphics.circle("fill", tx + tw * ratio, cy, knob, 32)
|
||||
return shown ~= tonumber(value) and shown or nil
|
||||
end
|
||||
|
||||
function Form.numberRow(S, Kit, id, title, value, l, x, y, w, apply, issue)
|
||||
local s, row = Kit.scale, Kit.controlH()
|
||||
if not require("Legality").integer(value, l.lo, l.hi) then
|
||||
issue = issue or ("Saved value must be a whole number from " .. l.lo .. " to " .. l.hi)
|
||||
end
|
||||
local right = Form.range(l)
|
||||
if l.remaining ~= nil then right = right .. " · " .. l.remaining .. " free" end
|
||||
local cy = y + Form.label(Kit, title, x, y, w, right, issue and PAL.red or PAL.muted)
|
||||
local boxW = math.floor(math.min(math.max(110 * s, Kit.textWidth("button", tostring(l.hi)) + 40 * s), w * 0.42))
|
||||
local gap = 16 * s
|
||||
local preview = Form.slider(S, Kit, id, value, l, x + boxW + gap, cy, w - boxW - gap, row, apply, issue)
|
||||
Form.number(S, Kit, id, value, x, cy, boxW, row, apply, issue, preview)
|
||||
cy = cy + row
|
||||
if issue then
|
||||
cy = cy + 6 * s + Touch.issue(Kit, issue, x, cy + 6 * s, w)
|
||||
end
|
||||
return cy - y
|
||||
end
|
||||
|
||||
function Form.segmented(Kit, id, options, value, x, y, w, h, apply, issue)
|
||||
local s = Kit.scale
|
||||
local inset, r = 4 * s, Theme.radius()
|
||||
Theme.fillRounded(x, y, w, h, PAL.rowBg, 0.7, r)
|
||||
Theme.stroke(x, y, w, h, r, issue and PAL.red or PAL.cardBorder, issue and 0.95 or 0.3, issue and 2 * s or 1)
|
||||
local segW = w / #options
|
||||
for i, o in ipairs(options) do
|
||||
local sx = x + (i - 1) * segW
|
||||
local active = o[1] == value
|
||||
if active then
|
||||
Theme.fillRounded(sx + inset, y + inset, segW - 2 * inset, h - 2 * inset, PAL.ink, 1, r - inset)
|
||||
elseif not Kit.blockClicks and Kit.hover(sx, y, segW, h) then
|
||||
Theme.fillRounded(sx + inset, y + inset, segW - 2 * inset, h - 2 * inset, PAL.raised, 1, r - inset)
|
||||
end
|
||||
local opts = { face = "bare", font = "button", ink = active and PAL.inverse or PAL.text, id = id .. "-" .. tostring(o[1]) }
|
||||
if Kit.button(sx, y, segW, h, o[2], opts) and not active then
|
||||
apply(o[1])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function flow(Kit, caption, actions, x, y, w, draw)
|
||||
local s, row = Kit.scale, Kit.controlH()
|
||||
local gap = 10 * s
|
||||
local cx, cy = x, y
|
||||
if caption then
|
||||
if draw then Kit.caption(x, y + (row - Kit.textHeight("caption")) / 2, caption) end
|
||||
cx = x + Kit.captionWidth(caption) + 18 * s
|
||||
end
|
||||
for _, a in ipairs(actions) do
|
||||
local opts = {}
|
||||
for k, v in pairs(a[3] or {}) do opts[k] = v end
|
||||
opts.font = opts.font or "button"
|
||||
local bw = math.min(w, Kit.buttonWidth(a[1], opts, row))
|
||||
if cx > x and cx + bw > x + w then
|
||||
cx, cy = x, cy + row + gap
|
||||
end
|
||||
if draw and Kit.button(cx, cy, bw, row, a[1], opts) then a[2]() end
|
||||
cx = cx + bw + gap
|
||||
end
|
||||
return cy + row - y
|
||||
end
|
||||
|
||||
function Form.flowH(Kit, caption, actions, w)
|
||||
return flow(Kit, caption, actions, 0, 0, w, false)
|
||||
end
|
||||
|
||||
function Form.flow(Kit, caption, actions, x, y, w)
|
||||
return flow(Kit, caption, actions, x, y, w, true)
|
||||
end
|
||||
|
||||
function Form.tabsFit(Kit, options, w, h)
|
||||
local s = Kit.scale
|
||||
local widths, total = {}, 0
|
||||
for i, o in ipairs(options) do
|
||||
local tw = Kit.textWidth("button", o[2]) + 28 * s + (Theme.BOLD_OFFSET or 1)
|
||||
if (o.errors or 0) > 0 then tw = tw + math.floor(h * 0.42) + math.floor(7 * s) end
|
||||
widths[i] = math.max(Kit.tapMin(), math.ceil(tw))
|
||||
total = total + widths[i]
|
||||
end
|
||||
return total <= w, widths
|
||||
end
|
||||
|
||||
function Form.tabs(S, Kit, key, options, x, y, w, h, pick)
|
||||
local fits, widths = Form.tabsFit(Kit, options, w, h)
|
||||
if not fits then return false end
|
||||
local s = Kit.scale
|
||||
local tx = x
|
||||
for i, o in ipairs(options) do
|
||||
local active = S[key] == o[1]
|
||||
local err = (o.errors or 0) > 0
|
||||
local opts = {
|
||||
face = "bare",
|
||||
font = "button",
|
||||
bold = active,
|
||||
ink = err and PAL.red or (active and PAL.heading or PAL.muted),
|
||||
icon = err and "triangle-alert" or nil,
|
||||
id = "tab-" .. key .. "-" .. tostring(o[1]),
|
||||
}
|
||||
if Kit.button(tx, y, widths[i], h, o[2], opts) and not active then
|
||||
pick(o[1], i)
|
||||
end
|
||||
if active then
|
||||
Theme.fillRounded(tx + 4 * s, y + h - 3 * s, widths[i] - 8 * s, 3 * s, err and PAL.red or PAL.heading, 1, 1.5 * s)
|
||||
end
|
||||
tx = tx + widths[i]
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return Form
|
||||
+929
-499
File diff suppressed because it is too large
Load Diff
@@ -272,7 +272,7 @@ function Kit.trackedControls()
|
||||
return tracked, trackedN
|
||||
end
|
||||
|
||||
local function audit(class, x, y, w, h, label)
|
||||
local function audit(class, x, y, w, h, label, id)
|
||||
if Kit.blockClicks then
|
||||
return
|
||||
end
|
||||
@@ -284,6 +284,9 @@ local function audit(class, x, y, w, h, label)
|
||||
return
|
||||
end
|
||||
local c = Kit._clipRect
|
||||
if c and (x >= c.x + c.w or y >= c.y + c.h or x + w <= c.x or y + h <= c.y) then
|
||||
return
|
||||
end
|
||||
a[#a + 1] = {
|
||||
class = class,
|
||||
x = x,
|
||||
@@ -291,6 +294,7 @@ local function audit(class, x, y, w, h, label)
|
||||
w = w,
|
||||
h = h,
|
||||
label = tostring(label or ""),
|
||||
id = id,
|
||||
clip = c and { x = c.x, y = c.y, w = c.w, h = c.h } or nil,
|
||||
}
|
||||
end
|
||||
@@ -562,7 +566,7 @@ function Kit.button(x, y, w, h, label, opts)
|
||||
opts.face = "selection"
|
||||
end
|
||||
opts.icon = opts.icon or ACTION_ICONS[label]
|
||||
audit("control", x, y, w, h, label)
|
||||
audit("control", x, y, w, h, label, opts.id)
|
||||
local hot = opts.enabled ~= false and Kit.hover(x, y, w, h)
|
||||
local shown = label
|
||||
if opts.iconOnly then
|
||||
@@ -738,6 +742,9 @@ function Kit.textfield(id, x, y, w, h, value, placeholder, opts)
|
||||
Kit.blur() -- commit/cancel also lowers the soft keyboard (#529)
|
||||
focused = false
|
||||
elseif e == "\27" then
|
||||
if opts and opts.onCancel then
|
||||
opts.onCancel()
|
||||
end
|
||||
Kit.blur()
|
||||
focused = false
|
||||
else
|
||||
|
||||
@@ -35,8 +35,11 @@ If the file isn't there (or you want another copy), use **Open...**, drop a
|
||||
|
||||
Party, Boxes, Items, Events, Map, Pokédex, Trainer and Checks are separate pages.
|
||||
The Pokemon inspector has Main, Stats, Moves, Origin, Extras and Checks sections.
|
||||
Phones open the roster and inspector as separate sliding pages; wide windows
|
||||
show them side by side. Buttons share the launcher's painter and font scale,
|
||||
Wide windows show the roster beside the inspector; phones stack a party grid
|
||||
above it on one scrolling page. The inspector opens with the Pokemon's sprite,
|
||||
types, level and HP, then section tabs (a dropdown when the tabs do not fit),
|
||||
the section form and a row of tools for that section. Numbers have a typed
|
||||
value box and a slider that commits when released. Buttons share the launcher's painter and font scale,
|
||||
with a minimum 44px target. Pages and screen sections use dropdown popup
|
||||
choosers on both phones and desktop. The current page stays in place behind
|
||||
the scrim; choosing a destination closes the popup and slides to that section.
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
-- Species art and the inspector shell. InspectorBody owns the six forms;
|
||||
-- phones open them as pages and wide windows dock them next to the roster.
|
||||
|
||||
local Theme = require("Theme")
|
||||
local PAL = Theme.PAL
|
||||
@@ -91,10 +89,30 @@ function MonEditor.drawSprite(S, Kit, species, x, y, size, mon)
|
||||
y + size / 2 - Kit.textHeight("micro") / 2, size, PAL.muted)
|
||||
end
|
||||
|
||||
-- The inspector uses sections so a phone gives each editing task its full height.
|
||||
function MonEditor.draw(S, Kit, x, y, w, h)
|
||||
Kit.card(x, y, w, h)
|
||||
require("InspectorBody").draw(S, Kit, x, y, w, h)
|
||||
function MonEditor.isShiny(S, mon)
|
||||
if not mon then return false end
|
||||
local g = Gen.ofState(S)
|
||||
if g == 3 then
|
||||
if mon.isShiny ~= nil then return mon.isShiny == true end
|
||||
local ok, shiny = pcall(require("src.core.game3.pokemon").isShiny, mon)
|
||||
return ok and shiny == true
|
||||
elseif g == 2 then
|
||||
return require("src.pokemon.Stats").isShiny(mon.dvs)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function MonEditor.displayName(S, mon)
|
||||
if mon.nickname and mon.nickname ~= "" then return mon.nickname end
|
||||
local def = S.data and S.data.pokemon and S.data.pokemon[mon.species or mon.speciesId]
|
||||
if def and def.name then return def.name end
|
||||
if type(mon.species) == "string" then return mon.species end
|
||||
if type(mon.name) == "string" and mon.name ~= "" then return mon.name end
|
||||
return tostring(mon.species or "POKEMON")
|
||||
end
|
||||
|
||||
function MonEditor.draw(S, Kit, x, y, w, h, prelude)
|
||||
require("InspectorBody").draw(S, Kit, x, y, w, h, prelude)
|
||||
end
|
||||
|
||||
return MonEditor
|
||||
|
||||
+107
-128
@@ -1,6 +1,3 @@
|
||||
-- Party roster and inspector: side by side when wide, sliding pages on phones.
|
||||
-- Each viewport scrolls with fixed readable controls.
|
||||
|
||||
local PartyMod = require("src.pokemon.Party")
|
||||
local Theme = require("Theme")
|
||||
local Ops = require("Ops")
|
||||
@@ -8,15 +5,9 @@ local MonEditor = require("MonEditor")
|
||||
local PAL = Theme.PAL
|
||||
|
||||
local Party = {}
|
||||
local Motion = require("Motion")
|
||||
|
||||
-- Roster column width: the design's 460px at the reference size, but it gives
|
||||
-- ground to the inspector on a narrow window so neither column collapses.
|
||||
-- The 300px floor used to be absolute, which on a phone handed the roster
|
||||
-- three quarters of the panel and left the inspector laying itself out at a
|
||||
-- negative width (#497); the floor now yields to a share of what there is.
|
||||
local function rosterWidth(w, s)
|
||||
return math.max(math.min(300 * s, w * 0.42), math.min(460 * s, w * 0.36))
|
||||
return Theme.clamp(w * 0.27, 250 * s, 340 * s)
|
||||
end
|
||||
|
||||
-- HP colour follows the game's own health bar thresholds.
|
||||
@@ -30,44 +21,63 @@ local function hpColor(frac)
|
||||
return PAL.green
|
||||
end
|
||||
|
||||
local function hpFrac(mon)
|
||||
local maxHp = mon.maxHp or (mon.stats and mon.stats.hp) or 1
|
||||
return Ops.clamp((tonumber(mon.hp) or 0) / math.max(maxHp, 1), 0, 1), maxHp
|
||||
end
|
||||
|
||||
local function toolbar(S, Kit, x, y, w, vertical)
|
||||
local s, row, gap = Kit.scale, Kit.controlH(), 8 * Kit.scale
|
||||
local selected = S.save.party[S.selectedParty or 0] ~= nil and S.editingMon == S.save.party[S.selectedParty]
|
||||
local n = #S.save.party
|
||||
if Kit.iconButton(x, y, row, row, vertical and "arrow-up" or "arrow-left", "Move up",
|
||||
{ enabled = selected and S.selectedParty > 1 }) then
|
||||
Ops.partyMove(S, -1)
|
||||
end
|
||||
if Kit.iconButton(x + row + gap, y, row, row, vertical and "arrow-down" or "arrow-right", "Move down",
|
||||
{ enabled = selected and S.selectedParty < n }) then
|
||||
Ops.partyMove(S, 1)
|
||||
end
|
||||
local armed = Ops.armLabel(S, "party-remove", "Remove") ~= "Remove"
|
||||
local removeW = armed and Kit.buttonWidth("Confirm?", { font = "button", icon = "check" }, row) or row
|
||||
local rx = x + w - removeW
|
||||
if armed then
|
||||
if Kit.button(rx, y, removeW, row, "Confirm?", { kind = "danger", font = "button", icon = "check" }) then
|
||||
Ops.partyRemove(S)
|
||||
end
|
||||
elseif Kit.iconButton(rx, y, row, row, "trash", "Remove", { kind = "danger", enabled = selected }) then
|
||||
Ops.partyRemove(S)
|
||||
end
|
||||
local addOpts = { font = "button", icon = "plus", enabled = n < PartyMod.MAX }
|
||||
local addW = Kit.buttonWidth("Add", addOpts, row)
|
||||
if Kit.button(rx - gap - addW, y, addW, row, "Add", addOpts) then
|
||||
Ops.partyAdd(S)
|
||||
end
|
||||
return row
|
||||
end
|
||||
|
||||
local function drawRoster(S, Kit, x, y, listW, h)
|
||||
local s = Kit.scale
|
||||
Kit.card(x, y, listW, h)
|
||||
local pad = 18 * s
|
||||
local pad = 16 * s
|
||||
local cx = x + pad
|
||||
local innerW = listW - 2 * pad
|
||||
|
||||
Kit.caption(cx, y + pad, "PARTY")
|
||||
Kit.textRight(
|
||||
"mono",
|
||||
("%d/%d"):format(#S.save.party, PartyMod.MAX),
|
||||
cx + innerW,
|
||||
y + pad,
|
||||
PAL.caption
|
||||
)
|
||||
Kit.textRight("mono", ("%d/%d"):format(#S.save.party, PartyMod.MAX), cx + innerW, y + pad, PAL.caption)
|
||||
|
||||
-- "+ Add mon" / "Remove" pinned to the card bottom; the roster fills above.
|
||||
local actH = Kit.controlH()
|
||||
local actY = y + h - pad - actH
|
||||
S.toastBottom = math.min(S.toastBottom or actY, actY)
|
||||
local listTop = y + pad + Kit.textHeight("caption") + 12 * s
|
||||
local listH = actY - 12 * s - listTop
|
||||
local listTop = y + pad + Kit.textHeight("caption") + 14 * s
|
||||
local listH = actY - 14 * s - listTop
|
||||
|
||||
if #S.save.party == 0 then
|
||||
Kit.emptyBox(
|
||||
cx,
|
||||
listTop,
|
||||
innerW,
|
||||
listH,
|
||||
"Party is empty - Add creates a Lv5 mon owned by the save's player."
|
||||
)
|
||||
Kit.emptyBox(cx, listTop, innerW, listH, "Party is empty. Add creates a Lv5 Pokemon owned by the player.")
|
||||
else
|
||||
local rowH = math.max(88 * s, Kit.tapMin() + 38 * s)
|
||||
local rowH = math.max(76 * s, Kit.tapMin() + 28 * s)
|
||||
local rowGap = 8 * s
|
||||
S.selectedParty = Ops.clamp(S.selectedParty or 1, 1, #S.save.party)
|
||||
-- The list used to `break` past the fold, silently hiding party slots on
|
||||
-- a short window; it scrolls instead now (#715), same offset contract as
|
||||
-- every other list in the editor.
|
||||
local drawn, shift = Kit.list(S, "partyOffset", cx, listTop, innerW, listH, #S.save.party, rowH + rowGap)
|
||||
Kit.pushClip(cx, listTop, innerW, listH)
|
||||
for i = 1, drawn do
|
||||
@@ -78,120 +88,89 @@ local function drawRoster(S, Kit, x, y, listW, h)
|
||||
end
|
||||
local ry = listTop + (i - 1) * (rowH + rowGap) - shift
|
||||
local selected = (S.editingMon == mon)
|
||||
if Kit.row(cx, ry, innerW, rowH, selected, PAL.green) then
|
||||
if Kit.row(cx, ry, innerW, rowH, selected, PAL.text) then
|
||||
Ops.selectParty(S, slot)
|
||||
Motion.change(S, "mobileInspector", true, 1)
|
||||
end
|
||||
|
||||
local rpad = 12 * s
|
||||
local icon = 44 * s
|
||||
local rpad = 10 * s
|
||||
local icon = math.min(56 * s, rowH - 12 * s)
|
||||
MonEditor.drawSprite(S, Kit, mon.species, cx + rpad, ry + (rowH - icon) / 2, icon, mon)
|
||||
|
||||
-- right cluster first, so the name knows how much room it has left
|
||||
local rightW = 52 * s
|
||||
local chipH = 20 * s
|
||||
local lvText = ("Lv%d"):format(mon.level)
|
||||
local chipW = Kit.textWidth("tiny", lvText) + 14 * s
|
||||
local chipX = cx + innerW - rpad - chipW
|
||||
Theme.stroke(chipX, ry + 10 * s, chipW, chipH, 6 * s, PAL.cardBorder, 0.3, 1)
|
||||
Kit.textCenter(
|
||||
"tiny",
|
||||
lvText,
|
||||
chipX,
|
||||
ry + 10 * s + (chipH - Kit.textHeight("tiny")) / 2,
|
||||
chipW,
|
||||
PAL.blueInk
|
||||
)
|
||||
if selected then
|
||||
local ab = Kit.tapMin()
|
||||
local aw = Kit.tapMin()
|
||||
local ax = cx + innerW - rpad - 2 * aw - 4 * s
|
||||
local ay = ry + rowH - 10 * s - ab
|
||||
if Kit.stepper(ax, ay, aw, ab, "up", { font = "tiny" }) then
|
||||
Ops.partyMove(S, -1)
|
||||
end
|
||||
if Kit.stepper(ax + aw + 4 * s, ay, aw, ab, "down", { font = "tiny" }) then
|
||||
Ops.partyMove(S, 1)
|
||||
end
|
||||
rightW = 2 * aw + 4 * s + rpad
|
||||
local tx = cx + rpad + icon + 10 * s
|
||||
local tw = math.max(40 * s, cx + innerW - rpad - tx)
|
||||
local lvText = ("Lv %d"):format(mon.level or 0)
|
||||
local lvW = Kit.textWidth("small", lvText)
|
||||
local nameY = ry + 12 * s
|
||||
Kit.textRight("small", lvText, tx + tw, nameY + (Kit.textHeight("monoRow") - Kit.textHeight("small")) / 2, PAL.muted)
|
||||
local shiny = MonEditor.isShiny(S, mon)
|
||||
local spark = shiny and Kit.textHeight("small") or 0
|
||||
local name = Kit.ellipsize("monoRow", MonEditor.displayName(S, mon), tw - lvW - spark - 16 * s)
|
||||
Kit.textBold("monoRow", name, tx, nameY, PAL.heading)
|
||||
if shiny then
|
||||
Kit.icon("sparkles", tx + Kit.textWidth("monoRow", name) + 6 * s,
|
||||
nameY + (Kit.textHeight("monoRow") - spark) / 2, spark, PAL.yellow)
|
||||
end
|
||||
|
||||
local tx = cx + rpad + icon + 12 * s
|
||||
local tw = math.max(40 * s, (cx + innerW - rightW - 10 * s) - tx)
|
||||
local monName = (mon.nickname and mon.nickname ~= "" and mon.nickname)
|
||||
or (type(mon.species) == "string" and mon.species)
|
||||
or (type(mon.name) == "string" and mon.name ~= "" and mon.name)
|
||||
or (S.data and S.data.pokemon and S.data.pokemon[mon.species or mon.speciesId] and S.data.pokemon[mon.species or mon.speciesId].name)
|
||||
or tostring(mon.species or "POKEMON")
|
||||
local name = Kit.ellipsize("monoRow", monName, tw - 34 * s)
|
||||
Kit.text("monoRow", name, tx, ry + 10 * s, PAL.heading)
|
||||
Kit.text(
|
||||
"tiny",
|
||||
("#%d"):format(slot),
|
||||
tx + Kit.textWidth("monoRow", name) + 8 * s,
|
||||
ry + 12 * s,
|
||||
PAL.caption
|
||||
)
|
||||
|
||||
local maxHp = (mon.stats and mon.stats.hp) or 1
|
||||
local frac = Ops.clamp((mon.hp or 0) / math.max(maxHp, 1), 0, 1)
|
||||
Kit.meter(tx, ry + rowH / 2 + 2 * s, tw, 6 * s, frac * 100, hpColor(frac))
|
||||
Kit.text(
|
||||
"tiny",
|
||||
("HP %d/%d"):format(mon.hp or 0, maxHp),
|
||||
tx,
|
||||
ry + rowH - 10 * s - Kit.textHeight("tiny"),
|
||||
PAL.muted
|
||||
)
|
||||
local frac, maxHp = hpFrac(mon)
|
||||
Kit.meter(tx, ry + rowH / 2 + 1 * s, tw, 5 * s, frac * 100, hpColor(frac))
|
||||
Kit.text("tiny", ("HP %d/%d"):format(mon.hp or 0, maxHp), tx, ry + rowH - 10 * s - Kit.textHeight("tiny"), PAL.muted)
|
||||
end
|
||||
Kit.popClip()
|
||||
Kit.listScrollbar(S, "partyOffset", cx, listTop, innerW, listH)
|
||||
end
|
||||
|
||||
local halfW = (innerW - 10 * s) / 2
|
||||
if
|
||||
Kit.button(
|
||||
cx,
|
||||
actY,
|
||||
halfW,
|
||||
actH,
|
||||
"Add mon",
|
||||
{ font = "small", radius = 9 * s, enabled = #S.save.party < PartyMod.MAX }
|
||||
)
|
||||
then
|
||||
Ops.partyAdd(S)
|
||||
end
|
||||
if
|
||||
Kit.button(
|
||||
cx + halfW + 10 * s,
|
||||
actY,
|
||||
halfW,
|
||||
actH,
|
||||
Ops.armLabel(S, "party-remove", "Remove"),
|
||||
{ kind = "danger", font = "small", radius = 9 * s }
|
||||
)
|
||||
then
|
||||
Ops.partyRemove(S)
|
||||
Theme.col(PAL.cardBorder, 0.22)
|
||||
love.graphics.rectangle("fill", x, actY - 8 * s, listW, 1)
|
||||
toolbar(S, Kit, cx, actY, innerW, true)
|
||||
end
|
||||
|
||||
local function drawStrip(S, Kit, x, y, w)
|
||||
local s = Kit.scale
|
||||
local gap = 10 * s
|
||||
local cy = y
|
||||
Kit.caption(x, cy, "PARTY")
|
||||
Kit.textRight("mono", ("%d/%d"):format(#S.save.party, PartyMod.MAX), x + w, cy, PAL.caption)
|
||||
cy = cy + Kit.textHeight("caption") + 12 * s
|
||||
local n = #S.save.party
|
||||
if n == 0 then
|
||||
local boxH = 80 * s
|
||||
Kit.emptyBox(x, cy, w, boxH, "Party is empty")
|
||||
cy = cy + boxH + gap
|
||||
else
|
||||
local cols = Theme.clamp(math.floor((w + gap) / (92 * s + gap)), 3, 6)
|
||||
local tileW = (w - (cols - 1) * gap) / cols
|
||||
local sprite = math.floor(math.min(tileW - 20 * s, 64 * s))
|
||||
local tileH = math.ceil(10 * s + sprite + 4 * s + Kit.textHeight("tiny") + 8 * s + 4 * s + 10 * s)
|
||||
for i, mon in ipairs(S.save.party) do
|
||||
local col, line = (i - 1) % cols, math.floor((i - 1) / cols)
|
||||
local tx, ty = x + col * (tileW + gap), cy + line * (tileH + gap)
|
||||
if Kit.row(tx, ty, tileW, tileH, S.editingMon == mon, PAL.text) then
|
||||
Ops.selectParty(S, i)
|
||||
end
|
||||
MonEditor.drawSprite(S, Kit, mon.species, tx + (tileW - sprite) / 2, ty + 10 * s, sprite, mon)
|
||||
local ly = ty + 10 * s + sprite + 4 * s
|
||||
Kit.textCenter("tiny", ("Lv %d"):format(mon.level or 0), tx, ly, tileW, PAL.muted)
|
||||
if MonEditor.isShiny(S, mon) then
|
||||
local spark = 14 * s
|
||||
Kit.icon("sparkles", tx + tileW - spark - 8 * s, ty + 8 * s, spark, PAL.yellow)
|
||||
end
|
||||
local frac = hpFrac(mon)
|
||||
local barW = tileW - 28 * s
|
||||
Kit.meter(tx + 14 * s, ly + Kit.textHeight("tiny") + 8 * s, barW, 4 * s, frac * 100, hpColor(frac))
|
||||
end
|
||||
cy = cy + math.ceil(n / cols) * (tileH + gap)
|
||||
end
|
||||
cy = cy + toolbar(S, Kit, x, cy, w, false)
|
||||
return cy - y
|
||||
end
|
||||
|
||||
function Party.draw(S, Kit, x, y, w, h)
|
||||
local s = Kit.scale
|
||||
local gap = 20 * s
|
||||
S.inspectorBack = false
|
||||
if w < 800 * s then
|
||||
if S.mobileInspector == nil then
|
||||
S.mobileInspector = S.editingMon ~= nil
|
||||
end
|
||||
Motion.pages(S, Kit, "mobileInspector", x, y, w, h, function(state, kit, dx, dy, dw, dh)
|
||||
if state.editingMon and state.mobileInspector then
|
||||
state.inspectorBack = true
|
||||
MonEditor.draw(state, kit, dx, dy, dw, dh)
|
||||
else
|
||||
drawRoster(state, kit, dx, dy, dw, dh)
|
||||
end
|
||||
MonEditor.draw(S, Kit, x, y, w, h, function(px, py, pw)
|
||||
return drawStrip(S, Kit, px, py, pw)
|
||||
end)
|
||||
else
|
||||
S.inspectorBack = false
|
||||
local gap = 14 * s
|
||||
local listW = rosterWidth(w, s)
|
||||
drawRoster(S, Kit, x, y, listW, h)
|
||||
MonEditor.draw(S, Kit, x + listW + gap, y, w - listW - gap, h)
|
||||
|
||||
Reference in New Issue
Block a user