Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion specs/renderer-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,7 @@ prevent overlap.
### 12.3 Render return type

The `render()` method currently returns a `RenderResult` object shaped as
`{ output: Uint8Array, events: PointerEvent[] }`.
`{ output: Uint8Array, events: PointerEvent[], info: RenderInfo }`.

The `output` field is the ANSI byte output specified normatively in Section 7.3
and Section 8.2.
Expand All @@ -643,6 +643,34 @@ but has acknowledged gaps (no modifier keys on click events) and its interaction
protocol (passing pointer state via render options, then reading events from the
return value) was arrived at through iteration rather than upfront design.

The `info` field implements `RenderInfo`, a read-only lookup keyed by element id
(the `id` parameter passed to `open()`):

```
interface RenderInfo {
get(id: string): ElementInfo | undefined;
}

interface ElementInfo {
bounds: BoundingBox;
}

interface BoundingBox {
x: number;
y: number;
width: number;
height: number;
}
```

Each `ElementInfo` provides post-layout metadata. The `bounds` field is the
element's computed bounding box in character cells, as determined by the layout
engine after the render transaction completes. `x` and `y` are zero-indexed from
the top-left corner of the layout root.

Querying an element with an empty-string id or an id not present in the frame
returns `undefined`.

The return type of `render()` has changed twice since the project's inception
(string, then `Uint8Array`, then `RenderResult`). While the ANSI bytes
commitment (Section 7.3) is stable, the wrapper shape around those bytes is not.
Expand Down
14 changes: 14 additions & 0 deletions src/clayterm.c
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,20 @@ char *output(struct Clayterm *ct) { return ct->out.data; }

int length(struct Clayterm *ct) { return ct->out.length; }

int get_element_bounds(const char *name, int name_len, float *out) {
Clay_String str = {.length = name_len, .chars = name};
Clay_ElementId eid = Clay__HashString(str, 0);
Clay_ElementData data = Clay_GetElementData(eid);
if (!data.found) {
return 0;
}
out[0] = data.boundingBox.x;
out[1] = data.boundingBox.y;
out[2] = data.boundingBox.width;
out[3] = data.boundingBox.height;
return 1;
}

int pointer_over_count(void) { return Clay_GetPointerOverIds().length; }

int pointer_over_id_string_length(int index) {
Expand Down
2 changes: 2 additions & 0 deletions src/clayterm.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ char *output(struct Clayterm *ct);
int length(struct Clayterm *ct);
void measure(int ret, int txt);

int get_element_bounds(const char *name, int name_len, float *out);

int pointer_over_count(void);
int pointer_over_id_string_length(int index);
int pointer_over_id_string_ptr(int index);
Expand Down
37 changes: 37 additions & 0 deletions term-native.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
import { f32, offsets, struct } from "./typedef.ts";

export interface BoundingBox {
x: number;
y: number;
width: number;
height: number;
}

const BoundingBoxStruct = struct<BoundingBox>({
x: f32(),
y: f32(),
width: f32(),
height: f32(),
});

const BOUNDING_BOX = offsets(BoundingBoxStruct);

export interface Native {
memory: WebAssembly.Memory;
statePtr: number;
Expand All @@ -7,6 +25,7 @@ export interface Native {
length(ct: number): number;
setPointer(x: number, y: number, down: boolean): void;
getPointerOverIds(): string[];
getElementBounds(id: string): BoundingBox | undefined;
}

import { compiled } from "./wasm.ts";
Expand Down Expand Up @@ -60,6 +79,7 @@ export async function createTermNative(
pointer_over_count(): number;
pointer_over_id_string_length(index: number): number;
pointer_over_id_string_ptr(index: number): number;
get_element_bounds(name: number, len: number, out: number): number;
};

let heap = ct.__heap_base.value as number;
Expand Down Expand Up @@ -101,5 +121,22 @@ export async function createTermNative(
}
return ids;
},
getElementBounds(id: string): BoundingBox | undefined {
let enc = new TextEncoder();
let bytes = enc.encode(id);
new Uint8Array(memory.buffer).set(bytes, opsBuf);
let out = opsBuf + 256;
let found = ct.get_element_bounds(opsBuf, bytes.length, out);
if (!found) {
return undefined;
}
let view = new DataView(memory.buffer);
return {
x: view.getFloat32(out + BOUNDING_BOX.x, true),
y: view.getFloat32(out + BOUNDING_BOX.y, true),
width: view.getFloat32(out + BOUNDING_BOX.width, true),
height: view.getFloat32(out + BOUNDING_BOX.height, true),
};
},
};
}
25 changes: 23 additions & 2 deletions term.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Op, pack } from "./ops.ts";
import { createTermNative } from "./term-native.ts";
import { type BoundingBox, createTermNative } from "./term-native.ts";

