CLOSES #779 , CLOSES #743 + new font

This commit is contained in:
bryanthaboi
2026-08-04 06:41:34 -04:00
parent fe139ca7f4
commit 3a6557ffe2
30 changed files with 1214 additions and 79 deletions
+54 -15
View File
@@ -112,7 +112,8 @@ function ScrollManager.new(config, deps)
self.touchScrollEnabled = config.touchScrollEnabled ~= false -- Default true
self.momentumScrollEnabled = config.momentumScrollEnabled ~= false -- Default true
self.bounceEnabled = config.bounceEnabled ~= false -- Default true
self.scrollFriction = config.scrollFriction or 0.95 -- Exponential decay per frame
self.scrollFriction = config.scrollFriction or 0.95 -- legacy per-frame decay; superseded by _momentumDecel
self._momentumDecel = config.momentumDecel or 2.0 -- fling decay rate (1/s), dt-based; ~iOS "normal"
self.bounceStiffness = config.bounceStiffness or 0.2 -- Spring constant
self.maxOverscroll = config.maxOverscroll or 100 -- pixels
@@ -926,9 +927,15 @@ function ScrollManager:handleTouchMove(touchX, touchY)
dx = -dx
dy = -dy
-- Calculate velocity (pixels per second)
self._scrollVelocityX = dx / dt
self._scrollVelocityY = dy / dt
-- Velocity tracking (pixels per second). Not the raw last-sample d/dt:
-- touch samples jitter, and the final pre-release sample often reads near
-- zero, which is why single-sample flings feel dead compared to a native
-- list. Blend toward the instantaneous velocity with a ~50 ms time
-- constant instead, like OS velocity trackers that average recent samples.
local instX, instY = dx / dt, dy / dt
local blend = math.min(1, dt / 0.05)
self._scrollVelocityX = self._scrollVelocityX + (instX - self._scrollVelocityX) * blend
self._scrollVelocityY = self._scrollVelocityY + (instY - self._scrollVelocityY) * blend
-- Apply scroll with bounce if enabled
if self.bounceEnabled then
@@ -969,6 +976,15 @@ function ScrollManager:handleTouchRelease()
self._touchScrolling = false
-- A finger that pauses and then lifts is a stop, not a fling: without
-- this, the smoothed velocity from the earlier drag still launches the
-- list after a deliberate hold (native lists kill the tracker the same
-- way).
if love.timer.getTime() - (self._lastTouchTime or 0) > 0.1 then
self._scrollVelocityX = 0
self._scrollVelocityY = 0
end
-- Start momentum scrolling if enabled and velocity is significant
if self.momentumScrollEnabled then
local velocityThreshold = 50 -- pixels per second
@@ -997,8 +1013,13 @@ function ScrollManager:update(dt)
-- rate; a fixed fraction made scrolling visibly slower whenever frame
-- time rose. _smoothScrollRate is the decay constant in 1/seconds:
-- ~90% of the remaining distance is covered in 2.3/rate seconds.
-- dt can legitimately be 0 here: in immediate mode beginFrame resets the
-- accumulated dt BEFORE endFrame updates the elements, so the launcher's
-- update order always hands this 0. Treat that (and nil) as one nominal
-- 60 Hz step so the interpolation still advances one frame's worth.
local step = (dt and dt > 0) and dt or (1 / 60)
if self._targetScrollX or self._targetScrollY then
local alpha = 1 - math.exp(-(self._smoothScrollRate or 30) * (dt or 0.016))
local alpha = 1 - math.exp(-(self._smoothScrollRate or 30) * step)
if self._targetScrollY then
local diff = self._targetScrollY - self._scrollY
if math.abs(diff) > 0.5 then
@@ -1028,25 +1049,43 @@ function ScrollManager:update(dt)
return
end
-- Apply velocity to scroll position
local dx = self._scrollVelocityX * dt
local dy = self._scrollVelocityY * dt
-- Apply velocity to scroll position (step, not dt: dt is 0 in the
-- immediate-mode endFrame path, which left touch flings completely dead)
local dx = self._scrollVelocityX * step
local dy = self._scrollVelocityY * step
if self.bounceEnabled then
-- Allow overscroll during momentum
self._scrollX = self._scrollX + dx
self._scrollY = self._scrollY + dy
-- Allow overscroll during momentum, but never past maxOverscroll: an
-- unclamped fling carried a list hundreds of pixels past its edge
-- (rows vanished into a blank card until the spring crawled back).
self._scrollX = self._utils.clamp(self._scrollX + dx,
-self.maxOverscroll, self._maxScrollX + self.maxOverscroll)
self._scrollY = self._utils.clamp(self._scrollY + dy,
-self.maxOverscroll, self._maxScrollY + self.maxOverscroll)
else
self:scrollBy(dx, dy)
end
-- Apply friction (exponential decay)
self._scrollVelocityX = self._scrollVelocityX * self.scrollFriction
self._scrollVelocityY = self._scrollVelocityY * self.scrollFriction
-- Frame-rate-independent exponential decay tuned to native list feel:
-- iOS "normal" deceleration is ~0.998 per millisecond, i.e. exp(-2.0 t).
-- The old per-frame 0.95 factor killed a fling in a fraction of a second
-- (and faster the higher the frame rate). Total fling travel is
-- velocity / rate.
local decay = math.exp(-(self._momentumDecel or 2.0) * step)
-- Past the content edge, kill the fling an order of magnitude faster so
-- the rubber band absorbs it instead of stretching to the clamp and
-- sitting there (native lists do the same).
local overX = self._scrollX < 0 or self._scrollX > self._maxScrollX
local overY = self._scrollY < 0 or self._scrollY > self._maxScrollY
if overX or overY then
decay = decay * math.exp(-20 * step)
end
self._scrollVelocityX = self._scrollVelocityX * decay
self._scrollVelocityY = self._scrollVelocityY * decay
-- Stop momentum when velocity is very low
local totalVelocity = math.sqrt(self._scrollVelocityX ^ 2 + self._scrollVelocityY ^ 2)
if totalVelocity < 1 then
if totalVelocity < 5 then
self._momentumScrolling = false
self._scrollVelocityX = 0
self._scrollVelocityY = 0