Files
slate32/sdcard/.lua/apps/Scroll/main.lua
T
2026-08-06 11:22:54 -04:00

114 lines
3.6 KiB
Lua

local ui = require "ui"
-- Rows are ordinary nodes inside a scrolling box, so this app draws nothing itself: the
-- tree clips the subtree to the box, the painter culls to the band, and a drag is one
-- integer per frame. Both axes pan -- the rows are wider than the pane -- so a diagonal
-- drag moves the content diagonally.
local ROWS = 60
local ROW_H = 64
local ROW_W = 420 -- wider than the pane, so there is something to pan across
local GAP = 8
-- Fraction of a flick's speed still left after a second. Applied as DECAY^(dt/1000) rather
-- than per frame, so the glide takes the same time whatever the frame rate.
local DECAY = 0.02
-- Below this the glide is no longer visible and would only keep the pane repainting.
local STOP_PX_PER_SEC = 8
---@class ScrollApp : SlateApp, TouchHandlers
local M = {}
local list
local scrollX, scrollY = 0, 0
-- Drag deltas accumulate here and are applied in draw(), because that is the only place
-- with a real elapsed time to turn them into a speed. Touch handlers just record.
local dragX, dragY = 0, 0
local velocityX, velocityY = 0, 0
local lastX, lastY
local dragging = false
function M.init()
log.info "scroll demo ready"
end
function M.onTouchDown(x, y)
dragging = true
lastX, lastY = x, y
dragX, dragY = 0, 0
velocityX, velocityY = 0, 0 -- a finger down catches the glide, like every touch UI
end
function M.onTouchMove(x, y)
if not dragging then
return
end
-- Content follows the finger, so dragging down moves the list up.
dragX = dragX + (lastX - x)
dragY = dragY + (lastY - y)
lastX, lastY = x, y
end
function M.onTouchUp()
dragging = false
end
---@param deltaMs integer
function M.draw(deltaMs)
if deltaMs <= 0 then
return
end
local seconds = deltaMs / 1000
if dragging then
scrollX, scrollY = scrollX + dragX, scrollY + dragY
-- Speed measured over the frame the movement actually arrived in. A finger that stopped
-- moving reports zero, which is what makes a hold-then-lift not flick.
velocityX, velocityY = dragX / seconds, dragY / seconds
dragX, dragY = 0, 0
elseif velocityX ~= 0 or velocityY ~= 0 then
scrollX, scrollY = scrollX + velocityX * seconds, scrollY + velocityY * seconds
local keep = DECAY ^ seconds
velocityX, velocityY = velocityX * keep, velocityY * keep
if math.abs(velocityX) < STOP_PX_PER_SEC then
velocityX = 0
end
if math.abs(velocityY) < STOP_PX_PER_SEC then
velocityY = 0
end
else
return -- nothing moving, so nothing to repaint
end
ui.setScroll(list, math.floor(scrollX), math.floor(scrollY))
-- Read back what the clamp allowed: without this the offset keeps running past the end
-- while the content sits still, and the list ignores the first part of the drag back.
local clampedX, clampedY = ui.getScroll(list)
if clampedX ~= math.floor(scrollX) then
scrollX, velocityX = clampedX, 0
end
if clampedY ~= math.floor(scrollY) then
scrollY, velocityY = clampedY, 0
end
end
function M.node()
-- Sized to the frame, not "fill": fill resolves against the whole panel, and the pane the
-- app was given is shorter by the status bar. The difference is the scroll range.
local frameW, frameH = ui.frame()
local rows = { gap = GAP, pad = GAP, scrollX = true, scrollY = true, w = frameW, h = frameH }
for i = 1, ROWS do
rows[#rows + 1] = ui.box {
w = ROW_W,
h = ROW_H - GAP,
justify = "center",
align = "center",
background = (i % 2 == 1) and ui.theme.face or ui.theme.background,
border = ui.theme.muted,
ui.label("Row " .. i, { font = screen.FONT_LARGE }),
}
end
list = ui.box(rows)
return list
end
return M