nx_browser_demo_test.nx source
↩ module page · 583 lines · 24938 B
1// nx_browser_demo_test.nx -- Session 3 of bits-up browser arc.
2//
3// Pops a real Windows window via WSLg and paints actual browser
4// chrome: gray URL bar with back/forward buttons + text area below.
5// Subscribes to Expose events; redraws when needed. Exits after
6// a 10-second visible window so user can see the layout.
7//
8// Compose-only: nx_x11_connect + nx_x11_window + nx_x11_paint.
9//
10// expect_exit: 0.
11// license_tier: ORIGINAL
12
13import "nx_syscalls.nx"
14import "nx_x11_connect.nx"
15import "nx_x11_window.nx"
16import "nx_x11_paint.nx"
17import "nx_x11_paint_text_5x7.nx"
18import "nx_x11_pixmap.nx"
19import "nx_x11_screenshot.nx"
20import "nx_render_journal.nx"
21import "nx_ux_standard.nx"
22import "nx_ux_score.nx"
23import "nx_ux_triangulate.nx"
24
25// SIGPIPE handling -- writes to a closed/disconnected X11 socket
26// generate SIGPIPE by default, which kills our process with signal
27// 13 (exit code 141). Setting SIG_IGN via rt_sigaction makes the
28// failing write just return -EPIPE instead, so the event loop can
29// exit cleanly when the user closes the window.
30//
31// rt_sigaction(int signum, const struct sigaction *act, ...
32// const struct sigaction *oldact, size_t sigsetsize)
33// sigaction struct (RV64 layout): sa_handler(8) + sa_flags(8) +
34// sa_restorer(8) + sa_mask(8)
35// SIG_IGN = 1, SIGPIPE = 13, NSIG/8 = 8 (sigset size in bytes)
36const NX_SIGPIPE: i64 = 13
37const NX_SIG_IGN: i64 = 1
38const NX_SYS_RT_SIGACTION: i64 = 134
39
40func _ignore_sigpipe() -> i64 {
41 let sa: *u8 = sys_mmap(64)
42 var i: i64 = 0
43 while i < 64 { sa[i] = 0; i = i + 1 }
44 // sa_handler = SIG_IGN at offset 0
45 sa[0] = NX_SIG_IGN as u8
46 __syscall(NX_SYS_RT_SIGACTION, NX_SIGPIPE, sa as i64, 0, 8, 0, 0)
47 return 0
48}
49
50func dump_dec(l0: i64, l1: i64, v: i64) -> i64 {
51 let lab: *u8 = sys_mmap(8)
52 lab[0]=l0 as u8; lab[1]=l1 as u8; lab[2]=0x3D
53 sys_write(2, lab, 3)
54 var av: i64 = v
55 if av < 0 {
56 let neg: *u8 = sys_mmap(8); neg[0]=0x2D; sys_write(2, neg, 1)
57 av = 0 - av
58 }
59 let buf: *u8 = sys_mmap(16)
60 var pos: i64 = 0
61 if av == 0 { buf[0]=0x30; pos = 1 }
62 var x: i64 = av
63 while x > 0 {
64 buf[pos] = (0x30 + (x % 10)) as u8
65 x = x / 10; pos = pos + 1
66 }
67 let out: *u8 = sys_mmap(16); var oi: i64 = 0
68 while oi < pos { out[oi] = buf[pos - 1 - oi]; oi = oi + 1 }
69 sys_write(2, out, pos)
70 let nl: *u8 = sys_mmap(8); nl[0]=0x0A; sys_write(2, nl, 1)
71 return 0
72}
73
74// Paint the browser chrome. Window is 800x500.
75// RECTANGLES-ONLY (no text yet). ImageText8 requires the GC to have
76// a font set; that wiring is Session 3b (OpenFont + SetFont, or
77// alternatively rasterise our 5x7 bitmap glyphs via PutImage).
78// Today's goal: prove visible chrome structure -- header bar, buttons,
79// URL box, content paragraphs, status bar -- all visible as colored
80// rectangles on the real Windows desktop.
81// Navigation state -- 10-slot history ring with a current index.
82// Each "URL" is represented by a synthetic color (different shade
83// per index) so the user can SEE position-in-history change as
84// they click back/forward. Real text URLs land in Session 6.
85struct NavState {
86 history_len: i64, // how many URLs visited so far (1..10)
87 history_idx: i64, // current position (0..history_len-1)
88 last_clicked: i64, // 0=back, 1=fwd, 2=reload, -1=none
89 flash_iters: i64, // remaining repaint iterations to flash
90 focused_idx: i64, // WCAG 2.4.7 focus -- which button has kb focus (-1 = none)
91 last_key: i64 // diagnostic
92}
93
94const NX_NAV_STATE_BYTES: i64 = 48
95
96// Per-index synthetic content color (cycles through a palette).
97func nav_color_for(idx: i64) -> i64 {
98 let palette_len: i64 = 6
99 let slot: i64 = idx - (idx / palette_len) * palette_len
100 if slot == 0 { return 0x00CC2233 } // red
101 if slot == 1 { return 0x002288CC } // blue
102 if slot == 2 { return 0x0033AA66 } // green
103 if slot == 3 { return 0x00FF8800 } // orange
104 if slot == 4 { return 0x009955CC } // purple
105 return 0x00666666 // gray
106}
107
108// Hit-test a click point against the 3 chrome buttons.
109// Returns 0=back, 1=forward, 2=reload, -1=miss.
110func hit_button(x: i64, y: i64) -> i64 {
111 if y < 4 { return 0 - 1 }
112 if y >= 36 { return 0 - 1 }
113 if x >= 4 { if x < 36 { return 0 } }
114 if x >= 40 { if x < 72 { return 1 } }
115 if x >= 76 { if x < 108 { return 2 } }
116 return 0 - 1
117}
118
119// Responsive chrome layout -- all positions/sizes computed relative
120// to the current window (w, h). URL bar pinned to top; status bar
121// pinned to bottom; content stretches to fill in between.
122// Paint a rect AND log it to the render journal with a semantic
123// intent tag. The journal is the substrate-honest alternative to
124// screen-scraping for introspection. j == NULL skips journaling.
125func paint_and_log(c: *X11Conn, wid: i64, j: *RenderJournal,
126 intent: *u8,
127 gc_color: i64,
128 x: i64, y: i64, w: i64, h: i64) -> i64 {
129 let gc: i64 = nx_x11_create_gc(c, wid, gc_color)
130 nx_x11_fill_rect(c, wid, gc, x, y, w, h)
131 if (j as i64) != 0 {
132 nx_journal_log_rect(j, intent, x, y, w, h, gc_color)
133 }
134 return 0
135}
136
137func paint_chrome(c: *X11Conn, wid: i64, w: i64, h: i64,
138 nav: *NavState, j: *RenderJournal) -> i64 {
139 let has_j: i64 = (j as i64)
140
141 // White content background fills the full window.
142 let gc_white: i64 = nx_x11_create_gc(c, wid, 0x00FFFFFF)
143 nx_x11_fill_rect(c, wid, gc_white, 0, 0, w, h)
144 if has_j != 0 { nx_journal_log_rect(j, "background" as *u8, 0, 0, w, h, 0x00FFFFFF) }
145
146 // Gray URL bar (full-width, 40px tall, anchored to top).
147 let gc_gray: i64 = nx_x11_create_gc(c, wid, 0x00334455)
148 nx_x11_fill_rect(c, wid, gc_gray, 0, 0, w, 40)
149 if has_j != 0 { nx_journal_log_rect(j, "url_bar_bg" as *u8, 0, 0, w, 40, 0x00334455) }
150
151 // 3 buttons. Each renders as the base color UNLESS it was the
152 // most recent click AND flash_iters > 0 -- in that case render
153 // brighter (yellow flash) so the user sees their click registered.
154 let base: i64 = 0x004488CC
155 let flash: i64 = 0x00FFCC22
156 var c0: i64 = base
157 var c1: i64 = base
158 var c2: i64 = base
159 // Dim back button if no history to go back to.
160 if nav.history_idx == 0 { c0 = 0x00779999 }
161 // Dim forward if at end.
162 if nav.history_idx + 1 >= nav.history_len { c1 = 0x00779999 }
163 if nav.flash_iters > 0 {
164 if nav.last_clicked == 0 { c0 = flash }
165 if nav.last_clicked == 1 { c1 = flash }
166 if nav.last_clicked == 2 { c2 = flash }
167 }
168 let gc_b0: i64 = nx_x11_create_gc(c, wid, c0)
169 let gc_b1: i64 = nx_x11_create_gc(c, wid, c1)
170 let gc_b2: i64 = nx_x11_create_gc(c, wid, c2)
171 nx_x11_fill_rect(c, wid, gc_b0, 4, 4, 32, 32)
172 nx_x11_fill_rect(c, wid, gc_b1, 40, 4, 32, 32)
173 nx_x11_fill_rect(c, wid, gc_b2, 76, 4, 32, 32)
174 if has_j != 0 {
175 nx_journal_log_rect(j, "back_button" as *u8, 4, 4, 32, 32, c0)
176 nx_journal_log_rect(j, "forward_button" as *u8, 40, 4, 32, 32, c1)
177 nx_journal_log_rect(j, "reload_button" as *u8, 76, 4, 32, 32, c2)
178 }
179
180 // URL input box: starts after buttons (x=120) + ends 10 px from right.
181 let url_x: i64 = 120
182 var url_w: i64 = w - url_x - 10
183 if url_w < 1 { url_w = 1 }
184 nx_x11_fill_rect(c, wid, gc_white, url_x, 6, url_w, 28)
185 let gc_black: i64 = nx_x11_create_gc(c, wid, 0x00000000)
186 nx_x11_fill_rect(c, wid, gc_black, url_x, 6, url_w, 1)
187 nx_x11_fill_rect(c, wid, gc_black, url_x, 33, url_w, 1)
188 nx_x11_fill_rect(c, wid, gc_black, url_x, 6, 1, 28)
189 nx_x11_fill_rect(c, wid, gc_black, url_x + url_w - 1, 6, 1, 28)
190 if has_j != 0 {
191 nx_journal_log_rect(j, "url_field_bg" as *u8, url_x, 6, url_w, 28, 0x00FFFFFF)
192 nx_journal_log_rect(j, "url_field_border" as *u8, url_x, 6, url_w, 28, 0x00000000)
193 }
194
195 // History breadcrumb -- one small bar inside the URL field per
196 // history slot, current slot rendered in the page's title color
197 // for visible "you-are-here" feedback. Hovers under the URL
198 // field would normally show the URL text; that lands once text
199 // rendering ships.
200 let bc_pad: i64 = 4
201 let bc_x: i64 = url_x + bc_pad
202 let bc_y: i64 = 14
203 let bc_h: i64 = 12
204 let bc_w_each: i64 = 14
205 let bc_gap: i64 = 2
206 var hi: i64 = 0
207 while hi < nav.history_len {
208 var slot_color: i64 = 0x00CCCCCC
209 if hi == nav.history_idx { slot_color = nav_color_for(hi) }
210 let gc_slot: i64 = nx_x11_create_gc(c, wid, slot_color)
211 let sx: i64 = bc_x + hi * (bc_w_each + bc_gap)
212 nx_x11_fill_rect(c, wid, gc_slot, sx, bc_y, bc_w_each, bc_h)
213 if has_j != 0 {
214 var lbl: *u8 = "breadcrumb_past" as *u8
215 if hi == nav.history_idx { lbl = "breadcrumb_current" as *u8 }
216 nx_journal_log_rect(j, lbl, sx, bc_y, bc_w_each, bc_h, slot_color)
217 }
218 hi = hi + 1
219 }
220
221 // Content area starts at y=50 (10 px below URL bar) and stretches.
222 // Title color cycles with history_idx so the user can SEE the
223 // current "page" change when they navigate.
224 let content_x: i64 = 16
225 var content_w: i64 = w - 32
226 if content_w < 1 { content_w = 1 }
227 let title_w: i64 = 200
228 let title_color: i64 = nav_color_for(nav.history_idx)
229 let gc_title: i64 = nx_x11_create_gc(c, wid, title_color)
230 nx_x11_fill_rect(c, wid, gc_title, content_x, 60, title_w, 24)
231 if has_j != 0 {
232 nx_journal_log_rect(j, "title_block" as *u8, content_x, 60, title_w, 24, title_color)
233 }
234
235 // Real text using the bits-up 5x7 bitmap font. Each glyph is
236 // rendered as px_size=2 (10x14 visible per char incl. spacing),
237 // dark text on the white content area.
238 let gc_text: i64 = nx_x11_create_gc(c, wid, 0x00112233)
239 let title_text: *u8 = "Example Domain"
240 nx_x11_paint_text_5x7(c, wid, gc_text, content_x + 4, 64, title_text, 14, 2)
241 let p1_text: *u8 = "This domain is for use in documentation examples."
242 nx_x11_paint_text_5x7(c, wid, gc_text, content_x, 100, p1_text, 49, 2)
243 let p2_text: *u8 = "Avoid use in operations."
244 nx_x11_paint_text_5x7(c, wid, gc_text, content_x, 130, p2_text, 24, 2)
245 if has_j != 0 {
246 nx_journal_log_rect(j, "text_title" as *u8, content_x + 4, 64, 168, 14, 0x00112233)
247 nx_journal_log_rect(j, "text_para_0" as *u8, content_x, 100, 588, 14, 0x00112233)
248 nx_journal_log_rect(j, "text_para_1" as *u8, content_x, 130, 288, 14, 0x00112233)
249 }
250
251 // Link block.
252 let gc_link: i64 = nx_x11_create_gc(c, wid, 0x00FF8800)
253 nx_x11_fill_rect(c, wid, gc_link, content_x, 210, 220, 12)
254 if has_j != 0 {
255 nx_journal_log_rect(j, "link_block" as *u8, content_x, 210, 220, 12, 0x00FF8800)
256 }
257
258 // Status bar pinned to BOTTOM (20 px tall, full width).
259 let status_y: i64 = h - 20
260 if status_y > 0 {
261 nx_x11_fill_rect(c, wid, gc_gray, 0, status_y, w, 20)
262 if has_j != 0 {
263 nx_journal_log_rect(j, "status_bar" as *u8, 0, status_y, w, 20, 0x00334455)
264 }
265 }
266
267 // ---- WCAG 2.4.7 focus indicator ---------------------------------
268 // Paint a 2-px-thick high-contrast yellow ring around the focused
269 // button so keyboard users can see where they are. Composed of
270 // 4 thin rects (top/bottom/left/right) AROUND the button so the
271 // button's own pixel area stays intact.
272 if nav.focused_idx >= 0 {
273 let fy: i64 = 2
274 var fx: i64 = 4 + nav.focused_idx * 36 // back=4, fwd=40, reload=76
275 if nav.focused_idx == 1 { fx = 40 }
276 if nav.focused_idx == 2 { fx = 76 }
277 let fw: i64 = 36
278 let fh: i64 = 36
279 let gc_focus: i64 = nx_x11_create_gc(c, wid, 0x00FFCC22) // yellow
280 nx_x11_fill_rect(c, wid, gc_focus, fx, fy, fw, 2) // top
281 nx_x11_fill_rect(c, wid, gc_focus, fx, fy + fh - 2, fw, 2) // bottom
282 nx_x11_fill_rect(c, wid, gc_focus, fx, fy, 2, fh) // left
283 nx_x11_fill_rect(c, wid, gc_focus, fx + fw - 2, fy, 2, fh) // right
284 if has_j != 0 {
285 nx_journal_log_rect(j, "focus_indicator" as *u8, fx, fy, fw, fh, 0x00FFCC22)
286 }
287 }
288
289 // ---- WCAG 4.1.2 name/role/value: emit a11y_role_* intents -------
290 // The accessibility tree IS the journal entries with these tags;
291 // an AT-bridge future primitive walks them to expose to screen
292 // readers. Today we just declare the semantic roles so the
293 // triangulator can score them.
294 if has_j != 0 {
295 nx_journal_log_rect(j, "a11y_role_button_back" as *u8, 4, 4, 32, 32, 0)
296 nx_journal_log_rect(j, "a11y_role_button_forward" as *u8, 40, 4, 32, 32, 0)
297 nx_journal_log_rect(j, "a11y_role_button_reload" as *u8, 76, 4, 32, 32, 0)
298 nx_journal_log_rect(j, "a11y_role_textbox_url" as *u8, 120, 6, w - 130, 28, 0)
299 nx_journal_log_rect(j, "a11y_role_region_content" as *u8, 0, 40, w, h - 60, 0)
300 nx_journal_log_rect(j, "a11y_role_status" as *u8, 0, h - 20, w, 20, 0)
301 }
302 return 0
303}
304
305func main() -> i64 {
306 // Ignore SIGPIPE so we exit cleanly when the user closes the
307 // window and writes start failing.
308 _ignore_sigpipe()
309
310 let rc: i64 = nx_x11_connect_local(0)
311 if rc < 0 { return 1 }
312 let c: *X11Conn = rc as *X11Conn
313 dump_dec(0x46, 0x44, c.fd)
314
315 // Subscribe to Expose + StructureNotify + ButtonPress + KeyPress.
316 let event_mask: i64 = NX_X11_EVENT_EXPOSURE
317 | NX_X11_EVENT_STRUCTURE
318 | NX_X11_EVENT_BUTTON_PRESS
319 | NX_X11_EVENT_KEY_PRESS
320 let bg: i64 = 0x00F0F0F0
321 var win_w: i64 = 800
322 var win_h: i64 = 500
323
324 // Navigation state -- start with 3 history entries so back/forward
325 // are immediately exercisable.
326 let nav_raw: *u8 = sys_mmap(NX_NAV_STATE_BYTES + 16)
327 let nav: *NavState = nav_raw as *NavState
328 nav.history_len = 3
329 nav.history_idx = 1 // start in middle so both arrows work
330 nav.last_clicked = 0 - 1
331 nav.flash_iters = 0
332 nav.focused_idx = 0 // Tab cycles; start on back button
333 nav.last_key = 0
334 let wid: i64 = nx_x11_create_window(c, 100, 100, win_w, win_h, bg, event_mask)
335 if wid < 0 { return 2 }
336 dump_dec(0x57, 0x49, wid)
337
338 let title: *u8 = "Nishi Bits-Up Browser"
339 nx_x11_set_wm_name(c, wid, title, 21)
340
341 if nx_x11_map_window(c, wid) < 0 { return 3 }
342 if nx_x11_flush_via_get_input_focus(c) < 0 { return 4 }
343
344 // ---- Substrate-honest UX/CX intelligence -------------------------
345 // Allocate a structured render journal. Every paint logs its
346 // semantic intent (e.g. "back_button", "url_bar_bg") + coords
347 // + color into the journal -- NOT screen-scraped from rendered
348 // pixels. Substrate truth lives in this data, not in pixel
349 // observation. Dump to /tmp/nishi_render_journal.log so the
350 // developer (or Claude via Read) can inspect what was drawn
351 // and WHY without snooping on the user's screen.
352 let j: *RenderJournal = nx_journal_new()
353
354 // ---- Snapshot pass: paint to pixmap + dump PPM as backup channel
355 let snap_w: i64 = 800
356 let snap_h: i64 = 500
357 let pix: i64 = nx_x11_create_pixmap(c, wid, snap_w, snap_h, c.root_depth)
358 if pix >= 0 {
359 paint_chrome(c, pix, snap_w, snap_h, nav, j)
360 nx_x11_flush_via_get_input_focus(c)
361 let path: *u8 = "/tmp/nishi_render.ppm\x00"
362 let sv: i64 = nx_x11_save_drawable_as_ppm(c, pix, snap_w, snap_h, path)
363 dump_dec(0x53, 0x4E, sv)
364 nx_x11_free_pixmap(c, pix)
365 }
366 // Dump the journal -- the PRIMARY introspection channel.
367 let log_path: *u8 = "/tmp/nishi_render_journal.log\x00"
368 nx_journal_dump_to_file(j, log_path)
369
370 // ---- UX Triangulation: NN/g + WCAG + Apple HIG + ISO 9241 -------
371 // Score the journal against every clause we have a rubric for,
372 // then run the ≥3-distinct-sources gate. Writes a structured
373 // report to /tmp/nishi_ux_triangulation.log so anyone (operator,
374 // Claude, future Nishi tooling) can audit the S-class claim.
375 let clause_ids_raw: *u8 = sys_mmap(64 * 8)
376 let clause_ids: *i64 = clause_ids_raw as *i64
377 clause_ids[0] = NX_UX_NN1_VISIBILITY
378 clause_ids[1] = NX_UX_NN3_USER_CONTROL
379 clause_ids[2] = NX_UX_NN4_CONSISTENCY
380 clause_ids[3] = NX_UX_NN6_RECOGNITION
381 clause_ids[4] = NX_UX_APPLE_FEEDBACK
382 clause_ids[5] = NX_UX_APPLE_CLARITY
383 clause_ids[6] = NX_UX_ISO_SELFDESCRIPTIVE
384 clause_ids[7] = NX_UX_WCAG_2_1_1_KEYBOARD
385 clause_ids[8] = NX_UX_WCAG_2_4_7_FOCUS
386 clause_ids[9] = NX_UX_WCAG_4_1_2_NRV
387 clause_ids[10] = NX_UX_NN5_ERROR_PREVENTION
388 clause_ids[11] = NX_UX_W3C_HTML_SEMANTIC
389 clause_ids[12] = NX_UX_W3C_ARIA_ROLES
390 clause_ids[13] = NX_UX_W3C_ARIA_LABELS
391 clause_ids[14] = NX_UX_W3C_DOM_TREE
392 clause_ids[15] = NX_UX_W3C_CSS_CONTRAST
393 clause_ids[16] = NX_UX_MS_INCL_EXCLUSION
394 let n_clauses: i64 = 17
395
396 let scores_raw: *u8 = sys_mmap(n_clauses * NX_UX_SCORE_BYTES + 16)
397 let scores: *UxScore = scores_raw as *UxScore
398
399 // Open the report file once.
400 let report_path: *u8 = "/tmp/nishi_ux_triangulation.log\x00"
401 let rfd: i64 = sys_openat_wr(report_path, 0x1A4)
402 if rfd > 0 {
403 sys_write(rfd, "---- Nishi UX triangulation report ----\n" as *u8, 40)
404 }
405
406 let note: *u8 = sys_mmap(256)
407 var i: i64 = 0
408 while i < n_clauses {
409 let cid: i64 = clause_ids[i]
410 let s: i64 = nx_ux_score_clause(j, cid, note, 256)
411 let sx: *UxScore = _ux_at(scores, i)
412 sx.clause_id = cid
413 sx.score = s
414 if rfd > 0 {
415 // line: " [src] clause-label : score=N"
416 let src: i64 = nx_ux_clause_source(cid)
417 let line: *u8 = sys_mmap(256)
418 line[0] = 0x20; line[1] = 0x20; line[2] = 0x5B
419 sys_write(rfd, line, 3)
420 let src_lbl: *u8 = nx_ux_source_label(src)
421 var k: i64 = 0
422 while src_lbl[k] != 0 { sys_write(rfd, src_lbl + k, 1); k = k + 1 }
423 sys_write(rfd, "] " as *u8, 2)
424 let lbl: *u8 = nx_ux_clause_label(cid)
425 k = 0
426 while lbl[k] != 0 { sys_write(rfd, lbl + k, 1); k = k + 1 }
427 sys_write(rfd, " : score=" as *u8, 9)
428 // decimal score
429 let d: *u8 = sys_mmap(8)
430 if s >= 10 {
431 d[0] = 0x31; d[1] = 0x30; sys_write(rfd, d, 2)
432 } else {
433 d[0] = (0x30 + s) as u8; sys_write(rfd, d, 1)
434 }
435 sys_write(rfd, "\n" as *u8, 1)
436 }
437 i = i + 1
438 }
439
440 let verdict: i64 = nx_ux_triangulate(scores, n_clauses)
441 if rfd > 0 {
442 sys_write(rfd, "VERDICT (global): " as *u8, 18)
443 let vlbl: *u8 = nx_ux_verdict_label(verdict)
444 var k: i64 = 0
445 while vlbl[k] != 0 { sys_write(rfd, vlbl + k, 1); k = k + 1 }
446 sys_write(rfd, "\n\n--- per-cluster verdicts ---\n" as *u8, 30)
447 var cl: i64 = 1
448 while cl < NX_UX_CLUSTER_N {
449 let cv: i64 = nx_ux_triangulate_cluster(scores, n_clauses, cl)
450 sys_write(rfd, " cluster=" as *u8, 10)
451 let clbl: *u8 = nx_ux_cluster_label(cl)
452 k = 0
453 while clbl[k] != 0 { sys_write(rfd, clbl + k, 1); k = k + 1 }
454 sys_write(rfd, " : " as *u8, 3)
455 let vlbl2: *u8 = nx_ux_verdict_label(cv)
456 k = 0
457 while vlbl2[k] != 0 { sys_write(rfd, vlbl2 + k, 1); k = k + 1 }
458 sys_write(rfd, "\n" as *u8, 1)
459 cl = cl + 1
460 }
461 sys_close(rfd)
462 }
463 dump_dec(0x55, 0x58, verdict) // UX= triangulation verdict
464
465 paint_chrome(c, wid, win_w, win_h, nav, 0 as *RenderJournal)
466 if nx_x11_flush_via_get_input_focus(c) < 0 { return 5 }
467
468 // Event loop.
469 let evt: *u8 = sys_mmap(64)
470 var n_iter: i64 = 0
471 let MAX_ITER: i64 = 100000
472 var keep: i64 = 1
473 while keep == 1 {
474 if n_iter >= MAX_ITER { keep = 0 }
475 if nx_x11_read_event(c, evt) < 0 { keep = 0 }
476 let kind: i64 = (evt[0] & 0xff) as i64
477 var need_repaint: i64 = 0
478 // 12 = Expose -- redraw current chrome
479 if kind == 12 { need_repaint = 1 }
480 // 22 = ConfigureNotify -- window resized
481 if kind == 22 {
482 let nw: i64 = ((evt[20] & 0xff) as i64)
483 | (((evt[21] & 0xff) as i64) << 8)
484 let nh: i64 = ((evt[22] & 0xff) as i64)
485 | (((evt[23] & 0xff) as i64) << 8)
486 if nw > 0 { win_w = nw }
487 if nh > 0 { win_h = nh }
488 need_repaint = 1
489 }
490 // 4 = ButtonPress -- bytes 32-33 = root_x, 34-35 = root_y,
491 // 36-37 = event_x, 38-39 = event_y, 41=button.
492 // But the X protocol event layout puts event_x/y at offsets
493 // 24-25 / 26-27. Use those for window-relative coords.
494 // 2 = KeyPress -- byte 1 holds keycode. Hardcoded XKB
495 // standard PC/AT keycodes: Tab=23, Enter=36, Space=65.
496 // Tab cycles focus across the 3 buttons; Enter/Space
497 // activates the focused button using the SAME dispatch
498 // as ButtonPress (WCAG 2.1.1 keyboard accessible).
499 if kind == 2 {
500 let keycode: i64 = (evt[1] & 0xff) as i64
501 nav.last_key = keycode
502 // Tab cycles forward (Shift+Tab variant queued).
503 if keycode == 23 {
504 nav.focused_idx = (nav.focused_idx + 1) - ((nav.focused_idx + 1) / 3) * 3
505 need_repaint = 1
506 }
507 // Enter or Space activate the focused button.
508 if keycode == 36 {
509 if nav.focused_idx >= 0 {
510 let btn: i64 = nav.focused_idx
511 nav.last_clicked = btn
512 nav.flash_iters = 1
513 if btn == 0 { if nav.history_idx > 0 { nav.history_idx = nav.history_idx - 1 } }
514 if btn == 1 { if nav.history_idx + 1 < nav.history_len { nav.history_idx = nav.history_idx + 1 } }
515 if btn == 2 { if nav.history_len < 10 {
516 nav.history_len = nav.history_len + 1
517 nav.history_idx = nav.history_len - 1
518 } }
519 need_repaint = 1
520 }
521 }
522 if keycode == 65 {
523 if nav.focused_idx >= 0 {
524 let btn: i64 = nav.focused_idx
525 nav.last_clicked = btn
526 nav.flash_iters = 1
527 if btn == 0 { if nav.history_idx > 0 { nav.history_idx = nav.history_idx - 1 } }
528 if btn == 1 { if nav.history_idx + 1 < nav.history_len { nav.history_idx = nav.history_idx + 1 } }
529 if btn == 2 { if nav.history_len < 10 {
530 nav.history_len = nav.history_len + 1
531 nav.history_idx = nav.history_len - 1
532 } }
533 need_repaint = 1
534 }
535 }
536 }
537 if kind == 4 {
538 let ex: i64 = ((evt[24] & 0xff) as i64)
539 | (((evt[25] & 0xff) as i64) << 8)
540 let ey: i64 = ((evt[26] & 0xff) as i64)
541 | (((evt[27] & 0xff) as i64) << 8)
542 let btn: i64 = hit_button(ex, ey)
543 if btn >= 0 {
544 nav.last_clicked = btn
545 nav.flash_iters = 1
546 // back: idx-- (clamped)
547 if btn == 0 {
548 if nav.history_idx > 0 {
549 nav.history_idx = nav.history_idx - 1
550 }
551 }
552 // forward: idx++ (clamped)
553 if btn == 1 {
554 if nav.history_idx + 1 < nav.history_len {
555 nav.history_idx = nav.history_idx + 1
556 }
557 }
558 // reload: append a new history entry after current (truncating forward stack)
559 if btn == 2 {
560 if nav.history_len < 10 {
561 nav.history_len = nav.history_len + 1
562 nav.history_idx = nav.history_len - 1
563 }
564 }
565 need_repaint = 1
566 }
567 }
568 if need_repaint == 1 {
569 paint_chrome(c, wid, win_w, win_h, nav)
570 nx_x11_flush_via_get_input_focus(c)
571 // Decay flash on next paint.
572 if nav.flash_iters > 0 {
573 nav.flash_iters = nav.flash_iters - 1
574 }
575 }
576 // 17 = DestroyNotify -- user closed window
577 if kind == 17 { keep = 0 }
578 n_iter = n_iter + 1
579 }
580
581 sys_close(c.fd)
582 return 0
583}