code wiki / (root) / nx_layout_block.nx

nx_layout_block.nx source

↩ module page · 2503 lines · 124638 B

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