subtitleLayout

Members

(static, constant) BADGE_ICON_GAP

Between a badge's icon and its label. Only a badge WITH an icon pays it — a spacing applied beside an empty icon slot is what indented, and clipped, an icon-less badge like "Forced".

Default Value
  • 6

(static, constant) BADGE_SPACING

Between two badges. Applied to the badge LayoutGroup in each row's init(), so the arithmetic below and the rendered layout use the same number.

Default Value
  • 16

(static, constant) COLUMNS_Y

Y of both columns inside the panel. Below the title, the file name and the control row.

Default Value
  • 190

(static, constant) COLUMN_HEADING_HEIGHT

Gap between a column's heading and the content beneath it.

Default Value
  • 44

(static, constant) EXISTING_WIDTH

Default Value
  • 570

(static, constant) PANEL_CONTENT_HEIGHT

Height of the panel's clipped content area (panelClip's clippingRect).

Default Value
  • 720

(static, constant) RESULTS_WIDTH

Column widths. The results column is wide because release names run long — 85 characters was the longest observed in a real result set — and width here directly buys fewer marquees.

Default Value
  • 1120

(static, constant) ROW_ACTION_GUTTER

Gap between the icon and whatever text ends before it.

Default Value
  • 16

(static, constant) ROW_ACTION_ICON_SIZE

── Row action affordance ────────────────────────────────────────────────── The delete icon on a downloaded-subtitle row. 24px matches the app's other affordance markers — TrackDropdown's chevron and the panel's own language button — so an icon that says "this control does something" is the same size wherever it appears.

Default Value
  • 24

(static, constant) ROW_BADGE_GUTTER

Gap between a row's text and the badges beside it. Wide enough that an ellipsized line never looks joined to the first badge.

Default Value
  • 24

(static, constant) ROW_DETAIL_MIN_WIDTH

The text keeps at least this much beside the badges; badges that would push it narrower are dropped whole. Also keeps a Label's width positive (a Label with width <= 0 renders its text unclipped).

Default Value
  • 120

(static, constant) ROW_H_PADDING

── Row padding, measured from the FOCUS RING rather than the row ──────────

These are small because the list's focus indicator is drawn ~10px OUTSIDE the row (see ROW_SPACING), so 10px of inset already exists before the row adds any. They were 12 and 10 — written when the row drew its own border AT its own bounds — which silently became insets of 22 and 21 once the list took the indicator over. Measured on a Stick 4K: a 92px ring around a 50px text block, 42px of it air.

That read as "off" for a reason worth keeping: 21px of padding inside the ring against a 24px gap BETWEEN rows meant the two were nearly equal, so nothing grouped — the eye cannot tell which space belongs to the row. Inside must be clearly tighter than outside.

2 and 1 rather than 0: the totals land at 12 and 11, which is what the original constants were actually asking for, and ROW_V_PADDING has to keep rowHeight divisible by 3 for the 720p grid (2P + 52 and 2P + 25 are both divisible by 3 only when P is 1, 4, 7, 10 …).

12 is also what finally MAKES GOOD the claim the old comment made: it matched TrackDropdownRow's 12 so the two would read as one control family, but TrackDropdownRow draws its own border at its own bounds with no overshoot, so matching the number was giving 22 against its 12. Matching the ring-to-text distance is what matches the look.

Default Value
  • 2

(static, constant) ROW_LINE_GAP

Gap between the row's two lines.

Default Value
  • 6

(static, constant) ROW_SPACING

Vertical gap between rows, and the reason the lists can no longer be flush.

The list draws the app's focus indicator (source/utils/listTheme.bs), and that 9-patch is drawn EXPANDED ~10px beyond the row on every side — measured on a Stick 4K: a 72px row's ring spanned 92px. With rows flush the ring's edge landed exactly on the neighbouring row's first line of text.

24 for a TWO-LINE row: it is JRLabelList's spacing, so these lists share the rhythm of every other list in the app, and it is divisible by 3 AND 6 — 6 for the design scale, 3 because 1080/720 means an absolute edge divisible by 3 lands on a 720p output row and stays sharp. Row heights are already divisible by 3, so a spacing that is too keeps every row boundary on the grid. See dialogLayout.bs's PIXEL_GRID note: the constant only helps because the absolute origin (306 panel + 190 column + 44 heading = 540) is on the grid.

Default Value
  • 24

(static, constant) ROW_SPACING_COMPACT

…and 12 for the COMPACT one-line row, which is a density decision rather than a second opinion about rhythm. A built-in track's row is 27px (rowHeightCompact at fontSizeSmaller 25 plus 2 x ROW_V_PADDING), so 24 would be very nearly a whole row of air between every entry — and it costs real content: the column's design target of one downloaded file plus six built-in tracks fits at 12 and does not at 24. 12 still clears the focus ring's 10px overshoot and is still divisible by 3 and 6.

