# Glass

The glass plugin draws Liquid Glass in native UI, the material of macOS 26
and later: what is under an element shows through it, frosted, bent near
its edges as through the rim of a lens, and lit along its hairline rim. It
also draws macOS's scroll edges, which content scrolling under
a bar fades or frosts under ([Scroll edges](#scroll-edges)), and blurs
what is under an element without the glass, evenly or fading along a
gradient ([Blur](#blur)). MyGo draws them with the plugin's shaders, on
the GPU or the CPU, so they look the same on macOS, Windows and Linux. It
is all Go: no JavaScript package, and nothing to `mygo.Use`. The
`examples/liquid-glass` app shows it: merged panes and an interactive
button beside a web page under the system's glass.

```go
import "github.com/egoist/mygo/plugins/glass"

ui.Row(c).Padding(8, 16).Gap(8).Radius(22).Material(glass.Glass{}).Children(func() {
	ui.Text(c, "On glass")
})
```

`glass.Glass` is a material (`ui.Material`): `Material` fills the element
with it in place of a background, and its `Radius` is its shape; its
children draw on it. What shows through is what was painted under it in
the same window: the elements before it, as content that scrolls under a
toolbar placed over it with `Absolute`, and other glass.

```go
ui.Box(c).Fill().Children(func() {
	ui.Scroll(c).Fill().Padding(64, 16, 16).ScrollbarInsets(64, 0, 0).Children(func() { /* ... */ })
	// The toolbar floats over the content, which scrolls under it.
	ui.Row(c).Absolute().Top(12).Left(12).Right(12).Padding(6).Radius(26).Material(glass.Glass{}).Children(func() {
		// ...
	})
})
```

## Styles

* **`glass.Regular`**, the default, frosts what shows through and
  lightens it, or darkens it in dark mode, so that what is on the glass
  reads over anything: bars, buttons and panels. Larger panes are
  frostier, as on macOS.
* **`glass.Clear`** barely blurs or tones what shows through: glass over
  photos and video, where what is on it brings its own contrast.

`Tint` colors the glass toward a color, by its alpha, as a prominent
button: `glass.Glass{Tint: t.Accent}`, with text in `t.AccentText`.
`Interactive` makes the glass grow a little while it is pressed, as
AppKit's interactive glass does on macOS 27: about 1.1 DIPs left and
right and 0.45 above and below, whatever its size, within 150 ms. AppKit
stretches the content too, by a pixel or so, which the children here are
not.

```go
done := ui.Row(c).Padding(8, 16).Radius(20).Material(glass.Glass{Tint: t.Accent, Interactive: true}).Children(func() {
	ui.Text(c, "Done").Bold().TextColor(t.AccentText)
})
if done.Clicked() {
	// ...
}
```

`Clarity`, from 0 to 1, makes panes as large as sidebars and inspectors
clearer, the content under them reading through: 0, the default, is
macOS's material, unchanged; 0.5 blurs up to 5 DIPs rather than 10, maps
black to 35% in light mode and white to 60% in dark mode, lights the rim
more and curves the last 48 DIPs; 1 is nearly clear, blurring up to 2
DIPs and mapping black to 20% in light mode, and black to 10% and white
to 85% in dark mode. Each half of the range is a straight step between
those. Panes up to 100 DIPs keep their material whatever the clarity,
which eases in up to 200 DIPs: `glass.Glass{Clarity: 0.5}`. Clear glass
barely tones what shows through already; on it, clarity only lights the
rim and curves the bezel.

In a drawing, `glass.Paint(p, r, radius, g)` paints a pane of glass over
what the painter painted before it.

## Merged panes

`glass.Group` puts a row of elements on one glass surface, as AppKit's
`NSGlassEffectContainerView` does: panes that nearly touch blend into one,
with one bezel around the whole surface, and separate when they move
apart.

```go
g := glass.NewGroup(c)
g.Radius = 22
g.Gap(6).Children(func() {
	g.Pane(ui.Box(c).Size(76, 44).Center().Children(func() { ui.Text(c, "Cut") }))
	g.Pane(ui.Box(c).Size(76, 44).Center().Children(func() { ui.Text(c, "Copy") }))
})
```

`Merge`, 24 DIPs by default, is how smoothly the panes blend: it bridges
panes up to about 12 DIPs apart, and 0 keeps them apart. A group holds up
to 8 panes. The elements are an ordinary row's and keep their own
backgrounds, borders and hit areas; the glass paints behind them.

## Native glass over a web view

On macOS 26 and later, `Glass{Native: true}` asks for the system's
`NSGlassEffectView` instead of the shader:

```go
ui.Row(c).Absolute().Top(0).Left(0).Right(0).Height(60).Radius(18).
	Material(glass.Glass{Native: true})
```

The system's glass is placed under the window's content and above a
`ui.WebView`'s page, where it samples the page, so a toolbar of native UI
over a web view gets real Liquid Glass, as macOS's own toolbars do.
Elsewhere, and with Reduce Transparency, the shader draws it, which sees
none of the page. The native glass samples what is under the surface: a web
view's page, or the window's material, never the elements that the frame
itself painted: the content is one surface above the glass. A native pane
over arbitrary native UI would need the frame drawn in two surfaces, with
the glass between them, which MyGo does not do; the shader draws those.

## Accessibility

With Reduce Transparency, `Glass`, `Blur` and `ScrollEdge` draw opaque
surfaces instead of translucent ones: the theme's `Surface` for glass, its
`Background` for blur and scroll edges. Increase Contrast lights the
glass's rim more and gives an opaque glass a border.

## Scroll edges

`glass.ScrollEdge` is the scroll edge effect of macOS 26 and later, a
material for an element over the edge of content that scrolls under a bar
of controls, so that they stand out from it, as SwiftUI's `safeAreaBar`
with `scrollEdgeEffectStyle` draws it:

```go
ui.Box(c).Fill().Children(func() {
	ui.Scroll(c).Fill().Padding(64, 16, 16).ScrollbarInsets(64, 0, 0).Children(func() { /* ... */ })
	// The content fades into the background under the bar.
	ui.Box(c).Absolute().Top(0).Left(0).Right(0).Height(74).PassThrough().Material(glass.ScrollEdge{})
	// Buttons of glass float over it.
	ui.Row(c).Absolute().Top(12).Left(12).Right(12).Gap(6).PassThrough().Children(func() {
		// ...
	})
})
```

* **Soft**, the default, fades what the content scrolls over into it:
  the background at 85% at the bar's edge, falling to nothing at the
  element's other edge, with no blur. Make the element the bar's height
  and 10 DIPs more, as macOS does.
* **`Hard`** frosts the content under the bar evenly: blurred by about 6
  DIPs, its colors saturated by a quarter, under 82% of the background,
  with a hairline along the element's other edge, black at 10% in light
  mode and white at 7% in dark mode. Make the element the bar's height.

`Bottom` puts the bar at the element's bottom, and `Background` sets what
the content scrolls over, the theme's `Background` unless set. Paint the
edge before what floats on it, and let `PassThrough` take the pointer to
the content.

They are macOS 27's, measured from SwiftUI's over test patterns: in its
layers, a soft edge replays the window's background under a gradient mask
from 85% to nothing, 10 points past the bar, and a hard edge blurs the
content (a Gaussian of radius 6, saturation 1.25 and 0.03 more), then
replays the background at 82.45% over it. Gray content matches AppKit's
within 1 of 255 in every row, light and dark; saturated colors differ by
up to 0.05, as AppKit mixes them in the display's wider color space and
MyGo's renderers in sRGB. macOS's window toolbars draw an edge of their
own, an even blur under 60% of the background, which a bar of native UI
does not have.

## Blur

`glass.Blur` is a backdrop blur, a material too: what is under the element
shows through it blurred by `Radius` DIPs (the standard deviation, as CSS's
`blur()` takes), with no tint, rim or shadow.

`Mask` makes it a progressive blur: a `LinearGradient` whose alpha says how
much of `Radius` each place blurs by, as CSS's `mask-image` does. Where the
gradient is opaque, what shows through blurs by `Radius`, where it is
translucent by less, and where it is transparent not at all; its colors do
not show. Under a bar, content then blurs the most at its edge, and less
and less further from it.

```go
ui.Box(c).Fill().Children(func() {
	ui.Scroll(c).Fill().Padding(64, 16, 16).ScrollbarInsets(88, 0, 0).Children(func() { /* ... */ })
	// What scrolls under the toolbar blurs, the more the nearer the top.
	ui.Box(c).Absolute().Top(0).Left(0).Right(0).Height(88).PassThrough().
		Material(glass.Blur{Radius: 6, Mask: &ui.LinearGradient{
			From: ui.RGB(0, 0, 0), To: ui.Transparent, Angle: 180, Start: 0.3, End: 1,
		}})
	// Buttons of glass float over it.
	ui.Row(c).Absolute().Top(12).Left(12).Right(12).Gap(6).PassThrough().Children(func() {
		// ...
	})
})
```

Here the blur is whole over the top 30% of the strip and fades out below,
`PassThrough` lets the pointer reach the content under the strip and
around the buttons, and `ScrollbarInsets` starts the scroll bar below the
strip, as AppKit starts a scroller below its toolbar's edge effect. Paint the blur before what floats on it: glass over
it shows the blurred content through. `Start` and `End` place the
gradient's colors along it, as everywhere; leaving `End` 0 puts both at
`Start`, a hard edge.

A blur that varies is drawn in steps, each blurring what the step before
drew a little more, shown where the blur wanted is more than the step's
own, mixed with the next between them: one step for each doubling of the
blur from two pixels, five for 12 DIPs on a Retina display. A pixel
between two steps shows both blurs mixed, as Core Animation's variable
blur does between the levels of its pyramid, and as the stacked
`backdrop-filter` layers of a progressive blur on the web do. Where the
gradient is opaque at an edge of the element in the middle of the window,
rather than at the window's edge, the blur differs a little within a few
pixels of that edge from one blurring each pixel by its own amount, as
each step reads what is around the element unblurred.

## How it looks like macOS

The glass follows macOS 27's, measured from AppKit's `NSGlassEffectView`,
with the optics of the open-source reproductions of Liquid Glass:

* **The bezel.** The surface is flat in the middle and curves down to its
  edges along Apple's squircle profile, over the last 36 DIPs, at most half
  of the pane. Light coming straight down refracts entering it, as through
  glass of refractive index 1.5, so what is near the edge comes from
  further inside: the rim mirrors what is just inside it, as a lens's
  does.
* **The material.** The regular glass blurs what shows through by up to 10
  DIPs, more for larger panes, and maps its lightness, black to 54% and
  white to 100% in light mode, and to 15% and 51% in dark mode, keeping
  its colors. The clear glass adds an eighth to it.
* **The light.** The rim is lit where it faces up or down and shaded where
  it faces the sides, within a pixel of the edge: a hairline, as AppKit's.
* **No shadow.** Neither `NSGlassEffectView` nor a button of the glass
  bezel casts one: the pane's edge is its rim alone.

## Performance

Each pane reads what is under it, averages and blurs it, then draws with
its own shader: a few small passes on the GPU, and a pane changes when
anything under it does, as content scrolling under a bar. When a window
draws on the CPU, without a GPU, as [Rendering](https://mygo.egoist.dev/docs/ui/rendering.md)
describes, a change under a pane redraws the pane and what its blur
reaches around it. A
progressive blur does this for each of its steps, where each shows: on
the CPU, a strip of 1360×176 pixels blurred by 24 takes 9 ms on an M5
fading, 1.7 evenly, and on the GPU a few small passes for each step.

A single material is a rounded rectangle. `glass.Group` merges up to 8
panes of it, as AppKit's `NSGlassEffectContainerView` does.
