package dev.darkpyonix.composerust.design
// A copy of GlassMaterial.kt from the liquid-glass module of the design-systems
// project, with the package changed and SurfaceMaterial taken from this module rather
// than from that project's core. That project is published on its own and must never
// depend on the renderer, so the material exists twice deliberately. Copies run in one
// direction: change the design systems file first, then bring the change here.
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* What a surface is made of.
*
* Most surfaces are a flat colour. Apple's are not: since iOS 26 and macOS 26 they
* translucently tint what is behind them and catch light along their edges.
*
* This mirrors the type of the same name in the design systems project's core module.
* The two are kept identical by hand because that project must stay publishable without
* the renderer, and the renderer must stay buildable without it.
*/
@Immutable
sealed interface SurfaceMaterial {
/** A flat fill. What Material 3, Fluent and the Linux systems use. */
@Immutable
data class Opaque(val color: Color) : SurfaceMaterial
/**
* A translucent, edge-lit surface: Liquid Glass.
*
* The blur applies to content this application drew. Sampling the system background
* behind the window is not something a Compose surface can do, so this describes a
* real effect over app content rather than a claim about the desktop behind it.
*/
@Immutable
data class Glass(
val tint: Color,
val tintAlpha: Float,
val blurRadius: Dp,
/** The bright edge where light would catch the top of the surface. */
val highlight: Color,
/** The darker edge along the bottom, which gives the surface thickness. */
val shade: Color,
/**
* The opaque colour to use instead when blur is unavailable or the reader asked
* for reduced transparency. It has to meet contrast on its own.
*/
val fallback: Color,
) : SurfaceMaterial
}
/**
* How much the material asserts itself over what is behind it.
*
* Apple's two glass recipes differ in exactly this: one is for surfaces that carry
* controls and text and therefore has to win against a busy backdrop, the other is for
* surfaces floating over media where the content underneath is the point.
*/
enum class GlassProminence {
/** Carries text and controls. More tint, more blur, legible over anything. */
Regular,
/** Floats over content that should stay visible. Barely tinted. */
Clear,
}
/**
* The scope of what this module can and cannot draw, stated once so that no caller has to
* guess.
*
* Compose Multiplatform cannot produce the real Liquid Glass material. On iOS 26 the
* system draws it, through native SwiftUI navigation containers, and a Compose app only
* gets it by hosting its content inside a SwiftUI shell; the glass then belongs to that
* shell's chrome, not to anything drawn here. On the desktop, sampling the wallpaper
* behind the window needs a platform compositing view outside the Compose surface.
*
* What this module draws is the part whose backdrop is content this application drew
* itself: continuous and concentric corners, a translucent tinted surface over app
* content, an edge lit along the top and shaded along the bottom, depth from layers
* overlapping, and Compose's own blur applied to the content underneath. Those are
* really drawn, not approximated, and they are most of what makes a screen read as iOS
* 26.
*
* The wallpaper is the host's to put there. Where it has put the platform's own material
* behind the window, [windowMaterial] is the recipe for chrome that sits straight on it,
* and that chrome then really does show the desktop. Where it has not, nothing here
* claims to.
*/
object LiquidGlass {
/**
* A glass material with a guaranteed readable fallback.
*
* [backdrop] is the colour the material expects to sit over most of the time, and is
* used only to work out what the translucent surface will actually look like once it
* is composited. [content] is the colour that will be drawn on top of the material,
* and is what the fallback has to stay legible against: the fallback is computed by
* compositing the tint over the backdrop and then, if that does not reach
* [minContrast], moving it away from [content] until it does.
*
* The translucent path carries no such guarantee and cannot, because the thing behind
* it changes every frame. That asymmetry is the reason the fallback exists as a
* stored colour rather than as a runtime alpha adjustment.
*/
fun material(
dark: Boolean,
prominence: GlassProminence = GlassProminence.Regular,
backdrop: Color,
content: Color,
minContrast: Float = MIN_CONTRAST_BODY,
): SurfaceMaterial.Glass {
val tint = if (dark) DARK_TINT else LIGHT_TINT
// Low enough that what is behind the surface colours it. That is the one property
// a glass surface has that a grey one does not: the composer over a blue page is
// bluish, the sidebar over a pink wallpaper is pinkish. At the 0.85 this used to
// be, a pale surface over a blue page composited to a flat grey whatever was
// behind it, and every screen drawn in this system read as the flat language
// beside it.
//
// A surface this translucent over a page of its own colour would vanish, which is
// what happened the first time the page went white. What keeps it on screen there
// is not more tint but the two things glass really has: the lit rim, and the soft
// shadow of [LIFT] that a caller puts under it with `glassLift`.
val alpha = when (prominence) {
GlassProminence.Regular -> if (dark) 0.66f else 0.70f
GlassProminence.Clear -> if (dark) 0.32f else 0.34f
}
val blur = when (prominence) {
GlassProminence.Regular -> 30.dp
GlassProminence.Clear -> 18.dp
}
return glass(dark, tint, alpha, blur, backdrop, content, minContrast)
}
/**
* Glass for chrome that sits straight on a window the platform has backed with its own
* material.
*
* The platform's view behind the window already blurs the desktop and lays its own
* frost over it, so a surface drawn on top of that has very little left to do. Drawn
* with the ordinary recipe it covers seventy percent of what the platform went to the
* trouble of showing, and the sidebar that should carry the wallpaper's colour comes
* out the grey of its own tint. So this one is mostly rim: enough tint to lift the
* surface a step off the window around it, and the rest is the desktop.
*
* The fallback is computed against [backdrop] exactly as [material]'s is, so a reader
* who asked for reduced transparency still gets an opaque surface with its contrast
* guaranteed.
*/
fun windowMaterial(
dark: Boolean,
backdrop: Color,
content: Color,
minContrast: Float = MIN_CONTRAST_BODY,
): SurfaceMaterial.Glass = glass(
dark = dark,
tint = if (dark) DARK_TINT else LIGHT_TINT,
alpha = if (dark) WINDOW_TINT_ALPHA_DARK else WINDOW_TINT_ALPHA_LIGHT,
blur = 0.dp,
backdrop = backdrop,
content = content,
minContrast = minContrast,
)
private fun glass(
dark: Boolean,
tint: Color,
alpha: Float,
blur: Dp,
backdrop: Color,
content: Color,
minContrast: Float,
): SurfaceMaterial.Glass {
// Light arrives from above, as it does in every Apple surface: the top edge
// catches it and the bottom edge falls into shadow. A single flat stroke all the
// way round reads as a drawn border; this reads as thickness.
val highlight = if (dark) Color.White.copy(alpha = 0.34f) else Color.White.copy(alpha = 0.90f)
val shade = if (dark) Color.Black.copy(alpha = 0.46f) else Color.Black.copy(alpha = 0.10f)
val composited = compositeOver(tint.copy(alpha = alpha), backdrop)
return SurfaceMaterial.Glass(
tint = tint,
tintAlpha = alpha,
blurRadius = blur,
highlight = highlight,
shade = shade,
fallback = ensureContrast(composited, content, minContrast),
)
}
/**
* The tint of the glass itself, before anything shows through it.
*
* Light is white. Glass in light mode brightens what is behind it, which is why a
* glass capsule over a blue page reads as a paler blue and not as a grey; a grey tint
* pulls every backdrop towards the same grey, and that is the flat look this system
* exists not to have. The cost is that white glass over a white page is a step of
* nothing, and the rim and the lift are what carry it there, as they do in Apple's own
* screens.
*
* Dark is a grey lighter than any page the surface can land on and than any fill that
* can land on the surface. It was once two levels from the secondary fill grey, and a
* tinted button on a bar disappeared into the bar.
*/
val LIGHT_TINT: Color = Color(0xFFFFFFFF)
val DARK_TINT: Color = Color(0xFF3A3A3C)
/**
* How much of [windowMaterial]'s tint is laid over the platform's own material.
*
* About a fifth. The reference sidebar sits over a pink and blue wallpaper and its top
* is pink and its foot is blue, so most of what reaches the eye is the desktop and
* what the surface adds is a lighter wash and its rim. It is laid twice where a
* sidebar made of chrome sits on a window made of chrome, and the two together still
* leave two thirds of the desktop showing.
*/
const val WINDOW_TINT_ALPHA_LIGHT: Float = 0.18f
const val WINDOW_TINT_ALPHA_DARK: Float = 0.16f
/**
* How far the soft shadow `glassLift` draws around a floating glass surface reaches.
*
* Wide and faint. A capsule floating over a page of its own colour is visible because
* of this and the rim, not because of its tint.
*/
val LIFT: Dp = 26.dp
/**
* How dark that shadow is at the surface's edge, where it is darkest.
*
* Measured off the reference window three times over. Its sidebar's edge is eleven
* levels below the page beside it and is still showing thirty five points out. At
* fourteen hundredths over a twenty point blur ours was twenty one below and gone by
* twenty eight, which reads as an outline drawn round the panel rather than as the
* panel standing off the page; the wider blur brought the reach to thirty three and
* eleven hundredths brought the edge to fifteen.
*/
const val LIFT_ALPHA: Float = 0.08f
/**
* The ratio the opaque fallback is held to: WCAG 2.2 AA for body text.
*
* Body text is the hardest case a surface has to carry, so meeting it means every
* lighter demand is met too.
*/
const val MIN_CONTRAST_BODY: Float = 4.5f
/**
* How far a glass layer is pushed by each layer already beneath it.
*
* Stacked glass in iOS 26 does not gain a heavier shadow; it gains tint. Each layer
* sees slightly less of the original backdrop through it, which is what separates a
* sheet over a panel over the background without drawing three shadows.
*/
const val DEPTH_TINT_STEP: Float = 0.08f
/** The shadow a glass layer casts at [depth], wide and soft rather than tight. */
fun depthShadow(depth: Int): Dp = when (depth.coerceAtLeast(0)) {
0 -> 0.dp
1 -> 8.dp
2 -> 20.dp
else -> 36.dp
}
}
/**
* [this] with its translucency increased for a layer sitting at [depth] in a stack.
*
* Depth 0 is unchanged. The alpha is capped below 1 so that a deeply stacked surface is
* still glass rather than quietly becoming a flat fill.
*/
fun SurfaceMaterial.Glass.atDepth(depth: Int): SurfaceMaterial.Glass {
if (depth <= 0) return this
val raised = (tintAlpha + LiquidGlass.DEPTH_TINT_STEP * depth).coerceAtMost(0.94f)
return copy(tintAlpha = raised)
}