code wiki / _hdl_build / nx_editor_canvas.nx

nx_editor_canvas.nx source

↩ module page · 287 lines · 14791 B

1// nx_editor_canvas.nx -- the EDITOR engine: the sovereign, gateable CORE of a visual editor, as data+logic so it can 2// be PROVEN without a browser. A canvas is a flat i64 array of N elements, 5 fields each: [kind, x, y, w, h]. 3// Operations: snap-all-to-grid, align (L/R/T/B/center-h/middle-v), distribute-horizontally (even left edges), nudge, 4// and one-deep undo (snapshot/restore). The surface is rendered NO-JS (ec_render); editing = server ops (apply an op, 5// re-render), never a client-side JS canvas -- a JS drag-drop canvas is the JS-heavy approach the Nishi ecosystem 6// REJECTS, not a maturity bar to chase. SOVEREIGN + no-JS + governed. REUSE: nx_brand_snap (bs_snap). 7// license_tier: ORIGINAL 8import "nx_syscalls.nx" 9import "nx_brand_snap.nx" 10import "nx_editstack_lib.nx" 11 12const EC_STRIDE: i64 = 5 // [kind, x, y, w, h] 13 14// snap every element's x/y/w onto the grid. 15func ec_snap_all(c: *i64, n: i64, grid: i64) -> i64 { 16 var i: i64 = 0 17 while i < n { 18 c[i*EC_STRIDE + 1] = bs_snap(c[i*EC_STRIDE + 1], grid) 19 c[i*EC_STRIDE + 2] = bs_snap(c[i*EC_STRIDE + 2], grid) 20 c[i*EC_STRIDE + 3] = bs_snap(c[i*EC_STRIDE + 3], grid) 21 i = i + 1 22 } 23 return 0 24} 25// align all elements: mode 0=left 1=right 2=top 3=bottom 4=center-h 5=middle-v (to the bounding box). 26func ec_align(c: *i64, n: i64, mode: i64) -> i64 { 27 if n < 1 { return 0 } 28 var minx: i64 = c[1]; var maxr: i64 = c[1] + c[3] 29 var miny: i64 = c[2]; var maxb: i64 = c[2] + c[4] 30 var i: i64 = 1 31 while i < n { 32 let x: i64 = c[i*EC_STRIDE+1]; let r: i64 = x + c[i*EC_STRIDE+3] 33 let y: i64 = c[i*EC_STRIDE+2]; let b: i64 = y + c[i*EC_STRIDE+4] 34 if x < minx { minx = x } 35 if r > maxr { maxr = r } 36 if y < miny { miny = y } 37 if b > maxb { maxb = b } 38 i = i + 1 39 } 40 i = 0 41 while i < n { 42 if mode == 0 { c[i*EC_STRIDE+1] = minx } 43 if mode == 1 { c[i*EC_STRIDE+1] = maxr - c[i*EC_STRIDE+3] } 44 if mode == 2 { c[i*EC_STRIDE+2] = miny } 45 if mode == 3 { c[i*EC_STRIDE+2] = maxb - c[i*EC_STRIDE+4] } 46 if mode == 4 { c[i*EC_STRIDE+1] = (minx + maxr)/2 - c[i*EC_STRIDE+3]/2 } 47 if mode == 5 { c[i*EC_STRIDE+2] = (miny + maxb)/2 - c[i*EC_STRIDE+4]/2 } 48 i = i + 1 49 } 50 return 0 51} 52// distribute left edges evenly between the leftmost and rightmost element (elements given in visual x-order). 53func ec_distribute_h(c: *i64, n: i64) -> i64 { 54 if n < 2 { return 0 } 55 var minx: i64 = c[1]; var maxx: i64 = c[1] 56 var i: i64 = 1 57 while i < n { let x: i64 = c[i*EC_STRIDE+1]; if x < minx { minx = x } if x > maxx { maxx = x } i = i + 1 } 58 let span: i64 = maxx - minx 59 i = 0 60 while i < n { c[i*EC_STRIDE+1] = minx + (i*span)/(n-1); i = i + 1 } 61 return 0 62} 63// move one element. 64func ec_nudge(c: *i64, idx: i64, dx: i64, dy: i64) -> i64 { 65 c[idx*EC_STRIDE+1] = c[idx*EC_STRIDE+1] + dx 66 c[idx*EC_STRIDE+2] = c[idx*EC_STRIDE+2] + dy 67 return 0 68} 69// snapshot / restore (one-deep undo; a history stack is the follow-on). 70func ec_save(c: *i64, n: i64, hist: *i64) -> i64 { var i: i64 = 0; while i < n*EC_STRIDE { hist[i] = c[i]; i = i + 1 } return 0 } 71func ec_undo(c: *i64, n: i64, hist: *i64) -> i64 { var i: i64 = 0; while i < n*EC_STRIDE { c[i] = hist[i]; i = i + 1 } return 0 } 72 73// ---- NO-JS render: the canvas as absolutely-positioned HTML via CSS only (ZERO JavaScript) ---- 74// The sovereign editor's surface is server-rendered + CSS-positioned -- editing is server ops (apply an engine op, 75// re-render), never a client-side JS canvas. This is the no-JS-unless-mandatory doctrine made literal. 76func ec_w(fd: i64, s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(fd,s,n); return 0 } 77func ec_wn(fd: i64, v: i64) -> i64 { 78 if v == 0 { sys_write(fd, "0" as *u8, 1); return 0 } 79 var m: i64 = v; if m < 0 { sys_write(fd, "-" as *u8, 1); m = 0 - m } 80 let d: *u8 = sys_mmap(24); var k: i64 = 0 81 while m > 0 { d[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 82 var i: i64 = k - 1 83 while i >= 0 { let o: *u8 = sys_mmap(1); o[0] = d[i]; sys_write(fd, o, 1); i = i - 1 } 84 return 0 85} 86func ec_kindlabel(k: i64) -> *u8 { if k==1 { return "heading" as *u8 } if k==2 { return "text" as *u8 } if k==3 { return "button" as *u8 } if k==4 { return "image" as *u8 } return "block" as *u8 } 87// render the canvas to an fd as absolutely-positioned divs (CSS only, NO JavaScript). Token-driven borders. 88func ec_render(c: *i64, n: i64, fd: i64) -> i64 { 89 ec_w(fd, "<div class=\"nx-canvas\" style=\"position:relative;min-height:480px;border:1px solid var(--nx-color-line)\">\n" as *u8) 90 var i: i64 = 0 91 while i < n { 92 ec_w(fd, "<div class=\"nx-el\" style=\"position:absolute;left:" as *u8) 93 ec_wn(fd, c[i*EC_STRIDE+1]); ec_w(fd, "px;top:" as *u8); ec_wn(fd, c[i*EC_STRIDE+2]) 94 ec_w(fd, "px;width:" as *u8); ec_wn(fd, c[i*EC_STRIDE+3]); ec_w(fd, "px;height:" as *u8); ec_wn(fd, c[i*EC_STRIDE+4]) 95 ec_w(fd, "px;border:1px solid var(--nx-color-line);padding:4px\">" as *u8) 96 ec_w(fd, ec_kindlabel(c[i*EC_STRIDE+0])) 97 ec_w(fd, "</div>\n" as *u8) 98 i = i + 1 99 } 100 ec_w(fd, "</div>\n" as *u8) 101 return 0 102} 103 104// ===================================================================================================== 105// THE OPERATION STACK ADOPTION -- multi-level undo/redo, bit-exact replay, and re-evaluation at depth. 106// ===================================================================================================== 107// ec_save/ec_undo above are a ONE-DEEP FULL-ARRAY SNAPSHOT. They are kept, unchanged, and still exercised 108// by the gate, because they have a caller and because the cheapest way to break a working thing is to 109// change its signature while adopting something better. Everything below is ADDITIVE: a canvas that never 110// calls ecs_* behaves exactly as it did. 111// 112// WHAT THE SNAPSHOT CANNOT DO, and why the log-shaped primitive in nx_editstack_lib is the answer: 113// undo twice (the snapshot holds one state), redo at all (the state it replaced is gone the moment you 114// restore), replay (there is no record of what happened), or re-evaluate a parameter (the operation was 115// never named, only its result was kept). It also costs O(state) on EVERY edit, where a log costs O(1) 116// to record. 117// 118// WHY THE CANVAS RECORDS ITS OWN OP KINDS RATHER THAN PRIMITIVE CELL WRITES. An align is not expressible 119// in the stack's generic SET/ADD/SCALE/RADD vocabulary as ONE op: each element receives a DIFFERENT value, 120// derived from a bounding box that is itself a function of the whole canvas. Recording it as N primitive 121// SET ops would make undo step backwards one ELEMENT at a time -- the user made one gesture and would have 122// to undo it n times -- and it would make re-evaluation meaningless, because the n recorded values are 123// consequences, not parameters. Recording the GESTURE instead (kind=align, a0=mode) gives one gesture one 124// op, and makes the DC2 property real for a layout editor: re-evaluate op 3 from align-left to align-right 125// and every operation after it is preserved and re-applied on top. That is the whole reason the stack 126// exists, and it is only reachable from the gesture vocabulary. 127// 128// THE COST, NAMED. A canvas op re-derives its bounding box on every replay step, so an undo is 129// O(ops x elements) rather than O(elements). That is the same bargain nx_editstack_lib already declared 130// for replay-from-base over inverse-ops, and it is paid for the same reason: replay from the base cannot 131// drift, so the bit-exact claim below is checkable rather than asserted. 132// 133// THE STACK'S LIVE CELLS ARE THE CANVAS, with no conversion and no shadow copy. The cell vector is mapped 134// straight onto the AoS [kind,x,y,w,h] layout the ec_* functions already take, so ecs_canvas hands those 135// same functions the stack's own memory. (nx_vpick_lib deliberately chose a COMPONENT-MAJOR mapping for 136// the opposite reason: its gesture is a contiguous range add, so it wanted the components adjacent. Here 137// no range op is ever recorded, so the layout that costs zero conversion is the right one.) Because there 138// is no second copy, es_digest always describes exactly the canvas being rendered. 139 140// Consumer op kinds. They sit at and above ES_OP_CONSUMER_BASE, which es_push_raw ENFORCES, so a canvas op 141// can never be mistaken for a generic one by the stack's own interpreter. 142const ECS_OP_SNAP: i64 = 1024 // a0 = grid 143const ECS_OP_ALIGN: i64 = 1025 // a0 = mode 0..5, exactly ec_align's mode 144const ECS_OP_DISTH: i64 = 1026 // no arguments 145const ECS_OP_NUDGE: i64 = 1027 // a0 = element index, a1 = dx, a2 = dy 146 147// ec_align interprets modes 0..5 and SILENTLY DOES NOTHING for anything else. A silent no-op is fine in a 148// direct call and is not fine in a log, where it would be replayed forever as a gesture that never happened, 149// so the recorded form REFUSES an unknown mode instead. 150const ECS_ALIGN_MODE_MAX: i64 = 5 151 152// Refusal codes deliberately clear of BOTH neighbours: nx_editstack_lib owns -1..-10 and nx_vpick_lib owns 153// -16..-26. A caller that propagates one of these unchanged can still tell which layer refused. 154const ECS_OK: i64 = 0 155const ECS_E_KIND: i64 = 0 - 33 156const ECS_E_MODE: i64 = 0 - 34 157const ECS_E_GRID: i64 = 0 - 35 158const ECS_E_IDX: i64 = 0 - 36 159const ECS_E_ARGS: i64 = 0 - 37 160 161func ecs_code_name(c: i64) -> *u8 { 162 if c == ECS_OK { return "OK" as *u8 } 163 if c == ECS_E_KIND { return "REFUSED-UNKNOWN-CANVAS-OP-KIND" as *u8 } 164 if c == ECS_E_MODE { return "REFUSED-ALIGN-MODE-OUT-OF-RANGE" as *u8 } 165 if c == ECS_E_GRID { return "REFUSED-SNAP-GRID-NOT-POSITIVE" as *u8 } 166 if c == ECS_E_IDX { return "REFUSED-ELEMENT-INDEX-OUT-OF-RANGE" as *u8 } 167 if c == ECS_E_ARGS { return "REFUSED-BAD-ARGUMENTS" as *u8 } 168 return es_err_name(c) 169} 170 171// THE CANVAS INTERPRETER. Every bound is checked BEFORE any element is touched, so a refused op leaves the 172// canvas exactly as it found it. The grid guard is load-bearing rather than decorative: bs_snap divides by 173// the grid, so a recorded grid of 0 would not merely be wrong, it would fault on every replay forever. 174func ecs_apply_op(c: *i64, n: i64, kind: i64, a0: i64, a1: i64, a2: i64) -> i64 { 175 if kind == ECS_OP_SNAP { 176 if a0 <= 0 { return ECS_E_GRID } 177 ec_snap_all(c, n, a0) 178 return ECS_OK 179 } 180 if kind == ECS_OP_ALIGN { 181 if a0 < 0 { return ECS_E_MODE } 182 if a0 > ECS_ALIGN_MODE_MAX { return ECS_E_MODE } 183 ec_align(c, n, a0) 184 return ECS_OK 185 } 186 if kind == ECS_OP_DISTH { 187 ec_distribute_h(c, n) 188 return ECS_OK 189 } 190 if kind == ECS_OP_NUDGE { 191 if a0 < 0 { return ECS_E_IDX } 192 if a0 >= n { return ECS_E_IDX } 193 ec_nudge(c, a0, a1, a2) 194 return ECS_OK 195 } 196 return ECS_E_KIND 197} 198 199// Returns 0 (a null pointer) on bad arguments rather than allocating something unusable, exactly as es_new 200// does -- one convention, so a caller checks for null once. 201func ecs_new(cap_ops: i64, n: i64) -> *i64 { 202 if n <= 0 { return 0 as *i64 } 203 return es_new(cap_ops, n * EC_STRIDE) 204} 205 206func ecs_nelem(st: *i64) -> i64 { return es_ncells(st) / EC_STRIDE } 207func ecs_canvas(st: *i64) -> *i64 { return es_live_ptr(st) } 208func ecs_digest(st: *i64) -> i64 { return es_digest(st) } 209 210// Seed the base (and therefore the live) canvas from an ordinary ec_* array. A size mismatch is REFUSED 211// rather than partially written: a half-seeded base would replay into a state nobody authored. 212func ecs_seed(st: *i64, c: *i64, n: i64) -> i64 { 213 if n <= 0 { return ECS_E_ARGS } 214 if es_ncells(st) != n * EC_STRIDE { return ECS_E_ARGS } 215 var i: i64 = 0 216 while i < n * EC_STRIDE { 217 es_base_set(st, i, c[i]) 218 i = i + 1 219 } 220 return ECS_OK 221} 222 223// Restore the base, then re-apply gestures [0, head) through the canvas interpreter. es_restore_base is 224// COMPOSED, not re-typed, so there is still exactly one base-to-live copier in the estate. 225func ecs_replay(st: *i64) -> i64 { 226 es_restore_base(st) 227 let c: *i64 = es_live_ptr(st) 228 let n: i64 = ecs_nelem(st) 229 let h: i64 = es_head(st) 230 var k: i64 = 0 231 while k < h { 232 let rc: i64 = ecs_apply_op(c, n, es_op_field(st, k, ES_F_KIND), es_op_field(st, k, ES_F_A0), es_op_field(st, k, ES_F_A1), es_op_field(st, k, ES_F_A2)) 233 if rc != ECS_OK { return rc } 234 k = k + 1 235 } 236 return ECS_OK 237} 238 239// Apply a gesture and record it. The cap is checked BEFORE the canvas is touched, for the reason es_push 240// states about itself. If the RECORD is refused after the canvas moved, the canvas is replayed back to the 241// log -- so a refused push can never leave state the log does not account for. 242func ecs_push(st: *i64, kind: i64, a0: i64, a1: i64, a2: i64) -> i64 { 243 if es_head(st) >= es_cap(st) { return ES_E_FULL } 244 let rc: i64 = ecs_apply_op(es_live_ptr(st), ecs_nelem(st), kind, a0, a1, a2) 245 if rc != ECS_OK { return rc } 246 let r2: i64 = es_push_raw(st, kind, a0, a1, a2, 0) 247 if r2 != ES_OK { 248 ecs_replay(st) 249 return r2 250 } 251 return ECS_OK 252} 253 254// MULTI-LEVEL undo and redo. The cursor and the record count live in the stack header, so "how many 255// gestures are undoable" has exactly one answer and it is impossible for a second counter to disagree 256// with it. Undo past the base and redo past the head REFUSE BY NAME and change nothing. 257func ecs_undo(st: *i64) -> i64 { 258 let h: i64 = es_head(st) 259 if h <= 0 { return ES_E_ATBASE } 260 es_head_set(st, h - 1) 261 return ecs_replay(st) 262} 263 264func ecs_redo(st: *i64) -> i64 { 265 let h: i64 = es_head(st) 266 if h >= es_count(st) { return ES_E_ATHEAD } 267 es_head_set(st, h + 1) 268 return ecs_replay(st) 269} 270 271// Change the PARAMETERS of gesture idx and re-evaluate. Gestures after idx are preserved and re-applied on 272// top of the new value -- the property an editor built on a snapshot cannot have at any price. 273func ecs_reeval(st: *i64, idx: i64, a0: i64, a1: i64, a2: i64) -> i64 { 274 let rc: i64 = es_op_args_set(st, idx, a0, a1, a2, 0) 275 if rc != ES_OK { return rc } 276 return ecs_replay(st) 277} 278 279// Named gestures, so a caller records intent rather than assembling a record by hand. 280func ecs_snap(st: *i64, grid: i64) -> i64 { return ecs_push(st, ECS_OP_SNAP, grid, 0, 0) } 281func ecs_align(st: *i64, mode: i64) -> i64 { return ecs_push(st, ECS_OP_ALIGN, mode, 0, 0) } 282func ecs_distribute_h(st: *i64) -> i64 { return ecs_push(st, ECS_OP_DISTH, 0, 0, 0) } 283func ecs_nudge(st: *i64, idx: i64, dx: i64, dy: i64) -> i64 { return ecs_push(st, ECS_OP_NUDGE, idx, dx, dy) } 284 285// Render the stack's live canvas through the SAME renderer the snapshot path uses. One renderer. 286func ecs_render(st: *i64, fd: i64) -> i64 { return ec_render(es_live_ptr(st), ecs_nelem(st), fd) } 287