Files
esp32-lua-api/lua/lib/ui/toast.lua
T
evan 94d298dd45 feat(ui): add ui.toast, a message that slides up from the bottom edge
The card sits in a scroll container of its own height over an empty strip of
the same, so the slide is a setScroll() delta on the laid-out tree rather than
a rebuild a frame, and the container scissors the part not yet arrived.
2026-08-06 21:32:03 -04:00

95 lines
2.3 KiB
Lua

-- A message that slides up from the bottom edge, holds, then slides back down.
--
-- The card sits inside a scroll container of its own height whose content is twice that:
-- an empty strip above the card. Sliding is therefore ui.setScroll() on the tree already
-- laid out -- a delta on stored coordinates -- rather than a rebuild a frame, and the
-- container scissors the half of the card that has not arrived yet.
--
-- Whatever mounts the tree includes toast.node() and calls toast.draw(deltaMs); show()
-- rebuilds so the node exists, and hide() rebuilds to drop it.
local ui = require "ui"
local M = {}
local HEIGHT = 44
local MARGIN = 12
local SLIDE_MS = 180
local text, box
-- nil, or "in" / "hold" / "out" with the milliseconds spent in it.
local phase, elapsed, holdMs
---@param message string
---@param durationMs? integer How long the card holds before it slides away.
function M.show(message, durationMs)
text, holdMs = message, durationMs or 1500
phase, elapsed = "in", 0
ui.rebuild()
end
function M.hide()
if phase then
phase, text, box = nil, nil, nil
ui.rebuild()
end
end
---@return NodeId|nil
function M.node()
if not phase then
return nil
end
box = ui.box {
at = { x = MARGIN, y = screen.getHeight() - HEIGHT - MARGIN },
w = screen.getWidth() - 2 * MARGIN,
h = HEIGHT,
scrollY = true,
ui.box {
w = "fill",
ui.spacer { w = "fill", h = HEIGHT },
ui.box {
w = "fill",
h = HEIGHT,
align = "center",
justify = "center",
background = ui.theme.face,
border = ui.theme.muted,
ui.text(text),
},
},
}
return box
end
local function progress()
if phase == "in" then
return elapsed / SLIDE_MS
elseif phase == "out" then
return 1 - elapsed / SLIDE_MS
end
return 1
end
---Advances the slide. Safe to call every frame whether or not a toast is up.
---@param deltaMs integer
function M.draw(deltaMs)
if not phase then
return
end
elapsed = elapsed + deltaMs
if phase == "in" and elapsed >= SLIDE_MS then
phase, elapsed = "hold", 0
elseif phase == "hold" and elapsed >= holdMs then
phase, elapsed = "out", 0
elseif phase == "out" and elapsed >= SLIDE_MS then
M.hide()
return
end
if box then
ui.setScroll(box, 0, math.floor(HEIGHT * progress()))
end
end
return M