Skip to content

Commit cc738ed

Browse files
authored
fix: pointer events firing on two elements at a shared edge (#161)
1 parent 0c62623 commit cc738ed

5 files changed

Lines changed: 69 additions & 2 deletions

File tree

‎.changeset/neat-cases-decide.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bomb.sh/tty": patch
3+
---
4+
5+
Fixes pointer events and pointer shape on a cell where two elements meet. The pointer at that cell is now over only the element that draws it, so clicking a row in a list no longer also clicks the row above it.

‎specs/renderer-spec.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,10 @@ events as part of the render result:
11981198
- `pointerclick` — a pointer-up occurred over an element that was also under the
11991199
pointer at pointer-down
12001200

1201+
Pointer coordinates are cell coordinates. The pointer is over an element when
1202+
its cell lies inside the element's bounding box, so a cell on an edge two
1203+
elements share belongs only to the element that draws it.
1204+
12011205
This surface is functional but should not be treated as stable contract. The
12021206
calling convention was discovered through iteration, the event model has
12031207
acknowledged gaps, and the approach may evolve.

‎src/clayterm.c‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -980,10 +980,12 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
980980
Clay_RenderCommandArray cmds = Clay_EndLayout(deltaTime);
981981

982982
/* pointer: 0 = absent, 1 = up, 2 = down. The hit test runs against this
983-
* frame's layout and fires cursor_hover for declaring elements. */
983+
* frame's layout and fires cursor_hover for declaring elements. Clay's
984+
* point-in-rect test includes both edges, and a cell's corner sits on the
985+
* edge of every element that ends there, so hit-test the cell's center. */
984986
ct->hover_shape = 0;
985987
if (pointer)
986-
Clay_SetPointerState((Clay_Vector2){px, py}, pointer == 2);
988+
Clay_SetPointerState((Clay_Vector2){px + 0.5f, py + 0.5f}, pointer == 2);
987989

988990
/* reset output state */
989991
ct->out.length = 0;

‎test/pointer-shape.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,15 @@ describe("pointer shape", () => {
155155
]);
156156
});
157157

158+
it("takes the shape of the element that owns a cell on a shared edge", () => {
159+
expect(sequences(term.render(layout(), over(10, 0)).output)).toEqual([
160+
osc22("pointer"),
161+
]);
162+
expect(sequences(term.render(layout(), over(20, 2)).output)).toEqual([
163+
osc22("default"),
164+
]);
165+
});
166+
158167
it("follows a declared shape that changes under a still pointer", () => {
159168
term.render(layout(), LINK);
160169
expect(

‎test/term.test.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,53 @@ hi
886886
});
887887
});
888888

889+
describe("pointer events", () => {
890+
let siblings: Op[] = [
891+
open("root", {
892+
layout: { width: grow(), height: grow(), direction: "ltr" },
893+
}),
894+
open("left", { layout: { width: fixed(20), height: grow() } }),
895+
close(),
896+
open("right", { layout: { width: fixed(20), height: grow() } }),
897+
close(),
898+
close(),
899+
];
900+
901+
let rows: Op[] = [
902+
open("root", {
903+
layout: { width: grow(), height: grow(), direction: "ttb" },
904+
}),
905+
open("row0", { layout: { width: grow(), height: fixed(1) } }),
906+
close(),
907+
open("row1", { layout: { width: grow(), height: fixed(1) } }),
908+
close(),
909+
close(),
910+
];
911+
912+
function click(frame: Op[], x: number, y: number): string[] {
913+
term.render(frame, { pointer: { x, y, down: true } });
914+
return term.render(frame, { pointer: { x, y, down: false } }).events
915+
.filter((event) => event.type === "pointerclick")
916+
.map((event) => event.id);
917+
}
918+
919+
it("clicks only the sibling that owns the first cell past a shared edge", () => {
920+
let ids = click(siblings, 20, 0);
921+
expect(ids).toContain("right");
922+
expect(ids).not.toContain("left");
923+
});
924+
925+
it("clicks only the row that owns the first row past a shared edge", () => {
926+
let ids = click(rows, 0, 1);
927+
expect(ids).toContain("row1");
928+
expect(ids).not.toContain("row0");
929+
});
930+
931+
it("reports no element for a pointer past the last column", () => {
932+
expect(click(siblings, 40, 0)).toEqual([]);
933+
});
934+
});
935+
889936
describe("synchronized output", () => {
890937
let frame: Op[] = [
891938
open("root", { layout: { width: grow(), height: grow() } }),

0 commit comments

Comments
 (0)