components_OverviewDialog.bs

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

' Layout constants
const DIALOG_WIDTH = 1600
const DIALOG_HEIGHT = 760
const DIALOG_PADDING = 40
const TITLE_HEIGHT = 50
const SEPARATOR_Y = 80
const TEXT_TOP = 115
const BUTTON_TOP_MARGIN = 20
const SCROLL_STEP = 150
const SCROLLBAR_THUMB_WIDTH = 12
const SCROLLBAR_TRACK_WIDTH = 3
const SCROLLBAR_MARGIN = 8

sub init()
  constants = m.global.constants

  ' Dialog position (centered on screen)
  dialogX = (1920 - DIALOG_WIDTH) / 2
  dialogY = (1080 - DIALOG_HEIGHT) / 2

  ' Background
  m.dialogBackground = m.top.findNode("dialogBackground")
  m.dialogBackground.translation = [dialogX, dialogY]
  m.dialogBackground.width = DIALOG_WIDTH
  m.dialogBackground.height = DIALOG_HEIGHT
  m.dialogBackground.blendColor = constants.colorBackgroundPrimary

  ' Title
  m.titleLabel = m.top.findNode("titleLabel")
  m.titleLabel.translation = [dialogX + DIALOG_PADDING, dialogY + 25]
  m.titleLabel.width = DIALOG_WIDTH - (DIALOG_PADDING * 2)
  m.titleLabel.height = TITLE_HEIGHT

  ' Separator
  separator = m.top.findNode("separator")
  separator.translation = [dialogX + DIALOG_PADDING, dialogY + SEPARATOR_Y]
  separator.width = DIALOG_WIDTH - (DIALOG_PADDING * 2)
  separator.color = constants.colorBackgroundSecondary

  ' Calculate text area dimensions (button is now outside dialog)
  m.textAreaHeight = DIALOG_HEIGHT - TEXT_TOP - DIALOG_PADDING
  m.textAreaWidth = DIALOG_WIDTH - (DIALOG_PADDING * 2) - SCROLLBAR_THUMB_WIDTH - SCROLLBAR_MARGIN

  ' Scrollable text area with clipping
  m.textClip = m.top.findNode("textClip")
  m.textClip.translation = [dialogX + DIALOG_PADDING, dialogY + TEXT_TOP]
  m.textClip.clippingRect = [0, 0, m.textAreaWidth, m.textAreaHeight]

  m.scrollContent = m.top.findNode("scrollContent")
  m.taglineText = invalid

  m.overviewText = m.top.findNode("overviewText")
  m.overviewText.width = m.textAreaWidth
  m.overviewText.maxLines = 500
  ' Enable render tracking once - observer fires when text is set/changed
  m.overviewText.enableRenderTracking = true
  m.overviewText.observeField("renderTracking", "onTextRendered")

  ' Scroll position indicator
  scrollTrackX = dialogX + DIALOG_WIDTH - DIALOG_PADDING - SCROLLBAR_THUMB_WIDTH
  scrollThumbX = scrollTrackX + (SCROLLBAR_THUMB_WIDTH - SCROLLBAR_TRACK_WIDTH) / 2
  m.scrollTrack = m.top.findNode("scrollTrack")
  m.scrollTrack.translation = [scrollThumbX, dialogY + TEXT_TOP]
  m.scrollTrack.height = m.textAreaHeight
  m.scrollTrack.width = SCROLLBAR_TRACK_WIDTH
  m.scrollTrack.color = constants.colorBackgroundSecondary

  m.scrollThumb = m.top.findNode("scrollThumb")
  m.scrollThumb.translation = [scrollTrackX, dialogY + TEXT_TOP]
  m.scrollThumb.width = SCROLLBAR_THUMB_WIDTH
  m.scrollThumb.color = constants.colorBackgroundSecondary
  m.scrollThumb.visible = false
  m.scrollTrack.visible = false

  ' OK button (centered below dialog)
  m.okButton = m.top.findNode("okButton")
  m.okButton.minWidth = 200
  m.okButton.translation = [dialogX + (DIALOG_WIDTH / 2) - 100, dialogY + DIALOG_HEIGHT + BUTTON_TOP_MARGIN]

  ' Observe focus changes to show/hide scrollbar
  m.top.observeField("focusedChild", "onFocusChanged")

  ' Scroll state
  m.scrollPosition = 0
  m.maxScroll = 0
  m.scrollBoundsCalculated = false
end sub

sub onTitleChanged()
  m.titleLabel.text = m.top.title
end sub

