package.path = "./?.lua;./?/init.lua;" .. package.path local T = require("tests.harness") local check, eq = T.check, T.eq love = love or require("tests.love_stub") love.graphics.setLineJoin = love.graphics.setLineJoin or function() end love.graphics.newShader = love.graphics.newShader or function() return {} end local Kit = require("src.ui.kit.Kit") local RomImporter = require("src.import.RomImporter") local LauncherView = require("src.import.LauncherView") local function window(w, h) love.graphics.getDimensions = function() return w, h end love.graphics.getPixelDimensions = function() return w, h end end local function pointer(x, y) love.mouse.getPosition = function() return x, y end end eq(Kit.scrollExtent(800, 500), 300, "the extent is exactly the overflow") eq(Kit.scrollExtent(400, 500), 0, "content that fits has no extent") eq(Kit.scrollExtent(400, -50), 400, "a negative viewport is no room, not more") eq(Kit.scrollExtent(nil, nil), 0, "an unmeasured region has no extent") eq(Kit.scrollClamp(-10, 300), 0, "an offset above the top clamps to it") eq(Kit.scrollClamp(5000, 300), 300, "an offset past the end clamps to it") eq(Kit.scrollClamp(120, 0), 0, "a region with no travel sits at the top") local at, left = Kit.scrollHandoff(0, 300, 120) eq(at, 120, "a move inside the extent is taken in full") eq(left, 0, "and hands nothing on") at, left = Kit.scrollHandoff(250, 300, 120) eq(at, 300, "a move past the end stops at the end") eq(left, 70, "and hands the remainder to whatever is behind it") at, left = Kit.scrollHandoff(0, 300, -80) eq(at, 0, "a move above the top stops at the top") eq(left, -80, "and hands that remainder on with its sign") local function wheelCase(offset, maxScroll, wheel, mx, my) Kit.blockClicks = false Kit.mouseX, Kit.mouseY = mx, my Kit.wheelY = wheel Kit._clipRect = nil local moved, took = Kit.scrollWheel(offset, maxScroll, 0, 0, 100, 100, 50) return moved, took, Kit.wheelY end local moved, took, leftWheel = wheelCase(0, 300, -1, 50, 50) eq(moved, 50, "a notch over the region moves it by one step") eq(took, true, "and reports the region took it") eq(leftWheel, 0, "so nothing reaches the surface behind it") moved, took, leftWheel = wheelCase(300, 300, -1, 50, 50) eq(moved, 300, "a region already at its bottom does not move") eq(took, false, "and does not claim the notch") eq(leftWheel, -1, "which is what lets the page scroll take over") moved, took, leftWheel = wheelCase(0, 300, 1, 50, 50) eq(moved, 0, "a region at the top ignores an upward notch") eq(leftWheel, 1, "and passes it on") moved, took, leftWheel = wheelCase(0, 300, -1, 400, 400) eq(took, false, "a notch outside the region is not the region's") eq(leftWheel, -1, "and stays queued") Kit.blockClicks = true Kit.mouseX, Kit.mouseY, Kit.wheelY = 50, 50, -1 moved, took = Kit.scrollWheel(0, 300, 0, 0, 100, 100, 50) eq(took, false, "a shielded frame (modal up) leaves the region alone") eq(Kit.wheelY, -1, "so the modal's own scroller still sees the notch") Kit.blockClicks = false local function skinLauncher(count) local imp = RomImporter.new(function() end, { launcher = true }) imp.tab = "skins" local skins = {} for i = 1, count do skins[i] = { id = "skin" .. i, source = "user", controls = 8, pages = 1 } end imp._skins = skins imp._ensureSkins = function() return skins end return imp end window(360, 780) local imp = skinLauncher(12) LauncherView.draw(imp) LauncherView.draw(imp) local rect = imp._tabRegionRect check(rect ~= nil, "the panel publishes the rect its region occupies") check((imp._tabScrollMax.skins or 0) > 0, "a panel with more rows than its viewport scrolls") eq(imp._tabScroll.skins, 0, "a freshly drawn panel sits at the top") check(imp._tabContentH.skins > rect.h, "the region's content is taller than the viewport it is clipped to") pointer(rect.x + 10, rect.y + 10) imp._wheelY = -1 LauncherView.draw(imp) local step = math.floor(48 * Kit.scale) eq(imp._tabScroll.skins, step, "one notch scrolls the panel by one step") eq(imp._pageScroll, 0, "and the page under it does not move while the panel still can") for _ = 1, 30 do imp._wheelY = -1 LauncherView.draw(imp) end eq(imp._tabScroll.skins, imp._tabScrollMax.skins, "held down, the panel reaches its own bottom") eq(imp._pageScroll, imp._pageScrollMax, "and only then does the leftover scroll the page") for _ = 1, 40 do imp._wheelY = 1 LauncherView.draw(imp) end eq(imp._tabScroll.skins, 0, "scrolling back up returns the panel to the top") eq(imp._pageScroll, 0, "and the page with it") imp._wheelY = -1 LauncherView.draw(imp) local parked = imp._tabScroll.skins check(parked > 0, "the skins panel is parked mid-scroll") imp:_switchTab("red") LauncherView.draw(imp) eq(imp._tabScroll.red or 0, 0, "the game tab has its own offset") eq(imp._tabScroll.skins, parked, "and the skins offset survives the switch") imp:_switchTab("skins") LauncherView.draw(imp) eq(imp._tabScroll.skins, parked, "coming back lands where the player left") imp._skins = {} imp._ensureSkins = function() return {} end LauncherView.draw(imp) LauncherView.draw(imp) eq(imp._tabScrollMax.skins, 0, "a panel that now fits has no travel") eq(imp._tabScroll.skins, 0, "and its offset comes back with it") local mods = {} for i = 1, 60 do mods[#mods + 1] = { id = "mod" .. i, name = "Mod " .. i, version = "1.0.0", status = "ok", badge = "gameplay", description = "a mod", enabledByVersion = { red = true }, } end window(360, 780) local modImp = RomImporter.new(function() end, { launcher = true }) modImp.tab = "mods" modImp.mods = mods modImp._ensureMods = function() return mods end LauncherView.draw(modImp) LauncherView.draw(modImp) local reg = modImp._tabRegionRect check((modImp._tabScrollMax.mods or 0) > reg.h, "60 installed mods are ONE continuous list: the region's travel spans " .. "the whole list, not one page of it") eq(modImp._pages.mods, nil, "and no page state is ever minted for it") pointer(reg.x + 10, reg.y + 40) modImp._wheelY = -1 LauncherView.draw(modImp) check((modImp._tabScroll.mods or 0) > 0, "a notch over the region scrolls the list like any other tab") eq(modImp._pageScroll, 0, "without reaching the page behind it") modImp._tabScroll.mods = modImp._tabScrollMax.mods LauncherView.draw(modImp) pointer(reg.x + 10, reg.y + reg.h * 0.5) modImp._wheelY = -1 LauncherView.draw(modImp) eq(modImp._pages.mods, nil, "bottoming the list out never auto-advances any pager") eq(modImp._tabScroll.mods, modImp._tabScrollMax.mods, "the list just rests at its end") modImp._modActions = "mod1" local shieldedAt = modImp._tabScroll.mods modImp._wheelY = -1 LauncherView.draw(modImp) eq(modImp._tabScroll.mods, shieldedAt, "a shielded mod list ignores the notch under the scrim") modImp._modActions = nil modImp._wheelY = 0 LauncherView.draw(modImp) window(360, 780) local gameImp = RomImporter.new(function() end, { launcher = true }) gameImp.tab = "red" gameImp.ready = { red = true } gameImp.slots = { red = {} } for i = 1, 8 do gameImp.slots.red[i] = { id = "slot" .. i, name = "Slot " .. i } end gameImp._ensureSlots = function() end LauncherView.draw(gameImp) LauncherView.draw(gameImp) check((gameImp._tabScrollMax.red or 0) > 0, "a game tab whose cart and slots outgrow the viewport scrolls too") check(gameImp._tabContentH.red > gameImp._tabRegionRect.h, "because it reports its NATURAL height, not the height it was given") pointer(gameImp._tabRegionRect.x + 10, gameImp._tabRegionRect.y + 10) gameImp._wheelY = -1 LauncherView.draw(gameImp) check((gameImp._tabScroll.red or 0) > 0, "and a notch over it moves it") window(360, 780) local touchImp = skinLauncher(12) LauncherView.draw(touchImp) LauncherView.draw(touchImp) local treg = touchImp._tabRegionRect local tmax = touchImp._tabScrollMax.skins check(tmax > 0, "the touched panel has travel") LauncherView.touchpressed(touchImp, 1, treg.x + 20, treg.y + 40) LauncherView.touchmoved(touchImp, 1, treg.x + 20, treg.y + 40 - 200) eq(touchImp._tabScroll.skins, math.min(200, tmax), "dragging up scrolls the panel by the finger's travel") eq(touchImp._pageScroll, 0, "while the panel still has travel, the page waits") LauncherView.touchmoved(touchImp, 1, treg.x + 20, treg.y + 40 - 200 - tmax * 2) eq(touchImp._tabScroll.skins, tmax, "a longer drag reaches the panel's bottom") check((touchImp._pageScroll or 0) > 0, "and spills into the page from there") LauncherView.touchreleased(touchImp, 1, treg.x + 20, treg.y - 400) window(360, 780) local dragMods = RomImporter.new(function() end, { launcher = true }) dragMods.tab = "mods" dragMods.mods = mods dragMods._ensureMods = function() return mods end LauncherView.draw(dragMods) LauncherView.draw(dragMods) local dreg = dragMods._tabRegionRect local dRegionMax = dragMods._tabScrollMax.mods check(dRegionMax > 0, "the mods region scrolls its overscan like any tab") LauncherView.touchpressed(dragMods, 7, dreg.x + 20, dreg.y + 30) LauncherView.touchmoved(dragMods, 7, dreg.x + 20, dreg.y + 30 - 60) eq(dragMods._tabScroll.mods, math.min(60, dRegionMax), "a drag moves the region by the finger's travel") LauncherView.touchmoved(dragMods, 7, dreg.x + 20, dreg.y + 30 - 60 - dRegionMax * 2) eq(dragMods._tabScroll.mods, dRegionMax, "carrying on saturates the region") check((dragMods._pageScroll or 0) > 0, "and only then reaches the page") LauncherView.touchreleased(dragMods, 7, dreg.x + 20, dreg.y - 900) dragMods._skins = { { id = "s1", source = "user", controls = 8, pages = 1 } } for i = 2, 12 do dragMods._skins[i] = { id = "s" .. i, source = "user", controls = 8, pages = 1 } end dragMods._ensureSkins = function() return dragMods._skins end local heldModsPage = dragMods._pages.mods or 1 local heldModsAt = dragMods._tabScroll.mods local overList = dreg.y + 60 dragMods:_switchTab("skins") LauncherView.draw(dragMods) LauncherView.draw(dragMods) local sreg = dragMods._tabRegionRect check((dragMods._tabScrollMax.skins or 0) > 0, "the skins tab has travel") LauncherView.touchpressed(dragMods, 9, sreg.x + 20, overList) LauncherView.touchmoved(dragMods, 9, sreg.x + 20, overList - 200) check((dragMods._tabScroll.skins or 0) > 0, "a drag on the skins tab scrolls the skins tab") eq(dragMods._pages.mods or 1, heldModsPage, "and leaves the mod list on the page the player parked it") eq(dragMods._tabScroll.mods, heldModsAt, "with its region offset held for the return trip") LauncherView.touchreleased(dragMods, 9, sreg.x + 20, sreg.y - 400) love.graphics.polygon = love.graphics.polygon or function() end window(360, 780) local padImp = skinLauncher(12) LauncherView.draw(padImp) LauncherView.draw(padImp) local preg = padImp._tabRegionRect padImp._padCursorActive = true padImp._padCursor = { x = preg.x + 10, y = preg.y + preg.h - 4 } LauncherView.wheelmoved(padImp, 0, -1) LauncherView.draw(padImp) check((padImp._tabScroll.skins or 0) > 0, "the pad's synthesized wheel scrolls the region its cursor sits in") eq(padImp._pageScroll, 0, "and not the page behind it") window(1280, 720) local edgeImp = skinLauncher(40) LauncherView.draw(edgeImp) LauncherView.draw(edgeImp) local ereg = edgeImp._tabRegionRect check((edgeImp._tabScrollMax.skins or 0) > 0, "the wide window still overflows") eq(edgeImp._pageScrollMax, 0, "with no page scroll left to catch the notch") check(ereg.y + ereg.h < 720, "and a region that ends above the safe area") edgeImp._padCursorActive = true edgeImp._padCursor = { x = ereg.x + 20, y = 719 } edgeImp._padAxis = { lefty = 1 } edgeImp._padDir = {} pointer(ereg.x + 20, 719) edgeImp:_updatePadCursor(0.5) check((edgeImp._wheelY or 0) < 0, "the edge push synthesizes a notch") LauncherView.draw(edgeImp) check((edgeImp._tabScroll.skins or 0) > 0, "which reaches the tab region even though the cursor is below it") local function read(path) local f = assert(io.open(path, "r")) local src = f:read("*a") f:close() return src end local view = read("src/import/LauncherView.lua") check(view:find("Kit.scrollBegin(", 1, true) ~= nil, "the panel dispatch opens a scroll region") check(view:find("Kit.scrollEnd(", 1, true) ~= nil, "and closes it") check(view:find("modListWantsWheel", 1, true) == nil, "no nested list steals the wheel from the region any more") check(view:find("start.region", 1, true) ~= nil, "a touch drag that began in the region scrolls the region") check(view:find("Kit.scrollGutter(", 1, true) ~= nil, "the panels lay out inside a gutter, so the thumb covers no control") check(view:find("Kit.scrollHandoff(tabScrollAt(imp)", 1, true) ~= nil, "and hands its leftover to the page, like the wheel does") local kit = read("src/ui/kit/Kit.lua") check(kit:find("function Kit.scrollWheel", 1, true) ~= nil, "the kit owns the wheel rule, so no panel hand-rolls a fifth copy") T.finish("launcher scroll regions")