export interface TermOptions {
height: number;
Expand Down Expand Up @@ -32,9 +32,20 @@ export type PointerEvent =
| { type: "pointerleave"; id: string }
| { type: "pointerclick"; id: string };

export type { BoundingBox };

export interface ElementInfo {
bounds: BoundingBox;
}

export interface RenderInfo {
get(id: string): ElementInfo | undefined;
}

export interface RenderResult {
output: Uint8Array;
events: PointerEvent[];
info: RenderInfo;
}

export interface Term {
Expand Down Expand Up @@ -103,7 +114,17 @@ export async function createTerm(options: TermOptions): Promise<Term> {
prev = current;
wasDown = down;

return { output, events };
let info: RenderInfo = {
get(id: string): ElementInfo | undefined {
let bounds = native.getElementBounds(id);
if (bounds) {
return { bounds };
}
return undefined;
},
};

return { output, events, info };
},
};
}
42 changes: 41 additions & 1 deletion test/term.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it } from "./suite.ts";
import { createTerm, type Term } from "../term.ts";
import { close, grow, open, rgba, text } from "../ops.ts";
import { close, fixed, grow, open, rgba, text } from "../ops.ts";
import { print } from "./print.ts";

const decode = (bytes: Uint8Array) => new TextDecoder().decode(bytes);
Expand Down Expand Up @@ -151,6 +151,46 @@ describe("term", () => {
});
});

describe("info", () => {
it("returns bounds for named elements", async () => {
let term = await createTerm({ width: 40, height: 10 });
let result = term.render([
open("root", {
layout: { width: grow(), height: grow(), direction: "ttb" },
}),
open("child", {
layout: { width: fixed(20), height: fixed(5) },
}),
close(),
close(),
]);

let root = result.info.get("root");
expect(root).toBeDefined();
expect(root!.bounds).toEqual({ x: 0, y: 0, width: 40, height: 10 });

let child = result.info.get("child");
expect(child).toBeDefined();
expect(child!.bounds).toEqual({ x: 0, y: 0, width: 20, height: 5 });
});

it("returns undefined for unknown ids", async () => {
let term = await createTerm({ width: 20, height: 5 });
term.render([
open("root", { layout: { width: grow(), height: grow() } }),
close(),
]);

let result = term.render([
open("root", { layout: { width: grow(), height: grow() } }),
close(),
]);

expect(result.info.get("nonexistent")).toBeUndefined();
expect(result.info.get("")).toBeUndefined();
});
});

describe("row offset", () => {
it("renders two frames at the offset position", async () => {
let term = await createTerm({ width: 20, height: 5 });
Expand Down
5 changes: 5 additions & 0 deletions typedef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export function array<T>(element: TypeDef<T>, length: number): Arr<T[]> {
};
}

export const f32 = (): TypeDef<number> => ({
type: "f32",
byteLength: 4,
byteAlign: 4,
});
export const int32 = (): TypeDef<number> => ({
type: "int32",
byteLength: 4,
Expand Down
Loading