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
5 changes: 5 additions & 0 deletions .changeset/neat-cases-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset needs revision.

Changeset package scope does not match the affected packages confidently.

View the proposed replacement
---
'@bomb.sh/tty': patch
---

TODO: Write changeset message (inference failed: HTTP 401: {"error":{"message":"User not found.","code":401}})

"@bomb.sh/tty": patch
---

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.
4 changes: 4 additions & 0 deletions specs/renderer-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,10 @@ events as part of the render result:
- `pointerclick` — a pointer-up occurred over an element that was also under the
pointer at pointer-down

Pointer coordinates are cell coordinates. The pointer is over an element when
its cell lies inside the element's bounding box, so a cell on an edge two
elements share belongs only to the element that draws it.

This surface is functional but should not be treated as stable contract. The
calling convention was discovered through iteration, the event model has
acknowledged gaps, and the approach may evolve.
Expand Down
6 changes: 4 additions & 2 deletions src/clayterm.c
Original file line number Diff line number Diff line change
Expand Up @@ -999,10 +999,12 @@ void reduce(struct Clayterm *ct, uint32_t *buf, int len, int mode, int row,
Clay_RenderCommandArray cmds = Clay_EndLayout(deltaTime);

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

/* reset output state */
ct->out.length = 0;
Expand Down
9 changes: 9 additions & 0 deletions test/pointer-shape.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@ describe("pointer shape", () => {
]);
});

it("takes the shape of the element that owns a cell on a shared edge", () => {
expect(sequences(term.render(layout(), over(10, 0)).output)).toEqual([
osc22("pointer"),
]);
expect(sequences(term.render(layout(), over(20, 2)).output)).toEqual([
osc22("default"),
]);
});

it("follows a declared shape that changes under a still pointer", () => {
term.render(layout(), LINK);
expect(
Expand Down
47 changes: 47 additions & 0 deletions test/term.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,53 @@ hi
});
});

describe("pointer events", () => {
let siblings: Op[] = [
open("root", {
layout: { width: grow(), height: grow(), direction: "ltr" },
}),
open("left", { layout: { width: fixed(20), height: grow() } }),
close(),
open("right", { layout: { width: fixed(20), height: grow() } }),
close(),
close(),
];

let rows: Op[] = [
open("root", {
layout: { width: grow(), height: grow(), direction: "ttb" },
}),
open("row0", { layout: { width: grow(), height: fixed(1) } }),
close(),
open("row1", { layout: { width: grow(), height: fixed(1) } }),
close(),
close(),
];

function click(frame: Op[], x: number, y: number): string[] {
term.render(frame, { pointer: { x, y, down: true } });
return term.render(frame, { pointer: { x, y, down: false } }).events
.filter((event) => event.type === "pointerclick")
.map((event) => event.id);
}

it("clicks only the sibling that owns the first cell past a shared edge", () => {
let ids = click(siblings, 20, 0);
expect(ids).toContain("right");
expect(ids).not.toContain("left");
});

it("clicks only the row that owns the first row past a shared edge", () => {
let ids = click(rows, 0, 1);
expect(ids).toContain("row1");
expect(ids).not.toContain("row0");
});

it("reports no element for a pointer past the last column", () => {
expect(click(siblings, 40, 0)).toEqual([]);
});
});

describe("synchronized output", () => {
let frame: Op[] = [
open("root", { layout: { width: grow(), height: grow() } }),
Expand Down
Loading