components_ui_tabbar_JRTabBar.bs
import "pkg:/source/utils/misc.bs"
sub init()
m.tabContainer = m.top.findNode("tabContainer")
m.focusedTabIndex = 0
' When the tab bar gains focus, highlight the focused tab
m.top.observeField("focusedChild", "onFocusChanged")
end sub
' Rebuilds tab children when the tabs array changes.
' Skips recreation when the tab configuration is unchanged (e.g. returning
' to the same screen) so already-sized JRTab nodes appear instantly.
sub onTabsChanged()
tabs = m.top.tabs
if not isValidAndNotEmpty(tabs)
m.tabContainer.removeChildren(m.tabContainer.getChildren(-1, 0))
m.focusedTabIndex = 0
m.top.isReady = false
return
end if
' Preserve existing tabs if the configuration matches — avoids the visible
' TextButton render/sizing dance when returning to the same screen
if tabsMatchExisting(tabs)
syncSelectedTab()
return
end if
' Configuration changed — full rebuild
m.top.isReady = false
m.tabContainer.removeChildren(m.tabContainer.getChildren(-1, 0))
matched = false
selectedIndex = 0
for i = 0 to tabs.count() - 1
tabData = tabs[i]
tabNode = CreateObject("roSGNode", "JRTab")
tabNode.tabTitle = tabData.title
tabNode.tabId = tabData.id
' Mark the first tab as selected by default if no selection exists
if m.top.selectedTabId = "" and tabData.id = tabs[0].id
tabNode.isButtonSelected = true
m.top.selectedTabId = tabData.id
selectedIndex = i
matched = true
else if tabData.id = m.top.selectedTabId
tabNode.isButtonSelected = true
selectedIndex = i
matched = true
end if
m.tabContainer.appendChild(tabNode)
end for
' Fallback: selectedTabId was stale (not found in new tabs); select and announce the first tab
if not matched
firstTab = m.tabContainer.getChild(0)
if isValid(firstTab)
firstTab.isButtonSelected = true
m.top.selectedTabId = firstTab.tabId
end if
selectedIndex = 0
end if
' Align the focus cursor to the selected tab
m.focusedTabIndex = selectedIndex
' Observe first tab's ready field to propagate sizing completion
firstTab = m.tabContainer.getChild(0)
if isValid(firstTab)
firstTab.observeField("isReady", "onFirstTabReady")
end if
updateFocusedTabVisuals()
end sub
' First tab has finished rendering/sizing — signal that the tab bar is ready
sub onFirstTabReady()
firstTab = m.tabContainer.getChild(0)
if isValid(firstTab)
firstTab.unobserveField("isReady")
end if
m.top.isReady = true
end sub
' Returns true if the existing tab children match the new tabs configuration
function tabsMatchExisting(tabs as object) as boolean
if m.tabContainer.getChildCount() <> tabs.count() then return false
for i = 0 to tabs.count() - 1
existing = m.tabContainer.getChild(i)
if existing.tabId <> tabs[i].id or existing.tabTitle <> tabs[i].title
return false
end if
end for
return true
end function
' Updates isSelected on existing tabs to match the current selectedTabId
' without recreating any nodes. Also aligns the focus cursor so it tracks
' the active tab after an external re-sync (re-login, return from details).
sub syncSelectedTab()
for i = 0 to m.tabContainer.getChildCount() - 1
tabNode = m.tabContainer.getChild(i)
isSelected = (tabNode.tabId = m.top.selectedTabId)
tabNode.isButtonSelected = isSelected
if isSelected
m.focusedTabIndex = i
end if
end for
end sub
' When the tab bar gains or loses focus, update tab visuals
sub onFocusChanged()
hasFocus = m.top.isInFocusChain()
' Internal m.focusedTabIndex is already kept in sync by:
' - onTabsChanged / syncSelectedTab (resets to selected tab on rebuild/return)
' - onFocusedTabIndexChanged (external sets, e.g. dropdown return)
' - onKeyEvent left/right (user navigation within the tab bar)
' No sync needed here — reading the interface field would clobber the
' remembered position with a stale value.
updateFocusedTabVisuals()
' Clear focus indicator on all tabs when losing focus
if not hasFocus
for i = 0 to m.tabContainer.getChildCount() - 1
m.tabContainer.getChild(i).isFocused = false
end for
end if
end sub
' Updates the isFocused visual state on tab children
sub updateFocusedTabVisuals()
if not m.top.isInFocusChain() then return
for i = 0 to m.tabContainer.getChildCount() - 1
tabNode = m.tabContainer.getChild(i)
tabNode.isFocused = (i = m.focusedTabIndex)
end for
end sub
' Selects a tab by index: updates isSelected on all children and fires selectedTabId
sub selectTab(index as integer)
for i = 0 to m.tabContainer.getChildCount() - 1
tabNode = m.tabContainer.getChild(i)
tabNode.isButtonSelected = (i = index)
end for
selectedTab = m.tabContainer.getChild(index)
if isValid(selectedTab)
m.top.selectedTabId = selectedTab.tabId
end if
end sub
function onKeyEvent(key as string, press as boolean) as boolean
if not press then return false
tabCount = m.tabContainer.getChildCount()
if tabCount = 0 then return false
if key = "right"
if m.focusedTabIndex = tabCount - 1
' At last tab — signal focus transfer to user dropdown (wraps right)
m.top.requestFocusToUserMenu = "right"
else
' Move focus right within tabs
m.focusedTabIndex = m.focusedTabIndex + 1
updateFocusedTabVisuals()
end if
return true
else if key = "left"
if m.focusedTabIndex = 0
' At first tab — signal focus transfer to user dropdown (wraps left)
m.top.requestFocusToUserMenu = "left"
else
' Move focus left within tabs
m.focusedTabIndex = m.focusedTabIndex - 1
updateFocusedTabVisuals()
end if
return true
else if key = "OK"
selectTab(m.focusedTabIndex)
return true
else if key = "down"
' Signal the parent screen to take focus back
m.top.requestFocusReturn = true
return true
end if
' UP: don't consume — let it bubble (nothing above the tab bar)
return false
end function
' Called when focusedTabIndex is set externally (e.g., returning from user dropdown).
' Syncs internal state and updates visuals.
sub onFocusedTabIndexChanged()
newIndex = m.top.focusedTabIndex
tabCount = m.tabContainer.getChildCount()
if tabCount = 0 then return
' Clamp to valid range
if newIndex < 0
newIndex = 0
else if newIndex >= tabCount
newIndex = tabCount - 1
end if
m.focusedTabIndex = newIndex
updateFocusedTabVisuals()
end sub
sub onDestroy()
m.top.unobserveField("focusedChild")
m.tabContainer.removeChildren(m.tabContainer.getChildren(-1, 0))
m.tabContainer = invalid
end sub