nx_paint_walk_layout.nx source
↩ module page · 665 lines · 30524 B
1// nx_paint_walk_layout.nx -- walk a laid-out LayoutBox tree and
2// paint each box's `background-color` declaration into a Framebuffer.
3// Phase 4 second primitive of NISHI_BROWSER_ROADMAP. THE
4// integration primitive that proves the full bits-up pipeline
5// composes end-to-end:
6//
7// CSS bytes -> nx_css_tokenize -> nx_css_parse -> nx_css_apply
8// (cascade -> per-box decls)
9// HTML tree -> nx_layout_box -> nx_layout_block
10// (per-box x, y, w, h)
11// Cascade -> nx_css_lookup_property -> nx_css_color_decode
12// (per-box CssColor)
13// Box+color -> nx_paint_solid_rect
14// (pixels into Framebuffer)
15//
16// This primitive owns the walk + per-box lookup + color decode +
17// paint orchestration. Each individual step is a sovereign primitive
18// shipped prior; this is the connective tissue.
19//
20// What it does NOT handle yet (Phase 4b queued, explicit in header):
21// - background-image / gradient backgrounds
22// - foreground text rendering (waits for nx_paint_text +
23// nx_font_bitmap glyph rasterizer)
24// - border-color / border-style / border-width stroke
25// - box-shadow / outline / drop shadow
26// - opacity / alpha blending (currently solid overwrite)
27// - z-index stacking contexts (currently document-order paint)
28// - overflow:hidden clip-rect propagation to children
29// - sub-pixel positioning
30//
31// Per cardinal feedback-honest-perf-verdict-no-aspirational-claims:
32// gap list is EXACT.
33//
34// genealogy_id: w3c_css_backgrounds_module_level_3 +
35// substrate_browser_phase_4_walk_paint
36// lineage_id: nishi_browser_paint_walk_layout_v1
37
38import "nx_syscalls.nx"
39import "nx_css_tokenize.nx"
40import "nx_css_apply.nx"
41import "nx_css_color_decode.nx"
42import "nx_layout_box.nx"
43import "nx_paint_solid_rect.nx"
44import "nx_paint_text.nx"
45import "nx_text_wrap.nx"
46const NX_MAGIC_100000: i64 = 100000
47const NX_MAGIC_65536: i64 = 65536
48
49// Property-name table for the paint walk. Mirrors the structure of
50// LayoutPropTable in nx_layout_block but is narrower -- paint only
51// needs background-color today; future additions (color for text,
52// border-color, etc.) extend this struct without breaking callers.
53struct PaintPropTable {
54 src: *u8,
55 background_color_off: i64,
56 background_color_len: i64
57}
58
59const NX_PAINT_PROP_TABLE_BYTES: i64 = 24
60
61// Context bundle so the walk function signature stays compact.
62//
63// `text_src` and `default_text_color` are OPTIONAL. When set, TEXT
64// boxes (kind = NX_LAYOUT_BOX_TEXT) get their body bytes rendered
65// via nx_paint_text using the default color. text_src is the byte
66// buffer that the LayoutBox.text_off/text_len fields index into
67// (typically the HTML source buffer that nx_layout_from_dom walked).
68// Passing 0 for text_src OR 0 for default_text_color disables text
69// rendering — preserves backward compatibility with the
70// background-only walk.
71struct PaintCtx {
72 fb: *Framebuffer,
73 tree: *LayoutTree,
74 computed: *CssComputedDecl,
75 n_computed: i64,
76 cascade_src: *u8,
77 table: *PaintPropTable,
78 text_src: *u8,
79 default_text_color: *CssColor,
80 boxel: *CssElement // box-indexed elements (tag/id/class); enables list-marker bullets. 0 = off.
81}
82
83const NX_PAINT_CTX_BYTES: i64 = 72
84
85// ---- helpers ----
86
87func _paint_put_byte(buf: *u8, i: i64, v: i64) -> i64 {
88 buf[i] = v as u8
89 return 0
90}
91
92// Initialize the property-name table. Caller supplies a writable
93// buffer of at least 16 bytes; this fn writes "background-color"
94// (16 bytes) into it.
95func nx_paint_prop_table_init(t: *PaintPropTable, buf: *u8) -> i64 {
96 t.src = buf
97 // "background-color"
98 _paint_put_byte(buf, 0, 98) // b
99 _paint_put_byte(buf, 1, 97) // a
100 _paint_put_byte(buf, 2, 99) // c
101 _paint_put_byte(buf, 3, 107) // k
102 _paint_put_byte(buf, 4, 103) // g
103 _paint_put_byte(buf, 5, 114) // r
104 _paint_put_byte(buf, 6, 111) // o
105 _paint_put_byte(buf, 7, 117) // u
106 _paint_put_byte(buf, 8, 110) // n
107 _paint_put_byte(buf, 9, 100) // d
108 _paint_put_byte(buf, 10, 45) // -
109 _paint_put_byte(buf, 11, 99) // c
110 _paint_put_byte(buf, 12, 111) // o
111 _paint_put_byte(buf, 13, 108) // l
112 _paint_put_byte(buf, 14, 111) // o
113 _paint_put_byte(buf, 15, 114) // r
114 t.background_color_off = 0
115 t.background_color_len = 16
116 return 0
117}
118
119// Compare two byte regions across two source buffers (the cascade
120// stores property names in cascade_src; the paint table stores its
121// canonical names in table.src). Returns 1 if equal-length and all
122// bytes match.
123func _paint_bytes_equal(a_src: *u8, a_off: i64, a_len: i64,
124 b_src: *u8, b_off: i64, b_len: i64) -> i64 {
125 if a_len != b_len { return 0 }
126 var i: i64 = 0
127 while i < a_len {
128 let ca: i64 = (a_src[a_off + i] as i64) & 255
129 let cb: i64 = (b_src[b_off + i] as i64) & 255
130 if ca != cb { return 0 }
131 i = i + 1
132 }
133 return 1
134}
135
136// Find the LAST CssComputedDecl matching (query_element, property
137// name in table). Returns the entry, or 0 if none. Reverse-scan
138// for source-order winner-take-all per CSS cascade.
139func _paint_lookup_decl(ctx: *PaintCtx,
140 element_idx: i64,
141 table_prop_off: i64,
142 table_prop_len: i64) -> *CssComputedDecl {
143 var i: i64 = ctx.n_computed - 1
144 while i >= 0 {
145 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
146 if cd.element_idx == element_idx {
147 if _paint_bytes_equal(ctx.cascade_src, cd.prop_off, cd.prop_len,
148 ctx.table.src, table_prop_off, table_prop_len) == 1 {
149 return cd
150 }
151 }
152 i = i - 1
153 }
154 return 0 as *CssComputedDecl
155}
156
157// Extract the FIRST color-decoding token from a `background` shorthand value into `out`. Handles the common
158// real-world forms: `background:red` (named), `background:#fff center`, `background:url(x) no-repeat blue`.
159// Tries hex (#-led) / named / rgb() per space-separated token; returns 1 at the first that decodes.
160func _bg_extract_color(src: *u8, off: i64, len: i64, out: *CssColor) -> i64 {
161 var p: i64 = off
162 let pe: i64 = off + len
163 var found: i64 = 0
164 while p < pe {
165 var sk: i64 = 1
166 while sk == 1 { if p < pe { if (src[p]&0xff) == 32 { p = p + 1 } else { sk = 0 } } else { sk = 0 } }
167 var te: i64 = p
168 var go: i64 = 1
169 while go == 1 { if te < pe { if (src[te]&0xff) == 32 { go = 0 } else { te = te + 1 } } else { go = 0 } }
170 if te > p {
171 var ok: i64 = 0
172 if (src[p]&0xff) == 35 { ok = nx_css_color_decode(src, p, te - p, out) }
173 else { ok = nx_css_color_named(src, p, te - p, out); if ok != 1 { ok = nx_css_color_rgb_func(src, p, te - p, out) } }
174 if ok == 1 { found = 1; p = pe } else { p = te }
175 } else { p = pe }
176 }
177 return found
178}
179
180// Look up the `background` shorthand decl (prop_len 10) for an element. Local literal -> no table change.
181func _paint_lookup_bg(ctx: *PaintCtx, element_idx: i64) -> *CssComputedDecl {
182 let bname: *u8 = "background\x00" as *u8
183 var i: i64 = ctx.n_computed - 1
184 while i >= 0 {
185 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
186 if cd.element_idx == element_idx {
187 if cd.prop_len == 10 {
188 var same: i64 = 1
189 var k: i64 = 0
190 while k < 10 { if (ctx.cascade_src[cd.prop_off + k] & 0xff) != (bname[k] & 0xff) { same = 0; k = 10 } else { k = k + 1 } }
191 if same == 1 { return cd }
192 }
193 }
194 i = i - 1
195 }
196 return 0 as *CssComputedDecl
197}
198
199// Paint one box's background, if it has a HEXCOLOR background-color
200// declaration in the cascade. Returns the number of pixels written
201// (0 if no background-color declared or value isn't HEXCOLOR).
202func _paint_box_background(ctx: *PaintCtx, box_idx: i64) -> i64 {
203 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
204 let color: *CssColor = (sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
205 var ok: i64 = 0
206 // 1. background-color longhand (preferred -- it's the fallback color even when a `background` image is set)
207 let decl: *CssComputedDecl = _paint_lookup_decl(ctx, box_idx,
208 ctx.table.background_color_off, ctx.table.background_color_len)
209 if decl != (0 as *CssComputedDecl) {
210 if decl.val_kind == NX_CSS_VAL_HEXCOLOR { ok = nx_css_color_decode(ctx.cascade_src, decl.val_off, decl.val_len, color) }
211 else { if decl.val_kind == NX_CSS_VAL_IDENT {
212 ok = nx_css_color_named(ctx.cascade_src, decl.val_off, decl.val_len, color)
213 if ok != 1 { ok = nx_css_color_rgb_func(ctx.cascade_src, decl.val_off, decl.val_len, color) }
214 } }
215 }
216 // 2. `background` SHORTHAND fallback (background:red / #fff / url(x) blue) -- extract its color token.
217 if ok != 1 {
218 let bd: *CssComputedDecl = _paint_lookup_bg(ctx, box_idx)
219 if bd != (0 as *CssComputedDecl) { ok = _bg_extract_color(ctx.cascade_src, bd.val_off, bd.val_len, color) }
220 }
221 if ok != 1 { return 0 }
222 if color.a == 0 { return 0 } // transparent -> nothing to paint
223
224 return nx_paint_solid_rect(ctx.fb, b.x, b.y, b.w, b.h, color)
225}
226
227// Find a computed `color` declaration for `element_idx` (the CSS text color, NOT background-color).
228// Matched with a local "color" literal so this needs no PaintPropTable change. prop_len==5 excludes
229// "background-color" (len 16); the byte-compare excludes other 5-char props like "width".
230func _paint_color_decl(ctx: *PaintCtx, element_idx: i64) -> *CssComputedDecl {
231 let cname: *u8 = "color\x00" as *u8
232 var i: i64 = ctx.n_computed - 1
233 while i >= 0 {
234 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
235 if cd.element_idx == element_idx {
236 if cd.prop_len == 5 {
237 var same: i64 = 1
238 var k: i64 = 0
239 while k < 5 {
240 if (ctx.cascade_src[cd.prop_off + k] & 0xff) != (cname[k] & 0xff) { same = 0; k = 5 } else { k = k + 1 }
241 }
242 if same == 1 { return cd }
243 }
244 }
245 i = i - 1
246 }
247 return 0 as *CssComputedDecl
248}
249
250// Effective text color for a box: nearest ANCESTOR (inclusive) with a computed `color` (CSS color
251// inherits), decoded into `out`. Returns 1 if found, 0 to fall back to the default ink. This is the
252// reusable "inherited computed property for a text run" walk (font-size will reuse the same shape).
253func _paint_text_color(ctx: *PaintCtx, box_idx: i64, out: *CssColor) -> i64 {
254 var cur: i64 = box_idx
255 var safety: i64 = 0
256 while safety < 256 {
257 safety = safety + 1
258 if cur < 0 { return 0 }
259 let decl: *CssComputedDecl = _paint_color_decl(ctx, cur)
260 if decl != (0 as *CssComputedDecl) {
261 if decl.val_kind == NX_CSS_VAL_HEXCOLOR {
262 if nx_css_color_decode(ctx.cascade_src, decl.val_off, decl.val_len, out) == 1 { return 1 }
263 }
264 // NAMED color (color:red / white / ...) or rgb()/rgba() -> the value is an IDENT, not a hex token.
265 if decl.val_kind == NX_CSS_VAL_IDENT {
266 if nx_css_color_named(ctx.cascade_src, decl.val_off, decl.val_len, out) == 1 { return 1 }
267 if nx_css_color_rgb_func(ctx.cascade_src, decl.val_off, decl.val_len, out) == 1 { return 1 }
268 }
269 }
270 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (cur as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
271 cur = b.parent_idx
272 }
273 return 0
274}
275
276// Find a computed `font-size` declaration for `element_idx` (DIMENSION, e.g. "32px"). Matched with a
277// local "font-size" literal (prop_len==9) so no PaintPropTable change. Returns the value in px, or -1.
278func _paint_fontsize_decl_px(ctx: *PaintCtx, element_idx: i64) -> i64 {
279 let fname: *u8 = "font-size\x00" as *u8
280 var i: i64 = ctx.n_computed - 1
281 while i >= 0 {
282 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
283 if cd.element_idx == element_idx {
284 if cd.prop_len == 9 {
285 var same: i64 = 1
286 var k: i64 = 0
287 while k < 9 {
288 if (ctx.cascade_src[cd.prop_off + k] & 0xff) != (fname[k] & 0xff) { same = 0; k = 9 } else { k = k + 1 }
289 }
290 if same == 1 {
291 // parse the numeric run of the value (e.g. "32" of "32px"); unit is a separate field.
292 var v: i64 = 0
293 var any: i64 = 0
294 var j: i64 = 0
295 while j < cd.val_len {
296 let c: i64 = ctx.cascade_src[cd.val_off + j] & 0xff
297 if c >= 48 { if c <= 57 { v = v * 10 + (c - 48); any = 1 } }
298 j = j + 1
299 }
300 if any == 1 { return v }
301 }
302 }
303 }
304 i = i - 1
305 }
306 return 0 - 1
307}
308
309// Inherited effective font-size (px) for a box: nearest ANCESTOR with computed font-size, else 16.
310func _paint_fontsize_px(ctx: *PaintCtx, box_idx: i64) -> i64 {
311 var cur: i64 = box_idx
312 var safety: i64 = 0
313 while safety < 256 {
314 safety = safety + 1
315 if cur < 0 { return 16 }
316 let px: i64 = _paint_fontsize_decl_px(ctx, cur)
317 if px >= 0 { return px }
318 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (cur as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
319 cur = b.parent_idx
320 }
321 return 16
322}
323
324// Integer glyph scale for a font-size in px (5x7 bitmap -> crisp integer steps). MUST match
325// _layout_scale_for_px in nx_layout_block so painted glyph size == reserved box size (no overlap).
326func _paint_scale_for_px(px: i64) -> i64 {
327 if px < 24 { return 1 }
328 if px < 36 { return 2 }
329 return 3
330}
331
332// Does the box have computed `display: none`? If so the walk skips it AND its subtree (matches the
333// layout, which gave it zero size). Local "display"/"none" literals -> no PaintPropTable change.
334func _paint_display_none(ctx: *PaintCtx, box_idx: i64) -> i64 {
335 let dn: *u8 = "display\x00" as *u8
336 let nn: *u8 = "none\x00" as *u8
337 var i: i64 = ctx.n_computed - 1
338 while i >= 0 {
339 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
340 if cd.element_idx == box_idx {
341 if cd.prop_len == 7 {
342 var sp: i64 = 1
343 var k: i64 = 0
344 while k < 7 { if (ctx.cascade_src[cd.prop_off+k]&0xff) != (dn[k]&0xff) { sp = 0; k = 7 } else { k = k + 1 } }
345 if sp == 1 {
346 if cd.val_len == 4 {
347 var sv: i64 = 1
348 var j: i64 = 0
349 while j < 4 { if (ctx.cascade_src[cd.val_off+j]&0xff) != (nn[j]&0xff) { sv = 0; j = 4 } else { j = j + 1 } }
350 if sv == 1 { return 1 }
351 }
352 }
353 }
354 }
355 i = i - 1
356 }
357 return 0
358}
359
360// Does box_idx have a computed `prop` whose IDENT value byte-equals `val`? Generic (font-weight:bold /
361// font-style:italic / ...). Reverse-scan = cascade winner first; prop present but != val -> 0.
362func _paint_prop_ident_is(ctx: *PaintCtx, box_idx: i64, prop: *u8, prop_len: i64, val: *u8, val_len: i64) -> i64 {
363 var i: i64 = ctx.n_computed - 1
364 while i >= 0 {
365 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
366 if cd.element_idx == box_idx {
367 if cd.prop_len == prop_len {
368 var sp: i64 = 1
369 var k: i64 = 0
370 while k < prop_len { if (ctx.cascade_src[cd.prop_off+k]&0xff) != (prop[k]&0xff) { sp = 0; k = prop_len } else { k = k + 1 } }
371 if sp == 1 {
372 if cd.val_len == val_len {
373 var sv: i64 = 1
374 var j: i64 = 0
375 while j < val_len { if (ctx.cascade_src[cd.val_off+j]&0xff) != (val[j]&0xff) { sv = 0; j = val_len } else { j = j + 1 } }
376 if sv == 1 { return 1 }
377 }
378 return 0
379 }
380 }
381 }
382 i = i - 1
383 }
384 return 0
385}
386
387// Inherited boolean style: nearest ANCESTOR (inclusive) with prop==val. font-weight/font-style INHERIT,
388// so a text run under <b>/<strong> (or <i>/<em>) picks up bold/italic. Same ancestor-walk shape as
389// _paint_text_color / _paint_fontsize_px (the reusable inherited-computed-property spine).
390func _paint_inherited_style(ctx: *PaintCtx, box_idx: i64, prop: *u8, prop_len: i64, val: *u8, val_len: i64) -> i64 {
391 var cur: i64 = box_idx
392 var safety: i64 = 0
393 while safety < 256 {
394 safety = safety + 1
395 if cur < 0 { return 0 }
396 if _paint_prop_ident_is(ctx, cur, prop, prop_len, val, val_len) == 1 { return 1 }
397 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (cur as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
398 cur = b.parent_idx
399 }
400 return 0
401}
402
403// Paint a TEXT box's body bytes using its INHERITED computed `color` (blue <a> links) and INHERITED
404// font-size (scaled headings), falling back to default ink / 1x. Returns pixels written.
405func _paint_box_text(ctx: *PaintCtx, box_idx: i64) -> i64 {
406 if ctx.text_src == (0 as *u8) { return 0 }
407 if ctx.default_text_color == (0 as *CssColor) { return 0 }
408 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
409 if b.kind != NX_LAYOUT_BOX_TEXT { return 0 }
410 if b.text_len <= 0 { return 0 }
411 let text_ptr: *u8 = (ctx.text_src as nx_int + b.text_off) as *u8
412 let col: *CssColor = (sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
413 var use_col: *CssColor = ctx.default_text_color
414 if _paint_text_color(ctx, b.parent_idx, col) == 1 { use_col = col }
415 let scale: i64 = _paint_scale_for_px(_paint_fontsize_px(ctx, b.parent_idx))
416 let bold: i64 = _paint_inherited_style(ctx, b.parent_idx, "font-weight\x00" as *u8, 11, "bold\x00" as *u8, 4)
417 let italic: i64 = _paint_inherited_style(ctx, b.parent_idx, "font-style\x00" as *u8, 10, "italic\x00" as *u8, 6)
418 // WRAP text to the box width so long runs don't overflow the column (the Wikipedia text-overlap fix).
419 // Character-level wrap at line_chars = b.w/6, MATCHING nx_layout_block's TEXT-box height reservation
420 // (glyph_w=6, glyph_h=10, lines=ceil(text_len/line_chars)) -> paint fills EXACTLY the reserved lines,
421 // never spilling into sibling boxes. Word-level breaking is a later refinement.
422 let glyph_adv: i64 = 6
423 var line_chars: i64 = b.w / glyph_adv
424 if line_chars < 1 { line_chars = 1 }
425 let line_h: i64 = 10
426 var off2: i64 = 0
427 var ly: i64 = b.y
428 var total: i64 = 0
429 // WORD-level wrap via the shared breaker (same breaks the layout reserved height for) -- break at
430 // spaces, not mid-word. Skip a single leading space per line so words don't start indented.
431 while off2 < b.text_len {
432 let e: i64 = tw_next_break(text_ptr, b.text_len, line_chars, off2)
433 var s2: i64 = off2
434 if s2 < e { if (text_ptr[s2]&0xff)==32 { s2 = s2 + 1 } }
435 let n: i64 = e - s2
436 if n > 0 {
437 let chunk_ptr: *u8 = (text_ptr as nx_int + s2) as *u8
438 total = total + nx_paint_text_styled(ctx.fb, b.x, ly, chunk_ptr, n, use_col, scale, bold, italic)
439 }
440 off2 = e
441 ly = ly + line_h
442 }
443 return total
444}
445
446// ---- public API ----
447
448func nx_paint_ctx_init(ctx: *PaintCtx,
449 fb: *Framebuffer,
450 tree: *LayoutTree,
451 computed: *CssComputedDecl,
452 n_computed: i64,
453 cascade_src: *u8,
454 table: *PaintPropTable) -> i64 {
455 ctx.fb = fb
456 ctx.tree = tree
457 ctx.computed = computed
458 ctx.n_computed = n_computed
459 ctx.cascade_src = cascade_src
460 ctx.table = table
461 ctx.text_src = 0 as *u8 // text rendering off by default
462 ctx.default_text_color = 0 as *CssColor // ditto
463 ctx.boxel = 0 as *CssElement // list-marker bullets off until set
464 return 0
465}
466
467// Enable list-marker bullets: supply the box-indexed CssElement array (tags). After this, a <li> box
468// whose parent is a <ul> gets a disc bullet painted in the list indent.
469func nx_paint_ctx_set_boxel(ctx: *PaintCtx, boxel: *CssElement) -> i64 {
470 ctx.boxel = boxel
471 return 0
472}
473
474// Enable TEXT-box rendering on top of the background-only walk.
475// Once set, encountering a LayoutBox of kind NX_LAYOUT_BOX_TEXT
476// during the walk paints its body bytes via nx_paint_text using
477// the given default color (inheritance of CSS `color` is queued
478// for Phase 3b).
479func nx_paint_ctx_set_text(ctx: *PaintCtx,
480 text_src: *u8,
481 default_text_color: *CssColor) -> i64 {
482 ctx.text_src = text_src
483 ctx.default_text_color = default_text_color
484 return 0
485}
486
487// If box_idx is a <li> whose parent is a <ul>, paint a small disc bullet in the list indent (left of
488// the content). Needs ctx.boxel (tags). <ol> items are skipped (they want numbers -- a later rung).
489func _paint_li_marker(ctx: *PaintCtx, box_idx: i64) -> i64 {
490 if ctx.boxel == (0 as *CssElement) { return 0 }
491 let e: *CssElement = (ctx.boxel as *u8 + (box_idx as nx_size) * (NX_CSS_ELEMENT_BYTES as nx_size)) as *CssElement
492 if e.tag_len != 2 { return 0 }
493 if (ctx.cascade_src[e.tag_off]&0xff) != 108 { return 0 } // 'l'
494 if (ctx.cascade_src[e.tag_off+1]&0xff) != 105 { return 0 } // 'i'
495 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
496 let p: i64 = b.parent_idx
497 if p < 0 { return 0 }
498 let pe: *CssElement = (ctx.boxel as *u8 + (p as nx_size) * (NX_CSS_ELEMENT_BYTES as nx_size)) as *CssElement
499 if pe.tag_len != 2 { return 0 }
500 let p0: i64 = ctx.cascade_src[pe.tag_off] & 0xff
501 let p1: i64 = ctx.cascade_src[pe.tag_off+1] & 0xff
502 // <ul> -> disc bullet
503 if p0 == 117 { if p1 == 108 { // "ul"
504 let col: *CssColor = (sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
505 col.r = 64; col.g = 64; col.b = 64; col.a = 255
506 return nx_paint_solid_rect(ctx.fb, b.x - 8, b.y + 4, 3, 3, col)
507 } }
508 // <ol> -> 1-based number "N." right-aligned in the ol's left padding
509 if p0 == 111 { if p1 == 108 { // "ol"
510 let pb: *LayoutBox = (ctx.tree.boxes as *u8 + (p as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
511 var n: i64 = 1
512 var sib: i64 = pb.first_child_idx
513 var stop: i64 = 0
514 var guard: i64 = 0
515 while stop == 0 {
516 if sib < 0 { stop = 1 }
517 else { if sib == box_idx { stop = 1 }
518 else { if guard >= NX_MAGIC_100000 { stop = 1 }
519 else {
520 let se: *CssElement = (ctx.boxel as *u8 + (sib as nx_size) * (NX_CSS_ELEMENT_BYTES as nx_size)) as *CssElement
521 if se.tag_len == 2 { if (ctx.cascade_src[se.tag_off]&0xff)==108 { if (ctx.cascade_src[se.tag_off+1]&0xff)==105 { n = n + 1 } } }
522 let sb: *LayoutBox = (ctx.tree.boxes as *u8 + (sib as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
523 sib = sb.next_sibling_idx
524 guard = guard + 1
525 } } }
526 }
527 let tmp: *u8 = sys_mmap(24)
528 var tk: i64 = 0
529 var v: i64 = n
530 if v == 0 { tmp[0] = 48 as u8; tk = 1 }
531 while v > 0 { tmp[tk] = (48 + (v % 10)) as u8; v = v / 10; tk = tk + 1 }
532 let nb: *u8 = sys_mmap(24)
533 var j: i64 = 0
534 while j < tk { nb[j] = tmp[tk-1-j]; j = j + 1 }
535 nb[tk] = 46 as u8 // '.'
536 let slen: i64 = tk + 1
537 let col2: *CssColor = (sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
538 col2.r = 0; col2.g = 0; col2.b = 0; col2.a = 255
539 return nx_paint_text(ctx.fb, b.x - slen * 6 - 3, b.y + 2, nb, slen, col2)
540 } }
541 return 0
542}
543
544// Find the computed `border` SHORTHAND decl for box_idx (reverse cascade = winner first), or 0. The CSS
545// parser's multi-token capture stores the whole value span ("1px solid #cc0000") at val_off/val_len.
546func _paint_border_lookup(ctx: *PaintCtx, box_idx: i64) -> *CssComputedDecl {
547 let nm: *u8 = "border\x00" as *u8
548 var i: i64 = ctx.n_computed - 1
549 while i >= 0 {
550 let cd: *CssComputedDecl = (ctx.computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
551 if cd.element_idx == box_idx {
552 if cd.prop_len == 6 {
553 var same: i64 = 1
554 var k: i64 = 0
555 while k < 6 { if (ctx.cascade_src[cd.prop_off + k] & 0xff) != (nm[k] & 0xff) { same = 0; k = 6 } else { k = k + 1 } }
556 if same == 1 { return cd }
557 }
558 }
559 i = i - 1
560 }
561 return 0 as *CssComputedDecl
562}
563func _hex_is(c: i64) -> i64 {
564 if c >= 48 { if c <= 57 { return 1 } }
565 if c >= 97 { if c <= 102 { return 1 } }
566 if c >= 65 { if c <= 70 { return 1 } }
567 return 0
568}
569// Paint a box's `border` shorthand as 4 solid edges INSIDE the box rect: width = leading integer of the
570// value; color = the first #hex token (default black if none); style assumed solid. Returns pixels
571// written (0 if no border). border-box REFLOW (border growing the box) + non-solid styles = later rungs;
572// this paints the edges within b's existing rect, composing nx_paint_solid_rect + nx_css_color_decode.
573func _paint_box_border(ctx: *PaintCtx, box_idx: i64) -> i64 {
574 let decl: *CssComputedDecl = _paint_border_lookup(ctx, box_idx)
575 if decl == (0 as *CssComputedDecl) { return 0 }
576 let src: *u8 = ctx.cascade_src
577 let end: i64 = decl.val_off + decl.val_len
578 var bw: i64 = 0
579 var any: i64 = 0
580 var p: i64 = decl.val_off
581 var rd: i64 = 1
582 while rd == 1 { if p < end { let c: i64 = src[p]&0xff; if c>=48 { if c<=57 { bw=bw*10+(c-48); any=1; p=p+1 } else { rd=0 } } else { rd=0 } } else { rd=0 } }
583 if any == 0 { bw = 1 }
584 if bw <= 0 { return 0 }
585 let color: *CssColor = (sys_mmap(NX_CSS_COLOR_BYTES as nx_size)) as *CssColor
586 color.r = 0; color.g = 0; color.b = 0; color.a = 255
587 var hp: i64 = decl.val_off
588 var fnd: i64 = 0
589 while fnd == 0 {
590 if hp >= end { fnd = 2 }
591 else { if (src[hp]&0xff)==35 { fnd = 1 } else { hp = hp + 1 } }
592 }
593 if fnd == 1 {
594 var hl: i64 = 0
595 var q: i64 = hp + 1
596 while q < end { if _hex_is(src[q]&0xff)==1 { hl = hl + 1; q = q + 1 } else { q = end } }
597 nx_css_color_decode(src, hp + 1, hl, color)
598 } else {
599 // No hex token -> scan the value's space-separated WORDS for a NAMED color ("1px solid gray").
600 var wp: i64 = decl.val_off
601 var done: i64 = 0
602 while done == 0 {
603 var ws: i64 = 1
604 while ws == 1 { if wp >= end { ws = 0 } else { let c: i64 = src[wp]&0xff; if c==32 { wp=wp+1 } else { if c==9 { wp=wp+1 } else { ws=0 } } } }
605 if wp >= end { done = 1 }
606 else {
607 var we: i64 = wp
608 var sd: i64 = 0
609 while sd == 0 { if we >= end { sd = 1 } else { let c: i64 = src[we]&0xff; if c==32 { sd=1 } else { if c==9 { sd=1 } else { we=we+1 } } } }
610 if nx_css_color_named(src, wp, we - wp, color) == 1 { done = 1 } else { wp = we }
611 }
612 }
613 }
614 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (box_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
615 if b.w <= 0 { return 0 }
616 if b.h <= 0 { return 0 }
617 var bwid: i64 = bw
618 if bwid > b.h { bwid = b.h }
619 if bwid > b.w { bwid = b.w }
620 var n: i64 = 0
621 n = n + nx_paint_solid_rect(ctx.fb, b.x, b.y, b.w, bwid, color)
622 n = n + nx_paint_solid_rect(ctx.fb, b.x, b.y + b.h - bwid, b.w, bwid, color)
623 n = n + nx_paint_solid_rect(ctx.fb, b.x, b.y, bwid, b.h, color)
624 n = n + nx_paint_solid_rect(ctx.fb, b.x + b.w - bwid, b.y, bwid, b.h, color)
625 return n
626}
627
628// Walk the layout tree in document order (DFS pre-order) starting at
629// `root_idx`, painting each box's background-color into the
630// framebuffer. Returns total pixels written across the whole tree.
631//
632// Document-order walk = ancestors paint first, then descendants
633// (children paint OVER parents). This implements the document-order
634// stacking semantics in lieu of z-index (queued for Phase 4b).
635func nx_paint_walk_layout(ctx: *PaintCtx, root_idx: i64) -> i64 {
636 if root_idx < 0 { return 0 }
637 if root_idx >= ctx.tree.count { return 0 }
638 if _paint_display_none(ctx, root_idx) == 1 { return 0 } // display:none -> skip box + whole subtree
639
640 let bg_written: i64 = _paint_box_background(ctx, root_idx)
641 let bd_written: i64 = _paint_box_border(ctx, root_idx)
642 let tx_written: i64 = _paint_box_text(ctx, root_idx)
643 let mk_written: i64 = _paint_li_marker(ctx, root_idx)
644 let written: i64 = bg_written + bd_written + tx_written + mk_written
645
646 let b: *LayoutBox = (ctx.tree.boxes as *u8 + (root_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
647 var total: i64 = written
648 var child_idx: i64 = b.first_child_idx
649 var safety: i64 = 0
650 let MAX_ITER: i64 = NX_MAGIC_65536
651 var keep: i64 = 1
652 while keep == 1 {
653 if safety >= MAX_ITER { keep = 0 }
654 else {
655 safety = safety + 1
656 if child_idx < 0 { keep = 0 }
657 else {
658 total = total + nx_paint_walk_layout(ctx, child_idx)
659 let c: *LayoutBox = (ctx.tree.boxes as *u8 + (child_idx as nx_size) * (NX_LAYOUT_BOX_BYTES as nx_size)) as *LayoutBox
660 child_idx = c.next_sibling_idx
661 }
662 }
663 }
664 return total
665}