Skip to content

Commit 16fcdda

Browse files
committed
refactor: resolve and emit pointer shapes in the native renderer
cursor was carried beside the packed ops (a shapes side-array on snapshots) and resolved in JS by rescanning ops against hovered ids. It now follows the caret model: pack() encodes it as PROP_CURSOR (one word, the shape's vocabulary index; unknown values omit the bit), and reduce() registers it with Clay_OnHover while decoding. Clay calls hover functions during Clay_SetPointerState in hit-test order, ancestors first, so the last call is the innermost declaring element and capture/passthrough floating behave as before. The pointer now enters through reduce() so the hit test runs after EndLayout and before the frame's bytes are finalized; OSC 22 is appended from C after the frame (after ESU when synchronized output wraps it). The emitted shape lives on the Clayterm instance. set_capabilities() reports when withdrawing pointerShape leaves a non-default shape on screen, and update() returns the reset. The native wrapper carries the emitted shape across re-init so a resize leaves it unchanged. Anonymous elements can now declare a cursor; the JS resolver matched by string id and missed them.
1 parent bcdb4d7 commit 16fcdda

9 files changed

Lines changed: 196 additions & 133 deletions

File tree

‎Makefile‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@ EXPORTS = \
1515
-Wl,--export=clayterm_size \
1616
-Wl,--export=init \
1717
-Wl,--export=set_capabilities \
18+
-Wl,--export=pointer_shape \
19+
-Wl,--export=set_pointer_shape \
20+
-Wl,--export=pointer_shape \
21+
-Wl,--export=set_pointer_shape \
1822
-Wl,--export=reduce \
1923
-Wl,--export=output \
2024
-Wl,--export=length \
2125
-Wl,--export=measure \
22-
-Wl,--export=Clay_SetPointerState \
2326
-Wl,--export=pointer_over_count \
2427
-Wl,--export=pointer_over_id_string_length \
2528
-Wl,--export=pointer_over_id_string_ptr \

‎ops.ts‎

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { PointerShape } from "./pointer-shape.ts";
1+
import { type PointerShape, pointerShapeIndex } from "./pointer-shape.ts";
22

33
export type { PointerShape };
44

@@ -38,6 +38,7 @@ const PROP_BORDER = 0x08;
3838
const PROP_CLIP = 0x10;
3939
const PROP_FLOATING = 0x20;
4040
const PROP_TRANSITION = 0x40;
41+
const PROP_CURSOR = 0x80;
4142

4243
const encoder = new TextEncoder();
4344

@@ -156,6 +157,8 @@ export function pack(
156157
if (op.clip) mask |= PROP_CLIP;
157158
if (op.floating) mask |= PROP_FLOATING;
158159
if (op.transition) mask |= PROP_TRANSITION;
160+
let cursor = pointerShapeIndex(op.cursor);
161+
if (cursor !== undefined) mask |= PROP_CURSOR;
159162
view.setUint32(o, mask, true);
160163
o += 4;
161164

@@ -283,6 +286,11 @@ export function pack(
283286
view.setUint8(o, t.interactive ? 1 : 0);
284287
o += 1;
285288
}
289+
290+
if (cursor !== undefined) {
291+
view.setUint32(o, cursor, true);
292+
o += 4;
293+
}
286294
break;
287295
}
288296

@@ -496,7 +504,6 @@ export interface Text {
496504
interface Snapshot {
497505
directive: typeof OP_SNAPSHOT;
498506
data: Uint8Array;
499-
shapes?: readonly (readonly [id: string, shape: string])[];
500507
}
501508

502509
export type Op = OpenElement | Text | CloseElement | Snapshot;
@@ -541,6 +548,7 @@ function packSize(ops: Op[]): number {
541548
// x, y, expand width/height, parent, attach/pointer, clip/z
542549
if (op.floating) n += 7 * 4;
543550
if (op.transition) n += 8;
551+
if (pointerShapeIndex(op.cursor) !== undefined) n += 4;
544552
break;
545553
}
546554
case OP_TEXT: {
@@ -557,19 +565,7 @@ export function snapshot(ops: Op[]): Op {
557565
let size = packSize(ops);
558566
let buf = new ArrayBuffer(size);
559567
let words = pack(ops, buf, 0, size);
560-
let data = new Uint8Array(buf, 0, words * 4);
561-
let shapes: (readonly [string, string])[] = [];
562-
for (let op of ops) {
563-
if (op.directive === OP_SNAPSHOT) {
564-
if (op.shapes) shapes.push(...op.shapes);
565-
} else if (
566-
op.directive === OP_OPEN_ELEMENT && op.cursor !== undefined
567-
) {
568-
shapes.push([op.id, op.cursor]);
569-
}
570-
}
571-
if (shapes.length === 0) return { directive: OP_SNAPSHOT, data };
572-
return { directive: OP_SNAPSHOT, data, shapes };
568+
return { directive: OP_SNAPSHOT, data: new Uint8Array(buf, 0, words * 4) };
573569
}
574570

575571
const TP_X = 1;

‎pointer-shape.ts‎

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { Op } from "./ops.ts";
2-
31
export const POINTER_SHAPES = [
42
"default",
53
"none",
@@ -43,39 +41,14 @@ export const POINTER_SHAPES = [
4341
*/
4442
export type PointerShape = typeof POINTER_SHAPES[number];
4543

46-
const KNOWN: ReadonlySet<string> = new Set(POINTER_SHAPES);
44+
const INDEX: ReadonlyMap<unknown, number> = new Map(
45+
POINTER_SHAPES.map((shape, i) => [shape, i]),
46+
);
4747

48-
export function resolvePointerShape(
49-
ops: readonly Op[],
50-
over: readonly string[],
51-
): PointerShape {
52-
let best = -1;
53-
let shape: PointerShape = "default";
54-
for (let op of ops) {
55-
if ("data" in op) {
56-
if (!op.shapes) continue;
57-
for (let [id, value] of op.shapes) {
58-
if (!KNOWN.has(value)) continue;
59-
let i = over.lastIndexOf(id);
60-
if (i > best) {
61-
best = i;
62-
shape = value as PointerShape;
63-
}
64-
}
65-
} else if ("cursor" in op && op.cursor !== undefined) {
66-
if (!KNOWN.has(op.cursor)) continue;
67-
let i = over.lastIndexOf(op.id);
68-
if (i > best) {
69-
best = i;
70-
shape = op.cursor;
71-
}
72-
}
73-
}
74-
return shape;
48+
export function pointerShapeIndex(shape: unknown): number | undefined {
49+
return INDEX.get(shape);
7550
}
7651

77-
const encoder = new TextEncoder();
78-
79-
export function osc22(shape: PointerShape): Uint8Array {
80-
return encoder.encode(`\x1b]22;${shape}\x1b\\`);
81-
}
52+
export const POINTER_SHAPE_RESET: Uint8Array = new TextEncoder().encode(
53+
"\x1b]22;default\x1b\\",
54+
);

‎src/clayterm.c‎

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ struct Clayterm *ct_active_context = NULL;
4343
#define PROP_CLIP 0x10
4444
#define PROP_FLOATING 0x20
4545
#define PROP_TRANSITION 0x40
46+
#define PROP_CURSOR 0x80
4647

4748
/* ── Instance state ───────────────────────────────────────────────── */
4849

@@ -90,6 +91,11 @@ struct Clayterm {
9091
/* CAP_* bits pushed by the host via set_capabilities(); init() zeroes
9192
* them, so the host re-pushes after every re-init */
9293
uint32_t caps;
94+
/* Pointer shape (renderer-spec 7.10): hover_shape is what this frame's
95+
* hit test resolved, pointer_shape is what was last emitted. Both index
96+
* POINTER_SHAPES; 0 is "default". */
97+
int hover_shape;
98+
int pointer_shape;
9399
/* Caret state for hardware-cursor management. The renderer records the
94100
* first text node carrying a caret declaration per frame, precomputes the
95101
* caret's byte offset into that node's content, then places the cell as
@@ -119,11 +125,34 @@ struct Clayterm {
119125
* to 4 (base SMP char) + 8×4 (combining marks) = 36 bytes of cluster text. */
120126
#define OUT_BYTES_PER_CELL 128
121127

122-
/* Fixed per-frame slack on top of the per-cell budget, for output that wraps
123-
* the whole frame rather than scaling with cell count — the BSU/ESU
124-
* synchronized-output pair. Keeps the wrap from being dropped on tiny grids
128+
/* Fixed per-frame slack on top of the per-cell budget, for output emitted
129+
* once per frame rather than per cell: the BSU/ESU synchronized-output pair
130+
* and one OSC 22 pointer shape. Keeps them from being dropped on tiny grids
125131
* where the per-cell budget alone is tight. */
126-
#define OUT_FRAME_OVERHEAD 32
132+
#define OUT_FRAME_OVERHEAD 64
133+
134+
/* Pointer shape names in wire-index order; mirrors POINTER_SHAPES in
135+
* pointer-shape.ts. */
136+
static const char *const POINTER_SHAPES[] = {
137+
"default", "none", "context-menu", "help", "pointer",
138+
"progress", "wait", "cell", "crosshair", "text",
139+
"vertical-text", "alias", "copy", "move", "no-drop",
140+
"not-allowed", "grab", "grabbing", "e-resize", "n-resize",
141+
"ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize",
142+
"w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize",
143+
"zoom-in", "zoom-out",
144+
};
145+
#define POINTER_SHAPE_COUNT \
146+
((int)(sizeof(POINTER_SHAPES) / sizeof(POINTER_SHAPES[0])))
147+
148+
/* Clay calls hover functions in hit-test order, ancestors first, so the
149+
* last call is the innermost declaring element. */
150+
static void cursor_hover(Clay_ElementId id, Clay_PointerData pointer,
151+
void *shape) {
152+
(void)id;
153+
(void)pointer;
154+
ct_active_context->hover_shape = (int)(uintptr_t)shape;
155+
}
127156

128157
/* ── Cell buffer ops ──────────────────────────────────────────────── */
129158

@@ -747,10 +776,24 @@ struct Clayterm *init(void *mem, int w, int h) {
747776
return ct;
748777
}
749778

750-
void set_capabilities(struct Clayterm *ct, uint32_t caps) { ct->caps = caps; }
779+
int set_capabilities(struct Clayterm *ct, uint32_t caps) {
780+
int withdrawn = (ct->caps & RENDER_CAP_POINTER_SHAPE) &&
781+
!(caps & RENDER_CAP_POINTER_SHAPE);
782+
int restore = withdrawn && ct->pointer_shape != 0;
783+
if (restore)
784+
ct->pointer_shape = 0;
785+
ct->caps = caps;
786+
return restore;
787+
}
788+
789+
int pointer_shape(struct Clayterm *ct) { return ct->pointer_shape; }
790+
791+
void set_pointer_shape(struct Clayterm *ct, int shape) {
792+
ct->pointer_shape = shape;
793+
}
751794

752795
void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
753-
float deltaTime) {
796+
float deltaTime, float px, float py, int pointer) {
754797
int i = 0;
755798
ct_active_context = ct;
756799
ct->error_count = 0;
@@ -884,7 +927,14 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
884927
: CLAY_TRANSITION_DISABLE_INTERACTIONS_WHILE_TRANSITIONING_POSITION;
885928
}
886929

930+
uint32_t cursor = POINTER_SHAPE_COUNT;
931+
if (mask & PROP_CURSOR)
932+
cursor = rd(buf, len, &i);
933+
887934
Clay__ConfigureOpenElement(decl);
935+
936+
if (cursor < POINTER_SHAPE_COUNT && (ct->caps & RENDER_CAP_POINTER_SHAPE))
937+
Clay_OnHover(cursor_hover, (void *)(uintptr_t)cursor);
888938
break;
889939
}
890940

@@ -948,6 +998,12 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
948998

949999
Clay_RenderCommandArray cmds = Clay_EndLayout(deltaTime);
9501000

1001+
/* pointer: 0 = absent, 1 = up, 2 = down. The hit test runs against this
1002+
* frame's layout and fires cursor_hover for declaring elements. */
1003+
ct->hover_shape = 0;
1004+
if (pointer)
1005+
Clay_SetPointerState((Clay_Vector2){px, py}, pointer == 2);
1006+
9511007
/* reset output state */
9521008
ct->out.length = 0;
9531009
ct->lastfg = ct->lastbg = 0xffffffff;
@@ -1063,6 +1119,14 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
10631119
present_cups(ct, row);
10641120
}
10651121

1122+
if ((ct->caps & RENDER_CAP_POINTER_SHAPE) &&
1123+
ct->hover_shape != ct->pointer_shape) {
1124+
ct->pointer_shape = ct->hover_shape;
1125+
buf_str(&ct->out, "\x1b]22;");
1126+
buf_str(&ct->out, POINTER_SHAPES[ct->pointer_shape]);
1127+
buf_str(&ct->out, "\x1b\\");
1128+
}
1129+
10661130
ct_active_context = NULL;
10671131
}
10681132

‎src/clayterm.h‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@ struct Clayterm;
1111

1212
/* Capability bits for set_capabilities(). */
1313
#define RENDER_CAP_SYNC_OUTPUT (1u << 0)
14+
#define RENDER_CAP_POINTER_SHAPE (1u << 1)
1415

1516
/* WASM exports */
1617
int clayterm_size(int w, int h);
1718
struct Clayterm *init(void *mem, int w, int h);
18-
void set_capabilities(struct Clayterm *ct, uint32_t caps);
19+
int set_capabilities(struct Clayterm *ct, uint32_t caps);
20+
int pointer_shape(struct Clayterm *ct);
21+
void set_pointer_shape(struct Clayterm *ct, int shape);
1922
void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
20-
float deltaTime);
23+
float deltaTime, float px, float py, int pointer);
2124
char *output(struct Clayterm *ct);
2225
int length(struct Clayterm *ct);
2326
int animating(struct Clayterm *ct);

0 commit comments

Comments
 (0)