sub onOverviewChanged()
  m.overviewText.text = m.top.overview
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.taglineText)
      ' Create tagline label dynamically and insert before overview text
      m.taglineText = CreateObject("roSGNode", "LabelPrimaryMedium")
      m.taglineText.bold = true
      m.taglineText.wrap = true
      m.taglineText.width = m.textAreaWidth
      m.taglineText.maxLines = 500
      m.taglineText.enableRenderTracking = true
      m.taglineText.observeField("renderTracking", "onTaglineRendered")
      m.scrollContent.insertChild(m.taglineText, 0)
    end if
    m.taglineText.text = tagline
  else
    if isValid(m.taglineText)
      m.scrollContent.removeChild(m.taglineText)
      m.taglineText = invalid
      ' Reset scroll state to recalculate without tagline
      m.scrollBoundsCalculated = false
      m.scrollPosition = 0
      m.scrollContent.translation = [0, 0]
      updateScrollThumb()
    end if
  end if
end sub

' Tagline has rendered, recalculate scroll bounds to include tagline height
sub onTaglineRendered()
  m.scrollBoundsCalculated = false
  calculateScrollBounds()
end sub

' Once the text has rendered, calculate scroll bounds and scrollbar
sub onTextRendered()
  ' Don't check for "full" - the observer may not fire again after "partial"
  ' The textHeight=0 guard in calculateScrollBounds handles unrendered text
  calculateScrollBounds()
end sub

sub calculateScrollBounds()
  if m.scrollBoundsCalculated then return

  overviewRect = m.overviewText.localBoundingRect()
  overviewHeight = overviewRect.height

  ' If overview height is 0, it hasn't rendered yet
  if overviewHeight = 0 then return

  ' Calculate total content height including tagline if present
  textHeight = overviewHeight
  if isValid(m.taglineText)
    taglineRect = m.taglineText.localBoundingRect()
    if taglineRect.height = 0 then return ' Tagline hasn't rendered yet
    textHeight = m.scrollContent.localBoundingRect().height
  end if

  m.scrollBoundsCalculated = true

  if textHeight > m.textAreaHeight
    m.maxScroll = textHeight - m.textAreaHeight
    thumbRatio = m.textAreaHeight / textHeight
    m.scrollThumb.height = m.textAreaHeight * thumbRatio
    ' Scrollable content: show scrollbar and focus text area
    m.scrollTrack.visible = true
    m.scrollThumb.visible = true
    m.textClip.setFocus(true)
    updateScrollbarFocus(true)
  else
    ' Non-scrollable content: hide scrollbar and focus OK button
    m.maxScroll = 0
    m.scrollTrack.visible = false
    m.scrollThumb.visible = false
    m.okButton.setFocus(true)
    updateScrollbarFocus(false)
  end if
end sub

' Updates the scroll thumb position to reflect the current scroll position
sub updateScrollThumb()
  if m.maxScroll <= 0 then return

  scrollRatio = m.scrollPosition / m.maxScroll
  trackRange = m.textAreaHeight - m.scrollThumb.height
  thumbY = m.scrollTrack.translation[1] + (trackRange * scrollRatio)
  m.scrollThumb.translation = [m.scrollThumb.translation[0], thumbY]
end sub

sub onFocusChanged()
  if m.textClip.hasFocus()
    calculateScrollBounds()
    if m.maxScroll > 0
      m.scrollTrack.visible = true
      m.scrollThumb.visible = true
    end if
    updateScrollbarFocus(true)
  else
    updateScrollbarFocus(false)
  end if
end sub

' Updates scrollbar thumb color based on focus state
sub updateScrollbarFocus(isFocused as boolean)
  constants = m.global.constants
  if isFocused
    m.scrollThumb.color = constants.colorPrimary
  else
    m.scrollThumb.color = constants.colorBackgroundSecondary
  end if
end sub

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

  if key = "back"
    m.global.sceneManager.callfunc("popScene")
    return true
  end if

  if key = "OK"
    if m.okButton.hasFocus()
      m.global.sceneManager.callfunc("popScene")
      return true
    end if
    m.okButton.setFocus(true)
    updateScrollbarFocus(false)
    return true
  end if

  if key = "down"
    if m.okButton.hasFocus() then return true

    calculateScrollBounds()

    if m.maxScroll > 0 and m.scrollPosition < m.maxScroll
      m.scrollPosition = m.scrollPosition + SCROLL_STEP
      if m.scrollPosition > m.maxScroll
        m.scrollPosition = m.maxScroll
      end if
      m.scrollContent.translation = [0, -m.scrollPosition]
      updateScrollThumb()
      return true
    end if

    m.okButton.setFocus(true)
    updateScrollbarFocus(false)
    return true
  end if

  if key = "up"
    if m.okButton.hasFocus()
      ' Only move back to text area if there's scrollable content
      if m.maxScroll > 0
        m.textClip.setFocus(true)
        updateScrollbarFocus(true)
      end if
      return true
    end if

    calculateScrollBounds()

    if m.scrollPosition > 0
      m.scrollPosition = m.scrollPosition - SCROLL_STEP
      if m.scrollPosition < 0
        m.scrollPosition = 0
      end if
      m.scrollContent.translation = [0, -m.scrollPosition]
      updateScrollThumb()
      return true
    end if
    ' At top of scroll, consume event to prevent bubbling
    return true
  end if

  return false
end function