mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
Scroll the launcher page so the footer stops covering the panel
On a window too short for the stacked single-column layout -- a phone, or a narrow desktop window -- the ROM / SAVE FILES / Play / SAVE SLOT stack ran past the bottom of the window while the footer stayed pinned there and drew over it. Nothing clipped the panel and nothing scrolled it, so the overflow was unreachable. Everything under the tab bar (panel, updater banner, footer) is now one scrolling column, used only when it is taller than the room below the tab bar. The strip, logo and tab bar stay pinned, so navigation is always on screen, and the footer is laid out downward from footerTop right after the content instead of upward from the window bottom. - RomImporter.pageScrollFor is the whole decision, pure and covered by tests/engine/launcher_page_scroll.lua. A window that grows back drags the offset down with it, so the page never stays parked past its own end. - The panels return their natural height as they draw, so the measurement is the previous frame's: the same one-frame settle the slot and mod lists already rely on. - One scroll axis at a time. While the page scrolls, the panels draw paged: the slot and mod lists take their natural height, keep no inner scroll region and report a max of 0, so wheel, right stick and drag all move the page. Two-column layouts do not overflow, paged stays false, and every one of these behaves exactly as before. - inside() and _ptIn() reject a rect that scrolled out of the viewport, so a control that slid under the tab bar cannot be clicked through it. Tab chips are pinned and exempt. pageScroll resets on a tab change. Android had no scroll gesture at all: the launcher is handed no move events (main.lua forwards neither touchmoved nor mousemoved while it is up) and its mouse emulation was never trusted, which is what "no reliable pointer polling" referred to. That was survivable while every scroll region was an inner list, and useless once the page itself scrolls, since a phone is exactly where it overflows. love.touch is pollable, so _pointerHold reads the first active touch there and hands _updateSlotDrag the same (held, y) pair the mouse gives on desktop. Slot rows and mod toggles consequently arm on press and commit on release on Android too, matching desktop, so a swipe that starts on a card scrolls instead of selecting it. All of it is gated on touchPollable: without love.touch every Android path is exactly what it was. conf.lua also grows minwidth/minheight (480x360) for the desktop window, under which the cards stop being readable. Mobile is fullscreen and ignores it. Verified on the Android emulator (1080x2400) against a build of the parent commit: before, the footer painted over the SAVE SLOT card with no way to reach it; after, the page pans by touch and the footer is reachable and intact.
This commit is contained in:
@@ -35,6 +35,14 @@ function love.conf(t)
|
||||
-- starting size, not the game's resolution.
|
||||
t.window.width = 1024
|
||||
t.window.height = 768
|
||||
-- Floor for the resizable desktop window. The launcher's single-column
|
||||
-- layout is laid out against ~420 logical px of content, and the game
|
||||
-- canvas letterboxes fine below that, so this only stops a drag that would
|
||||
-- squeeze the cards past the point where their buttons still read. Kept
|
||||
-- well under the smallest supported desktop display; mobile ignores it
|
||||
-- (fullscreen), so the handheld ports are unaffected.
|
||||
t.window.minwidth = 480
|
||||
t.window.minheight = 360
|
||||
end
|
||||
t.version = "11.5"
|
||||
t.window.vsync = 1
|
||||
|
||||
@@ -202,3 +202,64 @@ column, slot card below Play, when the window is too narrow for both
|
||||
(wheel, or drag on touch/desktop) clamped to their own content extent,
|
||||
recomputed every draw. The tab bar labels only the active chip so it stays
|
||||
narrow-safe, and content caps out at `~1440 * s` wide, centered.
|
||||
|
||||
The desktop window has a floor of 480x360 (`conf.lua` `minwidth`/`minheight`),
|
||||
under which the cards stop being readable at all. Mobile ignores it: those
|
||||
windows are fullscreen.
|
||||
|
||||
### Page scroll
|
||||
|
||||
Two columns fit any window the launcher is likely to open in; one stacked
|
||||
column does not. On a phone-shaped window the ROM card, SAVE FILES, Play and
|
||||
SAVE SLOT together run past the bottom, and a footer pinned to the window
|
||||
bottom painted over them with the overflow unreachable.
|
||||
|
||||
So the whole column under the tab bar -- panel, updater banner, footer --
|
||||
scrolls as one page whenever it is taller than the room below the tab bar:
|
||||
|
||||
- The strip, logo and tab bar stay pinned, so navigation is always on screen.
|
||||
Everything else draws at `contentTop - pageScroll` inside a scissor, and the
|
||||
footer is laid out downward from `footerTop` right after the content instead
|
||||
of upward from the window bottom.
|
||||
- `RomImporter.pageScrollFor(naturalH, viewportH, scroll)` is the whole
|
||||
decision, pure and pinned by `tests/engine/launcher_page_scroll.lua`. A
|
||||
window that grows back drags the offset down with it, so the page can never
|
||||
stay parked past its own end.
|
||||
- The panels report their natural height as they draw (`_drawGamePanel` and
|
||||
`_drawModsPanel` return it), so the decision reads the previous frame's
|
||||
measurement -- the same one-frame settle the two lists already rely on.
|
||||
- **One scroll axis at a time.** While the page scrolls, the panels draw
|
||||
`paged`: the slot and mod lists take their natural height, keep no inner
|
||||
scroll region and report a max of 0, so the wheel, the right stick and a drag
|
||||
all move the page and never fight a list for the same gesture. Two-column
|
||||
layouts do not overflow, `paged` stays false, and every one of these behaves
|
||||
exactly as it did before.
|
||||
- Hit testing follows the clip: `inside` (clicks) and `_ptIn` (hover) reject a
|
||||
rect that scrolled out of the viewport, so a control that slid under the tab
|
||||
bar cannot be clicked through it. Tab chips carry `pinned = true` and are
|
||||
exempt. `pageScroll` resets on a tab change, each tab being a different
|
||||
length.
|
||||
- A press on empty background pans the page, resolved in `_updateSlotDrag` like
|
||||
every other drag here.
|
||||
|
||||
### Dragging on Android
|
||||
|
||||
The launcher is handed no move events on any platform: `main.lua` forwards
|
||||
neither `touchmoved` nor `mousemoved` while it is up, which is why every drag
|
||||
here is resolved by polling inside `draw` instead. Desktop polls the mouse;
|
||||
Android used to poll nothing at all ("no reliable pointer polling" meant its
|
||||
mouse emulation), so it had no scroll gesture whatsoever -- fine while every
|
||||
scroll region was an inner list with a wheel alternative, useless the moment
|
||||
the page itself became the thing that scrolls, since a phone is exactly where
|
||||
it overflows.
|
||||
|
||||
`love.touch` is pollable, so `_pointerHold` reads the first active touch there
|
||||
and hands `_updateSlotDrag` the same (held, y) pair the mouse gives on desktop.
|
||||
Consequences:
|
||||
|
||||
- Slot rows and mod toggles ARM on press and commit on release on Android too,
|
||||
matching desktop, so a swipe that starts on a card scrolls instead of
|
||||
selecting the row it started on.
|
||||
- `touchPollable` (set once in `new`) gates all of it. Where `love.touch` is
|
||||
missing, every Android path is exactly what it was: act on press, never arm,
|
||||
no drag.
|
||||
|
||||
+285
-77
@@ -490,6 +490,14 @@ function RomImporter.new(onComplete, opts)
|
||||
forceImport = opts.forceImport or false,
|
||||
onEditSave = opts.onEditSave,
|
||||
android = android,
|
||||
-- Android drag: the launcher is handed no move events at all (main.lua
|
||||
-- forwards neither touchmoved nor mousemoved while it is up), and its mouse
|
||||
-- emulation is what "no reliable pointer polling" below refers to.
|
||||
-- love.touch IS pollable, so where it exists a touch drag can be resolved
|
||||
-- inside draw the same way the desktop mouse is. Where it does not, every
|
||||
-- Android path stays exactly as it was: act on press, never arm.
|
||||
touchPollable = android and love.touch ~= nil
|
||||
and love.touch.getTouches ~= nil and love.touch.getPosition ~= nil,
|
||||
tab = "red", -- active launcher tab: "red"/"blue"/"yellow"/"mods"
|
||||
logo = love.graphics.newImage("assets/logo/logo.png"),
|
||||
bcg = love.graphics.newImage("assets/logo/bcg.png"),
|
||||
@@ -515,6 +523,10 @@ function RomImporter.new(onComplete, opts)
|
||||
-- modScroll is the list scroll offset (px, clamped in draw); modNotice is
|
||||
-- the last install/delete result { ok, text } shown as a line above the list.
|
||||
mods = nil, modScroll = 0, modNotice = nil,
|
||||
-- Page scroll offset (px) for the column under the tab bar -- panel, updater
|
||||
-- banner and footer -- used only while that column is taller than the window
|
||||
-- (see draw()). Clamped against content in draw, reset on a tab change.
|
||||
pageScroll = 0,
|
||||
-- Android SAF: which game tab should receive the next picked_save.sav when
|
||||
-- focus consumes it (set by chooseSaveImport before opening the picker).
|
||||
androidPendingVersion = nil,
|
||||
@@ -1111,12 +1123,16 @@ function RomImporter:_updatePadCursor(dt)
|
||||
self._padCursor.y = math.max(0, math.min(h, ny))
|
||||
end
|
||||
|
||||
-- Right stick scrolls the active list (save slots or mods).
|
||||
-- Right stick scrolls the active list (save slots or mods), or the whole page
|
||||
-- when it is the thing that overflows.
|
||||
local ry = self._padAxis.righty or 0
|
||||
if math.abs(ry) > PAD_DEAD then
|
||||
self:_activatePadCursor()
|
||||
local step = -ry * 480 * dt
|
||||
if self.tab == "mods" then
|
||||
local maxPage = self._pageMax or 0
|
||||
if maxPage > 0 then
|
||||
self.pageScroll = math.max(0, math.min(maxPage, (self.pageScroll or 0) + step))
|
||||
elseif self.tab == "mods" then
|
||||
local maxS = self._modMax or 0
|
||||
if maxS > 0 then
|
||||
local next = (self.modScroll or 0) + step
|
||||
@@ -1381,6 +1397,24 @@ local function roundedCard(x, y, w, h, r)
|
||||
love.graphics.rectangle("line", x, y, w, h, r, r)
|
||||
end
|
||||
|
||||
-- {top, bottom} of the scrolling page viewport, or nil while the page fits and
|
||||
-- nothing scrolls. Written once per frame by draw(); read by the two hit tests
|
||||
-- (`inside` for clicks, `_ptIn` for hover) so a control scrolled out from under
|
||||
-- the pinned header, or past the window bottom, stops responding at the moment
|
||||
-- it stops being visible. Rects that live in the pinned header carry
|
||||
-- `pinned = true` and are exempt.
|
||||
local pageBand = nil
|
||||
|
||||
-- Page-scroll arithmetic, kept pure (no love, no self) so the engine tier can
|
||||
-- pin it: given how tall the column under the tab bar wants to be and how much
|
||||
-- room is left under it, say whether the page scrolls, where it sits, and how
|
||||
-- far it can go. A window that grew back pulls the offset down with it rather
|
||||
-- than leaving the page parked past its own end.
|
||||
function RomImporter.pageScrollFor(naturalH, viewportH, scroll)
|
||||
local maxPage = math.max(0, (naturalH or 0) - math.max(0, viewportH or 0))
|
||||
return maxPage > 0, clamp(scroll or 0, 0, maxPage), maxPage
|
||||
end
|
||||
|
||||
function RomImporter:draw()
|
||||
local width, height = love.graphics.getDimensions()
|
||||
local s = clamp(height / 768, 0.7, 1.6)
|
||||
@@ -1538,17 +1572,16 @@ function RomImporter:draw()
|
||||
end
|
||||
|
||||
-- Footer (Boi's Club Games logo + trust warning), measured first so the
|
||||
-- content region knows where it must stop. Drawn near the end.
|
||||
-- content region knows where it must stop. Only its height is fixed here:
|
||||
-- it is laid out from a top edge further down, which is the window bottom
|
||||
-- while the page fits and the end of the scrolled content when it does not.
|
||||
local warningWidth = math.min(appW - 32 * s, 640 * s)
|
||||
local _, warningLines = self.warningFont:getWrap(TRUST_WARNING, warningWidth)
|
||||
local warningH = #warningLines * self.warningFont:getHeight()
|
||||
local warningY = height - warningH - 12 * s
|
||||
local bcgW, bcgH = self.bcg:getDimensions()
|
||||
local bcgScale = math.min(math.min(appW - 48 * s, 190 * s) / bcgW, height * 0.06 / bcgH)
|
||||
local bcgDW, bcgDH = bcgW * bcgScale, bcgH * bcgScale
|
||||
local bcgX, bcgY = appX + (appW - bcgDW) / 2, warningY - bcgDH - 6 * s
|
||||
self.bcgButton = { x = bcgX, y = bcgY, width = bcgDW, height = bcgDH }
|
||||
local footerTop = bcgY - 10 * s
|
||||
local footerH = 10 * s + bcgDH + 6 * s + warningH + 12 * s
|
||||
|
||||
-- Logo: centred over the strip, width clamped, gentle bob + glow pulse. The
|
||||
-- resting metrics fix the tab bar's top so the layout never shifts as it bobs.
|
||||
@@ -1584,36 +1617,53 @@ function RomImporter:draw()
|
||||
-- Content region: from below the tab bar down to the footer, minus the
|
||||
-- updater band when one is showing.
|
||||
local contentTop = tabBarY + tabBarH + 16 * s
|
||||
local contentBottom = footerTop - (bannerActive and (bannerH + 20 * s) or 6 * s)
|
||||
local bannerBand = bannerActive and (bannerH + 20 * s) or 6 * s
|
||||
local cX = appX + padH
|
||||
local cW = appW - 2 * padH
|
||||
local contentBottom = height - footerH - bannerBand
|
||||
local cH = math.max(0, contentBottom - contentTop)
|
||||
|
||||
-- tab bar (rebuilds self.tabRects)
|
||||
-- Page scroll. Everything under the tab bar -- panel, updater banner and
|
||||
-- footer -- is one column: too short a window scrolls it instead of letting
|
||||
-- the panel run under a footer pinned to the window bottom (a stacked
|
||||
-- single-column layout on a phone-shaped window overflows by a card or two).
|
||||
-- The panels report their natural height as they draw, so the decision reads
|
||||
-- the previous frame's measurement, the same one-frame settle the slot and
|
||||
-- mod lists already rely on. While the page fits, `paged` is false and every
|
||||
-- measurement below is what it always was.
|
||||
local viewportH = math.max(0, height - contentTop)
|
||||
self._panelNaturalH = self._panelNaturalH or {}
|
||||
local naturalH = (self._panelNaturalH[self.tab] or 0) + bannerBand + footerH
|
||||
local paged, pageScroll, maxPage =
|
||||
RomImporter.pageScrollFor(naturalH, viewportH, self.pageScroll)
|
||||
self.pageScroll, self._pageMax = pageScroll, maxPage
|
||||
-- read by the hit tests; a scrolled control is live only inside the viewport
|
||||
pageBand = paged and { contentTop, height } or nil
|
||||
|
||||
-- tab bar (rebuilds self.tabRects). Pinned: it is the launcher's navigation,
|
||||
-- and it sits above the scrolling viewport.
|
||||
self:_drawTabBar(cX, tabBarY, cW, tabBarH, chip)
|
||||
|
||||
-- content: game panel for a version tab, mods panel for the mods tab
|
||||
if self.tab == "mods" then
|
||||
self:_drawModsPanel(cX, contentTop, cW, cH)
|
||||
else
|
||||
self:_drawGamePanel(self.tab, cX, contentTop, cW, cH)
|
||||
local panelY = contentTop - (paged and self.pageScroll or 0)
|
||||
if paged then
|
||||
love.graphics.setScissor(math.floor(appX), math.floor(contentTop),
|
||||
math.ceil(appW), math.ceil(viewportH))
|
||||
end
|
||||
|
||||
-- logo, over the split, with a gentle bob + gold glow + sweeping shine
|
||||
local bob = math.sin(pulse * (2 * math.pi / 4)) * 6 * s
|
||||
local lx, ly = (width - logoDW) / 2, logoY + bob
|
||||
love.graphics.setBlendMode("add")
|
||||
love.graphics.setColor(1, 0.85, 0.2, 0.16 + 0.12 * (0.5 + 0.5 * math.sin(pulse * 1.6)))
|
||||
love.graphics.draw(self.logo, (width - logoDW * 1.05) / 2, ly - logoDH * 0.025, 0,
|
||||
logoScale * 1.05, logoScale * 1.05)
|
||||
love.graphics.setBlendMode("alpha")
|
||||
local shineW = 0.16
|
||||
self.shineShader:send("shinePos", -shineW + ((pulse % 2.8) / 2.8) * (1 + 2 * shineW))
|
||||
self.shineShader:send("shineW", shineW)
|
||||
love.graphics.setShader(self.shineShader)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(self.logo, lx, ly, 0, logoScale, logoScale)
|
||||
love.graphics.setShader()
|
||||
-- content: game panel for a version tab, mods panel for the mods tab
|
||||
local panelH
|
||||
if self.tab == "mods" then
|
||||
panelH = self:_drawModsPanel(cX, panelY, cW, cH, paged)
|
||||
else
|
||||
panelH = self:_drawGamePanel(self.tab, cX, panelY, cW, cH, paged)
|
||||
end
|
||||
panelH = panelH or 0
|
||||
self._panelNaturalH[self.tab] = panelH
|
||||
|
||||
-- The updater band and the footer follow the content: pinned to the window
|
||||
-- bottom while the page fits, riding at the end of the scroll when it does not.
|
||||
local bandTop = paged and (panelY + panelH) or contentBottom
|
||||
local footerTop = bandTop + bannerBand
|
||||
|
||||
-- Self-updater banner: a compact pill centred in the reserved band just above
|
||||
-- the footer, on every tab. Same green "Play" treatment on its CTA.
|
||||
@@ -1621,7 +1671,7 @@ function RomImporter:draw()
|
||||
if bannerActive then
|
||||
local bannerW = math.min(appW - 32 * s, 560 * s)
|
||||
local bx = appX + (appW - bannerW) / 2
|
||||
local by = contentBottom + math.max(0, (footerTop - contentBottom - bannerH) / 2)
|
||||
local by = bandTop + math.max(0, (footerTop - bandTop - bannerH) / 2)
|
||||
local r = 12 * s
|
||||
local accent = PAL.gold
|
||||
|
||||
@@ -1702,11 +1752,17 @@ function RomImporter:draw()
|
||||
end
|
||||
|
||||
-- footer: a hairline top border, the BCG mark (inverted to white, glowing
|
||||
-- brighter on hover) + the trust warning with its live bois.icu link.
|
||||
-- brighter on hover) + the trust warning with its live bois.icu link. Laid
|
||||
-- out downward from footerTop, so the same code serves the pinned and the
|
||||
-- scrolled position.
|
||||
love.graphics.setLineWidth(1)
|
||||
col(PAL.cardBorder, 0.18)
|
||||
love.graphics.line(appX + padH, footerTop, appX + appW - padH, footerTop)
|
||||
|
||||
local bcgX, bcgY = appX + (appW - bcgDW) / 2, footerTop + 10 * s
|
||||
local warningY = bcgY + bcgDH + 6 * s
|
||||
self.bcgButton = { x = bcgX, y = bcgY, width = bcgDW, height = bcgDH }
|
||||
|
||||
local bcgHot = self:_hover(self.bcgButton)
|
||||
love.graphics.setShader(self.invertShader)
|
||||
love.graphics.setBlendMode("add")
|
||||
@@ -1745,6 +1801,35 @@ function RomImporter:draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- End of the scrolling column; the logo and the page scrollbar are pinned and
|
||||
-- draw outside it.
|
||||
if paged then love.graphics.setScissor() end
|
||||
|
||||
-- logo, over the split, with a gentle bob + gold glow + sweeping shine
|
||||
local bob = math.sin(pulse * (2 * math.pi / 4)) * 6 * s
|
||||
local lx, ly = (width - logoDW) / 2, logoY + bob
|
||||
love.graphics.setBlendMode("add")
|
||||
love.graphics.setColor(1, 0.85, 0.2, 0.16 + 0.12 * (0.5 + 0.5 * math.sin(pulse * 1.6)))
|
||||
love.graphics.draw(self.logo, (width - logoDW * 1.05) / 2, ly - logoDH * 0.025, 0,
|
||||
logoScale * 1.05, logoScale * 1.05)
|
||||
love.graphics.setBlendMode("alpha")
|
||||
local shineW = 0.16
|
||||
self.shineShader:send("shinePos", -shineW + ((pulse % 2.8) / 2.8) * (1 + 2 * shineW))
|
||||
self.shineShader:send("shineW", shineW)
|
||||
love.graphics.setShader(self.shineShader)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(self.logo, lx, ly, 0, logoScale, logoScale)
|
||||
love.graphics.setShader()
|
||||
|
||||
-- page scrollbar: the same thin thumb the lists use, against the app edge
|
||||
if paged then
|
||||
local thumbH = math.max(24 * s, viewportH * (viewportH / naturalH))
|
||||
local thumbY = contentTop + (viewportH - thumbH) * (self.pageScroll / maxPage)
|
||||
col(PAL.cardBorder, 0.35)
|
||||
love.graphics.rectangle("fill", appX + appW - padH * 0.5, thumbY, 3 * s, thumbH,
|
||||
1.5 * s, 1.5 * s)
|
||||
end
|
||||
|
||||
-- CRT scanlines + vignette, over everything
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(self.scanlineImage, self.scanlineQuad, 0, 0)
|
||||
@@ -1840,11 +1925,22 @@ function RomImporter:draw()
|
||||
end
|
||||
|
||||
local function inside(r, x, y)
|
||||
return r and x >= r.x and x <= r.x + r.width and y >= r.y and y <= r.y + r.height
|
||||
if not (r and x >= r.x and x <= r.x + r.width and y >= r.y and y <= r.y + r.height) then
|
||||
return false
|
||||
end
|
||||
-- Page-scroll mode: only the header is pinned, so any other rect is a
|
||||
-- scrolled one and is live only where the viewport actually shows it.
|
||||
if pageBand and not r.pinned and (y < pageBand[1] or y > pageBand[2]) then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function RomImporter:mousepressed(x, y, button)
|
||||
if self._rename then return end -- the rename modal swallows all clicks
|
||||
-- Whether a press can be ARMED and resolved on release, which needs a
|
||||
-- pollable pointer: always on desktop, on Android only where love.touch is.
|
||||
local armDrag = (not self.android) or self.touchPollable
|
||||
-- right-click a save-slot row to rename it (#205); desktop only (touch
|
||||
-- has no secondary button)
|
||||
if button == 2 then
|
||||
@@ -1883,6 +1979,10 @@ function RomImporter:mousepressed(x, y, button)
|
||||
self.tab = t.id
|
||||
self._slotPress = nil -- drop any half-started slot drag on tab change
|
||||
self._modPress = nil -- and any half-started mod toggle press
|
||||
self._pagePress = nil -- and any half-started page pan
|
||||
-- Each tab is its own column of a different length; carrying one tab's
|
||||
-- offset into another lands somewhere arbitrary.
|
||||
self.pageScroll = 0
|
||||
return
|
||||
end
|
||||
end
|
||||
@@ -1911,11 +2011,12 @@ function RomImporter:mousepressed(x, y, button)
|
||||
return
|
||||
end
|
||||
-- SAVE SLOT rows / Edit / Delete. The two labels are checked first so a tap
|
||||
-- on either never also selects the row. On desktop a press only ARMS a row
|
||||
-- click: _updateSlotDrag commits it on release when the pointer did not move
|
||||
-- (a moved pointer scrolls instead). Android has no reliable pointer
|
||||
-- polling, so it selects on press. Edit and Delete fire immediately (small
|
||||
-- fixed targets, no scroll conflict).
|
||||
-- on either never also selects the row. A press only ARMS a row click:
|
||||
-- _updateSlotDrag commits it on release when the pointer did not move (a
|
||||
-- moved pointer scrolls instead). Android arms too wherever love.touch can
|
||||
-- be polled; without that there is nothing to resolve a release with, so it
|
||||
-- keeps selecting on press. Edit and Delete fire immediately (small fixed
|
||||
-- targets, no scroll conflict).
|
||||
for _, r in ipairs(self.slotDeleteRects or {}) do
|
||||
if inside(r, x, y) then
|
||||
self:_deleteSlot(self.panelVersion, r.id)
|
||||
@@ -1930,11 +2031,12 @@ function RomImporter:mousepressed(x, y, button)
|
||||
end
|
||||
for _, r in ipairs(self.slotRects or {}) do
|
||||
if inside(r, x, y) then
|
||||
if self.android then
|
||||
if not armDrag then
|
||||
self:_selectSlot(self.panelVersion, r.id)
|
||||
else
|
||||
self._slotPress = { version = self.panelVersion, id = r.id, y0 = y,
|
||||
scroll0 = self.slotScroll[self.panelVersion] or 0, moved = false }
|
||||
scroll0 = self.slotScroll[self.panelVersion] or 0,
|
||||
pageScroll0 = self.pageScroll or 0, moved = false }
|
||||
end
|
||||
return
|
||||
end
|
||||
@@ -1957,15 +2059,20 @@ function RomImporter:mousepressed(x, y, button)
|
||||
end
|
||||
for _, r in ipairs(self.modRects or {}) do
|
||||
if inside(r, x, y) then
|
||||
if self.android then
|
||||
if not armDrag then
|
||||
self:_toggleMod(r.id)
|
||||
else
|
||||
self._modPress = { id = r.id, y0 = y,
|
||||
scroll0 = self.modScroll or 0, moved = false }
|
||||
self._modPress = { id = r.id, y0 = y, scroll0 = self.modScroll or 0,
|
||||
pageScroll0 = self.pageScroll or 0, moved = false }
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
-- Nothing was hit. On a scrolling page that is a press on empty background,
|
||||
-- which is the natural place to grab and pan from.
|
||||
if armDrag and (self._pageMax or 0) > 0 then
|
||||
self._pagePress = { y0 = y, scroll0 = self.pageScroll or 0 }
|
||||
end
|
||||
end
|
||||
|
||||
function RomImporter:keypressed(key)
|
||||
@@ -1997,7 +2104,14 @@ end
|
||||
|
||||
function RomImporter:_ptIn(r)
|
||||
local mx, my = self._mx, self._my
|
||||
return r and mx >= r.x and mx <= r.x + r.width and my >= r.y and my <= r.y + r.height
|
||||
if not (r and mx >= r.x and mx <= r.x + r.width and my >= r.y and my <= r.y + r.height) then
|
||||
return false
|
||||
end
|
||||
-- Same clip the click path applies, so nothing glows outside the viewport.
|
||||
if pageBand and not r.pinned and (my < pageBand[1] or my > pageBand[2]) then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function RomImporter:_hover(r)
|
||||
@@ -2126,8 +2240,9 @@ function RomImporter:_drawTabBar(x, y, w, h, chip)
|
||||
col(PAL.bgBot, 0.62)
|
||||
love.graphics.rectangle("fill", cursorX, chipY, chip, chip, r, r)
|
||||
end
|
||||
-- pinned: the tab bar never scrolls, so it stays live above the viewport
|
||||
self.tabRects[#self.tabRects + 1] =
|
||||
{ x = cursorX, y = chipY, width = chip, height = chip, id = t.id }
|
||||
{ x = cursorX, y = chipY, width = chip, height = chip, id = t.id, pinned = true }
|
||||
local segEnd = cursorX + chip
|
||||
if active then
|
||||
love.graphics.setFont(self.tabLabelFont)
|
||||
@@ -2159,7 +2274,12 @@ end
|
||||
|
||||
-- One version's game panel: header (name + status pill), then a responsive
|
||||
-- two-column grid (left: ROM + SAVE FILES cards + Play; right: SAVE SLOT).
|
||||
function RomImporter:_drawGamePanel(version, x, y, w, h)
|
||||
-- `paged`: the whole page is scrolling (see draw()), so nothing stretches to
|
||||
-- fill `h` -- Play sits right under the SAVE FILES card instead of being pinned
|
||||
-- to the column bottom, and the slot card takes its natural height. Returns
|
||||
-- the panel's natural height either way, which is what draw() measures the page
|
||||
-- against on the next frame.
|
||||
function RomImporter:_drawGamePanel(version, x, y, w, h, paged)
|
||||
local s, pulse = self._s, self.pulse
|
||||
self.panelVersion = version
|
||||
-- Defensive: only lock when the version is absent from GameVersion (never
|
||||
@@ -2294,8 +2414,9 @@ function RomImporter:_drawGamePanel(version, x, y, w, h)
|
||||
-- vertical placement of the left column
|
||||
local romY = bodyTop
|
||||
local saveFilesY = romY + romCardH + 12 * s
|
||||
local leftNaturalH = romCardH + 12 * s + saveFilesH + 12 * s + playH
|
||||
local playY
|
||||
if twoCol then
|
||||
if twoCol and not paged then
|
||||
playY = bodyTop + bodyH - playH -- pinned to the column's bottom
|
||||
else
|
||||
playY = saveFilesY + saveFilesH + 12 * s
|
||||
@@ -2372,15 +2493,29 @@ function RomImporter:_drawGamePanel(version, x, y, w, h)
|
||||
|
||||
-- SAVE SLOT card (right column, or stacked below Play when single-column).
|
||||
-- Skip only when the version is absent from GameVersion (no save backend).
|
||||
local slotNaturalH = 0
|
||||
if not locked then
|
||||
if twoCol then
|
||||
self:_drawSaveSlotPanel(version, rightX, bodyTop, colW, bodyH)
|
||||
_, slotNaturalH = self:_drawSaveSlotPanel(version, rightX, bodyTop, colW, bodyH, paged)
|
||||
else
|
||||
local slotY = playY + playH + 12 * s
|
||||
local slotH = math.max(160 * s, (bodyTop + bodyH) - slotY)
|
||||
self:_drawSaveSlotPanel(version, leftX, slotY, colW, slotH)
|
||||
_, slotNaturalH = self:_drawSaveSlotPanel(version, leftX, slotY, colW, slotH, paged)
|
||||
end
|
||||
end
|
||||
|
||||
-- Natural height: side by side the two columns overlap, stacked they add up.
|
||||
-- Measured from the panel's own top (y), so draw() can compare it against the
|
||||
-- viewport without knowing anything about the cards inside.
|
||||
local bodyNaturalH
|
||||
if twoCol then
|
||||
bodyNaturalH = math.max(leftNaturalH, slotNaturalH)
|
||||
elseif locked then
|
||||
bodyNaturalH = leftNaturalH
|
||||
else
|
||||
bodyNaturalH = leftNaturalH + 12 * s + slotNaturalH
|
||||
end
|
||||
return (bodyTop - y) + bodyNaturalH
|
||||
end
|
||||
|
||||
-- Reload a version's slot list + active id from SaveData (the source of truth).
|
||||
@@ -2457,17 +2592,53 @@ end
|
||||
-- launcher, so a press only ARMS a click (see mousepressed) and this resolves
|
||||
-- it: a pointer that moved past the threshold scrolls; one that did not, on
|
||||
-- release, selects. Desktop only -- Android selects on press instead.
|
||||
-- Where the pointer is this frame and whether it is held, read by polling
|
||||
-- because no move event ever reaches the launcher: the mouse on desktop, the
|
||||
-- first active touch on Android. A nil y means "nothing to read" -- the
|
||||
-- release branches below do not need one.
|
||||
function RomImporter:_pointerHold()
|
||||
if not self.android then return love.mouse.isDown(1), self._my end
|
||||
if not self.touchPollable then return false, nil end
|
||||
local ok, list = pcall(love.touch.getTouches)
|
||||
if not ok or type(list) ~= "table" or list[1] == nil then return false, nil end
|
||||
local ok2, _, ty = pcall(love.touch.getPosition, list[1])
|
||||
if not ok2 or type(ty) ~= "number" then return false, nil end
|
||||
return true, ty
|
||||
end
|
||||
|
||||
function RomImporter:_updateSlotDrag()
|
||||
if self.android then return end
|
||||
local down = love.mouse.isDown(1)
|
||||
if self.android and not self.touchPollable then return end
|
||||
local down, py = self:_pointerHold()
|
||||
py = py or self._my
|
||||
local maxPage = self._pageMax or 0
|
||||
|
||||
-- A press on empty background pans the page while it overflows. Nothing is
|
||||
-- armed by it, so there is no release action to resolve.
|
||||
local pp = self._pagePress
|
||||
if pp then
|
||||
if down then
|
||||
if maxPage > 0 then
|
||||
self.pageScroll = clamp(pp.scroll0 - (py - pp.y0), 0, maxPage)
|
||||
end
|
||||
else
|
||||
self._pagePress = nil
|
||||
end
|
||||
end
|
||||
|
||||
local p = self._slotPress
|
||||
if p then
|
||||
if down then
|
||||
local d = self._my - p.y0
|
||||
local d = py - p.y0
|
||||
if math.abs(d) > 4 * (self._s or 1) then p.moved = true end
|
||||
if p.moved then
|
||||
local maxS = (self._slotMax and self._slotMax[p.version]) or 0
|
||||
self.slotScroll[p.version] = clamp(p.scroll0 - d, 0, maxS)
|
||||
-- Paged, the list has no scroll of its own: the drag pans the page, so
|
||||
-- a swipe that starts on a slot row behaves like one starting beside it.
|
||||
if maxPage > 0 then
|
||||
self.pageScroll = clamp(p.pageScroll0 - d, 0, maxPage)
|
||||
else
|
||||
local maxS = (self._slotMax and self._slotMax[p.version]) or 0
|
||||
self.slotScroll[p.version] = clamp(p.scroll0 - d, 0, maxS)
|
||||
end
|
||||
end
|
||||
else
|
||||
if not p.moved then self:_selectSlot(p.version, p.id) end
|
||||
@@ -2479,10 +2650,14 @@ function RomImporter:_updateSlotDrag()
|
||||
local mp = self._modPress
|
||||
if mp then
|
||||
if down then
|
||||
local d = self._my - mp.y0
|
||||
local d = py - mp.y0
|
||||
if math.abs(d) > 4 * (self._s or 1) then mp.moved = true end
|
||||
if mp.moved then
|
||||
self.modScroll = clamp(mp.scroll0 - d, 0, self._modMax or 0)
|
||||
if maxPage > 0 then
|
||||
self.pageScroll = clamp(mp.pageScroll0 - d, 0, maxPage)
|
||||
else
|
||||
self.modScroll = clamp(mp.scroll0 - d, 0, self._modMax or 0)
|
||||
end
|
||||
end
|
||||
else
|
||||
if not mp.moved then self:_toggleMod(mp.id) end
|
||||
@@ -2496,6 +2671,13 @@ end
|
||||
-- content extent draw computed for that version.
|
||||
function RomImporter:wheelmoved(_, dy)
|
||||
local step = 48 * (self._s or 1)
|
||||
-- An overflowing page scrolls as a whole; the panels' own lists are flattened
|
||||
-- in that mode, so there is never a second scroll region competing for this.
|
||||
local maxPage = self._pageMax or 0
|
||||
if maxPage > 0 then
|
||||
self.pageScroll = clamp((self.pageScroll or 0) - dy * step, 0, maxPage)
|
||||
return
|
||||
end
|
||||
if self.tab == "mods" then
|
||||
local maxS = self._modMax or 0
|
||||
if maxS <= 0 then return end
|
||||
@@ -2512,15 +2694,35 @@ end
|
||||
-- SAVE SLOT card: header ("SAVE SLOT" + "N slots"), a scrollable list of slot
|
||||
-- rows (name + meta, LOADED pill on the active one), and a dashed "+ New save
|
||||
-- slot" button pinned to the bottom. Empty registries show a dashed hint box.
|
||||
function RomImporter:_drawSaveSlotPanel(version, x, y, w, h)
|
||||
-- `paged` (the whole launcher page is scrolling, see draw()) drops the inner
|
||||
-- scroll region: the card grows to its natural height, every row is drawn, and
|
||||
-- the page's own scrollbar is the only one on screen. Returns the height the
|
||||
-- card actually took, which is what the caller measures the page against.
|
||||
function RomImporter:_drawSaveSlotPanel(version, x, y, w, h, paged)
|
||||
local s = self._s
|
||||
local pad = 16 * s
|
||||
roundedCard(x, y, w, h, 16 * s)
|
||||
self:_ensureSlots(version)
|
||||
local slots = self.slots[version] or {}
|
||||
local active = self.activeSlot[version]
|
||||
local n = #slots
|
||||
|
||||
-- Row metrics up front: the natural height needs them, and the natural height
|
||||
-- decides the card's height before anything is drawn.
|
||||
local labelH = self.labelFont:getHeight()
|
||||
local newBtnH = math.max(38 * s, self.saveBtnFont:getHeight() + 18 * s)
|
||||
local nameH = self.slotNameFont:getHeight()
|
||||
local metaH = self.labelFont:getHeight()
|
||||
local rowPadV = 10 * s
|
||||
local rowH = rowPadV * 2 + nameH + 4 * s + metaH
|
||||
local rowGap = 8 * s
|
||||
local rr = 12 * s
|
||||
-- an empty registry shows a fixed-height dashed hint box instead of rows
|
||||
local totalH = (n > 0) and (n * rowH + (n - 1) * rowGap) or (96 * s)
|
||||
local naturalH = pad + labelH + 12 * s + totalH + 10 * s + newBtnH + pad
|
||||
if paged then h = naturalH end
|
||||
|
||||
roundedCard(x, y, w, h, 16 * s)
|
||||
|
||||
-- header: "SAVE SLOT" (left) + "N slots" / "1 slot" (right)
|
||||
love.graphics.setFont(self.labelFont)
|
||||
col(PAL.labelGray)
|
||||
@@ -2529,11 +2731,9 @@ function RomImporter:_drawSaveSlotPanel(version, x, y, w, h)
|
||||
local cw = self.labelFont:getWidth(countTxt)
|
||||
love.graphics.print(countTxt, x + w - pad - cw, y + pad)
|
||||
|
||||
local labelH = self.labelFont:getHeight()
|
||||
local listTop = y + pad + labelH + 12 * s
|
||||
|
||||
-- "+ New save slot" pinned to the card bottom; the list fills the gap above.
|
||||
local newBtnH = math.max(38 * s, self.saveBtnFont:getHeight() + 18 * s)
|
||||
local newBtnY = y + h - pad - newBtnH
|
||||
local listBottom = newBtnY - 10 * s
|
||||
local listH = math.max(0, listBottom - listTop)
|
||||
@@ -2553,16 +2753,10 @@ function RomImporter:_drawSaveSlotPanel(version, x, y, w, h)
|
||||
self.slotDeleteRects = {}
|
||||
self.slotEditRects = {}
|
||||
elseif listH > 0 then
|
||||
local nameH = self.slotNameFont:getHeight()
|
||||
local metaH = self.labelFont:getHeight()
|
||||
local rowPadV = 10 * s
|
||||
local rowH = rowPadV * 2 + nameH + 4 * s + metaH
|
||||
local rowGap = 8 * s
|
||||
local rr = 12 * s
|
||||
|
||||
-- clamp scroll against the current content extent, and stash the max so the
|
||||
-- wheel handler (which has no geometry) can clamp against the same value
|
||||
local totalH = n * rowH + (n - 1) * rowGap
|
||||
-- wheel handler (which has no geometry) can clamp against the same value.
|
||||
-- Paged, listH already equals totalH, so this is 0 and the wheel falls
|
||||
-- through to the page scroll.
|
||||
local maxScroll = math.max(0, totalH - listH)
|
||||
self._slotMax = self._slotMax or {}
|
||||
self._slotMax[version] = maxScroll
|
||||
@@ -2572,8 +2766,12 @@ function RomImporter:_drawSaveSlotPanel(version, x, y, w, h)
|
||||
self.slotRects = {}
|
||||
self.slotDeleteRects = {}
|
||||
self.slotEditRects = {}
|
||||
love.graphics.setScissor(math.floor(rx), math.floor(listTop),
|
||||
math.ceil(rw), math.ceil(listH))
|
||||
-- Paged, the page viewport's scissor is already set and nothing here
|
||||
-- overflows the card, so leave it alone rather than replace and clear it.
|
||||
if not paged then
|
||||
love.graphics.setScissor(math.floor(rx), math.floor(listTop),
|
||||
math.ceil(rw), math.ceil(listH))
|
||||
end
|
||||
for i, slot in ipairs(slots) do
|
||||
local ry = listTop - scroll + (i - 1) * (rowH + rowGap)
|
||||
if ry + rowH >= listTop and ry <= listBottom then
|
||||
@@ -2676,7 +2874,7 @@ function RomImporter:_drawSaveSlotPanel(version, x, y, w, h)
|
||||
end
|
||||
end
|
||||
end
|
||||
love.graphics.setScissor()
|
||||
if not paged then love.graphics.setScissor() end
|
||||
|
||||
-- thin scrollbar thumb when the list overflows
|
||||
if maxScroll > 0 then
|
||||
@@ -2700,6 +2898,7 @@ function RomImporter:_drawSaveSlotPanel(version, x, y, w, h)
|
||||
printfB("+ New save slot", nrect.x,
|
||||
nrect.y + (newBtnH - self.saveBtnFont:getHeight()) / 2, nrect.width, "center")
|
||||
self.newSlotRect = nrect
|
||||
return h, naturalH
|
||||
end
|
||||
|
||||
-- Reload the mods list from LauncherMods (the source of truth: it reads the
|
||||
@@ -2762,7 +2961,10 @@ end
|
||||
-- install-result / drag-drop notice line, then a scrollable list of mod cards
|
||||
-- (name + badge chip + description, a status chip, and a toggle switch). An
|
||||
-- empty install shows a friendly dashed hint box.
|
||||
function RomImporter:_drawModsPanel(x, y, w, h)
|
||||
-- `paged` behaves as it does on the game panel: no inner scroll region, the
|
||||
-- card list is drawn whole, and the returned natural height is what draw()
|
||||
-- measures the page against.
|
||||
function RomImporter:_drawModsPanel(x, y, w, h, paged)
|
||||
local s = self._s
|
||||
self:_ensureMods()
|
||||
local mods = self.mods or {}
|
||||
@@ -2807,7 +3009,7 @@ function RomImporter:_drawModsPanel(x, y, w, h)
|
||||
|
||||
-- empty state: a dashed box with a centred hint
|
||||
if #mods == 0 then
|
||||
local boxH = math.min(listH, 120 * s)
|
||||
local boxH = paged and (120 * s) or math.min(listH, 120 * s)
|
||||
love.graphics.setLineWidth(math.max(1, 1 * s))
|
||||
col(PAL.cardBorder, 0.45)
|
||||
dashedRoundRect(x, top, w, boxH, 14 * s, 7 * s, 5 * s)
|
||||
@@ -2821,7 +3023,7 @@ function RomImporter:_drawModsPanel(x, y, w, h)
|
||||
self.modRects = {}
|
||||
self.modDeleteRects = {}
|
||||
self._modMax = 0
|
||||
return
|
||||
return (top - y) + boxH
|
||||
end
|
||||
|
||||
-- card metrics (design: rounded 14, padding 14x16; toggle 56x28; Delete under)
|
||||
@@ -2858,6 +3060,9 @@ function RomImporter:_drawModsPanel(x, y, w, h)
|
||||
end
|
||||
total = total + (#mods - 1) * cardGap
|
||||
|
||||
-- Paged, the list band is the list itself: nothing to clip, nothing to scroll
|
||||
-- here, and the page's scrollbar covers the overflow.
|
||||
if paged then listH = total end
|
||||
local maxScroll = math.max(0, total - listH)
|
||||
self._modMax = maxScroll
|
||||
local scroll = clamp(self.modScroll or 0, 0, maxScroll)
|
||||
@@ -2865,8 +3070,10 @@ function RomImporter:_drawModsPanel(x, y, w, h)
|
||||
self.modRects = {}
|
||||
self.modDeleteRects = {}
|
||||
|
||||
love.graphics.setScissor(math.floor(x), math.floor(top),
|
||||
math.ceil(w), math.ceil(listH))
|
||||
if not paged then
|
||||
love.graphics.setScissor(math.floor(x), math.floor(top),
|
||||
math.ceil(w), math.ceil(listH))
|
||||
end
|
||||
local cy = top - scroll
|
||||
for i, m in ipairs(mods) do
|
||||
local L = layout[i]
|
||||
@@ -2962,7 +3169,7 @@ function RomImporter:_drawModsPanel(x, y, w, h)
|
||||
end
|
||||
cy = cy + cardH + cardGap
|
||||
end
|
||||
love.graphics.setScissor()
|
||||
if not paged then love.graphics.setScissor() end
|
||||
|
||||
-- thin scrollbar thumb when the list overflows
|
||||
if maxScroll > 0 then
|
||||
@@ -2971,6 +3178,7 @@ function RomImporter:_drawModsPanel(x, y, w, h)
|
||||
col(PAL.cardBorder, 0.35)
|
||||
love.graphics.rectangle("fill", x + w - 3 * s, thumbY, 3 * s, thumbH, 1.5 * s, 1.5 * s)
|
||||
end
|
||||
return (top - y) + total
|
||||
end
|
||||
|
||||
return RomImporter
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
-- Launcher page scroll (src/import/RomImporter.lua): the column under the tab
|
||||
-- bar -- panel, updater banner, footer -- scrolls as one when the window is too
|
||||
-- short to hold it. Before this, a stacked single-column layout on a narrow
|
||||
-- window ran under a footer pinned to the window bottom, and the part below the
|
||||
-- fold could not be reached at all. The arithmetic is pure, so pin it here
|
||||
-- rather than in a screenshot.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local RomImporter = require("src.import.RomImporter")
|
||||
|
||||
local pageScrollFor = RomImporter.pageScrollFor
|
||||
|
||||
-- ------------------------------------------------------------- fits: inert
|
||||
local paged, scroll, maxPage = pageScrollFor(400, 600, 0)
|
||||
T.eq(paged, false, "a column shorter than the viewport does not scroll")
|
||||
T.eq(maxPage, 0, "and has nowhere to scroll to")
|
||||
T.eq(scroll, 0, "and sits at the top")
|
||||
|
||||
-- An exact fit is still not a scroll: one pixel of slack would show a thumb
|
||||
-- for nothing and put the wheel on the page instead of the slot list.
|
||||
paged, _, maxPage = pageScrollFor(600, 600, 0)
|
||||
T.eq(paged, false, "a column exactly as tall as the viewport does not scroll")
|
||||
T.eq(maxPage, 0, "an exact fit has no scroll extent")
|
||||
|
||||
-- --------------------------------------------------------- overflows: scrolls
|
||||
paged, scroll, maxPage = pageScrollFor(900, 600, 0)
|
||||
T.eq(paged, true, "a column taller than the viewport scrolls")
|
||||
T.eq(maxPage, 300, "the extent is exactly the overflow")
|
||||
T.eq(scroll, 0, "a fresh page starts at the top")
|
||||
|
||||
-- The bottom of the travel shows the footer: the whole overflow is reachable,
|
||||
-- which is the point of the change (#footer under the fold).
|
||||
_, scroll = pageScrollFor(900, 600, 300)
|
||||
T.eq(scroll, 300, "the offset can reach the end of the column")
|
||||
_, scroll = pageScrollFor(900, 600, 5000)
|
||||
T.eq(scroll, 300, "an offset past the end clamps to it")
|
||||
_, scroll = pageScrollFor(900, 600, -40)
|
||||
T.eq(scroll, 0, "an offset above the top clamps to it")
|
||||
|
||||
-- ------------------------------------------------------- the window grows back
|
||||
-- Resizing taller has to pull the page back down with it; leaving the offset
|
||||
-- where it was would park the content above the viewport with no way back.
|
||||
_, scroll, maxPage = pageScrollFor(900, 800, 300)
|
||||
T.eq(maxPage, 100, "a taller window leaves less to scroll")
|
||||
T.eq(scroll, 100, "and drags a deeper offset back to the new end")
|
||||
paged, scroll = pageScrollFor(900, 900, 300)
|
||||
T.eq(paged, false, "growing past the content stops the scrolling")
|
||||
T.eq(scroll, 0, "and returns the page to the top")
|
||||
|
||||
-- ---------------------------------------------------------------- degenerate
|
||||
-- draw() computes the viewport from the window height, so a window smaller than
|
||||
-- the pinned header hands this a negative number; it must not become extra
|
||||
-- travel.
|
||||
_, _, maxPage = pageScrollFor(500, -120, 0)
|
||||
T.eq(maxPage, 500, "a negative viewport counts as no room, not as more of it")
|
||||
paged, scroll, maxPage = pageScrollFor(nil, nil, nil)
|
||||
T.eq(paged, false, "a first frame with nothing measured yet does not scroll")
|
||||
T.eq(scroll, 0, "and sits at the top")
|
||||
T.eq(maxPage, 0, "with no extent")
|
||||
|
||||
T.finish("launcher page scroll")
|
||||
Reference in New Issue
Block a user