nx_layout_block.nx source
↩ module page · 2503 lines · 124638 B
1// nx_layout_block.nx -- normal-flow block layout for the Nishi
2// browser. Phase 3 second primitive of NISHI_BROWSER_ROADMAP. Walks
3// a LayoutBox tree (from nx_layout_box, shipped previously) top-down,
4// queries each box's CssComputedDecl slice (from nx_css_apply, Phase 2)
5// for width / height / margin / padding, and assigns concrete
6// (x, y, w, h) integer pixels.
7//
8// Subset implemented:
9// - BLOCK boxes only (INLINE / INLINE_BLOCK handled by nx_layout_inline,
10// queued)
11// - Normal flow ONLY (no positioned / float / grid / flex)
12// - content-box sizing (CSS default; box-sizing: border-box is
13// Phase 3b queued)
14// - Per-box style queries via nx_css_lookup_property + value
15// decode via nx_css_dimension_to_px
16// - margin-top, margin-bottom, padding-top, padding-bottom
17// (horizontal margin/padding deferred to Phase 3b -- horizontal
18// layout currently sets w = parent.content_w for all blocks)
19// - Explicit width / height declarations consume cascade value;
20// auto height = sum of children heights
21//
22// What it does NOT handle (Phase 3b queued):
23// - margin / padding on left / right axes
24// - margin collapse between adjacent block siblings
25// - border-top / border-bottom contribution to height
26// - width: auto with horizontal centering via margin: auto
27// - min-width / max-width / min-height / max-height
28// - height: percentage of parent (needs parent.h resolved first)
29// - INLINE child layout (inline boxes inside block context)
30// - float / clear
31// - positioned (relative / absolute / fixed / sticky)
32// - overflow / scroll
33//
34// Per cardinal feedback-honest-perf-verdict-no-aspirational-claims:
35// gap list is EXACT.
36//
37// genealogy_id: w3c_css_box_model_module_level_3_normal_flow +
38// substrate_browser_phase_3_block_layout
39// lineage_id: nishi_browser_layout_block_v1
40//
41// nx_safety_envelope:
42// intended_use: "Block layout normal flow -- browser layout"
43// sil_target: SIL1
44// evidence: [W3C_CSS_Box_Model_canonical_basis,
45// bounded_tree_walk,
46// integer_px_arithmetic_no_FP]
47// verdict: NOT_YET_EVALUATED
48
49import "nx_syscalls.nx"
50import "nx_text_wrap.nx"
51import "nx_font8x8.nx" // measured-text mode: proportional advances from the font's metrics SSOT
52import "nx_font.nx" // VECTOR metrics (font_adv_em): opt-in vec-measure mode so layout reserves the
53 // SAME widths the vector paint draws (S21: bitmap-reserve vs vector-draw drift
54 // put gaps around every styled inline span/link)
55import "nx_css_tokenize.nx"
56import "nx_css_parse.nx"
57import "nx_css_selector_match.nx"
58import "nx_css_apply.nx"
59import "nx_css_number_parse.nx"
60import "nx_css_dimension_to_px.nx"
61import "nx_layout_box.nx"
62const NX_MAGIC_100000: i64 = 100000
63const NX_MAGIC_37795: i64 = 37795
64const NX_MAGIC_10000: i64 = 10000
65const NX_MAGIC_65536: i64 = 65536
66const NX_MAGIC_1000000000: i64 = 1000000000
67const NX_MAGIC_4096: i64 = 4096
68
69// Layout context bundle. Caller-supplied; pointers shared across the
70// recursive walk so signatures stay under the nxc2 16-arg parser
71// limit per [[feedback-nishilang-16-arg-function-limit]].
72struct LayoutCtx {
73 tree: *LayoutTree,
74 computed: *CssComputedDecl,
75 n_computed: i64,
76 src: *u8,
77 viewport_w: i64,
78 resolve_ctx: *CssResolveCtx,
79 text_measured: i64, // 1 = PROPORTIONAL text sizing/wrap (font8x8 measured advances); 0 = legacy 9px cells
80 font_tab: i64 // cached font8x8_table() when text_measured=1 (as i64; 0 = lazily filled)
81}
82
83const NX_LAYOUT_CTX_BYTES: i64 = 64
84
85// Property names live in a small caller-supplied byte buffer so the
86// cascade lookups can byte-equal them. Offsets are fixed by
87// _layout_property_table_init below.
88struct LayoutPropTable {
89 src: *u8,
90 width_off: i64,
91 width_len: i64,
92 height_off: i64,
93 height_len: i64,
94 margin_top_off: i64,
95 margin_top_len: i64,
96 margin_bottom_off: i64,
97 margin_bottom_len: i64,
98 margin_left_off: i64,
99 margin_left_len: i64,
100 margin_right_off: i64,
101 margin_right_len: i64,
102 padding_top_off: i64,
103 padding_top_len: i64,
104 padding_bottom_off: i64,
105 padding_bottom_len: i64,
106 padding_left_off: i64,
107 padding_left_len: i64,
108 padding_right_off: i64,
109 padding_right_len: i64
110}
111
112const NX_LAYOUT_PROP_TABLE_BYTES: i64 = 168
113
114// ---- helpers ----
115
116func _layout_put_byte(buf: *u8, i: i64, v: i64) -> i64 {
117 buf[i] = v as u8
118 return 0
119}
120
121// Bundle the canonical property name bytes into the table. The
122// table's `src` buffer must be at least 64 bytes.
123func nx_layout_prop_table_init(t: *LayoutPropTable, buf: *u8) -> i64 {
124 t.src = buf
125 // "width" offset 0 len 5
126 _layout_put_byte(buf, 0, 119)
127 _layout_put_byte(buf, 1, 105)
128 _layout_put_byte(buf, 2, 100)
129 _layout_put_byte(buf, 3, 116)
130 _layout_put_byte(buf, 4, 104)
131 t.width_off = 0
132 t.width_len = 5
133 // "height" offset 5 len 6
134 _layout_put_byte(buf, 5, 104)
135 _layout_put_byte(buf, 6, 101)
136 _layout_put_byte(buf, 7, 105)
137 _layout_put_byte(buf, 8, 103)
138 _layout_put_byte(buf, 9, 104)
139 _layout_put_byte(buf, 10, 116)
140 t.height_off = 5
141 t.height_len = 6
142 // "margin-top" offset 11 len 10
143 _layout_put_byte(buf, 11, 109)
144 _layout_put_byte(buf, 12, 97)
145 _layout_put_byte(buf, 13, 114)
146 _layout_put_byte(buf, 14, 103)
147 _layout_put_byte(buf, 15, 105)
148 _layout_put_byte(buf, 16, 110)
149 _layout_put_byte(buf, 17, 45)
150 _layout_put_byte(buf, 18, 116)
151 _layout_put_byte(buf, 19, 111)
152 _layout_put_byte(buf, 20, 112)
153 t.margin_top_off = 11
154 t.margin_top_len = 10
155 // "margin-bottom" offset 21 len 13
156 _layout_put_byte(buf, 21, 109)
157 _layout_put_byte(buf, 22, 97)
158 _layout_put_byte(buf, 23, 114)
159 _layout_put_byte(buf, 24, 103)
160 _layout_put_byte(buf, 25, 105)
161 _layout_put_byte(buf, 26, 110)
162 _layout_put_byte(buf, 27, 45)
163 _layout_put_byte(buf, 28, 98)
164 _layout_put_byte(buf, 29, 111)
165 _layout_put_byte(buf, 30, 116)
166 _layout_put_byte(buf, 31, 116)
167 _layout_put_byte(buf, 32, 111)
168 _layout_put_byte(buf, 33, 109)
169 t.margin_bottom_off = 21
170 t.margin_bottom_len = 13
171 // "padding-top" offset 34 len 11
172 _layout_put_byte(buf, 34, 112)
173 _layout_put_byte(buf, 35, 97)
174 _layout_put_byte(buf, 36, 100)
175 _layout_put_byte(buf, 37, 100)
176 _layout_put_byte(buf, 38, 105)
177 _layout_put_byte(buf, 39, 110)
178 _layout_put_byte(buf, 40, 103)
179 _layout_put_byte(buf, 41, 45)
180 _layout_put_byte(buf, 42, 116)
181 _layout_put_byte(buf, 43, 111)
182 _layout_put_byte(buf, 44, 112)
183 t.padding_top_off = 34
184 t.padding_top_len = 11
185 // "padding-bottom" offset 45 len 14
186 _layout_put_byte(buf, 45, 112)
187 _layout_put_byte(buf, 46, 97)
188 _layout_put_byte(buf, 47, 100)
189 _layout_put_byte(buf, 48, 100)
190 _layout_put_byte(buf, 49, 105)
191 _layout_put_byte(buf, 50, 110)
192 _layout_put_byte(buf, 51, 103)
193 _layout_put_byte(buf, 52, 45)
194 _layout_put_byte(buf, 53, 98)
195 _layout_put_byte(buf, 54, 111)
196 _layout_put_byte(buf, 55, 116)
197 _layout_put_byte(buf, 56, 116)
198 _layout_put_byte(buf, 57, 111)
199 _layout_put_byte(buf, 58, 109)
200 t.padding_bottom_off = 45
201 t.padding_bottom_len = 14
202 // "margin-left" offset 59 len 11
203 _layout_put_byte(buf, 59, 109)
204 _layout_put_byte(buf, 60, 97)
205 _layout_put_byte(buf, 61, 114)
206 _layout_put_byte(buf, 62, 103)
207 _layout_put_byte(buf, 63, 105)
208 _layout_put_byte(buf, 64, 110)
209 _layout_put_byte(buf, 65, 45)
210 _layout_put_byte(buf, 66, 108)
211 _layout_put_byte(buf, 67, 101)
212 _layout_put_byte(buf, 68, 102)
213 _layout_put_byte(buf, 69, 116)
214 t.margin_left_off = 59
215 t.margin_left_len = 11
216 // "margin-right" offset 70 len 12
217 _layout_put_byte(buf, 70, 109)
218 _layout_put_byte(buf, 71, 97)
219 _layout_put_byte(buf, 72, 114)
220 _layout_put_byte(buf, 73, 103)
221 _layout_put_byte(buf, 74, 105)
222 _layout_put_byte(buf, 75, 110)
223 _layout_put_byte(buf, 76, 45)
224 _layout_put_byte(buf, 77, 114)
225 _layout_put_byte(buf, 78, 105)
226 _layout_put_byte(buf, 79, 103)
227 _layout_put_byte(buf, 80, 104)
228 _layout_put_byte(buf, 81, 116)
229 t.margin_right_off = 70
230 t.margin_right_len = 12
231 // "padding-left" offset 82 len 12
232 _layout_put_byte(buf, 82, 112)
233 _layout_put_byte(buf, 83, 97)
234 _layout_put_byte(buf, 84, 100)
235 _layout_put_byte(buf, 85, 100)
236 _layout_put_byte(buf, 86, 105)
237 _layout_put_byte(buf, 87, 110)
238 _layout_put_byte(buf, 88, 103)
239 _layout_put_byte(buf, 89, 45)
240 _layout_put_byte(buf, 90, 108)
241 _layout_put_byte(buf, 91, 101)
242 _layout_put_byte(buf, 92, 102)
243 _layout_put_byte(buf, 93, 116)
244 t.padding_left_off = 82
245 t.padding_left_len = 12
246 // "padding-right" offset 94 len 13
247 _layout_put_byte(buf, 94, 112)
248 _layout_put_byte(buf, 95, 97)
249 _layout_put_byte(buf, 96, 100)
250 _layout_put_byte(buf, 97, 100)
251 _layout_put_byte(buf, 98, 105)
252 _layout_put_byte(buf, 99, 110)
253 _layout_put_byte(buf, 100, 103)
254 _layout_put_byte(buf, 101, 45)
255 _layout_put_byte(buf, 102, 114)
256 _layout_put_byte(buf, 103, 105)
257 _layout_put_byte(buf, 104, 103)
258 _layout_put_byte(buf, 105, 104)
259 _layout_put_byte(buf, 106, 116)
260 t.padding_right_off = 94
261 t.padding_right_len = 13
262 return 0
263}
264
265// Look up a property for the box at `box_idx` and resolve its
266// dimension value to integer px. Returns -1 if the property is not
267// in the cascade (caller substitutes default).
268//
269// Note: nx_css_lookup_property uses the caller's `src` buffer for
270// byte-equality. We pass the CASCADE source buffer (where decl
271// property names live) -- but the lookup compares against the
272// `prop_*` arguments which point into the LayoutPropTable buffer.
273// To make this byte-equal work, the table buffer's bytes must match
274// the cascade buffer's property-name bytes for the queried names.
275// The smoke fixture arranges for property names to be co-located in
276// the same buffer; production integration ensures the same.
277func _layout_lookup_px(ctx: *LayoutCtx, table_src: *u8,
278 box_idx: i64,
279 prop_off: i64, prop_len: i64) -> i64 {
280 // Walk computed array in reverse for source-order winner.
281 var i: i64 = ctx.n_computed - 1
282 while i >= 0 {
283 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
284 if cd.element_idx == box_idx {
285 if cd.prop_len == prop_len {
286 var same: i64 = 1
287 var k: i64 = 0
288 while k < prop_len {
289 let ca: i64 = (ctx.src[cd.prop_off + k] as i64) & 255
290 let cb: i64 = (table_src[prop_off + k] as i64) & 255
291 if ca != cb {
292 same = 0
293 k = prop_len
294 } else {
295 k = k + 1
296 }
297 }
298 if same == 1 {
299 if cd.val_kind == NX_CSS_VAL_DIMENSION {
300 return nx_css_dimension_to_px(ctx.src,
301 cd.val_off, cd.val_len,
302 cd.unit_off, cd.unit_len, ctx.resolve_ctx)
303 }
304 return -1
305 }
306 }
307 }
308 i = i - 1
309 }
310 return -1
311}
312
313// HEIGHT-specific lookup: a PERCENTAGE height against a parent whose height is not explicit is AUTO
314// (CSS 2.1 s10.5). The generic lookup resolves every % against resolve_ctx.parent_dimension_px -- which
315// br_layout seeds with the VIEWPORT WIDTH -- so wikipedia's `html,body{height:100%}` manufactured
316// ~1,050px-tall mostly-empty containers at the top of every styled page = the CSS-on first-screen VOID
317// (region x-ray, 2026-07-28: boxes 0/1/35/39 all h=1024..1052 from height:100%). We never track a
318// definite parent height, so %-height == auto, always.
319func _layout_lookup_height_px(ctx: *LayoutCtx, table_src: *u8,
320 box_idx: i64,
321 prop_off: i64, prop_len: i64) -> i64 {
322 var i: i64 = ctx.n_computed - 1
323 while i >= 0 {
324 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
325 if cd.element_idx == box_idx {
326 if cd.prop_len == prop_len {
327 var same: i64 = 1
328 var k: i64 = 0
329 while k < prop_len {
330 let ca: i64 = (ctx.src[cd.prop_off + k] as i64) & 255
331 let cb: i64 = (table_src[prop_off + k] as i64) & 255
332 if ca != cb { same = 0; k = prop_len } else { k = k + 1 }
333 }
334 if same == 1 {
335 if cd.unit_len == 1 { if (ctx.src[cd.unit_off] & 0xff) == 37 { return -1 } } // % -> auto
336 if cd.val_kind == NX_CSS_VAL_DIMENSION {
337 let hv: i64 = nx_css_dimension_to_px(ctx.src,
338 cd.val_off, cd.val_len,
339 cd.unit_off, cd.unit_len, ctx.resolve_ctx)
340 // SANITY (linkedin 2^62 y-leak, 2026-07-28): dimension_to_px overflowed to
341 // ~i64-max on an exotic height value; that height became align-items'
342 // cross_size and shifted "Join now" to y=2^62 (off-page forever). An absurd
343 // computed height is INVALID per CSS -> treated as auto, never propagated.
344 if hv > NX_MAGIC_100000 { return -1 }
345 if hv < 0 { return -1 }
346 return hv
347 }
348 return -1
349 }
350 }
351 }
352 i = i - 1
353 }
354 return -1
355}
356
357// "Unspecified" sentinel for margins -- distinct from a real negative margin (now a valid value). 2^30;
358// no real margin px reaches it. _layout_lookup_px returns -1 for not-found, which would collide with a
359// genuine -1px, so margins use this dedicated lookup instead.
360const NX_MARGIN_UNSET: i64 = 1073741824
361
362// Like _layout_lookup_px but for MARGINS: returns the resolved px (POSITIVE OR NEGATIVE), or
363// NX_MARGIN_UNSET when the property is absent / not a dimension (e.g. `auto`). Negative margins are a
364// valid value (box overlap), so -- unlike width/padding -- the caller must NOT clamp them to 0.
365func _layout_margin_px(ctx: *LayoutCtx, table_src: *u8, box_idx: i64, prop_off: i64, prop_len: i64) -> i64 {
366 var i: i64 = ctx.n_computed - 1
367 while i >= 0 {
368 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
369 if cd.element_idx == box_idx {
370 if cd.prop_len == prop_len {
371 var same: i64 = 1
372 var k: i64 = 0
373 while k < prop_len {
374 let ca: i64 = (ctx.src[cd.prop_off + k] as i64) & 255
375 let cb: i64 = (table_src[prop_off + k] as i64) & 255
376 if ca != cb { same = 0; k = prop_len } else { k = k + 1 }
377 }
378 if same == 1 {
379 if cd.val_kind == NX_CSS_VAL_DIMENSION {
380 return nx_css_dimension_to_px(ctx.src, cd.val_off, cd.val_len, cd.unit_off, cd.unit_len, ctx.resolve_ctx)
381 }
382 return NX_MARGIN_UNSET
383 }
384 }
385 }
386 i = i - 1
387 }
388 return NX_MARGIN_UNSET
389}
390
391// CSS box-model side resolver with SHORTHAND support (`padding: a b c d` / `margin: a b c d`) -- the S21
392// height-axis rung: real pages set the box model almost entirely through shorthands (th,td{padding:8px
393// 12px}, h2{margin:1.6em 0 .5em}), which the longhand-only lookups above can't see, so we rendered ~40%
394// denser than Chrome. ONE reverse scan matches EITHER the side's longhand OR the shorthand -- the LAST
395// declaration in cascade order wins between them (the same origin-order law as br_comp_bg: the UA sheet's
396// p{margin-top:14px} longhand must LOSE to a LATER author p{margin:10px 0 30px} shorthand, and vice
397// versa). side: 0=top 1=right 2=bottom 3=left. Token units: px / em / rem in Q3 fractional math (".5em"
398// at 16px = 8) + bare 0; %/auto/unknown -> miss (the declaration still SHADOWS earlier ones, per cascade).
399func _layout_boxside_px(ctx: *LayoutCtx, table_src: *u8, box_idx: i64, lh_off: i64, lh_len: i64, pname: *u8, pnlen: i64, side: i64, miss: i64) -> i64 {
400 var i: i64 = ctx.n_computed - 1
401 while i >= 0 {
402 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
403 if cd.element_idx == box_idx {
404 // the side's LONGHAND (name bytes from the prop table)
405 if cd.prop_len == lh_len {
406 var samel: i64 = 1
407 var kl: i64 = 0
408 while kl < lh_len {
409 if ((ctx.src[cd.prop_off + kl] as i64) & 255) != ((table_src[lh_off + kl] as i64) & 255) { samel = 0; kl = lh_len } else { kl = kl + 1 }
410 }
411 if samel == 1 {
412 if cd.val_kind == NX_CSS_VAL_DIMENSION {
413 return nx_css_dimension_to_px(ctx.src, cd.val_off, cd.val_len, cd.unit_off, cd.unit_len, ctx.resolve_ctx)
414 }
415 return miss
416 }
417 }
418 // the SHORTHAND
419 if cd.prop_len == pnlen {
420 var same: i64 = 1
421 var k: i64 = 0
422 while k < pnlen {
423 if ((ctx.src[cd.prop_off + k] as i64) & 255) != ((pname[k] as i64) & 255) { same = 0; k = pnlen } else { k = k + 1 }
424 }
425 if same == 1 {
426 // tokenize the raw value span on spaces -> up to 4 (off,len) pairs
427 let toff: *i64 = sys_mmap(64) as *i64
428 let tlen: *i64 = sys_mmap(64) as *i64
429 var ntok: i64 = 0
430 var over: i64 = 0
431 var p: i64 = cd.val_off
432 let e: i64 = cd.val_off + cd.val_len
433 var scanning: i64 = 1
434 while scanning == 1 {
435 var sk: i64 = 1
436 while sk == 1 {
437 if p >= e { sk = 0 }
438 else { if ((ctx.src[p] as i64) & 255) == 32 { p = p + 1 } else { sk = 0 } }
439 }
440 if p >= e { scanning = 0 }
441 else {
442 var q: i64 = p
443 var sc: i64 = 1
444 while sc == 1 {
445 if q >= e { sc = 0 }
446 else { if ((ctx.src[q] as i64) & 255) != 32 { q = q + 1 } else { sc = 0 } }
447 }
448 if ntok < 4 { toff[ntok] = p; tlen[ntok] = q - p; ntok = ntok + 1 }
449 else { over = 1; scanning = 0 }
450 p = q
451 }
452 }
453 if ntok < 1 { return miss }
454 if over == 1 { return miss }
455 // CSS side mapping: 1 val -> all; 2 -> [vertical, horizontal]; 3 -> [top, horiz, bottom]; 4 -> [t r b l]
456 var idx: i64 = 0
457 if ntok == 2 { if side == 1 { idx = 1 } if side == 3 { idx = 1 } }
458 if ntok == 3 { if side == 1 { idx = 1 } if side == 3 { idx = 1 } if side == 2 { idx = 2 } }
459 if ntok == 4 { idx = side }
460 let to: i64 = toff[idx]
461 let tl: i64 = tlen[idx]
462 // split token into number span + unit span ('-', digits, '.' = number)
463 var nl: i64 = 0
464 var ns: i64 = 1
465 while ns == 1 {
466 if nl >= tl { ns = 0 }
467 else {
468 let c: i64 = (ctx.src[to + nl] as i64) & 255
469 var isnum: i64 = 0
470 if c >= 48 { if c <= 57 { isnum = 1 } }
471 if c == 46 { isnum = 1 }
472 if c == 45 { if nl == 0 { isnum = 1 } }
473 if isnum == 1 { nl = nl + 1 } else { ns = 0 }
474 }
475 }
476 if nl == 0 { return miss }
477 let q3: i64 = nx_css_number_parse(ctx.src, to, nl)
478 if q3 == NX_CSS_NUMBER_PARSE_ERROR { return miss }
479 let ul: i64 = tl - nl
480 if ul == 0 {
481 if q3 == 0 { return 0 }
482 return miss
483 }
484 if ul == 2 {
485 let u0: i64 = (ctx.src[to+nl] as i64) & 255
486 let u1: i64 = (ctx.src[to+nl+1] as i64) & 255
487 if u0 == 112 { if u1 == 120 { return q3 / 1000 } } // px
488 if u0 == 101 { if u1 == 109 { // em
489 let fs: i64 = _layout_fontsize_inherited(ctx, box_idx)
490 return (q3 * fs) / 1000
491 } }
492 return miss
493 }
494 if ul == 3 {
495 let r0: i64 = (ctx.src[to+nl] as i64) & 255
496 let r1: i64 = (ctx.src[to+nl+1] as i64) & 255
497 let r2: i64 = (ctx.src[to+nl+2] as i64) & 255
498 if r0 == 114 { if r1 == 101 { if r2 == 109 { // rem
499 return (q3 * ctx.resolve_ctx.root_font_size_px) / 1000
500 } } }
501 }
502 return miss
503 }
504 }
505 }
506 i = i - 1
507 }
508 return miss
509}
510
511// ---- inline formatting context (line-box flow) ----
512// Inline-level children (INLINE/INLINE_BLOCK boxes + TEXT runs) flow horizontally on a line and
513// wrap when the pen exceeds the available width, instead of each consuming a full-width block line.
514// This is what makes paragraph prose read like prose rather than one-fragment-per-line. v1 wraps at
515// inline-box / text-node boundaries (word-level wrap inside a single long run is a queued
516// refinement) and advances each line by a fixed NX_INLINE_LINE_H (no baseline alignment yet).
517struct InlinePen {
518 pen_x: i64,
519 pen_y: i64,
520 line_h: i64,
521 avail_w: i64,
522 origin_x: i64,
523 origin_y: i64,
524 table: i64 // *LayoutPropTable stashed as i64: box-model reads for atomic inline-block
525}
526const NX_INLINE_PEN_BYTES: i64 = 56
527const NX_INLINE_GLYPH_W: i64 = 9 // matches the live browser's 9x15 X font advance (was 6 = paint_text cell; 6 made 9px text overflow its boxes ~1.5x)
528const NX_INLINE_LINE_H: i64 = 17 // matches the 9x15 font height (15px) + 2px leading; was 10
529
530// Find a computed `font-size` (DIMENSION, e.g. "32px") for element_idx; return px or -1. Local
531// "font-size" literal (prop_len==9) -> no LayoutPropTable change. Numeric run parsed (unit separate).
532// ALSO matches the `font` SHORTHAND when its value LEADS with a size ("15px/1.55 system-ui") -- one
533// reverse scan across both properties = correct last-wins cascade between them. The paint side
534// (br_comp_fontsize_inh) implements the IDENTICAL rule so measure==paint holds.
535// Declared font-size for an element, in px, or -1 if none is declared.
536// `pct_out[0]` is set to 1 when the declared value was a PERCENTAGE, in which case the
537// returned number is the percentage itself (e.g. 100 for "100%") and the caller must
538// resolve it against the parent's computed size. Sole caller: _layout_fontsize_inherited.
539// FONT-SIZE VALUE RESOLVER (seq1150, 2026-07-28) -- shared by the measure resolver below AND the
540// paint mirror br_comp_fontsize_own (nx_browser_render imports this file; ONE implementation, both
541// sides, measure==paint by construction). Returns px >= 0, or -1 = UNEVALUABLE (calc()/keywords --
542// CSS drops such a decl; caller falls through to the older cascade decl / inherit). pct_out[0]=1
543// means the return value is a PERCENT of the parent's size (the % and em cases).
544// "15px"/"15" -> 15 "13.5px" -> 13 (TRUNCATE: the fontsize_frac gate's contract)
545// "100%" -> pct 100 "0.875rem" -> 14 (x16, rounded -- was digit-scanned to ZERO,
546// "0.875em" -> pct 87 the fs=0 root behind the vec-measure w=0 column class)
547// "0" -> 0 (legit icon-text hide) "calc(...)"/"small" -> -1 (drop)
548func _lb_lc(b: i64) -> i64 { if b >= 65 { if b <= 90 { return b + 32 } } return b }
549func nx_fontsize_value_px(src: *u8, voff: i64, vlen: i64, uoff: i64, ulen: i64, pct_out: *i64) -> i64 {
550 if vlen <= 0 { return 0 - 1 }
551 let c0: i64 = src[voff] & 0xff
552 var lead_ok: i64 = 0
553 if c0 >= 48 { if c0 <= 57 { lead_ok = 1 } }
554 if c0 == 46 { lead_ok = 1 }
555 if lead_ok == 0 { return 0 - 1 }
556 // integer + up to 3 fraction digits -> thousandths (q3)
557 var ip: i64 = 0
558 var j: i64 = 0
559 var go: i64 = 1
560 while go == 1 {
561 if j >= vlen { go = 0 }
562 else {
563 let c: i64 = src[voff + j] & 0xff
564 if c >= 48 { if c <= 57 { ip = ip * 10 + (c - 48); j = j + 1 } else { go = 0 } } else { go = 0 }
565 }
566 }
567 var fr: i64 = 0
568 if j < vlen { if (src[voff + j] & 0xff) == 46 {
569 j = j + 1
570 var fd: i64 = 0
571 var g2: i64 = 1
572 while g2 == 1 {
573 if j >= vlen { g2 = 0 }
574 else {
575 let c2: i64 = src[voff + j] & 0xff
576 if c2 >= 48 { if c2 <= 57 {
577 if fd == 0 { fr = fr + (c2 - 48) * 100 }
578 if fd == 1 { fr = fr + (c2 - 48) * 10 }
579 if fd == 2 { fr = fr + (c2 - 48) }
580 fd = fd + 1
581 j = j + 1
582 } else { g2 = 0 } } else { g2 = 0 }
583 }
584 }
585 } }
586 let q3: i64 = ip * 1000 + fr
587 // unit: from unit_off/unit_len (DIMENSION tokens carry it separately) with an in-value '%' fallback
588 var ispct: i64 = 0
589 if ulen == 1 { if (src[uoff] & 0xff) == 37 { ispct = 1 } }
590 var us: i64 = 0
591 while us < vlen { if (src[voff + us] & 0xff) == 37 { ispct = 1; us = vlen } else { us = us + 1 } }
592 if ispct == 1 { pct_out[0] = 1; return q3 / 1000 }
593 if ulen == 2 { if (src[uoff] & 0xff) == 101 { if (src[uoff+1] & 0xff) == 109 { // em -> % of parent
594 pct_out[0] = 1
595 return (q3 * 100 + 500) / 1000 // ROUND: 0.875em -> 88% -> 14px of 16 (truncating gave 13)
596 } } }
597 if ulen == 3 { if (src[uoff] & 0xff) == 114 { if (src[uoff+1] & 0xff) == 101 { if (src[uoff+2] & 0xff) == 109 { // rem -> x16 rounded
598 return (q3 * 16 + 500) / 1000
599 } } } }
600 // ABSOLUTE CSS UNITS (CSS Values §5: 1in = 96px = 72pt = 6pc = 2.54cm = 25.4mm). `pt` is NOT
601 // legacy trivia -- news.ycombinator.com sizes its entire page in pt (`font-size:10pt`), and
602 // treating 10pt as 10px instead of 13.3px pushed every run under the OCR legibility cliff:
603 // measured recall 438 -> 0 with author CSS on (region x-ray, 2026-07-28). Unit-less values and
604 // px fall through to the q3 rounding below.
605 if ulen == 2 {
606 let u0: i64 = _lb_lc(src[uoff] & 0xff)
607 let u1: i64 = _lb_lc(src[uoff+1] & 0xff)
608 if u0 == 112 { if u1 == 116 { return (q3 * 4 / 3 + 500) / 1000 } } // pt = 4/3 px
609 if u0 == 112 { if u1 == 99 { return (q3 * 16 + 500) / 1000 } } // pc = 16px
610 if u0 == 105 { if u1 == 110 { return (q3 * 96 + 500) / 1000 } } // in = 96px
611 if u0 == 99 { if u1 == 109 { return (q3 * NX_MAGIC_37795 / 1000 + 500) / 1000 } } // cm = 96/2.54 px
612 if u0 == 109 { if u1 == 109 { return (q3 * NX_MAGIC_37795 / NX_MAGIC_10000 + 500) / 1000 } }// mm = cm/10
613 }
614 return q3 / 1000 // px / unit-less: TRUNCATE ("13.5px" -> 13, the fontsize_frac gate's contract)
615}
616
617func _layout_fontsize_decl_px(ctx: *LayoutCtx, element_idx: i64, pct_out: *i64) -> i64 {
618 let fname: *u8 = "font-size\x00" as *u8
619 let fsh: *u8 = "font\x00" as *u8
620 var i: i64 = ctx.n_computed - 1
621 while i >= 0 {
622 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
623 if cd.element_idx == element_idx {
624 if cd.prop_len == 4 {
625 var same4: i64 = 1
626 var k4: i64 = 0
627 while k4 < 4 {
628 if (ctx.src[cd.prop_off + k4] & 0xff) != (fsh[k4] & 0xff) { same4 = 0; k4 = 4 } else { k4 = k4 + 1 }
629 }
630 if same4 == 1 {
631 // digit-leading value only ("15px/..." -- "italic 15px/..." is the documented v1 gap)
632 let c0: i64 = (ctx.src[cd.val_off] as i64) & 255
633 if c0 >= 48 { if c0 <= 57 {
634 var v4: i64 = 0
635 var j4: i64 = 0
636 var g4: i64 = 1
637 while g4 == 1 {
638 if j4 >= cd.val_len { g4 = 0 }
639 else {
640 let c4: i64 = (ctx.src[cd.val_off + j4] as i64) & 255
641 if c4 >= 48 { if c4 <= 57 { v4 = v4 * 10 + (c4 - 48); j4 = j4 + 1 } else { g4 = 0 } } else { g4 = 0 }
642 }
643 }
644 if v4 > 0 { return v4 }
645 } }
646 }
647 }
648 if cd.prop_len == 9 {
649 var same: i64 = 1
650 var k: i64 = 0
651 while k < 9 {
652 if (ctx.src[cd.prop_off + k] & 0xff) != (fname[k] & 0xff) { same = 0; k = 9 } else { k = k + 1 }
653 }
654 if same == 1 {
655 // ONE resolver for the value (nx_fontsize_value_px above): px/%/em/rem/fractions,
656 // calc()/keywords DROPPED (-1) like CSS drops an unevaluable decl -> fall through
657 // to the older cascade decl / inherit. History this replaces: the digit-scan read
658 // `font-size:100%` as 100px (the 07-27 CSS-on height blowup), `calc(1rem*0.875)`
659 // as 10px, and `0.875rem` as ZERO -- the fs=0 root behind the vec-measure w=0
660 // column class (region x-ray, 2026-07-28).
661 let fv: i64 = nx_fontsize_value_px(ctx.src, cd.val_off, cd.val_len, cd.unit_off, cd.unit_len, pct_out)
662 if fv >= 0 { return fv }
663 }
664 }
665 }
666 i = i - 1
667 }
668 return 0 - 1
669}
670// Inherited effective font-size (px) for a box: nearest ANCESTOR with computed font-size, else 16.
671func _layout_fontsize_inherited(ctx: *LayoutCtx, box_idx: i64) -> i64 {
672 let pct: *i64 = sys_mmap(16) as *i64
673 var cur: i64 = box_idx
674 var safety: i64 = 0
675 while safety < 256 {
676 safety = safety + 1
677 if cur < 0 { return 16 }
678 pct[0] = 0
679 let px: i64 = _layout_fontsize_decl_px(ctx, cur, pct)
680 if px >= 0 {
681 if pct[0] == 0 { return px }
682 // PERCENTAGE (CSS 2.1 §15.7): relative to the PARENT's computed size. Walking on
683 // from the parent rather than recursing keeps this iterative and inside the same
684 // 256-step bound. 100% therefore means "same as parent", which is the whole point
685 // of the universal reset -- not a 100-pixel font.
686 let bp: *LayoutBox = (ctx.tree.boxes as *u8 + (cur as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
687 let parent_px: i64 = _layout_fontsize_inherited(ctx, bp.parent_idx)
688 var r: i64 = (parent_px * px) / 100
689 if r < 1 { r = 1 } // never collapse a font to zero
690 return r
691 }
692 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (cur as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
693 cur = b.parent_idx
694 }
695 return 16
696}
697// Integer glyph scale for a font-size in px. MUST match _paint_scale_for_px in nx_paint_walk_layout
698// so the reserved line height == the painted glyph height (headings don't overlap the next line).
699func _layout_scale_for_px(px: i64) -> i64 {
700 if px < 24 { return 1 }
701 if px < 36 { return 2 }
702 return 3
703}
704
705// Does the box have computed `display: none`? (The page's CSS hides chrome with this.) Local literals
706// for "display"(7)/"none"(4) -> no LayoutPropTable change. A none box takes ZERO space + lays out no
707// children (paint skips it too). The keystone for applying a page's own stylesheet correctly.
708func _layout_display_none(ctx: *LayoutCtx, box_idx: i64) -> i64 {
709 let dn: *u8 = "display\x00" as *u8
710 let nn: *u8 = "none\x00" as *u8
711 var i: i64 = ctx.n_computed - 1
712 while i >= 0 {
713 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
714 if cd.element_idx == box_idx {
715 if cd.prop_len == 7 {
716 var sp: i64 = 1
717 var k: i64 = 0
718 while k < 7 { if (ctx.src[cd.prop_off+k]&0xff) != (dn[k]&0xff) { sp = 0; k = 7 } else { k = k + 1 } }
719 if sp == 1 {
720 if cd.val_len == 4 {
721 var sv: i64 = 1
722 var j: i64 = 0
723 while j < 4 { if (ctx.src[cd.val_off+j]&0xff) != (nn[j]&0xff) { sv = 0; j = 4 } else { j = j + 1 } }
724 if sv == 1 { return 1 }
725 }
726 }
727 }
728 }
729 i = i - 1
730 }
731 return 0
732}
733
734func _il_text_w(b: *LayoutBox, avail_w: i64) -> i64 {
735 var w_px: i64 = b.text_len * NX_INLINE_GLYPH_W
736 if w_px > avail_w { w_px = avail_w }
737 if w_px < 0 { w_px = 0 }
738 return w_px
739}
740
741// Flow one inline-level box (and its inline subtree) into the pen. TEXT boxes are placed and advance
742// the pen; INLINE boxes recurse so their inline descendants share the same line context. Returns 0.
743// CSS line-height resolver (the S21 height-axis remainder): supports `line-height: 1.55` (unitless
744// MULTIPLIER of the element's own font-size -- inherits as the number, so it recomputes against each
745// descendant's fs), `line-height: 24px` (fixed), and the `font` SHORTHAND's /N part ("15px/1.55 ...").
746// One reverse scan per element = last-wins across both properties; nearest ancestor with a decl wins
747// (CSS inheritance). Returns px, or -1 when nothing declares it (callers keep the legacy 17*scale line).
748// The paint side (br_comp_lineheight_px) implements the IDENTICAL rule -- measure==paint law.
749func _layout_lineheight_decl_px(ctx: *LayoutCtx, element_idx: i64, fs: i64) -> i64 {
750 let lname: *u8 = "line-height\x00" as *u8
751 let fsh: *u8 = "font\x00" as *u8
752 var i: i64 = ctx.n_computed - 1
753 while i >= 0 {
754 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
755 if cd.element_idx == element_idx {
756 if cd.prop_len == 11 {
757 var same: i64 = 1
758 var k: i64 = 0
759 while k < 11 {
760 if ((ctx.src[cd.prop_off + k] as i64) & 255) != ((lname[k] as i64) & 255) { same = 0; k = 11 } else { k = k + 1 }
761 }
762 if same == 1 {
763 // number span then optional unit ("1.55" unitless / "24px")
764 var nl: i64 = 0
765 var ns: i64 = 1
766 while ns == 1 {
767 if nl >= cd.val_len { ns = 0 }
768 else {
769 let c: i64 = (ctx.src[cd.val_off + nl] as i64) & 255
770 var isn: i64 = 0
771 if c >= 48 { if c <= 57 { isn = 1 } }
772 if c == 46 { isn = 1 }
773 if isn == 1 { nl = nl + 1 } else { ns = 0 }
774 }
775 }
776 if nl == 0 { return 0 - 1 }
777 let q3: i64 = nx_css_number_parse(ctx.src, cd.val_off, nl)
778 if q3 == NX_CSS_NUMBER_PARSE_ERROR { return 0 - 1 }
779 // px: DIMENSION tokens carry the unit in the SEPARATE unit fields (val = digits only);
780 // swept spans carry it inline. Unitless must be EXACTLY the number (else miss, not 16x).
781 if cd.unit_len == 2 {
782 if ((ctx.src[cd.unit_off] as i64) & 255) == 112 { if ((ctx.src[cd.unit_off + 1] as i64) & 255) == 120 {
783 return q3 / 1000
784 } }
785 }
786 if nl + 1 < cd.val_len {
787 if ((ctx.src[cd.val_off + nl] as i64) & 255) == 112 { if ((ctx.src[cd.val_off + nl + 1] as i64) & 255) == 120 {
788 return q3 / 1000
789 } }
790 }
791 if cd.unit_len == 0 { if nl == cd.val_len { return (q3 * fs) / 1000 } }
792 return 0 - 1
793 }
794 }
795 if cd.prop_len == 4 {
796 var same4: i64 = 1
797 var k4: i64 = 0
798 while k4 < 4 {
799 if ((ctx.src[cd.prop_off + k4] as i64) & 255) != ((fsh[k4] as i64) & 255) { same4 = 0; k4 = 4 } else { k4 = k4 + 1 }
800 }
801 if same4 == 1 {
802 let c0: i64 = (ctx.src[cd.val_off] as i64) & 255
803 if c0 >= 48 { if c0 <= 57 {
804 // skip the leading size token to the '/' (e.g. "15px/1.55 system-ui")
805 var j: i64 = 0
806 var sl: i64 = 0 - 1
807 var g: i64 = 1
808 while g == 1 {
809 if j >= cd.val_len { g = 0 }
810 else {
811 let cj: i64 = (ctx.src[cd.val_off + j] as i64) & 255
812 if cj == 47 { sl = j; g = 0 }
813 else { if cj == 32 { g = 0 } else { j = j + 1 } }
814 }
815 }
816 if sl >= 0 {
817 let vo: i64 = cd.val_off + sl + 1
818 let vmax: i64 = cd.val_len - sl - 1
819 var n2: i64 = 0
820 var g2: i64 = 1
821 while g2 == 1 {
822 if n2 >= vmax { g2 = 0 }
823 else {
824 let c2: i64 = (ctx.src[vo + n2] as i64) & 255
825 var isn2: i64 = 0
826 if c2 >= 48 { if c2 <= 57 { isn2 = 1 } }
827 if c2 == 46 { isn2 = 1 }
828 if isn2 == 1 { n2 = n2 + 1 } else { g2 = 0 }
829 }
830 }
831 if n2 > 0 {
832 let q32: i64 = nx_css_number_parse(ctx.src, vo, n2)
833 if q32 != NX_CSS_NUMBER_PARSE_ERROR {
834 if n2 + 1 < vmax {
835 if ((ctx.src[vo + n2] as i64) & 255) == 112 { if ((ctx.src[vo + n2 + 1] as i64) & 255) == 120 {
836 return q32 / 1000
837 } }
838 }
839 return (q32 * fs) / 1000
840 }
841 }
842 }
843 } }
844 }
845 }
846 }
847 i = i - 1
848 }
849 return 0 - 1
850}
851func _layout_lineheight_px(ctx: *LayoutCtx, box_idx: i64, fs: i64) -> i64 {
852 var cur: i64 = box_idx
853 var guard: i64 = 0
854 while cur >= 0 {
855 if guard > 64 { return 0 - 1 }
856 guard = guard + 1
857 let v: i64 = _layout_lineheight_decl_px(ctx, cur, fs)
858 if v >= 0 { return v }
859 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (cur as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
860 cur = b.parent_idx
861 }
862 return 0 - 1
863}
864
865// VEC-MEASURE mode (opt-in, default OFF so every existing gate is byte-unchanged): when the renderer will
866// paint runs with the proportional VECTOR font (the GUI: vec_headings=2), layout must reserve widths from
867// the SAME metric (font_adv_em) or every styled inline span/link lands at bitmap-width positions while
868// glyphs paint narrower -> the measured gap bug (S21 F3; dominant Wikipedia paint delta). Same module-
869// toggle pattern as the paint side's br_set_fast_vec.
870static l_vec_measure: i64
871func nx_layout_set_vec_measure(v: i64) -> i64 { l_vec_measure = v; return 0 }
872// Y-TRAP (debug instrument, seq linkedin-2^62): when armed, any box whose computed y exceeds 10^9
873// prints its idx + the exact operands so the corrupt ADDEND is named in one run. Never on by default.
874static l_ytrap: i64
875func nx_layout_set_ytrap(v: i64) -> i64 { l_ytrap = v; return 0 }
876func _yt_w(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(2,s,n); return 0 }
877func _yt_n(v: i64) -> i64 { let b: *u8=sys_mmap(28); var m: i64=v; if m<0{_yt_w("-" as *u8);m=0-m} let t: *u8=sys_mmap(28); var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var i: i64=0; while i<k{b[i]=t[k-1-i];i=i+1} sys_write(2,b,k); return 0 }
878
879// ---- TABLE COLUMN LAYOUT (CSS 2.1 §17.5.2 "auto", minimal) -- seq1158, 2026-07-28 --------------
880// Rows are flex containers (UA sheet `tr{display:flex}`) and the flex path gave every auto child an
881// EQUAL share, so a 3-column table rendered as equal thirds regardless of content. On a table-built
882// site (news.ycombinator.com: rank | vote | title) that scatters the cells -- measured 57permille with
883// author CSS on while the page HEIGHT proved the content was all there. Real table layout sizes a
884// column by its MAX-CONTENT across ALL rows and shares that width down the column.
885// The widths live in statics because the row (flex) path is reached through the generic block
886// recursion; they are SAVED/RESTORED around each table so nested tables (HN nests them) are correct.
887static l_tbl_active: i64 // 1 while laying out inside a table whose columns are resolved
888static l_tbl_idx: i64 // box index of that table (a row only uses columns of ITS OWN table)
889static l_tbl_ncols: i64
890static l_tbl_colw_p: i64 // *i64 column widths (as i64; cast at use)
891const NX_TBL_MAXCOLS: i64 = 64
892
893// vector width of text[start..end) at font-size fs (1x layout units) -- the EXACT sum the vector paint
894// advances by (font_vec_adv_px2 = font_adv_em*empx/1000 with empx = fs*paint_scale; layout is 1x).
895func _layout_vec_text_w(t: *u8, start: i64, end: i64, fs: i64) -> i64 {
896 var w: i64 = 0
897 var i: i64 = start
898 while i < end { w = w + (font_adv_em((t[i] as i64) & 255) * fs) / 1000; i = i + 1 }
899 return w
900}
901
902// Byte-equal a property name at src[off..off+n) against `name`. 1 if equal, 0 otherwise.
903func _il_prop_is(src: *u8, off: i64, name: *u8, n: i64) -> i64 {
904 var k: i64 = 0
905 while k < n { if (src[off+k]&0xff) != (name[k]&0xff) { return 0 } k = k + 1 }
906 return 1
907}
908
909// Border width (px) for a box: leading integer of the `border` shorthand ("1px solid #26304a" -> 1) or
910// `border-width`. Reverse cascade scan of ctx.computed (winner first). 0 if none. Used to size + inset an
911// atomic inline-block so its border-box matches what the painter strokes (br_comp_border).
912func _layout_border_w(ctx: *LayoutCtx, box_idx: i64) -> i64 {
913 var i: i64 = ctx.n_computed - 1
914 while i >= 0 {
915 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
916 if cd.element_idx == box_idx {
917 var hit: i64 = 0
918 if cd.prop_len == 6 { if _il_prop_is(ctx.src, cd.prop_off, "border\x00" as *u8, 6) == 1 { hit = 1 } }
919 if cd.prop_len == 12 { if _il_prop_is(ctx.src, cd.prop_off, "border-width\x00" as *u8, 12) == 1 { hit = 1 } }
920 if hit == 1 {
921 let v: i64 = _grid_int_val(ctx.src, cd.val_off, cd.val_len)
922 if v < 0 { return 0 }
923 return v
924 }
925 }
926 i = i - 1
927 }
928 return 0
929}
930
931// Max-content width (px) of an inline subtree: the width its TEXT occupies on ONE unwrapped line, using
932// the SAME font metrics as the TEXT layout branch (measure==paint). Sums children (an inline run). Used to
933// shrink-to-fit an atomic inline-block that has no explicit `width` (chips/tags/badges).
934func _layout_intrinsic_w(ctx: *LayoutCtx, box_idx: i64) -> i64 {
935 if box_idx < 0 { return 0 }
936 if box_idx >= ctx.tree.count { return 0 }
937 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
938 if b.kind == NX_LAYOUT_BOX_TEXT {
939 let fs: i64 = _layout_fontsize_inherited(ctx, b.parent_idx)
940 let sc: i64 = _layout_scale_for_px(fs)
941 let tptr: *u8 = ((ctx.src as i64) + b.text_off) as *u8
942 if ctx.text_measured == 1 {
943 if ctx.font_tab == 0 { ctx.font_tab = (font8x8_table()) as i64 }
944 return font8x8_text_w(ctx.font_tab as *u8, tptr, 0, b.text_len) * sc
945 }
946 if l_vec_measure == 1 { if font_run_has_ext(tptr, b.text_len) == 1 { return _layout_vec_text_w(tptr, 0, b.text_len, fs) } }
947 return b.text_len * NX_INLINE_GLYPH_W * sc
948 }
949 var w: i64 = 0
950 var ci: i64 = b.first_child_idx
951 var sf: i64 = 0
952 var keep: i64 = 1
953 while keep == 1 {
954 if sf >= NX_MAGIC_65536 { keep = 0 }
955 else {
956 sf = sf + 1
957 if ci < 0 { keep = 0 }
958 else {
959 w = w + _layout_intrinsic_w(ctx, ci)
960 let c: *LayoutBox = (ctx.tree.boxes as *u8 + (ci as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
961 ci = c.next_sibling_idx
962 }
963 }
964 }
965 return w
966}
967
968func _layout_inline_flow(ctx: *LayoutCtx, pen: *InlinePen, box_idx: i64) -> i64 {
969 if box_idx < 0 { return 0 }
970 if box_idx >= ctx.tree.count { return 0 }
971 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
972
973 // display:none on an INLINE-LEVEL element (span/a in flow): contributes nothing, flows no children,
974 // pen unmoved -- the same rule _layout_block_recurse applies to block boxes (seq1130 companion).
975 // TEXT boxes carry no computed decls, so the O(ncomp) scan is skipped for them.
976 if b.kind != NX_LAYOUT_BOX_TEXT {
977 if _layout_display_none(ctx, box_idx) == 1 { return 0 }
978 }
979
980 if b.kind == NX_LAYOUT_BOX_TEXT {
981 // Scale glyph advance + line height by the run's inherited font-size so big headings reserve
982 // proportional space (paint scales the glyph identically -> no overlap with the next line).
983 let fs_run: i64 = _layout_fontsize_inherited(ctx, b.parent_idx)
984 let sc: i64 = _layout_scale_for_px(fs_run)
985 let gw: i64 = NX_INLINE_GLYPH_W * sc
986 var lh: i64 = NX_INLINE_LINE_H * sc
987 // CSS line-height (S21 height axis): a declared line-height/font-shorthand-/N sets the EXACT
988 // line-box height (Chrome parity), floored at 8px against pathology. Absent -> legacy 17*scale.
989 let lhq: i64 = _layout_lineheight_px(ctx, b.parent_idx, fs_run)
990 if lhq >= 8 { lh = lhq }
991 let tptr: *u8 = ((ctx.src as i64) + b.text_off) as *u8
992 // MEASURED mode: width = the SUM of per-glyph proportional advances (font8x8 metrics SSOT) so the
993 // reserved box exactly equals what the paint draws. Legacy mode keeps the 9px-cell arithmetic.
994 var meas: i64 = 0
995 var ftab: *u8 = 0 as *u8
996 if ctx.text_measured == 1 {
997 meas = 1
998 if ctx.font_tab == 0 { ctx.font_tab = (font8x8_table()) as i64 }
999 ftab = ctx.font_tab as *u8
1000 }
1001 var full_w: i64 = b.text_len * gw
1002 if meas == 1 { full_w = font8x8_text_w(ftab, tptr, 0, b.text_len) * sc }
1003 // vec-measure: reserve the width the VECTOR paint will actually advance (same coverage test the
1004 // GUI paint uses: font_run_has_ext). Uncovered runs keep bitmap widths -- exactly mirroring the
1005 // paint-side fallback, so measure==paint in BOTH modes.
1006 var vec_run: i64 = 0
1007 if l_vec_measure == 1 { if font_run_has_ext(tptr, b.text_len) == 1 { vec_run = 1 } }
1008 if vec_run == 1 { full_w = _layout_vec_text_w(tptr, 0, b.text_len, fs_run) }
1009 if full_w <= pen.avail_w {
1010 // SINGLE-LINE run: wrap to a new line first if it won't fit in the remaining space (and we're
1011 // not already at line start), then place it and advance the pen horizontally.
1012 var cw: i64 = full_w
1013 if cw < 0 { cw = 0 }
1014 if pen.pen_x > 0 { if pen.pen_x + cw > pen.avail_w {
1015 pen.pen_y = pen.pen_y + pen.line_h
1016 pen.pen_x = 0
1017 pen.line_h = 0
1018 } }
1019 b.x = pen.origin_x + pen.pen_x
1020 b.y = pen.origin_y + pen.pen_y
1021 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
1022 _yt_w("YTRAP@1007 idx=" as *u8); _yt_n(box_idx)
1023 _yt_w(" oy=" as *u8); _yt_n(pen.origin_y)
1024 _yt_w("
1025" as *u8)
1026 } }
1027 b.w = cw
1028 b.h = lh
1029 pen.pen_x = pen.pen_x + cw
1030 if pen.line_h < lh { pen.line_h = lh }
1031 } else {
1032 // MULTI-LINE run: word-wraps. Reserve the FULL height and advance the pen DOWN to the last
1033 // line. Measured mode breaks by SUMMED ADVANCES (font8x8_next_break); legacy by char count
1034 // (tw_next_break) -- each mode's paint mirrors its break fn exactly.
1035 if pen.pen_x > 0 {
1036 pen.pen_y = pen.pen_y + pen.line_h
1037 pen.pen_x = 0
1038 pen.line_h = 0
1039 }
1040 var cpl: i64 = pen.avail_w / gw
1041 if cpl < 1 { cpl = 1 }
1042 var avail_units: i64 = pen.avail_w / sc
1043 if avail_units < 4 { avail_units = 4 }
1044 var s: i64 = 0
1045 var nlines: i64 = 0
1046 var last_start: i64 = 0
1047 var last_end: i64 = 0
1048 while s < b.text_len {
1049 last_start = s
1050 var e: i64 = 0
1051 if meas == 1 { e = font8x8_next_break(ftab, tptr, b.text_len, avail_units, s) }
1052 else { e = tw_next_break(tptr, b.text_len, cpl, s) }
1053 last_end = e
1054 nlines = nlines + 1
1055 s = e
1056 }
1057 if nlines < 1 { nlines = 1 }
1058 b.x = pen.origin_x
1059 b.y = pen.origin_y + pen.pen_y
1060 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
1061 _yt_w("YTRAP@1040 idx=" as *u8); _yt_n(box_idx)
1062 _yt_w(" oy=" as *u8); _yt_n(pen.origin_y)
1063 _yt_w("
1064" as *u8)
1065 } }
1066 b.w = pen.avail_w
1067 b.h = nlines * lh
1068 pen.pen_y = pen.pen_y + (nlines - 1) * lh
1069 if meas == 1 { pen.pen_x = font8x8_text_w(ftab, tptr, last_start, last_end) * sc }
1070 else { pen.pen_x = (last_end - last_start) * gw }
1071 pen.line_h = lh
1072 }
1073 return 0
1074 }
1075
1076 // INLINE-BLOCK: an ATOMIC inline-level box that sizes like a block internally (padding/border/width),
1077 // sits on the line, and WRAPS as a unit. Children lay out in a PRIVATE sub-pen at the inner (content)
1078 // origin so bg/border paint around them. Covers chips/tags/badges/buttons/pills + <img>/<input>/<button>.
1079 if b.kind == NX_LAYOUT_BOX_INLINE_BLOCK {
1080 let tbl: *LayoutPropTable = (pen.table) as *LayoutPropTable
1081 var plp: i64 = _layout_boxside_px(ctx, tbl.src, box_idx, tbl.padding_left_off, tbl.padding_left_len, "padding\x00" as *u8, 7, 3, 0)
1082 var prp: i64 = _layout_boxside_px(ctx, tbl.src, box_idx, tbl.padding_right_off, tbl.padding_right_len, "padding\x00" as *u8, 7, 1, 0)
1083 var ptp: i64 = _layout_boxside_px(ctx, tbl.src, box_idx, tbl.padding_top_off, tbl.padding_top_len, "padding\x00" as *u8, 7, 0, 0)
1084 var pbp: i64 = _layout_boxside_px(ctx, tbl.src, box_idx, tbl.padding_bottom_off, tbl.padding_bottom_len, "padding\x00" as *u8, 7, 2, 0)
1085 var mll: i64 = _layout_boxside_px(ctx, tbl.src, box_idx, tbl.margin_left_off, tbl.margin_left_len, "margin\x00" as *u8, 6, 3, 0)
1086 var mrr: i64 = _layout_boxside_px(ctx, tbl.src, box_idx, tbl.margin_right_off, tbl.margin_right_len, "margin\x00" as *u8, 6, 1, 0)
1087 if plp < 0 { plp = 0 }
1088 if prp < 0 { prp = 0 }
1089 if ptp < 0 { ptp = 0 }
1090 if pbp < 0 { pbp = 0 }
1091 if mll < 0 { mll = 0 }
1092 if mrr < 0 { mrr = 0 }
1093 let bdw: i64 = _layout_border_w(ctx, box_idx)
1094 let wexp: i64 = _layout_lookup_px(ctx, tbl.src, box_idx, tbl.width_off, tbl.width_len)
1095 let hexp: i64 = _layout_lookup_height_px(ctx, tbl.src, box_idx, tbl.height_off, tbl.height_len)
1096 var inw: i64 = wexp
1097 if inw < 0 { inw = _layout_intrinsic_w(ctx, box_idx) }
1098 if inw < 0 { inw = 0 }
1099 let fs_ib: i64 = _layout_fontsize_inherited(ctx, box_idx)
1100 let sc_ib: i64 = _layout_scale_for_px(fs_ib)
1101 var inh: i64 = hexp
1102 if inh < 0 { inh = NX_INLINE_LINE_H * sc_ib }
1103 let full_w: i64 = inw + plp + prp + bdw + bdw
1104 let full_h: i64 = inh + ptp + pbp + bdw + bdw
1105 // wrap the WHOLE box to the next line if it will not fit and we are not already at line start
1106 if pen.pen_x > 0 { if pen.pen_x + mll + full_w > pen.avail_w {
1107 pen.pen_y = pen.pen_y + pen.line_h
1108 pen.pen_x = 0
1109 pen.line_h = 0
1110 } }
1111 let bx: i64 = pen.origin_x + pen.pen_x + mll
1112 let by: i64 = pen.origin_y + pen.pen_y
1113 b.x = bx
1114 b.y = by
1115 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
1116 _yt_w("YTRAP@iblk idx=" as *u8); _yt_n(box_idx)
1117 _yt_w(" by=" as *u8); _yt_n(by)
1118 _yt_w("
1119" as *u8)
1120 } }
1121 b.w = full_w
1122 b.h = full_h
1123 // lay out children in a PRIVATE sub-pen at the inner content origin (so the bg/border box wraps them)
1124 let ibp: *InlinePen = (sys_mmap(NX_INLINE_PEN_BYTES as nx_size)) as *InlinePen
1125 ibp.pen_x = 0
1126 ibp.pen_y = 0
1127 ibp.line_h = 0
1128 ibp.avail_w = inw
1129 ibp.origin_x = bx + plp + bdw
1130 ibp.origin_y = by + ptp + bdw
1131 ibp.table = pen.table
1132 var cib: i64 = b.first_child_idx
1133 var sfib: i64 = 0
1134 var kib: i64 = 1
1135 while kib == 1 {
1136 if sfib >= NX_MAGIC_65536 { kib = 0 }
1137 else {
1138 sfib = sfib + 1
1139 if cib < 0 { kib = 0 }
1140 else {
1141 _layout_inline_flow(ctx, ibp, cib)
1142 let cb: *LayoutBox = (ctx.tree.boxes as *u8 + (cib as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1143 cib = cb.next_sibling_idx
1144 }
1145 }
1146 }
1147 // advance the parent pen past the atomic box + its horizontal margins; grow the line height
1148 pen.pen_x = pen.pen_x + mll + full_w + mrr
1149 if pen.line_h < full_h { pen.line_h = full_h }
1150 return 0
1151 }
1152
1153 // INLINE / other: transparent wrapper -- record start, flow children on the SHARED line, span to pen end.
1154 b.x = pen.origin_x + pen.pen_x
1155 b.y = pen.origin_y + pen.pen_y
1156 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
1157 _yt_w("YTRAP inline idx=" as *u8); _yt_n(box_idx)
1158 _yt_w(" origin_y=" as *u8); _yt_n(pen.origin_y)
1159 _yt_w(" pen_y=" as *u8); _yt_n(pen.pen_y)
1160 _yt_w(" line_h=" as *u8); _yt_n(pen.line_h)
1161 _yt_w("\n" as *u8)
1162 } }
1163 var child_idx: i64 = b.first_child_idx
1164 var safety: i64 = 0
1165 var keep: i64 = 1
1166 while keep == 1 {
1167 if safety >= NX_MAGIC_65536 { keep = 0 }
1168 else {
1169 safety = safety + 1
1170 if child_idx < 0 { keep = 0 }
1171 else {
1172 _layout_inline_flow(ctx, pen, child_idx)
1173 let c: *LayoutBox = (ctx.tree.boxes as *u8 + (child_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1174 child_idx = c.next_sibling_idx
1175 }
1176 }
1177 }
1178 var span_w: i64 = (pen.origin_x + pen.pen_x) - b.x
1179 if span_w < 0 { span_w = 0 }
1180 b.w = span_w
1181 b.h = pen.line_h
1182 return 0
1183}
1184
1185// Recursive layout pass over a single block subtree rooted at
1186// box_idx. `avail_w` is the content width passed down by the
1187// parent. (`origin_x`, `origin_y`) is the top-left of the content
1188// area at this box's level. Returns the OUTER height of this box
1189// (margin-top + border-top + padding-top + content_h +
1190// padding-bottom + border-bottom + margin-bottom) so the parent's
1191// sibling-stacking loop can advance.
1192//
1193// Phase 3 subset: no borders, no horizontal margin/padding, so the
1194// outer height = margin-top + padding-top + content_h +
1195// padding-bottom + margin-bottom.
1196//
1197// Does the box have computed `display: flex`? (Vector-2022 and modern pages arrange sidebar+content
1198// this way.) Same lookup shape as _layout_display_none but value "flex".
1199func _layout_display_flex(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1200 let dn: *u8 = "display\x00" as *u8
1201 let fx: *u8 = "flex\x00" as *u8
1202 var i: i64 = ctx.n_computed - 1
1203 while i >= 0 {
1204 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1205 if cd.element_idx == box_idx {
1206 if cd.prop_len == 7 {
1207 var sp: i64 = 1
1208 var k: i64 = 0
1209 while k < 7 { if (ctx.src[cd.prop_off+k]&0xff) != (dn[k]&0xff) { sp = 0; k = 7 } else { k = k + 1 } }
1210 if sp == 1 {
1211 if cd.val_len == 4 {
1212 var sv: i64 = 1
1213 var j: i64 = 0
1214 while j < 4 { if (ctx.src[cd.val_off+j]&0xff) != (fx[j]&0xff) { sv = 0; j = 4 } else { j = j + 1 } }
1215 if sv == 1 { return 1 }
1216 }
1217 }
1218 }
1219 }
1220 i = i - 1
1221 }
1222 return 0
1223}
1224
1225// Does the box have computed `display: grid`? (Same lookup shape as _layout_display_flex, value "grid".)
1226// Is this box's computed margin AUTO on the given side (3=left, 1=right)? Longhand margin-left/right
1227// value "auto", or the `margin` shorthand's side token per the CSS 1/2/3/4-value mapping. Cascade
1228// winner = first hit in reverse. Powers §10.3.3 auto-margin centering (wiki `margin:0 auto`).
1229func _lb_margin_auto(ctx: *LayoutCtx, box_idx: i64, side: i64) -> i64 {
1230 let ml_lit: *u8 = "margin-left\x00" as *u8
1231 let mr_lit: *u8 = "margin-right\x00" as *u8
1232 let m_lit: *u8 = "margin\x00" as *u8
1233 var i: i64 = ctx.n_computed - 1
1234 while i >= 0 {
1235 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1236 if cd.element_idx == box_idx {
1237 var want: i64 = 0
1238 var lit: *u8 = m_lit
1239 var ll: i64 = 6
1240 if side == 3 { if cd.prop_len == 11 { lit = ml_lit; ll = 11; want = 1 } }
1241 if side == 1 { if cd.prop_len == 12 { lit = mr_lit; ll = 12; want = 1 } }
1242 if cd.prop_len == 6 { want = 2 }
1243 if want > 0 {
1244 var sp: i64 = 1
1245 var k: i64 = 0
1246 while k < ll { if (ctx.src[cd.prop_off+k]&0xff) != (lit[k]&0xff) { sp = 0; k = ll } else { k = k + 1 } }
1247 if sp == 1 {
1248 if want == 1 {
1249 // longhand: value == "auto"?
1250 if cd.val_len == 4 { if (ctx.src[cd.val_off]&0xff)==97 { if (ctx.src[cd.val_off+1]&0xff)==117 { return 1 } } }
1251 return 0
1252 }
1253 // shorthand: split into tokens, map side (1:all 2:[v h] 3:[t h b] 4:[t r b l])
1254 let to: *i64 = sys_mmap(8*4) as *i64
1255 let tl: *i64 = sys_mmap(8*4) as *i64
1256 var nt: i64 = 0
1257 var p: i64 = cd.val_off
1258 let e: i64 = cd.val_off + cd.val_len
1259 while p < e {
1260 while p < e { if (ctx.src[p]&0xff) == 32 { p = p + 1 } else { p = e + 1000 } }
1261 if p > e { p = p - 1000 }
1262 if p < e {
1263 let s0: i64 = p
1264 while p < e { if (ctx.src[p]&0xff) != 32 { p = p + 1 } else { p = e + 1000 } }
1265 if p > e { p = p - 1000 }
1266 if nt < 4 { to[nt] = s0; tl[nt] = p - s0; nt = nt + 1 }
1267 }
1268 }
1269 if nt == 0 { return 0 }
1270 var ti2: i64 = 0
1271 if nt == 1 { ti2 = 0 }
1272 if nt == 2 { ti2 = 1 }
1273 if nt == 3 { ti2 = 1 }
1274 if nt == 4 { if side == 1 { ti2 = 1 } else { ti2 = 3 } }
1275 if tl[ti2] == 4 { if (ctx.src[to[ti2]]&0xff)==97 { if (ctx.src[to[ti2]+1]&0xff)==117 { return 1 } } }
1276 return 0
1277 }
1278 }
1279 }
1280 i = i - 1
1281 }
1282 return 0
1283}
1284// Does this box use NAMED GRID AREAS (`grid-template-areas`)? We place grid children by AUTO-FLOW
1285// order only, so a page that assigns items to named areas gets them in the WRONG tracks — wikipedia
1286// ≥1120px put its main content into the 12.25rem SIDEBAR column and the page exploded to 88,202px
1287// (measured 2026-07-29; the track math itself was correct). Until named placement exists, a grid we
1288// cannot place correctly DEGRADES TO NORMAL BLOCK FLOW — full-width stacking is always readable,
1289// mis-placement never is. Same principle as flex-direction:column → block.
1290func _layout_has_grid_areas(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1291 let ga: *u8 = "grid-template-areas\x00" as *u8
1292 var i: i64 = ctx.n_computed - 1
1293 while i >= 0 {
1294 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1295 if cd.element_idx == box_idx {
1296 if cd.prop_len == 19 {
1297 var sp: i64 = 1
1298 var k: i64 = 0
1299 while k < 19 { if (ctx.src[cd.prop_off+k]&0xff) != (ga[k]&0xff) { sp = 0; k = 19 } else { k = k + 1 } }
1300 if sp == 1 { return 1 }
1301 }
1302 }
1303 i = i - 1
1304 }
1305 return 0
1306}
1307// flex-direction: column(-reverse)? Our flex branch is ROW-only; a COLUMN container must NOT take it.
1308// A column flex stacks children vertically at full width = exactly the normal block flow this engine
1309// already does, so the correct minimal treatment is "not row-flex" (align-items offsets = later rung).
1310// Found via linkedin (2026-07-28): its hero is flex-direction:column; the row path laid the children
1311// SIDEWAYS and starved the H1 to w=0 x h=1482 (the "Welcome to your professional community" column).
1312func _layout_flex_dir_col(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1313 let fn2: *u8 = "flex-direction\x00" as *u8
1314 var i: i64 = ctx.n_computed - 1
1315 while i >= 0 {
1316 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1317 if cd.element_idx == box_idx {
1318 if cd.prop_len == 14 {
1319 var sp: i64 = 1
1320 var k: i64 = 0
1321 while k < 14 { if (ctx.src[cd.prop_off+k]&0xff) != (fn2[k]&0xff) { sp = 0; k = 14 } else { k = k + 1 } }
1322 if sp == 1 {
1323 if cd.val_len >= 6 { if (ctx.src[cd.val_off]&0xff) == 99 { return 1 } } // c(olumn)
1324 return 0
1325 }
1326 }
1327 }
1328 i = i - 1
1329 }
1330 return 0
1331}
1332func _layout_display_grid(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1333 let dn: *u8 = "display\x00" as *u8
1334 let gx: *u8 = "grid\x00" as *u8
1335 var i: i64 = ctx.n_computed - 1
1336 while i >= 0 {
1337 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1338 if cd.element_idx == box_idx {
1339 if cd.prop_len == 7 {
1340 var sp: i64 = 1
1341 var k: i64 = 0
1342 while k < 7 { if (ctx.src[cd.prop_off+k]&0xff) != (dn[k]&0xff) { sp = 0; k = 7 } else { k = k + 1 } }
1343 if sp == 1 {
1344 if cd.val_len == 4 {
1345 var sv: i64 = 1
1346 var j: i64 = 0
1347 while j < 4 { if (ctx.src[cd.val_off+j]&0xff) != (gx[j]&0xff) { sv = 0; j = 4 } else { j = j + 1 } }
1348 if sv == 1 { return 1 }
1349 }
1350 }
1351 }
1352 }
1353 i = i - 1
1354 }
1355 return 0
1356}
1357// `display:table` (the UA sheet marks every <table> with it) -- same lookup shape as flex/grid.
1358// This is how the layout knows a box is a table without carrying tag names in the box tree.
1359func _layout_display_table(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1360 let dn: *u8 = "display\x00" as *u8
1361 let tx: *u8 = "table\x00" as *u8
1362 var i: i64 = ctx.n_computed - 1
1363 while i >= 0 {
1364 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1365 if cd.element_idx == box_idx {
1366 if cd.prop_len == 7 {
1367 var sp: i64 = 1
1368 var k: i64 = 0
1369 while k < 7 { if (ctx.src[cd.prop_off+k]&0xff) != (dn[k]&0xff) { sp = 0; k = 7 } else { k = k + 1 } }
1370 if sp == 1 {
1371 if cd.val_len == 5 {
1372 var sv: i64 = 1
1373 var j: i64 = 0
1374 while j < 5 { if (ctx.src[cd.val_off+j]&0xff) != (tx[j]&0xff) { sv = 0; j = 5 } else { j = j + 1 } }
1375 if sv == 1 { return 1 }
1376 }
1377 }
1378 }
1379 }
1380 i = i - 1
1381 }
1382 return 0
1383}
1384func _lb_box(ctx: *LayoutCtx, i: i64) -> *LayoutBox {
1385 return (ctx.tree.boxes as *u8 + (i as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1386}
1387func _layout_nearest_table(ctx: *LayoutCtx, idx: i64) -> i64 {
1388 var cur: i64 = idx
1389 var guard: i64 = 0
1390 while guard < 64 {
1391 guard = guard + 1
1392 if cur < 0 { return 0 - 1 }
1393 if _layout_display_table(ctx, cur) == 1 { return cur }
1394 let b: *LayoutBox = _lb_box(ctx, cur)
1395 cur = b.parent_idx
1396 }
1397 return 0 - 1
1398}
1399// Resolve column widths for the table at tbl_idx into colw[0..ncols); returns ncols.
1400// A ROW is any descendant with display:flex whose nearest table ancestor is tbl_idx (skips the rows
1401// of nested tables by construction). Column width = max over rows of the cell's max-content width
1402// (_layout_intrinsic_w -- the same metric the inline path measures with) + the UA cell padding.
1403// Then: over-wide -> scale to fit; under-wide -> distribute the slack proportionally (real tables are
1404// width:100%, and this preserves the content ratios that make a table readable).
1405func _layout_table_cols(ctx: *LayoutCtx, tbl_idx: i64, avail_w: i64, colw: *i64) -> i64 {
1406 var k: i64 = 0
1407 while k < NX_TBL_MAXCOLS { colw[k] = 0; k = k + 1 }
1408 var ncols: i64 = 0
1409 // document order => a subtree is a contiguous index range; stop at the first non-descendant.
1410 var i: i64 = tbl_idx + 1
1411 var go: i64 = 1
1412 while go == 1 {
1413 if i >= ctx.tree.count { go = 0 }
1414 else {
1415 // descendant test (bounded parent walk)
1416 var cur: i64 = i
1417 var isdesc: i64 = 0
1418 var g2: i64 = 0
1419 while g2 < 64 {
1420 g2 = g2 + 1
1421 if cur < 0 { g2 = 64 }
1422 else {
1423 if cur == tbl_idx { isdesc = 1; g2 = 64 }
1424 else { let pb: *LayoutBox = _lb_box(ctx, cur); cur = pb.parent_idx }
1425 }
1426 }
1427 if isdesc == 0 { go = 0 }
1428 else {
1429 if _layout_display_flex(ctx, i) == 1 {
1430 if _layout_nearest_table(ctx, i) == tbl_idx {
1431 let rb: *LayoutBox = _lb_box(ctx, i)
1432 var c: i64 = rb.first_child_idx
1433 var col: i64 = 0
1434 var g3: i64 = 0
1435 while c >= 0 {
1436 if g3 >= NX_MAGIC_4096 { c = 0 - 1 }
1437 else {
1438 g3 = g3 + 1
1439 let cb: *LayoutBox = _lb_box(ctx, c)
1440 // colspan (seq1159): from_dom stashes N>=2 in source_node_idx; a spanning
1441 // cell occupies N slots and its max-content contributes ACROSS the span
1442 // (iw/N per column), never to one column (CSS 2.1 s17.5.2).
1443 var cs: i64 = 1
1444 if cb.source_node_idx >= 2 { cs = cb.source_node_idx }
1445 if col < NX_TBL_MAXCOLS {
1446 var iw: i64 = _layout_intrinsic_w(ctx, c) + 8 // + UA td padding
1447 if iw > avail_w { iw = avail_w }
1448 var share: i64 = iw / cs
1449 var sk: i64 = 0
1450 while sk < cs {
1451 let sc2: i64 = col + sk
1452 if sc2 < NX_TBL_MAXCOLS {
1453 if share > colw[sc2] { colw[sc2] = share }
1454 if sc2 + 1 > ncols { ncols = sc2 + 1 }
1455 }
1456 sk = sk + 1
1457 }
1458 }
1459 col = col + cs
1460 c = cb.next_sibling_idx
1461 }
1462 }
1463 }
1464 }
1465 i = i + 1
1466 }
1467 }
1468 }
1469 if ncols <= 0 { return 0 }
1470 var sum: i64 = 0
1471 var j: i64 = 0
1472 while j < ncols { sum = sum + colw[j]; j = j + 1 }
1473 if sum <= 0 { return 0 }
1474 if sum > avail_w {
1475 j = 0
1476 while j < ncols { colw[j] = (colw[j] * avail_w) / sum; j = j + 1 }
1477 } else {
1478 let slack: i64 = avail_w - sum
1479 j = 0
1480 while j < ncols { colw[j] = colw[j] + (colw[j] * slack) / sum; j = j + 1 }
1481 }
1482 return ncols
1483}
1484// Parse the leading integer of src[voff..voff+vlen) (e.g. "16" of "16px"); -1 if none. For px values.
1485func _grid_int_val(src: *u8, voff: i64, vlen: i64) -> i64 {
1486 let end: i64 = voff + vlen
1487 var v: i64 = 0
1488 var any: i64 = 0
1489 var i: i64 = voff
1490 var rd: i64 = 1
1491 while rd == 1 {
1492 if i < end { let c: i64 = src[i]&0xff; if c>=48 { if c<=57 { v=v*10+(c-48); any=1; i=i+1 } else { rd=0 } } else { rd=0 } }
1493 else { rd = 0 }
1494 }
1495 if any == 0 { return 0 - 1 }
1496 return v
1497}
1498// Find a property by EXACT name on box_idx (reverse cascade = winner first) and return its leading
1499// integer value (px), or -1 if absent. Used for gap/column-gap/row-gap.
1500func _grid_prop_px(ctx: *LayoutCtx, box_idx: i64, name: *u8, nlen: i64) -> i64 {
1501 var i: i64 = ctx.n_computed - 1
1502 while i >= 0 {
1503 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1504 if cd.element_idx == box_idx {
1505 if cd.prop_len == nlen {
1506 var sp: i64 = 1
1507 var k: i64 = 0
1508 while k < nlen { if (ctx.src[cd.prop_off+k]&0xff) != (name[k]&0xff) { sp = 0; k = nlen } else { k = k + 1 } }
1509 if sp == 1 { return _grid_int_val(ctx.src, cd.val_off, cd.val_len) }
1510 }
1511 }
1512 i = i - 1
1513 }
1514 return 0 - 1
1515}
1516// Resolve the column gap (want_row=0) or row gap (want_row=1) for a grid box: column-gap/row-gap wins,
1517// else the `gap` shorthand, else 0.
1518func _grid_gap(ctx: *LayoutCtx, box_idx: i64, want_row: i64) -> i64 {
1519 if want_row == 1 {
1520 let rg: i64 = _grid_prop_px(ctx, box_idx, "row-gap\x00" as *u8, 7)
1521 if rg >= 0 { return rg }
1522 } else {
1523 let cg: i64 = _grid_prop_px(ctx, box_idx, "column-gap\x00" as *u8, 10)
1524 if cg >= 0 { return cg }
1525 }
1526 let g: i64 = _grid_prop_px(ctx, box_idx, "gap\x00" as *u8, 3)
1527 if g >= 0 { return g }
1528 return 0
1529}
1530// Get the grid-template-columns VALUE span for box_idx into ooff/olen; 1 if present, 0 if not.
1531func _layout_grid_template(ctx: *LayoutCtx, box_idx: i64, ooff: *i64, olen: *i64) -> i64 {
1532 let gp: *u8 = "grid-template-columns\x00" as *u8
1533 var i: i64 = ctx.n_computed - 1
1534 while i >= 0 {
1535 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1536 if cd.element_idx == box_idx {
1537 if cd.prop_len == 21 {
1538 var sp: i64 = 1
1539 var k: i64 = 0
1540 while k < 21 { if (ctx.src[cd.prop_off+k]&0xff) != (gp[k]&0xff) { sp = 0; k = 21 } else { k = k + 1 } }
1541 if sp == 1 { ooff[0] = cd.val_off; olen[0] = cd.val_len; return 1 }
1542 }
1543 // `grid-template` SHORTHAND (rows / columns): the COLUMNS live after the '/'. Wikipedia's
1544 // vector-2022 declares its sidebar grid ONLY this way (`grid-template:min-content 1fr
1545 // min-content / 12.25rem minmax(0,1fr)`) -- without this the grid never activated and the
1546 // whole page rendered flush-left (the styled dx~150 regime, 2026-07-28).
1547 if cd.prop_len == 13 {
1548 let gs: *u8 = "grid-template\x00" as *u8
1549 var sp2: i64 = 1
1550 var k2: i64 = 0
1551 while k2 < 13 { if (ctx.src[cd.prop_off+k2]&0xff) != (gs[k2]&0xff) { sp2 = 0; k2 = 13 } else { k2 = k2 + 1 } }
1552 if sp2 == 1 {
1553 var sl: i64 = 0 - 1
1554 var q: i64 = cd.val_off
1555 let qe: i64 = cd.val_off + cd.val_len
1556 while q < qe { if (ctx.src[q]&0xff) == 47 { sl = q; q = qe } else { q = q + 1 } }
1557 if sl >= 0 {
1558 ooff[0] = sl + 1
1559 olen[0] = qe - (sl + 1)
1560 return 1
1561 }
1562 }
1563 }
1564 }
1565 i = i - 1
1566 }
1567 return 0
1568}
1569// Parse one track size token src[ts..te): sets fr_out[0]=1 for an `fr` (flexible) track, 0 for `px`
1570// (fixed). Returns the fr WEIGHT (>=1) for fr tracks or the px VALUE for fixed. A token with no digits
1571// (auto/min-content/etc) is treated as 1fr (so unknown tracks still take a flexible share, not zero).
1572func _grid_parse_size(src: *u8, ts: i64, te: i64, fr_out: *i64) -> i64 {
1573 // q3 thousandths (fractions matter: "12.25rem" was digit-scanned to 12px; it is 196px)
1574 var ip2: i64 = 0
1575 var any: i64 = 0
1576 var i: i64 = ts
1577 var rd: i64 = 1
1578 while rd == 1 {
1579 if i < te { let c: i64 = src[i]&0xff; if c>=48 { if c<=57 { ip2=ip2*10+(c-48); any=1; i=i+1 } else { rd=0 } } else { rd=0 } }
1580 else { rd = 0 }
1581 }
1582 var fr3: i64 = 0
1583 if i < te { if (src[i]&0xff) == 46 {
1584 i = i + 1
1585 var fd2: i64 = 0
1586 var g5: i64 = 1
1587 while g5 == 1 {
1588 if i < te { let c5: i64 = src[i]&0xff; if c5>=48 { if c5<=57 {
1589 if fd2 == 0 { fr3 = fr3 + (c5-48)*100 }
1590 if fd2 == 1 { fr3 = fr3 + (c5-48)*10 }
1591 if fd2 == 2 { fr3 = fr3 + (c5-48) }
1592 fd2 = fd2 + 1; i = i + 1
1593 } else { g5 = 0 } } else { g5 = 0 } }
1594 else { g5 = 0 }
1595 }
1596 } }
1597 let q3: i64 = ip2 * 1000 + fr3
1598 var isfr: i64 = 0
1599 var isrem: i64 = 0
1600 var j: i64 = i
1601 var u: i64 = 0
1602 while u == 0 {
1603 if j >= te { u = 1 }
1604 else {
1605 let c: i64 = src[j]&0xff
1606 if c==102 { isfr = 1; u = 1 } // fr
1607 else { if c==112 { u = 1 } // px
1608 else { if c==114 { isrem = 1; u = 1 } // rem
1609 else { if c==101 { isrem = 1; u = 1 } // em (~root-relative approx)
1610 else { j = j + 1 } } } }
1611 }
1612 }
1613 if any == 0 { fr_out[0] = 1; return 1 }
1614 fr_out[0] = isfr
1615 if isfr == 1 { var w5: i64 = q3 / 1000; if w5 < 1 { w5 = 1 } return w5 }
1616 if isrem == 1 { return (q3 * 16 + 500) / 1000 }
1617 return q3 / 1000
1618}
1619// Resolve grid-template-columns src[voff..voff+vlen) into per-column px widths[] for total available
1620// width `avail` with `col_gap` between columns. Supports a track list `200px 1fr 2fr` AND `repeat(N,
1621// SIZE)`. fr tracks split the leftover (avail - fixed_px - gaps) by weight. Returns the column count.
1622func _grid_resolve_tracks(src: *u8, voff: i64, vlen: i64, avail: i64, col_gap: i64, widths: *i64, maxcols: i64) -> i64 {
1623 let end: i64 = voff + vlen
1624 let is_fr: *i64 = sys_mmap(8 * (maxcols + 2)) as *i64
1625 let sz: *i64 = sys_mmap(8 * (maxcols + 2)) as *i64
1626 let frb: *i64 = sys_mmap(8) as *i64
1627 var ncols: i64 = 0
1628
1629 // detect a repeat( ... ) form
1630 var rep: i64 = 0 - 1
1631 var ri: i64 = voff
1632 while ri + 7 <= end {
1633 if (src[ri]&0xff)==114 { if (src[ri+1]&0xff)==101 { if (src[ri+2]&0xff)==112 { if (src[ri+3]&0xff)==101 { if (src[ri+4]&0xff)==97 { if (src[ri+5]&0xff)==116 { if (src[ri+6]&0xff)==40 {
1634 rep = ri + 7
1635 } } } } } } }
1636 ri = ri + 1
1637 }
1638 if rep >= 0 {
1639 // repeat( COUNT , TRACK ): COUNT is N or the keyword auto-fit/auto-fill; TRACK may be minmax(MIN,MAX).
1640 // auto-fit/fill = "as many MIN-wide columns as fit" -- THE modern responsive grid (office .newrow/.fgrid);
1641 // the old parser read auto-fit as N=0->1 -> single column -> everything STACKED.
1642 var j: i64 = rep
1643 var sk: i64 = 1
1644 while sk == 1 { if j < end { if (src[j]&0xff)==32 { j = j + 1 } else { sk = 0 } } else { sk = 0 } }
1645 var autofit: i64 = 0
1646 var nrep: i64 = 0
1647 if j < end { if (src[j]&0xff)==97 { autofit = 1 } } // 'a' -> auto-fit / auto-fill
1648 if autofit == 1 {
1649 while j < end { if (src[j]&0xff)==44 { break } j = j + 1 }
1650 } else {
1651 var anyN: i64 = 0
1652 var rd: i64 = 1
1653 while rd == 1 { if j < end { let c: i64 = src[j]&0xff; if c>=48 { if c<=57 { nrep=nrep*10+(c-48); anyN=1; j=j+1 } else { rd=0 } } else { rd=0 } } else { rd=0 } }
1654 if anyN == 0 { nrep = 1 }
1655 }
1656 if j < end { if (src[j]&0xff)==44 { j = j + 1 } }
1657 var sk2: i64 = 1
1658 while sk2 == 1 { if j < end { if (src[j]&0xff)==32 { j = j + 1 } else { sk2 = 0 } } else { sk2 = 0 } }
1659 // TRACK: minmax(MIN,MAX) -> MIN px sets the auto-fit column count, MAX is the emitted track size.
1660 var minpx: i64 = 0
1661 var val: i64 = 1
1662 var ismm: i64 = 0
1663 if j + 7 <= end { if (src[j]&0xff)==109 { if (src[j+1]&0xff)==105 { if (src[j+2]&0xff)==110 { if (src[j+3]&0xff)==109 { if (src[j+4]&0xff)==97 { if (src[j+5]&0xff)==120 { if (src[j+6]&0xff)==40 { ismm = 1 } } } } } } } }
1664 let tfr: *i64 = sys_mmap(8) as *i64
1665 if ismm == 1 {
1666 var mj: i64 = j + 7
1667 var mc: i64 = mj
1668 while mc < end { let c: i64 = src[mc]&0xff; if c==44 { break } if c==41 { break } mc = mc + 1 }
1669 minpx = _grid_parse_size(src, mj, mc, tfr)
1670 var xj: i64 = mc
1671 if xj < end { if (src[xj]&0xff)==44 { xj = xj + 1 } }
1672 var wsx: i64 = 1
1673 while wsx == 1 { if xj < end { if (src[xj]&0xff)==32 { xj = xj + 1 } else { wsx = 0 } } else { wsx = 0 } }
1674 var xc: i64 = xj
1675 while xc < end { if (src[xc]&0xff)==41 { break } xc = xc + 1 }
1676 val = _grid_parse_size(src, xj, xc, frb)
1677 } else {
1678 var te: i64 = j
1679 var sd: i64 = 0
1680 while sd == 0 { if te >= end { sd = 1 } else { let c: i64 = src[te]&0xff; if c==41 { sd = 1 } else { if c==32 { sd = 1 } else { if c==44 { sd = 1 } else { te = te + 1 } } } } }
1681 val = _grid_parse_size(src, j, te, frb)
1682 if frb[0] == 0 { minpx = val } else { minpx = 120 }
1683 }
1684 var count: i64 = nrep
1685 if autofit == 1 {
1686 var mp: i64 = minpx
1687 if mp < 1 { mp = 1 }
1688 count = (avail + col_gap) / (mp + col_gap)
1689 if count < 1 { count = 1 }
1690 }
1691 var c2: i64 = 0
1692 while c2 < count { if ncols < maxcols { is_fr[ncols] = frb[0]; sz[ncols] = val; ncols = ncols + 1 } c2 = c2 + 1 }
1693 } else {
1694 // explicit whitespace-separated track list
1695 var p: i64 = voff
1696 var scan: i64 = 1
1697 while scan == 1 {
1698 var sw: i64 = 1
1699 while sw == 1 { if p >= end { sw = 0 } else { let c: i64 = src[p]&0xff; if c==32 { p=p+1 } else { if c==9 { p=p+1 } else { if c==10 { p=p+1 } else { if c==13 { p=p+1 } else { sw=0 } } } } } }
1700 if p >= end { scan = 0 }
1701 else {
1702 var te2: i64 = p
1703 var sd2: i64 = 0
1704 while sd2 == 0 { if te2 >= end { sd2 = 1 } else { let c: i64 = src[te2]&0xff; if c==32 { sd2=1 } else { if c==9 { sd2=1 } else { if c==10 { sd2=1 } else { if c==13 { sd2=1 } else { te2=te2+1 } } } } } }
1705 let val2: i64 = _grid_parse_size(src, p, te2, frb)
1706 if ncols < maxcols { is_fr[ncols] = frb[0]; sz[ncols] = val2; ncols = ncols + 1 }
1707 p = te2
1708 }
1709 }
1710 }
1711 if ncols < 1 { ncols = 1; is_fr[0] = 1; sz[0] = 1 }
1712
1713 var fixed_total: i64 = 0
1714 var fr_weight: i64 = 0
1715 var i: i64 = 0
1716 while i < ncols { if is_fr[i]==1 { fr_weight = fr_weight + sz[i] } else { fixed_total = fixed_total + sz[i] } i = i + 1 }
1717 var total_gap: i64 = 0
1718 if ncols > 1 { total_gap = (ncols - 1) * col_gap }
1719 var free: i64 = avail - fixed_total - total_gap
1720 if free < 0 { free = 0 }
1721 var fr_unit: i64 = 0
1722 if fr_weight > 0 { fr_unit = free / fr_weight }
1723 i = 0
1724 while i < ncols {
1725 if is_fr[i]==1 { widths[i] = sz[i] * fr_unit } else { widths[i] = sz[i] }
1726 i = i + 1
1727 }
1728 return ncols
1729}
1730
1731// Computed `float` for a box: 0=none, 1=left, 2=right (R5 rung 1). Reverse-scan = cascade winner first.
1732// Property "float" (5), value "left"(4)/"right"(5); anything else (incl. "none") -> 0.
1733func _layout_float(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1734 let fp: *u8 = "float\x00" as *u8
1735 var i: i64 = ctx.n_computed - 1
1736 while i >= 0 {
1737 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1738 if cd.element_idx == box_idx {
1739 if cd.prop_len == 5 {
1740 var sp: i64 = 1
1741 var k: i64 = 0
1742 while k < 5 { if (ctx.src[cd.prop_off+k]&0xff) != (fp[k]&0xff) { sp = 0; k = 5 } else { k = k + 1 } }
1743 if sp == 1 {
1744 // "left" -> 1
1745 if cd.val_len == 4 {
1746 if (ctx.src[cd.val_off]&0xff)==108 { if (ctx.src[cd.val_off+1]&0xff)==101 { if (ctx.src[cd.val_off+2]&0xff)==102 { if (ctx.src[cd.val_off+3]&0xff)==116 { return 1 } } } }
1747 }
1748 // "right" -> 2
1749 if cd.val_len == 5 {
1750 if (ctx.src[cd.val_off]&0xff)==114 { if (ctx.src[cd.val_off+1]&0xff)==105 { if (ctx.src[cd.val_off+2]&0xff)==103 { if (ctx.src[cd.val_off+3]&0xff)==104 { if (ctx.src[cd.val_off+4]&0xff)==116 { return 2 } } } } }
1751 }
1752 return 0
1753 }
1754 }
1755 }
1756 i = i - 1
1757 }
1758 return 0
1759}
1760// Computed `position` for a box: 0=static, 1=relative, 2=absolute, 3=fixed, 4=sticky.
1761// Same reverse-scan (cascade winner first) shape as _layout_float. Property "position" (8).
1762// Values matched on length + a distinguishing byte, the idiom used throughout this file:
1763// relative(8,'r') · absolute(8,'a') · fixed(5) · sticky(6) · static(6,'s'-at-0 w/ 'a' at 2)
1764// `static` and anything unknown -> 0, so an unrecognised value can only ever mean NORMAL FLOW.
1765func _layout_position(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1766 let pp: *u8 = "position\x00" as *u8
1767 var i: i64 = ctx.n_computed - 1
1768 while i >= 0 {
1769 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1770 if cd.element_idx == box_idx {
1771 if cd.prop_len == 8 {
1772 var sp: i64 = 1
1773 var k: i64 = 0
1774 while k < 8 { if (ctx.src[cd.prop_off+k]&0xff) != (pp[k]&0xff) { sp = 0; k = 8 } else { k = k + 1 } }
1775 if sp == 1 {
1776 let v0: i64 = ctx.src[cd.val_off] & 0xff
1777 if cd.val_len == 8 {
1778 if v0 == 114 { return 1 } // relative
1779 if v0 == 97 { return 2 } // absolute
1780 }
1781 if cd.val_len == 5 { if v0 == 102 { return 3 } } // fixed
1782 if cd.val_len == 6 { if v0 == 115 {
1783 // sticky vs static: byte 2 is 'i' (105) for sticky, 'a' (97) for static
1784 if (ctx.src[cd.val_off+2]&0xff) == 105 { return 4 }
1785 return 0
1786 } }
1787 return 0
1788 }
1789 }
1790 }
1791 i = i - 1
1792 }
1793 return 0
1794}
1795// Computed `justify-content` for a flex container (main-axis distribution). Returns:
1796// 0 flex-start (default) · 1 center · 2 flex-end · 3 space-between · 4 space-around · 5 space-evenly.
1797// Matched by value length (+ one byte for around/evenly) -- standard keyword set.
1798func _layout_justify(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1799 let jp: *u8 = "justify-content\x00" as *u8
1800 var i: i64 = ctx.n_computed - 1
1801 while i >= 0 {
1802 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1803 if cd.element_idx == box_idx {
1804 if cd.prop_len == 15 {
1805 var sp: i64 = 1
1806 var k: i64 = 0
1807 while k < 15 { if (ctx.src[cd.prop_off+k]&0xff) != (jp[k]&0xff) { sp = 0; k = 15 } else { k = k + 1 } }
1808 if sp == 1 {
1809 let vl: i64 = cd.val_len
1810 let vo: i64 = cd.val_off
1811 if vl == 6 { return 1 } // center
1812 if vl == 3 { return 2 } // end
1813 if vl == 5 { return 0 } // start
1814 if vl == 8 { return 2 } // flex-end
1815 if vl == 10 { return 0 } // flex-start
1816 if vl == 13 { return 3 } // space-between
1817 if vl == 12 { if (ctx.src[vo+6]&0xff)==97 { return 4 } return 5 } // space-around(a)/-evenly(e)
1818 return 0
1819 }
1820 }
1821 }
1822 i = i - 1
1823 }
1824 return 0
1825}
1826// Computed `align-items` for a flex container (cross-axis = vertical for a row). Returns:
1827// 0 stretch/flex-start (default) · 1 center · 2 flex-end. (baseline approximated as start.)
1828func _layout_align(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1829 let ap: *u8 = "align-items\x00" as *u8
1830 var i: i64 = ctx.n_computed - 1
1831 while i >= 0 {
1832 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1833 if cd.element_idx == box_idx {
1834 if cd.prop_len == 11 {
1835 var sp: i64 = 1
1836 var k: i64 = 0
1837 while k < 11 { if (ctx.src[cd.prop_off+k]&0xff) != (ap[k]&0xff) { sp = 0; k = 11 } else { k = k + 1 } }
1838 if sp == 1 {
1839 let vl: i64 = cd.val_len
1840 if vl == 6 { return 1 } // center
1841 if vl == 3 { return 2 } // end
1842 if vl == 8 { if (ctx.src[cd.val_off]&0xff)==102 { return 2 } return 0 } // flex-end(f) vs baseline(b)
1843 return 0 // flex-start / start / stretch
1844 }
1845 }
1846 }
1847 i = i - 1
1848 }
1849 return 0
1850}
1851// Computed `flex-grow` for a flex ITEM (the CHILD box, not the container). Returns the grow factor in Q3
1852// THOUSANDTHS (1.0 -> 1000; fractional 0.1 -> 100; 0 = does not grow) so fractional grow works. Also
1853// accepts the `flex` shorthand's FIRST token (`flex:1` -> 1000, `flex:0 0 0%` -> 0) since `flex:N` is more
1854// common on real pages. Negative/invalid -> 0 (negative flex-grow is invalid per spec). Reverse-walk =
1855// source-order winner, so a later `flex-grow` correctly overrides an earlier `flex` shorthand (and v.v.).
1856func _layout_flex_grow(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1857 let fg: *u8 = "flex-grow\x00" as *u8 // len 9 (exact compare -- must NOT match flex-wrap, also len 9)
1858 let fx: *u8 = "flex\x00" as *u8 // len 4 (the shorthand)
1859 var i: i64 = ctx.n_computed - 1
1860 while i >= 0 {
1861 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1862 if cd.element_idx == box_idx {
1863 var matched: i64 = 0
1864 if cd.prop_len == 9 {
1865 var sp: i64 = 1
1866 var k: i64 = 0
1867 while k < 9 { if (ctx.src[cd.prop_off+k]&0xff) != (fg[k]&0xff) { sp = 0; k = 9 } else { k = k + 1 } }
1868 if sp == 1 { matched = 1 }
1869 }
1870 if matched == 0 { if cd.prop_len == 4 {
1871 var sp2: i64 = 1
1872 var k2: i64 = 0
1873 while k2 < 4 { if (ctx.src[cd.prop_off+k2]&0xff) != (fx[k2]&0xff) { sp2 = 0; k2 = 4 } else { k2 = k2 + 1 } }
1874 if sp2 == 1 { matched = 1 }
1875 } }
1876 if matched == 1 {
1877 // Parse the FIRST token of the value as a number in Q3 thousandths (so fractional grow like
1878 // 0.1 -> 100 works). For the `flex` shorthand ("0 0 0%") only the first token is the grow
1879 // factor. Skip leading spaces, take bytes up to the next space, hand to nx_css_number_parse.
1880 var p: i64 = cd.val_off
1881 let pe: i64 = cd.val_off + cd.val_len
1882 var skip: i64 = 1
1883 while skip == 1 {
1884 if p < pe { if (ctx.src[p]&0xff) == 32 { p = p + 1 } else { skip = 0 } }
1885 else { skip = 0 }
1886 }
1887 var te: i64 = p
1888 var go: i64 = 1
1889 while go == 1 { if te < pe { if (ctx.src[te]&0xff) == 32 { go = 0 } else { te = te + 1 } } else { go = 0 } }
1890 if te > p {
1891 let q3: i64 = nx_css_number_parse(ctx.src, p, te - p)
1892 if q3 == NX_CSS_NUMBER_PARSE_ERROR { return 0 }
1893 if q3 < 0 { return 0 }
1894 return q3
1895 }
1896 return 0
1897 }
1898 }
1899 i = i - 1
1900 }
1901 return 0
1902}
1903// Shift a box and its WHOLE subtree vertically by dy (used to cross-align a flex item after it's placed).
1904func _layout_shift_subtree(ctx: *LayoutCtx, box_idx: i64, dy: i64) -> i64 {
1905 if box_idx < 0 { return 0 }
1906 if box_idx >= ctx.tree.count { return 0 }
1907 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1908 b.y = b.y + dy
1909 if l_ytrap == 1 { if b.y + dy > NX_MAGIC_1000000000 {
1910 _yt_w("YTRAP@shift idx=" as *u8); _yt_n(box_idx)
1911 _yt_w(" oldy=" as *u8); _yt_n(b.y)
1912 _yt_w(" dy=" as *u8); _yt_n(dy)
1913 _yt_w("
1914" as *u8)
1915 } }
1916 var c: i64 = b.first_child_idx
1917 var safety: i64 = 0
1918 while c >= 0 {
1919 if safety >= NX_MAGIC_65536 { c = 0 - 1 }
1920 else {
1921 safety = safety + 1
1922 _layout_shift_subtree(ctx, c, dy)
1923 let cc: *LayoutBox = (ctx.tree.boxes as *u8 + (c as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1924 c = cc.next_sibling_idx
1925 }
1926 }
1927 return 0
1928}
1929func _layout_block_recurse(ctx: *LayoutCtx, table: *LayoutPropTable,
1930 box_idx: i64,
1931 avail_w: i64,
1932 origin_x: i64, origin_y: i64) -> i64 {
1933 if box_idx < 0 { return 0 }
1934 if box_idx >= ctx.tree.count { return 0 }
1935
1936 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1937
1938 // display:none -> the box (and its subtree) take ZERO space and are not laid out (paint skips too).
1939 if _layout_display_none(ctx, box_idx) == 1 {
1940 b.x = origin_x
1941 b.y = origin_y
1942 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
1943 _yt_w("YTRAP@1775 idx=" as *u8); _yt_n(box_idx)
1944 _yt_w(" oy=" as *u8); _yt_n(origin_y)
1945 _yt_w("
1946" as *u8)
1947 } }
1948 b.w = 0
1949 b.h = 0
1950 return 0
1951 }
1952
1953 // ---- TEXT box intrinsic sizing (5x7 bitmap font, 1px spacing) ----
1954 // Real CSS font-metrics integration is a separate arc; this gives
1955 // text boxes non-zero dimensions so block-flow stacking produces
1956 // visible vertical extent. Compute width = min(text_len * 6, avail_w),
1957 // height = lines * 10 where lines = ceil(text_len / line_chars) and
1958 // line_chars = max(1, avail_w / 6). Bits-up basis: see
1959 // nx_font_bitmap_5x7.nx for the underlying 5x7 glyph metrics.
1960 if b.kind == NX_LAYOUT_BOX_TEXT {
1961 let glyph_w: i64 = 9 // matches the 9x15 X render font advance (was 6)
1962 let glyph_h: i64 = 17 // matches 9x15 font height + leading (was 10)
1963 let tl: i64 = b.text_len
1964 if ctx.text_measured == 1 {
1965 // MEASURED mode: proportional width + measured word-wrap line count (font8x8 metrics SSOT).
1966 if ctx.font_tab == 0 { ctx.font_tab = (font8x8_table()) as i64 }
1967 let ftab: *u8 = ctx.font_tab as *u8
1968 let tp: *u8 = ((ctx.src as i64) + b.text_off) as *u8
1969 var mw: i64 = font8x8_text_w(ftab, tp, 0, tl)
1970 var av: i64 = avail_w
1971 if av < 4 { av = 4 }
1972 var ml: i64 = 0
1973 var ms: i64 = 0
1974 while ms < tl { ms = font8x8_next_break(ftab, tp, tl, av, ms); ml = ml + 1 }
1975 if ml < 1 { ml = 1 }
1976 if mw > avail_w { mw = avail_w }
1977 b.x = origin_x
1978 b.y = origin_y
1979 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
1980 _yt_w("YTRAP@1806 idx=" as *u8); _yt_n(box_idx)
1981 _yt_w(" oy=" as *u8); _yt_n(origin_y)
1982 _yt_w("
1983" as *u8)
1984 } }
1985 b.w = mw
1986 b.h = ml * glyph_h
1987 return b.h
1988 }
1989 var line_chars: i64 = avail_w / glyph_w
1990 if line_chars < 1 { line_chars = 1 }
1991 var lines: i64 = tl / line_chars
1992 if tl - lines * line_chars > 0 { lines = lines + 1 }
1993 if lines < 1 { lines = 1 }
1994 var w_px: i64 = tl * glyph_w
1995 if w_px > avail_w { w_px = avail_w }
1996 b.x = origin_x
1997 b.y = origin_y
1998 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
1999 _yt_w("YTRAP@1819 idx=" as *u8); _yt_n(box_idx)
2000 _yt_w(" oy=" as *u8); _yt_n(origin_y)
2001 _yt_w("
2002" as *u8)
2003 } }
2004 b.w = w_px
2005 b.h = lines * glyph_h
2006 return b.h
2007 }
2008
2009 // Resolve box-model values from cascade (-1 / UNSET = not specified). Each side = ONE reverse scan
2010 // across its longhand AND the `margin`/`padding` shorthand -- last declaration wins (cascade order).
2011 let mt_raw: i64 = _layout_boxside_px(ctx, table.src, box_idx, table.margin_top_off, table.margin_top_len, "margin" as *u8, 6, 0, NX_MARGIN_UNSET)
2012 let mb_raw: i64 = _layout_boxside_px(ctx, table.src, box_idx, table.margin_bottom_off, table.margin_bottom_len, "margin" as *u8, 6, 2, NX_MARGIN_UNSET)
2013 let ml_raw: i64 = _layout_boxside_px(ctx, table.src, box_idx, table.margin_left_off, table.margin_left_len, "margin" as *u8, 6, 3, NX_MARGIN_UNSET)
2014 let mr_raw: i64 = _layout_boxside_px(ctx, table.src, box_idx, table.margin_right_off, table.margin_right_len, "margin" as *u8, 6, 1, NX_MARGIN_UNSET)
2015 let pt_raw: i64 = _layout_boxside_px(ctx, table.src, box_idx, table.padding_top_off, table.padding_top_len, "padding" as *u8, 7, 0, 0 - 1)
2016 let pb_raw: i64 = _layout_boxside_px(ctx, table.src, box_idx, table.padding_bottom_off, table.padding_bottom_len, "padding" as *u8, 7, 2, 0 - 1)
2017 let pl_raw: i64 = _layout_boxside_px(ctx, table.src, box_idx, table.padding_left_off, table.padding_left_len, "padding" as *u8, 7, 3, 0 - 1)
2018 let pr_raw: i64 = _layout_boxside_px(ctx, table.src, box_idx, table.padding_right_off, table.padding_right_len, "padding" as *u8, 7, 1, 0 - 1)
2019 let w_raw: i64 = _layout_lookup_px(ctx, table.src, box_idx, table.width_off, table.width_len)
2020 let h_raw: i64 = _layout_lookup_height_px(ctx, table.src, box_idx, table.height_off, table.height_len)
2021
2022 // SCREEN-READER-ONLY pattern (seq1150, 2026-07-28): width:1px + height:1px (+ clip/overflow:hidden,
2023 // which this engine does not implement) is the universal accessibility hide -- wikipedia's
2024 // .mw-jump-link, WordPress .screen-reader-text. Chrome shows NOTHING readable (contents clipped to
2025 // the 1x1 box); without clipping we char-wrapped the text into a 1px-wide, 255px-tall visible
2026 // column (probe-proven on the real page). An element EXPLICITLY sized <=1x1 on BOTH axes lays out
2027 // as hidden: subtree untouched, the paint's h==0 guard skips it. A 1px DIVIDER (width:1px,
2028 // height auto/large) is NOT matched -- both axes must be explicit and <=1.
2029 if w_raw >= 0 { if w_raw <= 1 { if h_raw >= 0 { if h_raw <= 1 {
2030 b.x = origin_x
2031 b.y = origin_y
2032 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
2033 _yt_w("YTRAP@1847 idx=" as *u8); _yt_n(box_idx)
2034 _yt_w(" oy=" as *u8); _yt_n(origin_y)
2035 _yt_w("
2036" as *u8)
2037 } }
2038 b.w = 0
2039 b.h = 0
2040 return 0
2041 } } } }
2042
2043 var mt: i64 = mt_raw
2044 if mt == NX_MARGIN_UNSET { mt = 0 } // unset -> 0; a real NEGATIVE margin is KEPT (overlap), not clamped
2045 var mb: i64 = mb_raw
2046 if mb == NX_MARGIN_UNSET { mb = 0 }
2047 var ml: i64 = ml_raw
2048 if ml == NX_MARGIN_UNSET { ml = 0 }
2049 var mr: i64 = mr_raw
2050 if mr == NX_MARGIN_UNSET { mr = 0 }
2051 var pt: i64 = pt_raw
2052 if pt < 0 { pt = 0 }
2053 var pb: i64 = pb_raw
2054 if pb < 0 { pb = 0 }
2055 var pl: i64 = pl_raw
2056 if pl < 0 { pl = 0 }
2057 var pr: i64 = pr_raw
2058 if pr < 0 { pr = 0 }
2059
2060 // Width: explicit, else fill avail_w (minus horizontal margins).
2061 var content_w: i64 = w_raw
2062 if content_w < 0 {
2063 content_w = avail_w - ml - mr
2064 if content_w < 0 { content_w = 0 }
2065 }
2066 // MAX-WIDTH (CSS 2.1 §10.4) via the literal-source lookup (the top/left trick -- no prop-table
2067 // widening). Constrains both explicit and fill widths; absurd values already rejected upstream.
2068 let mxw: i64 = _layout_lookup_px(ctx, "max-width\x00" as *u8, box_idx, 0, 9)
2069 if mxw >= 0 { if mxw < NX_MAGIC_100000 { if content_w > mxw { content_w = mxw } } }
2070
2071 // Position the border-box. Horizontal margin shifts x; padding
2072 // expands children's available width and offsets their origin.
2073 b.x = origin_x + ml
2074 // AUTO-MARGIN CENTERING (§10.3.3): both horizontal margins auto + used width < avail -> the free
2075 // space splits equally. THE styled-wikipedia dx≈150 regime: `.mw-page-container{margin:0 auto;
2076 // max-width:...}` -- Chrome centers the whole page column; we rendered it flush-left (2026-07-28).
2077 if content_w < avail_w {
2078 if _lb_margin_auto(ctx, box_idx, 3) == 1 { if _lb_margin_auto(ctx, box_idx, 1) == 1 {
2079 b.x = origin_x + (avail_w - content_w) / 2
2080 } }
2081 }
2082 b.y = origin_y + mt
2083 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
2084 _yt_w("YTRAP block idx=" as *u8); _yt_n(box_idx)
2085 _yt_w(" origin_y=" as *u8); _yt_n(origin_y)
2086 _yt_w(" mt=" as *u8); _yt_n(mt)
2087 _yt_w(" mt_raw=" as *u8); _yt_n(mt_raw)
2088 _yt_w("\n" as *u8)
2089 } }
2090 b.w = content_w
2091
2092 // Lay out children inside the content box.
2093 let content_origin_x: i64 = b.x + pl
2094 let content_origin_y: i64 = b.y + pt
2095 let child_avail_w: i64 = content_w - pl - pr
2096
2097 var children_h: i64 = 0
2098 var use_grid: i64 = _layout_display_grid(ctx, box_idx)
2099 if use_grid == 1 { if _layout_has_grid_areas(ctx, box_idx) == 1 { use_grid = 0 } } // named areas -> block flow
2100 if use_grid == 1 {
2101 // ---- GRID (rung 2) ---- tracks from grid-template-columns with PER-TRACK sizing (px fixed + fr
2102 // flexible share of leftover) + column-gap/row-gap/gap. Children placed ROW-MAJOR, wrapping every
2103 // ncols; row height = tallest child outer height; rows separated by row_gap. (rung 1 = equal cols
2104 // is the special case all-fr-equal/no-gap.) grid-template-rows, line placement + spanning = later.
2105 let g_toff: *i64 = sys_mmap(8) as *i64
2106 let g_tlen: *i64 = sys_mmap(8) as *i64
2107 let g_widths: *i64 = sys_mmap(8 * 258) as *i64
2108 let col_gap: i64 = _grid_gap(ctx, box_idx, 0)
2109 let row_gap: i64 = _grid_gap(ctx, box_idx, 1)
2110 var ncols: i64 = 1
2111 if _layout_grid_template(ctx, box_idx, g_toff, g_tlen) == 1 {
2112 ncols = _grid_resolve_tracks(ctx.src, g_toff[0], g_tlen[0], child_avail_w, col_gap, g_widths, 256)
2113 } else { g_widths[0] = child_avail_w }
2114 if ncols < 1 { ncols = 1 }
2115 var gcol: i64 = 0
2116 var rows_done: i64 = 0
2117 var row_y: i64 = content_origin_y
2118 var row_h: i64 = 0
2119 var col_x: i64 = content_origin_x
2120 var gci: i64 = b.first_child_idx
2121 var gsf: i64 = 0
2122 while gci >= 0 {
2123 if gsf >= NX_MAGIC_4096 { gci = 0 - 1 }
2124 else {
2125 gsf = gsf + 1
2126 if gcol == 0 {
2127 if rows_done > 0 { row_y = row_y + row_gap }
2128 col_x = content_origin_x
2129 }
2130 let gcw: i64 = g_widths[gcol]
2131 let gchh: i64 = _layout_block_recurse(ctx, table, gci, gcw, col_x, row_y)
2132 if gchh > row_h { row_h = gchh }
2133 let gcc: *LayoutBox = (ctx.tree.boxes as *u8 + (gci as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2134 let gnxt: i64 = gcc.next_sibling_idx
2135 col_x = col_x + gcw + col_gap
2136 gcol = gcol + 1
2137 if gcol >= ncols { row_y = row_y + row_h; rows_done = rows_done + 1; row_h = 0; gcol = 0 }
2138 gci = gnxt
2139 }
2140 }
2141 if gcol > 0 { row_y = row_y + row_h }
2142 children_h = row_y - content_origin_y
2143 } else {
2144 // TABLE (seq1158): resolve this table's column widths ONCE, publish them for the row (flex) path
2145 // below, lay the subtree out normally, then RESTORE the previous table context -- nested tables
2146 // (news.ycombinator.com nests them) each get their own columns and the outer one survives intact.
2147 var tbl_saved_active: i64 = 0
2148 var tbl_saved_idx: i64 = 0
2149 var tbl_saved_ncols: i64 = 0
2150 var tbl_saved_p: i64 = 0
2151 var tbl_entered: i64 = 0
2152 if _layout_display_table(ctx, box_idx) == 1 {
2153 tbl_saved_active = l_tbl_active
2154 tbl_saved_idx = l_tbl_idx
2155 tbl_saved_ncols = l_tbl_ncols
2156 tbl_saved_p = l_tbl_colw_p
2157 tbl_entered = 1
2158 let cwbuf: *i64 = sys_mmap(8 * NX_TBL_MAXCOLS) as *i64
2159 let nc: i64 = _layout_table_cols(ctx, box_idx, child_avail_w, cwbuf)
2160 if nc > 0 {
2161 l_tbl_active = 1
2162 l_tbl_idx = box_idx
2163 l_tbl_ncols = nc
2164 l_tbl_colw_p = cwbuf as i64
2165 } else { l_tbl_active = 0 }
2166 }
2167 var is_rowflex: i64 = _layout_display_flex(ctx, box_idx)
2168 if is_rowflex == 1 { if _layout_flex_dir_col(ctx, box_idx) == 1 { is_rowflex = 0 } } // column flex = block stacking
2169 if is_rowflex == 1 {
2170 // ---- FLEX ROW (rung 2: + justify-content) ---- children LEFT-TO-RIGHT; width = explicit `width`
2171 // else an equal share of the remaining space (auto-fill); container height = tallest child.
2172 // justify-content distributes the FREE space (avail - used) on the main axis -- a no-op when
2173 // children auto-fill (free=0, so tables/sidebars are unchanged), active when children have explicit
2174 // widths. flex-grow/shrink/basis, wrap, align-items = subsequent rungs.
2175 var nauto: i64 = 0
2176 var sum_explicit: i64 = 0
2177 var sum_base: i64 = 0 // Σ flex-basis (explicit width else content width) -- auto != 0
2178 var n_children: i64 = 0
2179 var sum_grow: i64 = 0 // Σ flex-grow over children, in Q3 thousandths (rung 3; fractional-aware)
2180 var has_grow: i64 = 0 // any child with flex-grow>0 -> use the grow distribution path
2181 var ci: i64 = b.first_child_idx
2182 var sf: i64 = 0
2183 while ci >= 0 {
2184 if sf >= NX_MAGIC_4096 { ci = 0 - 1 }
2185 else {
2186 sf = sf + 1
2187 n_children = n_children + 1
2188 let cw0: i64 = _layout_lookup_px(ctx, table.src, ci, table.width_off, table.width_len)
2189 let cg: i64 = _layout_flex_grow(ctx, ci)
2190 // flex-grow accrues for EVERY growing item (explicit-width OR auto basis). BASE size =
2191 // explicit width else the item's CONTENT width -- CSS flex-basis defaults to AUTO, not 0.
2192 // The old base-0 model collapsed every grow-0 auto sibling to w=0: x.com's login card laid
2193 // "Continue with Google" (explicit) while "Continue with phone/Apple"/"or"/"Email or
2194 // username" all rendered as w=0 slivers (region x-ray, 2026-07-28).
2195 if cg > 0 { has_grow = 1; sum_grow = sum_grow + cg }
2196 var cb0: i64 = cw0
2197 if cb0 < 0 { cb0 = _layout_intrinsic_w(ctx, ci) }
2198 sum_base = sum_base + cb0
2199 if cw0 >= 0 { sum_explicit = sum_explicit + cw0 } else { nauto = nauto + 1 }
2200 let cc: *LayoutBox = (ctx.tree.boxes as *u8 + (ci as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2201 ci = cc.next_sibling_idx
2202 }
2203 }
2204 var rem: i64 = child_avail_w - sum_explicit
2205 // FLEX-SHRINK (minimal, 2026-07-28): when explicit-width children OVERFLOW the row, real CSS
2206 // shrinks them (flex-shrink defaults to 1); our old model kept them full-size and starved every
2207 // auto sibling to w=0 (the x.com login-card sliver that survived the basis fix). Shrink-mode
2208 // sizes EVERY child proportionally to its basis so the row exactly fits. Fires ONLY in the
2209 // previously-broken overflow case -- healthy rows are untouched by construction.
2210 var shrinkmode: i64 = 0
2211 if has_grow == 0 { if rem <= 0 { if sum_base > 0 { if n_children > 1 { shrinkmode = 1 } } } }
2212 if rem < 0 { rem = 0 }
2213 var auto_w: i64 = 0
2214 if nauto > 0 { auto_w = rem / nauto }
2215 // free space on the main axis after laying children at their widths -> justify-content distributes it
2216 let total_used: i64 = sum_explicit + nauto * auto_w
2217 var free: i64 = child_avail_w - total_used
2218 if free < 0 { free = 0 }
2219 // flex-grow (rung 3): distribute the leftover space (avail - Σ explicit-width siblings) among the
2220 // AUTO-basis growing items, proportionally to their grow factors (basis 0). Grown items consume
2221 // all the leftover, so there is no main-axis free space for justify-content (free=0 -> no-op).
2222 var grow_free: i64 = 0
2223 if has_grow == 1 {
2224 grow_free = child_avail_w - sum_base // leftover AFTER content-sized bases (basis:auto)
2225 if grow_free < 0 { grow_free = 0 }
2226 free = 0
2227 }
2228 let jc: i64 = _layout_justify(ctx, box_idx)
2229 var start_off: i64 = 0
2230 var spacing: i64 = 0
2231 if jc == 1 { start_off = free / 2 } // center
2232 if jc == 2 { start_off = free } // flex-end
2233 if jc == 3 { if n_children > 1 { spacing = free / (n_children - 1) } } // space-between
2234 if jc == 4 { if n_children > 0 { spacing = free / n_children; start_off = spacing / 2 } } // space-around
2235 if jc == 5 { if n_children > 0 { spacing = free / (n_children + 1); start_off = spacing } } // space-evenly
2236 var rx: i64 = content_origin_x + start_off
2237 var max_h: i64 = 0
2238 var tcell_i: i64 = 0 // 0-based cell index = the COLUMN index for a table row
2239 ci = b.first_child_idx
2240 sf = 0
2241 while ci >= 0 {
2242 if sf >= NX_MAGIC_4096 { ci = 0 - 1 }
2243 else {
2244 sf = sf + 1
2245 let cwe: i64 = _layout_lookup_px(ctx, table.src, ci, table.width_off, table.width_len)
2246 var cw: i64 = auto_w
2247 if cwe >= 0 { cw = cwe }
2248 if shrinkmode == 1 {
2249 var sbase: i64 = cwe
2250 if sbase < 0 { sbase = _layout_intrinsic_w(ctx, ci) }
2251 cw = (sbase * child_avail_w) / sum_base
2252 }
2253 // TABLE COLUMN (seq1158): an auto-width cell of a row belonging to the ACTIVE table takes
2254 // its COLUMN's shared max-content width instead of an equal share. Guarded by the row's
2255 // nearest-table identity, so a plain display:flex div inside a cell is unaffected.
2256 // colspan (seq1159): a spanning cell takes the SUM of its N columns and advances N slots.
2257 var tcell_adv: i64 = 1
2258 if l_tbl_active == 1 { if cwe < 0 { if tcell_i < l_tbl_ncols {
2259 if _layout_nearest_table(ctx, box_idx) == l_tbl_idx {
2260 let tcolw: *i64 = l_tbl_colw_p as *i64
2261 let cellb: *LayoutBox = _lb_box(ctx, ci)
2262 if cellb.source_node_idx >= 2 { tcell_adv = cellb.source_node_idx }
2263 var csum: i64 = 0
2264 var ck: i64 = 0
2265 while ck < tcell_adv {
2266 if tcell_i + ck < l_tbl_ncols { csum = csum + tcolw[tcell_i + ck] }
2267 ck = ck + 1
2268 }
2269 cw = csum
2270 }
2271 } } }
2272 // flex-grow distribution: width = base + its proportional share of the leftover, where
2273 // base = explicit width (flex-basis) else 0. A grow-0 auto item collapses to 0; a grow-0
2274 // explicit item keeps its width. Active only in a has_grow container, so non-grow flex
2275 // layouts (the common case) are byte-identical to before.
2276 if has_grow == 1 {
2277 let cg2: i64 = _layout_flex_grow(ctx, ci)
2278 var base: i64 = cwe
2279 if base < 0 { base = _layout_intrinsic_w(ctx, ci) } // flex-basis:auto = content size
2280 var share: i64 = 0
2281 // divisor = max(1.0, Σgrow) in Q3 thousandths: when Σgrow<1, only that fraction of the
2282 // free space is distributed (the rest stays unused), per CSS Flexbox §9.7. When Σgrow>=1
2283 // it normalizes (grow_i/Σgrow) as usual. Integer grow (1->1000) is unchanged.
2284 if cg2 > 0 {
2285 var divisor: i64 = sum_grow
2286 if divisor < 1000 { divisor = 1000 }
2287 share = (cg2 * grow_free) / divisor
2288 }
2289 cw = base + share
2290 }
2291 let chh: i64 = _layout_block_recurse(ctx, table, ci, cw, rx, content_origin_y)
2292 // explicit-width grow item: the recurse sized the box to its explicit width (it reads the
2293 // CSS width directly, ignoring the grown avail we passed) -> force the grown main-size onto
2294 // the box. Exact for leaf items; a non-leaf's children keep the explicit width (minor approx).
2295 if has_grow == 1 { if cwe >= 0 {
2296 let ccg: *LayoutBox = (ctx.tree.boxes as *u8 + (ci as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2297 ccg.w = cw
2298 } }
2299 rx = rx + cw + spacing
2300 tcell_i = tcell_i + tcell_adv
2301 if chh > max_h { max_h = chh }
2302 let cc2: *LayoutBox = (ctx.tree.boxes as *u8 + (ci as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2303 ci = cc2.next_sibling_idx
2304 }
2305 }
2306 // align-items (cross axis): items were placed at the top; center/flex-end shift each down within
2307 // the row's height (max_h). A no-op for the default (stretch/flex-start) and when all items are
2308 // equal-height (offset 0), so existing flex layouts are unchanged.
2309 let ai: i64 = _layout_align(ctx, box_idx)
2310 if ai != 0 {
2311 // cross-size = the container's content height (explicit height wins, else the tallest item)
2312 var cross_size: i64 = max_h
2313 if h_raw >= 0 { cross_size = h_raw }
2314 var ci3: i64 = b.first_child_idx
2315 var sf3: i64 = 0
2316 while ci3 >= 0 {
2317 if sf3 >= NX_MAGIC_4096 { ci3 = 0 - 1 }
2318 else {
2319 sf3 = sf3 + 1
2320 let cb: *LayoutBox = (ctx.tree.boxes as *u8 + (ci3 as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2321 let chh2: i64 = cb.h
2322 var aoff: i64 = 0
2323 if ai == 1 { aoff = (cross_size - chh2) / 2 }
2324 if ai == 2 { aoff = cross_size - chh2 }
2325 if aoff < 0 { aoff = 0 }
2326 if aoff > NX_MAGIC_100000 { aoff = 0 } // a cross-shift beyond any page is corrupt input, never applied
2327 let cnext: i64 = cb.next_sibling_idx
2328 if aoff > 0 { _layout_shift_subtree(ctx, ci3, aoff) }
2329 ci3 = cnext
2330 }
2331 }
2332 }
2333 children_h = max_h
2334 } else {
2335 // Mixed block/inline flow: BLOCK children stack vertically; runs of INLINE/TEXT children flow
2336 // horizontally via a line-box pen (wrapping at avail_w). The pen's pen_y is the single vertical
2337 // cursor; a BLOCK child first flushes any open inline line, then advances pen_y by its height.
2338 let pen: *InlinePen = (sys_mmap(NX_INLINE_PEN_BYTES as nx_size)) as *InlinePen
2339 pen.pen_x = 0
2340 pen.pen_y = 0
2341 pen.line_h = 0
2342 pen.avail_w = child_avail_w
2343 pen.origin_x = content_origin_x
2344 pen.origin_y = content_origin_y
2345 pen.table = table as i64
2346 // float state (R5 rung 1): ONE active float region; fl_bottom/fl_w are content-relative.
2347 // float_extent = lowest float bottom seen (so the container grows to contain a tall float).
2348 var fl_side: i64 = 0 // 0 none, 1 left, 2 right
2349 var fl_bottom: i64 = 0
2350 var fl_w: i64 = 0
2351 var float_extent: i64 = 0
2352 var child_idx: i64 = b.first_child_idx
2353 var safety: i64 = 0
2354 let MAX_ITER: i64 = NX_MAGIC_65536
2355 var keep: i64 = 1
2356 while keep == 1 {
2357 if safety >= MAX_ITER { keep = 0 }
2358 else {
2359 safety = safety + 1
2360 if child_idx < 0 { keep = 0 }
2361 else {
2362 let c: *LayoutBox = (ctx.tree.boxes as *u8 + (child_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2363 if c.kind == NX_LAYOUT_BOX_BLOCK {
2364 if pen.pen_x > 0 {
2365 pen.pen_y = pen.pen_y + pen.line_h
2366 pen.pen_x = 0
2367 pen.line_h = 0
2368 }
2369 // OUT-OF-FLOW POSITIONING (CSS 2.1 §9.3): absolute/fixed boxes are removed
2370 // from normal flow -- they do NOT advance the cursor and do NOT contribute
2371 // height. Before this, an overlay/dropdown/sticky-header stacked as a normal
2372 // block, so real stylesheets built a tall scattered ribbon instead of a
2373 // layout: MEASURED 2026-07-27, stackoverflow's page_h ran 18,617 vs Chrome's
2374 // ~7,000 with CSS on, and layout_pos_probe showed an absolute box adding its
2375 // full 100px to the page. They are still laid out and painted (at the current
2376 // flow origin, offset by top/left when given) so content stays visible --
2377 // full containing-block resolution is the next rung, but consuming flow space
2378 // is the part that actually wrecks the page.
2379 var oof: i64 = 0
2380 let posk: i64 = _layout_position(ctx, child_idx)
2381 if posk == 2 { oof = 1 }
2382 if posk == 3 { oof = 1 }
2383 if oof == 1 {
2384 // `top`/`left` are not in LayoutPropTable (it carries only margin/padding/
2385 // width/height), and widening that struct would touch every constructor.
2386 // _layout_lookup_px compares against a CALLER-SUPPLIED buffer, so a string
2387 // literal at offset 0 is a legal property source -- no struct change needed.
2388 let poff_t: i64 = _layout_lookup_px(ctx, "top\x00" as *u8, child_idx, 0, 3)
2389 let poff_l: i64 = _layout_lookup_px(ctx, "left\x00" as *u8, child_idx, 0, 4)
2390 var pax: i64 = content_origin_x
2391 var pay: i64 = content_origin_y + pen.pen_y
2392 if poff_l >= 0 { pax = content_origin_x + poff_l }
2393 if poff_t >= 0 { pay = content_origin_y + poff_t }
2394 // laid out + painted, but pen.pen_y is NOT advanced -> zero flow height.
2395 _layout_block_recurse(ctx, table, child_idx, child_avail_w, pax, pay)
2396 }
2397 let flo: i64 = _layout_float(ctx, child_idx)
2398 let fwe: i64 = _layout_lookup_px(ctx, table.src, child_idx, table.width_off, table.width_len)
2399 if oof == 1 { fl_side = fl_side } // out-of-flow: nothing else to do
2400 else { if flo != 0 { if fwe >= 0 {
2401 // FLOATED block with explicit width: pin to the left/right edge at the current
2402 // pen_y, OUT of normal flow (pen_y does NOT advance -> following blocks flow beside).
2403 var fx: i64 = content_origin_x
2404 if flo == 2 { fx = content_origin_x + child_avail_w - fwe }
2405 let fh: i64 = _layout_block_recurse(ctx, table, child_idx, fwe, fx, content_origin_y + pen.pen_y)
2406 fl_side = flo
2407 fl_w = fwe
2408 let fbot: i64 = pen.pen_y + fh
2409 if fbot > fl_bottom { fl_bottom = fbot }
2410 if fbot > float_extent { float_extent = fbot }
2411 } else {
2412 // float w/o explicit width: shrink-to-fit is a later rung -> normal full-width block.
2413 let chn: i64 = _layout_block_recurse(ctx, table, child_idx, child_avail_w, content_origin_x, content_origin_y + pen.pen_y)
2414 pen.pen_y = pen.pen_y + chn
2415 } }
2416 else {
2417 // NORMAL block: while an active float covers this y, flow BESIDE it (shifted + narrower).
2418 var bx: i64 = content_origin_x
2419 var bw: i64 = child_avail_w
2420 if fl_side != 0 { if pen.pen_y < fl_bottom {
2421 bw = child_avail_w - fl_w
2422 if bw < 0 { bw = 0 }
2423 if fl_side == 1 { bx = content_origin_x + fl_w } // left float pushes content right
2424 } }
2425 let child_outer_h: i64 = _layout_block_recurse(ctx, table, child_idx, bw, bx, content_origin_y + pen.pen_y)
2426 pen.pen_y = pen.pen_y + child_outer_h
2427 if fl_side != 0 { if pen.pen_y >= fl_bottom { fl_side = 0; fl_w = 0 } } // cleared the float
2428 } }
2429 } else {
2430 // INLINE content beside a float: narrow/shift the pen so inline text flows BESIDE
2431 // the float (the same treatment BLOCK children get above) instead of overlapping it
2432 // -- the Wikipedia header fix (a float:right infobox was overlapping the intro text).
2433 // ALWAYS recompute the pen constraints from the CURRENT float coverage (not just when
2434 // active) so a cleared float resets cleanly to full width. Right float narrows
2435 // avail_w; left float also shifts origin_x past the float.
2436 var iaw: i64 = child_avail_w
2437 var ileft: i64 = content_origin_x
2438 if fl_side != 0 { if pen.pen_y < fl_bottom {
2439 iaw = child_avail_w - fl_w
2440 if iaw < 0 { iaw = 0 }
2441 if fl_side == 1 { ileft = content_origin_x + fl_w }
2442 } }
2443 pen.avail_w = iaw
2444 pen.origin_x = ileft
2445 _layout_inline_flow(ctx, pen, child_idx)
2446 if fl_side != 0 { if pen.pen_y >= fl_bottom { fl_side = 0; fl_w = 0 } }
2447 }
2448 child_idx = c.next_sibling_idx
2449 }
2450 }
2451 }
2452 if pen.pen_x > 0 {
2453 pen.pen_y = pen.pen_y + pen.line_h
2454 pen.pen_x = 0
2455 pen.line_h = 0
2456 }
2457 children_h = pen.pen_y
2458 if float_extent > children_h { children_h = float_extent } // contain a float taller than the text
2459 }
2460 }
2461 // leaving a table: restore the enclosing table's column context (nested-table correctness)
2462 if tbl_entered == 1 {
2463 l_tbl_active = tbl_saved_active
2464 l_tbl_idx = tbl_saved_idx
2465 l_tbl_ncols = tbl_saved_ncols
2466 l_tbl_colw_p = tbl_saved_p
2467 }
2468
2469 // Content height: explicit `height` declaration wins, else
2470 // sum of children heights.
2471 var content_h: i64 = children_h
2472 if h_raw >= 0 { content_h = h_raw }
2473
2474 b.h = pt + content_h + pb
2475
2476 return mt + b.h + mb
2477}
2478
2479// ---- public API ----
2480
2481func nx_layout_block_layout(ctx: *LayoutCtx, table: *LayoutPropTable,
2482 root_idx: i64) -> i64 {
2483 if root_idx < 0 { return -1 }
2484 if root_idx >= ctx.tree.count { return -1 }
2485 let outer_h: i64 = _layout_block_recurse(ctx, table, root_idx,
2486 ctx.viewport_w, 0, 0)
2487 return outer_h
2488}
2489
2490func nx_layout_ctx_init(ctx: *LayoutCtx, tree: *LayoutTree,
2491 computed: *CssComputedDecl, n_computed: i64,
2492 src: *u8, viewport_w: i64,
2493 resolve_ctx: *CssResolveCtx) -> i64 {
2494 ctx.tree = tree
2495 ctx.computed = computed
2496 ctx.n_computed = n_computed
2497 ctx.src = src
2498 ctx.viewport_w = viewport_w
2499 ctx.resolve_ctx = resolve_ctx
2500 ctx.text_measured = 0 // legacy monospace cells unless the consumer opts in (additive)
2501 ctx.font_tab = 0
2502 return 0
2503}