components_ui_label_FocusableOverview.bs

import "pkg:/source/utils/misc.bs"

const LABEL_PADDING = 30
const FOCUS_BORDER_PADDING = 12

sub init()
  m.background = m.top.findNode("background")
  m.background.visible = false ' No background by default
  m.focusBorder = m.top.findNode("focusBorder")
  m.contentGroup = m.top.findNode("contentGroup")
  m.label = m.top.findNode("label")
  m.taglineLabel = invalid

  ' Track content renders for auto-sizing when height is not explicitly set
  m.contentGroup.enableRenderTracking = true
  m.contentGroup.observeField("renderTracking", "onContentRendered")

  m.top.observeField("focusedChild", "onFocusChanged")
end sub

' Updates the label text when the text field changes
sub onTextChanged()
  m.label.text = m.top.text
end sub

' Dynamically creates or removes the tagline label based on tagline text
sub onTaglineChanged()
  tagline = m.top.tagline

  if isValidAndNotEmpty(tagline)
    if not isValid(m.taglineLabel)
      ' Create tagline label dynamically and insert before description label
      m.taglineLabel = CreateObject("roSGNode", "LabelSecondaryMedium")
      m.taglineLabel.bold = true
      m.taglineLabel.wrap = true
      m.taglineLabel.lineSpacing = 0
      m.taglineLabel.ellipsisText = "..."
      m.taglineLabel.width = m.label.width
      if m.top.taglineMaxLines > 0
        m.taglineLabel.maxLines = m.top.taglineMaxLines
      end if
      m.contentGroup.insertChild(m.taglineLabel, 0)
    end if
    m.taglineLabel.text = tagline
  else
    ' Remove tagline label when text is cleared
    if isValid(m.taglineLabel)
      m.contentGroup.removeChild(m.taglineLabel)
      m.taglineLabel = invalid
    end if
  end if
end sub

' Sets maxLines on the description label
sub onMaxLinesChanged()
  m.label.maxLines = m.top.maxLines
end sub

' Sets maxLines on the tagline label (if it exists)
sub onTaglineMaxLinesChanged()
  if isValid(m.taglineLabel)
    m.taglineLabel.maxLines = m.top.taglineMaxLines
  end if
end sub

' Sizes the focus border based on background dimensions
' When showBackground is true, border matches background exactly
' since content padding already provides breathing room.
' When showBackground is false, border expands outward so it
' doesn't overlap flush text content.
sub updateFocusBorderLayout()
  ' Only expand outward when focused and no background padding exists
  ' Invisible nodes still affect layout in SceneGraph, so the border must
  ' match the background when unfocused to avoid unwanted layout shifts
  if not m.top.showBackground and m.top.hasFocus()
    p = FOCUS_BORDER_PADDING
    m.focusBorder.translation = [-p, -p]
    m.focusBorder.width = m.background.width + (p * 2)
    m.focusBorder.height = m.background.height + (p * 2)
  else
    m.focusBorder.translation = [0, 0]
    m.focusBorder.width = m.background.width
    m.focusBorder.height = m.background.height
  end if
end sub

' Resizes background, border, and content when width or height changes
sub onSizeChanged()
  componentWidth = m.top.width
  componentHeight = m.top.height

  m.background.width = componentWidth

  ' Apply padding only when background is shown
  ' Without background, content is flush for alignment with sibling elements
  if m.top.showBackground
    padding = LABEL_PADDING
  else
    padding = 0
  end if

  m.contentGroup.translation = [padding, padding]
  labelWidth = componentWidth - (padding * 2)
  m.label.width = labelWidth

  if isValid(m.taglineLabel)
    m.taglineLabel.width = labelWidth
  end if

  ' Fixed height mode: explicitly set background, border, and label heights
  ' Auto-size mode (height=0): heights are set by onContentRendered instead
  if componentHeight > 0
    m.background.height = componentHeight

    ' Only set explicit label height when maxLines is not specified
    ' This preserves clipping behavior for consumers that set maxLines
    if m.top.maxLines = 0
      m.label.height = componentHeight - (padding * 2)
    end if
  end if

  updateFocusBorderLayout()
end sub

' Auto-sizes background and border height based on rendered content
' Only active when height is not explicitly set (height=0)
sub onContentRendered()
  if m.top.height > 0 then return ' Fixed height mode

  contentRect = m.contentGroup.localBoundingRect()
  contentHeight = contentRect.height
  if contentHeight = 0 then return ' Not rendered yet

  if m.top.showBackground
    padding = LABEL_PADDING
  else
    padding = 0
  end if

  totalHeight = contentHeight + (padding * 2)
  m.background.height = totalHeight
  updateFocusBorderLayout()
end sub

' Toggles the background rectangle and recalculates layout padding
sub onShowBackgroundChanged()
  m.background.visible = m.top.showBackground
  onSizeChanged()
end sub

' Passes horizAlign through to the internal label
sub onHorizAlignChanged()
  m.label.horizAlign = m.top.horizAlign
end sub

' Passes vertAlign through to the internal label
sub onVertAlignChanged()
  m.label.vertAlign = m.top.vertAlign
end sub

' Shows or hides the 9patch focus border based on focus state
sub onFocusChanged()
  if m.top.hasFocus()
    m.focusBorder.blendColor = m.global.constants.colorPrimary
    m.focusBorder.visible = true
  else
    m.focusBorder.visible = false
  end if
  updateFocusBorderLayout()
end sub

' Opens an OverviewDialog pushed onto the scene stack
' Passes tagline separately so the dialog can style it distinctly
sub openOverviewDialog()
  dialog = createObject("roSGNode", "OverviewDialog")

  if m.top.dialogTitle <> ""
    dialog.title = m.top.dialogTitle
  end if

  if isValidAndNotEmpty(m.top.tagline)
    dialog.tagline = m.top.tagline
  end if

  dialog.overview = m.label.text
  m.global.sceneManager.callFunc("pushScene", dialog)
end sub

function onKeyEvent(key as string, press as boolean) as boolean
  if not press then return false

  if key = "OK"
    openOverviewDialog()
    return true
  end if

  return false
end function