nx_layout_block.nx source
↩ module page · 2734 lines · 138056 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
734// ---- FACE RESOLUTION (BR27, 2026-09-02): font-weight / font-style / font-family -> face slot ------------
735// ONE resolver for BOTH stages. Layout calls it through the ctx, paint calls it with the same four values
736// (src, computed, ncomp, tree), so a run cannot be measured with one face and painted with another. The
737// answer is memoised per box for the lifetime of a tree (a fresh br_layout allocates a fresh tree, which
738// resets the memo; the paint pass over the SAME tree reuses it), so the cost is one reverse scan of the
739// computed declarations per ELEMENT, the same class _layout_fontsize_inherited already pays.
740// Memo value: 0 = unresolved, else 1 + bold + 2*italic + 4*serif.
741static lf_memo: i64
742static lf_memo_tree: i64
743static lf_memo_count: i64
744const LF_UNDECIDED: i64 = 0 - 1
745func _lf_memo_for(tree: *LayoutTree) -> *i64 {
746 if lf_memo != 0 { if lf_memo_tree == (tree as i64) { if lf_memo_count == tree.count { return lf_memo as *i64 } } }
747 let m: *i64 = sys_mmap(8*(tree.count + 1)) as *i64
748 var i: i64 = 0
749 while i < tree.count { m[i] = 0; i = i + 1 }
750 lf_memo = m as i64
751 lf_memo_tree = tree as i64
752 lf_memo_count = tree.count
753 return m
754}
755func _lf_prop_is(src: *u8, cd: *CssComputedDecl, lit: *u8, len: i64) -> i64 {
756 if cd.prop_len != len { return 0 }
757 var k: i64 = 0
758 while k < len { if (src[cd.prop_off+k]&0xff) != (lit[k]&0xff) { return 0 } k = k + 1 }
759 return 1
760}
761// case-insensitive substring test over the declaration VALUE
762func _lf_val_has(src: *u8, cd: *CssComputedDecl, lit: *u8, len: i64) -> i64 {
763 if cd.val_len < len { return 0 }
764 var i: i64 = 0
765 while i + len <= cd.val_len {
766 var k: i64 = 0
767 var ok: i64 = 1
768 while k < len {
769 var c: i64 = src[cd.val_off+i+k]&0xff
770 if c >= 65 { if c <= 90 { c = c + 32 } }
771 if c != (lit[k]&0xff) { ok = 0; k = len } else { k = k + 1 }
772 }
773 if ok == 1 { return 1 }
774 i = i + 1
775 }
776 return 0
777}
778// weight from a font-weight (or font shorthand) value: 1 bold, 0 normal, -1 says nothing
779func _lf_weight_of(src: *u8, cd: *CssComputedDecl) -> i64 {
780 if _lf_val_has(src, cd, "bold\x00" as *u8, 4) == 1 { return 1 }
781 if _lf_val_has(src, cd, "normal\x00" as *u8, 6) == 1 { return 0 }
782 if _lf_val_has(src, cd, "lighter\x00" as *u8, 7) == 1 { return 0 }
783 // numeric weight: the first run of digits, 600 and up is bold
784 var v: i64 = 0
785 var any: i64 = 0
786 var i: i64 = 0
787 while i < cd.val_len {
788 let c: i64 = src[cd.val_off+i]&0xff
789 if c >= 48 { if c <= 57 { v = v*10 + (c-48); any = 1 } }
790 if any == 1 { if c < 48 { i = cd.val_len } }
791 if c > 57 { if any == 1 { i = cd.val_len } }
792 i = i + 1
793 }
794 if any == 1 { if v >= 600 { return 1 } if v >= 100 { return 0 } }
795 return LF_UNDECIDED
796}
797func _lf_style_of(src: *u8, cd: *CssComputedDecl) -> i64 {
798 if _lf_val_has(src, cd, "italic\x00" as *u8, 6) == 1 { return 1 }
799 if _lf_val_has(src, cd, "oblique\x00" as *u8, 7) == 1 { return 1 }
800 if _lf_val_has(src, cd, "normal\x00" as *u8, 6) == 1 { return 0 }
801 return LF_UNDECIDED
802}
803// family class: 1 serif, 0 sans (monospace reads as sans until a mono face exists), -1 says nothing
804func _lf_family_of(src: *u8, cd: *CssComputedDecl) -> i64 {
805 if _lf_val_has(src, cd, "sans\x00" as *u8, 4) == 1 { return 0 }
806 if _lf_val_has(src, cd, "arial\x00" as *u8, 5) == 1 { return 0 }
807 if _lf_val_has(src, cd, "helvetica\x00" as *u8, 9) == 1 { return 0 }
808 if _lf_val_has(src, cd, "verdana\x00" as *u8, 7) == 1 { return 0 }
809 if _lf_val_has(src, cd, "roboto\x00" as *u8, 6) == 1 { return 0 }
810 if _lf_val_has(src, cd, "segoe\x00" as *u8, 5) == 1 { return 0 }
811 if _lf_val_has(src, cd, "system-ui\x00" as *u8, 9) == 1 { return 0 }
812 if _lf_val_has(src, cd, "monospace\x00" as *u8, 9) == 1 { return 0 }
813 if _lf_val_has(src, cd, "serif\x00" as *u8, 5) == 1 { return 1 }
814 if _lf_val_has(src, cd, "georgia\x00" as *u8, 7) == 1 { return 1 }
815 if _lf_val_has(src, cd, "times\x00" as *u8, 5) == 1 { return 1 }
816 if _lf_val_has(src, cd, "libertine\x00" as *u8, 9) == 1 { return 1 }
817 if _lf_val_has(src, cd, "garamond\x00" as *u8, 8) == 1 { return 1 }
818 if _lf_val_has(src, cd, "cambria\x00" as *u8, 7) == 1 { return 1 }
819 return LF_UNDECIDED
820}
821// the face triple declared ON one element (last decl wins per property; the `font` shorthand counts for
822// all three): out[0]=bold out[1]=italic out[2]=serif, each -1 when the element says nothing
823func _lf_own(src: *u8, computed: *CssComputedDecl, ncomp: i64, box_idx: i64, out: *i64) -> i64 {
824 out[0] = LF_UNDECIDED; out[1] = LF_UNDECIDED; out[2] = LF_UNDECIDED
825 var i: i64 = ncomp - 1
826 while i >= 0 {
827 let cd: *CssComputedDecl = (computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
828 if cd.element_idx == box_idx {
829 if out[0] == LF_UNDECIDED { if _lf_prop_is(src, cd, "font-weight\x00" as *u8, 11) == 1 { out[0] = _lf_weight_of(src, cd) } }
830 if out[1] == LF_UNDECIDED { if _lf_prop_is(src, cd, "font-style\x00" as *u8, 10) == 1 { out[1] = _lf_style_of(src, cd) } }
831 if out[2] == LF_UNDECIDED { if _lf_prop_is(src, cd, "font-family\x00" as *u8, 11) == 1 { out[2] = _lf_family_of(src, cd) } }
832 if _lf_prop_is(src, cd, "font\x00" as *u8, 4) == 1 {
833 if out[0] == LF_UNDECIDED { out[0] = _lf_weight_of(src, cd) }
834 if out[1] == LF_UNDECIDED { out[1] = _lf_style_of(src, cd) }
835 if out[2] == LF_UNDECIDED { out[2] = _lf_family_of(src, cd) }
836 }
837 }
838 i = i - 1
839 }
840 return 0
841}
842// THE resolver: the face slot for the text under box_idx, inheriting each undeclared property from the
843// nearest ancestor that declares it (CSS inheritance), root defaults = sans regular upright.
844func nx_layout_face_for_box(src: *u8, computed: *CssComputedDecl, ncomp: i64, tree: *LayoutTree, box_idx: i64) -> i64 {
845 if box_idx < 0 { return 0 }
846 if box_idx >= tree.count { return 0 }
847 let memo: *i64 = _lf_memo_for(tree)
848 if memo[box_idx] != 0 { return memo[box_idx] - 1 }
849 let own: *i64 = sys_mmap(32) as *i64
850 _lf_own(src, computed, ncomp, box_idx, own)
851 var bold: i64 = own[0]
852 var italic: i64 = own[1]
853 var serif: i64 = own[2]
854 if bold == LF_UNDECIDED { bold = 0 }
855 if italic == LF_UNDECIDED { italic = 0 }
856 if serif == LF_UNDECIDED { serif = 0 }
857 var need: i64 = 0
858 if own[0] == LF_UNDECIDED { need = 1 }
859 if own[1] == LF_UNDECIDED { need = 1 }
860 if own[2] == LF_UNDECIDED { need = 1 }
861 if need == 1 {
862 let b: *LayoutBox = (tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
863 if b.parent_idx >= 0 { if b.parent_idx != box_idx {
864 let pidx: i64 = nx_layout_face_for_box(src, computed, ncomp, tree, b.parent_idx)
865 if own[0] == LF_UNDECIDED { bold = pidx % 2 }
866 if own[1] == LF_UNDECIDED { italic = (pidx / 2) % 2 }
867 if own[2] == LF_UNDECIDED { serif = (pidx / 4) % 2 }
868 } }
869 }
870 let idx: i64 = font_face_pick(bold, italic, serif)
871 memo[box_idx] = idx + 1
872 return idx
873}
874func _layout_face_inherited(ctx: *LayoutCtx, box_idx: i64) -> i64 {
875 return nx_layout_face_for_box(ctx.src, ctx.computed, ctx.n_computed, ctx.tree, box_idx)
876}
877func _il_text_w(b: *LayoutBox, avail_w: i64) -> i64 {
878 var w_px: i64 = b.text_len * NX_INLINE_GLYPH_W
879 if w_px > avail_w { w_px = avail_w }
880 if w_px < 0 { w_px = 0 }
881 return w_px
882}
883
884// Flow one inline-level box (and its inline subtree) into the pen. TEXT boxes are placed and advance
885// the pen; INLINE boxes recurse so their inline descendants share the same line context. Returns 0.
886// CSS line-height resolver (the S21 height-axis remainder): supports `line-height: 1.55` (unitless
887// MULTIPLIER of the element's own font-size -- inherits as the number, so it recomputes against each
888// descendant's fs), `line-height: 24px` (fixed), and the `font` SHORTHAND's /N part ("15px/1.55 ...").
889// One reverse scan per element = last-wins across both properties; nearest ancestor with a decl wins
890// (CSS inheritance). Returns px, or -1 when nothing declares it (callers keep the legacy 17*scale line).
891// The paint side (br_comp_lineheight_px) implements the IDENTICAL rule -- measure==paint law.
892func _layout_lineheight_decl_px(ctx: *LayoutCtx, element_idx: i64, fs: i64) -> i64 {
893 let lname: *u8 = "line-height\x00" as *u8
894 let fsh: *u8 = "font\x00" as *u8
895 var i: i64 = ctx.n_computed - 1
896 while i >= 0 {
897 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
898 if cd.element_idx == element_idx {
899 if cd.prop_len == 11 {
900 var same: i64 = 1
901 var k: i64 = 0
902 while k < 11 {
903 if ((ctx.src[cd.prop_off + k] as i64) & 255) != ((lname[k] as i64) & 255) { same = 0; k = 11 } else { k = k + 1 }
904 }
905 if same == 1 {
906 // number span then optional unit ("1.55" unitless / "24px")
907 var nl: i64 = 0
908 var ns: i64 = 1
909 while ns == 1 {
910 if nl >= cd.val_len { ns = 0 }
911 else {
912 let c: i64 = (ctx.src[cd.val_off + nl] as i64) & 255
913 var isn: i64 = 0
914 if c >= 48 { if c <= 57 { isn = 1 } }
915 if c == 46 { isn = 1 }
916 if isn == 1 { nl = nl + 1 } else { ns = 0 }
917 }
918 }
919 if nl == 0 { return 0 - 1 }
920 let q3: i64 = nx_css_number_parse(ctx.src, cd.val_off, nl)
921 if q3 == NX_CSS_NUMBER_PARSE_ERROR { return 0 - 1 }
922 // px: DIMENSION tokens carry the unit in the SEPARATE unit fields (val = digits only);
923 // swept spans carry it inline. Unitless must be EXACTLY the number (else miss, not 16x).
924 if cd.unit_len == 2 {
925 if ((ctx.src[cd.unit_off] as i64) & 255) == 112 { if ((ctx.src[cd.unit_off + 1] as i64) & 255) == 120 {
926 return q3 / 1000
927 } }
928 }
929 if nl + 1 < cd.val_len {
930 if ((ctx.src[cd.val_off + nl] as i64) & 255) == 112 { if ((ctx.src[cd.val_off + nl + 1] as i64) & 255) == 120 {
931 return q3 / 1000
932 } }
933 }
934 if cd.unit_len == 0 { if nl == cd.val_len { return (q3 * fs) / 1000 } }
935 return 0 - 1
936 }
937 }
938 if cd.prop_len == 4 {
939 var same4: i64 = 1
940 var k4: i64 = 0
941 while k4 < 4 {
942 if ((ctx.src[cd.prop_off + k4] as i64) & 255) != ((fsh[k4] as i64) & 255) { same4 = 0; k4 = 4 } else { k4 = k4 + 1 }
943 }
944 if same4 == 1 {
945 let c0: i64 = (ctx.src[cd.val_off] as i64) & 255
946 if c0 >= 48 { if c0 <= 57 {
947 // skip the leading size token to the '/' (e.g. "15px/1.55 system-ui")
948 var j: i64 = 0
949 var sl: i64 = 0 - 1
950 var g: i64 = 1
951 while g == 1 {
952 if j >= cd.val_len { g = 0 }
953 else {
954 let cj: i64 = (ctx.src[cd.val_off + j] as i64) & 255
955 if cj == 47 { sl = j; g = 0 }
956 else { if cj == 32 { g = 0 } else { j = j + 1 } }
957 }
958 }
959 if sl >= 0 {
960 let vo: i64 = cd.val_off + sl + 1
961 let vmax: i64 = cd.val_len - sl - 1
962 var n2: i64 = 0
963 var g2: i64 = 1
964 while g2 == 1 {
965 if n2 >= vmax { g2 = 0 }
966 else {
967 let c2: i64 = (ctx.src[vo + n2] as i64) & 255
968 var isn2: i64 = 0
969 if c2 >= 48 { if c2 <= 57 { isn2 = 1 } }
970 if c2 == 46 { isn2 = 1 }
971 if isn2 == 1 { n2 = n2 + 1 } else { g2 = 0 }
972 }
973 }
974 if n2 > 0 {
975 let q32: i64 = nx_css_number_parse(ctx.src, vo, n2)
976 if q32 != NX_CSS_NUMBER_PARSE_ERROR {
977 if n2 + 1 < vmax {
978 if ((ctx.src[vo + n2] as i64) & 255) == 112 { if ((ctx.src[vo + n2 + 1] as i64) & 255) == 120 {
979 return q32 / 1000
980 } }
981 }
982 return (q32 * fs) / 1000
983 }
984 }
985 }
986 } }
987 }
988 }
989 }
990 i = i - 1
991 }
992 return 0 - 1
993}
994func _layout_lineheight_px(ctx: *LayoutCtx, box_idx: i64, fs: i64) -> i64 {
995 var cur: i64 = box_idx
996 var guard: i64 = 0
997 while cur >= 0 {
998 if guard > 64 { return 0 - 1 }
999 guard = guard + 1
1000 let v: i64 = _layout_lineheight_decl_px(ctx, cur, fs)
1001 if v >= 0 { return v }
1002 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (cur as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1003 cur = b.parent_idx
1004 }
1005 return 0 - 1
1006}
1007
1008// VEC-MEASURE mode (opt-in, default OFF so every existing gate is byte-unchanged): when the renderer will
1009// paint runs with the proportional VECTOR font (the GUI: vec_headings=2), layout must reserve widths from
1010// the SAME metric (font_adv_em) or every styled inline span/link lands at bitmap-width positions while
1011// glyphs paint narrower -> the measured gap bug (S21 F3; dominant Wikipedia paint delta). Same module-
1012// toggle pattern as the paint side's br_set_fast_vec.
1013static l_vec_measure: i64
1014// PINNING (2026-08-25). br_layout supplies a DEFAULT (measure with whatever the paint side will
1015// draw with). A caller that deliberately wants the other mode must WIN over that default, so the
1016// explicit setter PINS and the default setter YIELDS. Without this, br_layout's default silently
1017// overrode nx_inline_vec_measure_gate's T2, which sets bitmap mode BEFORE calling br_layout --
1018// i.e. the default would have measured the wrong mode and that tooth would test nothing.
1019// Two states cannot express "nobody has chosen yet"; the pin is the third state.
1020static l_vec_measure_pinned: i64
1021func nx_layout_set_vec_measure(v: i64) -> i64 { l_vec_measure = v; l_vec_measure_pinned = 1; return 0 }
1022// For DEFAULT-setters (br_layout). Yields to any explicit nx_layout_set_vec_measure call.
1023func nx_layout_default_vec_measure(v: i64) -> i64 { if l_vec_measure_pinned == 0 { l_vec_measure = v } return 0 }
1024// Release the pin so a later default applies again (a harness between independent subjects).
1025func nx_layout_clear_vec_measure_pin() -> i64 { l_vec_measure_pinned = 0; return 0 }
1026
1027// ---- IMAGE REFLOW (2026-08-26) ----
1028// An <img> has no children and no CSS height, so its content height resolved to 0 and the flow
1029// reserved NOTHING for it. The image pass then blitted the decoded bitmap at its natural size over
1030// whatever text had flowed into that space -- measured on en.wikipedia.org/wiki/Ogre, the painting
1031// covered five lines of body text. The render core's own comment called this out as the accepted
1032// cost of being additive ("images paint over the flow"), because intrinsic size is only known after
1033// fetch+decode, which happens AFTER layout.
1034// The fix is what every browser does: RE-FLOW once the sizes are known. These statics carry the
1035// decoded dimensions in, indexed by BOX INDEX, using the same setter shape l_vec_measure already
1036// uses in this file rather than widening LayoutCtx (which would touch every constructor).
1037// UNARMED IS THE DEFAULT AND IS BYTE-IDENTICAL: with l_img_n == 0 every lookup returns 0 and the
1038// height arithmetic below is unchanged, so pass 1 and every non-image consumer are untouched.
1039static l_img_w_p: i64
1040static l_img_h_p: i64
1041static l_img_n: i64
1042func nx_layout_set_img_dims(wp: *i64, hp: *i64, n: i64) -> i64 {
1043 l_img_w_p = wp as i64
1044 l_img_h_p = hp as i64
1045 l_img_n = n
1046 return 0
1047}
1048func nx_layout_clear_img_dims() -> i64 { l_img_w_p = 0; l_img_h_p = 0; l_img_n = 0; return 0 }
1049// Intrinsic height for box `idx`, or 0 when unarmed / out of range / not an image.
1050func _layout_img_h(idx: i64) -> i64 {
1051 if l_img_h_p == 0 { return 0 }
1052 if idx < 0 { return 0 }
1053 if idx >= l_img_n { return 0 }
1054 let hp: *i64 = l_img_h_p as *i64
1055 let v: i64 = hp[idx]
1056 if v < 0 { return 0 }
1057 return v
1058}
1059func _layout_img_w(idx: i64) -> i64 {
1060 if l_img_w_p == 0 { return 0 }
1061 if idx < 0 { return 0 }
1062 if idx >= l_img_n { return 0 }
1063 let wp: *i64 = l_img_w_p as *i64
1064 let v: i64 = wp[idx]
1065 if v < 0 { return 0 }
1066 return v
1067}
1068// Y-TRAP (debug instrument, seq linkedin-2^62): when armed, any box whose computed y exceeds 10^9
1069// prints its idx + the exact operands so the corrupt ADDEND is named in one run. Never on by default.
1070static l_ytrap: i64
1071func nx_layout_set_ytrap(v: i64) -> i64 { l_ytrap = v; return 0 }
1072func _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 }
1073func _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 }
1074
1075// ---- TABLE COLUMN LAYOUT (CSS 2.1 §17.5.2 "auto", minimal) -- seq1158, 2026-07-28 --------------
1076// Rows are flex containers (UA sheet `tr{display:flex}`) and the flex path gave every auto child an
1077// EQUAL share, so a 3-column table rendered as equal thirds regardless of content. On a table-built
1078// site (news.ycombinator.com: rank | vote | title) that scatters the cells -- measured 57permille with
1079// author CSS on while the page HEIGHT proved the content was all there. Real table layout sizes a
1080// column by its MAX-CONTENT across ALL rows and shares that width down the column.
1081// The widths live in statics because the row (flex) path is reached through the generic block
1082// recursion; they are SAVED/RESTORED around each table so nested tables (HN nests them) are correct.
1083static l_tbl_active: i64 // 1 while laying out inside a table whose columns are resolved
1084static l_tbl_idx: i64 // box index of that table (a row only uses columns of ITS OWN table)
1085static l_tbl_ncols: i64
1086static l_tbl_colw_p: i64 // *i64 column widths (as i64; cast at use)
1087const NX_TBL_MAXCOLS: i64 = 64
1088
1089// vector width of text[start..end) at font-size fs (1x layout units) -- the EXACT sum the vector paint
1090// advances by (font_vec_adv_px2 = font_adv_em*empx/1000 with empx = fs*paint_scale; layout is 1x).
1091// DELEGATES to the shared absolute-px metric in nx_font.nx (2026-08-25). This used to be a
1092// private second copy of that arithmetic, and the wrap scan had no vector arm at all, so a run
1093// whose width was reserved here was then BROKEN by the 8x8 bitmap break function. Width and
1094// break are now literally the same expression reached from one definition, so they cannot drift.
1095func _layout_vec_text_w(t: *u8, start: i64, end: i64, fs: i64) -> i64 {
1096 return font_vec_text_w_fs(t, start, end, fs)
1097}
1098
1099// Byte-equal a property name at src[off..off+n) against `name`. 1 if equal, 0 otherwise.
1100func _il_prop_is(src: *u8, off: i64, name: *u8, n: i64) -> i64 {
1101 var k: i64 = 0
1102 while k < n { if (src[off+k]&0xff) != (name[k]&0xff) { return 0 } k = k + 1 }
1103 return 1
1104}
1105
1106// Border width (px) for a box: leading integer of the `border` shorthand ("1px solid #26304a" -> 1) or
1107// `border-width`. Reverse cascade scan of ctx.computed (winner first). 0 if none. Used to size + inset an
1108// atomic inline-block so its border-box matches what the painter strokes (br_comp_border).
1109func _layout_border_w(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1110 var i: i64 = ctx.n_computed - 1
1111 while i >= 0 {
1112 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1113 if cd.element_idx == box_idx {
1114 var hit: i64 = 0
1115 if cd.prop_len == 6 { if _il_prop_is(ctx.src, cd.prop_off, "border\x00" as *u8, 6) == 1 { hit = 1 } }
1116 if cd.prop_len == 12 { if _il_prop_is(ctx.src, cd.prop_off, "border-width\x00" as *u8, 12) == 1 { hit = 1 } }
1117 if hit == 1 {
1118 let v: i64 = _grid_int_val(ctx.src, cd.val_off, cd.val_len)
1119 if v < 0 { return 0 }
1120 return v
1121 }
1122 }
1123 i = i - 1
1124 }
1125 return 0
1126}
1127
1128// Max-content width (px) of an inline subtree: the width its TEXT occupies on ONE unwrapped line, using
1129// the SAME font metrics as the TEXT layout branch (measure==paint). Sums children (an inline run). Used to
1130// shrink-to-fit an atomic inline-block that has no explicit `width` (chips/tags/badges).
1131func _layout_intrinsic_w(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1132 if box_idx < 0 { return 0 }
1133 if box_idx >= ctx.tree.count { return 0 }
1134 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1135 if b.kind == NX_LAYOUT_BOX_TEXT {
1136 let fs: i64 = _layout_fontsize_inherited(ctx, b.parent_idx)
1137 let sc: i64 = _layout_scale_for_px(fs)
1138 let tptr: *u8 = ((ctx.src as i64) + b.text_off) as *u8
1139 if ctx.text_measured == 1 {
1140 if ctx.font_tab == 0 { ctx.font_tab = (font8x8_table()) as i64 }
1141 return font8x8_text_w(ctx.font_tab as *u8, tptr, 0, b.text_len) * sc
1142 }
1143 if l_vec_measure == 1 { font_face_select(_layout_face_inherited(ctx, b.parent_idx)) } // BR27: the run's face (weight/style/family) BEFORE its width is asked
1144 if l_vec_measure == 1 { if font_run_vec_ok(tptr, b.text_len) == 1 { return _layout_vec_text_w(tptr, 0, b.text_len, fs) } } // font_run_vec_ok = the SAME run test paint uses (face-aware since 2026-09-02)
1145 return b.text_len * NX_INLINE_GLYPH_W * sc
1146 }
1147 var w: i64 = 0
1148 var ci: i64 = b.first_child_idx
1149 var sf: i64 = 0
1150 var keep: i64 = 1
1151 while keep == 1 {
1152 if sf >= NX_MAGIC_65536 { keep = 0 }
1153 else {
1154 sf = sf + 1
1155 if ci < 0 { keep = 0 }
1156 else {
1157 w = w + _layout_intrinsic_w(ctx, ci)
1158 let c: *LayoutBox = (ctx.tree.boxes as *u8 + (ci as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1159 ci = c.next_sibling_idx
1160 }
1161 }
1162 }
1163 return w
1164}
1165
1166func _layout_inline_flow(ctx: *LayoutCtx, pen: *InlinePen, box_idx: i64) -> i64 {
1167 if box_idx < 0 { return 0 }
1168 if box_idx >= ctx.tree.count { return 0 }
1169 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1170
1171 // display:none on an INLINE-LEVEL element (span/a in flow): contributes nothing, flows no children,
1172 // pen unmoved -- the same rule _layout_block_recurse applies to block boxes (seq1130 companion).
1173 // TEXT boxes carry no computed decls, so the O(ncomp) scan is skipped for them.
1174 if b.kind != NX_LAYOUT_BOX_TEXT {
1175 if _layout_display_none(ctx, box_idx) == 1 { return 0 }
1176 }
1177
1178 if b.kind == NX_LAYOUT_BOX_TEXT {
1179 // Scale glyph advance + line height by the run's inherited font-size so big headings reserve
1180 // proportional space (paint scales the glyph identically -> no overlap with the next line).
1181 let fs_run: i64 = _layout_fontsize_inherited(ctx, b.parent_idx)
1182 let sc: i64 = _layout_scale_for_px(fs_run)
1183 let gw: i64 = NX_INLINE_GLYPH_W * sc
1184 var lh: i64 = NX_INLINE_LINE_H * sc
1185 // CSS line-height (S21 height axis): a declared line-height/font-shorthand-/N sets the EXACT
1186 // line-box height (Chrome parity), floored at 8px against pathology. Absent -> legacy 17*scale.
1187 let lhq: i64 = _layout_lineheight_px(ctx, b.parent_idx, fs_run)
1188 if lhq >= 8 { lh = lhq }
1189 let tptr: *u8 = ((ctx.src as i64) + b.text_off) as *u8
1190 // MEASURED mode: width = the SUM of per-glyph proportional advances (font8x8 metrics SSOT) so the
1191 // reserved box exactly equals what the paint draws. Legacy mode keeps the 9px-cell arithmetic.
1192 var meas: i64 = 0
1193 var ftab: *u8 = 0 as *u8
1194 if ctx.text_measured == 1 {
1195 meas = 1
1196 if ctx.font_tab == 0 { ctx.font_tab = (font8x8_table()) as i64 }
1197 ftab = ctx.font_tab as *u8
1198 }
1199 var full_w: i64 = b.text_len * gw
1200 if meas == 1 { full_w = font8x8_text_w(ftab, tptr, 0, b.text_len) * sc }
1201 // vec-measure: reserve the width the VECTOR paint will actually advance (same coverage test the
1202 // GUI paint uses: font_run_vec_ok -- the extended stroke set, or with a REAL FACE loaded (2026-09-02)
1203 // every byte that face covers). Uncovered runs keep bitmap widths -- exactly mirroring the
1204 // paint-side fallback, so measure==paint in BOTH modes.
1205 var vec_run: i64 = 0
1206 if l_vec_measure == 1 { font_face_select(_layout_face_inherited(ctx, b.parent_idx)) } // BR27: select the run's face first
1207 if l_vec_measure == 1 { if font_run_vec_ok(tptr, b.text_len) == 1 { vec_run = 1 } }
1208 if vec_run == 1 { full_w = _layout_vec_text_w(tptr, 0, b.text_len, fs_run) }
1209 if full_w <= pen.avail_w {
1210 // SINGLE-LINE run: wrap to a new line first if it won't fit in the remaining space (and we're
1211 // not already at line start), then place it and advance the pen horizontally.
1212 var cw: i64 = full_w
1213 if cw < 0 { cw = 0 }
1214 if pen.pen_x > 0 { if pen.pen_x + cw > pen.avail_w {
1215 pen.pen_y = pen.pen_y + pen.line_h
1216 pen.pen_x = 0
1217 pen.line_h = 0
1218 } }
1219 b.x = pen.origin_x + pen.pen_x
1220 b.y = pen.origin_y + pen.pen_y
1221 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
1222 _yt_w("YTRAP@1007 idx=" as *u8); _yt_n(box_idx)
1223 _yt_w(" oy=" as *u8); _yt_n(pen.origin_y)
1224 _yt_w("
1225" as *u8)
1226 } }
1227 b.w = cw
1228 b.h = lh
1229 pen.pen_x = pen.pen_x + cw
1230 if pen.line_h < lh { pen.line_h = lh }
1231 } else {
1232 // MULTI-LINE run: word-wraps. Reserve the FULL height and advance the pen DOWN to the last
1233 // line. Measured mode breaks by SUMMED ADVANCES (font8x8_next_break); legacy by char count
1234 // (tw_next_break) -- each mode's paint mirrors its break fn exactly.
1235 if pen.pen_x > 0 {
1236 pen.pen_y = pen.pen_y + pen.line_h
1237 pen.pen_x = 0
1238 pen.line_h = 0
1239 }
1240 var cpl: i64 = pen.avail_w / gw
1241 if cpl < 1 { cpl = 1 }
1242 var avail_units: i64 = pen.avail_w / sc
1243 if avail_units < 4 { avail_units = 4 }
1244 var s: i64 = 0
1245 var nlines: i64 = 0
1246 var last_start: i64 = 0
1247 var last_end: i64 = 0
1248 while s < b.text_len {
1249 last_start = s
1250 var e: i64 = 0
1251 // VECTOR arm (2026-08-25). A run whose width was reserved by _layout_vec_text_w
1252 // MUST also break on vector advances. Without this arm the branch had only the
1253 // measured (8x8 bitmap) and legacy (char-count) arms, so every wrapped vector run
1254 // broke at bitmap positions and painted at vector ones -- measure and paint
1255 // disagreeing inside one function. Same guard as the width side (vec_run), same
1256 // metric (font_vec_*_fs), same font size (fs_run), and it breaks against the real
1257 // pixel budget pen.avail_w rather than the bitmap-derived avail_units.
1258 if vec_run == 1 { e = font_vec_next_break_fs(tptr, b.text_len, pen.avail_w, s, fs_run) }
1259 else { if meas == 1 { e = font8x8_next_break(ftab, tptr, b.text_len, avail_units, s) }
1260 else { e = tw_next_break(tptr, b.text_len, cpl, s) } }
1261 last_end = e
1262 nlines = nlines + 1
1263 s = e
1264 }
1265 if nlines < 1 { nlines = 1 }
1266 b.x = pen.origin_x
1267 b.y = pen.origin_y + pen.pen_y
1268 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
1269 _yt_w("YTRAP@1040 idx=" as *u8); _yt_n(box_idx)
1270 _yt_w(" oy=" as *u8); _yt_n(pen.origin_y)
1271 _yt_w("
1272" as *u8)
1273 } }
1274 b.w = pen.avail_w
1275 b.h = nlines * lh
1276 pen.pen_y = pen.pen_y + (nlines - 1) * lh
1277 if meas == 1 { pen.pen_x = font8x8_text_w(ftab, tptr, last_start, last_end) * sc }
1278 else { pen.pen_x = (last_end - last_start) * gw }
1279 pen.line_h = lh
1280 }
1281 return 0
1282 }
1283
1284 // INLINE-BLOCK: an ATOMIC inline-level box that sizes like a block internally (padding/border/width),
1285 // sits on the line, and WRAPS as a unit. Children lay out in a PRIVATE sub-pen at the inner (content)
1286 // origin so bg/border paint around them. Covers chips/tags/badges/buttons/pills + <img>/<input>/<button>.
1287 if b.kind == NX_LAYOUT_BOX_INLINE_BLOCK {
1288 let tbl: *LayoutPropTable = (pen.table) as *LayoutPropTable
1289 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)
1290 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)
1291 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)
1292 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)
1293 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)
1294 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)
1295 if plp < 0 { plp = 0 }
1296 if prp < 0 { prp = 0 }
1297 if ptp < 0 { ptp = 0 }
1298 if pbp < 0 { pbp = 0 }
1299 if mll < 0 { mll = 0 }
1300 if mrr < 0 { mrr = 0 }
1301 let bdw: i64 = _layout_border_w(ctx, box_idx)
1302 let wexp: i64 = _layout_lookup_px(ctx, tbl.src, box_idx, tbl.width_off, tbl.width_len)
1303 let hexp: i64 = _layout_lookup_height_px(ctx, tbl.src, box_idx, tbl.height_off, tbl.height_len)
1304 // IMAGE REFLOW (2026-08-26) -- THIS is where an <img> was mis-sized, and it is an INLINE_BLOCK
1305 // (see this branch's own comment: "Covers ... + <img>/<input>/<button>"), which is why the
1306 // block-path reservation alone did nothing. With no CSS height the fallback below gave a
1307 // replaced element ONE LINE HEIGHT: measured on en.wikipedia.org/wiki/Ogre the box came out
1308 // ~17px tall while the decoded bitmap is ~220px, so the blit overflowed its own box by ~200px
1309 // and covered five lines of body text. The intrinsic size is known by then -- load_images
1310 // writes page.bimg_w/bimg_h per BOX INDEX -- it simply was never consulted here.
1311 // CSS still wins: wexp/hexp are checked FIRST and this only fills the otherwise-guessed case.
1312 // Unarmed (pass 1, or any consumer that never calls nx_layout_set_img_dims) both lookups
1313 // return 0 and the original fallbacks run unchanged, so this is byte-identical by default.
1314 var inw: i64 = wexp
1315 if inw < 0 {
1316 let iiw: i64 = _layout_img_w(box_idx)
1317 if iiw > 0 { inw = iiw } else { inw = _layout_intrinsic_w(ctx, box_idx) }
1318 }
1319 if inw < 0 { inw = 0 }
1320 let fs_ib: i64 = _layout_fontsize_inherited(ctx, box_idx)
1321 let sc_ib: i64 = _layout_scale_for_px(fs_ib)
1322 var inh: i64 = hexp
1323 if inh < 0 {
1324 let iih: i64 = _layout_img_h(box_idx)
1325 if iih > 0 { inh = iih } else { inh = NX_INLINE_LINE_H * sc_ib }
1326 }
1327 let full_w: i64 = inw + plp + prp + bdw + bdw
1328 let full_h: i64 = inh + ptp + pbp + bdw + bdw
1329 // wrap the WHOLE box to the next line if it will not fit and we are not already at line start
1330 if pen.pen_x > 0 { if pen.pen_x + mll + full_w > pen.avail_w {
1331 pen.pen_y = pen.pen_y + pen.line_h
1332 pen.pen_x = 0
1333 pen.line_h = 0
1334 } }
1335 let bx: i64 = pen.origin_x + pen.pen_x + mll
1336 let by: i64 = pen.origin_y + pen.pen_y
1337 b.x = bx
1338 b.y = by
1339 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
1340 _yt_w("YTRAP@iblk idx=" as *u8); _yt_n(box_idx)
1341 _yt_w(" by=" as *u8); _yt_n(by)
1342 _yt_w("
1343" as *u8)
1344 } }
1345 b.w = full_w
1346 b.h = full_h
1347 // lay out children in a PRIVATE sub-pen at the inner content origin (so the bg/border box wraps them)
1348 let ibp: *InlinePen = (sys_mmap(NX_INLINE_PEN_BYTES as nx_size)) as *InlinePen
1349 ibp.pen_x = 0
1350 ibp.pen_y = 0
1351 ibp.line_h = 0
1352 ibp.avail_w = inw
1353 ibp.origin_x = bx + plp + bdw
1354 ibp.origin_y = by + ptp + bdw
1355 ibp.table = pen.table
1356 var cib: i64 = b.first_child_idx
1357 var sfib: i64 = 0
1358 var kib: i64 = 1
1359 while kib == 1 {
1360 if sfib >= NX_MAGIC_65536 { kib = 0 }
1361 else {
1362 sfib = sfib + 1
1363 if cib < 0 { kib = 0 }
1364 else {
1365 _layout_inline_flow(ctx, ibp, cib)
1366 let cb: *LayoutBox = (ctx.tree.boxes as *u8 + (cib as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1367 cib = cb.next_sibling_idx
1368 }
1369 }
1370 }
1371 // advance the parent pen past the atomic box + its horizontal margins; grow the line height
1372 pen.pen_x = pen.pen_x + mll + full_w + mrr
1373 if pen.line_h < full_h { pen.line_h = full_h }
1374 return 0
1375 }
1376
1377 // INLINE / other: transparent wrapper -- record start, flow children on the SHARED line, span to pen end.
1378 b.x = pen.origin_x + pen.pen_x
1379 b.y = pen.origin_y + pen.pen_y
1380 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
1381 _yt_w("YTRAP inline idx=" as *u8); _yt_n(box_idx)
1382 _yt_w(" origin_y=" as *u8); _yt_n(pen.origin_y)
1383 _yt_w(" pen_y=" as *u8); _yt_n(pen.pen_y)
1384 _yt_w(" line_h=" as *u8); _yt_n(pen.line_h)
1385 _yt_w("\n" as *u8)
1386 } }
1387 var child_idx: i64 = b.first_child_idx
1388 var safety: i64 = 0
1389 var keep: i64 = 1
1390 while keep == 1 {
1391 if safety >= NX_MAGIC_65536 { keep = 0 }
1392 else {
1393 safety = safety + 1
1394 if child_idx < 0 { keep = 0 }
1395 else {
1396 _layout_inline_flow(ctx, pen, child_idx)
1397 let c: *LayoutBox = (ctx.tree.boxes as *u8 + (child_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1398 child_idx = c.next_sibling_idx
1399 }
1400 }
1401 }
1402 var span_w: i64 = (pen.origin_x + pen.pen_x) - b.x
1403 if span_w < 0 { span_w = 0 }
1404 b.w = span_w
1405 b.h = pen.line_h
1406 return 0
1407}
1408
1409// Recursive layout pass over a single block subtree rooted at
1410// box_idx. `avail_w` is the content width passed down by the
1411// parent. (`origin_x`, `origin_y`) is the top-left of the content
1412// area at this box's level. Returns the OUTER height of this box
1413// (margin-top + border-top + padding-top + content_h +
1414// padding-bottom + border-bottom + margin-bottom) so the parent's
1415// sibling-stacking loop can advance.
1416//
1417// Phase 3 subset: no borders, no horizontal margin/padding, so the
1418// outer height = margin-top + padding-top + content_h +
1419// padding-bottom + margin-bottom.
1420//
1421// Does the box have computed `display: flex`? (Vector-2022 and modern pages arrange sidebar+content
1422// this way.) Same lookup shape as _layout_display_none but value "flex".
1423func _layout_display_flex(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1424 let dn: *u8 = "display\x00" as *u8
1425 let fx: *u8 = "flex\x00" as *u8
1426 var i: i64 = ctx.n_computed - 1
1427 while i >= 0 {
1428 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1429 if cd.element_idx == box_idx {
1430 if cd.prop_len == 7 {
1431 var sp: i64 = 1
1432 var k: i64 = 0
1433 while k < 7 { if (ctx.src[cd.prop_off+k]&0xff) != (dn[k]&0xff) { sp = 0; k = 7 } else { k = k + 1 } }
1434 if sp == 1 {
1435 if cd.val_len == 4 {
1436 var sv: i64 = 1
1437 var j: i64 = 0
1438 while j < 4 { if (ctx.src[cd.val_off+j]&0xff) != (fx[j]&0xff) { sv = 0; j = 4 } else { j = j + 1 } }
1439 if sv == 1 { return 1 }
1440 }
1441 }
1442 }
1443 }
1444 i = i - 1
1445 }
1446 return 0
1447}
1448
1449// Does the box have computed `display: grid`? (Same lookup shape as _layout_display_flex, value "grid".)
1450// Is this box's computed margin AUTO on the given side (3=left, 1=right)? Longhand margin-left/right
1451// value "auto", or the `margin` shorthand's side token per the CSS 1/2/3/4-value mapping. Cascade
1452// winner = first hit in reverse. Powers §10.3.3 auto-margin centering (wiki `margin:0 auto`).
1453func _lb_margin_auto(ctx: *LayoutCtx, box_idx: i64, side: i64) -> i64 {
1454 let ml_lit: *u8 = "margin-left\x00" as *u8
1455 let mr_lit: *u8 = "margin-right\x00" as *u8
1456 let m_lit: *u8 = "margin\x00" as *u8
1457 var i: i64 = ctx.n_computed - 1
1458 while i >= 0 {
1459 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1460 if cd.element_idx == box_idx {
1461 var want: i64 = 0
1462 var lit: *u8 = m_lit
1463 var ll: i64 = 6
1464 if side == 3 { if cd.prop_len == 11 { lit = ml_lit; ll = 11; want = 1 } }
1465 if side == 1 { if cd.prop_len == 12 { lit = mr_lit; ll = 12; want = 1 } }
1466 if cd.prop_len == 6 { want = 2 }
1467 if want > 0 {
1468 var sp: i64 = 1
1469 var k: i64 = 0
1470 while k < ll { if (ctx.src[cd.prop_off+k]&0xff) != (lit[k]&0xff) { sp = 0; k = ll } else { k = k + 1 } }
1471 if sp == 1 {
1472 if want == 1 {
1473 // longhand: value == "auto"?
1474 if cd.val_len == 4 { if (ctx.src[cd.val_off]&0xff)==97 { if (ctx.src[cd.val_off+1]&0xff)==117 { return 1 } } }
1475 return 0
1476 }
1477 // shorthand: split into tokens, map side (1:all 2:[v h] 3:[t h b] 4:[t r b l])
1478 let to: *i64 = sys_mmap(8*4) as *i64
1479 let tl: *i64 = sys_mmap(8*4) as *i64
1480 var nt: i64 = 0
1481 var p: i64 = cd.val_off
1482 let e: i64 = cd.val_off + cd.val_len
1483 while p < e {
1484 while p < e { if (ctx.src[p]&0xff) == 32 { p = p + 1 } else { p = e + 1000 } }
1485 if p > e { p = p - 1000 }
1486 if p < e {
1487 let s0: i64 = p
1488 while p < e { if (ctx.src[p]&0xff) != 32 { p = p + 1 } else { p = e + 1000 } }
1489 if p > e { p = p - 1000 }
1490 if nt < 4 { to[nt] = s0; tl[nt] = p - s0; nt = nt + 1 }
1491 }
1492 }
1493 if nt == 0 { return 0 }
1494 var ti2: i64 = 0
1495 if nt == 1 { ti2 = 0 }
1496 if nt == 2 { ti2 = 1 }
1497 if nt == 3 { ti2 = 1 }
1498 if nt == 4 { if side == 1 { ti2 = 1 } else { ti2 = 3 } }
1499 if tl[ti2] == 4 { if (ctx.src[to[ti2]]&0xff)==97 { if (ctx.src[to[ti2]+1]&0xff)==117 { return 1 } } }
1500 return 0
1501 }
1502 }
1503 }
1504 i = i - 1
1505 }
1506 return 0
1507}
1508// Does this box use NAMED GRID AREAS (`grid-template-areas`)? We place grid children by AUTO-FLOW
1509// order only, so a page that assigns items to named areas gets them in the WRONG tracks — wikipedia
1510// ≥1120px put its main content into the 12.25rem SIDEBAR column and the page exploded to 88,202px
1511// (measured 2026-07-29; the track math itself was correct). Until named placement exists, a grid we
1512// cannot place correctly DEGRADES TO NORMAL BLOCK FLOW — full-width stacking is always readable,
1513// mis-placement never is. Same principle as flex-direction:column → block.
1514func _layout_has_grid_areas(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1515 let ga: *u8 = "grid-template-areas\x00" as *u8
1516 var i: i64 = ctx.n_computed - 1
1517 while i >= 0 {
1518 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1519 if cd.element_idx == box_idx {
1520 if cd.prop_len == 19 {
1521 var sp: i64 = 1
1522 var k: i64 = 0
1523 while k < 19 { if (ctx.src[cd.prop_off+k]&0xff) != (ga[k]&0xff) { sp = 0; k = 19 } else { k = k + 1 } }
1524 if sp == 1 { return 1 }
1525 }
1526 }
1527 i = i - 1
1528 }
1529 return 0
1530}
1531// flex-direction: column(-reverse)? Our flex branch is ROW-only; a COLUMN container must NOT take it.
1532// A column flex stacks children vertically at full width = exactly the normal block flow this engine
1533// already does, so the correct minimal treatment is "not row-flex" (align-items offsets = later rung).
1534// Found via linkedin (2026-07-28): its hero is flex-direction:column; the row path laid the children
1535// SIDEWAYS and starved the H1 to w=0 x h=1482 (the "Welcome to your professional community" column).
1536func _layout_flex_dir_col(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1537 let fn2: *u8 = "flex-direction\x00" as *u8
1538 var i: i64 = ctx.n_computed - 1
1539 while i >= 0 {
1540 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1541 if cd.element_idx == box_idx {
1542 if cd.prop_len == 14 {
1543 var sp: i64 = 1
1544 var k: i64 = 0
1545 while k < 14 { if (ctx.src[cd.prop_off+k]&0xff) != (fn2[k]&0xff) { sp = 0; k = 14 } else { k = k + 1 } }
1546 if sp == 1 {
1547 if cd.val_len >= 6 { if (ctx.src[cd.val_off]&0xff) == 99 { return 1 } } // c(olumn)
1548 return 0
1549 }
1550 }
1551 }
1552 i = i - 1
1553 }
1554 return 0
1555}
1556func _layout_display_grid(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1557 let dn: *u8 = "display\x00" as *u8
1558 let gx: *u8 = "grid\x00" as *u8
1559 var i: i64 = ctx.n_computed - 1
1560 while i >= 0 {
1561 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1562 if cd.element_idx == box_idx {
1563 if cd.prop_len == 7 {
1564 var sp: i64 = 1
1565 var k: i64 = 0
1566 while k < 7 { if (ctx.src[cd.prop_off+k]&0xff) != (dn[k]&0xff) { sp = 0; k = 7 } else { k = k + 1 } }
1567 if sp == 1 {
1568 if cd.val_len == 4 {
1569 var sv: i64 = 1
1570 var j: i64 = 0
1571 while j < 4 { if (ctx.src[cd.val_off+j]&0xff) != (gx[j]&0xff) { sv = 0; j = 4 } else { j = j + 1 } }
1572 if sv == 1 { return 1 }
1573 }
1574 }
1575 }
1576 }
1577 i = i - 1
1578 }
1579 return 0
1580}
1581// `display:table` (the UA sheet marks every <table> with it) -- same lookup shape as flex/grid.
1582// This is how the layout knows a box is a table without carrying tag names in the box tree.
1583func _layout_display_table(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1584 let dn: *u8 = "display\x00" as *u8
1585 let tx: *u8 = "table\x00" as *u8
1586 var i: i64 = ctx.n_computed - 1
1587 while i >= 0 {
1588 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1589 if cd.element_idx == box_idx {
1590 if cd.prop_len == 7 {
1591 var sp: i64 = 1
1592 var k: i64 = 0
1593 while k < 7 { if (ctx.src[cd.prop_off+k]&0xff) != (dn[k]&0xff) { sp = 0; k = 7 } else { k = k + 1 } }
1594 if sp == 1 {
1595 if cd.val_len == 5 {
1596 var sv: i64 = 1
1597 var j: i64 = 0
1598 while j < 5 { if (ctx.src[cd.val_off+j]&0xff) != (tx[j]&0xff) { sv = 0; j = 5 } else { j = j + 1 } }
1599 if sv == 1 { return 1 }
1600 }
1601 }
1602 }
1603 }
1604 i = i - 1
1605 }
1606 return 0
1607}
1608func _lb_box(ctx: *LayoutCtx, i: i64) -> *LayoutBox {
1609 return (ctx.tree.boxes as *u8 + (i as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
1610}
1611func _layout_nearest_table(ctx: *LayoutCtx, idx: i64) -> i64 {
1612 var cur: i64 = idx
1613 var guard: i64 = 0
1614 while guard < 64 {
1615 guard = guard + 1
1616 if cur < 0 { return 0 - 1 }
1617 if _layout_display_table(ctx, cur) == 1 { return cur }
1618 let b: *LayoutBox = _lb_box(ctx, cur)
1619 cur = b.parent_idx
1620 }
1621 return 0 - 1
1622}
1623// Resolve column widths for the table at tbl_idx into colw[0..ncols); returns ncols.
1624// A ROW is any descendant with display:flex whose nearest table ancestor is tbl_idx (skips the rows
1625// of nested tables by construction). Column width = max over rows of the cell's max-content width
1626// (_layout_intrinsic_w -- the same metric the inline path measures with) + the UA cell padding.
1627// Then: over-wide -> scale to fit; under-wide -> distribute the slack proportionally (real tables are
1628// width:100%, and this preserves the content ratios that make a table readable).
1629func _layout_table_cols(ctx: *LayoutCtx, tbl_idx: i64, avail_w: i64, colw: *i64) -> i64 {
1630 var k: i64 = 0
1631 while k < NX_TBL_MAXCOLS { colw[k] = 0; k = k + 1 }
1632 var ncols: i64 = 0
1633 // document order => a subtree is a contiguous index range; stop at the first non-descendant.
1634 var i: i64 = tbl_idx + 1
1635 var go: i64 = 1
1636 while go == 1 {
1637 if i >= ctx.tree.count { go = 0 }
1638 else {
1639 // descendant test (bounded parent walk)
1640 var cur: i64 = i
1641 var isdesc: i64 = 0
1642 var g2: i64 = 0
1643 while g2 < 64 {
1644 g2 = g2 + 1
1645 if cur < 0 { g2 = 64 }
1646 else {
1647 if cur == tbl_idx { isdesc = 1; g2 = 64 }
1648 else { let pb: *LayoutBox = _lb_box(ctx, cur); cur = pb.parent_idx }
1649 }
1650 }
1651 if isdesc == 0 { go = 0 }
1652 else {
1653 if _layout_display_flex(ctx, i) == 1 {
1654 if _layout_nearest_table(ctx, i) == tbl_idx {
1655 let rb: *LayoutBox = _lb_box(ctx, i)
1656 var c: i64 = rb.first_child_idx
1657 var col: i64 = 0
1658 var g3: i64 = 0
1659 while c >= 0 {
1660 if g3 >= NX_MAGIC_4096 { c = 0 - 1 }
1661 else {
1662 g3 = g3 + 1
1663 let cb: *LayoutBox = _lb_box(ctx, c)
1664 // colspan (seq1159): from_dom stashes N>=2 in source_node_idx; a spanning
1665 // cell occupies N slots and its max-content contributes ACROSS the span
1666 // (iw/N per column), never to one column (CSS 2.1 s17.5.2).
1667 var cs: i64 = 1
1668 if cb.source_node_idx >= 2 { cs = cb.source_node_idx }
1669 if col < NX_TBL_MAXCOLS {
1670 var iw: i64 = _layout_intrinsic_w(ctx, c) + 8 // + UA td padding
1671 if iw > avail_w { iw = avail_w }
1672 var share: i64 = iw / cs
1673 var sk: i64 = 0
1674 while sk < cs {
1675 let sc2: i64 = col + sk
1676 if sc2 < NX_TBL_MAXCOLS {
1677 if share > colw[sc2] { colw[sc2] = share }
1678 if sc2 + 1 > ncols { ncols = sc2 + 1 }
1679 }
1680 sk = sk + 1
1681 }
1682 }
1683 col = col + cs
1684 c = cb.next_sibling_idx
1685 }
1686 }
1687 }
1688 }
1689 i = i + 1
1690 }
1691 }
1692 }
1693 if ncols <= 0 { return 0 }
1694 var sum: i64 = 0
1695 var j: i64 = 0
1696 while j < ncols { sum = sum + colw[j]; j = j + 1 }
1697 if sum <= 0 { return 0 }
1698 if sum > avail_w {
1699 j = 0
1700 while j < ncols { colw[j] = (colw[j] * avail_w) / sum; j = j + 1 }
1701 } else {
1702 let slack: i64 = avail_w - sum
1703 j = 0
1704 while j < ncols { colw[j] = colw[j] + (colw[j] * slack) / sum; j = j + 1 }
1705 }
1706 return ncols
1707}
1708// Parse the leading integer of src[voff..voff+vlen) (e.g. "16" of "16px"); -1 if none. For px values.
1709func _grid_int_val(src: *u8, voff: i64, vlen: i64) -> i64 {
1710 let end: i64 = voff + vlen
1711 var v: i64 = 0
1712 var any: i64 = 0
1713 var i: i64 = voff
1714 var rd: i64 = 1
1715 while rd == 1 {
1716 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 } }
1717 else { rd = 0 }
1718 }
1719 if any == 0 { return 0 - 1 }
1720 return v
1721}
1722// Find a property by EXACT name on box_idx (reverse cascade = winner first) and return its leading
1723// integer value (px), or -1 if absent. Used for gap/column-gap/row-gap.
1724func _grid_prop_px(ctx: *LayoutCtx, box_idx: i64, name: *u8, nlen: i64) -> i64 {
1725 var i: i64 = ctx.n_computed - 1
1726 while i >= 0 {
1727 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1728 if cd.element_idx == box_idx {
1729 if cd.prop_len == nlen {
1730 var sp: i64 = 1
1731 var k: i64 = 0
1732 while k < nlen { if (ctx.src[cd.prop_off+k]&0xff) != (name[k]&0xff) { sp = 0; k = nlen } else { k = k + 1 } }
1733 if sp == 1 { return _grid_int_val(ctx.src, cd.val_off, cd.val_len) }
1734 }
1735 }
1736 i = i - 1
1737 }
1738 return 0 - 1
1739}
1740// Resolve the column gap (want_row=0) or row gap (want_row=1) for a grid box: column-gap/row-gap wins,
1741// else the `gap` shorthand, else 0.
1742func _grid_gap(ctx: *LayoutCtx, box_idx: i64, want_row: i64) -> i64 {
1743 if want_row == 1 {
1744 let rg: i64 = _grid_prop_px(ctx, box_idx, "row-gap\x00" as *u8, 7)
1745 if rg >= 0 { return rg }
1746 } else {
1747 let cg: i64 = _grid_prop_px(ctx, box_idx, "column-gap\x00" as *u8, 10)
1748 if cg >= 0 { return cg }
1749 }
1750 let g: i64 = _grid_prop_px(ctx, box_idx, "gap\x00" as *u8, 3)
1751 if g >= 0 { return g }
1752 return 0
1753}
1754// Get the grid-template-columns VALUE span for box_idx into ooff/olen; 1 if present, 0 if not.
1755func _layout_grid_template(ctx: *LayoutCtx, box_idx: i64, ooff: *i64, olen: *i64) -> i64 {
1756 let gp: *u8 = "grid-template-columns\x00" as *u8
1757 var i: i64 = ctx.n_computed - 1
1758 while i >= 0 {
1759 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1760 if cd.element_idx == box_idx {
1761 if cd.prop_len == 21 {
1762 var sp: i64 = 1
1763 var k: i64 = 0
1764 while k < 21 { if (ctx.src[cd.prop_off+k]&0xff) != (gp[k]&0xff) { sp = 0; k = 21 } else { k = k + 1 } }
1765 if sp == 1 { ooff[0] = cd.val_off; olen[0] = cd.val_len; return 1 }
1766 }
1767 // `grid-template` SHORTHAND (rows / columns): the COLUMNS live after the '/'. Wikipedia's
1768 // vector-2022 declares its sidebar grid ONLY this way (`grid-template:min-content 1fr
1769 // min-content / 12.25rem minmax(0,1fr)`) -- without this the grid never activated and the
1770 // whole page rendered flush-left (the styled dx~150 regime, 2026-07-28).
1771 if cd.prop_len == 13 {
1772 let gs: *u8 = "grid-template\x00" as *u8
1773 var sp2: i64 = 1
1774 var k2: i64 = 0
1775 while k2 < 13 { if (ctx.src[cd.prop_off+k2]&0xff) != (gs[k2]&0xff) { sp2 = 0; k2 = 13 } else { k2 = k2 + 1 } }
1776 if sp2 == 1 {
1777 var sl: i64 = 0 - 1
1778 var q: i64 = cd.val_off
1779 let qe: i64 = cd.val_off + cd.val_len
1780 while q < qe { if (ctx.src[q]&0xff) == 47 { sl = q; q = qe } else { q = q + 1 } }
1781 if sl >= 0 {
1782 ooff[0] = sl + 1
1783 olen[0] = qe - (sl + 1)
1784 return 1
1785 }
1786 }
1787 }
1788 }
1789 i = i - 1
1790 }
1791 return 0
1792}
1793// Parse one track size token src[ts..te): sets fr_out[0]=1 for an `fr` (flexible) track, 0 for `px`
1794// (fixed). Returns the fr WEIGHT (>=1) for fr tracks or the px VALUE for fixed. A token with no digits
1795// (auto/min-content/etc) is treated as 1fr (so unknown tracks still take a flexible share, not zero).
1796func _grid_parse_size(src: *u8, ts: i64, te: i64, fr_out: *i64) -> i64 {
1797 // q3 thousandths (fractions matter: "12.25rem" was digit-scanned to 12px; it is 196px)
1798 var ip2: i64 = 0
1799 var any: i64 = 0
1800 var i: i64 = ts
1801 var rd: i64 = 1
1802 while rd == 1 {
1803 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 } }
1804 else { rd = 0 }
1805 }
1806 var fr3: i64 = 0
1807 if i < te { if (src[i]&0xff) == 46 {
1808 i = i + 1
1809 var fd2: i64 = 0
1810 var g5: i64 = 1
1811 while g5 == 1 {
1812 if i < te { let c5: i64 = src[i]&0xff; if c5>=48 { if c5<=57 {
1813 if fd2 == 0 { fr3 = fr3 + (c5-48)*100 }
1814 if fd2 == 1 { fr3 = fr3 + (c5-48)*10 }
1815 if fd2 == 2 { fr3 = fr3 + (c5-48) }
1816 fd2 = fd2 + 1; i = i + 1
1817 } else { g5 = 0 } } else { g5 = 0 } }
1818 else { g5 = 0 }
1819 }
1820 } }
1821 let q3: i64 = ip2 * 1000 + fr3
1822 var isfr: i64 = 0
1823 var isrem: i64 = 0
1824 var j: i64 = i
1825 var u: i64 = 0
1826 while u == 0 {
1827 if j >= te { u = 1 }
1828 else {
1829 let c: i64 = src[j]&0xff
1830 if c==102 { isfr = 1; u = 1 } // fr
1831 else { if c==112 { u = 1 } // px
1832 else { if c==114 { isrem = 1; u = 1 } // rem
1833 else { if c==101 { isrem = 1; u = 1 } // em (~root-relative approx)
1834 else { j = j + 1 } } } }
1835 }
1836 }
1837 if any == 0 { fr_out[0] = 1; return 1 }
1838 fr_out[0] = isfr
1839 if isfr == 1 { var w5: i64 = q3 / 1000; if w5 < 1 { w5 = 1 } return w5 }
1840 if isrem == 1 { return (q3 * 16 + 500) / 1000 }
1841 return q3 / 1000
1842}
1843// Resolve grid-template-columns src[voff..voff+vlen) into per-column px widths[] for total available
1844// width `avail` with `col_gap` between columns. Supports a track list `200px 1fr 2fr` AND `repeat(N,
1845// SIZE)`. fr tracks split the leftover (avail - fixed_px - gaps) by weight. Returns the column count.
1846func _grid_resolve_tracks(src: *u8, voff: i64, vlen: i64, avail: i64, col_gap: i64, widths: *i64, maxcols: i64) -> i64 {
1847 let end: i64 = voff + vlen
1848 let is_fr: *i64 = sys_mmap(8 * (maxcols + 2)) as *i64
1849 let sz: *i64 = sys_mmap(8 * (maxcols + 2)) as *i64
1850 let frb: *i64 = sys_mmap(8) as *i64
1851 var ncols: i64 = 0
1852
1853 // detect a repeat( ... ) form
1854 var rep: i64 = 0 - 1
1855 var ri: i64 = voff
1856 while ri + 7 <= end {
1857 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 {
1858 rep = ri + 7
1859 } } } } } } }
1860 ri = ri + 1
1861 }
1862 if rep >= 0 {
1863 // repeat( COUNT , TRACK ): COUNT is N or the keyword auto-fit/auto-fill; TRACK may be minmax(MIN,MAX).
1864 // auto-fit/fill = "as many MIN-wide columns as fit" -- THE modern responsive grid (office .newrow/.fgrid);
1865 // the old parser read auto-fit as N=0->1 -> single column -> everything STACKED.
1866 var j: i64 = rep
1867 var sk: i64 = 1
1868 while sk == 1 { if j < end { if (src[j]&0xff)==32 { j = j + 1 } else { sk = 0 } } else { sk = 0 } }
1869 var autofit: i64 = 0
1870 var nrep: i64 = 0
1871 if j < end { if (src[j]&0xff)==97 { autofit = 1 } } // 'a' -> auto-fit / auto-fill
1872 if autofit == 1 {
1873 while j < end { if (src[j]&0xff)==44 { break } j = j + 1 }
1874 } else {
1875 var anyN: i64 = 0
1876 var rd: i64 = 1
1877 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 } }
1878 if anyN == 0 { nrep = 1 }
1879 }
1880 if j < end { if (src[j]&0xff)==44 { j = j + 1 } }
1881 var sk2: i64 = 1
1882 while sk2 == 1 { if j < end { if (src[j]&0xff)==32 { j = j + 1 } else { sk2 = 0 } } else { sk2 = 0 } }
1883 // TRACK: minmax(MIN,MAX) -> MIN px sets the auto-fit column count, MAX is the emitted track size.
1884 var minpx: i64 = 0
1885 var val: i64 = 1
1886 var ismm: i64 = 0
1887 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 } } } } } } } }
1888 let tfr: *i64 = sys_mmap(8) as *i64
1889 if ismm == 1 {
1890 var mj: i64 = j + 7
1891 var mc: i64 = mj
1892 while mc < end { let c: i64 = src[mc]&0xff; if c==44 { break } if c==41 { break } mc = mc + 1 }
1893 minpx = _grid_parse_size(src, mj, mc, tfr)
1894 var xj: i64 = mc
1895 if xj < end { if (src[xj]&0xff)==44 { xj = xj + 1 } }
1896 var wsx: i64 = 1
1897 while wsx == 1 { if xj < end { if (src[xj]&0xff)==32 { xj = xj + 1 } else { wsx = 0 } } else { wsx = 0 } }
1898 var xc: i64 = xj
1899 while xc < end { if (src[xc]&0xff)==41 { break } xc = xc + 1 }
1900 val = _grid_parse_size(src, xj, xc, frb)
1901 } else {
1902 var te: i64 = j
1903 var sd: i64 = 0
1904 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 } } } } }
1905 val = _grid_parse_size(src, j, te, frb)
1906 if frb[0] == 0 { minpx = val } else { minpx = 120 }
1907 }
1908 var count: i64 = nrep
1909 if autofit == 1 {
1910 var mp: i64 = minpx
1911 if mp < 1 { mp = 1 }
1912 count = (avail + col_gap) / (mp + col_gap)
1913 if count < 1 { count = 1 }
1914 }
1915 var c2: i64 = 0
1916 while c2 < count { if ncols < maxcols { is_fr[ncols] = frb[0]; sz[ncols] = val; ncols = ncols + 1 } c2 = c2 + 1 }
1917 } else {
1918 // explicit whitespace-separated track list
1919 var p: i64 = voff
1920 var scan: i64 = 1
1921 while scan == 1 {
1922 var sw: i64 = 1
1923 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 } } } } } }
1924 if p >= end { scan = 0 }
1925 else {
1926 var te2: i64 = p
1927 var sd2: i64 = 0
1928 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 } } } } } }
1929 let val2: i64 = _grid_parse_size(src, p, te2, frb)
1930 if ncols < maxcols { is_fr[ncols] = frb[0]; sz[ncols] = val2; ncols = ncols + 1 }
1931 p = te2
1932 }
1933 }
1934 }
1935 if ncols < 1 { ncols = 1; is_fr[0] = 1; sz[0] = 1 }
1936
1937 var fixed_total: i64 = 0
1938 var fr_weight: i64 = 0
1939 var i: i64 = 0
1940 while i < ncols { if is_fr[i]==1 { fr_weight = fr_weight + sz[i] } else { fixed_total = fixed_total + sz[i] } i = i + 1 }
1941 var total_gap: i64 = 0
1942 if ncols > 1 { total_gap = (ncols - 1) * col_gap }
1943 var free: i64 = avail - fixed_total - total_gap
1944 if free < 0 { free = 0 }
1945 var fr_unit: i64 = 0
1946 if fr_weight > 0 { fr_unit = free / fr_weight }
1947 i = 0
1948 while i < ncols {
1949 if is_fr[i]==1 { widths[i] = sz[i] * fr_unit } else { widths[i] = sz[i] }
1950 i = i + 1
1951 }
1952 return ncols
1953}
1954
1955// Computed `float` for a box: 0=none, 1=left, 2=right (R5 rung 1). Reverse-scan = cascade winner first.
1956// Property "float" (5), value "left"(4)/"right"(5); anything else (incl. "none") -> 0.
1957func _layout_float(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1958 let fp: *u8 = "float\x00" as *u8
1959 var i: i64 = ctx.n_computed - 1
1960 while i >= 0 {
1961 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1962 if cd.element_idx == box_idx {
1963 if cd.prop_len == 5 {
1964 var sp: i64 = 1
1965 var k: i64 = 0
1966 while k < 5 { if (ctx.src[cd.prop_off+k]&0xff) != (fp[k]&0xff) { sp = 0; k = 5 } else { k = k + 1 } }
1967 if sp == 1 {
1968 // "left" -> 1
1969 if cd.val_len == 4 {
1970 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 } } } }
1971 }
1972 // "right" -> 2
1973 if cd.val_len == 5 {
1974 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 } } } } }
1975 }
1976 return 0
1977 }
1978 }
1979 }
1980 i = i - 1
1981 }
1982 return 0
1983}
1984// Computed `position` for a box: 0=static, 1=relative, 2=absolute, 3=fixed, 4=sticky.
1985// Same reverse-scan (cascade winner first) shape as _layout_float. Property "position" (8).
1986// Values matched on length + a distinguishing byte, the idiom used throughout this file:
1987// relative(8,'r') · absolute(8,'a') · fixed(5) · sticky(6) · static(6,'s'-at-0 w/ 'a' at 2)
1988// `static` and anything unknown -> 0, so an unrecognised value can only ever mean NORMAL FLOW.
1989func _layout_position(ctx: *LayoutCtx, box_idx: i64) -> i64 {
1990 let pp: *u8 = "position\x00" as *u8
1991 var i: i64 = ctx.n_computed - 1
1992 while i >= 0 {
1993 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
1994 if cd.element_idx == box_idx {
1995 if cd.prop_len == 8 {
1996 var sp: i64 = 1
1997 var k: i64 = 0
1998 while k < 8 { if (ctx.src[cd.prop_off+k]&0xff) != (pp[k]&0xff) { sp = 0; k = 8 } else { k = k + 1 } }
1999 if sp == 1 {
2000 let v0: i64 = ctx.src[cd.val_off] & 0xff
2001 if cd.val_len == 8 {
2002 if v0 == 114 { return 1 } // relative
2003 if v0 == 97 { return 2 } // absolute
2004 }
2005 if cd.val_len == 5 { if v0 == 102 { return 3 } } // fixed
2006 if cd.val_len == 6 { if v0 == 115 {
2007 // sticky vs static: byte 2 is 'i' (105) for sticky, 'a' (97) for static
2008 if (ctx.src[cd.val_off+2]&0xff) == 105 { return 4 }
2009 return 0
2010 } }
2011 return 0
2012 }
2013 }
2014 }
2015 i = i - 1
2016 }
2017 return 0
2018}
2019// Computed `justify-content` for a flex container (main-axis distribution). Returns:
2020// 0 flex-start (default) · 1 center · 2 flex-end · 3 space-between · 4 space-around · 5 space-evenly.
2021// Matched by value length (+ one byte for around/evenly) -- standard keyword set.
2022func _layout_justify(ctx: *LayoutCtx, box_idx: i64) -> i64 {
2023 let jp: *u8 = "justify-content\x00" as *u8
2024 var i: i64 = ctx.n_computed - 1
2025 while i >= 0 {
2026 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
2027 if cd.element_idx == box_idx {
2028 if cd.prop_len == 15 {
2029 var sp: i64 = 1
2030 var k: i64 = 0
2031 while k < 15 { if (ctx.src[cd.prop_off+k]&0xff) != (jp[k]&0xff) { sp = 0; k = 15 } else { k = k + 1 } }
2032 if sp == 1 {
2033 let vl: i64 = cd.val_len
2034 let vo: i64 = cd.val_off
2035 if vl == 6 { return 1 } // center
2036 if vl == 3 { return 2 } // end
2037 if vl == 5 { return 0 } // start
2038 if vl == 8 { return 2 } // flex-end
2039 if vl == 10 { return 0 } // flex-start
2040 if vl == 13 { return 3 } // space-between
2041 if vl == 12 { if (ctx.src[vo+6]&0xff)==97 { return 4 } return 5 } // space-around(a)/-evenly(e)
2042 return 0
2043 }
2044 }
2045 }
2046 i = i - 1
2047 }
2048 return 0
2049}
2050// Computed `align-items` for a flex container (cross-axis = vertical for a row). Returns:
2051// 0 stretch/flex-start (default) · 1 center · 2 flex-end. (baseline approximated as start.)
2052func _layout_align(ctx: *LayoutCtx, box_idx: i64) -> i64 {
2053 let ap: *u8 = "align-items\x00" as *u8
2054 var i: i64 = ctx.n_computed - 1
2055 while i >= 0 {
2056 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
2057 if cd.element_idx == box_idx {
2058 if cd.prop_len == 11 {
2059 var sp: i64 = 1
2060 var k: i64 = 0
2061 while k < 11 { if (ctx.src[cd.prop_off+k]&0xff) != (ap[k]&0xff) { sp = 0; k = 11 } else { k = k + 1 } }
2062 if sp == 1 {
2063 let vl: i64 = cd.val_len
2064 if vl == 6 { return 1 } // center
2065 if vl == 3 { return 2 } // end
2066 if vl == 8 { if (ctx.src[cd.val_off]&0xff)==102 { return 2 } return 0 } // flex-end(f) vs baseline(b)
2067 return 0 // flex-start / start / stretch
2068 }
2069 }
2070 }
2071 i = i - 1
2072 }
2073 return 0
2074}
2075// Computed `flex-grow` for a flex ITEM (the CHILD box, not the container). Returns the grow factor in Q3
2076// THOUSANDTHS (1.0 -> 1000; fractional 0.1 -> 100; 0 = does not grow) so fractional grow works. Also
2077// accepts the `flex` shorthand's FIRST token (`flex:1` -> 1000, `flex:0 0 0%` -> 0) since `flex:N` is more
2078// common on real pages. Negative/invalid -> 0 (negative flex-grow is invalid per spec). Reverse-walk =
2079// source-order winner, so a later `flex-grow` correctly overrides an earlier `flex` shorthand (and v.v.).
2080func _layout_flex_grow(ctx: *LayoutCtx, box_idx: i64) -> i64 {
2081 let fg: *u8 = "flex-grow\x00" as *u8 // len 9 (exact compare -- must NOT match flex-wrap, also len 9)
2082 let fx: *u8 = "flex\x00" as *u8 // len 4 (the shorthand)
2083 var i: i64 = ctx.n_computed - 1
2084 while i >= 0 {
2085 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
2086 if cd.element_idx == box_idx {
2087 var matched: i64 = 0
2088 if cd.prop_len == 9 {
2089 var sp: i64 = 1
2090 var k: i64 = 0
2091 while k < 9 { if (ctx.src[cd.prop_off+k]&0xff) != (fg[k]&0xff) { sp = 0; k = 9 } else { k = k + 1 } }
2092 if sp == 1 { matched = 1 }
2093 }
2094 if matched == 0 { if cd.prop_len == 4 {
2095 var sp2: i64 = 1
2096 var k2: i64 = 0
2097 while k2 < 4 { if (ctx.src[cd.prop_off+k2]&0xff) != (fx[k2]&0xff) { sp2 = 0; k2 = 4 } else { k2 = k2 + 1 } }
2098 if sp2 == 1 { matched = 1 }
2099 } }
2100 if matched == 1 {
2101 // Parse the FIRST token of the value as a number in Q3 thousandths (so fractional grow like
2102 // 0.1 -> 100 works). For the `flex` shorthand ("0 0 0%") only the first token is the grow
2103 // factor. Skip leading spaces, take bytes up to the next space, hand to nx_css_number_parse.
2104 var p: i64 = cd.val_off
2105 let pe: i64 = cd.val_off + cd.val_len
2106 var skip: i64 = 1
2107 while skip == 1 {
2108 if p < pe { if (ctx.src[p]&0xff) == 32 { p = p + 1 } else { skip = 0 } }
2109 else { skip = 0 }
2110 }
2111 var te: i64 = p
2112 var go: i64 = 1
2113 while go == 1 { if te < pe { if (ctx.src[te]&0xff) == 32 { go = 0 } else { te = te + 1 } } else { go = 0 } }
2114 if te > p {
2115 let q3: i64 = nx_css_number_parse(ctx.src, p, te - p)
2116 if q3 == NX_CSS_NUMBER_PARSE_ERROR { return 0 }
2117 if q3 < 0 { return 0 }
2118 return q3
2119 }
2120 return 0
2121 }
2122 }
2123 i = i - 1
2124 }
2125 return 0
2126}
2127// Shift a box and its WHOLE subtree vertically by dy (used to cross-align a flex item after it's placed).
2128func _layout_shift_subtree(ctx: *LayoutCtx, box_idx: i64, dy: i64) -> i64 {
2129 if box_idx < 0 { return 0 }
2130 if box_idx >= ctx.tree.count { return 0 }
2131 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2132 b.y = b.y + dy
2133 if l_ytrap == 1 { if b.y + dy > NX_MAGIC_1000000000 {
2134 _yt_w("YTRAP@shift idx=" as *u8); _yt_n(box_idx)
2135 _yt_w(" oldy=" as *u8); _yt_n(b.y)
2136 _yt_w(" dy=" as *u8); _yt_n(dy)
2137 _yt_w("
2138" as *u8)
2139 } }
2140 var c: i64 = b.first_child_idx
2141 var safety: i64 = 0
2142 while c >= 0 {
2143 if safety >= NX_MAGIC_65536 { c = 0 - 1 }
2144 else {
2145 safety = safety + 1
2146 _layout_shift_subtree(ctx, c, dy)
2147 let cc: *LayoutBox = (ctx.tree.boxes as *u8 + (c as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2148 c = cc.next_sibling_idx
2149 }
2150 }
2151 return 0
2152}
2153func _layout_block_recurse(ctx: *LayoutCtx, table: *LayoutPropTable,
2154 box_idx: i64,
2155 avail_w: i64,
2156 origin_x: i64, origin_y: i64) -> i64 {
2157 if box_idx < 0 { return 0 }
2158 if box_idx >= ctx.tree.count { return 0 }
2159
2160 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2161
2162 // display:none -> the box (and its subtree) take ZERO space and are not laid out (paint skips too).
2163 if _layout_display_none(ctx, box_idx) == 1 {
2164 b.x = origin_x
2165 b.y = origin_y
2166 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
2167 _yt_w("YTRAP@1775 idx=" as *u8); _yt_n(box_idx)
2168 _yt_w(" oy=" as *u8); _yt_n(origin_y)
2169 _yt_w("
2170" as *u8)
2171 } }
2172 b.w = 0
2173 b.h = 0
2174 return 0
2175 }
2176
2177 // ---- TEXT box intrinsic sizing (5x7 bitmap font, 1px spacing) ----
2178 // Real CSS font-metrics integration is a separate arc; this gives
2179 // text boxes non-zero dimensions so block-flow stacking produces
2180 // visible vertical extent. Compute width = min(text_len * 6, avail_w),
2181 // height = lines * 10 where lines = ceil(text_len / line_chars) and
2182 // line_chars = max(1, avail_w / 6). Bits-up basis: see
2183 // nx_font_bitmap_5x7.nx for the underlying 5x7 glyph metrics.
2184 if b.kind == NX_LAYOUT_BOX_TEXT {
2185 let glyph_w: i64 = 9 // matches the 9x15 X render font advance (was 6)
2186 let glyph_h: i64 = 17 // matches 9x15 font height + leading (was 10)
2187 let tl: i64 = b.text_len
2188 if ctx.text_measured == 1 {
2189 // MEASURED mode: proportional width + measured word-wrap line count (font8x8 metrics SSOT).
2190 if ctx.font_tab == 0 { ctx.font_tab = (font8x8_table()) as i64 }
2191 let ftab: *u8 = ctx.font_tab as *u8
2192 let tp: *u8 = ((ctx.src as i64) + b.text_off) as *u8
2193 var mw: i64 = font8x8_text_w(ftab, tp, 0, tl)
2194 var av: i64 = avail_w
2195 if av < 4 { av = 4 }
2196 var ml: i64 = 0
2197 var ms: i64 = 0
2198 while ms < tl { ms = font8x8_next_break(ftab, tp, tl, av, ms); ml = ml + 1 }
2199 if ml < 1 { ml = 1 }
2200 if mw > avail_w { mw = avail_w }
2201 b.x = origin_x
2202 b.y = origin_y
2203 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
2204 _yt_w("YTRAP@1806 idx=" as *u8); _yt_n(box_idx)
2205 _yt_w(" oy=" as *u8); _yt_n(origin_y)
2206 _yt_w("
2207" as *u8)
2208 } }
2209 b.w = mw
2210 b.h = ml * glyph_h
2211 return b.h
2212 }
2213 var line_chars: i64 = avail_w / glyph_w
2214 if line_chars < 1 { line_chars = 1 }
2215 var lines: i64 = tl / line_chars
2216 if tl - lines * line_chars > 0 { lines = lines + 1 }
2217 if lines < 1 { lines = 1 }
2218 var w_px: i64 = tl * glyph_w
2219 if w_px > avail_w { w_px = avail_w }
2220 b.x = origin_x
2221 b.y = origin_y
2222 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
2223 _yt_w("YTRAP@1819 idx=" as *u8); _yt_n(box_idx)
2224 _yt_w(" oy=" as *u8); _yt_n(origin_y)
2225 _yt_w("
2226" as *u8)
2227 } }
2228 b.w = w_px
2229 b.h = lines * glyph_h
2230 return b.h
2231 }
2232
2233 // Resolve box-model values from cascade (-1 / UNSET = not specified). Each side = ONE reverse scan
2234 // across its longhand AND the `margin`/`padding` shorthand -- last declaration wins (cascade order).
2235 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)
2236 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)
2237 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)
2238 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)
2239 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)
2240 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)
2241 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)
2242 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)
2243 let w_raw: i64 = _layout_lookup_px(ctx, table.src, box_idx, table.width_off, table.width_len)
2244 let h_raw: i64 = _layout_lookup_height_px(ctx, table.src, box_idx, table.height_off, table.height_len)
2245
2246 // SCREEN-READER-ONLY pattern (seq1150, 2026-07-28): width:1px + height:1px (+ clip/overflow:hidden,
2247 // which this engine does not implement) is the universal accessibility hide -- wikipedia's
2248 // .mw-jump-link, WordPress .screen-reader-text. Chrome shows NOTHING readable (contents clipped to
2249 // the 1x1 box); without clipping we char-wrapped the text into a 1px-wide, 255px-tall visible
2250 // column (probe-proven on the real page). An element EXPLICITLY sized <=1x1 on BOTH axes lays out
2251 // as hidden: subtree untouched, the paint's h==0 guard skips it. A 1px DIVIDER (width:1px,
2252 // height auto/large) is NOT matched -- both axes must be explicit and <=1.
2253 if w_raw >= 0 { if w_raw <= 1 { if h_raw >= 0 { if h_raw <= 1 {
2254 b.x = origin_x
2255 b.y = origin_y
2256 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
2257 _yt_w("YTRAP@1847 idx=" as *u8); _yt_n(box_idx)
2258 _yt_w(" oy=" as *u8); _yt_n(origin_y)
2259 _yt_w("
2260" as *u8)
2261 } }
2262 b.w = 0
2263 b.h = 0
2264 return 0
2265 } } } }
2266
2267 var mt: i64 = mt_raw
2268 if mt == NX_MARGIN_UNSET { mt = 0 } // unset -> 0; a real NEGATIVE margin is KEPT (overlap), not clamped
2269 var mb: i64 = mb_raw
2270 if mb == NX_MARGIN_UNSET { mb = 0 }
2271 var ml: i64 = ml_raw
2272 if ml == NX_MARGIN_UNSET { ml = 0 }
2273 var mr: i64 = mr_raw
2274 if mr == NX_MARGIN_UNSET { mr = 0 }
2275 var pt: i64 = pt_raw
2276 if pt < 0 { pt = 0 }
2277 var pb: i64 = pb_raw
2278 if pb < 0 { pb = 0 }
2279 var pl: i64 = pl_raw
2280 if pl < 0 { pl = 0 }
2281 var pr: i64 = pr_raw
2282 if pr < 0 { pr = 0 }
2283
2284 // Width: explicit, else fill avail_w (minus horizontal margins).
2285 var content_w: i64 = w_raw
2286 if content_w < 0 {
2287 content_w = avail_w - ml - mr
2288 if content_w < 0 { content_w = 0 }
2289 }
2290 // MAX-WIDTH (CSS 2.1 §10.4) via the literal-source lookup (the top/left trick -- no prop-table
2291 // widening). Constrains both explicit and fill widths; absurd values already rejected upstream.
2292 let mxw: i64 = _layout_lookup_px(ctx, "max-width\x00" as *u8, box_idx, 0, 9)
2293 if mxw >= 0 { if mxw < NX_MAGIC_100000 { if content_w > mxw { content_w = mxw } } }
2294
2295 // Position the border-box. Horizontal margin shifts x; padding
2296 // expands children's available width and offsets their origin.
2297 b.x = origin_x + ml
2298 // AUTO-MARGIN CENTERING (§10.3.3): both horizontal margins auto + used width < avail -> the free
2299 // space splits equally. THE styled-wikipedia dx≈150 regime: `.mw-page-container{margin:0 auto;
2300 // max-width:...}` -- Chrome centers the whole page column; we rendered it flush-left (2026-07-28).
2301 if content_w < avail_w {
2302 if _lb_margin_auto(ctx, box_idx, 3) == 1 { if _lb_margin_auto(ctx, box_idx, 1) == 1 {
2303 b.x = origin_x + (avail_w - content_w) / 2
2304 } }
2305 }
2306 b.y = origin_y + mt
2307 if l_ytrap == 1 { if b.y > NX_MAGIC_1000000000 {
2308 _yt_w("YTRAP block idx=" as *u8); _yt_n(box_idx)
2309 _yt_w(" origin_y=" as *u8); _yt_n(origin_y)
2310 _yt_w(" mt=" as *u8); _yt_n(mt)
2311 _yt_w(" mt_raw=" as *u8); _yt_n(mt_raw)
2312 _yt_w("\n" as *u8)
2313 } }
2314 b.w = content_w
2315
2316 // Lay out children inside the content box.
2317 let content_origin_x: i64 = b.x + pl
2318 let content_origin_y: i64 = b.y + pt
2319 let child_avail_w: i64 = content_w - pl - pr
2320
2321 var children_h: i64 = 0
2322 var use_grid: i64 = _layout_display_grid(ctx, box_idx)
2323 if use_grid == 1 { if _layout_has_grid_areas(ctx, box_idx) == 1 { use_grid = 0 } } // named areas -> block flow
2324 if use_grid == 1 {
2325 // ---- GRID (rung 2) ---- tracks from grid-template-columns with PER-TRACK sizing (px fixed + fr
2326 // flexible share of leftover) + column-gap/row-gap/gap. Children placed ROW-MAJOR, wrapping every
2327 // ncols; row height = tallest child outer height; rows separated by row_gap. (rung 1 = equal cols
2328 // is the special case all-fr-equal/no-gap.) grid-template-rows, line placement + spanning = later.
2329 let g_toff: *i64 = sys_mmap(8) as *i64
2330 let g_tlen: *i64 = sys_mmap(8) as *i64
2331 let g_widths: *i64 = sys_mmap(8 * 258) as *i64
2332 let col_gap: i64 = _grid_gap(ctx, box_idx, 0)
2333 let row_gap: i64 = _grid_gap(ctx, box_idx, 1)
2334 var ncols: i64 = 1
2335 if _layout_grid_template(ctx, box_idx, g_toff, g_tlen) == 1 {
2336 ncols = _grid_resolve_tracks(ctx.src, g_toff[0], g_tlen[0], child_avail_w, col_gap, g_widths, 256)
2337 } else { g_widths[0] = child_avail_w }
2338 if ncols < 1 { ncols = 1 }
2339 var gcol: i64 = 0
2340 var rows_done: i64 = 0
2341 var row_y: i64 = content_origin_y
2342 var row_h: i64 = 0
2343 var col_x: i64 = content_origin_x
2344 var gci: i64 = b.first_child_idx
2345 var gsf: i64 = 0
2346 while gci >= 0 {
2347 if gsf >= NX_MAGIC_4096 { gci = 0 - 1 }
2348 else {
2349 gsf = gsf + 1
2350 if gcol == 0 {
2351 if rows_done > 0 { row_y = row_y + row_gap }
2352 col_x = content_origin_x
2353 }
2354 let gcw: i64 = g_widths[gcol]
2355 let gchh: i64 = _layout_block_recurse(ctx, table, gci, gcw, col_x, row_y)
2356 if gchh > row_h { row_h = gchh }
2357 let gcc: *LayoutBox = (ctx.tree.boxes as *u8 + (gci as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2358 let gnxt: i64 = gcc.next_sibling_idx
2359 col_x = col_x + gcw + col_gap
2360 gcol = gcol + 1
2361 if gcol >= ncols { row_y = row_y + row_h; rows_done = rows_done + 1; row_h = 0; gcol = 0 }
2362 gci = gnxt
2363 }
2364 }
2365 if gcol > 0 { row_y = row_y + row_h }
2366 children_h = row_y - content_origin_y
2367 } else {
2368 // TABLE (seq1158): resolve this table's column widths ONCE, publish them for the row (flex) path
2369 // below, lay the subtree out normally, then RESTORE the previous table context -- nested tables
2370 // (news.ycombinator.com nests them) each get their own columns and the outer one survives intact.
2371 var tbl_saved_active: i64 = 0
2372 var tbl_saved_idx: i64 = 0
2373 var tbl_saved_ncols: i64 = 0
2374 var tbl_saved_p: i64 = 0
2375 var tbl_entered: i64 = 0
2376 if _layout_display_table(ctx, box_idx) == 1 {
2377 tbl_saved_active = l_tbl_active
2378 tbl_saved_idx = l_tbl_idx
2379 tbl_saved_ncols = l_tbl_ncols
2380 tbl_saved_p = l_tbl_colw_p
2381 tbl_entered = 1
2382 let cwbuf: *i64 = sys_mmap(8 * NX_TBL_MAXCOLS) as *i64
2383 let nc: i64 = _layout_table_cols(ctx, box_idx, child_avail_w, cwbuf)
2384 if nc > 0 {
2385 l_tbl_active = 1
2386 l_tbl_idx = box_idx
2387 l_tbl_ncols = nc
2388 l_tbl_colw_p = cwbuf as i64
2389 } else { l_tbl_active = 0 }
2390 }
2391 var is_rowflex: i64 = _layout_display_flex(ctx, box_idx)
2392 if is_rowflex == 1 { if _layout_flex_dir_col(ctx, box_idx) == 1 { is_rowflex = 0 } } // column flex = block stacking
2393 if is_rowflex == 1 {
2394 // ---- FLEX ROW (rung 2: + justify-content) ---- children LEFT-TO-RIGHT; width = explicit `width`
2395 // else an equal share of the remaining space (auto-fill); container height = tallest child.
2396 // justify-content distributes the FREE space (avail - used) on the main axis -- a no-op when
2397 // children auto-fill (free=0, so tables/sidebars are unchanged), active when children have explicit
2398 // widths. flex-grow/shrink/basis, wrap, align-items = subsequent rungs.
2399 var nauto: i64 = 0
2400 var sum_explicit: i64 = 0
2401 var sum_base: i64 = 0 // Σ flex-basis (explicit width else content width) -- auto != 0
2402 var n_children: i64 = 0
2403 var sum_grow: i64 = 0 // Σ flex-grow over children, in Q3 thousandths (rung 3; fractional-aware)
2404 var has_grow: i64 = 0 // any child with flex-grow>0 -> use the grow distribution path
2405 var ci: i64 = b.first_child_idx
2406 var sf: i64 = 0
2407 while ci >= 0 {
2408 if sf >= NX_MAGIC_4096 { ci = 0 - 1 }
2409 else {
2410 sf = sf + 1
2411 n_children = n_children + 1
2412 let cw0: i64 = _layout_lookup_px(ctx, table.src, ci, table.width_off, table.width_len)
2413 let cg: i64 = _layout_flex_grow(ctx, ci)
2414 // flex-grow accrues for EVERY growing item (explicit-width OR auto basis). BASE size =
2415 // explicit width else the item's CONTENT width -- CSS flex-basis defaults to AUTO, not 0.
2416 // The old base-0 model collapsed every grow-0 auto sibling to w=0: x.com's login card laid
2417 // "Continue with Google" (explicit) while "Continue with phone/Apple"/"or"/"Email or
2418 // username" all rendered as w=0 slivers (region x-ray, 2026-07-28).
2419 if cg > 0 { has_grow = 1; sum_grow = sum_grow + cg }
2420 var cb0: i64 = cw0
2421 if cb0 < 0 { cb0 = _layout_intrinsic_w(ctx, ci) }
2422 sum_base = sum_base + cb0
2423 if cw0 >= 0 { sum_explicit = sum_explicit + cw0 } else { nauto = nauto + 1 }
2424 let cc: *LayoutBox = (ctx.tree.boxes as *u8 + (ci as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2425 ci = cc.next_sibling_idx
2426 }
2427 }
2428 var rem: i64 = child_avail_w - sum_explicit
2429 // FLEX-SHRINK (minimal, 2026-07-28): when explicit-width children OVERFLOW the row, real CSS
2430 // shrinks them (flex-shrink defaults to 1); our old model kept them full-size and starved every
2431 // auto sibling to w=0 (the x.com login-card sliver that survived the basis fix). Shrink-mode
2432 // sizes EVERY child proportionally to its basis so the row exactly fits. Fires ONLY in the
2433 // previously-broken overflow case -- healthy rows are untouched by construction.
2434 var shrinkmode: i64 = 0
2435 if has_grow == 0 { if rem <= 0 { if sum_base > 0 { if n_children > 1 { shrinkmode = 1 } } } }
2436 if rem < 0 { rem = 0 }
2437 var auto_w: i64 = 0
2438 if nauto > 0 { auto_w = rem / nauto }
2439 // free space on the main axis after laying children at their widths -> justify-content distributes it
2440 let total_used: i64 = sum_explicit + nauto * auto_w
2441 var free: i64 = child_avail_w - total_used
2442 if free < 0 { free = 0 }
2443 // flex-grow (rung 3): distribute the leftover space (avail - Σ explicit-width siblings) among the
2444 // AUTO-basis growing items, proportionally to their grow factors (basis 0). Grown items consume
2445 // all the leftover, so there is no main-axis free space for justify-content (free=0 -> no-op).
2446 var grow_free: i64 = 0
2447 if has_grow == 1 {
2448 grow_free = child_avail_w - sum_base // leftover AFTER content-sized bases (basis:auto)
2449 if grow_free < 0 { grow_free = 0 }
2450 free = 0
2451 }
2452 let jc: i64 = _layout_justify(ctx, box_idx)
2453 var start_off: i64 = 0
2454 var spacing: i64 = 0
2455 if jc == 1 { start_off = free / 2 } // center
2456 if jc == 2 { start_off = free } // flex-end
2457 if jc == 3 { if n_children > 1 { spacing = free / (n_children - 1) } } // space-between
2458 if jc == 4 { if n_children > 0 { spacing = free / n_children; start_off = spacing / 2 } } // space-around
2459 if jc == 5 { if n_children > 0 { spacing = free / (n_children + 1); start_off = spacing } } // space-evenly
2460 var rx: i64 = content_origin_x + start_off
2461 var max_h: i64 = 0
2462 var tcell_i: i64 = 0 // 0-based cell index = the COLUMN index for a table row
2463 ci = b.first_child_idx
2464 sf = 0
2465 while ci >= 0 {
2466 if sf >= NX_MAGIC_4096 { ci = 0 - 1 }
2467 else {
2468 sf = sf + 1
2469 let cwe: i64 = _layout_lookup_px(ctx, table.src, ci, table.width_off, table.width_len)
2470 var cw: i64 = auto_w
2471 if cwe >= 0 { cw = cwe }
2472 if shrinkmode == 1 {
2473 var sbase: i64 = cwe
2474 if sbase < 0 { sbase = _layout_intrinsic_w(ctx, ci) }
2475 cw = (sbase * child_avail_w) / sum_base
2476 }
2477 // TABLE COLUMN (seq1158): an auto-width cell of a row belonging to the ACTIVE table takes
2478 // its COLUMN's shared max-content width instead of an equal share. Guarded by the row's
2479 // nearest-table identity, so a plain display:flex div inside a cell is unaffected.
2480 // colspan (seq1159): a spanning cell takes the SUM of its N columns and advances N slots.
2481 var tcell_adv: i64 = 1
2482 if l_tbl_active == 1 { if cwe < 0 { if tcell_i < l_tbl_ncols {
2483 if _layout_nearest_table(ctx, box_idx) == l_tbl_idx {
2484 let tcolw: *i64 = l_tbl_colw_p as *i64
2485 let cellb: *LayoutBox = _lb_box(ctx, ci)
2486 if cellb.source_node_idx >= 2 { tcell_adv = cellb.source_node_idx }
2487 var csum: i64 = 0
2488 var ck: i64 = 0
2489 while ck < tcell_adv {
2490 if tcell_i + ck < l_tbl_ncols { csum = csum + tcolw[tcell_i + ck] }
2491 ck = ck + 1
2492 }
2493 cw = csum
2494 }
2495 } } }
2496 // flex-grow distribution: width = base + its proportional share of the leftover, where
2497 // base = explicit width (flex-basis) else 0. A grow-0 auto item collapses to 0; a grow-0
2498 // explicit item keeps its width. Active only in a has_grow container, so non-grow flex
2499 // layouts (the common case) are byte-identical to before.
2500 if has_grow == 1 {
2501 let cg2: i64 = _layout_flex_grow(ctx, ci)
2502 var base: i64 = cwe
2503 if base < 0 { base = _layout_intrinsic_w(ctx, ci) } // flex-basis:auto = content size
2504 var share: i64 = 0
2505 // divisor = max(1.0, Σgrow) in Q3 thousandths: when Σgrow<1, only that fraction of the
2506 // free space is distributed (the rest stays unused), per CSS Flexbox §9.7. When Σgrow>=1
2507 // it normalizes (grow_i/Σgrow) as usual. Integer grow (1->1000) is unchanged.
2508 if cg2 > 0 {
2509 var divisor: i64 = sum_grow
2510 if divisor < 1000 { divisor = 1000 }
2511 share = (cg2 * grow_free) / divisor
2512 }
2513 cw = base + share
2514 }
2515 let chh: i64 = _layout_block_recurse(ctx, table, ci, cw, rx, content_origin_y)
2516 // explicit-width grow item: the recurse sized the box to its explicit width (it reads the
2517 // CSS width directly, ignoring the grown avail we passed) -> force the grown main-size onto
2518 // the box. Exact for leaf items; a non-leaf's children keep the explicit width (minor approx).
2519 if has_grow == 1 { if cwe >= 0 {
2520 let ccg: *LayoutBox = (ctx.tree.boxes as *u8 + (ci as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2521 ccg.w = cw
2522 } }
2523 rx = rx + cw + spacing
2524 tcell_i = tcell_i + tcell_adv
2525 if chh > max_h { max_h = chh }
2526 let cc2: *LayoutBox = (ctx.tree.boxes as *u8 + (ci as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2527 ci = cc2.next_sibling_idx
2528 }
2529 }
2530 // align-items (cross axis): items were placed at the top; center/flex-end shift each down within
2531 // the row's height (max_h). A no-op for the default (stretch/flex-start) and when all items are
2532 // equal-height (offset 0), so existing flex layouts are unchanged.
2533 let ai: i64 = _layout_align(ctx, box_idx)
2534 if ai != 0 {
2535 // cross-size = the container's content height (explicit height wins, else the tallest item)
2536 var cross_size: i64 = max_h
2537 if h_raw >= 0 { cross_size = h_raw }
2538 var ci3: i64 = b.first_child_idx
2539 var sf3: i64 = 0
2540 while ci3 >= 0 {
2541 if sf3 >= NX_MAGIC_4096 { ci3 = 0 - 1 }
2542 else {
2543 sf3 = sf3 + 1
2544 let cb: *LayoutBox = (ctx.tree.boxes as *u8 + (ci3 as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2545 let chh2: i64 = cb.h
2546 var aoff: i64 = 0
2547 if ai == 1 { aoff = (cross_size - chh2) / 2 }
2548 if ai == 2 { aoff = cross_size - chh2 }
2549 if aoff < 0 { aoff = 0 }
2550 if aoff > NX_MAGIC_100000 { aoff = 0 } // a cross-shift beyond any page is corrupt input, never applied
2551 let cnext: i64 = cb.next_sibling_idx
2552 if aoff > 0 { _layout_shift_subtree(ctx, ci3, aoff) }
2553 ci3 = cnext
2554 }
2555 }
2556 }
2557 children_h = max_h
2558 } else {
2559 // Mixed block/inline flow: BLOCK children stack vertically; runs of INLINE/TEXT children flow
2560 // horizontally via a line-box pen (wrapping at avail_w). The pen's pen_y is the single vertical
2561 // cursor; a BLOCK child first flushes any open inline line, then advances pen_y by its height.
2562 let pen: *InlinePen = (sys_mmap(NX_INLINE_PEN_BYTES as nx_size)) as *InlinePen
2563 pen.pen_x = 0
2564 pen.pen_y = 0
2565 pen.line_h = 0
2566 pen.avail_w = child_avail_w
2567 pen.origin_x = content_origin_x
2568 pen.origin_y = content_origin_y
2569 pen.table = table as i64
2570 // float state (R5 rung 1): ONE active float region; fl_bottom/fl_w are content-relative.
2571 // float_extent = lowest float bottom seen (so the container grows to contain a tall float).
2572 var fl_side: i64 = 0 // 0 none, 1 left, 2 right
2573 var fl_bottom: i64 = 0
2574 var fl_w: i64 = 0
2575 var float_extent: i64 = 0
2576 var child_idx: i64 = b.first_child_idx
2577 var safety: i64 = 0
2578 let MAX_ITER: i64 = NX_MAGIC_65536
2579 var keep: i64 = 1
2580 while keep == 1 {
2581 if safety >= MAX_ITER { keep = 0 }
2582 else {
2583 safety = safety + 1
2584 if child_idx < 0 { keep = 0 }
2585 else {
2586 let c: *LayoutBox = (ctx.tree.boxes as *u8 + (child_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
2587 if c.kind == NX_LAYOUT_BOX_BLOCK {
2588 if pen.pen_x > 0 {
2589 pen.pen_y = pen.pen_y + pen.line_h
2590 pen.pen_x = 0
2591 pen.line_h = 0
2592 }
2593 // OUT-OF-FLOW POSITIONING (CSS 2.1 §9.3): absolute/fixed boxes are removed
2594 // from normal flow -- they do NOT advance the cursor and do NOT contribute
2595 // height. Before this, an overlay/dropdown/sticky-header stacked as a normal
2596 // block, so real stylesheets built a tall scattered ribbon instead of a
2597 // layout: MEASURED 2026-07-27, stackoverflow's page_h ran 18,617 vs Chrome's
2598 // ~7,000 with CSS on, and layout_pos_probe showed an absolute box adding its
2599 // full 100px to the page. They are still laid out and painted (at the current
2600 // flow origin, offset by top/left when given) so content stays visible --
2601 // full containing-block resolution is the next rung, but consuming flow space
2602 // is the part that actually wrecks the page.
2603 var oof: i64 = 0
2604 let posk: i64 = _layout_position(ctx, child_idx)
2605 if posk == 2 { oof = 1 }
2606 if posk == 3 { oof = 1 }
2607 if oof == 1 {
2608 // `top`/`left` are not in LayoutPropTable (it carries only margin/padding/
2609 // width/height), and widening that struct would touch every constructor.
2610 // _layout_lookup_px compares against a CALLER-SUPPLIED buffer, so a string
2611 // literal at offset 0 is a legal property source -- no struct change needed.
2612 let poff_t: i64 = _layout_lookup_px(ctx, "top\x00" as *u8, child_idx, 0, 3)
2613 let poff_l: i64 = _layout_lookup_px(ctx, "left\x00" as *u8, child_idx, 0, 4)
2614 var pax: i64 = content_origin_x
2615 var pay: i64 = content_origin_y + pen.pen_y
2616 if poff_l >= 0 { pax = content_origin_x + poff_l }
2617 if poff_t >= 0 { pay = content_origin_y + poff_t }
2618 // laid out + painted, but pen.pen_y is NOT advanced -> zero flow height.
2619 _layout_block_recurse(ctx, table, child_idx, child_avail_w, pax, pay)
2620 }
2621 let flo: i64 = _layout_float(ctx, child_idx)
2622 let fwe: i64 = _layout_lookup_px(ctx, table.src, child_idx, table.width_off, table.width_len)
2623 if oof == 1 { fl_side = fl_side } // out-of-flow: nothing else to do
2624 else { if flo != 0 { if fwe >= 0 {
2625 // FLOATED block with explicit width: pin to the left/right edge at the current
2626 // pen_y, OUT of normal flow (pen_y does NOT advance -> following blocks flow beside).
2627 var fx: i64 = content_origin_x
2628 if flo == 2 { fx = content_origin_x + child_avail_w - fwe }
2629 let fh: i64 = _layout_block_recurse(ctx, table, child_idx, fwe, fx, content_origin_y + pen.pen_y)
2630 fl_side = flo
2631 fl_w = fwe
2632 let fbot: i64 = pen.pen_y + fh
2633 if fbot > fl_bottom { fl_bottom = fbot }
2634 if fbot > float_extent { float_extent = fbot }
2635 } else {
2636 // float w/o explicit width: shrink-to-fit is a later rung -> normal full-width block.
2637 let chn: i64 = _layout_block_recurse(ctx, table, child_idx, child_avail_w, content_origin_x, content_origin_y + pen.pen_y)
2638 pen.pen_y = pen.pen_y + chn
2639 } }
2640 else {
2641 // NORMAL block: while an active float covers this y, flow BESIDE it (shifted + narrower).
2642 var bx: i64 = content_origin_x
2643 var bw: i64 = child_avail_w
2644 if fl_side != 0 { if pen.pen_y < fl_bottom {
2645 bw = child_avail_w - fl_w
2646 if bw < 0 { bw = 0 }
2647 if fl_side == 1 { bx = content_origin_x + fl_w } // left float pushes content right
2648 } }
2649 let child_outer_h: i64 = _layout_block_recurse(ctx, table, child_idx, bw, bx, content_origin_y + pen.pen_y)
2650 pen.pen_y = pen.pen_y + child_outer_h
2651 if fl_side != 0 { if pen.pen_y >= fl_bottom { fl_side = 0; fl_w = 0 } } // cleared the float
2652 } }
2653 } else {
2654 // INLINE content beside a float: narrow/shift the pen so inline text flows BESIDE
2655 // the float (the same treatment BLOCK children get above) instead of overlapping it
2656 // -- the Wikipedia header fix (a float:right infobox was overlapping the intro text).
2657 // ALWAYS recompute the pen constraints from the CURRENT float coverage (not just when
2658 // active) so a cleared float resets cleanly to full width. Right float narrows
2659 // avail_w; left float also shifts origin_x past the float.
2660 var iaw: i64 = child_avail_w
2661 var ileft: i64 = content_origin_x
2662 if fl_side != 0 { if pen.pen_y < fl_bottom {
2663 iaw = child_avail_w - fl_w
2664 if iaw < 0 { iaw = 0 }
2665 if fl_side == 1 { ileft = content_origin_x + fl_w }
2666 } }
2667 pen.avail_w = iaw
2668 pen.origin_x = ileft
2669 _layout_inline_flow(ctx, pen, child_idx)
2670 if fl_side != 0 { if pen.pen_y >= fl_bottom { fl_side = 0; fl_w = 0 } }
2671 }
2672 child_idx = c.next_sibling_idx
2673 }
2674 }
2675 }
2676 if pen.pen_x > 0 {
2677 pen.pen_y = pen.pen_y + pen.line_h
2678 pen.pen_x = 0
2679 pen.line_h = 0
2680 }
2681 children_h = pen.pen_y
2682 if float_extent > children_h { children_h = float_extent } // contain a float taller than the text
2683 }
2684 }
2685 // leaving a table: restore the enclosing table's column context (nested-table correctness)
2686 if tbl_entered == 1 {
2687 l_tbl_active = tbl_saved_active
2688 l_tbl_idx = tbl_saved_idx
2689 l_tbl_ncols = tbl_saved_ncols
2690 l_tbl_colw_p = tbl_saved_p
2691 }
2692
2693 // Content height: explicit `height` declaration wins, else
2694 // sum of children heights.
2695 var content_h: i64 = children_h
2696 if h_raw >= 0 { content_h = h_raw }
2697 // IMAGE REFLOW: reserve the DECODED intrinsic height when it is known (pass 2 only). An explicit
2698 // CSS height still wins -- this is a FLOOR for the childless <img> case that otherwise resolves
2699 // to 0, not an override of the cascade.
2700 if h_raw < 0 {
2701 let iih: i64 = _layout_img_h(box_idx)
2702 if iih > content_h { content_h = iih }
2703 }
2704
2705 b.h = pt + content_h + pb
2706
2707 return mt + b.h + mb
2708}
2709
2710// ---- public API ----
2711
2712func nx_layout_block_layout(ctx: *LayoutCtx, table: *LayoutPropTable,
2713 root_idx: i64) -> i64 {
2714 if root_idx < 0 { return -1 }
2715 if root_idx >= ctx.tree.count { return -1 }
2716 let outer_h: i64 = _layout_block_recurse(ctx, table, root_idx,
2717 ctx.viewport_w, 0, 0)
2718 return outer_h
2719}
2720
2721func nx_layout_ctx_init(ctx: *LayoutCtx, tree: *LayoutTree,
2722 computed: *CssComputedDecl, n_computed: i64,
2723 src: *u8, viewport_w: i64,
2724 resolve_ctx: *CssResolveCtx) -> i64 {
2725 ctx.tree = tree
2726 ctx.computed = computed
2727 ctx.n_computed = n_computed
2728 ctx.src = src
2729 ctx.viewport_w = viewport_w
2730 ctx.resolve_ctx = resolve_ctx
2731 ctx.text_measured = 0 // legacy monospace cells unless the consumer opts in (additive)
2732 ctx.font_tab = 0
2733 return 0
2734}