for that one guy who has a mouse but for some reason it doesnt have a scroll wheel

This commit is contained in:
bryanthaboi
2026-08-20 11:54:08 -04:00
parent 69ef1bfc77
commit 1ac5b867bb
3 changed files with 214 additions and 17 deletions
+85 -12
View File
@@ -124,12 +124,48 @@ function LauncherView.detach(imp)
Kit.clearCaches()
end
local function markNoDrag(imp, x, y, w, h)
if Kit.blockClicks then return end
local t = imp._noDragRects
if not t then t = {}; imp._noDragRects = t end
local n = (imp._noDragN or 0) + 1
imp._noDragN = n
local r = t[n]
if not r then r = {}; t[n] = r end
r.x, r.y, r.w, r.h = x, y, w, h
end
local function noDragAt(imp, x, y)
local rects = imp._noDragRects
for i = 1, imp._noDragN or 0 do
if inRect(rects[i], x, y) then return true end
end
return false
end
local function armMouse(imp, x, y)
if noDragAt(imp, x, y) then
imp._clickPt = { x = x, y = y }
return
end
local shielded = imp._modalUpNow
imp._mouseAt = {
x = x, y = y,
region = not shielded and tabScrollMax(imp) > 0
and inRect(imp._tabRegionRect, x, y) or false,
page = not shielded and (imp._pageScrollMax or 0) > 0 or false,
}
Kit.dragBegin(x, y)
end
-- ---------------------------------------------------------------- input
-- The kit is polled, not evented: update() samples the mouse and turns a
-- rising edge into a click point that the next draw consumes. Host-forwarded
-- mousepressed stays unused, exactly as before, so Android's synthesized
-- mouse path cannot double-fire a tap (#553) -- the dedup window below is the
-- other half of that guarantee.
-- The kit is polled, not evented: update() samples the mouse. A press arms
-- a drag that scrolls like a finger, and the click dispatches on RELEASE so
-- the drag can disqualify it, exactly like the touch path below; only the
-- cartridge (which owns its own spin-drag) keeps the press-down click.
-- Host-forwarded mousepressed stays unused, exactly as before, so Android's
-- synthesized mouse path cannot double-fire a tap (#553) -- the dedup window
-- below is the other half of that guarantee.
function LauncherView.update(imp, dt)
if not imp._flex then return end
if imp._launchFade then return end
@@ -154,7 +190,39 @@ function LauncherView.update(imp, dt)
if not touching and now >= (imp._suppressMouseUntil or 0)
and now >= (imp._suppressClickUntil or 0) then
local mx, my = love.mouse.getPosition()
imp._clickPt = { x = mx, y = my }
armMouse(imp, mx, my)
end
elseif down and imp._mouseAt then
local start = imp._mouseAt
local mx, my = love.mouse.getPosition()
local ddx, ddy = mx - start.x, my - start.y
if ddx * ddx + ddy * ddy > TAP_SLOP2 then
start.dragged = true
end
if start.dragged then
local last = start.lastY or start.y
local move = -(my - last)
if move ~= 0 and start.region then
local at, leftover = Kit.scrollHandoff(tabScrollAt(imp),
tabScrollMax(imp), move)
setTabScroll(imp, at)
move = leftover
end
if move ~= 0 and start.page and (imp._pageScrollMax or 0) > 0 then
local at, leftover = Kit.scrollHandoff(imp._pageScroll or 0,
imp._pageScrollMax, move)
imp._pageScroll = at
move = leftover
end
if move ~= 0 then Kit.dragAdd(move) end
end
start.lastY = my
elseif not down and imp._mouseAt then
local start = imp._mouseAt
imp._mouseAt = nil
Kit.dragEnd()
if not start.dragged then
imp._clickPt = { x = start.x, y = start.y }
end
end
imp._prevMouseDown = down
@@ -233,10 +301,12 @@ function LauncherView.clickAt(imp, x, y)
imp._clickPt = { x = x, y = y }
end
-- Event-driven click: a macOS trackpad tap delivers press+release inside one
-- frame, so update()'s love.mouse.isDown poll never sees it. Mint the click
-- from the press event under the poll's own suppression rules, and mark the
-- press seen so the poll cannot mint a second one when isDown does catch it.
-- Event-driven press: a macOS trackpad tap delivers press+release inside one
-- frame, so update()'s love.mouse.isDown poll never sees it. Arm the drag
-- from the press event under the poll's own suppression rules -- the poll's
-- release branch then mints the tap, still within the same frame for a
-- one-frame tap -- and mark the press seen so the poll cannot arm a second
-- one when isDown does catch it.
function LauncherView.mousepressed(imp, x, y)
if not imp._flex then return end
local now = love.timer.getTime()
@@ -245,7 +315,7 @@ function LauncherView.mousepressed(imp, x, y)
or now < (imp._suppressClickUntil or 0) then
return
end
imp._clickPt = { x = x, y = y }
if not imp._mouseAt then armMouse(imp, x, y) end
imp._prevMouseDown = true
end
@@ -495,6 +565,7 @@ end
local function cartridgeButton(imp, x, y, w, h, key, version, gameName, action)
local state = cartridgeState(imp, version)
markNoDrag(imp, x, y, w, h)
local focused = Kit.focusable(key, x, y, w, h)
local hot = Kit.hover(x, y, w, h)
local active = state.active
@@ -4868,13 +4939,15 @@ function LauncherView.draw(imp)
Kit.beginFrame(mx, my, click ~= nil, imp._wheelY or 0)
imp._clickPt = nil
imp._wheelY = 0
imp._noDragN = 0
Theme.field()
-- Everything from here to buildModals sits UNDER any open modal, so the
-- whole stage draws shielded (no clicks, no hover, no focus ring) while
-- one is up; buildModals lowers the shield for the modal's own controls.
Kit.blockClicks = modalUp(imp)
imp._modalUpNow = modalUp(imp)
Kit.blockClicks = imp._modalUpNow
local step = Kit.scrollStep(m.s)
do
+46 -4
View File
@@ -918,16 +918,58 @@ function Kit.rowsThatFit(h, rowH, gap, minRows, maxRows)
return math.max(minRows or 1, math.min(maxRows or 99, per))
end
Kit.dragX = nil
Kit.dragY = nil
Kit.dragAccum = 0
function Kit.dragBegin(x, y)
Kit.dragX, Kit.dragY, Kit.dragAccum = x, y, 0
end
function Kit.dragAdd(dy)
if Kit.dragX then Kit.dragAccum = Kit.dragAccum + (dy or 0) end
end
function Kit.dragEnd()
Kit.dragX, Kit.dragY, Kit.dragAccum = nil, nil, 0
end
local function dragOriginIn(x, y, w, h)
if not Kit.dragX then return false end
local x1, y1, x2, y2 = x, y, x + w, y + h
local c = Kit._clipRect
if c then
x1, y1 = math.max(x1, c.x), math.max(y1, c.y)
x2, y2 = math.min(x2, c.x + c.w), math.min(y2, c.y + c.h)
end
return Kit.dragX >= x1 and Kit.dragX <= x2
and Kit.dragY >= y1 and Kit.dragY <= y2
end
-- Mouse wheel over a paginated list turns PAGES. The wheel still has to do
-- something (users expect it), but it moves a bounded page index rather than
-- driving a pixel offset, so there is no scroll state and no interpolation.
function Kit.wheelPage(x, y, w, h, page, total, perPage)
if Kit.blockClicks or (Kit.wheelY or 0) == 0 then return page end
if not Kit.hit(x, y, w, h) then return page end
if Kit.blockClicks then return page end
local pages = math.max(1, math.ceil(total / math.max(1, perPage)))
local moved = Theme.clamp((page or 1) + (Kit.wheelY > 0 and -1 or 1), 1, pages)
local out = page
if (Kit.wheelY or 0) ~= 0 and Kit.hit(x, y, w, h) then
out = math.floor(Theme.clamp((out or 1) + (Kit.wheelY > 0 and -1 or 1),
1, pages))
Kit.wheelY = 0
return math.floor(moved)
end
local acc = Kit.dragAccum or 0
if acc ~= 0 and dragOriginIn(x, y, w, h) then
local stepPx = math.max(1, math.floor((h or 0) / 2))
local flips = acc >= 0 and math.floor(acc / stepPx)
or -math.floor(-acc / stepPx)
if flips ~= 0 then
local want = (out or 1) + flips
out = math.floor(Theme.clamp(want, 1, pages))
Kit.dragAccum = out ~= want and 0 or acc - flips * stepPx
end
end
return out
end
function Kit.scrollExtent(contentH, viewH)
+82
View File
@@ -300,6 +300,88 @@ LauncherView.draw(edgeImp)
check((edgeImp._tabScroll.skins or 0) > 0,
"which reaches the tab region even though the cursor is below it")
Kit.blockClicks = false
Kit._clipRect = nil
Kit.wheelY = 0
Kit.mouseX, Kit.mouseY = 400, 400
Kit.dragBegin(50, 50)
Kit.dragAdd(120)
local pg = Kit.wheelPage(0, 0, 100, 100, 1, 100, 10)
eq(pg, 3, "a drag that crossed two half-heights turns two pages")
eq(Kit.dragAccum, 20, "and keeps the remainder for the next flip")
Kit.dragAdd(-140)
pg = Kit.wheelPage(0, 0, 100, 100, pg, 100, 10)
eq(pg, 1, "dragging back down returns those pages")
eq(Kit.dragAccum, -20, "with the remainder's sign preserved")
Kit.dragAdd(-80)
pg = Kit.wheelPage(0, 0, 100, 100, pg, 100, 10)
eq(pg, 1, "a drag past the first page clamps to it")
eq(Kit.dragAccum, 0, "and drops the pile-up so reversing is instant")
Kit.dragAdd(200)
pg = Kit.wheelPage(200, 200, 100, 100, pg, 100, 10)
eq(pg, 1, "a drag that began outside the list is not the list's")
eq(Kit.dragAccum, 200, "and its travel stays queued")
Kit.dragEnd()
eq(Kit.dragAccum, 0, "releasing the button retires the gesture")
window(360, 780)
local mouseImp = skinLauncher(12)
LauncherView.draw(mouseImp)
LauncherView.draw(mouseImp)
local mreg = mouseImp._tabRegionRect
local mmax = mouseImp._tabScrollMax.skins
check(mmax > 0, "the mouse-dragged panel has travel")
local mdown = false
love.mouse.isDown = function() return mdown end
pointer(mreg.x + 20, mreg.y + 40)
mdown = true
LauncherView.update(mouseImp, 0.016)
check(mouseImp._clickPt == nil,
"a press over a scrollable region mints no click")
check(mouseImp._mouseAt ~= nil, "it arms a drag instead")
pointer(mreg.x + 20, mreg.y + 40 - 200)
LauncherView.update(mouseImp, 0.016)
eq(mouseImp._tabScroll.skins, math.min(200, mmax),
"dragging the held mouse scrolls the panel by its travel")
eq(mouseImp._pageScroll or 0, 0,
"while the panel still has travel, the page waits")
pointer(mreg.x + 20, mreg.y + 40 - 200 - mmax * 2)
LauncherView.update(mouseImp, 0.016)
eq(mouseImp._tabScroll.skins, mmax,
"a longer mouse drag reaches the panel's bottom")
check((mouseImp._pageScroll or 0) > 0, "and spills into the page from there")
mdown = false
LauncherView.update(mouseImp, 0.016)
check(mouseImp._clickPt == nil, "a released drag is not a click")
check(mouseImp._mouseAt == nil, "and the gesture is retired")
pointer(mreg.x + 20, mreg.y + 40)
mdown = true
LauncherView.update(mouseImp, 0.016)
check(mouseImp._clickPt == nil, "a fresh press still holds its click back")
mdown = false
LauncherView.update(mouseImp, 0.016)
check(mouseImp._clickPt ~= nil,
"press and release without travel is still a tap, on release")
mouseImp._clickPt = nil
LauncherView.draw(gameImp)
LauncherView.draw(gameImp)
check((gameImp._noDragN or 0) > 0, "the game tab publishes its cartridge rect")
local cart = gameImp._noDragRects[1]
pointer(cart.x + cart.w / 2, cart.y + cart.h / 2)
mdown = true
LauncherView.update(gameImp, 0.016)
check(gameImp._clickPt ~= nil,
"a press on the cartridge clicks at once so its own spin-drag still owns "
.. "the gesture")
check(gameImp._mouseAt == nil, "and never arms the scroll drag")
gameImp._clickPt = nil
mdown = false
LauncherView.update(gameImp, 0.016)
love.mouse.isDown = nil
local function read(path)
local f = assert(io.open(path, "r"))
local src = f:read("*a")