The arithmetic is pinned in subtitleLayout.spec.bs rather than restated here, so a future padding or font change fails a test instead of quietly making this paragraph wrong — which is exactly what happened to the numbers it used to carry, written against the 45px row of the pre-padding-trim draft.

Default Value
  • 12

(static, constant) ROW_V_PADDING

Default Value
  • 1

(static, constant) SECTION_GAP

Default Value
  • 18

(static, constant) SECTION_HEADING_HEIGHT

A section heading INSIDE the "on this item" column ("Downloaded files", "In the video file"), and the gap between one section and the next.

Default Value
  • 34

(static, constant) SECTION_HEADING_TOP_GAP

Extra whitespace ABOVE a section heading, so it binds to the rows it labels rather than to the column heading above it.

This is a proximity fix, and the numbers are why it was needed. The column heading is fontSizeMedium (32) in a 44px slot and a section heading is fontSizeSmaller (25) in a 34px slot, so the actual whitespace was 12px above the section heading and 9px below it — near enough to equal that the heading grouped ambiguously, and because it is also the SMALLER of the two it read as a subtitle of the column heading instead of a label for its own list. With one section present that produced a run-on title ("On this item / In the video file") with no content between the two lines to break it up, which is exactly what was reported from device.

Widening the gap above rather than tightening the one below, because 9px is already close: a heading needs to sit nearer its rows than its neighbour, and only one of those two distances had room to move.

Default Value
  • 14

(static, constant) VISIBLE_ROWS

Rows visible in the RESULTS column without scrolling.

SIX, and it is six again only because the row got shorter: at the old 72px row the gaps pushed six rows to 552px against a 486px column, so five was all that fit. Trimming the double padding took the row to 54, and 6 x 54 + 5 x 24 = 444 — which leaves room for the focus ring below the last row too.

Drives the MarkupList's numRows AND the perfect-match filter's visibility threshold — the filter is pointless when the whole result set already fits on screen, so the two must be the same number by construction.

Default Value
  • 6

Methods

(static) actionZoneWidth(hasAction) → {integer}

actionZoneWidth: Width a row reserves for the action icon, ON ITS SECOND LINE.

THE SECOND LINE, AND THAT IS THE POINT. The first line is already fully committed — a title that has to hold "Portuguese (Brazil) · SUBRIP" beside a right-anchored badge zone wide enough for "Hearing impaired" and "Forced" — so taking the icon's strip out of the row as a whole ellipsized the title. Measured on device: it truncated "English · SUBRIP" to "English · SUB…" on a row carrying no badges at all, while 300px of badge zone sat empty beside it. The SECOND line holds only the filename tail (".en.sdh.srt") across the full width, has no badges to collide with, and had ~500px of slack. Only a downloaded file is ever deletable and only a downloaded file has a second line, so the space is always there when the icon is.

RESERVED BY DELETABILITY, NOT BY FOCUS. The icon is only DRAWN while the row is focused, but the space is held whenever the row can be acted on — so focusing a row cannot shift its filename sideways. Every row in the downloaded section reserves the same amount (they share one canDelete), so the reservation never makes the section look ragged.

Parameters:
NameTypeDescription
hasActionboolean

whether this row has an action to offer

Returns:

width to subtract from the row's SECOND line

Type: 
integer

(static) badgeRunWidth(widths, count) → {float}

badgeRunWidth: Total width of the first count badges, spacing included.

Parameters:
NameTypeDescription
widthsobject

badge widths, most significant first

countinteger

how many are shown (from badgesThatFit)

Returns:

width of the right-aligned badge run

Type: 
float

(static) badgeWidth(iconWidth, labelWidth) → {float}

badgeWidth: One badge's rendered width from its measured parts.

Parameters:
NameTypeDescription
iconWidthfloat

0 for a badge without an icon

labelWidthfloat

the label's measured width

Returns:

width the badge occupies in the row

Type: 
float

(static) badgesThatFit(widths, available) → {integer}

badgesThatFit: How many LEADING badges fit in available, spacing included.

Badges arrive in significance order, so stopping at the first one that does not fit always drops the least significant. Whole badges only — a badge is shown in full or not at all, never cut mid-word.

Parameters:
NameTypeDescription
widthsobject

badge widths, most significant first

availablefloat

the widest run the row allows

Returns:

number of badges to show

Type: 
integer

(static) detailWidthBeside(rowWidth, runWidth) → {float}

detailWidthBeside: Width left for a row's text beside its badge run.

No badges means no gutter: the text takes the row's whole usable width.

Parameters:
NameTypeDescription
rowWidthinteger

full width of the row

runWidthfloat

badgeRunWidth() of what the row shows

Returns:

text width, always at least 1

Type: 
float

(static) existingColumnHeight() → {integer}

existingColumnHeight: Room the "on this item" column has for its sections.

From the bottom of its heading to the panel's clip. Derived rather than written down, so moving the columns cannot silently push a section past the edge where it would render as a half-row.

