components_ui_toast_Toast.bs
import "pkg:/source/utils/misc.bs"
' Layout constants — padding synced to TextButton (30px)
const TOAST_PADDING = 30
const TOAST_ICON_SIZE = 32
const TOAST_ICON_GAP = 12
const TOAST_MAX_WIDTH = 800
' 5% action safe zone: 1920 * 0.05 = 96px from each edge
const TOAST_ACTION_SAFE_MARGIN = 96
const TOAST_RIGHT_EDGE = 1824 ' 1920 - TOAST_ACTION_SAFE_MARGIN
const TOAST_BOTTOM_EDGE = 984 ' 1080 - TOAST_ACTION_SAFE_MARGIN
sub init()
m.container = m.top.findNode("toastContainer")
m.toastBackground = m.top.findNode("toastBackground")
m.toastIcon = m.top.findNode("toastIcon")
m.toastMessage = m.top.findNode("toastMessage")
m.fadeIn = m.top.findNode("fadeIn")
m.fadeOut = m.top.findNode("fadeOut")
m.dismissTimer = m.top.findNode("dismissTimer")
m.dismissTimer.observeField("fire", "onDismissTimer")
m.fadeOut.observeField("state", "onFadeOutComplete")
' Enable render tracking for text measurement (same pattern as TextButton)
m.top.enableRenderTracking = true
m.top.observeField("renderTracking", "onRenderComplete")
m.container.opacity = 0
m.top.visible = false
end sub
sub onShouldShow()
if not m.top.shouldShow then return
constants = m.global.constants
toastType = m.top.toastType
' Set icon and icon color based on toast type
if toastType = "success"
m.toastIcon.uri = constants.iconSuccess
m.toastIcon.blendColor = constants.colorSuccess
else if toastType = "warning"
m.toastIcon.uri = constants.iconWarning
m.toastIcon.blendColor = constants.colorWarning
else if toastType = "info"
m.toastIcon.uri = constants.iconInfo
m.toastIcon.blendColor = constants.colorInfo
else
' Default: error
m.toastIcon.uri = constants.iconError
m.toastIcon.blendColor = constants.colorError
end if
' Set background to app's secondary background color
m.toastBackground.blendColor = constants.colorBackgroundSecondary
' Set message text
m.toastMessage.text = m.top.message
' Stop any in-progress animations
m.fadeOut.control = "stop"
m.dismissTimer.control = "stop"
' Show container so render tracking can measure the label
m.top.visible = true
m.container.opacity = 0
' Size and position — if already rendered, do it now; otherwise wait for onRenderComplete
if m.top.renderTracking = "full"
sizeAndPosition()
end if
end sub
' Called when the component has rendered and we can measure the label accurately
sub onRenderComplete()
if m.top.renderTracking <> "full" then return
if not m.top.shouldShow then return
sizeAndPosition()
end sub
' Measure text, size the toast, and anchor right edge to action safe zone
sub sizeAndPosition()
if not isValid(m.toastMessage) then return
if m.toastMessage.text.Len() = 0 then return
' Reset label size to allow accurate measurement of text content
m.toastMessage.width = 0
m.toastMessage.height = 0
' Get accurate text dimensions from rendered label
boundingRect = m.toastMessage.localBoundingRect()
textWidth = boundingRect.width
' If not rendered yet, wait for next onRenderComplete
if textWidth = 0 then return
' Calculate toast width: padding + icon + gap + text + padding
contentWidth = TOAST_ICON_SIZE + TOAST_ICON_GAP + textWidth
toastWidth = contentWidth + (TOAST_PADDING * 2)
' Clamp to max width
if toastWidth > TOAST_MAX_WIDTH
toastWidth = TOAST_MAX_WIDTH
end if
' Calculate height from font size (same approach as TextButton)
fontSize = m.toastMessage.font.size
visibleTextHeight = fontSize * 0.7
toastHeight = visibleTextHeight + (TOAST_PADDING * 2)
' Ensure toast is at least as tall as icon + padding
minHeight = TOAST_ICON_SIZE + (TOAST_PADDING * 2)
if toastHeight < minHeight
toastHeight = minHeight
end if
' Size background
m.toastBackground.width = toastWidth
m.toastBackground.height = toastHeight
' Position icon — vertically centered
iconY = (toastHeight - TOAST_ICON_SIZE) / 2
m.toastIcon.translation = [TOAST_PADDING, iconY]
' Position label — after icon + gap, vertically centered
labelX = TOAST_PADDING + TOAST_ICON_SIZE + TOAST_ICON_GAP
labelWidth = toastWidth - labelX - TOAST_PADDING
m.toastMessage.width = labelWidth
m.toastMessage.height = toastHeight
m.toastMessage.translation = [labelX, 0]
' Anchor toast: right edge at action safe boundary, bottom at action safe boundary
toastX = TOAST_RIGHT_EDGE - toastWidth
toastY = TOAST_BOTTOM_EDGE - toastHeight
m.top.translation = [toastX, toastY]
' Fade in and start dismiss timer
m.fadeIn.control = "start"
m.dismissTimer.control = "start"
end sub
sub onDismissTimer()
m.fadeOut.control = "start"
end sub
sub onFadeOutComplete()
if m.fadeOut.state = "stopped"
m.top.visible = false
m.container.opacity = 0
end if
end sub