Skip to content

Releases: crabtalk/bezel

v0.1.4

Choose a tag to compare

@clearloop clearloop released this 31 Aug 05:40
3cf2103

Measured surfaces

1. Frost and glass are one model. Both families resolve to the same numbers — SurfaceSpec { gain, saturation, tint, blur, rim, edge, edge_width, edge_aa, shadow }, where out = gain * saturated(backdrop) + tint over a backdrop blurred at blur. A caller still names the thing rather than the numbers: SurfaceStyle::Frost(Frost::Regular) or SurfaceStyle::Glass(Glass::Clear). The specs sit on Theme, per appearance, so they flow with the palette the way every other token does.

2. Frost is one material at five thicknesses. Measured 2026-08-31: across SwiftUI's five materials the tone implied by tint / (1 - gain) holds to within 9% (49.8 down to 45.3), and the sigma does not move with thickness at all (23.1 / 20.2 / 21.0pt). So Frost is a knob, not five looks — UltraThin through UltraThick are opacities 0.440 / 0.543 / 0.638 / 0.737 / 0.825 over one FrostSpec. Light is not a tone-flip of dark: it keeps the scale (0.622 at Regular against dark's 0.638) and swaps a 47 grey base for a 235 white one.

3. Glass, re-measured off a real NSGlassEffectView. One whole-canvas tone at a time — a fill the size of the probe cannot be contaminated by a 10pt blur, which is what every earlier reading of this look got wrong. Six greys give the transfer line at rms 0.9 levels, four saturated tones agree on the saturation to 0.1, and the sigma is the gaussian that best fits a 70pt bar edge at the centre of a 320pt glass, rms 0.4 levels.

appearance look gain saturation tint blur
dark Regular 0.311 2.55 11/255 10.8pt
dark Clear 1.029 1.00 16/255 1.2pt
light Regular 0.139 4.27 214/255 8.9pt
light Clear 1.041 1.00 18.8/255 1.2pt

4. Saturation is its own field. A gain alone moves level and colour together, so a surface could not go dark and keep its colours. saturation pushes the backdrop's chroma off its own grey before the gain drops the level — 1 is the pass-through Clear measures, and light Regular needs 4.27 because 84% of what it puts out is tint. Over a backdrop that is not locally flat the real material saturates less than these numbers reproduce, so this is not the material's own per-pixel saturation; it is the one that reproduces it.

5. The rim is a length, not a share of the box. Read off a position-coded backdrop — green ramping once across it, red sawtoothing every 32pt, so a pixel under the glass names where it came from — pooled over four shapes from 96pt to 320pt and r24 to r84: one curve, rms 1.8pt. SurfaceSpec::rim is 18.75pt whatever the shape, and Theme::glass_bevel(extent) is gone. The lit edge is separate, and measured over black where the coverage blend can only pull down: +25 levels at the boundary, gone by 1pt, the same in both appearances.

6. Every menu moves on one token. Theme::popover_surface is what the popover layers mount on — menus, dialogs, sheets, tooltips. It ships as Glass::Regular; point it at a Frost thickness and they all follow. A component that owns its surface names its own style instead.

Breaking

ui::material is now ui::surface, and its two entry points are one:

0.1.3 0.1.4
material::Glass (trait) surface::Surfaced
card.material(MENU_BLUR) card.surface(&theme, SurfaceStyle::Frost(Frost::Regular))
card.glass_effect() card.surface(&theme, SurfaceStyle::Glass(Glass::Clear))
material::material(r, blur, child) surface::of(r, style, child), or surface::popover(r, child)
material::MENU_BLUR, material::PANEL_BLUR gone — blur belongs to the look
.blurred(sigma) gone, same reason
theme::set_glass_bevel, theme::set_glass_magnify Theme::glass_magnify is a field; the bevel is SurfaceSpec::rim
Theme::glass_clear(), glass_bevel(), glass_magnify(), glass_dispersion() Theme::glass_clear, glass_regular, frost, glass_magnify, glass_dispersion — all fields

Apple exposes no blur, gain or bevel parameter on either family, and neither does this. A caller who wants different glass hands over a different theme.

Elsewhere

The meter reads memory. ui::stats::Stats gains a MEM row — this process's resident bytes, sampled at the tick rather than differenced across one, and where the platform hands out no process figure, a browser tab included. It paints itself as Glass::Regular now: a meter floats over the thing it is measuring, and the everyday material is the one that both refracts and carries dense content.

