code wiki / _hdl_build / nx_browser_render.nx
nx_browser_render.nx source
↩ module page · 1482 lines · 81039 B
1// nx_browser_render.nx -- REUSABLE sovereign CSS render core. Holds nx_browser's OS-AGNOSTIC pipeline:
2// br_layout (HTML body -> CSS cascade -> box/flex/float layout -> LayoutTree + computed styles) and
3// br_draw_fb / br_shot_png (paint the laid-out page into an RGBA framebuffer / PNG). NO X11, NO network --
4// pure compute over sys_mmap, so it compiles into the native-Windows GUI PE exactly as into the WSL/X11
5// browser. The window/blit/input + fetch live in the consumers. CSS-on-native-Windows arc R2.
6// NOTE (R3 dedup pending): these fns are still ALSO defined in runtime/_hdl_build/nx_browser.nx; once this
7// organ is proven, nx_browser.nx imports this and drops its copies. license_tier: ORIGINAL
8import "nx_syscalls.nx"
9import "nx_font8x8.nx"
10import "nx_html_tokenizer.nx"
11import "nx_dom_query.nx"
12import "nx_html_entities.nx"
13import "nx_render_html.nx"
14import "nx_css_color_decode.nx"
15import "nx_layout_box.nx"
16import "nx_layout_default_display.nx"
17import "nx_layout_from_dom.nx"
18import "nx_layout_block.nx"
19import "nx_paint_solid_rect.nx"
20import "nx_paint_text.nx"
21import "nx_png_write.nx"
22import "nx_font.nx" // sovereign stroke-VECTOR font (modern heading typeface) + metrics
23import "nx_aa_raster.nx" // AA contour rasterizer for the vector glyphs
24
25struct Page {
26 buf: *u8,
27 tree: *LayoutTree,
28 computed: *CssComputedDecl,
29 ncomp: i64,
30 page_h: i64,
31 bhref_off: *i64, // per box_idx: href byte-offset into buf, or -1
32 bhref_len: *i64,
33 ok: i64,
34 raw: *u8, // fetched response body (kept so resize can re-layout WITHOUT re-fetching)
35 raw_len: i64,
36 bsrc_off: *i64, // per box_idx: <img src> byte-offset into buf, or -1
37 bsrc_len: *i64,
38 bimg: *i64, // per box_idx: decoded RGB (w*h*3) pointer, or 0 (filled by the consumer)
39 bimg_w: *i64, // per box_idx: decoded image width
40 bimg_h: *i64, // per box_idx: decoded image height
41 find_ptr: i64, // FIND-IN-PAGE: search-term bytes (as i64 ptr), 0 = no active find
42 find_len: i64, // search-term length
43 vec_headings: i64, // 1 = paint headings with the sovereign stroke-VECTOR font (modern typeface);
44 // 0 = bitmap (default; native-GUI 60fps stays alloc-free). Set by once-render organs.
45 dark_mode: i64, // 1 = dark reader theme (the OLED look): dark bg + light text default stylesheet.
46 // Set BEFORE br_layout (it selects the injected default CSS). 0 = light (default).
47 bcanvas: *i64 // per box_idx: 1 = <canvas> -- a FIRST-CLASS SOURCELESS render surface (seq103):
48 // the consumer fills page.bimg[i]/bimg_w/bimg_h each FRAME and repaints; the
49 // image pass blits the CURRENT buffer at the laid-out box (the game substrate).
50}
51const NX_PAGE_BYTES: i64 = 160
52const NX_CHROME_H: i64 = 44 // browser chrome (toolbar + address bar) height; page viewport is below it
53
54func br_slen(s: *u8) -> i64 { var k: i64=0; while s[k]!=(0 as u8){k=k+1} return k }
55
56// ---- PER-BOX COMPUTED-DECL INDEX (the eagler paint-cliff fix): br_comp_* used to scan ALL ncomp decls
57// per lookup, per box, per ancestor hop -- O(boxes x ancestors x ncomp) exploded on Next.js-scale CSS
58// (layout <20s, paint >170s). br_cidx_build buckets the computed array BY ELEMENT (stable copy = cascade
59// order preserved within each element, so reverse scan = the same last-wins winner), and every br_comp_*
60// then scans only that box's bucket. OFF by default (statics 0) -> gates with hand-built arrays are
61// byte-identical; br_layout switches it off during its run and rebuilds at the end. ----
62static br_cidx_on: i64
63static br_cidx_arr: i64 // *CssComputedDecl grouped copy (alloc once, reused)
64static br_cidx_first: i64 // *i64 per box: bucket start
65static br_cidx_cnt: i64 // *i64 per box: bucket len
66static br_cidx_cur: i64 // *i64 scratch cursor
67static br_cidx_nbox: i64
68static br_cidx_src: i64 // the computed-array pointer the index was built FROM: every window guard
69 // checks it, so querying a DIFFERENT page's array (multi-page gates) falls
70 // back to the full scan instead of reading a stale index (self-validating)
71
72func br_cidx_build(tree: *LayoutTree, computed: *CssComputedDecl, ncomp: i64) -> i64 {
73 let nbox: i64 = tree.count
74 if br_cidx_arr == 0 {
75 br_cidx_arr = sys_mmap((NX_CSS_COMPUTED_DECL_BYTES * 262144) as nx_size) as i64
76 br_cidx_first = sys_mmap((16384 * 8) as nx_size) as i64
77 br_cidx_cnt = sys_mmap((16384 * 8) as nx_size) as i64
78 br_cidx_cur = sys_mmap((16384 * 8) as nx_size) as i64
79 }
80 if nbox > 16384 { br_cidx_on = 0; return 0 }
81 if ncomp > 262144 { br_cidx_on = 0; return 0 }
82 let ff: *i64 = br_cidx_first as *i64
83 let cc: *i64 = br_cidx_cnt as *i64
84 let cu: *i64 = br_cidx_cur as *i64
85 var i: i64 = 0
86 while i < nbox { cc[i] = 0; i = i + 1 }
87 var j: i64 = 0
88 while j < ncomp {
89 let cd: *CssComputedDecl = ((computed as i64) + j * NX_CSS_COMPUTED_DECL_BYTES) as *CssComputedDecl
90 let e: i64 = cd.element_idx
91 if e >= 0 { if e < nbox { cc[e] = cc[e] + 1 } }
92 j = j + 1
93 }
94 var acc: i64 = 0
95 i = 0
96 while i < nbox { ff[i] = acc; cu[i] = acc; acc = acc + cc[i]; i = i + 1 }
97 // stable grouped copy (struct copy field-by-field; 64B decls)
98 let dst0: i64 = br_cidx_arr
99 j = 0
100 while j < ncomp {
101 let cd2: *CssComputedDecl = ((computed as i64) + j * NX_CSS_COMPUTED_DECL_BYTES) as *CssComputedDecl
102 let e2: i64 = cd2.element_idx
103 if e2 >= 0 { if e2 < nbox {
104 let dp: *CssComputedDecl = (dst0 + cu[e2] * NX_CSS_COMPUTED_DECL_BYTES) as *CssComputedDecl
105 dp.element_idx = cd2.element_idx
106 dp.prop_off = cd2.prop_off
107 dp.prop_len = cd2.prop_len
108 dp.val_kind = cd2.val_kind
109 dp.val_off = cd2.val_off
110 dp.val_len = cd2.val_len
111 dp.unit_off = cd2.unit_off
112 dp.unit_len = cd2.unit_len
113 cu[e2] = cu[e2] + 1
114 } }
115 j = j + 1
116 }
117 br_cidx_nbox = nbox
118 br_cidx_src = computed as i64
119 br_cidx_on = 1
120 return 0
121}
122func br_comp_int(src: *u8, computed: *CssComputedDecl, ncomp: i64, box_idx: i64, prop: *u8, plen: i64, dflt: i64) -> i64 {
123 var base: i64 = computed as i64
124 var lo: i64 = 0
125 var hi: i64 = ncomp
126 if br_cidx_on == 1 { if (computed as i64) == br_cidx_src { if box_idx >= 0 { if box_idx < br_cidx_nbox {
127 let ff: *i64 = br_cidx_first as *i64
128 let cc: *i64 = br_cidx_cnt as *i64
129 base = br_cidx_arr
130 lo = ff[box_idx]
131 hi = lo + cc[box_idx]
132 } } } }
133 var j: i64 = hi - 1
134 while j >= lo {
135 let cd: *CssComputedDecl = (base + j * NX_CSS_COMPUTED_DECL_BYTES) as *CssComputedDecl
136 if cd.element_idx == box_idx { if cd.prop_len == plen {
137 var ok: i64 = 1; var k: i64 = 0
138 while k < plen { if (src[cd.prop_off+k]&0xff) != (prop[k]&0xff) { ok=0; k=plen } else { k=k+1 } }
139 if ok == 1 {
140 // integer-truncate at the decimal point ("13.5px" is 13, NOT 135 -- the same skip-the-dot
141 // digit-accumulator bug fixed in _layout_fontsize_decl_px; measure==paint requires both)
142 var v: i64 = 0; var any: i64 = 0; var i: i64 = 0
143 while i < cd.val_len {
144 let c: i64 = src[cd.val_off+i]&0xff
145 if c == 46 { i = cd.val_len }
146 else { if c>=48 { if c<=57 { v=v*10+(c-48); any=1 } } i=i+1 }
147 }
148 if any == 1 { return v }
149 return dflt
150 }
151 } }
152 j = j - 1
153 }
154 return dflt
155}
156func br_comp_color(src: *u8, computed: *CssComputedDecl, ncomp: i64, box_idx: i64, prop: *u8, plen: i64) -> i64 {
157 var base: i64 = computed as i64
158 var lo: i64 = 0
159 var hi: i64 = ncomp
160 if br_cidx_on == 1 { if (computed as i64) == br_cidx_src { if box_idx >= 0 { if box_idx < br_cidx_nbox {
161 let ff: *i64 = br_cidx_first as *i64
162 let cc: *i64 = br_cidx_cnt as *i64
163 base = br_cidx_arr
164 lo = ff[box_idx]
165 hi = lo + cc[box_idx]
166 } } } }
167 var j: i64 = hi - 1
168 while j >= lo {
169 let cd: *CssComputedDecl = (base + j * NX_CSS_COMPUTED_DECL_BYTES) as *CssComputedDecl
170 if cd.element_idx == box_idx { if cd.prop_len == plen {
171 var ok: i64 = 1; var k: i64 = 0
172 while k < plen { if (src[cd.prop_off+k]&0xff) != (prop[k]&0xff) { ok=0; k=plen } else { k=k+1 } }
173 if ok == 1 {
174 let col: *CssColor = (sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
175 var got: i64 = 0
176 if cd.val_kind == NX_CSS_VAL_HEXCOLOR { got = nx_css_color_decode(src, cd.val_off, cd.val_len, col) }
177 else { if cd.val_kind == NX_CSS_VAL_IDENT { got = nx_css_color_named(src, cd.val_off, cd.val_len, col); if got != 1 { got = nx_css_color_rgb_func(src, cd.val_off, cd.val_len, col) } } }
178 if got == 1 { if col.a >= 128 { return (col.r << 16) | (col.g << 8) | col.b } } // SOLID painter: <50% alpha = tint, not paint (linkedin rgba(0,0,0,0.08) painted BLACK, 2026-07-28)
179 return 0 - 1
180 }
181 } }
182 j = j - 1
183 }
184 return 0 - 1
185}
186
187// INHERITED color resolve (CSS: `color` inherits): walk up the parent chain to the nearest box with a
188// computed color decl. Without this, author colors on body/.doc never reached p/li/td TEXT (the UA sheet
189// only "worked" because it fakes inheritance with explicit p{color}/li{color} rules -- measured 07-09).
190// FONT-SIZE resolver for one element: matches `font-size` (integer-truncating parse) OR the `font`
191// SHORTHAND when the value leads with a size ("15px/1.55 system-ui") -- one bucketed reverse scan across
192// both = last-wins cascade between them. MIRRORS _layout_fontsize_decl_px exactly (measure==paint law).
193// `pct_out[0]` <- 1 when the declared value is a PERCENTAGE; the return is then the percentage
194// number itself, which br_comp_fontsize_inh resolves against the parent. Mirrors the layout-side
195// _layout_fontsize_decl_px signature exactly (measure==paint law: if only one side learned about
196// percentages, every styled run would overflow its reserved box and split mid-word).
197func br_comp_fontsize_own(src: *u8, computed: *CssComputedDecl, ncomp: i64, box_idx: i64, pct_out: *i64) -> i64 {
198 var base: i64 = computed as i64
199 var lo: i64 = 0
200 var hi: i64 = ncomp
201 if br_cidx_on == 1 { if (computed as i64) == br_cidx_src { if box_idx >= 0 { if box_idx < br_cidx_nbox {
202 let ff: *i64 = br_cidx_first as *i64
203 let cc: *i64 = br_cidx_cnt as *i64
204 base = br_cidx_arr
205 lo = ff[box_idx]
206 hi = lo + cc[box_idx]
207 } } } }
208 var j: i64 = hi - 1
209 while j >= lo {
210 let cd: *CssComputedDecl = (base + j * NX_CSS_COMPUTED_DECL_BYTES) as *CssComputedDecl
211 if cd.element_idx == box_idx {
212 if cd.prop_len == 4 { if br_prop_eq(src, cd.prop_off, "font\x00" as *u8, 4) == 1 {
213 let c0: i64 = (src[cd.val_off] as i64) & 255
214 if c0 >= 48 { if c0 <= 57 {
215 var v4: i64 = 0
216 var j4: i64 = 0
217 var g4: i64 = 1
218 while g4 == 1 {
219 if j4 >= cd.val_len { g4 = 0 }
220 else {
221 let c4: i64 = (src[cd.val_off + j4] as i64) & 255
222 if c4 >= 48 { if c4 <= 57 { v4 = v4 * 10 + (c4 - 48); j4 = j4 + 1 } else { g4 = 0 } } else { g4 = 0 }
223 }
224 }
225 if v4 > 0 { return v4 }
226 } }
227 } }
228 if cd.prop_len == 9 { if br_prop_eq(src, cd.prop_off, "font-size\x00" as *u8, 9) == 1 {
229 // ONE value resolver, shared with the measure side (nx_fontsize_value_px lives in
230 // nx_layout_block.nx which this file imports): px/%/em/rem/fractions, calc()/keywords
231 // dropped (-1) -> fall through to the older cascade decl. measure==paint BY CONSTRUCTION.
232 let fv: i64 = nx_fontsize_value_px(src, cd.val_off, cd.val_len, cd.unit_off, cd.unit_len, pct_out)
233 if fv >= 0 { return fv }
234 } }
235 }
236 j = j - 1
237 }
238 return 0 - 1
239}
240// LINE-HEIGHT resolver (paint mirror of _layout_lineheight_decl_px/_layout_lineheight_px -- measure==paint
241// law): `line-height: 1.55` unitless multiplier of fs / `line-height: 24px` / the `font` shorthand's /N.
242// Bucketed reverse scan per element; nearest ancestor with a decl wins. Returns px or -1.
243func br_comp_lineheight_own(src: *u8, computed: *CssComputedDecl, ncomp: i64, box_idx: i64, fs: i64) -> i64 {
244 var base: i64 = computed as i64
245 var lo: i64 = 0
246 var hi: i64 = ncomp
247 if br_cidx_on == 1 { if (computed as i64) == br_cidx_src { if box_idx >= 0 { if box_idx < br_cidx_nbox {
248 let ff: *i64 = br_cidx_first as *i64
249 let cc: *i64 = br_cidx_cnt as *i64
250 base = br_cidx_arr
251 lo = ff[box_idx]
252 hi = lo + cc[box_idx]
253 } } } }
254 var j: i64 = hi - 1
255 while j >= lo {
256 let cd: *CssComputedDecl = (base + j * NX_CSS_COMPUTED_DECL_BYTES) as *CssComputedDecl
257 if cd.element_idx == box_idx {
258 if cd.prop_len == 11 { if br_prop_eq(src, cd.prop_off, "line-height\x00" as *u8, 11) == 1 {
259 var nl: i64 = 0
260 var ns: i64 = 1
261 while ns == 1 {
262 if nl >= cd.val_len { ns = 0 }
263 else {
264 let c: i64 = (src[cd.val_off + nl] as i64) & 255
265 var isn: i64 = 0
266 if c >= 48 { if c <= 57 { isn = 1 } }
267 if c == 46 { isn = 1 }
268 if isn == 1 { nl = nl + 1 } else { ns = 0 }
269 }
270 }
271 if nl == 0 { return 0 - 1 }
272 let q3: i64 = nx_css_number_parse(src, cd.val_off, nl)
273 if q3 == NX_CSS_NUMBER_PARSE_ERROR { return 0 - 1 }
274 // px unit: DIMENSION tokens carry it in the SEPARATE unit fields (val = digits only);
275 // swept spans carry it inline after the digits. Check both. Unitless requires the value
276 // to be EXACTLY the number (no trailing unit we don't understand -> miss, not 16x).
277 if cd.unit_len == 2 {
278 if ((src[cd.unit_off] as i64) & 255) == 112 { if ((src[cd.unit_off + 1] as i64) & 255) == 120 {
279 return q3 / 1000
280 } }
281 }
282 if nl + 1 < cd.val_len {
283 if ((src[cd.val_off + nl] as i64) & 255) == 112 { if ((src[cd.val_off + nl + 1] as i64) & 255) == 120 {
284 return q3 / 1000
285 } }
286 }
287 if cd.unit_len == 0 { if nl == cd.val_len { return (q3 * fs) / 1000 } }
288 return 0 - 1
289 } }
290 if cd.prop_len == 4 { if br_prop_eq(src, cd.prop_off, "font\x00" as *u8, 4) == 1 {
291 let c0: i64 = (src[cd.val_off] as i64) & 255
292 if c0 >= 48 { if c0 <= 57 {
293 var j2: i64 = 0
294 var sl: i64 = 0 - 1
295 var g: i64 = 1
296 while g == 1 {
297 if j2 >= cd.val_len { g = 0 }
298 else {
299 let cj: i64 = (src[cd.val_off + j2] as i64) & 255
300 if cj == 47 { sl = j2; g = 0 }
301 else { if cj == 32 { g = 0 } else { j2 = j2 + 1 } }
302 }
303 }
304 if sl >= 0 {
305 let vo: i64 = cd.val_off + sl + 1
306 let vmax: i64 = cd.val_len - sl - 1
307 var n2: i64 = 0
308 var g2: i64 = 1
309 while g2 == 1 {
310 if n2 >= vmax { g2 = 0 }
311 else {
312 let c2: i64 = (src[vo + n2] as i64) & 255
313 var isn2: i64 = 0
314 if c2 >= 48 { if c2 <= 57 { isn2 = 1 } }
315 if c2 == 46 { isn2 = 1 }
316 if isn2 == 1 { n2 = n2 + 1 } else { g2 = 0 }
317 }
318 }
319 if n2 > 0 {
320 let q32: i64 = nx_css_number_parse(src, vo, n2)
321 if q32 != NX_CSS_NUMBER_PARSE_ERROR {
322 if n2 + 1 < vmax {
323 if ((src[vo + n2] as i64) & 255) == 112 { if ((src[vo + n2 + 1] as i64) & 255) == 120 {
324 return q32 / 1000
325 } }
326 }
327 return (q32 * fs) / 1000
328 }
329 }
330 }
331 } }
332 } }
333 }
334 j = j - 1
335 }
336 return 0 - 1
337}
338func br_comp_lineheight_px(src: *u8, computed: *CssComputedDecl, ncomp: i64, tree: *LayoutTree, box_idx: i64, fs: i64) -> i64 {
339 var cur: i64 = box_idx
340 var guard: i64 = 0
341 while cur >= 0 {
342 if guard > 64 { return 0 - 1 }
343 guard = guard + 1
344 let v: i64 = br_comp_lineheight_own(src, computed, ncomp, cur, fs)
345 if v >= 0 { return v }
346 let b: *LayoutBox = ((tree.boxes as i64) + cur * NX_LAYOUT_BOX_BYTES) as *LayoutBox
347 cur = b.parent_idx
348 }
349 return 0 - 1
350}
351// INHERITED font-size: nearest ancestor with a font-size OR font-shorthand decl, else dflt.
352func br_comp_fontsize_inh(src: *u8, computed: *CssComputedDecl, ncomp: i64, tree: *LayoutTree, box_idx: i64, dflt: i64) -> i64 {
353 let pct: *i64 = sys_mmap(16) as *i64
354 var cur: i64 = box_idx
355 var guard: i64 = 0
356 while cur >= 0 {
357 if guard > 64 { return dflt }
358 guard = guard + 1
359 pct[0] = 0
360 let v: i64 = br_comp_fontsize_own(src, computed, ncomp, cur, pct)
361 if v >= 0 {
362 if pct[0] == 0 { return v }
363 let bp: *LayoutBox = ((tree.boxes as i64) + cur * NX_LAYOUT_BOX_BYTES) as *LayoutBox
364 let parent_px: i64 = br_comp_fontsize_inh(src, computed, ncomp, tree, bp.parent_idx, dflt)
365 var r: i64 = (parent_px * v) / 100
366 if r < 1 { r = 1 }
367 return r
368 }
369 let b: *LayoutBox = ((tree.boxes as i64) + cur * NX_LAYOUT_BOX_BYTES) as *LayoutBox
370 cur = b.parent_idx
371 }
372 return dflt
373}
374func br_comp_color_inh(src: *u8, computed: *CssComputedDecl, ncomp: i64, tree: *LayoutTree, box_idx: i64) -> i64 {
375 var cur: i64 = box_idx
376 var guard: i64 = 0
377 while cur >= 0 {
378 if guard > 64 { return 0 - 1 }
379 guard = guard + 1
380 let c: i64 = br_comp_color(src, computed, ncomp, cur, "color\x00" as *u8, 5)
381 if c >= 0 { return c }
382 let b: *LayoutBox = ((tree.boxes as i64) + cur * NX_LAYOUT_BOX_BYTES) as *LayoutBox
383 cur = b.parent_idx
384 }
385 return 0 - 1
386}
387
388// background resolve: background-color, else the `background` SHORTHAND's hex value (a single-hex shorthand
389// like background:#eef1f5 tokenizes as HEXCOLOR and decodes; complex shorthands fail closed to -1). bg does
390// NOT inherit in CSS, so no parent walk.
391func br_prop_eq(src: *u8, off: i64, lit: *u8, len: i64) -> i64 {
392 var k: i64 = 0
393 while k < len { if (src[off+k]&0xff) != (lit[k]&0xff) { return 0 } k=k+1 }
394 return 1
395}
396// S21 fix: background must respect CASCADE ORDER across BOTH `background-color` and the `background` shorthand.
397// The old version tried `background-color` first -> the UA sheet's body{background-color:#fff} ALWAYS beat an
398// author body{background:var(--bg)} shorthand -> our dark-theme pages rendered light-text-on-white (invisible).
399// Now: scan from the END (highest cascade) for the LAST decl of EITHER property and decode that.
400func br_comp_bg(src: *u8, computed: *CssComputedDecl, ncomp: i64, box_idx: i64) -> i64 {
401 var base: i64 = computed as i64
402 var lo: i64 = 0
403 var hi: i64 = ncomp
404 if br_cidx_on == 1 { if (computed as i64) == br_cidx_src { if box_idx >= 0 { if box_idx < br_cidx_nbox {
405 let ff: *i64 = br_cidx_first as *i64
406 let cc: *i64 = br_cidx_cnt as *i64
407 base = br_cidx_arr
408 lo = ff[box_idx]
409 hi = lo + cc[box_idx]
410 } } } }
411 var j: i64 = hi - 1
412 while j >= lo {
413 let cd: *CssComputedDecl = (base + j * NX_CSS_COMPUTED_DECL_BYTES) as *CssComputedDecl
414 if cd.element_idx == box_idx {
415 var isbg: i64 = 0
416 if cd.prop_len == 16 { if br_prop_eq(src, cd.prop_off, "background-color\x00" as *u8, 16)==1 { isbg=1 } }
417 if isbg == 0 { if cd.prop_len == 10 { if br_prop_eq(src, cd.prop_off, "background\x00" as *u8, 10)==1 { isbg=1 } } }
418 if isbg == 1 {
419 let col: *CssColor = (sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
420 var got: i64 = 0
421 if cd.val_kind == NX_CSS_VAL_HEXCOLOR { got = nx_css_color_decode(src, cd.val_off, cd.val_len, col) }
422 else { if cd.val_kind == NX_CSS_VAL_IDENT { got = nx_css_color_named(src, cd.val_off, cd.val_len, col); if got != 1 { got = nx_css_color_rgb_func(src, cd.val_off, cd.val_len, col) } } }
423 if got == 1 { if col.a >= 128 { return (col.r << 16) | (col.g << 8) | col.b } } // SOLID painter: <50% alpha = tint, not paint (linkedin rgba(0,0,0,0.08) painted BLACK, 2026-07-28)
424 return 0 - 1
425 }
426 }
427 j = j - 1
428 }
429 return 0 - 1
430}
431
432// `border` SHORTHAND resolve: the computed value is a swept span like "1px solid #cbd5e1" -- parse the
433// leading width int + the #hex color out of the span. Returns the color (wout[0]=width px, clamped 1..8)
434// or -1 when the box has no parseable border. (border-style variants beyond solid paint as solid -- v1.)
435func br_comp_border(src: *u8, computed: *CssComputedDecl, ncomp: i64, box_idx: i64, wout: *i64) -> i64 {
436 var base: i64 = computed as i64
437 var lo: i64 = 0
438 var hi: i64 = ncomp
439 if br_cidx_on == 1 { if (computed as i64) == br_cidx_src { if box_idx >= 0 { if box_idx < br_cidx_nbox {
440 let ff: *i64 = br_cidx_first as *i64
441 let cc: *i64 = br_cidx_cnt as *i64
442 base = br_cidx_arr
443 lo = ff[box_idx]
444 hi = lo + cc[box_idx]
445 } } } }
446 var j: i64 = hi - 1
447 while j >= lo {
448 let cd: *CssComputedDecl = (base + j * NX_CSS_COMPUTED_DECL_BYTES) as *CssComputedDecl
449 if cd.element_idx == box_idx { if cd.prop_len == 6 {
450 let P: *u8 = "border\x00" as *u8
451 var ok: i64 = 1
452 var k: i64 = 0
453 while k < 6 { if (src[cd.prop_off+k]&0xff) != (P[k]&0xff) { ok=0; k=6 } else { k=k+1 } }
454 if ok == 1 {
455 var wv: i64 = 0
456 var i2: i64 = cd.val_off
457 let e2: i64 = cd.val_off + cd.val_len
458 while i2 < e2 { let ch: i64 = src[i2]&0xff; if ch>=48 { if ch<=57 { wv=wv*10+(ch-48); i2=i2+1 } else { i2=e2 } } else { i2=e2 } }
459 if wv <= 0 { wv = 1 }
460 if wv > 8 { wv = 8 }
461 var hp: i64 = 0 - 1
462 var i3: i64 = cd.val_off
463 while i3 < e2 { if src[i3]==(35 as u8) { hp=i3; i3=e2 } else { i3=i3+1 } }
464 if hp >= 0 {
465 let col: *CssColor = (sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
466 if nx_css_color_decode(src, hp+1, 6, col) == 1 {
467 wout[0] = wv
468 return (col.r << 16) | (col.g << 8) | col.b
469 }
470 }
471 return 0 - 1
472 }
473 } }
474 j = j - 1
475 }
476 return 0 - 1
477}
478
479// Extract elements (tag/id/class) AND per-element href, mirroring rh_extract's chrome-skip EXACTLY (so the
480// element order == the layout box order). ehref_off/ehref_len get the <a href> span, or -1.
481func br_extract(html: *u8, hlen: i64, elements: *CssElement, ehref_off: *i64, ehref_len: *i64, esrc_off: *i64, esrc_len: *i64, maxn: i64) -> i64 {
482 let c: *HtmlCursor=sys_mmap(24) as *HtmlCursor; nx_html_cursor_init(c,html,hlen)
483 let tok: *HtmlToken=sys_mmap(56) as *HtmlToken
484 let ao: *i64=sys_mmap(8) as *i64; let al: *i64=sys_mmap(8) as *i64; let bo: *i64=sys_mmap(8) as *i64; let bl: *i64=sys_mmap(8) as *i64
485 let ho: *i64=sys_mmap(8) as *i64; let hl: *i64=sys_mmap(8) as *i64
486 var n: i64=0
487 var cskip: i64=0
488 while 1==1 {
489 nx_html_next_token(c,tok)
490 if tok.kind==NX_HTML_TOK_EOF { return n }
491 if n>=maxn { return n }
492 var ie: i64=0
493 if cskip > 0 {
494 // void START tags never get an END -- exempt from depth (MIRRORS nx_layout_from_dom exactly)
495 if tok.kind==NX_HTML_TOK_START_TAG { if _lfd_is_void_tag(html, tok.name_off, tok.name_len) == 0 { cskip = cskip + 1 } }
496 if tok.kind==NX_HTML_TOK_END_TAG { cskip = cskip - 1 }
497 } else {
498 if tok.kind==NX_HTML_TOK_START_TAG {
499 if _lfd_is_chrome_start(html, tok.name_off, tok.name_len, tok.src_off, tok.src_len) == 1 { cskip = 1 }
500 else { ie = 1 }
501 }
502 if tok.kind==NX_HTML_TOK_SELF_CLOSING { ie = 1 }
503 }
504 // RAW-TEXT bodies (script/style/textarea/title) must be consumed with the HTML5 raw-text
505 // scanner, EXACTLY as nx_layout_from_dom does -- br_extract was the consumer that forgot.
506 // Without this, tag-like text inside a JS string ("<div class=...>") mints PHANTOM elements
507 // here that from_dom never boxed, and the positional box<->element pairing in br_layout
508 // SKEWS at the first such script: every later box wears the WRONG identity (wrong classes ->
509 // wrong widths/heights/colors). Control-probe proven 2026-07-28: one phantom-bearing script
510 // before a div shifted even <body> off its own UA padding (x=44 -> 0). The k-map's two
511 // walkers must see the SAME element stream, so this mirror is a CORRECTNESS invariant.
512 if tok.kind==NX_HTML_TOK_START_TAG {
513 if nx_html_is_raw_text_tag(html, tok.name_off, tok.name_len) == 1 {
514 let rawtok: *HtmlToken = sys_mmap(56) as *HtmlToken
515 nx_html_consume_raw_text(c, ((html as i64) + tok.name_off) as *u8, tok.name_len, rawtok)
516 // cursor is left AT the close tag (tokenizer contract) -- the arriving END_TAG token
517 // balances cskip naturally, so NO depth fixup here (a fixup would double-decrement).
518 }
519 }
520 if ie==1 {
521 let e: *CssElement=((elements as i64)+n*NX_CSS_ELEMENT_BYTES) as *CssElement
522 e.src=html; e.tag_off=tok.name_off; e.tag_len=tok.name_len
523 if nx_dom_find_attr(html,tok.src_off,tok.src_len,"id\x00" as *u8,ao,al)==1 { e.id_off=ao[0]; e.id_len=al[0] } else { e.id_off=0; e.id_len=0 }
524 if nx_dom_find_attr(html,tok.src_off,tok.src_len,"class\x00" as *u8,bo,bl)==1 { e.class_off=bo[0]; e.class_len=bl[0] } else { e.class_off=0; e.class_len=0 }
525 ehref_off[n] = 0 - 1; ehref_len[n] = 0 - 1
526 if tok.name_len==1 { if (html[tok.name_off]&0xff)==97 { // <a>
527 if nx_dom_find_attr(html,tok.src_off,tok.src_len,"href\x00" as *u8,ho,hl)==1 { ehref_off[n]=ho[0]; ehref_len[n]=hl[0] }
528 } }
529 esrc_off[n] = 0 - 1; esrc_len[n] = 0 - 1
530 if tok.name_len==3 { if (html[tok.name_off]&0xff)==105 { if (html[tok.name_off+1]&0xff)==109 { if (html[tok.name_off+2]&0xff)==103 { // <img>
531 if nx_dom_find_attr(html,tok.src_off,tok.src_len,"src\x00" as *u8,ho,hl)==1 { esrc_off[n]=ho[0]; esrc_len[n]=hl[0] }
532 } } } }
533 n=n+1
534 }
535 }
536 return n
537}
538
539// layout-only pass: (re)build the style buffer + lay out page.raw at viewport width `vw`.
540// Split from the fetch so a window RESIZE re-lays-out at the new width with NO network round-trip.
541func br_layout(page: *Page, vw: i64) -> i64 {
542 page.ok = 0
543 br_cidx_on = 0 // index describes the PREVIOUS layout's array -- off until rebuilt below
544 let body: *u8 = page.raw
545 let body_len: i64 = page.raw_len
546
547 // UA sheet note (07-09): headings + p/li carry NO color -- like real UA sheets they INHERIT body color
548 // (br_comp_color_inh), so author header{color:#fff} / body{color:...} reach them. body keeps the default.
549 var css: *u8 = "h1{margin-top:30px;margin-bottom:16px;font-size:40px}h2{margin-top:28px;margin-bottom:12px;font-size:28px}h3{margin-top:20px;margin-bottom:8px;font-size:22px}body{background-color:#ffffff;color:#2a2a33;padding-left:44px;padding-right:44px;padding-top:22px}p{margin-top:14px;margin-bottom:14px}ul{padding-left:20px;margin-top:6px;margin-bottom:6px}ol{padding-left:28px;margin-top:6px;margin-bottom:6px}li{margin-bottom:5px}table{display:table}tr{display:flex}th{font-weight:bold;padding-left:4px;padding-right:4px}td{padding-left:4px;padding-right:4px}a{color:#2563eb}b{font-weight:bold}strong{font-weight:bold}i{font-style:italic}em{font-style:italic}.vector-menu-content-list{display:none}.vector-dropdown{display:none}.mw-interlanguage-selector{display:none}.wbc-editpage{display:none}.vector-toc{display:none}.vector-toc-list{display:none}.vector-toc-list-item{display:none}.vector-toc-link{display:none}.vector-page-toolbar{display:none}.infobox{float:right;width:300px;margin-left:8px;background-color:#f7f7fa}.nishi-buyguard{background-color:#fff3cd;color:#332701;padding-left:14px;padding-right:14px;padding-top:8px;padding-bottom:8px;margin-bottom:10px;font-size:18px}\x00"
550 // DARK READER THEME (the OLED look): near-black bg + light text; same structure so layout is identical.
551 if page.dark_mode == 1 {
552 css = "h1{margin-top:30px;margin-bottom:16px;font-size:40px}h2{margin-top:28px;margin-bottom:12px;font-size:28px}h3{margin-top:20px;margin-bottom:8px;font-size:22px}body{background-color:#0e1116;color:#d4dae3;padding-left:44px;padding-right:44px;padding-top:22px}p{margin-top:14px;margin-bottom:14px}ul{padding-left:20px;margin-top:6px;margin-bottom:6px}ol{padding-left:28px;margin-top:6px;margin-bottom:6px}li{margin-bottom:5px}table{display:table}tr{display:flex}th{font-weight:bold;padding-left:4px;padding-right:4px}td{padding-left:4px;padding-right:4px}a{color:#6aa3ff}b{font-weight:bold}strong{font-weight:bold}i{font-style:italic}em{font-style:italic}.vector-menu-content-list{display:none}.vector-dropdown{display:none}.mw-interlanguage-selector{display:none}.wbc-editpage{display:none}.vector-toc{display:none}.vector-toc-list{display:none}.vector-toc-list-item{display:none}.vector-toc-link{display:none}.vector-page-toolbar{display:none}.infobox{float:right;width:300px;margin-left:8px;background-color:#1a1f28}.nishi-buyguard{background-color:#2a2410;color:#ffe08a;padding-left:14px;padding-right:14px;padding-top:8px;padding-bottom:8px;margin-bottom:10px;font-size:18px}\x00"
553 }
554 let clen: i64 = br_slen(css)
555 let STYCAP: i64 = 262144
556 let buf: *u8 = sys_mmap(body_len + STYCAP + clen + 16)
557 var bi: i64 = 0
558 while bi < body_len { buf[bi]=body[bi]; bi=bi+1 }
559 // CASCADE ORIGIN ORDER (07-09 root fix): UA defaults FIRST, author <style> AFTER -- the resolvers take
560 // the LAST matching decl, so author rules now OVERRIDE the defaults with UA as the fallback (correct CSS
561 // origin order). Before this swap the appended defaults silently beat every author color on styled pages
562 // (measured: identical palettes with and without author colors -- the dfb probes, 2026-07-09).
563 var ci: i64 = 0
564 while ci < clen { buf[body_len + ci] = css[ci]; ci=ci+1 }
565 let rawsty: *u8 = sys_mmap(STYCAP)
566 let rawstylen: i64 = rh_extract_styles(body, body_len, rawsty, STYCAP)
567 var stylen: i64 = rh_filter_media(rawsty, 0, rawstylen, ((buf as i64)+body_len+clen) as *u8, STYCAP, vw)
568 // STYLED-PAGE UA CORRECTION (2026-07-28): our UA body padding (44px) is a READABILITY choice for
569 // unstyled pages; Chrome's real UA default is body margin 8px. On a page that ships author CSS,
570 // keeping 44px shifted ALL content right of Chrome (measured: chrome first-ink x=24, ours 68 on
571 // styled wikipedia). Inject the Chrome-parity body box AFTER our UA sheet (wins over it) but
572 // BEFORE author styles (so the page's own body rules still win over both). +padding-bottom for
573 // symmetry; sites that set body padding override all of this normally.
574 if stylen > 0 { if stylen + 100 < STYCAP {
575 let uafix: *u8 = "body{padding-left:8px;padding-right:8px;padding-top:8px;padding-bottom:8px}\x00" as *u8
576 var ufl: i64 = 0
577 while uafix[ufl] != (0 as u8) { ufl = ufl + 1 }
578 // shift the author block right by ufl and place the fix between UA and author
579 var mv: i64 = stylen - 1
580 while mv >= 0 { buf[body_len + clen + ufl + mv] = buf[body_len + clen + mv]; mv = mv - 1 }
581 var uc: i64 = 0
582 while uc < ufl { buf[body_len + clen + uc] = uafix[uc]; uc = uc + 1 }
583 stylen = stylen + ufl
584 } }
585 let total: i64 = body_len + clen + stylen
586
587 var VW: i64 = vw
588 if VW < 200 { VW = 200 }
589 let boxes: *LayoutBox = (sys_mmap(NX_LAYOUT_BOX_BYTES*16384+16)) as *LayoutBox
590 let tree: *LayoutTree = (sys_mmap(64)) as *LayoutTree
591 nx_layout_tree_init(tree, boxes, 16384)
592 let stk: *LayoutFromDomStack = (sys_mmap(NX_LAYOUT_FROM_DOM_STACK_BYTES+8)) as *LayoutFromDomStack
593 let sidx: *i64 = (sys_mmap(8*128)) as *i64
594 nx_layout_from_dom_stack_init(stk, sidx, 128)
595 nx_layout_from_dom(buf, body_len, tree, stk)
596
597 let ext: *CssElement = (sys_mmap(NX_CSS_ELEMENT_BYTES*16384 as nx_size)) as *CssElement
598 let ehoff: *i64 = sys_mmap(8*16384) as *i64
599 let ehlen: *i64 = sys_mmap(8*16384) as *i64
600 let esoff: *i64 = sys_mmap(8*16384) as *i64
601 let eslen: *i64 = sys_mmap(8*16384) as *i64
602 let n_ext: i64 = br_extract(buf, body_len, ext, ehoff, ehlen, esoff, eslen, 16384)
603 let boxel: *CssElement = (sys_mmap((NX_CSS_ELEMENT_BYTES*(tree.count+1)) as nx_size)) as *CssElement
604 let bhoff: *i64 = sys_mmap(8*(tree.count+1)) as *i64
605 let bhlen: *i64 = sys_mmap(8*(tree.count+1)) as *i64
606 let bsoff: *i64 = sys_mmap(8*(tree.count+1)) as *i64
607 let bslen: *i64 = sys_mmap(8*(tree.count+1)) as *i64
608 let bimg: *i64 = sys_mmap(8*(tree.count+1)) as *i64
609 let bimw: *i64 = sys_mmap(8*(tree.count+1)) as *i64
610 let bimh: *i64 = sys_mmap(8*(tree.count+1)) as *i64
611 let bcvs: *i64 = sys_mmap(8*(tree.count+1)) as *i64
612 var k: i64 = 0
613 var ii: i64 = 0
614 while ii < tree.count {
615 let b: *LayoutBox = ((tree.boxes as i64)+ii*NX_LAYOUT_BOX_BYTES) as *LayoutBox
616 let e: *CssElement = ((boxel as i64)+ii*NX_CSS_ELEMENT_BYTES) as *CssElement
617 e.src = buf
618 bhoff[ii] = 0 - 1; bhlen[ii] = 0 - 1
619 bsoff[ii] = 0 - 1; bslen[ii] = 0 - 1
620 bimg[ii] = 0; bimw[ii] = 0; bimh[ii] = 0; bcvs[ii] = 0
621 var mapped: i64 = 0
622 if rh_is_elem(b.kind)==1 { if b.parent_idx != (0-1) { if k < n_ext {
623 let s: *CssElement = ((ext as i64)+k*NX_CSS_ELEMENT_BYTES) as *CssElement
624 e.tag_off=s.tag_off; e.tag_len=s.tag_len; e.id_off=s.id_off; e.id_len=s.id_len; e.class_off=s.class_off; e.class_len=s.class_len
625 bhoff[ii] = ehoff[k]; bhlen[ii] = ehlen[k]
626 bsoff[ii] = esoff[k]; bslen[ii] = eslen[k]
627 // <canvas> = sourceless render surface (seq103): match the element's own tag bytes in buf
628 if s.tag_len==6 { if (buf[s.tag_off]&0xff)==99 { if (buf[s.tag_off+1]&0xff)==97 { if (buf[s.tag_off+2]&0xff)==110 { if (buf[s.tag_off+3]&0xff)==118 { if (buf[s.tag_off+4]&0xff)==97 { if (buf[s.tag_off+5]&0xff)==115 { bcvs[ii]=1 } } } } } } }
629 k=k+1; mapped=1
630 } } }
631 if mapped==0 { e.tag_off=0; e.tag_len=0; e.id_off=0; e.id_len=0; e.class_off=0; e.class_len=0 }
632 ii = ii + 1
633 }
634
635 let cur: *CssCursor = (sys_mmap(NX_CSS_CURSOR_BYTES) as nx_size) as *CssCursor
636 nx_css_cursor_init(cur, buf, total); cur.pos = body_len
637 let tok: *CssToken = (sys_mmap(NX_CSS_TOKEN_BYTES) as nx_size) as *CssToken
638 let rules: *CssRule = (sys_mmap(NX_CSS_RULE_BYTES*8192 as nx_size)) as *CssRule
639 let decls: *CssDeclaration = (sys_mmap(NX_CSS_DECLARATION_BYTES*32768 as nx_size)) as *CssDeclaration
640 let st: *CssParseState = (sys_mmap(128) as nx_size) as *CssParseState
641 nx_css_parse_state_init(st, cur, tok, rules, 8192, decls, 32768)
642 nx_css_parse(st)
643 let computed: *CssComputedDecl = (sys_mmap(NX_CSS_COMPUTED_DECL_BYTES*262144 as nx_size)) as *CssComputedDecl
644 let cb: *i64 = (sys_mmap(8)) as *i64
645 rh_cascade(buf, rules, st.rule_count, decls, st.decl_count, boxel, tree.count, tree, computed, 262144, cb)
646 let ncomp: i64 = cb[0]
647 rh_promote_display_kinds(tree, computed, ncomp, buf) // inline + display:flex/grid/block -> BLOCK (sized + painted)
648
649 let pbuf: *u8 = sys_mmap(256)
650 let ltable: *LayoutPropTable = (sys_mmap(NX_LAYOUT_PROP_TABLE_BYTES)) as *LayoutPropTable
651 nx_layout_prop_table_init(ltable, pbuf)
652 let resolve: *CssResolveCtx = (sys_mmap(NX_CSS_RESOLVE_CTX_BYTES)) as *CssResolveCtx
653 resolve.root_font_size_px = 16; resolve.parent_font_size_px = 16; resolve.parent_dimension_px = VW
654 let lctx: *LayoutCtx = (sys_mmap(NX_LAYOUT_CTX_BYTES)) as *LayoutCtx
655 nx_layout_ctx_init(lctx, tree, computed, ncomp, buf, VW, resolve)
656 lctx.text_measured = 1 // PROPORTIONAL text: layout measures with the same font metrics the paint draws with
657 let page_h: i64 = nx_layout_block_layout(lctx, ltable, 0)
658
659 page.buf = buf; page.tree = tree; page.computed = computed; page.ncomp = ncomp
660 br_cidx_build(tree, computed, ncomp) // paint-cliff fix: per-box decl buckets for br_comp_* lookups
661 page.page_h = page_h; page.bhref_off = bhoff; page.bhref_len = bhlen; page.ok = 1
662 page.bsrc_off = bsoff; page.bsrc_len = bslen; page.bimg = bimg; page.bimg_w = bimw; page.bimg_h = bimh
663 page.bcanvas = bcvs
664 page.find_ptr = 0; page.find_len = 0
665 // MATCH THE DAILY DRIVER (2026-07-27). This was hardcoded 0 = the crude 8x8 BITMAP font for
666 // EVERYTHING, while nishi.exe (nishi_gui.nx:530) paints vec_headings=2 = the proportional
667 // VECTOR font. So every headless shot taken through this path UNDERSOLD the real browser --
668 // it already produced one wrong conclusion in this ecosystem ("body font is THE rock",
669 // later DISPROVEN: the vector body font is at Chrome parity). An instrument that does not
670 // render what the user sees is a liar; default to what ships.
671 // Callers that deliberately want the bitmap font set vec_headings themselves AFTER this
672 // call (nx_browser_bg_gate does exactly that at :20/:59/:97), so they are unaffected.
673 page.vec_headings = 2
674 return 0
675}
676
677// find the next <canvas> box at or after `from` (-1 = none). The canvas CONTRACT (seq103, the game substrate):
678// the consumer renders into its OWN RGB buffer each frame, points page.bimg[idx] (+bimg_w/bimg_h) at it, and
679// repaints (br_draw_fb / br_shot_png); the image pass blits the CURRENT buffer at the box's laid-out position.
680func br_find_canvas(page: *Page, from: i64) -> i64 {
681 var i: i64 = from
682 if i < 0 { i = 0 }
683 while i < page.tree.count { if page.bcanvas[i] == 1 { return i } i = i + 1 }
684 return 0 - 1
685}
686
687// paint a heading run with the sovereign stroke-VECTOR font (modern typeface). Word-wraps by the vector
688// metrics within avail_w px, renders each line via font_text -> aa_render -> alpha-blend onto the RGBA fb
689// at (x0,y0), em size = empx px. Vector advances are TIGHTER than the bitmap the layout reserved with, so
690// the run always fits the box (never overflows). Allocation-bearing -> only called on opt-in once-render
691// paths. Returns the drawn width of the FIRST line (for the link underline; headings rarely link).
692// REUSED vector-paint scratch (was sys_mmap PER LINE -> per-box -> gigabytes committed under the PE's
693// VirtualAlloc, hanging the native GUI on a full page). Allocated ONCE, reused across every box + line.
694static bpv_xs: i64
695static bpv_ys: i64
696static bpv_cst: i64
697static bpv_cln: i64
698static bpv_cov: i64
699static bpv_cs2: i64
700static bpv_sn2: i64
701static bpv_np: i64
702static bpv_nc: i64
703// ---- GLYPH-ATLAS FAST PATH (GUI-only, gated by bpv_fast_on) ------------------------------------------
704// Rasterize each (char,size) glyph ONCE into a cached coverage cell, then just alpha-blend the cell per
705// draw. Turns O(glyphs drawn) rasterization into O(unique glyph x size). Pixel-snapped positioning (no
706// sub-pixel phase) -> crisp, ~visually-identical, MUCH faster. The EXACT per-line path (used by the shot /
707// NishiOS / OCR / polish gates) is untouched: they never set bpv_fast_on, so the dispatch below is a no-op
708// for them -> zero gate regression. Only nishi_gui (br_set_fast_vec(1)) opts in.
709static bpv_fast_on: i64
710static bpv_line_adv: i64 // wrapped-line advance override (px at paint scale; 0 = legacy (empx*12)/10+2).
711 // Set by the TEXT paint pass from the CSS line-height so paint fills exactly
712 // the lines layout reserved (measure==paint); single-threaded paint -> safe.
713static atk: i64 // hash keys (ch*100000+empx), -1 = empty slot
714static atcov: i64 // per-slot coverage buffer ptr
715static atgw: i64 // per-slot cell width (px)
716static atgh: i64 // per-slot cell height (px)
717static atmg: i64 // per-slot left margin (px) baked into the cell
718static atcap: i64
719func br_set_fast_vec(v: i64) -> i64 { bpv_fast_on = v; return 0 }
720func bpv_atlas_init() -> i64 {
721 if bpv_xs == 0 {
722 bpv_xs = sys_mmap(8*200000) as i64; bpv_ys = sys_mmap(8*200000) as i64
723 bpv_cst = sys_mmap(8*20000) as i64; bpv_cln = sys_mmap(8*20000) as i64
724 bpv_cov = sys_mmap(4194304) as i64
725 bpv_cs2 = sys_mmap(8*16) as i64; bpv_sn2 = sys_mmap(8*16) as i64
726 bpv_np = sys_mmap(16) as i64; bpv_nc = sys_mmap(16) as i64
727 }
728 if atk == 0 {
729 atcap = 8192
730 atk = sys_mmap(8*atcap) as i64; atcov = sys_mmap(8*atcap) as i64
731 atgw = sys_mmap(8*atcap) as i64; atgh = sys_mmap(8*atcap) as i64; atmg = sys_mmap(8*atcap) as i64
732 let ka: *i64 = atk as *i64; var z: i64 = 0
733 while z < atcap { ka[z] = 0 - 1; z = z + 1 }
734 }
735 return 0
736}
737// get-or-build the cached coverage cell for (ch,empx). out[0]=covptr out[1]=gw out[2]=gh out[3]=leftmargin_px
738func bpv_cell(ch: i64, empx: i64, cs: *i64, sn: *i64, out: *i64) -> i64 {
739 let ka: *i64 = atk as *i64
740 let key: i64 = ch * 100000 + empx
741 var h: i64 = key % atcap
742 if h < 0 { h = 0 - h }
743 var probes: i64 = 0
744 while probes < atcap {
745 let cova: *i64 = atcov as *i64; let gwa: *i64 = atgw as *i64; let gha: *i64 = atgh as *i64; let mga: *i64 = atmg as *i64
746 if ka[h] == key { out[0]=cova[h]; out[1]=gwa[h]; out[2]=gha[h]; out[3]=mga[h]; return 0 }
747 if ka[h] == (0 - 1) {
748 let ss: i64 = 3
749 let S: i64 = empx * ss
750 let mgn: i64 = 4
751 let adv: i64 = font_vec_adv_px2(ch, empx)
752 var gw: i64 = adv + 2*mgn
753 if gw < empx + 2*mgn { gw = empx + 2*mgn }
754 let gh: i64 = empx + (empx*35)/100 + 2
755 let xs: *i64 = bpv_xs as *i64; let ys: *i64 = bpv_ys as *i64
756 let cstart: *i64 = bpv_cst as *i64; let clen: *i64 = bpv_cln as *i64
757 let np: *i64 = bpv_np as *i64; np[0]=0
758 let nc: *i64 = bpv_nc as *i64; nc[0]=0
759 font_emit(ch, xs, ys, cstart, clen, np, nc, cs, sn, mgn*ss, 2*ss, S)
760 let cov: *u8 = sys_mmap(gw*gh + 16)
761 var zi: i64 = 0
762 while zi < gw*gh { cov[zi]=0 as u8; zi=zi+1 }
763 aa_render(xs, ys, cstart, clen, nc[0], cov, gw, gh, ss)
764 ka[h]=key; cova[h]=cov as i64; gwa[h]=gw; gha[h]=gh; mga[h]=mgn
765 out[0]=cov as i64; out[1]=gw; out[2]=gh; out[3]=mgn
766 return 0
767 }
768 h = h + 1; if h >= atcap { h = 0 }
769 probes = probes + 1
770 }
771 out[0]=0; return 1
772}
773// alpha-blend a coverage cell onto the RGBA framebuffer (matches br_paint_vec's blend exactly)
774func bpv_blit_rgba(fb: *Framebuffer, ox: i64, oy: i64, cov: *u8, gw: i64, gh: i64, cr: i64, cg: i64, cb: i64) -> i64 {
775 let fw: i64 = fb.width; let fh: i64 = fb.height; let pxb: *u8 = fb.pixels
776 var y: i64 = 0
777 while y < gh {
778 let fyr: i64 = oy + y
779 if fyr >= 0 { if fyr < fh {
780 var x: i64 = 0
781 while x < gw {
782 let a: i64 = cov[y*gw + x] & 0xff
783 if a > 0 {
784 let fxr: i64 = ox + x
785 if fxr >= 0 { if fxr < fw {
786 let o: i64 = (fyr*fw + fxr) * 4
787 let ia: i64 = 255 - a
788 pxb[o] = (((pxb[o] as i64)*ia + cr*a)/255) as u8
789 pxb[o+1] = (((pxb[o+1] as i64)*ia + cg*a)/255) as u8
790 pxb[o+2] = (((pxb[o+2] as i64)*ia + cb*a)/255) as u8
791 pxb[o+3] = 255 as u8
792 } }
793 }
794 x = x + 1
795 }
796 } }
797 y = y + 1
798 }
799 return 0
800}
801func br_paint_vec_atlas(fb: *Framebuffer, x0: i64, y0: i64, text: *u8, text_len: i64, empx: i64, color: *CssColor, avail_w: i64) -> i64 {
802 bpv_atlas_init()
803 let cs: *i64 = bpv_cs2 as *i64
804 let sn: *i64 = bpv_sn2 as *i64
805 font_trig_init(cs, sn)
806 let cr: i64 = color.r & 255
807 let cg: i64 = color.g & 255
808 let cb: i64 = color.b & 255
809 var lineh: i64 = (empx * 12) / 10 + 2
810 if bpv_line_adv >= 8 { lineh = bpv_line_adv } // CSS line-height (already paint-scaled by the caller)
811 var aw: i64 = avail_w
812 if aw < empx { aw = empx }
813 let cell: *i64 = sys_mmap(64) as *i64
814 var pos: i64 = 0
815 var li: i64 = 0
816 var w1: i64 = 0
817 while pos < text_len {
818 var e: i64 = font_vec_next_break_px(text, text_len, aw, pos, empx)
819 if e <= pos { e = text_len }
820 let basey: i64 = y0 + li * lineh
821 var penpx: i64 = x0
822 var k: i64 = pos
823 while k < e {
824 let ch: i64 = text[k] & 0xff
825 bpv_cell(ch, empx, cs, sn, cell)
826 let covp: i64 = cell[0]
827 let shp: i64 = (((((font_sp_shift_disp(ch) * font_ws()) / 100) * font_wdth()) / 100) * empx) / 1000
828 if covp != 0 { bpv_blit_rgba(fb, penpx + shp - cell[3], basey, covp as *u8, cell[1], cell[2], cr, cg, cb) }
829 penpx = penpx + font_vec_adv_px2(ch, empx)
830 k = k + 1
831 }
832 if li == 0 { w1 = penpx - x0 }
833 pos = e
834 li = li + 1
835 if li > 40 { pos = text_len }
836 }
837 return w1
838}
839func br_paint_vec(fb: *Framebuffer, x0: i64, y0: i64, text: *u8, text_len: i64, empx: i64, color: *CssColor, avail_w: i64) -> i64 {
840 if bpv_fast_on == 1 { return br_paint_vec_atlas(fb, x0, y0, text, text_len, empx, color, avail_w) }
841 let ss: i64 = 3
842 let S: i64 = empx * ss
843 if bpv_xs == 0 {
844 bpv_xs = sys_mmap(8*200000) as i64
845 bpv_ys = sys_mmap(8*200000) as i64
846 bpv_cst = sys_mmap(8*20000) as i64
847 bpv_cln = sys_mmap(8*20000) as i64
848 bpv_cov = sys_mmap(4194304) as i64
849 bpv_cs2 = sys_mmap(8*16) as i64
850 bpv_sn2 = sys_mmap(8*16) as i64
851 bpv_np = sys_mmap(16) as i64
852 bpv_nc = sys_mmap(16) as i64
853 }
854 let cs: *i64 = bpv_cs2 as *i64
855 let sn: *i64 = bpv_sn2 as *i64
856 font_trig_init(cs, sn)
857 let fw: i64 = fb.width
858 let fh: i64 = fb.height
859 let pxb: *u8 = fb.pixels
860 let cr: i64 = color.r & 255
861 let cg: i64 = color.g & 255
862 let cb: i64 = color.b & 255
863 let lineh: i64 = (empx * 12) / 10 + 2 // ~1.2em line height
864 var aw: i64 = avail_w
865 if aw < empx { aw = empx }
866 var pos: i64 = 0
867 var li: i64 = 0
868 var w1: i64 = 0
869 while pos < text_len {
870 var e: i64 = font_vec_next_break_px(text, text_len, aw, pos, empx)
871 if e <= pos { e = text_len }
872 // render this line [pos,e) into a coverage buffer sized to its width x (em band)
873 let lw: i64 = font_vec_text_w_px(text, pos, e, empx) + empx
874 let GH: i64 = empx + (empx*35)/100 // ascender..descender band
875 var GW: i64 = lw
876 if GW < empx { GW = empx }
877 if GW > fw { GW = fw }
878 let xs: *i64 = bpv_xs as *i64 // reused (was sys_mmap per line)
879 let ys: *i64 = bpv_ys as *i64
880 let cstart: *i64 = bpv_cst as *i64
881 let clen: *i64 = bpv_cln as *i64
882 let np: *i64 = bpv_np as *i64; np[0]=0
883 let nc: *i64 = bpv_nc as *i64; nc[0]=0
884 font_text(((text as i64)+pos) as *u8, e-pos, xs, ys, cstart, clen, np, nc, cs, sn, 2*ss, 2*ss, S)
885 if GW*GH > 4194288 { GW = 4194288 / GH } // clamp to the reused cov buffer (4 MB) -- defensive
886 let cov: *u8 = bpv_cov as *u8
887 var zi: i64 = 0
888 while zi < GW*GH { cov[zi]=0 as u8; zi=zi+1 }
889 aa_render(xs, ys, cstart, clen, nc[0], cov, GW, GH, ss)
890 // alpha-blend coverage onto the RGBA fb
891 let basey: i64 = y0 + li * lineh
892 var gy: i64 = 0
893 while gy < GH {
894 let fyr: i64 = basey + gy
895 if fyr >= 0 { if fyr < fh {
896 var gx: i64 = 0
897 while gx < GW {
898 let a: i64 = cov[gy*GW+gx] & 0xff
899 if a > 0 {
900 let fxr: i64 = x0 + gx
901 if fxr >= 0 { if fxr < fw {
902 let o: i64 = (fyr*fw+fxr)*4
903 let ia: i64 = 255 - a
904 pxb[o] = (((pxb[o] as i64)*ia + cr*a)/255) as u8
905 pxb[o+1] = (((pxb[o+1] as i64)*ia + cg*a)/255) as u8
906 pxb[o+2] = (((pxb[o+2] as i64)*ia + cb*a)/255) as u8
907 pxb[o+3] = 255 as u8
908 } }
909 }
910 gx = gx + 1
911 }
912 } }
913 gy = gy + 1
914 }
915 if li == 0 { w1 = lw }
916 pos = e
917 li = li + 1
918 if li > 40 { pos = text_len }
919 }
920 return w1
921}
922// vector metrics in absolute px (em size empx), for the paint's own wrap
923func font_vec_adv_px2(ch: i64, empx: i64) -> i64 { return (font_adv_em(ch) * empx) / 1000 }
924func font_vec_text_w_px(text: *u8, start: i64, end: i64, empx: i64) -> i64 {
925 var w: i64 = 0
926 var i: i64 = start
927 while i < end { w = w + font_vec_adv_px2(text[i] & 0xff, empx); i = i + 1 }
928 return w
929}
930func font_vec_next_break_px(text: *u8, len: i64, avail_px: i64, start: i64, empx: i64) -> i64 {
931 if start >= len { return len }
932 var w: i64 = 0
933 var i: i64 = start
934 var brk: i64 = 0 - 1
935 while i < len {
936 w = w + font_vec_adv_px2(text[i] & 0xff, empx)
937 if w > avail_px {
938 if brk > start { return brk + 1 }
939 if i > start { return i }
940 return start + 1
941 }
942 if (text[i] & 0xff) == 32 { brk = i }
943 i = i + 1
944 }
945 return len
946}
947
948// FIND-IN-PAGE (Ctrl+F, a universally-expected browser feature): case-insensitive search of the laid-out
949// text for `term`. Sets page.find_ptr/find_len so br_draw_fb_at highlights every matching text box; returns
950// the match count and, via yb, the page-y of the FIRST match (for scroll-to). A term absent from the page
951// returns 0 (highlights nothing). Matching is per-text-box substring (the layout already split text into
952// boxes at inline boundaries) -- the same units the render + hit-test use.
953func br_ci(a: i64) -> i64 { if a >= 65 { if a <= 90 { return a + 32 } } return a }
954func br_box_has_term(buf: *u8, off: i64, len: i64, term: *u8, tlen: i64) -> i64 {
955 if tlen <= 0 { return 0 }
956 var i: i64 = 0
957 while i + tlen <= len {
958 var j: i64 = 0
959 var ok: i64 = 1
960 while j < tlen { if br_ci(buf[off+i+j]&0xff) != br_ci(term[j]&0xff) { ok=0; j=tlen } else { j=j+1 } }
961 if ok == 1 { return 1 }
962 i = i + 1
963 }
964 return 0
965}
966func br_find(page: *Page, term: *u8, tlen: i64, yb: *i64) -> i64 {
967 page.find_ptr = term as i64
968 page.find_len = tlen
969 yb[0] = 0 - 1
970 if tlen <= 0 { page.find_ptr = 0; return 0 }
971 let tree: *LayoutTree = page.tree
972 var matches: i64 = 0
973 var i: i64 = 0
974 while i < tree.count {
975 let b: *LayoutBox = ((tree.boxes as i64) + i * NX_LAYOUT_BOX_BYTES) as *LayoutBox
976 if b.kind == NX_LAYOUT_BOX_TEXT { if b.text_len > 0 {
977 if br_box_has_term(page.buf, b.text_off, b.text_len, term, tlen) == 1 {
978 matches = matches + 1
979 if yb[0] < 0 { yb[0] = b.y }
980 }
981 } }
982 i = i + 1
983 }
984 if matches == 0 { page.find_ptr = 0 } // nothing to highlight
985 return matches
986}
987
988func br_color_from_int(c: i64) -> *CssColor {
989 let col: *CssColor = sys_mmap(NX_CSS_COLOR_BYTES) as *CssColor
990 col.r = (c >> 16) & 0xff
991 col.g = (c >> 8) & 0xff
992 col.b = c & 0xff
993 col.a = 255
994 return col
995}
996
997func br_bit(ft8: *u8, go: i64, col: i64, row: i64) -> i64 {
998 if col < 0 { return 0 }
999 if col > 7 { return 0 }
1000 if row < 0 { return 0 }
1001 if row > 7 { return 0 }
1002 if ((ft8[go + row] >> col) & 1) == 1 { return 255 }
1003 return 0
1004}
1005// bilinear coverage of glyph `go` at bit-space (fx256,fy256) in 1/256 fixed point (bit centers on integers)
1006func br_glyph_cov(ft8: *u8, go: i64, fx256: i64, fy256: i64) -> i64 {
1007 let x0: i64 = fx256 >> 8
1008 let y0: i64 = fy256 >> 8
1009 let fxr: i64 = fx256 & 255
1010 let fyr: i64 = fy256 & 255
1011 let v00: i64 = br_bit(ft8, go, x0, y0)
1012 let v10: i64 = br_bit(ft8, go, x0+1, y0)
1013 let v01: i64 = br_bit(ft8, go, x0, y0+1)
1014 let v11: i64 = br_bit(ft8, go, x0+1, y0+1)
1015 let top: i64 = v00*(256-fxr) + v10*fxr
1016 let bot: i64 = v01*(256-fxr) + v11*fxr
1017 return (top*(256-fyr) + bot*fyr) >> 16
1018}
1019// SHARPEN the raw bilinear coverage: solidify stroke interiors (>= BR_HI -> 255, keeps thin 1-bit stems
1020// crisp = legibility) while KEEPING the outer edge ramp as intermediate gray (= anti-aliasing). This is
1021// the knob that makes both the OCR judge (legibility) AND the polish census (AA) happy at once.
1022const BR_LO: i64 = 40
1023const BR_HI: i64 = 165
1024func br_sharpen(c: i64) -> i64 {
1025 if c <= BR_LO { return 0 }
1026 if c >= BR_HI { return 255 }
1027 return ((c - BR_LO) * 255) / (BR_HI - BR_LO)
1028}
1029// coverage for one output pixel: INTERIOR pixels (nearest bit + its 4 orthogonal neighbors all set) stay
1030// SOLID 255 -- pixel-exact, no resample shift, so legibility is preserved; only EDGE pixels take the
1031// sharpened bilinear ramp = anti-aliasing. This is what holds 984 OCR AND closes the polish gap.
1032func br_cov_hybrid(ft8: *u8, go: i64, fx256: i64, fy256: i64) -> i64 {
1033 let cc: i64 = (fx256 + 128) >> 8 // nearest bit col
1034 let rr: i64 = (fy256 + 128) >> 8 // nearest bit row
1035 if br_bit(ft8, go, cc, rr) == 255 {
1036 if br_bit(ft8, go, cc-1, rr) == 255 { if br_bit(ft8, go, cc+1, rr) == 255 {
1037 if br_bit(ft8, go, cc, rr-1) == 255 { if br_bit(ft8, go, cc, rr+1) == 255 {
1038 return 255
1039 } }
1040 } }
1041 }
1042 return br_sharpen(br_glyph_cov(ft8, go, fx256, fy256))
1043}
1044
1045// PROPORTIONAL + ANTI-ALIASED bitmap text: bilinear coverage of the glyph field (smooth edges) + a
1046// sharpening curve (solid interiors), alpha-blended onto the RGBA framebuffer. Closes the polish gap vs
1047// Chrome AND holds the 984 legibility. Pen advance = font8x8_adv (measured-layout SSOT), layout truthful.
1048func br_paint_text2x(fb: *Framebuffer, x: i64, y: i64, text: *u8, text_len: i64, color: *CssColor, scale: i64) -> i64 {
1049 let ft8: *u8 = font8x8_table()
1050 let blk: i64 = scale
1051 let GH: i64 = 8 * blk // glyph render height in px
1052 let fw: i64 = fb.width
1053 let fh: i64 = fb.height
1054 let pxb: *u8 = fb.pixels
1055 let cr: i64 = color.r & 255
1056 let cg: i64 = color.g & 255
1057 let cb: i64 = color.b & 255
1058 var pen: i64 = x
1059 var i: i64 = 0
1060 while i < text_len {
1061 let ch: i64 = text[i] & 0xff
1062 if ch > 0x20 { if ch <= 0x7E {
1063 let go: i64 = (ch - 0x20) * 8
1064 let iw: i64 = font8x8_ink_w(ft8, ch) // ink columns (proportional)
1065 var GW: i64 = iw * blk
1066 if GW < blk { GW = blk }
1067 let sxn: i64 = (iw * 256) / GW // bit-cols per output px * 256
1068 let syn: i64 = (8 * 256) / GH // bit-rows per output px * 256
1069 var oy: i64 = 0
1070 while oy < GH {
1071 let fy256: i64 = ((oy * 2 + 1) * syn) / 2 - 128 // (oy+0.5)*syn - 0.5 bit
1072 let fyr: i64 = y + oy
1073 if fyr >= 0 { if fyr < fh {
1074 var ox: i64 = 0
1075 while ox < GW {
1076 let fx256: i64 = ((ox * 2 + 1) * sxn) / 2 - 128
1077 let a: i64 = br_cov_hybrid(ft8, go, fx256, fy256)
1078 if a > 0 {
1079 let fxr: i64 = pen + ox
1080 if fxr >= 0 { if fxr < fw {
1081 let o: i64 = (fyr * fw + fxr) * 4
1082 let ia: i64 = 255 - a
1083 pxb[o] = (((pxb[o] as i64) * ia + cr * a) / 255) as u8
1084 pxb[o+1] = (((pxb[o+1] as i64) * ia + cg * a) / 255) as u8
1085 pxb[o+2] = (((pxb[o+2] as i64) * ia + cb * a) / 255) as u8
1086 pxb[o+3] = 255 as u8
1087 } }
1088 }
1089 ox = ox + 1
1090 }
1091 } }
1092 oy = oy + 1
1093 }
1094 } }
1095 pen = pen + font8x8_adv(ft8, ch) * blk
1096 i = i + 1
1097 }
1098 return pen - x
1099}
1100
1101// Blit a decoded RGB image (src = w*h*3, row-major) into the RGBA framebuffer at (dx,dy), nearest-neighbor
1102// scaled to boxW x boxH, clipped to fb bounds. The missing image-paint primitive (nx_paint_solid_rect does
1103// solid fills; this does image copy). A=255 (opaque). RGB source because nx_img_to_rgb/JPEG emit w*h*3.
1104func br_blit_rgb(fb: *Framebuffer, dx: i64, dy: i64, boxW: i64, boxH: i64, src: *u8, sw: i64, sh: i64) -> i64 {
1105 if boxW <= 0 { return 0 }
1106 if boxH <= 0 { return 0 }
1107 if sw <= 0 { return 0 }
1108 if sh <= 0 { return 0 }
1109 let fw: i64 = fb.width
1110 let fh: i64 = fb.height
1111 let px: *u8 = fb.pixels
1112 var ty: i64 = 0
1113 while ty < boxH {
1114 let py: i64 = dy + ty
1115 if py >= 0 { if py < fh {
1116 let sy: i64 = (ty * sh) / boxH
1117 var tx: i64 = 0
1118 while tx < boxW {
1119 let pxx: i64 = dx + tx
1120 if pxx >= 0 { if pxx < fw {
1121 let sx: i64 = (tx * sw) / boxW
1122 let so: i64 = (sy * sw + sx) * 3
1123 let dof: i64 = (py * fw + pxx) * 4
1124 px[dof + 0] = src[so + 0]
1125 px[dof + 1] = src[so + 1]
1126 px[dof + 2] = src[so + 2]
1127 px[dof + 3] = 255 as u8
1128 } }
1129 tx = tx + 1
1130 }
1131 } }
1132 ty = ty + 1
1133 }
1134 return 0
1135}
1136
1137// OFF-SCREEN render: rasterize the laid-out page into an in-memory RGBA framebuffer (no X server). Uses the
1138// 5x7 bitmap-font paint primitives (for verifying colors/layout/chrome). The same fn the GUI calls to paint.
1139// draw with a vertical SCROLL offset (page-space px): content shifts up by scroll_y, chrome stays fixed.
1140// br_draw_fb delegates with scroll 0, so existing callers (incl the parallel-owned GUI) are unaffected.
1141func br_draw_fb_at(fb: *Framebuffer, page: *Page, win_w: i64, cur_url: *u8, ulen: i64, scale: i64, scroll_y: i64) -> i64 {
1142 let tree: *LayoutTree = page.tree
1143 let src: *u8 = page.buf
1144 let computed: *CssComputedDecl = page.computed
1145 let ncomp: i64 = page.ncomp
1146 let fdec: *u8 = sys_mmap(1100) // reused per text box: decode HTML entities before painting
1147 var base_bg: i64 = 0x00FFFFFF
1148 if page.dark_mode == 1 { base_bg = 0x000E1116 } // dark base so uncovered margin areas match the theme
1149 // CSS BACKGROUND PROPAGATION (SPEC semantics): the canvas takes the background of the ROOT elements
1150 // ONLY -- html/body (boxes 0..1) -- exactly like Chrome. The first version scanned ALL blocks for the
1151 // first non-white bg; the top-20 suite caught it painting google.com's whole canvas dark off a banner
1152 // div (coarse parity 0.0%). var() bgs on body resolve through cx_expand, so /experiential's dark body
1153 // still propagates. Gated on dark_mode==0 (explicit reader theme keeps its base).
1154 if page.dark_mode == 0 {
1155 // walk the ROOT CHAIN: from the parentless root, descend while the current box has exactly ONE
1156 // full-width BLOCK child (html -> wrapper -> body in our tree); the first background on that chain
1157 // is the canvas (spec: html/body propagate). A box with SIBLING blocks (google's banner divs) ends
1158 // the chain -- content never propagates. (The first-non-white-anywhere version painted google's
1159 // canvas dark off a banner; the parentless-root-only version missed body at depth 2 -- both caught
1160 // by the top-20 suite + the /experiential void-luma regression check.)
1161 var cur: i64 = 0 - 1
1162 var cbi: i64 = 0
1163 while cbi < tree.count {
1164 if cur < 0 {
1165 let cb0: *LayoutBox = ((tree.boxes as i64) + cbi * NX_LAYOUT_BOX_BYTES) as *LayoutBox
1166 if cb0.kind == NX_LAYOUT_BOX_BLOCK { if cb0.parent_idx < 0 { cur = cbi } }
1167 }
1168 cbi = cbi + 1
1169 }
1170 var hops: i64 = 0
1171 while cur >= 0 {
1172 if hops >= 6 { cur = 0 - 1 }
1173 else {
1174 hops = hops + 1
1175 let cb: *LayoutBox = ((tree.boxes as i64) + cur * NX_LAYOUT_BOX_BYTES) as *LayoutBox
1176 let cbg: i64 = br_comp_bg(src, computed, ncomp, cur)
1177 if cbg >= 0 { if cbg != 0x00FFFFFF { base_bg = cbg; cur = 0 - 1 } }
1178 if cur >= 0 {
1179 // exactly one full-width BLOCK child continues the chain
1180 var only: i64 = 0 - 1
1181 var nfull: i64 = 0
1182 var ci: i64 = cb.first_child_idx
1183 var guard: i64 = 0
1184 while ci >= 0 {
1185 if guard >= 4096 { ci = 0 - 1 }
1186 else {
1187 guard = guard + 1
1188 let cc: *LayoutBox = ((tree.boxes as i64) + ci * NX_LAYOUT_BOX_BYTES) as *LayoutBox
1189 if cc.kind == NX_LAYOUT_BOX_BLOCK { if cc.w == cb.w { nfull = nfull + 1; only = ci } }
1190 ci = cc.next_sibling_idx
1191 }
1192 }
1193 if nfull == 1 { cur = only } else { cur = 0 - 1 }
1194 }
1195 }
1196 }
1197 }
1198 nx_paint_solid_rect(fb, 0, 0, win_w * scale, fb.height, br_color_from_int(base_bg))
1199 var i: i64 = 0
1200 let bwp: *i64 = sys_mmap(16) as *i64 // border width out-slot (hoisted: no mmap in the loop)
1201 while i < tree.count {
1202 let b: *LayoutBox = ((tree.boxes as i64) + i * NX_LAYOUT_BOX_BYTES) as *LayoutBox
1203 var _pbx: i64 = 0
1204 if b.kind == NX_LAYOUT_BOX_BLOCK { _pbx = 1 }
1205 if b.kind == NX_LAYOUT_BOX_INLINE_BLOCK { _pbx = 1 } // atomic inline-block (chips/tags/buttons) now paint bg+border
1206 if _pbx == 1 { if b.w > 0 { if b.h > 0 {
1207 let bg: i64 = br_comp_bg(src, computed, ncomp, i)
1208 if bg >= 0 { if bg != 0x00FFFFFF { nx_paint_solid_rect(fb, b.x * scale, (b.y - scroll_y + NX_CHROME_H) * scale, b.w * scale, b.h * scale, br_color_from_int(bg)) } }
1209 // CSS borders (the `border` shorthand): stroke the 4 edges -- tables/cards get real lines
1210 let bc: i64 = br_comp_border(src, computed, ncomp, i, bwp)
1211 if bc >= 0 {
1212 let bw2: i64 = bwp[0]
1213 nx_paint_solid_rect(fb, b.x * scale, (b.y - scroll_y + NX_CHROME_H) * scale, b.w * scale, bw2 * scale, br_color_from_int(bc))
1214 nx_paint_solid_rect(fb, b.x * scale, (b.y - scroll_y + NX_CHROME_H + b.h - bw2) * scale, b.w * scale, bw2 * scale, br_color_from_int(bc))
1215 nx_paint_solid_rect(fb, b.x * scale, (b.y - scroll_y + NX_CHROME_H) * scale, bw2 * scale, b.h * scale, br_color_from_int(bc))
1216 nx_paint_solid_rect(fb, (b.x + b.w - bw2) * scale, (b.y - scroll_y + NX_CHROME_H) * scale, bw2 * scale, b.h * scale, br_color_from_int(bc))
1217 }
1218 } } }
1219 i = i + 1
1220 }
1221 // FIND-IN-PAGE highlight pass: a yellow band behind every text box matching the active search term
1222 // (drawn under the text so the glyphs stay readable). Only runs when a find is active.
1223 if page.find_ptr != 0 {
1224 let fterm: *u8 = page.find_ptr as *u8
1225 i = 0
1226 while i < tree.count {
1227 let b: *LayoutBox = ((tree.boxes as i64) + i * NX_LAYOUT_BOX_BYTES) as *LayoutBox
1228 if b.kind == NX_LAYOUT_BOX_TEXT { if b.text_len * b.h > 0 { // h==0 = never laid out (hidden subtree) -> no highlight
1229 if br_box_has_term(src, b.text_off, b.text_len, fterm, page.find_len) == 1 {
1230 var hw: i64 = b.w
1231 if hw < 8 { hw = 8 }
1232 nx_paint_solid_rect(fb, b.x * scale, (b.y - scroll_y + NX_CHROME_H) * scale, hw * scale, 17 * scale, br_color_from_int(0x00FFE066))
1233 }
1234 } }
1235 i = i + 1
1236 }
1237 }
1238 i = 0
1239 while i < tree.count {
1240 let b: *LayoutBox = ((tree.boxes as i64) + i * NX_LAYOUT_BOX_BYTES) as *LayoutBox
1241 let syt: i64 = (b.y - scroll_y + NX_CHROME_H) * scale // VIEWPORT CULL: off-screen text boxes skip the
1242 var vis: i64 = 1 // expensive vector/bitmap raster (fb.height = full
1243 if syt > fb.height { vis = 0 } // page for the shot path -> renders all; = 720 for
1244 if syt + (b.h + 80) * scale < 0 { vis = 0 } // the GUI -> only ~visible boxes -> fast + no hang)
1245 if vis == 1 {
1246 // NEVER-LAID GUARD (seq1130): a text box under a display:none ancestor is never reached by
1247 // _layout_block_recurse -- it keeps x=0,y=0,w=0,h=0 from tree init. Painting it wraps the run
1248 // at the w=0 minimum and stacks every hidden run at the origin = the "blob rail". Layout is the
1249 // only writer of b.h and always sets it > 0 for a placed text box, so h==0 means NOT LAID OUT.
1250 // text_len*h > 0 == (text_len > 0 AND h > 0) for the non-negative fields.
1251 if b.kind == NX_LAYOUT_BOX_TEXT { if b.text_len * b.h > 0 {
1252 let par: i64 = b.parent_idx
1253 var col: i64 = br_comp_color_inh(src, computed, ncomp, tree, par) // CSS-inherited color
1254 if col < 0 { col = 0 }
1255 var nn: i64 = b.text_len
1256 if nn > 960 { nn = 960 }
1257 let dn0: i64 = nx_html_decode_entities(src, b.text_off, nn, fdec, 1024)
1258 let dn: i64 = nx_fold_ascii(fdec, 0, dn0, fdec, 1024) // UTF-8 -> renderable ASCII (accents->base, curly->straight, mdash->-): real pages render in the vector font. Identity for ASCII -> gate pages byte-unchanged.
1259 let fs: i64 = br_comp_fontsize_inh(src, computed, ncomp, tree, par, 16) // PROPORTIONAL: size text by the INHERITED cascade incl the `font` shorthand (same rule as layout -> measure==paint)
1260 let lh_run: i64 = br_comp_lineheight_px(src, computed, ncomp, tree, par, fs) // CSS line-height (same resolver rule as layout)
1261 var tscale: i64 = 1
1262 if fs >= 24 { tscale = 2 } // MUST match _layout_scale_for_px (24/36 buckets)
1263 if fs >= 36 { tscale = 3 } // so painted glyph size == the layout's reserved metrics
1264 // VECTOR TEXT: opt-in; any run (headings AND body -- vector is now OCR-legible to 14px, proven by
1265 // nx_vecfont_body_bench's size ladder) whose chars all have vector glyphs is painted with the
1266 // proportional stroke font instead of the bitmap monospace (the "typewriter" body). empx = the
1267 // cascade font-size x paint scale (exact). Vector is proportional+tighter than the bitmap the
1268 // layout reserved -> fits the box. vec_headings==2 forces body-too; ==1 keeps headings-only.
1269 var did_vec: i64 = 0
1270 var vec_ok: i64 = 0
1271 if page.vec_headings == 1 { if tscale >= 2 { vec_ok = 1 } }
1272 if page.vec_headings == 2 { vec_ok = 1 }
1273 if vec_ok == 1 {
1274 var run_ok: i64 = font_run_has(fdec, dn)
1275 if bpv_fast_on == 1 { run_ok = font_run_has_ext(fdec, dn) } // GUI/atlas covers % [ ] ; " = + too
1276 if run_ok == 1 {
1277 let empx: i64 = fs * scale
1278 // HALF-LEADING: CSS centers the glyph line inside the line box; without this, text
1279 // sits at box top and every line lands ~leading/2 higher than Chrome.
1280 var hl: i64 = 0
1281 if lh_run >= 8 {
1282 let gl: i64 = (fs * 12) / 10 + 2
1283 if lh_run > gl { hl = (lh_run - gl) / 2 }
1284 }
1285 if lh_run >= 8 { bpv_line_adv = lh_run * scale } else { bpv_line_adv = 0 }
1286 br_paint_vec(fb, (b.x + 4) * scale, (b.y + hl - scroll_y + NX_CHROME_H) * scale, fdec, dn, empx, br_color_from_int(col), b.w * scale)
1287 bpv_line_adv = 0
1288 did_vec = 1
1289 }
1290 }
1291 if did_vec == 0 {
1292 // WORD-WRAP long runs by MEASURED advances -- the SAME font8x8_next_break the measured layout
1293 // reserved lines with, over the same avail budget (b.w/tscale in 1x units), so painted lines
1294 // exactly fill the reserved box. Short runs take the single-line path unchanged.
1295 let wtab: *u8 = font8x8_table()
1296 var avail_u: i64 = b.w / tscale
1297 if avail_u < 4 { avail_u = 4 }
1298 var lhadv: i64 = 17 * tscale
1299 if lh_run >= 8 { lhadv = lh_run } // CSS line-height: advance wrapped lines exactly as layout reserved
1300 var hl2: i64 = 0
1301 if lhadv > 17 * tscale { hl2 = (lhadv - 17 * tscale) / 2 } // half-leading (center in the line box)
1302 var pos: i64 = 0
1303 var li: i64 = 0
1304 var w1: i64 = 0 // drawn width of the first line (underline = text width)
1305 while pos < dn {
1306 var e: i64 = font8x8_next_break(wtab, fdec, dn, avail_u, pos)
1307 if e <= pos { e = dn }
1308 let dw: i64 = br_paint_text2x(fb, (b.x + 4) * scale, (b.y + hl2 - scroll_y + NX_CHROME_H + 1 + li * lhadv) * scale, ((fdec as i64)+pos) as *u8, e - pos, br_color_from_int(col), tscale * scale)
1309 if li == 0 { w1 = dw }
1310 pos = e
1311 li = li + 1
1312 if li > 60 { pos = dn }
1313 }
1314 var is_link: i64 = 0
1315 if col == 0x2563eb { is_link = 1 } // light-theme link blue
1316 if col == 0x6aa3ff { is_link = 1 } // dark-theme link blue
1317 if is_link == 1 { nx_paint_solid_rect(fb, (b.x + 4) * scale, (b.y - scroll_y + NX_CHROME_H + 1 + 14 * tscale) * scale, w1, tscale * scale, br_color_from_int(col)) } // underline links (drawn-text width)
1318 }
1319 } } }
1320 i = i + 1
1321 }
1322 // IMAGE pass: blit decoded <img> pixels (the consumer fills page.bimg via fetch+decode) at each img
1323 // box's laid-out position. ADDITIVE -- the layout algorithm is untouched; images paint over the flow.
1324 i = 0
1325 while i < tree.count {
1326 let ib: *LayoutBox = ((tree.boxes as i64) + i * NX_LAYOUT_BOX_BYTES) as *LayoutBox
1327 let ip: i64 = page.bimg[i]
1328 if ip != 0 {
1329 let iw: i64 = page.bimg_w[i]
1330 let ih: i64 = page.bimg_h[i]
1331 var dw: i64 = iw
1332 // HIDDEN-SUBTREE GUARD, CORRECTED (2026-07-29). The seq1130 rule "h<=0 ⇒ never laid ⇒
1333 // don't paint" is right for TEXT but WRONG for <img>: an image with no CSS width/height
1334 // legitimately has h=0 until its intrinsic size is known (only after fetch+decode), so
1335 // that guard silently killed 100% of images on every page (measured: imgsrc=11,
1336 // imgsrc_laid=0). Ask the RIGHT question instead — is the image's CONTAINER laid out? A
1337 // hidden subtree has a 0-height PARENT; a not-yet-sized <img> sits inside a laid parent.
1338 if ib.h <= 0 {
1339 var par_laid: i64 = 0
1340 if ib.parent_idx >= 0 {
1341 let ipb: *LayoutBox = ((tree.boxes as i64) + ib.parent_idx * NX_LAYOUT_BOX_BYTES) as *LayoutBox
1342 if ipb.h > 0 { par_laid = 1 }
1343 }
1344 if par_laid == 0 { dw = 0 }
1345 }
1346 let avail: i64 = win_w - ib.x - 8
1347 if dw > avail { dw = avail } // cap to viewport width
1348 var dh: i64 = ih
1349 if dw < iw { if iw > 0 { dh = ih * dw / iw } } // preserve aspect on downscale
1350 if dw > 0 { if dh > 0 {
1351 br_blit_rgb(fb, ib.x * scale, (ib.y - scroll_y + NX_CHROME_H) * scale, dw * scale, dh * scale, ip as *u8, iw, ih)
1352 } }
1353 }
1354 i = i + 1
1355 }
1356 // chrome (all coords * scale) -- Chrome-palette grays, same geometry (hit-tests unchanged). Dark theme
1357 // swaps the palette (near-black toolbar + light glyphs) so the whole browser is coherent in dark mode.
1358 var c_bar: i64 = 0x00F1F3F4
1359 var c_edge: i64 = 0x00DADCE0
1360 var c_btn: i64 = 0x00F6F8FA
1361 var c_field: i64 = 0x00FFFFFF
1362 var c_ink: i64 = 0x00202124
1363 if page.dark_mode == 1 { c_bar=0x001A1F28; c_edge=0x00303845; c_btn=0x00232833; c_field=0x000E1116; c_ink=0x00D4DAE3 }
1364 nx_paint_solid_rect(fb, 0, 0, win_w * scale, NX_CHROME_H * scale, br_color_from_int(c_bar))
1365 nx_paint_solid_rect(fb, 0, (NX_CHROME_H - 1) * scale, win_w * scale, scale, br_color_from_int(c_edge))
1366 nx_paint_solid_rect(fb, 8 * scale, 8 * scale, 30 * scale, 28 * scale, br_color_from_int(c_btn))
1367 nx_paint_solid_rect(fb, 44 * scale, 8 * scale, 30 * scale, 28 * scale, br_color_from_int(c_btn))
1368 nx_paint_solid_rect(fb, 80 * scale, 8 * scale, 30 * scale, 28 * scale, br_color_from_int(c_btn))
1369 br_paint_text2x(fb, 18 * scale, 16 * scale, "<\x00" as *u8, 1, br_color_from_int(c_ink), scale)
1370 br_paint_text2x(fb, 54 * scale, 16 * scale, ">\x00" as *u8, 1, br_color_from_int(c_ink), scale)
1371 br_paint_text2x(fb, 90 * scale, 16 * scale, "R\x00" as *u8, 1, br_color_from_int(c_ink), scale)
1372 let bx: i64 = 118
1373 nx_paint_solid_rect(fb, bx * scale, 8 * scale, (win_w - bx - 10) * scale, 28 * scale, br_color_from_int(c_field))
1374 var un: i64 = ulen
1375 if un > 110 { un = 110 }
1376 br_paint_text2x(fb, (bx + 6) * scale, 14 * scale, cur_url, un, br_color_from_int(c_ink), scale)
1377 return 0
1378}
1379
1380// the un-scrolled draw every existing caller uses (delegates; page top at the viewport top).
1381func br_draw_fb(fb: *Framebuffer, page: *Page, win_w: i64, cur_url: *u8, ulen: i64, scale: i64) -> i64 {
1382 return br_draw_fb_at(fb, page, win_w, cur_url, ulen, scale, 0)
1383}
1384
1385// render the laid-out page to a 1000xSH RGBA framebuffer and write it as a PNG at `path`
1386// FULL-PAGE SHOT (2026-07-27). SH was hardcoded 1000 = ONE SCREENFUL, while Chrome's
1387// --screenshot captures the WHOLE document. Comparing the two graded Nishi on ~21% of a
1388// page against Chrome on 100% of it -- so the only honest fixes are to raise Nishi to
1389// Chrome's behaviour (this) or to cripple the oracle (never). Capture the full laid-out
1390// height, clamped: floor 1000 (an empty/short page still yields a normal-looking shot),
1391// ceiling 20000 (a runaway page_h can't ask for a gigabyte framebuffer).
1392func br_shot_png(page: *Page, cur_url: *u8, ulen: i64, path: *u8) -> i64 {
1393 let SW: i64 = 1000
1394 var SH: i64 = page.page_h
1395 if SH < 1000 { SH = 1000 }
1396 if SH > 20000 { SH = 20000 }
1397 let fb: *Framebuffer = sys_mmap(NX_FRAMEBUFFER_BYTES) as *Framebuffer
1398 let px: *u8 = sys_mmap(SW * SH * 4 + 64)
1399 nx_framebuffer_init(fb, px, SW, SH)
1400 br_draw_fb(fb, page, SW, cur_url, ulen, 1)
1401 let rgb: *u8 = sys_mmap(SW * SH * 3 + 64)
1402 var p: i64 = 0
1403 while p < SW * SH {
1404 rgb[p * 3 + 0] = px[p * 4 + 0]
1405 rgb[p * 3 + 1] = px[p * 4 + 1]
1406 rgb[p * 3 + 2] = px[p * 4 + 2]
1407 p = p + 1
1408 }
1409 nx_png_write_rgb(path, rgb, SW, SH)
1410 return 0
1411}
1412
1413// hit-test: find a box with an href whose rect contains doc-point (dx,dy). 1 + sets off/len, else 0.
1414// CONTAINER ANCHORS (2026-07-03): an <a> wrapping block children (e.g. a card link: <a><h3>..</h3>
1415// <p>..</p></a>) lays out with a ZERO own-rect -- its geometry lives in its DESCENDANT boxes. Such
1416// links were dead to clicks (found by nx_aw_browser_gate on the real andelinwest pages). For an href
1417// box with no height, hit-test the UNION of its descendants' rects instead. Purely additive: boxes
1418// with real rects behave exactly as before; zero-rect href boxes previously matched NOTHING.
1419func br_hit_link(page: *Page, dx: i64, dy: i64, off: *i64, len: *i64) -> i64 {
1420 let tree: *LayoutTree = page.tree
1421 var i: i64 = 0
1422 while i < tree.count {
1423 if page.bhref_off[i] >= 0 {
1424 let b: *LayoutBox = ((tree.boxes as i64) + i * NX_LAYOUT_BOX_BYTES) as *LayoutBox
1425 var bx: i64 = b.x
1426 var by: i64 = b.y
1427 var bw: i64 = b.w
1428 var bh: i64 = b.h
1429 if bh <= 0 {
1430 // SIBLING-SPAN effective rect: the layout FLATTENS a container anchor's children to
1431 // its SIBLINGS (observed: card content boxes follow the zero-rect anchor box under
1432 // the same parent). Union the rects of the boxes AFTER i while they stay inside i's
1433 // parent's subtree, stopping at the next href-carrying box (= the next link).
1434 let par: i64 = b.parent_idx
1435 var x0: i64 = 0
1436 var y0: i64 = 0
1437 var x1: i64 = 0
1438 var y1: i64 = 0
1439 var any: i64 = 0
1440 var j: i64 = i + 1
1441 var span: i64 = 1
1442 while span == 1 {
1443 if j >= tree.count { span = 0 }
1444 else { if page.bhref_off[j] >= 0 { span = 0 }
1445 else {
1446 let cb: *LayoutBox = ((tree.boxes as i64) + j * NX_LAYOUT_BOX_BYTES) as *LayoutBox
1447 // is j inside par's subtree? (bounded parent-chain walk, cycle-safe)
1448 var p: i64 = cb.parent_idx
1449 var hop: i64 = 0
1450 var inside: i64 = 0
1451 while hop < 64 {
1452 if p == (0 - 1) { hop = 64 }
1453 else { if p == par { inside = 1; hop = 64 }
1454 else { let pb: *LayoutBox = ((tree.boxes as i64) + p * NX_LAYOUT_BOX_BYTES) as *LayoutBox; p = pb.parent_idx; hop = hop + 1 } }
1455 }
1456 if inside == 0 { span = 0 }
1457 else {
1458 if cb.w > 0 { if cb.h > 0 {
1459 if any == 0 { x0 = cb.x; y0 = cb.y; x1 = cb.x + cb.w; y1 = cb.y + cb.h; any = 1 }
1460 else {
1461 if cb.x < x0 { x0 = cb.x }
1462 if cb.y < y0 { y0 = cb.y }
1463 if cb.x + cb.w > x1 { x1 = cb.x + cb.w }
1464 if cb.y + cb.h > y1 { y1 = cb.y + cb.h }
1465 }
1466 } }
1467 j = j + 1
1468 }
1469 } }
1470 }
1471 if any == 1 { bx = x0; by = y0; bw = x1 - x0; bh = y1 - y0 }
1472 }
1473 var hitw: i64 = bw // layout metric now matches the 9px render font, so b.w is the true width
1474 if hitw < 12 { hitw = 12 }
1475 if dx >= bx { if dx < bx + hitw { if dy >= by { if dy < by + bh {
1476 off[0] = page.bhref_off[i]; len[0] = page.bhref_len[i]; return 1
1477 } } } }
1478 }
1479 i = i + 1
1480 }
1481 return 0
1482}