Skip to content

core: glass that merges with its place's edge, and stays inside it - #583

Merged
foxnne merged 7 commits into
mainfrom
core/glass-edge
Oct 19, 2026
Merged

foxnne merged 7 commits into
mainfrom
core/glass-edge

Conversation

@foxnne

@foxnne foxnne commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator

Part of fizzyedit/plans#29 (the liquid edge, step 2 in the comment that revised it).

What changes

Glass can belong to a place and merge with that place's edge. This is the primitive behind controls that come out of a viewport's edge, metaball-like, without crossing it.

  • LiquidField.edge: a place's outline, its rect and corner radii (rounded or square). The outside of the place is one more shape in the field. A shape near the outline runs into it through the field's smooth minimum, so it joins the edge by a neck. The outline then cuts the glass's coverage, so none of it crosses.
  • No rim along the cut. The lens, the rim and the light are worked out from the joined shape, not the cut one, so where the glass meets the edge it reads as carrying on past it.
  • Never drawn on its own. The edge's surface starts 1px outside the outline (EDGE_GAP), so nothing shows along the edge, only the necks where shapes come near it.
  • Computed only near the shapes. Each quad is still only its group's shapes, grown by half the merge width (as far as a neck can reach) and clipped to the place, so no draw covers the whole place.
  • No new uniforms. The edge takes the program's last shape slot, flagged 2 where a shape's round flag goes. Plugins packing with an older LiquidField never set it. A field with an edge holds one shape fewer.
  • glassBubbles(..., .{ .edge = ... }) passes it on. Without the glass program, the bubbles are cut to the edge's rect.
  • LiquidField.sample, the CPU mirror, does the same, line for line with the HLSL.

The compiled forms (SPIR-V, DXIL, Metal, GLSL) come from CI's Shaders job and are committed in a second commit here.

SDK impact

  • None
  • Core-only or additive: reaches plugins at the next SDK release
  • Fingerprint moved: recorded in sdk/src/version.zig, sdk_version untouched, PR labelled sdk
  • SDK release: bumps sdk_version, lists the sdk PRs since the last sdk-v* tag

Verified

  • macOS:
    • zig build, zig build check-web, zig build test-sdk-version all pass.
    • zig build test: 109/109 steps.
    • zig build test-integration: 78/78 steps, 609/609 tests, including a new test against LiquidField.sample: a drop near the edge joins it by a neck; there's no glass along the edge away from it; a drop pushed past the edge is cut there, round a curved corner too; and a field with an edge holds one shape fewer.
    • Not yet seen on screen: needs the compiled shaders, next commit.
  • Windows:
  • Linux:
  • Web: check-web only

Follow-ups

🤖 Generated with Claude Code

@foxnne
foxnne force-pushed the core/glass-edge branch 3 times, most recently from 1ed44f5 to 1afd3ae Compare October 18, 2026 20:44
`LiquidField.edge`: a place's outline (rect and corner radii). Its outside is one
more shape the field's shapes run into, so a shape near the outline joins the edge
by a neck, metaball-like, and the outline cuts the glass's coverage, so none of it
crosses. The rim and bend come from the joined shape, so there is no rim along the
cut. The edge takes the program's last shape slot, flagged 2, so the uniforms keep
their layout. `glassBubbles` takes it as `.edge`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
foxnne added a commit that referenced this pull request Oct 19, 2026
Fixes the flaky step check in the integration tour test ("Integration
tests (Linux)" on #583, 2026-10-18: `.added` where
`.joined` was expected).

## What changes

Typing in a document with unsaved changes stays one undo step, however
long you type. Before, the crash checkpoint split it every few seconds.

**The cause wasn't a join window.** The text plugin joins keystrokes by
run of operation (VSCode's rule), with no timer. What cut the run was
the crash checkpoint (`src/editor/Checkpoint.zig`). It runs on the wall
clock, at least 3 s after the last checkpoint and once there has been
input, and captures every dirty document through
`KeptDocuments.capture`. For the unsaved bytes, that calls
`documentBytes`, which goes `documentSnapshot` → `Document.savedBytes`,
and `savedBytes` called `history.closeGroup()` as though the save had
happened. On a slow runner, a checkpoint landed between the test's two
keystrokes. Reproduced locally with a 4 s sleep between them (gives the
CI failure exactly); a 1.5 s sleep doesn't, because it stays inside the
checkpoint's spacing.

**The fix:** `savedBytes` still remembers which edit the bytes stand
for, but no longer closes the group. `written` (the `documentWritten`
hook, which only a real save reports: `DocumentIo` and the web download)
makes the undo stop, while the written state is still the top step. The
synchronous `save` path is unchanged.

One behavior moves: a keystroke typed while a save's write is in flight
(milliseconds, off-thread) now joins the step the save captured. The
document stays dirty, which is correct because that step is newer than
the file, but undo goes past the saved state instead of stopping on it.
Before, the snapshot closed the group up front.

**The test** doesn't touch the clock. It takes a checkpoint's capture
itself (`KeptDocuments.capture`) between "a" and "b" and still expects
`.joined`, so it covers the bug however long a frame takes. A new check
covers the save side: type, save through the host's path
(`documentBytes` + `documentWritten`), type, undo. That should give back
the saved text, clean. Each check fails when its half of the fix is
removed (verified by reverting `savedBytes`, and separately by dropping
the `closeGroup` in `written`).

## SDK impact

- [x] None
- [ ] Core-only or additive: reaches plugins at the next SDK release
- [ ] Fingerprint moved: recorded in `sdk/src/version.zig`,
`sdk_version` untouched, PR labelled `sdk`
- [ ] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the
last `sdk-v*` tag

## Verified

- [x] macOS: `zig build test-integration` (the tour test with the new
checks; it fails with either half of the fix reverted), plus `zig
build`, `test`, `check-web`, `test-sdk-version`, `scripts/check-fmt.sh`
- [ ] Windows:
- [ ] Linux: CI
- [ ] Web: `check-web` builds; not run

## Follow-ups

The root of this is the host using the save's snapshot for things that
aren't saves: the crash checkpoint, a restart or plugin reload
(`KeptDocuments`), and Tracks' share export. This PR fixes the undo
split inside the text plugin. Two problems stay, and want a host-side
seam (bytes as a save would write them, without a save's bookkeeping):

- With format-on-save on (off by default), a checkpoint runs the
formatter on a dirty document while it is being edited
(`documentSnapshot` formats before taking the bytes).
- A checkpoint taken while a save is in flight overwrites
`serialized_op_id`. If an edit came between the two, the save lands and
marks the document clean at an edit newer than what was written.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rom the top right

`LiquidField.Edge.cut` cuts glass by its place's outline with a bevel and lens band of its own along
the cut, rather than joining the place's outside: a piece of glass laid in a corner, bending the
border and ring it lies along. `light_across` turns a drop's rim light a quarter, for glass whose
edges that show face across the usual light (a place's top-right or bottom-right corner). Both reach
`core.dialogs.glassBubbles` as options.
@foxnne
foxnne force-pushed the core/glass-edge branch 2 times, most recently from e1a838a to 8dcf15b Compare October 19, 2026 15:03
@foxnne
foxnne marked this pull request as ready for review October 19, 2026 16:12
@foxnne
foxnne merged commit eb3d57a into main Oct 19, 2026
12 checks passed
@foxnne
foxnne deleted the core/glass-edge branch October 19, 2026 16:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant