source_utils_infoRowFit.bs

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

' How much of a row of text items fits a width budget, and where to cut the rest.
'
' ItemDetails' info rows are horizontal LayoutGroups of labels with a bullet divider
' between items, and a LayoutGroup never clips: whatever does not fit runs on
' past the budget. On the details screen that means into the logo, which sits at the
' right edge. This answers the fitting question ONCE, from widths, so the screen only
' has to apply the answer.
'
' Pure on purpose: no node access, no m, no globals. The caller measures (a Label's
' boundingRect() is correct the instant its text is set, so this runs before the
' first paint) and the spec can drive every overflow shape without a device.
'
' The cut keeps whole items in order and ends the row on the item that crosses the
' budget, narrowed to the space left so the Label can ellipsize it. Items after that
' one are dropped. If the space left is too small to read, that item is dropped too
' rather than rendered as a stub, EXCEPT the first item: a row is never emptied, so
' the first item always gets the whole budget.
'
' @param {object} itemWidths - array of each item's natural width, in row order
' @param {integer} separatorWidth - the full footprint BETWEEN two items (for
'   ItemDetails: spacing + divider + spacing)
' @param {integer} budget - the width the row may occupy
' @param {integer} minCutWidth - narrowest width worth showing a cut item at
' @return {object} { keep: items to keep, cutWidth: width for the last kept item,
'   or 0 when every kept item renders at its natural width }
function fitTextRow(itemWidths as object, separatorWidth as integer, budget as integer, minCutWidth as integer) as object
  if not isValid(itemWidths) then return { keep: 0, cutWidth: 0 }

  x = 0
  for i = 0 to itemWidths.count() - 1
    start = x
    if i > 0 then start = x + separatorWidth
    finish = start + itemWidths[i]

    if finish <= budget
      x = finish
    else
      remaining = budget - start
      if i = 0 then return { keep: 1, cutWidth: budget }
      if remaining >= minCutWidth then return { keep: i + 1, cutWidth: remaining }
      return { keep: i, cutWidth: 0 }
    end if
  end for

  return { keep: itemWidths.count(), cutWidth: 0 }
end function

' The width a line of text starting at textLeftX may use, given where the logo is.
'
' A line that ends above the logo (with `clearance` to spare) runs to rightEdgeX; one
' beside the logo stops `clearance` short of the logo's left edge. With no logo on
' screen every line runs to rightEdgeX.
'
' @param {integer} lineBottomY - the line's bottom edge, in screen space
' @param {integer} textLeftX - the line's left edge, in screen space
' @param {object} logoBox - { x, y } of the logo's top-left corner, or invalid for no logo
' @param {integer} clearance - the gap kept between text and the logo, on both sides
' @param {integer} rightEdgeX - the right edge text never passes
' @return {integer} the line's width budget
function textLineWidth(lineBottomY as integer, textLeftX as integer, logoBox as object, clearance as integer, rightEdgeX as integer) as integer
  if not isValid(logoBox) then return rightEdgeX - textLeftX
  if lineBottomY <= logoBox.y - clearance then return rightEdgeX - textLeftX
  return logoBox.x - clearance - textLeftX
end function