Returns:

available height in pixels

Type: 
integer

(static) maxBadgeRunWidth(rowWidth) → {float}

maxBadgeRunWidth: The widest badge run a row allows before its text would drop below ROW_DETAIL_MIN_WIDTH.

Parameters:
NameTypeDescription
rowWidthinteger

full width of the row

Returns:

available width for badges, never negative

Type: 
float

(static) rowHeight(fontSmaller, fontSmallest) → {integer}

rowHeight: Total height of one subtitle row.

Exactly two lines, and that is a shape decision rather than an arithmetic one: the first draft reserved a third line for a badge strip on EVERY row so both columns would stay a uniform height, which left rows without badges looking broken next to rows with them. Badges now sit on the second line beside the detail text, so a badge-free row is correctly spaced by construction and the reserved strip is gone.

Pure by construction — the font sizes are passed in rather than read off m.global — so it is unit-testable and callable from either component's scope.

Parameters:
NameTypeDescription
fontSmallerinteger

constants.fontSizeSmaller (first line)

fontSmallestinteger

constants.fontSizeSmallest (second line)

Returns:

row height in pixels

Type: 
integer

(static) rowHeightCompact(fontSmaller) → {integer}

rowHeightCompact: A one-line row.

Used for a subtitle built into the video file, which has nothing to put on a second line: it has no filename of its own, and the section heading above it already says where it comes from. Giving it the two-line height anyway is what the first draft of this column did — every such row then carried the word "Embedded" purely to fill the space, which made the least informative text on screen the most repeated.

Parameters:
NameTypeDescription
fontSmallerinteger

constants.fontSizeSmaller (the single line)

Returns:

row height in pixels

Type: 
integer

(static) rowsHeight(rows, rowHeight, spacing) → {integer}

rowsHeight: What n rows actually occupy, gaps included.

Gaps sit BETWEEN rows, so n rows have n-1 of them. Spacing is a PARAMETER rather than read from the constant, because the two row shapes use different gaps; passing it makes each call site say which rhythm it is stacking.

Parameters:
NameTypeDescription
rowsinteger
rowHeightinteger
spacinginteger
Returns:
Type: 
integer

(static) rowsThatFit(available, rowHeight, spacing) → {integer}

rowsThatFit: The most whole rows, with their gaps, that fit in available.

The inverse of rowsHeight, and it must stay its inverse: solving n*h + (n-1)*s <= available for n gives (available + s) / (h + s). The two are used on opposite sides of the same decision — one rations rows to a height, the other places them — so a disagreement is a clipped row no build can see.

Parameters:
NameTypeDescription
availableinteger
rowHeightinteger
spacinginteger
Returns:
Type: 
integer

(static) sectionHeadingSpace() → {integer}

sectionHeadingSpace: Total vertical cost of one section heading.

The top gap and the heading slot are always spent together — placeSection cannot draw one without the other — so they are added up in ONE place and every consumer asks here. Summing them at each of the four sites in sectionRowCounts() is how a section ends up drawing a row past the panel's clip: miss one and the arithmetic under-counts, which is invisible in a build and has already produced that bug twice in this column.

Returns:

pixels from the top of a section to the top of its rows

Type: 
integer

(static) sectionRowCounts(downloadedCount, embeddedCount, downloadedRowHeight, embeddedRowHeight) → {object}

sectionRowCounts: How many rows each "on this item" section may render.

Both sections compete for one fixed column, and neither may overflow the panel's clip — a list that renders one row past the edge is the exact bug the results column shipped with, and it is invisible in a build.

The rule when they cannot both fit in full: every non-empty section is guaranteed its heading plus ONE row, and the downloaded section gets first claim on whatever is left. Downloaded files are the actionable ones — they are what you came here to manage and the only ones you can delete — while a built-in track is reference information. A section that had to be trimmed still scrolls, so nothing becomes unreachable; it is only the number visible at rest that is being rationed.

⚠️ THAT LAST SENTENCE IS A CLAIM ON THE PANEL, NOT A PROPERTY OF THIS FUNCTION, and it was false for two months. Rationing is only honest while BOTH sections can take focus, because a MarkupList that cannot be focused cannot be scrolled — and the built-in section was deliberately not a focus stop, so on a 3-downloaded/8-embedded item four tracks were simply unreachable. SubtitlePanel.canFocusDownloaded/canFocusEmbedded now gate on rows alone for exactly this reason. Re-narrow either of them and this function silently starts hiding content again.

Returning zero for a section means it has no content, NOT that it was squeezed out: a section with content always keeps at least one row.

Parameters:
NameTypeDescription
downloadedCountinteger

external subtitles on the item

embeddedCountinteger

subtitles inside the video file

downloadedRowHeightinteger

height of a two-line row

embeddedRowHeightinteger

height of a one-line row

Returns:

{ downloadedRows, embeddedRows }

Type: 
object