code wiki / (root) / nx_layout_block.nx

nx_layout_block.nx source

↩ module page · 2734 lines · 138056 B

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