@@ -73,6 +73,39 @@ const layoutLines = (
7373
7474type layoutParagraphEngines = Pick < Engines , 'linebreaker' > ;
7575
76+ // Mirror Firefox's `text-wrap: balance` cap. Above this, the spec lets us fall
77+ // back to plain wrap to avoid pathological binary-search cost on body copy.
78+ const BALANCE_LINE_LIMIT = 10 ;
79+ // Width precision (pt) for the balance binary search.
80+ const BALANCE_PRECISION = 1 ;
81+
82+ /**
83+ * Find the smallest container width that still yields the same number of
84+ * wrapped lines as the natural width. Equalizes line lengths for
85+ * `text-wrap: balance` (titles, short headings).
86+ */
87+ const computeBalancedWidth = (
88+ linebreak : ReturnType < Engines [ 'linebreaker' ] > ,
89+ paragraph : AttributedString ,
90+ width : number ,
91+ naturalLineCount : number ,
92+ ) : number => {
93+ let lo = 0 ;
94+ let hi = width ;
95+
96+ while ( hi - lo > BALANCE_PRECISION ) {
97+ const mid = ( lo + hi ) / 2 ;
98+ const lines = linebreak ( paragraph , [ mid ] ) ;
99+ if ( lines . length === naturalLineCount ) {
100+ hi = mid ;
101+ } else {
102+ lo = mid ;
103+ }
104+ }
105+
106+ return hi ;
107+ } ;
108+
76109/**
77110 * Performs line breaking and layout
78111 *
@@ -92,11 +125,37 @@ const layoutParagraph = (
92125 const height = stringHeight ( paragraph ) ;
93126 const indent = paragraph . runs ?. [ 0 ] ?. attributes ?. indent || 0 ;
94127 const rects = generateLineRects ( container , height ) ;
128+ const linebreak = engines . linebreaker ( options ) ;
129+
130+ let availableWidths : number [ ] ;
131+
132+ if ( options . textWrap === 'nowrap' ) {
133+ availableWidths = [ Infinity ] ;
134+ } else if ( options . textWrap === 'balance' ) {
135+ const naturalWidths = rects . map ( ( r ) => r . width ) ;
136+ const naturalLines = linebreak ( paragraph , naturalWidths ) ;
137+
138+ if (
139+ naturalLines . length > 1 &&
140+ naturalLines . length <= BALANCE_LINE_LIMIT
141+ ) {
142+ const balanced = computeBalancedWidth (
143+ linebreak ,
144+ paragraph ,
145+ naturalWidths [ 0 ] ,
146+ naturalLines . length ,
147+ ) ;
148+ availableWidths = [ balanced ] ;
149+ } else {
150+ availableWidths = naturalWidths ;
151+ availableWidths . unshift ( availableWidths [ 0 ] - indent ) ;
152+ }
153+ } else {
154+ availableWidths = rects . map ( ( r ) => r . width ) ;
155+ availableWidths . unshift ( availableWidths [ 0 ] - indent ) ;
156+ }
95157
96- const availableWidths = rects . map ( ( r ) => r . width ) ;
97- availableWidths . unshift ( availableWidths [ 0 ] - indent ) ;
98-
99- const lines = engines . linebreaker ( options ) ( paragraph , availableWidths ) ;
158+ const lines = linebreak ( paragraph , availableWidths ) ;
100159
101160 return layoutLines ( rects , lines , indent ) ;
102161 } ;
0 commit comments