The gallery fits a narrow window. Below 684pt — the rail plus the padded column every component page is designed for, derived rather than chosen — the rail folds into a scrolling nav strip and a pattern's canvas pans instead of shrinking. The web gallery's "not supported on mobile" notice is deleted along with it: on touch an inline embed is a preview that opens into the dialog, so a swipe scrolls the page rather than being eaten by the canvas. The pixel mascots moved onto the avatar page, and the appearance switch is one icon button.

CI. fmt, clippy, and cargo nextest run --workspace on macOS and Linux, plus a wasm build — Linux is the platform with no lens that must still compile and pass, and wasm fails in ways no native build sees.

gpui. The fork moves 0.3.6 to 0.3.8.

What's Changed

  • feat(theme): model every surface as measured frost or glass by @clearloop in #10

Full Changelog: v0.1.3...v0.1.4

v0.1.3

Choose a tag to compare

@clearloop clearloop released this 29 Aug 14:14
2cfff23
liquid.720p.mov

Liquid Glass

1. A real refracting surface. glass_effect() paints a bevel at the rim that displaces the backdrop, a per-channel fringe across it, and an additive lift over a flat interior. The profile is measured off SwiftUI's .glassEffect(.clear), not eyeballed: on a 460x120 capsule the ruler behind it is displaced over the outer 27pt and is exactly unperturbed below, which is where the 0.225 bevel share comes from; the interior veil measures 1.042 * backdrop + 19/255 across five luminances.

2. It runs everywhere gpui does. The backdrop-blur primitive was Metal-only, so on the web a glass card came out an opaque slab. It's now implemented in the wgpu renderer too, via bezel-gpui 0.3.6 — the frame renders offscreen only when a scene actually contains a blur, and the gaussian is rebuilt as fragment passes since WebGL2 has no compute. Verified on Metal, WebGPU and WebGL2.

3. Breaking: Frosted is now Glass. material() is unchanged; glass_effect() is the new entry point beside it.

4. Motion stops when nobody's looking. pause_when_inactive, on by default: a spinner in a backgrounded window held 30fps and 22% of a core indefinitely, against 2% with this on.

What's Changed

Full Changelog: v0.1.2...v0.1.3

v0.1.2

Choose a tag to compare

@clearloop clearloop released this 27 Aug 12:34
0bd3940

Performance Updates

stats.720p.mov

1. Idle costs nothing

A bezel window at rest draws zero frames. In gpui one element can hold a whole window at the display's rate for as long as it stays mounted — a spinner written the obvious way cost 36% of a core. So nothing here asks the window for a frame.

2. One clock, leased per view

motion::Painter::lease(fps, until) is the only way anything in the library claims frames. One timer serves the whole app: it notifies the leasing view alone and parks when the last claim lapses. Spinners run at 30fps, hover fades at 60.

Two defects in it are fixed:

  • A claim gets the rate it asked for. The scheduler fell back to a claim's lapse horizon while a notify was outstanding, so a spinner asking for 30fps drew about 5.
  • A claim taken from an event listener keeps painting. Every hover fade is one, and no render renews it — they froze after a single frame, leaving a wash on rows the pointer had already left.

3. Dragging pays a rate, not a pointer

gpui refreshes the entire window on every mouse-move while a drag is active. ui::floating::panel leases frames instead, so a burst of 60 pointer samples costs at most 2 redraws: 100% of a core → 30%.

4. Caching is safe to opt into

Theme::install_custom now refreshes every window, so a cached view can no longer survive an appearance switch still painting the old palette.

5. Measure your own app

ui::stats::Stats, mounted in a floating panel, reports the frames your window drew, process CPU as a percentage of one core, and GPU time per frame. Idle should read 0.

Figures above are debug builds; release is roughly 5× cheaper per frame.

What's Changed

  • feat(bezel): the component library, docs browser, and facade crate by @clearloop in #1
  • feat: floating control bar and the Patterns tab by @clearloop in #2
  • feat: the catalog-trait component architecture by @clearloop in #3
  • feat: syntax highlighting, blob avatars and the agent crate by @clearloop in #4
  • feat(editor): add a Notion-style block editor over the markdown model by @clearloop in #5
  • feat: publish bezel 0.1.0 to crates.io by @clearloop in #6
  • feat: brand the palette from the gallery Create page by @clearloop in #7
  • feat(ui): add a floating frame, CPU and GPU meter by @clearloop in #8

New Contributors

Full Changelog: https://github.com/crabtalk/bezel/commits/v0.1.2