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