# g1455: Liquid Glass for Flutter Every page of https://g1455.plugfox.dev, version 0.1.4 of the package. --- # Installation > Add the package, put one GlassHost above the navigator, and lay the first glass over content. - Live: https://g1455.plugfox.dev/start/installation - API: [`GlassHost`](https://pub.dev/documentation/g1455/latest/g1455/GlassHost-class.html), [`GlassBar`](https://pub.dev/documentation/g1455/latest/g1455/GlassBar-class.html), [`kTextContrastAA`](https://pub.dev/documentation/g1455/latest/g1455/kTextContrastAA-constant.html) - Source: [`lib/src/surface/glass_host.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_host.dart) g1455 draws Liquid Glass in Flutter: refraction, blur, tint and a rim over whatever is painted behind it. Getting it on screen takes three steps: add the package, put one `GlassHost` at the top of the app, and place glass over some content. The package is pure Dart and shaders. It ships no platform code, so there is nothing to configure in Xcode or Gradle. ## Install ```bash flutter pub add g1455 ``` It needs Flutter 3.47 or later. Then import it wherever you use glass: ```dart import 'package:g1455/g1455.dart'; ``` ## Add the host Every piece of glass needs a [GlassHost](https://g1455.plugfox.dev/foundations/host.md) above it. The host records what is painted under all the glass of the screen into one shared image, and every surface samples its own slice of it. Without a host, a glass surface draws **no glass at all**, only its child. Put the host in the `builder:` of your `MaterialApp` (or `WidgetsApp`, `CupertinoApp`). That places it above the navigator, which is also where dialogs, sheets, menus and popovers are built, so they find the host too: ```dart MaterialApp( builder: (BuildContext context, Widget? child) => GlassHost(child: child!), home: const HomePage(), ) ``` One host per app is the normal setup. Don't nest a host around each widget. ## The first glass Glass shows what is behind it, so it needs something behind it: lay it over content in a `Stack`. [GlassBar](https://g1455.plugfox.dev/components/bar.md) is a floating capsule for navigation that also picks a legible label colour for its children: ```dart Stack( children: [ Positioned.fill(child: content), // what the glass refracts const Positioned( top: 16, left: 16, right: 16, child: GlassBar(child: Text('Library')), ), ], ) ``` The "Code" tab has a complete `main.dart` with a scrolling list under a bar. > [!TIP] > Tell the host what is behind the glass. For a list of photos or a feed, pass `richBackdrop: true` and > `minLabelContrast: kTextContrastAA`; over a flat colour, pass `backdrop:`. Labels are then chosen and kept readable > for that case. See [Legibility & theme](https://g1455.plugfox.dev/foundations/legibility.md). ## The first frame has no glass The host captures the backdrop after a frame has been painted, and the glass draws that capture on the next frame. So the very first frame of a screen shows the glass's children without the glass itself. That is by design and is not visible in practice, but it matters in widget tests: pump one more frame before you look for glass. ## Compile the shaders before the first frame The host loads the package's shaders when it mounts, and a shader is compiled asynchronously. Until it lands, glass that has a capture draws a stand-in: the captured, blurred backdrop clipped to its shape, with no tint, rim or bend. Compile them before `runApp` and the first frame that has a capture is drawn through the optics: ```dart Future main() async { WidgetsFlutterBinding.ensureInitialized(); await GlassHost.precache(); runApp(const GlassApp()); } ``` `GlassHost.precache()` shares the loads a host starts on its own, so nothing is compiled twice, and once the programs are in it costs nothing. `group: false` and `ripple: false` leave out the programs of [groups](https://g1455.plugfox.dev/foundations/groups.md) and the [ripple](https://g1455.plugfox.dev/foundations/ripple.md) for an app that uses neither. It completes with the error of a load that fails, so catch it if the app should start regardless. ## Next steps - [How it works](https://g1455.plugfox.dev/start/how-it-works.md): one capture for the whole screen, and only when something changed. - [What the app declares](https://g1455.plugfox.dev/start/declarations.md): the backdrop, reduce transparency, contrast, thermal state. - [GlassHost](https://g1455.plugfox.dev/foundations/host.md) and [GlassSurface](https://g1455.plugfox.dev/foundations/surface.md): the two building blocks. - [Finishes](https://g1455.plugfox.dev/foundations/finishes.md): regular, clear and frosted glass. - [Scaffold](https://g1455.plugfox.dev/components/scaffold.md): a whole screen, a bar, a tab bar and a list scrolling under them, wired for you. - The components: [Bar](https://g1455.plugfox.dev/components/bar.md), [Button](https://g1455.plugfox.dev/components/button.md), [Card](https://g1455.plugfox.dev/components/card.md), [Switch](https://g1455.plugfox.dev/components/switch.md), [Slider](https://g1455.plugfox.dev/components/slider.md), [Tab bar](https://g1455.plugfox.dev/components/tab-bar.md), [Alert](https://g1455.plugfox.dev/components/alert.md), [Sheet](https://g1455.plugfox.dev/components/sheet.md) and more. - [Adaptive glass](https://g1455.plugfox.dev/foundations/adaptive.md): glass that reads its backdrop, for screens over photographs. - [Performance](https://g1455.plugfox.dev/foundations/performance.md): what glass costs and how to keep it cheap. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); // Compiles the shaders before the first frame, so the first glass on screen // is drawn through its optics. await GlassHost.precache(); runApp(const GlassApp()); } class GlassApp extends StatelessWidget { const GlassApp({super.key}); @override Widget build(BuildContext context) => MaterialApp( title: 'Glass', theme: ThemeData.dark(), // One host for the whole app, above the navigator, so pages, dialogs, // sheets and menus are all under it. builder: (BuildContext context, Widget? child) => GlassHost( // A feed of colourful tiles scrolls under the glass: choose labels for // the worst case, and keep them at WCAG AA. richBackdrop: true, minLabelContrast: kTextContrastAA, backdrop: const Color(0xFF101014), child: child!, ), home: const LibraryPage(), ); } class LibraryPage extends StatelessWidget { const LibraryPage({super.key}); @override Widget build(BuildContext context) { final EdgeInsets safe = MediaQuery.paddingOf(context); return Scaffold( backgroundColor: const Color(0xFF101014), body: Stack( children: [ // The content: what the glass refracts. ListView.builder( padding: EdgeInsets.fromLTRB(16, safe.top + 76, 16, safe.bottom + 16), itemCount: 30, itemBuilder: (BuildContext context, int i) => Container( height: 120, margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: LinearGradient( colors: [ HSVColor.fromAHSV(1, (i * 37) % 360.0, 0.7, 0.9).toColor(), HSVColor.fromAHSV(1, (i * 37 + 60) % 360.0, 0.8, 0.5).toColor(), ], ), ), ), ), // The glass: a bar floating over the list. Positioned( top: safe.top + 8, left: 16, right: 16, child: const GlassBar( child: Row( children: [ Icon(Icons.photo_library_outlined), SizedBox(width: 12), Expanded( child: Text('Library', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600)), ), Icon(Icons.search), ], ), ), ), ], ), ); } } ``` --- # How it works > One host captures the backdrop once for every glass surface, only when something under the glass changed. - Live: https://g1455.plugfox.dev/start/how-it-works - API: [`GlassHost`](https://pub.dev/documentation/g1455/latest/g1455/GlassHost-class.html), [`GlassTravel`](https://pub.dev/documentation/g1455/latest/g1455/GlassTravel-class.html) - Source: [`lib/src/surface/glass_host.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_host.dart) Most glass packages put a `BackdropFilter` on every surface, which means the engine reads the backdrop once per surface on every frame. g1455 takes a different route, built for cost first: one capture for the whole screen, taken only when it has to be, and a blur that comes almost for free. Knowing the route helps you predict what is cheap and what is not, which is most of what there is to know about performance with this package. ## One host, one capture A [GlassHost](https://g1455.plugfox.dev/foundations/host.md) records what is painted under **all** of its glass into one atlas, at a resolution picked against a measured quality budget. Every surface then samples its own slot of that atlas and draws the refraction, blur, tint and rim in one shader pass. So ten surfaces do not mean ten reads of the backdrop. They mean one capture and ten draws. ## A capture only when something changed The host walks the composited layer tree. When nothing under the glass changed, it keeps the capture it already has. That covers a still screen, and a moving glass over content that stays put. Keeping the capture is the default, and it is the biggest saving in the package: 79.4% and 66.3% of the route's added cost on Adreno, 97.8% on Metal. What triggers a new capture: - content under the glass repaints: a list scrolls under a bar, an animation plays behind a card, a video runs; - the glass appears, disappears, resizes or moves to a new place; - a surface's `materialize` animates, because that changes the blur. What does not: - a still screen, however much glass it has; - glass moving inside a [GlassTravel](https://g1455.plugfox.dev/foundations/travel.md) region over still content, such as a slider's knob, a dragged lens or orbiting blobs; - a blinking caret or typing in a [text field](https://g1455.plugfox.dev/components/text-field.md); - repaints that happen away from every glass surface. ## The blur is a downscale A capture taken at 1/N of the screen's resolution and scaled back up is a Gaussian blur of σ ≈ N/2, to within about 1%, at a fraction of the price of a real Gaussian. The host picks the downscale for each finish against measured quality tables: a blurry finish such as [frosted](https://g1455.plugfox.dev/foundations/finishes.md) tolerates a smaller capture, and clear glass, which has no blur, needs a sharper one. ## What it costs Measured costs, each on its own platform, because the platforms are not comparable: | platform | glass vs stock Material | engine `BackdropFilter.grouped` | |---|---|---| | Android, Impeller/Vulkan, Adreno 830 (GPU cycles) | ×0.99…1.08 | ×1.78…3.15 | | iPad, Impeller/Metal (GPU ms, scrolling screen) | ×1.93…2.02, or ×1.46…1.54 with thermal throttling | — | ## Where the numbers come from Every number on this site was measured in a profile build, on the device named, before the package's first release: the code that became 0.1.0 on 2026-10-03, on Flutter **3.47.1** stable (framework `6655482ec0`, engine `5d53178869`, Dart 3.13.1). The raw digests are in [`provenance/`](https://github.com/PlugFox/g1455/tree/master/provenance) in the repository. | device | GPU | system | renderer | metric | dates | |---|---|---|---|---|---| | Samsung Galaxy S25 Ultra (SM-S938B) | Snapdragon 8 Elite, Adreno 830 | Android 16 | Impeller, Vulkan; 1080×2340 at 3×, 120 Hz | GPU cycles a frame (kgsl `busy × freq`), windows of 30 s × 3 | 2026-08-24 to 2026-09-23 | | iPad Pro 11″, 4th generation (iPad14,3) | Apple M2 | iPadOS 26.6.1 | Impeller, Metal; 1668×2388 at 2×, 120 Hz | GPU ms a frame (the engine's `GPUTracer`), after a reboot, windows of 4 s × 3 | 2026-09-08 to 2026-09-26 | | Samsung Galaxy S22 Ultra (SM-S908B) | Exynos 2200, Xclipse 920 | Android 16 | Impeller, Vulkan; 720×1544 | GPU `busy × freq`, which does not see the capture: a cross-check only | 2026-09-08 to 2026-09-26 | | MacBook Pro | Apple M3 Max | macOS 26.4.1 | Impeller, Metal; 1600×1200 at 2× | raster ms a frame | 2026-09-15 | The ratios compare scenes on one device, run in one binary in a shuffled order. They do not carry from one device to another, and they are not frame times. ## What this means for your app - A still screen with glass costs almost nothing beyond drawing the glass itself. - Glass that moves over still content belongs in a [GlassTravel](https://g1455.plugfox.dev/foundations/travel.md) region. The built-in switch, slider, segmented control and tab bar already do this. - Content that changes under glass costs one capture per changed frame. That is the honest price of a list scrolling under a bar, and it is still one capture, not one per surface. - Each surface is still one draw, and the overhead grows faster than the count. Keep glass in the navigation and controls layers. See [Performance](https://g1455.plugfox.dev/foundations/performance.md). - Glass on top of other glass needs one capture per level. Glass nested inside glass gets that automatically; sibling glass that floats above other glass needs [GlassAbove](https://g1455.plugfox.dev/foundations/above.md). > [!NOTE] > If you have reason to distrust the change detection, `GlassHost(content: GlassContentDeclaration.undeclared)` > re-captures every frame. It is the most expensive thing the package can be asked to do; use it to rule the > detection out, not to ship. --- # What the app declares > What the package cannot read from the render tree: the backdrop, reduce transparency, contrast, thermal state. - Live: https://g1455.plugfox.dev/start/declarations - API: [`GlassHost`](https://pub.dev/documentation/g1455/latest/g1455/GlassHost-class.html), [`GlassTierPolicy`](https://pub.dev/documentation/g1455/latest/g1455/GlassTierPolicy-class.html), [`GlassThermalState`](https://pub.dev/documentation/g1455/latest/g1455/GlassThermalState.html), [`GlassHardware`](https://pub.dev/documentation/g1455/latest/g1455/GlassHardware.html) - Source: [`lib/src/surface/glass_host.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_host.dart) Some things the glass needs to know are not in the render tree, and Flutter does not pass some platform settings on. g1455 ships no platform code to go and read them, so the application declares them, all as parameters of [GlassHost](https://g1455.plugfox.dev/foundations/host.md). Everything here is optional: leave a declaration out and you get a safe default. ## What is behind the glass Components such as [GlassBar](https://g1455.plugfox.dev/components/bar.md) and [GlassCard](https://g1455.plugfox.dev/components/card.md) choose black or white text. To choose well, the host needs to know what the glass sits over: - `backdrop:` the screen's average background colour, for a flat background. It is also what the opaque tier fills with, so declare it whenever you might use that tier. - `richBackdrop: true` for an image, a video, a map or a feed. Labels are then chosen for the worst case over any backdrop. - `minLabelContrast:` a contrast floor, such as `kTextContrastAA` (4.5, WCAG AA for body text). When the finish cannot reach it, the glass is dimmed just enough to do so. Without any of these, labels are picked against the worst case, and in debug the package warns once when a finish cannot be read over it. More on [Legibility & theme](https://g1455.plugfox.dev/foundations/legibility.md). ## Reduce transparency Flutter does not expose the operating system's Reduce Transparency switch. Read it natively and pass it to a [GlassTierPolicy](https://g1455.plugfox.dev/foundations/tiers.md), which answers with the opaque tier: ```dart GlassHost( backdrop: const Color(0xFF101014), // the opaque tier fills with this tier: GlassTierPolicy(reduceTransparency: reduceTransparency).choose(), child: child, ) ``` The same policy takes a `ceiling` for low-end devices, such as `GlassTier.cheap`. The package never switches tiers on its own. ## Increase contrast `GlassHost.highContrast` draws an opaque, visible outline instead of the subtle rim. On iOS and on Android 34 and later the host already reads it from `MediaQuery`. On macOS the engine does not pass it on, so read it natively and pass `highContrast: true`. ## Thermal state Pass the device's thermal state as a `GlassThermalState` (`nominal`, `fair`, `serious`, `critical`, Apple's four names). Under `serious` and `critical` the host may reuse a slightly stale capture for a frame or two on screens that change. Blurry finishes tolerate that; `clear` gets none. Tiers are never changed by thermals. On Android, map `PowerManager`'s thermal status: NONE to `nominal`, LIGHT and MODERATE to `fair`, SEVERE to `serious`, and CRITICAL, EMERGENCY and SHUTDOWN to `critical`. ## Hardware `GlassHost.hardware` says which device family's measurements apply. `GlassHardware.detect()` returns `appleMetal` on iOS and macOS and `unmeasured` elsewhere, because Dart cannot name the GPU. Declare `adrenoVulkan` only for a Snapdragon with an Adreno 830-class GPU. Undeclared hardware gets the same behaviour with no price attached: it changes reported costs and the default texture limit, never correctness. > [!WARNING] > A `GlassTierPolicy(pinned: ...)` overrides everything, the user's Reduce Transparency setting included. Pin a tier > for tests and benchmarks, not for users. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// The settings the package cannot read for itself. Fill them from your own /// platform code (a method channel, a plugin) and rebuild when they change. class DeviceSignals { const DeviceSignals({ this.reduceTransparency = false, this.increaseContrast, this.thermal = GlassThermalState.nominal, this.lowEndDevice = false, }); final bool reduceTransparency; final bool? increaseContrast; // null: let the host read MediaQuery final GlassThermalState thermal; final bool lowEndDevice; } class DeclaredApp extends StatelessWidget { const DeclaredApp({super.key, required this.signals, required this.home}); final DeviceSignals signals; final Widget home; @override Widget build(BuildContext context) => MaterialApp( builder: (BuildContext context, Widget? child) => GlassHost( // What is behind the glass: a feed over a near-black page. backdrop: const Color(0xFF101014), richBackdrop: true, minLabelContrast: kTextContrastAA, // Reduce transparency gives the opaque tier; a low-end device stops at cheap. tier: GlassTierPolicy( reduceTransparency: signals.reduceTransparency, ceiling: signals.lowEndDevice ? GlassTier.cheap : null, ).choose(), highContrast: signals.increaseContrast, thermal: signals.thermal, hardware: GlassHardware.detect(), child: child!, ), home: home, ); } ``` --- # Platforms & web > The same glass on Impeller (Metal, Vulkan, GLES), on Skia, and on the web with CanvasKit and Skwasm. - Live: https://g1455.plugfox.dev/start/platforms g1455 is Dart and fragment shaders, with no platform code of its own. The glass renders byte-identically on Impeller and on Skia, and it works on the web. ## Renderers - **Impeller** on Metal (iOS, macOS), Vulkan and GLES (Android). - **Skia/GLES**, which Android falls back to below API 29 and on Vivante GPUs. There is nothing to switch on per platform: the same `GlassHost` and the same surfaces work everywhere. ## Web The glass works on Flutter web with both renderers, **CanvasKit** and **Skwasm**. This very site is a Flutter web app: every demo on it is the package running in your browser, under one `GlassHost` in the app's `builder:`. CanvasKit is the slow one. Every capture goes through `Picture.toImageSync`, which on CanvasKit reads the pixels back from the GPU and waits for them: on a MacBook Pro with an M3 Max (macOS 26.4.1), a frame of full glass on this site took 15 to 30 ms on CanvasKit and 4 to 10 ms on Skwasm, in WebKit and in Chromium alike (g1455 0.1.1 on Flutter 3.47.1, 2026-10-04). Flutter picks Skwasm only in Chromium browsers unless the app allows WebKit too; where an app still runs on CanvasKit, a [cheaper tier](https://g1455.plugfox.dev/foundations/tiers.md) captures nothing and the two renderers are level there. > [!NOTE] > Browsers do not tell an app about Reduce Transparency, increased contrast or thermal state. Declare what you know, > as on any platform: see [What the app declares](https://g1455.plugfox.dev/start/declarations.md). ## Shaders Every bundled shader is compiled for all five shader targets (SkSL, Vulkan, GLES, GLES3 and Metal) in the package's own test suite, so a shader that one of the compilers would reject fails the package's tests rather than your app. ## Hardware and cost What the glass costs differs between GPU families, and the package's measurements come from two of them: an Adreno 830 on Vulkan and Apple GPUs on Metal; the devices, systems and dates are in [Where the numbers come from](https://g1455.plugfox.dev/start/how-it-works.md). On other hardware the glass works the same; only the reported prices are missing. See [What the app declares](https://g1455.plugfox.dev/start/declarations.md) for `GlassHost.hardware`, and [Performance](https://g1455.plugfox.dev/foundations/performance.md) for keeping the cost down on any device. --- # AI agents > An agent skill for Claude Code, Codex, Cursor, Antigravity, Gemini CLI and Copilot: the rules for writing glass, and every page of this site, installed with one command. - Live: https://g1455.plugfox.dev/start/agents g1455 ships an [agent skill](https://agentskills.io): a `SKILL.md` that tells a coding agent how to write glass that works the first time (one host above the navigator, what the app declares, what costs a capture, which widget fits), with every page of this site beside it as a reference file. The agent loads it on its own when a project uses g1455 or you ask for liquid glass, and reads a component's page before it writes one. ## Any agent ```bash npx skills add PlugFox/g1455 ``` The installer ([skills.sh](https://skills.sh)) asks which agents to install for and puts the skill where each one looks. `-a claude-code -a codex` picks agents without asking, `-g` installs for your user instead of the project, `-y` skips the questions. `npx skills update` brings it up to date. The same works from this site's address, which publishes the skill at `/.well-known/agent-skills/index.json`: ```bash npx skills add https://g1455.plugfox.dev ``` ## Claude Code The repository is a plugin marketplace with the skill as its one plugin. In Claude Code: ```text /plugin marketplace add PlugFox/g1455 /plugin install g1455@g1455 ``` `/plugin marketplace update g1455` fetches a newer one. ## Without Node The skill is one archive. Unpack it where your agent looks for skills: ```bash mkdir -p .agents/skills/g1455 curl -fsSL https://g1455.plugfox.dev/.well-known/agent-skills/g1455.tar.gz | tar -xz -C .agents/skills/g1455 ``` | Agent | In the project | For your user | |---|---|---| | Claude Code | `.claude/skills/g1455` | `~/.claude/skills/g1455` | | Codex, Cursor, Antigravity, Gemini CLI, GitHub Copilot | `.agents/skills/g1455` | `~/.agents/skills/g1455` | Commit the project's copy and everyone working on the app gets it. ## Without installing Tell the agent to read the skill from the site, and it follows the links it needs: ```text Read https://g1455.plugfox.dev/SKILL.md and follow it. ``` Every page of the site is also markdown at its address plus `.md`, such as [/components/slider.md](https://g1455.plugfox.dev/components/slider.md). [llms.txt](https://g1455.plugfox.dev/llms.txt) lists them all, and [llms-full.txt](https://g1455.plugfox.dev/llms-full.txt) is every page in one file. > [!NOTE] > The skill describes the package's current version, and the API is 0.x. Update the skill when you update the package; > it tells the agent to check the changelog when the versions differ. --- # GlassHost > The engine room of a screen: one capture of the backdrop for all its glass, re-taken only when it changed. - Live: https://g1455.plugfox.dev/foundations/host - API: [`GlassHost`](https://pub.dev/documentation/g1455/latest/g1455/GlassHost-class.html), [`GlassThermalPolicy`](https://pub.dev/documentation/g1455/latest/g1455/GlassThermalPolicy-class.html), [`ProxyResolution`](https://pub.dev/documentation/g1455/latest/g1455/ProxyResolution-class.html) - Source: [`lib/src/surface/glass_host.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_host.dart) `GlassHost` records the content under every glass surface below it into one shared, downscaled image, and only re-records it when something under the glass actually changed. Every surface then samples its own slice of that image. It is also where a screen's glass is configured: the default finish, the tier, what is behind the glass, contrast, thermal state and the ripple. The host installs a [GlassTheme](https://g1455.plugfox.dev/foundations/legibility.md) with those values for everything below it. ## When to use - Always: any screen with glass needs exactly one host above it. A surface at the full tier with no host above draws no glass, only its child. - Put it in `MaterialApp(builder: ...)`, above the navigator, so dialogs, sheets, menus and popovers find it too. They throw a debug error when they can't. - Don't nest a host around each widget, and don't put it below the `Navigator` if you use any modal. ## Usage ```dart MaterialApp( builder: (BuildContext context, Widget? child) => GlassHost( backdrop: const Color(0xFF101014), richBackdrop: true, minLabelContrast: kTextContrastAA, child: child!, ), home: const HomePage(), ) ``` ## Behaviour - **One capture for the screen.** Ten surfaces are one capture and ten draws, not ten reads of the backdrop. - **Captures only on change.** When nothing under the glass changed since the last frame, the host keeps the capture it has. A still screen and glass moving inside a [GlassTravel](https://g1455.plugfox.dev/foundations/travel.md) region cost no capture. Content that repaints under glass, such as a list scrolling under a bar, costs one capture per changed frame. - **The first frame has no glass.** The host captures after a frame is painted, and surfaces draw the capture on the next frame. - **Shaders compile when the host mounts.** Until they land, glass draws the blurred backdrop with no tint, rim or bend. `await GlassHost.precache()` in `main()`, before `runApp`, compiles them first. See [Installation](https://g1455.plugfox.dev/start/installation.md). - **The resolution is chosen for you** against a quality budget (`budgetDeltaE`), from the finishes in use. Pin it with `resolution:` only for tests and benchmarks. The demo above counts the host's captures. Leave everything still and the count stays flat; turn on the drifting backdrop and it captures every frame. ## Theme The host builds a `GlassTheme` from its parameters and puts it below itself. That has two consequences: - A `GlassTheme` placed **above** the host is ignored. Configure the screen through the host's parameters. - A `GlassTheme` placed **below** the host overrides a subtree: a different finish for one panel, or the cheap tier for a list of cards. See [Legibility & theme](https://g1455.plugfox.dev/foundations/legibility.md) and [Tiers & fallbacks](https://g1455.plugfox.dev/foundations/tiers.md). When `finish` is null the host uses Apple's `.regular`, which is two materials: it picks `regularDark` or `regularLight` from `backdrop` and the platform's appearance, and follows appearance changes. With `adaptive:` set it picks per glass instead, from what each one reads under it: see [Adaptive glass](https://g1455.plugfox.dev/foundations/adaptive.md). The host also sets the screen's motion: `ripple:` for a touch wave ([Ripple](https://g1455.plugfox.dev/foundations/ripple.md)) and `dropMotion:` for how the held drops of the controls stretch and squash ([Drop motion](https://g1455.plugfox.dev/foundations/drop-motion.md)). ## Gotchas > [!WARNING] > `maxCaptures`, `blurPass`, `resolution` and `content: GlassContentDeclaration.undeclared` are diagnostics. They are > there to measure and to rule things out; don't ship them. - No platform code ships. Reduce transparency, thermal state and macOS increase-contrast must be read by your app and passed in. See [What the app declares](https://g1455.plugfox.dev/start/declarations.md). - On a very large window, `maxTextureSide` (8192 on Apple, 4096 elsewhere) may limit the capture. Raise it, for example to 16384, if you know the GPU supports it. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; void main() => runApp(const HostApp(reduceTransparency: false)); class HostApp extends StatelessWidget { const HostApp({super.key, required this.reduceTransparency}); /// Read natively by the app: Flutter does not pass it on. final bool reduceTransparency; @override Widget build(BuildContext context) => MaterialApp( // Above the navigator, so dialogs, sheets and menus find the host too. builder: (BuildContext context, Widget? child) => GlassHost( // What is behind the glass: images and colour over a near-black page. backdrop: const Color(0xFF101014), richBackdrop: true, minLabelContrast: kTextContrastAA, // Reduce transparency gives the opaque tier, which fills with `backdrop`. tier: GlassTierPolicy(reduceTransparency: reduceTransparency).choose(), thermal: GlassThermalState.nominal, child: child!, ), home: const HomePage(), ); } class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) => Scaffold( backgroundColor: const Color(0xFF101014), body: Stack( children: [ // Still content: the host captures it once and keeps the capture. const Positioned.fill(child: FlutterLogo(style: FlutterLogoStyle.stacked)), Positioned( left: 16, right: 16, bottom: MediaQuery.paddingOf(context).bottom + 16, child: GlassBar( child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ IconButton(onPressed: () {}, icon: const Icon(Icons.home)), IconButton(onPressed: () {}, icon: const Icon(Icons.search)), IconButton(onPressed: () {}, icon: const Icon(Icons.person)), ], ), ), ), ], ), ); } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `child` | `Widget` | **required** | The screen. Everything the glass shows must be inside it. | | `finish` | `GlassFinish?` | `null` | The default material for every surface below. Null is Apple's `.regular`: `regularDark` or `regularLight`, picked from `backdrop` and the platform's appearance. | | `tier` | `GlassTierChoice` | `GlassTierChoice.byDefault` | Full glass, a cheap translucent fill, or an opaque fill. Usually from `GlassTierPolicy.choose()`. | | `backdrop` | `Color?` | `null` | The screen's average background colour. Used for the label colour, and needed for the opaque tier to look right. | | `richBackdrop` | `bool` | `false` | The content behind the glass is an image, video, map or feed, so labels are chosen for the worst case. | | `minLabelContrast` | `double?` | `null` | Minimum label contrast, e.g. `kTextContrastAA` (4.5). The glass is dimmed just enough to meet it. | | `highContrast` | `bool?` | `null` | Draws an opaque outline instead of the subtle rim. Null reads `MediaQuery.highContrastOf`; on macOS pass it yourself. | | `ripple` | `GlassRipple?` | `null` | A touch wave for every surface below. None by default. | | `dropMotion` | `GlassDropMotion` | `GlassDropMotion()` | How the held drop of the switch, slider, segmented control and tab bar stretches and squashes. `GlassDropMotion.none` keeps it round. | | `adaptive` | `GlassAdaptive?` | `null` | Each bar, card and button reads the backdrop under it and picks its branch and label. Null reads nothing. | | `thermal` | `GlassThermalState?` | `null` | The device's thermal state, read by your app. Null is nominal. | | `thermalPolicy` | `GlassThermalPolicy` | `GlassThermalPolicy()` | How much staleness each thermal state may spend. `GlassThermalPolicy.never` keeps every frame fresh. | | `hardware` | `GlassHardware?` | `null` | Which device family's measurements apply. Null detects: `appleMetal` on Apple, otherwise `unmeasured`. | | `budgetDeltaE` | `double` | `ProxyResolutionPolicy.defaultDamageBudgetDeltaE` | The quality budget the host trades for speed when it picks the capture's resolution. | | `maxTextureSide` | `int?` | `null` | Largest GPU texture side in device pixels. Null is the hardware's floor: 8192 on Apple, 4096 elsewhere. | | `content` | `GlassContentDeclaration` | `GlassContentDeclaration.byDefault` | `undeclared` forces a capture every frame. Diagnostic. | | `resolution` | `ProxyResolution?` | `null` | Pins the capture's downscale (`full()`, `half()`, `quarter()`, `divisor(n)`). For tests and benchmarks. | | `blurPass` | `ProxyBlurPass?` | `null` | How the residual blur is applied. Diagnostic. | | `maxCaptures` | `int?` | `null` | Stops capturing after N captures. Diagnostic; never ship it. | ### GlassHost.precache `static Future precache({bool group = true, bool ripple = true})`: compiles the package's shaders now, so the first glass on screen is drawn through its optics. Call it in `main()` after `WidgetsFlutterBinding.ensureInitialized()`. Idempotent, and it shares the loads a host starts, so nothing compiles twice. `group: false` and `ripple: false` leave out those programs. Completes with a load's error if one fails. --- # GlassSurface > The primitive: a box of the screen that is glass. It refracts, blurs and tints the backdrop, then paints its child. - Live: https://g1455.plugfox.dev/foundations/surface - API: [`GlassSurface`](https://pub.dev/documentation/g1455/latest/g1455/GlassSurface-class.html), [`kGlassCapsule`](https://pub.dev/documentation/g1455/latest/g1455/kGlassCapsule-constant.html), [`GlassFade`](https://pub.dev/documentation/g1455/latest/g1455/GlassFade-class.html) - Source: [`lib/src/surface/glass_surface.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_surface.dart) `GlassSurface` says "this box of the screen is glass". It refracts, blurs and tints what is behind it, draws a thin rim along its edge, then paints its child on top. Its shape is a smooth rounded rectangle, Apple's continuous-corner squircle, drawn as the engine's own `RSuperellipse`. Every component in the package is built from it. Reach for it when you need a shape no component offers. ## When to use - Custom glass: lenses, blobs, a now-playing pill, a panel of your own design. - Members of a [GlassGroup](https://g1455.plugfox.dev/foundations/groups.md) or `GlassUnion`, which fuse surfaces into one silhouette. - Not when a component exists. [GlassBar](https://g1455.plugfox.dev/components/bar.md), [GlassCard](https://g1455.plugfox.dev/components/card.md) and [GlassButton](https://g1455.plugfox.dev/components/button.md) also choose a legible label colour, which a raw surface does not. ## Usage ```dart const SizedBox( width: 220, height: 120, child: GlassSurface( borderRadius: BorderRadius.all(Radius.circular(28)), child: Center(child: Text('Glass', style: TextStyle(color: Color(0xFFFFFFFF)))), ), ) ``` A surface is a plain box: give it a size with a `SizedBox`, a `Positioned` with a width and height, or a child that has one. `kGlassCapsule` makes a pill or a circle at any size. ## Appearing and leaving Two values, from 0 to 1, and they do different things: - `materialize` is how far the **material** has arrived: the bend and the blur first, the tint last, over the whole shape. That is Apple's materialize transition, and the one to animate when a panel appears or leaves. At 0 nothing is drawn or captured. - `presence` is how much of the **shape** exists. Inside a [GlassGroup](https://g1455.plugfox.dev/foundations/groups.md) a member growing from 0 buds out of its neighbours. On a lone panel it narrows the shape to a line, which is rarely what you want. ## Labels `labelled` (true by default) says text sits on this glass. When the host declares `minLabelContrast`, labelled glass may be dimmed to keep that text readable. Set `labelled: false` on glass that carries no text, such as lenses, drops and blobs, so it keeps its finish exactly as named. Inside a raw surface, set the text colour yourself; see [Legibility & theme](https://g1455.plugfox.dev/foundations/legibility.md). ## Performance - Each surface is one draw. Keep the count low and prefer one bigger surface to many small ones. - Animating `presence` costs no capture. Animating `materialize` changes the blur, which means a capture every frame while it runs. - Moving glass belongs in a [GlassTravel](https://g1455.plugfox.dev/foundations/travel.md) region so the motion costs no capture. > [!TIP] > Set `debugPaintGlassSurfaces = true` in debug builds to outline every surface in cyan. ## Gotchas - Hit-testing goes to the child only: an empty surface is not tappable unless it has a ripple. - Glass sitting beside other glass does not show it. To float a surface above other glass, wrap it in [GlassAbove](https://g1455.plugfox.dev/foundations/above.md). ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A now-playing pill that materializes in, next to a magnifying lens. /// Assumes a GlassHost above, e.g. in MaterialApp.builder. class NowPlaying extends StatefulWidget { const NowPlaying({super.key}); @override State createState() => _NowPlayingState(); } class _NowPlayingState extends State with SingleTickerProviderStateMixin { late final AnimationController _in = AnimationController( vsync: this, duration: const Duration(milliseconds: 400), )..forward(); @override void dispose() { _in.dispose(); super.dispose(); } @override Widget build(BuildContext context) => Row( mainAxisSize: MainAxisSize.min, children: [ AnimatedBuilder( animation: _in, builder: (BuildContext context, Widget? child) => GlassSurface( borderRadius: kGlassCapsule, // Blur and bend arrive first, the tint last. materialize: Curves.easeOut.transform(_in.value), child: child, ), child: const Padding( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.music_note, color: Colors.white), SizedBox(width: 8), Text('Now playing', style: TextStyle(color: Colors.white)), ], ), ), ), const SizedBox(width: 16), // A text-free lens: `labelled: false` keeps the clear finish undimmed. SizedBox( width: 72, height: 72, child: GlassSurface( borderRadius: kGlassCapsule, finish: GlassFinish.clear.copyWith(optics: const GlassOptics(zoom: 1.4)), labelled: false, ), ), ], ); } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `borderRadius` | `BorderRadius` | `BorderRadius.all(Radius.circular(24))` | Corner radii. `kGlassCapsule` gives a pill or a circle at any size. | | `finish` | `GlassFinish?` | `null` | The material for this surface. Null takes the theme's. | | `materialize` | `double` | `1` | 0 to 1: how far the material has arrived, blur and bend first, tint last. At 0 nothing is drawn or captured. | | `presence` | `double` | `1` | 0 to 1: how much of the shape exists. In a group a member buds from its neighbours; alone it narrows to a line. | | `labelled` | `bool` | `true` | Text sits on this glass, so it may be dimmed to meet `minLabelContrast`. `false` for lenses and drops. | | `fade` | `GlassFade?` | `null` | Fades the glass out across the surface, scroll-edge style. Ignored for members of a fusing group. | | `ripple` | `GlassRipple?` | `null` | A touch wave for this surface. Null takes the theme's, which is none by default. | | `child` | `Widget?` | `null` | Drawn on top of the glass. It receives the hits. | ### GlassFade | Constructor | Description | |---|---| | `GlassFade({required Offset begin, required Offset end})` | Whole at `begin`, gone at `end`, in local logical pixels, with a smoothstep between. | | `GlassFade.vertical({required double from, required double extent})` | The vertical form. A negative `extent` fades upward. | ### Constants | Name | Value | Description | |---|---|---| | `kGlassCapsule` | `BorderRadius.all(Radius.circular(1e9))` | An over-large radius the engine clamps to half the short side: a stadium at any size. | | `debugPaintGlassSurfaces` | `false` | A top-level variable. In debug builds, outlines every surface in cyan. | --- # Finishes > The material of the glass: regular dark and light, clear and frosted, calibrated against Apple's iOS 26 materials. - Live: https://g1455.plugfox.dev/foundations/finishes - API: [`GlassFinish`](https://pub.dev/documentation/g1455/latest/g1455/GlassFinish-class.html), [`GlassOptics`](https://pub.dev/documentation/g1455/latest/g1455/GlassOptics-class.html), [`kCalibratedRim`](https://pub.dev/documentation/g1455/latest/g1455/kCalibratedRim-constant.html) - Source: [`lib/src/surface/glass_finish.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_finish.dart) A `GlassFinish` is the material the glass is made of: how much it blurs, the tint it lays over the refracted backdrop, the rim along its edge, and the shape of the refraction (`GlassOptics`). The presets are calibrated against Apple's own materials on iOS 26. A finish can be set for the whole app (`GlassHost.finish`), for a subtree (a [GlassTheme](https://g1455.plugfox.dev/foundations/legibility.md) below the host), or for one surface (`finish:` on any component or `GlassSurface`). ## The presets | Preset | Blur σ | Tint | What it is | |---|---|---|---| | `GlassFinish.regularDark` | 2.6 | `rgba(29, 29, 32, 0.693)` | Apple's `.regular` over dark content. | | `GlassFinish.regularLight` | 2.6 | `rgba(252, 252, 252, 0.718)` | Apple's `.regular` over light content. | | `GlassFinish.clear` | 0 | `rgba(249, 249, 249, 0.22)` | No blur and very transparent: the clearest glass, and the hardest to read text on. | | `GlassFinish.frosted` | 8 | `rgba(249, 249, 249, 0.22)` | A heavy blur and a light tint, closer to the older iOS blur material. | Apple's `.regular` is two materials, dark over dark content and light over light. `GlassFinish.regular(appearance:, backdrop:)` picks the branch the way Apple does, and it is what the host uses when you name no finish. ## When to use - **Regular** (the default) for bars, cards, buttons and anything carrying text. - **Clear** for lenses, drops and media overlays, where the content should show through. Pair it with `minLabelContrast` when text sits on it; see [Legibility & theme](https://g1455.plugfox.dev/foundations/legibility.md). - **Frosted** when the content behind should be suggested rather than seen. - Don't invent a new `name`. The name keys the package's measured quality and cost tables, and an unknown name falls back to a full-resolution capture, which is slower. ## Usage ```dart // A brand tint that keeps the measured name, so the tables still apply. final GlassFinish brand = GlassFinish.regularDark.copyWith( tint: const Color.fromRGBO(20, 30, 60, 0.6), ); GlassCard(finish: GlassFinish.clear, child: Text('Clear')) ``` ## Tint and optics - **Tint**: the alpha is how opaque the glass is, the colour is its hue. To tint glass for a brand, `copyWith` a preset's tint and keep its alpha, as the demo does. - **Rim**: added along a 0.79 px outline (`kCalibratedRim` by default). It is also the press highlight of [GlassButton](https://g1455.plugfox.dev/components/button.md). - **Optics**: `GlassOptics(thickness:, strength:, edgePower:, shoulder:, widen:, zoom:)` shapes the bend. `strength` is the peak bend at the rim in pixels, negative being inward (Apple's direction); `zoom` magnifies about the centre, which is how a lens is made. `GlassOptics.none` turns the bend off. > [!NOTE] > The demo sets the tint and the backdrop for its own panels only. The menu at the top of the site sets the finish and > tint of the whole site through the host. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// The four presets side by side, plus a brand-tinted one. /// Assumes a GlassHost above, e.g. in MaterialApp.builder. class FinishSwatches extends StatelessWidget { const FinishSwatches({super.key}); /// Indigo glass that keeps regularDark's name, alpha and optics. static final GlassFinish brand = GlassFinish.regularDark.copyWith( tint: const Color(0xFF28348C).withValues(alpha: GlassFinish.regularDark.tint.a), optics: const GlassOptics(strength: -40), ); static final List<(String, GlassFinish)> finishes = <(String, GlassFinish)>[ ('Regular dark', GlassFinish.regularDark), ('Regular light', GlassFinish.regularLight), ('Clear', GlassFinish.clear), ('Frosted', GlassFinish.frosted), ('Brand', brand), ]; @override Widget build(BuildContext context) => Wrap( spacing: 12, runSpacing: 12, children: [ for (final (String name, GlassFinish finish) in finishes) SizedBox( width: 140, height: 96, // GlassCard picks black or white text for each finish. child: GlassCard( finish: finish, child: Align(alignment: Alignment.bottomLeft, child: Text(name)), ), ), ], ); } /// Apple's two-branch `.regular`, chosen from the appearance and the page colour. Widget regularHost(BuildContext context, Widget page) => GlassHost( finish: GlassFinish.regular( appearance: MediaQuery.platformBrightnessOf(context), backdrop: const Color(0xFFF2F2F7), ), backdrop: const Color(0xFFF2F2F7), child: page, ); ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `name` | `String` | **required** | Key into the measured quality and cost tables. Use a preset's name. | | `blurSigmaLogical` | `double` | **required** | The blur, in logical pixels. | | `tint` | `Color` | **required** | Laid over the refracted backdrop. Its alpha is how opaque the glass is. | | `rim` | `Color` | `kCalibratedRim` | Added along the 0.79 px outline; also the button press highlight. | | `optics` | `GlassOptics` | `GlassOptics()` | The shape of the refraction. | ### GlassOptics | Parameter | Type | Default | Description | |---|---|---|---| | `thickness` | `double` | `21` | How far in from the rim the refraction reaches, in pixels. | | `strength` | `double` | `-58.2` | Peak bend at the rim, in pixels. Negative is inward; closer to 0 is subtler. | | `edgePower` | `double` | `1.9` | Falloff exponent. | | `shoulder` | `double` | `0.6` | Falloff shape exponent. | | `widen` | `double` | `0` | Shows backdrop from this many pixels beyond the box, which minifies. | | `zoom` | `double` | `1` | Magnification about the centre. Must be greater than 0. | ### Presets and methods | Name | Description | |---|---| | `GlassFinish.regularDark` | Blur 2.6, tint `rgba(29, 29, 32, 0.693)`. Apple's `.regular` over dark content. | | `GlassFinish.regularLight` | Blur 2.6, tint `rgba(252, 252, 252, 0.718)`. Apple's `.regular` over light content. | | `GlassFinish.clear` | Blur 0, tint `rgba(249, 249, 249, 0.22)`. | | `GlassFinish.frosted` | Blur 8, tint `rgba(249, 249, 249, 0.22)`. | | `GlassFinish.identity` | Invisible: no blur, no tint, no rim, no bend. For tests. | | `GlassFinish.regular({required Brightness appearance, Color? backdrop})` | Picks `regularDark` or `regularLight` the way Apple does. | | `copyWith({name, blurSigmaLogical, tint, rim, optics})` | A variation that keeps everything you don't name. | | `GlassOptics.none` | No bend at all. | --- # Legibility & theme > How glass components choose black or white labels, keep them readable, and how a subtree gets its own look. - Live: https://g1455.plugfox.dev/foundations/legibility - API: [`GlassTheme`](https://pub.dev/documentation/g1455/latest/g1455/GlassTheme-class.html), [`GlassThemeData`](https://pub.dev/documentation/g1455/latest/g1455/GlassThemeData-class.html), [`GlassLegibility`](https://pub.dev/documentation/g1455/latest/g1455/GlassLegibility-class.html), [`kTextContrastAA`](https://pub.dev/documentation/g1455/latest/g1455/kTextContrastAA-constant.html) - Source: [`lib/src/surface/glass_theme.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_theme.dart) Text on glass sits over whatever the glass sits over, so its colour cannot be fixed in advance. The components ([GlassBar](https://g1455.plugfox.dev/components/bar.md), [GlassButton](https://g1455.plugfox.dev/components/button.md), [GlassCard](https://g1455.plugfox.dev/components/card.md), the [text field](https://g1455.plugfox.dev/components/text-field.md), the [alert](https://g1455.plugfox.dev/components/alert.md), the [menu](https://g1455.plugfox.dev/components/menu.md) and the [popover](https://g1455.plugfox.dev/components/popover.md)) choose black or white for their labels, from the finish and from what you told the host is behind the glass. All of that lives in a `GlassTheme`: the host installs one, and you can nest another to give a subtree its own finish, tier or backdrop. ## When to use - Declare the backdrop on the host, always: `backdrop:` for a flat colour, `richBackdrop: true` for images and feeds. - Add `minLabelContrast: kTextContrastAA` when text sits on clear glass or over bright content. - Nest a `GlassTheme` below the host when one part of a screen differs: a light panel, a cheaper list. - Read `GlassTheme.of(context).legibility()` when you put your own text on a raw `GlassSurface`. - Don't put a `GlassTheme` above the host: the host installs its own and overrides it. ## Usage ```dart // A panel over a light page, below the app's host. GlassTheme( data: GlassTheme.of(context).copyWith( backdrop: const Color(0xFFF2F2F7), finish: GlassFinish.regularLight, ), child: const GlassCard(child: Text('Black text, chosen for you')), ) ``` ## How the label is chosen - **A flat backdrop** (`backdrop:` declared, `richBackdrop` false): the label is whichever of black or white stands out more against the glass laid over that colour. Exact, because every pixel under the glass is that colour. - **A rich backdrop**, or **none declared**: the label is whichever has the better *worst* case over any backdrop. In debug, the package warns once when that worst case cannot reach WCAG AA. - **A contrast floor** (`minLabelContrast:`): when neither colour reaches it, the glass is dimmed by the least amount that does. Dimming is a change of tint, so it costs nothing to draw. `regularDark` needs no dim for AA over any backdrop; `clear` does. Glass with `labelled: false` has no label to protect and is never dimmed. The demo shows it: drag the backdrop from dark to light and watch the label flip and the contrast change, then switch the floor on with clear glass. ## Custom glass A raw `GlassSurface` does not colour its child. Ask the theme: ```dart final GlassLegibility look = GlassTheme.of(context).legibility(GlassFinish.clear); Text('Now playing', style: TextStyle(color: look.label)); ``` `look.finish` is the finish actually drawn (dimmed, if a floor asked for it), and `look.rim` the opaque outline under increased contrast, or null. ## Glass that reads its backdrop Everything above goes by what you declared: one `backdrop` for the whole screen. Over photographs, where one bar sits on a bright sky and a button on a dark shadow, let each glass read what is under it instead with `GlassHost(adaptive: GlassAdaptive())`. See [Adaptive glass](https://g1455.plugfox.dev/foundations/adaptive.md). > [!NOTE] > `GlassThemeData.copyWith` cannot set a nullable field back to null. To drop `minLabelContrast` or `backdrop` for a > subtree, build a new `GlassThemeData`. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A light settings panel inside a dark app, and custom glass that reads /// its label colour from the theme. Assumes a GlassHost above. class LightPanel extends StatelessWidget { const LightPanel({super.key}); static const Color page = Color(0xFFF2F2F7); @override Widget build(BuildContext context) { final GlassThemeData outer = GlassTheme.of(context); return ColoredBox( color: page, child: GlassTheme( // Inherit the host's tier and contrast; only the backdrop and finish differ. data: outer.copyWith( backdrop: page, richBackdrop: false, finish: GlassFinish.regularLight, minLabelContrast: kTextContrastAA, ), child: Builder( builder: (BuildContext context) { final GlassLegibility look = GlassTheme.of(context).legibility(GlassFinish.clear); return Column( mainAxisSize: MainAxisSize.min, children: [ // A component picks its own label colour. const GlassCard(child: Text('Notifications')), const SizedBox(height: 16), // A raw surface: take the colour from the theme. SizedBox( width: 200, height: 56, child: GlassSurface( borderRadius: kGlassCapsule, finish: GlassFinish.clear, child: Center( child: Text('Clear glass', style: TextStyle(color: look.label)), ), ), ), ], ); }, ), ), ); } } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `finish` | `GlassFinish` | `GlassFinish.regularDark` | The material every surface below wears unless it names its own. | | `tier` | `GlassTierChoice` | `GlassTierChoice.byDefault` | Which rung is drawn: full, cheap or opaque. | | `backdrop` | `Color?` | `null` | The average colour behind the glass. Chooses the label over a flat page; fills the opaque tier. | | `highContrast` | `bool` | `false` | Draws an opaque outline instead of the calibrated rim. | | `richBackdrop` | `bool` | `false` | The backdrop is an image or feed: labels are chosen for the worst case. | | `minLabelContrast` | `double?` | `null` | The least label contrast. The glass is dimmed just enough to meet it. | | `ripple` | `GlassRipple?` | `null` | The default touch wave. | | `dropMotion` | `GlassDropMotion` | `GlassDropMotion()` | How held drops stretch and squash. See [Drop motion](https://g1455.plugfox.dev/foundations/drop-motion.md). | | `adaptive` | `GlassAdaptive?` | `null` | Whether glass reads its backdrop. Installed by `GlassHost.adaptive`. See [Adaptive glass](https://g1455.plugfox.dev/foundations/adaptive.md). | | `regularAppearance` | `Brightness?` | `null` | The appearance `finish` was picked in when it is `.regular` and nobody named it. Set by an adaptive host. | | `reading` | `GlassBackdropReading?` | `null` | What the glass this theme was installed for read of its backdrop. | ### GlassTheme | Member | Description | |---|---| | `GlassTheme({required GlassThemeData data, required Widget child})` | Gives a subtree its own glass configuration. Must be below the host. | | `GlassTheme.of(context)` | The theme in force, or the defaults with the platform's `.regular` branch. | | `GlassTheme.maybeOf(context)` | The theme in force, or null. | | `GlassThemeData.legibility([GlassFinish? own, bool labelled = true])` | The finish to draw, the label colour and the outline, for this theme. | ### GlassLegibility | Field | Type | Description | |---|---|---| | `finish` | `GlassFinish` | The finish to draw: the declared one, or it dimmed to meet the floor. | | `label` | `Color` | Black or white, whichever reads best. | | `rim` | `Color?` | The opaque outline under increased contrast, or null. | ### Constants | Name | Value | Description | |---|---|---| | `kTextContrastAA` | `4.5` | WCAG AA for body text. | | `kNonTextContrast` | `3` | WCAG's floor for non-text elements. | --- # Adaptive glass > Glass that reads its own backdrop: a bar over a bright sky turns light, a button over a shadow stays dark, each with a label to match. Off by default and free when off. - Live: https://g1455.plugfox.dev/foundations/adaptive - API: [`GlassAdaptive`](https://pub.dev/documentation/g1455/latest/g1455/GlassAdaptive-class.html), [`GlassBackdropReading`](https://pub.dev/documentation/g1455/latest/g1455/GlassBackdropReading-class.html), [`GlassHost`](https://pub.dev/documentation/g1455/latest/g1455/GlassHost-class.html), [`GlassThemeData`](https://pub.dev/documentation/g1455/latest/g1455/GlassThemeData-class.html) - Source: [`lib/src/surface/glass_adaptive.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_adaptive.dart) Apple's `.regular` is two materials, dark over dark content and light over light. Without help the package picks one branch for the whole screen, from what you declared: the host's `backdrop` and the platform's appearance. A screen is not one level, though. A bar over a photograph's sky and a button over its shadow sit on different branches of Apple's own material. `GlassHost(adaptive: GlassAdaptive())` lets each glass look. The host already holds the pixels under every surface, so it reads back the mean level inside each one's box, and a [GlassBar](https://g1455.plugfox.dev/components/bar.md), a [GlassCard](https://g1455.plugfox.dev/components/card.md) or a [GlassButton](https://g1455.plugfox.dev/components/button.md) picks the branch of `.regular` and its label colour from it. ## When to use - Glass over photographs, maps or video, where one declared level is wrong for half the screen. - Bars and buttons that stay put while the content under them changes from light to dark, such as a full-bleed hero image scrolling under a bar. - **Not** over a flat page: declare its colour as the host's `backdrop` instead, which is exact and reads nothing. ## Usage ```dart MaterialApp( builder: (BuildContext context, Widget? child) => GlassHost( adaptive: const GlassAdaptive(), child: child!, ), home: const PhotoPage(), ) ``` That's all. The components under the host follow what is under them. ## What a reading changes - **The branch of `.regular`.** Only when nobody named a finish. A finish named by the component, by an inner `GlassTheme` or by the host (`GlassHost(finish: GlassFinish.regularDark)`) is kept. Only its label follows the reading. - **The label.** The reading stands in for the declared `backdrop` for that one glass: the label colour, the high-contrast outline and the `minLabelContrast` dim are all chosen against it. - **Not under `richBackdrop: true`.** A mean says nothing about the brightest corner of a photograph, so there the label stays chosen against every backdrop, and the reading moves only the branch. Until a glass has its first reading (its first couple of frames, or a [tier](https://g1455.plugfox.dev/foundations/tiers.md) that captures nothing), it wears what the declarations give, exactly as with adaptive off. ## Behaviour - **No flicker.** A reading moves a glass only when it is more than `band` (12) code values from the one it last moved on, and not within `hold` (600 ms) of its last move. A list of light and dark rows scrolling under a bar keeps the bar on the branch it has. - **Moves animate.** A glass crossing between branches tweens its tint over `duration` (300 ms), and on every frame of it the label is the one that reads on the glass as drawn, so the floor holds halfway too. Under reduced motion it is a cut. - **What adapts.** `GlassBar`, `GlassCard` and `GlassButton`. A raw `GlassSurface`, the [scroll edge](https://g1455.plugfox.dev/foundations/scroll-edge.md), the [tab bar](https://g1455.plugfox.dev/components/tab-bar.md) and the [segmented control](https://g1455.plugfox.dev/components/segmented-control.md) do not adapt yet. The demo above turns adaptive on in the site's own host while the page is open, so the site's top bar and side panel follow it too. The site declares a rich backdrop, so the stage nests a `GlassTheme` with `richBackdrop: false` to let the labels follow as well. It needs the full tier (the High or Ultra setting), the Regular material and the Neutral tint: a lower tier captures nothing to read, and a named material is kept — and a tint names one, the material with that colour. ## Content on the glass Inside a component that reads its backdrop, `GlassTheme.of(context).reading` is what it read, so an icon that is not a label, or a custom painter, can follow it: ```dart final GlassBackdropReading? reading = GlassTheme.of(context).reading; final bool overLight = reading?.brightness == Brightness.light; ``` It is null with adaptive off and before the first reading. `level` is the mean's luma in code values (0 to 255), `luminance` its WCAG relative luminance, and `brightness` whether black or white stands out more against it. The branch the glass is on is `GlassTheme.of(context).finish`. A custom component does what the built-in ones do with `GlassThemeData.adaptedTo(reading)`. To turn adaptive off for a subtree, nest `GlassTheme(data: GlassTheme.of(context).withAdaptive(null), ...)`. ## Cost - **Off: nothing.** No reader exists, nothing is recorded, read back or scheduled. - **On, a still screen: nothing** after the first reading. A frame that keeps its capture has nothing new under the glass and reads nothing. - **On, a frame that captures:** at most one read-back of a 4 × 4-pixel cell per surface, asynchronously, and at most once per `interval`. The frame that asked doesn't wait for it. - **On the web it is dearer.** CanvasKit reads back synchronously, a GPU flush on the frame it lands in, so `interval` is a second there rather than 250 ms. Raise it further for a screen that scrolls a lot. > [!TIP] > The finish table of [Finishes](https://g1455.plugfox.dev/foundations/finishes.md) and the label rules of > [Legibility & theme](https://g1455.plugfox.dev/foundations/legibility.md) still apply: adaptive only gives each glass its own `backdrop`. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; void main() => runApp(const PhotoApp()); class PhotoApp extends StatelessWidget { const PhotoApp({super.key}); @override Widget build(BuildContext context) => MaterialApp( // One host, reading the backdrop under each glass. No finish named, so // each bar and button may take the branch of `.regular` it reads. builder: (BuildContext context, Widget? child) => GlassHost(adaptive: const GlassAdaptive(), child: child!), home: const PhotoPage(), ); } class PhotoPage extends StatelessWidget { const PhotoPage({super.key}); @override Widget build(BuildContext context) { final EdgeInsets safe = MediaQuery.paddingOf(context); return Scaffold( body: Stack( children: [ // Bright at the top, dark at the bottom: a sky and its shadow. const Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Color(0xFFEAF2FA), Color(0xFFB9D3EA), Color(0xFF1B2A1F), Color(0xFF0A110C)], stops: [0, 0.5, 0.52, 1], ), ), ), ), // Over the sky: turns light, with a dark label. Positioned( top: safe.top + 8, left: 16, right: 16, child: const GlassBar(child: Text('Lake Tekapo')), ), // Over the shadow: stays dark, with a white label. Positioned( bottom: safe.bottom + 16, left: 16, child: GlassButton(onPressed: () {}, child: const Text('Directions')), ), // Content that follows the reading itself. Positioned( bottom: safe.bottom + 16, right: 16, child: GlassButton( onPressed: () {}, semanticLabel: 'Weather', child: Builder( builder: (BuildContext context) { final GlassBackdropReading? reading = GlassTheme.of(context).reading; return Icon(reading?.brightness == Brightness.light ? Icons.wb_sunny : Icons.nightlight_round); }, ), ), ), ], ), ); } } ``` ## API `GlassHost`: | Parameter | Type | Default | Description | |---|---|---|---| | `adaptive` | `GlassAdaptive?` | `null` | Turns reading on, and says how. Null: glass goes by what is declared, and nothing is read. | `GlassAdaptive`: | Parameter | Type | Default | Description | |---|---|---|---| | `band` | `double` | `GlassAdaptive.kDefaultBand` (12) | How far, in code values of luma, a reading must be from the last one to move the glass. Must be ≥ 0. | | `hold` | `Duration` | `GlassAdaptive.kDefaultHold` (600 ms) | The least time between two moves of one glass. | | `interval` | `Duration` | `GlassAdaptive.kDefaultInterval` (250 ms; 1 s on the web) | The least time between two read-backs. | | `duration` | `Duration` | `GlassAdaptive.kDefaultDuration` (300 ms) | How long a glass takes to cross between branches. None under reduced motion. | ### GlassBackdropReading | Member | Type | Description | |---|---|---| | `mean` | `Color` | The mean colour of the captured backdrop under the glass, opaque. | | `level` | `double` | The mean's luma in code values, 0 to 255: the scale `.regular` switches on. | | `luminance` | `double` | The mean's WCAG relative luminance. | | `brightness` | `Brightness` | Light when black stands out more against the mean, dark when white does. Not the branch of the glass. | ### GlassThemeData | Member | Description | |---|---| | `adaptive` | The host's `GlassAdaptive`, or null. Installed by `GlassHost.adaptive`. | | `reading` | What the glass this theme was installed for read, or null. Read it as `GlassTheme.of(context).reading`. | | `adaptedTo(GlassBackdropReading reading)` | This theme as it applies over one glass that read `reading`: what a custom component installs around its content. | | `withAdaptive(GlassAdaptive? adaptive)` | This theme with `adaptive` replaced, null included: turns reading off for a subtree. | --- # Tiers & fallbacks > Full glass, a cheap translucent fill, or opaque: the rung for reduce transparency, low-end devices and tests. - Live: https://g1455.plugfox.dev/foundations/tiers - API: [`GlassTier`](https://pub.dev/documentation/g1455/latest/g1455/GlassTier.html), [`GlassTierPolicy`](https://pub.dev/documentation/g1455/latest/g1455/GlassTierPolicy-class.html), [`GlassTierChoice`](https://pub.dev/documentation/g1455/latest/g1455/GlassTierChoice-class.html), [`GlassTierReason`](https://pub.dev/documentation/g1455/latest/g1455/GlassTierReason.html) - Source: [`lib/src/surface/glass_tier.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_tier.dart) Glass comes in three rungs. The package never switches between them on its own: you decide, and `GlassTierPolicy` turns your signals into a choice. | Tier | What is drawn | Captures | |---|---|---| | `GlassTier.full` | Real glass: refraction, blur, tint and rim. | Yes | | `GlassTier.cheap` | The same shape and rim, with the tint laid straight over what is behind. No blur, no refraction. | No | | `GlassTier.opaque` | A solid fill matching the glass's average look over the declared backdrop. | No | ## When to use - **Opaque** for the operating system's Reduce Transparency setting. That is what the setting asks for. - **Cheap** as a ceiling on low-end devices, or for a long list of cards below full-glass bars. - **Pinned** tiers for tests, screenshots and benchmarks. - Don't expect auto-detection. Flutter doesn't expose Reduce Transparency, so read it natively. ## Usage ```dart GlassHost( backdrop: const Color(0xFF101014), // the opaque tier fills with this tier: GlassTierPolicy( reduceTransparency: reduceTransparency, // read natively by the app ceiling: lowEndDevice ? GlassTier.cheap : null, ).choose(), child: child, ) ``` `choose()` resolves in this order: `pinned`, then `reduceTransparency` (opaque), then `ceiling`, then full. The `GlassTierChoice` it returns carries the tier and a `GlassTierReason` for reporting. ## A tier for a subtree The host's tier applies to the whole screen. To run one part of it on another rung, nest a `GlassTheme` below the host. The demo above does exactly that around its stage: ```dart GlassTheme( data: GlassTheme.of(context).copyWith( tier: const GlassTierPolicy(ceiling: GlassTier.cheap).choose(), ), child: cardList, // cheap cards under full-glass bars ) ``` ## Behaviour - Below the full tier nothing is captured, so the cheap and opaque rungs cost no capture at all. - Below full, [groups](https://g1455.plugfox.dev/foundations/groups.md) stop fusing (no bridges between members) and [ripples](https://g1455.plugfox.dev/foundations/ripple.md) are off. - The cheap and opaque rungs draw even without a host above, as flat panels. > [!WARNING] > With `opaque`, declare `backdrop` on the host or the theme. Without it the fill is the tint alone, which is wrong, > and you get a debug error when the rung is painted. > [!WARNING] > `pinned` overrides everything, the user's Reduce Transparency setting included. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// The app's tier from what the app knows about the device and the user. class TieredApp extends StatelessWidget { const TieredApp({ super.key, required this.reduceTransparency, required this.lowEndDevice, required this.home, }); final bool reduceTransparency; // read natively: Flutter does not pass it on final bool lowEndDevice; // your own device table or benchmark final Widget home; @override Widget build(BuildContext context) => MaterialApp( builder: (BuildContext context, Widget? child) => GlassHost( backdrop: const Color(0xFF101014), // needed by the opaque tier tier: GlassTierPolicy( reduceTransparency: reduceTransparency, ceiling: lowEndDevice ? GlassTier.cheap : null, ).choose(), child: child!, ), home: home, ); } /// Full-glass bar over a list of cards held at the cheap tier. class CheapCards extends StatelessWidget { const CheapCards({super.key}); @override Widget build(BuildContext context) => Stack( children: [ GlassTheme( data: GlassTheme.of(context).copyWith( tier: const GlassTierChoice(GlassTier.cheap, GlassTierReason.deviceCeiling), ), child: ListView( padding: const EdgeInsets.fromLTRB(16, 88, 16, 16), children: [ for (var i = 0; i < 20; i++) Padding( padding: const EdgeInsets.only(bottom: 12), child: GlassCard(child: Text('Card $i')), ), ], ), ), const Positioned( top: 24, left: 16, right: 16, child: GlassAbove(child: GlassBar(child: Text('Inbox'))), ), ], ); } ``` ## API ### GlassTierPolicy | Parameter | Type | Default | Description | |---|---|---|---| | `pinned` | `GlassTier?` | `null` | Forces a tier. Overrides everything, including the user's accessibility setting. | | `reduceTransparency` | `bool` | `false` | The OS Reduce Transparency switch, as your app read it. Gives `opaque`. | | `ceiling` | `GlassTier?` | `null` | The richest tier this device should run, e.g. `cheap` on low-end hardware. | `choose()` returns a `GlassTierChoice`, resolving `pinned`, then `reduceTransparency`, then `ceiling`, then full. ### GlassTierChoice | Parameter | Type | Default | Description | |---|---|---|---| | `tier` | `GlassTier` | **required** (positional) | The rung drawn. | | `reason` | `GlassTierReason` | **required** (positional) | Why, for reporting. | `GlassTierChoice.byDefault` is `(GlassTier.full, GlassTierReason.byDefault)`. ### Enums | Name | Values | Description | |---|---|---| | `GlassTier` | `full`, `cheap`, `opaque` | The rungs. `readsBackdrop` is true only for `full`. | | `GlassTierReason` | `byDefault`, `pinnedByHost`, `reduceTransparency`, `deviceCeiling` | Why a rung was chosen. Reporting only. | --- # Ripple > An optional liquid wave when glass is touched, from water to honey. Not an Apple behaviour, off under reduced motion, and it costs no capture. - Live: https://g1455.plugfox.dev/foundations/ripple - API: [`GlassRipple`](https://pub.dev/documentation/g1455/latest/g1455/GlassRipple-class.html), [`kMaxRippleWaves`](https://pub.dev/documentation/g1455/latest/g1455/kMaxRippleWaves-constant.html) - Source: [`lib/src/surface/glass_ripple.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_ripple.dart) `GlassRipple` makes glass answer a touch like a liquid: a dimple forms under the finger, a ring travels outward from it, and the dimple springs back when you let go. One knob, `viscosity`, runs from water (`0`, thin rings that overshoot) to honey (`1`, one slow, broad bump). This is **not** something Apple's Liquid Glass does. iOS 26 answers a touch with light and a springy scale, and never deforms the material. The ripple is opt-in, and nothing ripples unless you ask for it. ## When to use - Playful or brand moments: a hero panel, an onboarding card, a game UI. - Large, text-free glass where a wave reads well. - **Not** when you want platform fidelity. An app that should feel like a stock iOS 26 app should leave it off. - **Not** on glass that sits over other tappable things: a rippling surface becomes hit-testable over its whole shape, even without a child. ## Usage Declare it once on the host for every surface on the screen, or on one surface: ```dart // Every surface below the host ripples. GlassHost(ripple: const GlassRipple(), child: page); // Just this panel, thick and subtle. const GlassSurface( finish: GlassFinish.clear, labelled: false, ripple: GlassRipple(viscosity: 0.9, amplitude: 8, press: 0.5, light: 0.05), ); ``` A `GlassTheme` below the host can also set `ripple:` for a subtree through [`GlassThemeData`](https://pub.dev/documentation/g1455/latest/g1455/GlassThemeData-class.html). A surface's own `ripple:` wins over the theme's. ## Behaviour - **Viscosity is one mechanism.** A thick liquid loses its rings (the front is a single bump), spreads wider, fades sooner and stops overshooting. A thin one rings. - **Each touch makes two impulses:** one on press and one on release. Up to [`kMaxRippleWaves`](https://pub.dev/documentation/g1455/latest/g1455/kMaxRippleWaves-constant.html) (4) waves run on one surface at once. - **It switches itself off** when the platform asks for reduced motion (`MediaQuery.disableAnimations`), for members of a fusing [group](https://g1455.plugfox.dev/foundations/groups.md), and below the full [tier](https://g1455.plugfox.dev/foundations/tiers.md). - The defaults were chosen by eye. There is no platform reference to measure them against. ## Performance A wave changes how the captured backdrop is sampled, not the backdrop itself, so it **takes no capture and repaints nothing**. The ripple has its own shader, which runs only on frames where a wave is alive. A surface at rest draws exactly as it would without a ripple. > [!TIP] > Glass without text, like the panel in the demo, should set `labelled: false`. Otherwise a clear finish may be dimmed to keep labels legible, and the wave is harder to see. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A hero panel that answers a touch with a wave. Assumes a GlassHost above, /// in MaterialApp.builder. For every surface at once, pass /// `ripple: const GlassRipple()` to the GlassHost instead. class RippleHero extends StatefulWidget { const RippleHero({super.key}); @override State createState() => _RippleHeroState(); } class _RippleHeroState extends State { bool _honey = false; @override Widget build(BuildContext context) => Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 320, height: 200, child: GlassSurface( borderRadius: const BorderRadius.all(Radius.circular(32)), finish: GlassFinish.clear, // No text to read on it, so keep the clear finish exactly as named. labelled: false, ripple: GlassRipple( viscosity: _honey ? 0.95 : 0.15, // 0 is water, 1 is honey amplitude: 8, light: 0.06, ), child: const Center(child: Icon(Icons.waves, size: 40, color: Colors.white)), ), ), const SizedBox(height: 16), GlassButton( onPressed: () => setState(() => _honey = !_honey), child: Text(_honey ? 'Make it water' : 'Make it honey'), ), ], ); } ``` ## API `const GlassRipple({...})`. Every field is optional. | Parameter | Type | Default | Description | |---|---|---|---| | `amplitude` | `double` | `6` | Peak displacement of the wave, in logical px. Must be ≥ 0. | | `speed` | `double` | `360` | How fast the ring travels, in px/s. Must be > 0. | | `width` | `double` | `12` | Half-width of the ring when it is born, in px. Must be > 0. | | `viscosity` | `double` | `0.6` | `0` is water (thin, ringing), `1` is honey (one slow bump). | | `press` | `double` | `0.8` | Depth of the dimple under the finger, as a fraction of `amplitude`. `0` means no dimple. | | `pressRadius` | `double` | `26` | Radius of the dimple, in px. Must be > 0. | | `light` | `double` | `0.08` | Highlight and shadow on the slopes. `0` means refraction only. | Also `copyWith(...)` for every field. Where it goes: `GlassHost.ripple`, `GlassThemeData.ripple` or `GlassSurface.ripple` (all `GlassRipple?`, `null` means none or "inherit"). ### Constants | Name | Value | Description | |---|---|---| | `kMaxRippleWaves` | `4` | The most waves one surface draws at once. | --- # Drop motion > The held drop of the switch, slider, segmented control and tab bar stretches as it sets off, squashes as it stops and springs back round. One spec for the app, or per control. - Live: https://g1455.plugfox.dev/foundations/drop-motion - API: [`GlassDropMotion`](https://pub.dev/documentation/g1455/latest/g1455/GlassDropMotion-class.html), [`GlassDropStretch`](https://pub.dev/documentation/g1455/latest/g1455/GlassDropStretch-class.html), [`GlassDropStretchDriver`](https://pub.dev/documentation/g1455/latest/g1455/GlassDropStretchDriver-class.html) - Source: [`lib/src/surface/glass_drop_motion.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_drop_motion.dart) Four controls lift their selection into a clear glass drop while a finger is on it: the [switch](https://g1455.plugfox.dev/components/switch.md), the [slider](https://g1455.plugfox.dev/components/slider.md), the [segmented control](https://g1455.plugfox.dev/components/segmented-control.md) and the [tab bar](https://g1455.plugfox.dev/components/tab-bar.md). `GlassDropMotion` makes that drop move like a liquid: long and thin as it sets off, short and fat as it stops, then a little wobble back to round. It is on by default. At rest, and gliding at a constant speed, the drop keeps its shape: only speeding up and slowing down deform it. ## When to use - Leave the default on for the feel of iOS 26, whose held drops lean into a fast slide and bulge as they stop. - Turn it down, or off with `GlassDropMotion.none`, for a calmer app or a dense, serious UI. - Turn it up for a playful one. It never deforms more than `maxStretch` (at most 0.5). ## Usage For every control in the app, on the host: ```dart GlassHost( dropMotion: const GlassDropMotion(maxStretch: 0.2), child: child!, ) ``` For one control, which wins over the theme's: ```dart GlassTabBar( items: tabs, selectedIndex: tab, onSelected: (int i) => setState(() => tab = i), dropMotion: GlassDropMotion.none, ) ``` A `GlassTheme` below the host can set `dropMotion:` for a subtree through `GlassThemeData`, like the ripple. ## Behaviour - **It follows the acceleration.** The drop is stretched along its travel while it speeds up, in either direction, and squashed while it slows down. The deformation keeps the drop's area: `w × (1 + s)` by `h / (1 + s)`. - **It saturates.** At `saturation` (10 000 px/s²) the stretch reaches three quarters of `maxStretch`, and it eases towards `maxStretch` past it. With the defaults a tab bar's drop springing one tab over is about 7% long setting off and 5% short arriving; three tabs over, about 11% and 12%. The switch's short throw deforms only about 3%. - **It springs back.** The shape follows its target through a spring (`stiffness` 900, `damping` 30, a damping ratio of 0.5): a little jelly, back to round in about half a second. - **Reduced motion turns it off**, whatever was declared. The demo puts all four controls on one spec. Tap a far segment or tab and watch the drop lean into the move. ## Cost - **No capture.** The drop changes shape inside the [travel](https://g1455.plugfox.dev/foundations/travel.md) region it already moves in. Each control grows that region by the most the spec can stretch the drop, so the deformation never leaves it. - **A repaint of the drop's own layer** on the frames it deforms: the same one draw a frame. - **A ticker** while the drop moves or springs back, and none at rest or while a finger holds it still. ## Your own drop `GlassDropStretch` is the model alone, with no widget and no ticker: feed it where your drop is with `step(dt, x)` and draw `GlassDropStretch.apply(size, value)`. `GlassDropStretchDriver` runs one off a ticker, as the package's controls do: call `wake()` when the drop moves, and listen for `value`. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; void main() => runApp(const DropApp()); class DropApp extends StatelessWidget { const DropApp({super.key}); @override Widget build(BuildContext context) => MaterialApp( builder: (BuildContext context, Widget? child) => GlassHost( // Every drop in the app: a little more stretch, a little less wobble. dropMotion: const GlassDropMotion(maxStretch: 0.18, damping: 40), child: child!, ), home: const SettingsPage(), ); } class SettingsPage extends StatefulWidget { const SettingsPage({super.key}); @override State createState() => _SettingsPageState(); } class _SettingsPageState extends State { int _range = 0; double _volume = 0.4; @override Widget build(BuildContext context) => Scaffold( backgroundColor: const Color(0xFF101014), body: Center( child: SizedBox( width: 320, child: GlassCard( child: Column( mainAxisSize: MainAxisSize.min, children: [ // Takes the host's motion. GlassSegmentedControl( segments: const [Text('Day'), Text('Week'), Text('Month')], selectedIndex: _range, onSelected: (int i) => setState(() => _range = i), ), const SizedBox(height: 16), // Names its own: this drop keeps its shape. GlassSlider( value: _volume, dropMotion: GlassDropMotion.none, onChanged: (double v) => setState(() => _volume = v), ), ], ), ), ), ), ); } ``` ## API `const GlassDropMotion({...})`. Every field is optional. | Parameter | Type | Default | Description | |---|---|---|---| | `maxStretch` | `double` | `0.12` | The most the drop stretches or squashes: 0.12 is up to 12% longer launching and 12% shorter braking. From 0 to 0.5; 0 is `none`. | | `saturation` | `double` | `10000` | The acceleration, in px/s², that takes the drop to three quarters of `maxStretch`. Must be > 0. | | `smoothing` | `Duration` | `Duration(milliseconds: 16)` | The time constant of the low-pass on the velocity and the acceleration. | | `stiffness` | `double` | `900` | The spring the shape follows its target with, at unit mass. Must be > 0. | | `damping` | `double` | `30` | The spring's damping. With 900, a damping ratio of 0.5. Must be ≥ 0. | | Member | Description | |---|---| | `GlassDropMotion.none` | No deformation: the drop keeps the shape it is held at. | | `isNone` | Whether this deforms nothing. | | `GlassDropMotion.resolve(context, declared)` | What a control uses: its own, else the theme's, and `none` under reduced motion. | | `copyWith(...)` | Every field. | Where it goes: `GlassHost.dropMotion` and `GlassThemeData.dropMotion` (`GlassDropMotion`, default `GlassDropMotion()`), and `dropMotion` on `GlassSwitch`, `GlassSlider`, `GlassSegmentedControl` and `GlassTabBar` (`GlassDropMotion?`, `null` takes the theme's). ### GlassDropStretch | Member | Description | |---|---| | `GlassDropStretch([GlassDropMotion motion = const GlassDropMotion()])` | The model of one drop. `motion` may be replaced. | | `double step(double dt, double x)` | Advances `dt` seconds to a drop at `x` px along its travel; returns `value`. | | `void jump(double x)` | The drop is at `x` without having travelled there. | | `void reset()` | Round, still, and the next `step` is a first sample. | | `value` | The deformation, from `-maxStretch` (squashed) to `maxStretch` (stretched). | | `isSettled` | Round, still and heading nowhere. | | `static Size apply(Size size, double stretch)` | `size` deformed along x, the area kept. | ### GlassDropStretchDriver | Member | Description | |---|---| | `GlassDropStretchDriver({required TickerProvider vsync, required double Function() position})` | Runs a `GlassDropStretch` off a ticker. A `ChangeNotifier`: notifies when `value` changes. | | `motion` | The spec. Set it from `GlassDropMotion.resolve` in `didChangeDependencies`. | | `void wake()` | The drop moved: sample it every frame until it settles. | | `void jump()` | The drop is where it is without having travelled there. | | `value` | The deformation to draw. | --- # Groups & unions > Draw several glass surfaces as one piece of liquid glass: GlassGroup fuses them when they come close, GlassUnion keeps them joined at any distance. - Live: https://g1455.plugfox.dev/foundations/groups - API: [`GlassGroup`](https://pub.dev/documentation/g1455/latest/g1455/GlassGroup-class.html), [`GlassUnion`](https://pub.dev/documentation/g1455/latest/g1455/GlassUnion-class.html), [`kMaxFusedShapes`](https://pub.dev/documentation/g1455/latest/g1455/kMaxFusedShapes-constant.html), [`unionBlendRadius()`](https://pub.dev/documentation/g1455/latest/g1455/unionBlendRadius.html), [`GlassBlendGroup`](https://pub.dev/documentation/g1455/latest/g1455/GlassBlendGroup-class.html), [`GlassGroupScope`](https://pub.dev/documentation/g1455/latest/g1455/GlassGroupScope-class.html) - Source: [`lib/src/surface/glass_group.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_group.dart) A `GlassGroup` draws every `GlassSurface` inside it as **one piece of glass**, like SwiftUI's `GlassEffectContainer`. Members closer than `spacing` grow a smooth bridge and merge into one silhouette, then separate again as they move apart. A member whose `presence` animates up from 0 "buds" out of its neighbours instead of appearing on its own. A `GlassUnion` is the always-joined variant, like SwiftUI's `glassEffectUnion`. Its members form **one connected piece however far apart they are**. The blend radius is solved so that everyone just connects, so the further apart the members, the puffier the whole silhouette. ## When to use - **GlassGroup:** the merging-blob look, or controls that should visibly melt together as they approach (a button that buds out of a bar, a drop that leaves its track). - **GlassUnion:** separated controls that must read as one glass object, such as a split pill or a cluster of buttons. - **Not** as a performance trick. A group costs *more* than the same surfaces drawn separately, and `spacing: 0` shares a draw but saves nothing. - **Not** around your page background. Everything inside the group paints on top of the glass. ## Usage ```dart // Two circles that fuse when they are within 16 px of each other. GlassGroup( spacing: 16, labelled: false, child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox(width: 56, height: 56, child: GlassSurface(borderRadius: kGlassCapsule)), SizedBox(width: 8), SizedBox(width: 56, height: 56, child: GlassSurface(borderRadius: kGlassCapsule)), ], ), ); ``` Swap `GlassGroup` for `GlassUnion` (it has no `spacing`) and the two stay joined however far apart you put them. ## Behaviour - **One finish per group.** The group's `finish` (or the host's) is used for every member. A member's own `finish` is ignored, with a debug warning once. - **At most [`kMaxFusedShapes`](https://pub.dev/documentation/g1455/latest/g1455/kMaxFusedShapes-constant.html) (12) members fuse.** A bigger group stops fusing, and its members draw separately without bridges. - **The nearest group wins.** A union nested inside a group takes its members out of the group. - **Below the full [tier](https://g1455.plugfox.dev/foundations/tiers.md), nothing fuses**: the bridges disappear and members draw as separate shapes. - `fade` and `ripple` are not drawn on fused members. - Raw `GlassSurface` members don't pick a label colour for you. Set text colours yourself, or put components such as `GlassButton` inside. ## Moving members Blobs that orbit or follow a finger move over still content, so wrap them in a [`GlassTravel`](https://g1455.plugfox.dev/foundations/travel.md) and a `RepaintBoundary`, like the demo above. The motion then costs no capture. ## Plumbing `GlassBlendGroup` is the membership object behind a group or union, and `GlassGroupScope` is the inherited widget that hands it to the surfaces below. You don't build these yourself, but `GlassGroupScope.maybeOf(context) != null` tells a widget whether it is inside a group. `unionBlendRadius(boxes, radii)` returns the blend radius a union would use for those boxes. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// Blobs that fuse, one that buds, and a split pill. Assumes a GlassHost above. class FusingControls extends StatefulWidget { const FusingControls({super.key}); @override State createState() => _FusingControlsState(); } class _FusingControlsState extends State { bool _third = false; Widget _blob(double size, {double presence = 1}) => SizedBox( width: size, height: size, child: GlassSurface(borderRadius: kGlassCapsule, presence: presence, labelled: false), ); @override Widget build(BuildContext context) => Column( mainAxisSize: MainAxisSize.min, children: [ // Members closer than `spacing` grow a bridge and fuse. GlassGroup( spacing: 20, finish: GlassFinish.clear, labelled: false, child: Row( mainAxisSize: MainAxisSize.min, children: [ _blob(64), const SizedBox(width: 12), _blob(64), const SizedBox(width: 12), // Animating presence from 0 makes it bud out of its neighbours. TweenAnimationBuilder( tween: Tween(end: _third ? 1 : 0), duration: const Duration(milliseconds: 400), builder: (BuildContext context, double p, Widget? _) => _blob(48, presence: p), ), ], ), ), const SizedBox(height: 24), // A union is always one piece, however far apart its members are. SizedBox( width: 280, child: GlassUnion( child: Row( children: [ SizedBox( width: 48, height: 48, child: GlassSurface( borderRadius: kGlassCapsule, child: IconButton( onPressed: () => setState(() => _third = !_third), icon: const Icon(Icons.add, color: Colors.white), ), ), ), const Spacer(), const SizedBox( width: 160, height: 48, child: GlassSurface( borderRadius: kGlassCapsule, child: Center( child: Text('Now playing', style: TextStyle(color: Colors.white)), ), ), ), ], ), ), ), ], ); } ``` ## API `GlassGroup`: | Parameter | Type | Default | Description | |---|---|---|---| | `child` | `Widget` | **required** | The subtree that contains the member surfaces. | | `spacing` | `double` | `0` | Edge-to-edge gap, in px, at which members fuse. `0` keeps shapes separate but shares one draw. | | `finish` | `GlassFinish?` | `null` (the host's) | The finish of the whole group. Members' own finishes are ignored. | | `labelled` | `bool` | `true` | Whether text sits on the glass. Set `false` for text-free blobs so the finish isn't dimmed. | `GlassUnion`: | Parameter | Type | Default | Description | |---|---|---|---| | `child` | `Widget` | **required** | The subtree that contains the member surfaces. | | `finish` | `GlassFinish?` | `null` (the host's) | The finish of the whole union. | | `labelled` | `bool` | `true` | As `GlassGroup.labelled`. | Related: `double unionBlendRadius(List boxes, List radii)` returns the blend radius a union would use. `GlassGroupScope({required GlassBlendGroup group, required Widget child})` with `static GlassBlendGroup? maybeOf(BuildContext context)`. ### Constants | Name | Value | Description | |---|---|---| | `kMaxFusedShapes` | `12` | The most members a group or union fuses. Past it, members draw separately. | --- # Travel > Declare the region moving glass travels in, so a dragged lens or a sliding knob is redrawn from the capture the host already holds instead of triggering a new one. - Live: https://g1455.plugfox.dev/foundations/travel - API: [`GlassTravel`](https://pub.dev/documentation/g1455/latest/g1455/GlassTravel-class.html), [`GlassTravelScope`](https://pub.dev/documentation/g1455/latest/g1455/GlassTravelScope-class.html), [`GlassTravelRegion`](https://pub.dev/documentation/g1455/latest/g1455/GlassTravelRegion-class.html) - Source: [`lib/src/surface/glass_travel.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_travel.dart) `GlassTravel` is a performance hint: "glass inside this box may move anywhere within it." The host then captures the **whole box** once, and glass that moves inside it is redrawn from that capture instead of triggering a new one on every frame. Without it, glass that moves over still content still costs a capture per frame, because each surface's slot in the capture is its own box plus a margin, and any move leaves it. The package can't see where a surface is *going*. `GlassTravel` is how you tell it. The built-in [switch](https://g1455.plugfox.dev/components/switch.md), [slider](https://g1455.plugfox.dev/components/slider.md), [segmented control](https://g1455.plugfox.dev/components/segmented-control.md) and [tab bar](https://g1455.plugfox.dev/components/tab-bar.md) already use it for their drops. ## When to use - A custom control or effect whose glass moves while the content under it stays still: a draggable lens, a custom knob, orbiting blobs. - **Not** for glass that sits still. The capture becomes bigger for no benefit. - **Not** as a cure for glass over content that is itself changing (a scrolling list, a video). When the content under the glass changes, the host re-captures regardless. ## Usage ```dart SizedBox( width: 300, height: 60, child: GlassTravel( child: Stack( children: [ Positioned(left: x, top: 0, width: 60, height: 60, child: const GlassSurface(borderRadius: kGlassCapsule)), ], ), ), ); ``` `GlassTravel` is transparent to layout, paint and hit-testing. It only marks a region. ## Making motion free The motion is free only if **moving the glass repaints nothing else**. A repaint anywhere under the glass looks like changed content and triggers a capture. So: 1. Put the content the glass moves over behind its own `RepaintBoundary`. 2. Put the moving glass behind another `RepaintBoundary`, inside the `GlassTravel`, with a parent that paints nothing of its own. The example in the Code tab follows that layout. Turn the switch in the demo off and the lens looks exactly the same, but every frame of the drag now re-captures. > [!NOTE] > The declaration is a price, never a correctness claim. Glass that leaves the region is simply captured the normal way: correct, just not free. ## Plumbing `GlassTravelScope` is the inherited widget that carries the region down to the surfaces, and `GlassTravelRegion` is the region itself. `GlassTravelScope.maybeOf(context)?.globalRect` gives its current rectangle in global logical pixels. You don't need either to use `GlassTravel`. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A magnifier you drag over a photo. Inside GlassTravel the drag costs no /// capture: the lens is redrawn from the capture the host already holds. /// Assumes a GlassHost above. class Magnifier extends StatefulWidget { const Magnifier({super.key, required this.photo}); final ImageProvider photo; @override State createState() => _MagnifierState(); } class _MagnifierState extends State { static const double _lens = 96; Offset _at = const Offset(120, 120); @override Widget build(BuildContext context) => Stack( children: [ // The content, behind its own boundary: the drag repaints none of it. Positioned.fill( child: RepaintBoundary( child: Image(image: widget.photo, fit: BoxFit.cover), ), ), // The region the lens may move in: the whole photo. Positioned.fill( child: GlassTravel( // The moving glass, behind a boundary of its own. child: RepaintBoundary( child: Stack( children: [ Positioned( left: _at.dx - _lens / 2, top: _at.dy - _lens / 2, width: _lens, height: _lens, child: GestureDetector( onPanUpdate: (DragUpdateDetails d) => setState(() => _at += d.delta), child: GlassSurface( borderRadius: kGlassCapsule, labelled: false, finish: GlassFinish.clear.copyWith(optics: const GlassOptics(zoom: 1.5)), child: const SizedBox.expand(), ), ), ), ], ), ), ), ), ], ); } ``` ## API `GlassTravel`: | Parameter | Type | Default | Description | |---|---|---|---| | `child` | `Widget` | **required** | The region. Glass that moves goes inside it. | `GlassTravelScope` (plumbing): | Parameter | Type | Default | Description | |---|---|---|---| | `region` | `GlassTravelRegion` | **required** | The region the surfaces below may move within. | | `child` | `Widget` | **required** | The subtree. | `static GlassTravelRegion? maybeOf(BuildContext context)` finds the nearest region. `GlassTravelRegion.globalRect` (`Rect?`) is where it is now, or `null` when it can't say. --- # Glass on glass > Glass inside other glass refracts it automatically. A bar floating over sibling glass, such as glass cards, needs GlassAbove to show them. - Live: https://g1455.plugfox.dev/foundations/above - API: [`GlassAbove`](https://pub.dev/documentation/g1455/latest/g1455/GlassAbove-class.html), [`kGlassModalLift`](https://pub.dev/documentation/g1455/latest/g1455/kGlassModalLift-constant.html) - Source: [`lib/src/surface/glass_above.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_above.dart) Glass can stand on other glass in two ways, and the package treats them differently. - **Nested:** glass written *inside* other glass refracts it automatically. A `GlassButton` in a `GlassBar`, or the drop of a tab bar, shows the bar under it. Nothing to declare. - **Siblings:** glass *beside* other glass in the tree doesn't see it. A bar floating over a list of `GlassCard`s is the cards' sibling, so on its own it shows the page with **the cards cut out**. Wrap the bar in `GlassAbove` and it refracts them. The demo shows exactly that. Switch `GlassAbove` off and watch the bar as cards scroll under it. ## When to use - Bars, tab bars, floating buttons and custom overlays over a page that has glass of its own. - **Already done for you** by [`GlassScrollEdge`](https://g1455.plugfox.dev/foundations/scroll-edge.md) (and the bar in its `child`), [dialogs](https://g1455.plugfox.dev/components/alert.md), [sheets](https://g1455.plugfox.dev/components/sheet.md), [menus](https://g1455.plugfox.dev/components/menu.md) and [popovers](https://g1455.plugfox.dev/components/popover.md). - **Not needed** over plain content. It does no harm there and costs nothing. ## Usage ```dart Stack( children: [ Positioned.fill(child: cardList), // a list of GlassCards const Positioned( top: 0, left: 16, right: 16, child: SafeArea( child: GlassAbove(child: GlassBar(child: Text('Inbox'))), ), ), ], ); ``` ## Levels Every glass surface sits on a *level*: the number of glass surfaces it is written inside, plus the lifts above it. A level's capture draws all the glass of lower levels, which is how the bar gets to see the cards. - `lift: 1` (the default) raises a bar above the page's glass. - [`kGlassModalLift`](https://pub.dev/documentation/g1455/latest/g1455/kGlassModalLift-constant.html) (`2`) is for a modal-like layer that must also stand above bars that are themselves lifted. The package's own modals use it. ## Performance Each occupied extra level costs **one more capture** on frames that re-capture. A lifted bar over plain content stays on level 0 and costs nothing extra; the levels that count are the ones with glass under them. ## Gotchas - **Levels are a declaration, not paint order.** Glass *beside* a lifted subtree but painted on top of it still appears in its capture. A bar left unlifted next to a lifted scroll edge shows up blurred inside the edge, so lift such bars together (put the bar in `GlassScrollEdge.child`). - Each lift is another capture level, so don't lift what has no glass under it "just in case" deep in a list. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// Glass cards scrolling under a glass bar. Assumes a GlassHost above. class InboxPage extends StatelessWidget { const InboxPage({super.key, required this.subjects}); final List subjects; @override Widget build(BuildContext context) { final EdgeInsets safe = MediaQuery.paddingOf(context); return Stack( children: [ Positioned.fill( child: ListView.builder( padding: EdgeInsets.fromLTRB(16, safe.top + 80, 16, safe.bottom + 16), itemCount: subjects.length, itemBuilder: (BuildContext context, int i) => Padding( padding: const EdgeInsets.only(bottom: 12), child: GlassCard(child: Text(subjects[i])), ), ), ), Positioned( top: safe.top + 8, left: 16, right: 16, // The bar is a sibling of the cards, not their child: without // GlassAbove it would show the page with the cards cut out. child: GlassAbove( child: GlassBar( child: Row( children: [ const Expanded( child: Text('Inbox', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600)), ), // A button inside the bar is glass *on* the bar already: // it refracts the bar with no GlassAbove of its own. GlassButton( onPressed: () {}, semanticLabel: 'Compose', padding: EdgeInsets.zero, child: const Icon(Icons.edit), ), ], ), ), ), ), ], ); } } ``` ## API `GlassAbove`: | Parameter | Type | Default | Description | |---|---|---|---| | `lift` | `int` | `1` | How many levels the glass below is raised. Must be > 0. | | `child` | `Widget?` | `null` | The glass that stands on its neighbours. | `GlassAbove` has no layout or paint of its own. It is a marker the host reads. ### Constants | Name | Value | Description | |---|---|---| | `kGlassModalLift` | `2` | The lift of a modal layer (menu, dialog, sheet): above the page's glass and above bars lifted over it. | --- # Capture control > Tell the capture what a subtree is: paint a stand-in for a video or a platform view, leave a subtree out, mark an opaque cover, or keep a blur the shadow filter would drop. - Live: https://g1455.plugfox.dev/foundations/capture - API: [`GlassProxy`](https://pub.dev/documentation/g1455/latest/g1455/GlassProxy-class.html), [`GlassProxyRole`](https://pub.dev/documentation/g1455/latest/g1455/GlassProxyRole.html), [`GlassProxyPainter`](https://pub.dev/documentation/g1455/latest/g1455/GlassProxyPainter-class.html), [`GradientProxyPainter`](https://pub.dev/documentation/g1455/latest/g1455/GradientProxyPainter-class.html), [`SolidProxyPainter`](https://pub.dev/documentation/g1455/latest/g1455/SolidProxyPainter-class.html), [`RenderGlassProxy`](https://pub.dev/documentation/g1455/latest/g1455/RenderGlassProxy-class.html) - Source: [`lib/src/proxy/proxy_role.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/proxy/proxy_role.dart) The host captures what is under its glass by painting that part of the tree a second time, into a picture of its own (see [How it works](https://g1455.plugfox.dev/start/how-it-works.md)). Most of the time that is exactly right, and there is nothing to declare. `GlassProxy` is for the subtrees where it is not: it tells the capture what a subtree is, and changes nothing in the frame the user sees. ## Four declarations | Constructor | The capture… | For | |---|---|---| | `GlassProxy.replace(painter:)` | paints the painter's stand-in instead of the subtree | a video, a camera preview, a map or any platform view: they record nothing, and the glass would show a hole | | `GlassProxy.hidden()` | leaves the subtree out, and does not run its `paint` | a subtree the glass should not show, or one whose `paint` has side effects (counters, analytics, lazy loading) that should not run twice a frame | | `GlassProxy.opaque()` | takes the subtree as covering its own box | a full-bleed image or panel: the capture stops looking under it | | `GlassProxy.verbatim()` | exempts the subtree from the shadow filter | a highlight drawn through a `MaskFilter` on purpose, which the filter would drop as a shadow | The outermost declaration wins: a `replace` inside a `hidden` is never reached. ## A stand-in A stand-in is a `GlassProxyPainter`, shaped like a `CustomPainter`. Two come with the package: - `SolidProxyPainter(color)`: one flat colour, the cheapest stand-in there is. - `GradientProxyPainter(gradient)`: a gradient, which keeps the colour across the box where one colour would not. Write your own for anything else: the last frame of the video as an image, the map's tiles at a lower zoom, the camera's average colour. The canvas is clipped to the subtree's box before `paint` runs, so a stand-in cannot spill onto glass elsewhere on the screen. ```dart class PosterProxyPainter extends GlassProxyPainter { const PosterProxyPainter(this.poster); final ui.Image poster; @override void paint(Canvas canvas, Size size) => paintImage( canvas: canvas, rect: Offset.zero & size, image: poster, fit: BoxFit.cover, ); @override bool shouldRepaint(PosterProxyPainter old) => old.poster != poster; } ``` `shouldRepaint` tells the host the stand-in itself changed, and the next frame captures it. `isOpaque` (false by default) says the stand-in covers its whole box, which makes it a cover as `opaque` does. A rounded stand-in is not opaque: its corners are where the page shows through. > [!NOTE] > A stand-in changes what the glass sees, not when the host captures. The host still watches the composited layers > under its glass, and a video that composites a new frame is a change it captures for. What `replace` buys is a > picture where there would have been a hole. ## What it does not do `GlassProxy` does not simplify content to save time: the capture already runs at a fraction of the screen's resolution, and that is a cheaper and better-looking saving than swapping text for blocks of colour. Declare what the capture cannot know, not what it can. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A video player under a glass control bar. The player is a platform view, /// which a capture cannot read: without a stand-in, the bar would refract a /// hole. With one, it refracts the poster's colours. class PlayerScreen extends StatelessWidget { const PlayerScreen({super.key, required this.player}); /// The video: a platform view, a `Texture`, anything that paints outside /// Flutter's own pictures. final Widget player; @override Widget build(BuildContext context) => Stack( fit: StackFit.expand, children: [ GlassProxy.replace( painter: const GradientProxyPainter( LinearGradient(colors: [Color(0xFF1B1F2A), Color(0xFF3A2E5C), Color(0xFFFF9F0A)]), ), child: player, ), // An overlay that counts its own paints: the capture should not run it a // second time every frame. const Positioned(top: 16, right: 16, child: GlassProxy.hidden(child: _ViewerCount())), Positioned( left: 16, right: 16, bottom: 16, child: GlassBar( child: Row( children: [ IconButton(onPressed: () {}, icon: const Icon(Icons.pause)), const Expanded(child: Text('Live')), IconButton(onPressed: () {}, icon: const Icon(Icons.fullscreen)), ], ), ), ), ], ); } class _ViewerCount extends StatelessWidget { const _ViewerCount(); @override Widget build(BuildContext context) => const Text('1,204 watching'); } ``` ## API `GlassProxy`: | Constructor | Parameters | Role | |---|---|---| | `GlassProxy.replace` | `painter` (`GlassProxyPainter`, **required**), `child` (**required**) | `GlassProxyRole.replace` | | `GlassProxy.hidden` | `child` (**required**) | `GlassProxyRole.hidden` | | `GlassProxy.opaque` | `child` (**required**) | `GlassProxyRole.opaque` | | `GlassProxy.verbatim` | `child` (**required**) | `GlassProxyRole.verbatim` | `GlassProxyPainter` (abstract): | Member | Type | Description | |---|---|---| | `paint(Canvas canvas, Size size)` | `void` | Paints the stand-in, in the subtree's local space, clipped to `size`. | | `shouldRepaint(covariant GlassProxyPainter old)` | `bool` | Whether the stand-in changed and has to be captured again. | | `isOpaque` | `bool` | Whether `paint` covers the whole box. `false` by default. | `SolidProxyPainter(Color color)` and `GradientProxyPainter(Gradient gradient)` are the two the package ships. The real frame never changes: every declaration is a `RenderGlassProxy`, a proxy box that paints its child as usual. --- # Scroll edge > iOS 26's scroll edge effect: content softly blurs and fades as it scrolls under a bar, or meets an opaque band. It also holds and lifts the bar. - Live: https://g1455.plugfox.dev/foundations/scroll-edge - API: [`GlassScrollEdge`](https://pub.dev/documentation/g1455/latest/g1455/GlassScrollEdge-class.html), [`GlassScrollEdgeStyle`](https://pub.dev/documentation/g1455/latest/g1455/GlassScrollEdgeStyle.html), [`GlassScrollEdgeSide`](https://pub.dev/documentation/g1455/latest/g1455/GlassScrollEdgeSide.html), [`GlassScrollEdgeAppearance`](https://pub.dev/documentation/g1455/latest/g1455/GlassScrollEdgeAppearance.html), [`kGlassScrollEdgeSigma`](https://pub.dev/documentation/g1455/latest/g1455/kGlassScrollEdgeSigma-constant.html), [`kGlassScrollEdgeHardSigma`](https://pub.dev/documentation/g1455/latest/g1455/kGlassScrollEdgeHardSigma-constant.html), [`kGlassScrollEdgeLightTint`](https://pub.dev/documentation/g1455/latest/g1455/kGlassScrollEdgeLightTint-constant.html), [`kGlassScrollEdgeDarkTint`](https://pub.dev/documentation/g1455/latest/g1455/kGlassScrollEdgeDarkTint-constant.html), [`kGlassScrollEdgeHardFill`](https://pub.dev/documentation/g1455/latest/g1455/kGlassScrollEdgeHardFill-constant.html) - Source: [`lib/src/surface/glass_scroll_edge.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_scroll_edge.dart) `GlassScrollEdge` draws what iOS 26 draws where a list scrolls under a bar. In the **soft** style (iOS's default), content blurs slightly and fades into a tint as it goes under the bar. In the **hard** style (macOS's default), it meets an opaque white band with a sharp edge. The values were read off Apple's own effect on iOS 26 simulators. It also **holds your bar**: pass the bar as `child` and it is laid out inside `extent` and lifted together with the effect, so it shows glass scrolling under it (see [Glass on glass](https://g1455.plugfox.dev/foundations/above.md)). Touches outside the bar pass through to the list. ## When to use - A list that scrolls under a top app bar, or under a bottom toolbar or [tab bar](https://g1455.plugfox.dev/components/tab-bar.md). - **Not** without a scrolling list beneath. Over a still page, a plain [`GlassAbove`](https://g1455.plugfox.dev/foundations/above.md) around the bar is enough. > [!TIP] > For a whole screen, a top bar over a list with an optional tab bar, [`GlassScaffold`](https://g1455.plugfox.dev/components/scaffold.md) puts the bar in a soft scroll edge, works out `extent`, and tells the list its padding. Reach for `GlassScrollEdge` itself for anything else. ## Usage Place it full-width against its edge, over the list: ```dart Stack( children: [ Positioned.fill(child: list), Positioned( top: 0, left: 0, right: 0, child: GlassScrollEdge( side: GlassScrollEdgeSide.top, extent: MediaQuery.paddingOf(context).top + 60, child: appBar, ), ), ], ); ``` `extent` is the distance from the screen edge to the bar's inner edge: the status bar plus the bar at the top, the bar plus the home indicator at the bottom. Pad the list by the same amount so its first and last rows can be seen. ## Styles and sides | | Top | Bottom | |---|---|---| | `soft` | A light blur (σ [`kGlassScrollEdgeSigma`](https://pub.dev/documentation/g1455/latest/g1455/kGlassScrollEdgeSigma-constant.html) = 1.6) plus a tint that fades in toward the edge. This is a glass surface. | A tint only, no blur, as Apple draws it. A gradient, nothing captured. | | `hard` | A near-opaque white band (`kGlassScrollEdgeHardFill`, 90%) with a sharp edge. | The same band at the bottom. | The effect reaches **past** `extent` into the content, by about 40 px at the soft top. That is the fade. ## Appearance A soft edge tints toward white over light content and toward black over dark content (`kGlassScrollEdgeLightTint`, `kGlassScrollEdgeDarkTint`). Apple picks the direction from the content itself. The package can't read the content, so with `appearance: null` it reads the host's declared `backdrop`: dark below a relative luminance of 0.4, light otherwise, **and light when no backdrop is declared**. Over a dark app, declare `backdrop` on the [host](https://g1455.plugfox.dev/foundations/host.md) or pass `appearance: GlassScrollEdgeAppearance.dark`. ## Performance - The soft top is one glass surface the width of the screen. Under a scrolling list, what's under it changes every frame, so **it re-captures on every scrolling frame** and costs nothing once the list stops. - The soft bottom is a gradient and costs no capture. - Being lifted costs one extra capture level, but only when there is glass under it. > [!WARNING] > Put the bar **in** `child`. An unlifted bar placed beside a lifted edge appears blurred inside the edge's capture. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A list under a top app bar and a bottom toolbar, each in a scroll edge. /// Assumes a GlassHost above. class LibraryPage extends StatelessWidget { const LibraryPage({super.key, required this.titles}); final List titles; @override Widget build(BuildContext context) { final EdgeInsets safe = MediaQuery.paddingOf(context); final double top = safe.top + 64; // status bar + bar final double bottom = safe.bottom + 72; // toolbar + home indicator return Stack( children: [ Positioned.fill( child: ListView.builder( padding: EdgeInsets.only(top: top, bottom: bottom), itemCount: titles.length, itemBuilder: (BuildContext context, int i) => ListTile(title: Text(titles[i])), ), ), Positioned( top: 0, left: 0, right: 0, child: GlassScrollEdge( side: GlassScrollEdgeSide.top, extent: top, // The bar goes in `child`, so it is lifted with the edge. child: Padding( padding: EdgeInsets.fromLTRB(16, safe.top + 8, 16, 4), child: const GlassBar(child: Center(child: Text('Library'))), ), ), ), Positioned( left: 0, right: 0, bottom: 0, child: GlassScrollEdge( side: GlassScrollEdgeSide.bottom, extent: bottom, style: GlassScrollEdgeStyle.hard, // macOS-style opaque band child: Padding( padding: EdgeInsets.fromLTRB(16, 8, 16, safe.bottom + 8), child: Align( alignment: Alignment.topRight, child: GlassButtonGroup( items: [ GlassToolbarItem(icon: const Icon(Icons.add), label: 'Add', onPressed: () {}), GlassToolbarItem(icon: const Icon(Icons.ios_share), label: 'Share', onPressed: () {}), ], ), ), ), ), ), ], ); } } ``` ## API `GlassScrollEdge`: | Parameter | Type | Default | Description | |---|---|---|---| | `side` | `GlassScrollEdgeSide` | **required** | `top` or `bottom`. | | `extent` | `double` | **required** | From the screen edge to the bar's inner edge, in logical px. Must be ≥ 0. | | `style` | `GlassScrollEdgeStyle` | `GlassScrollEdgeStyle.soft` | `soft` (blur and tint, iOS) or `hard` (opaque band, macOS). | | `appearance` | `GlassScrollEdgeAppearance?` | `null` (dark if the theme's `backdrop` luminance is < 0.4, otherwise light) | Which way a soft edge tints: `light` (white) or `dark` (black). | | `blurSigma` | `double` | `kGlassScrollEdgeSigma` (1.6) | The soft edge's blur. | | `child` | `Widget?` | `null` | The bar, laid out within `extent` and lifted with the effect. | ### Constants | Name | Value | Description | |---|---|---| | `kGlassScrollEdgeSigma` | `1.6` | The soft edge's blur, in logical px. | | `kGlassScrollEdgeHardSigma` | `2.15` | The hard band's blur. | | `kGlassScrollEdgeLightTint` | `Color.fromRGBO(255, 255, 255, 0.85)` | The soft tint at the edge, light appearance. | | `kGlassScrollEdgeDarkTint` | `Color.fromRGBO(0, 0, 0, 0.25)` | The soft tint at the edge, dark appearance. | | `kGlassScrollEdgeHardFill` | `Color.fromRGBO(255, 255, 255, 0.90)` | The hard band. | --- # Performance > What glass costs and how to keep it cheap: surface count, button groups, travel regions, thermal state, and the ledger that reports the glass on screen. - Live: https://g1455.plugfox.dev/foundations/performance - API: [`GlassLedger`](https://pub.dev/documentation/g1455/latest/g1455/GlassLedger-class.html), [`GlassScope`](https://pub.dev/documentation/g1455/latest/g1455/GlassScope-class.html), [`GlassLoad`](https://pub.dev/documentation/g1455/latest/g1455/GlassLoad-class.html), [`GlassLoadVerdict`](https://pub.dev/documentation/g1455/latest/g1455/GlassLoadVerdict.html), [`GlassHardware`](https://pub.dev/documentation/g1455/latest/g1455/GlassHardware.html), [`GlassThermalState`](https://pub.dev/documentation/g1455/latest/g1455/GlassThermalState.html), [`GlassThermalPolicy`](https://pub.dev/documentation/g1455/latest/g1455/GlassThermalPolicy-class.html), [`debugPaintGlassSurfaces`](https://pub.dev/documentation/g1455/latest/g1455/debugPaintGlassSurfaces.html) - Source: [`lib/src/surface/glass_ledger.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_ledger.dart) g1455 is built for cost first. Instead of a `BackdropFilter` on every surface, one [`GlassHost`](https://g1455.plugfox.dev/foundations/host.md) records what's under all of its glass into **one** shared, downscaled capture, and **only re-records when something under the glass changed**. A still screen costs no capture at all. See [How it works](https://g1455.plugfox.dev/start/how-it-works.md) for the details. What's left for you is to not spend that budget by accident. ## What costs what - **Each surface is one draw**, and the overhead grows faster than the surface count. A bar with five `GlassButton`s is six surfaces. - **Changing content under glass** (scrolling, video, an animated background) means a capture on that frame. That's expected, and it's what the measured numbers cover (the devices and the dates: [How it works](https://g1455.plugfox.dev/start/how-it-works.md)). - **Moving glass over still content** costs a capture per frame, unless it moves inside a [`GlassTravel`](https://g1455.plugfox.dev/foundations/travel.md). - **Glass on glass** adds a capture level for each occupied level (nested glass, or [`GlassAbove`](https://g1455.plugfox.dev/foundations/above.md)). - **Animating `materialize`** changes the blur, so it captures every frame while it runs. Animating `presence` doesn't. - **A ripple** costs no capture. **Groups** cost more than the same surfaces drawn separately. ## Practical advice - **Keep glass in the navigation and controls layer**, as Apple's guidelines do. Bars, tab bars, toolbars and floating controls, not every card in a feed. - **Prefer [`GlassButtonGroup`](https://g1455.plugfox.dev/components/toolbar.md)** for rows of actions. It's one surface no matter how many items, where N `GlassButton`s are N surfaces. - **Inside a glass bar, plain icons are cheaper** than `GlassButton`s, which are glass on glass. - **Wrap moving glass in `GlassTravel`** with `RepaintBoundary`s around the glass and the content. - **Offer cheaper [tiers](https://g1455.plugfox.dev/foundations/tiers.md)** on low-end devices (`GlassTierPolicy(ceiling: GlassTier.cheap)`), and honour Reduce Transparency. - **Measure in profile mode on a real device.** Debug builds are much slower across the board, so their frame times tell you little about glass. ## Reading the ledger The host keeps a register of every glass surface on the screen, the `GlassLedger`. Reach it with `GlassScope.maybeOf(context)` and call `read()` to get a `GlassLoad`: the surface count, how many of them are captured, the glass area, *screens of glass* (area relative to the screen), and a `verdict` against what was measured on that hardware. ```dart final GlassLedger? ledger = GlassScope.maybeOf(context); final GlassLoad? load = ledger?.read( viewSize: MediaQuery.sizeOf(context), model: GlassHardware.detect().surfaceCostModel, ); // e.g. "6 surfaces, 0.12 screens, withinMeasured" ``` The ledger notifies when surfaces are added or removed, not when they move: geometry is read on demand. For a debug overlay, poll it (the code example uses a timer). The demo above shows the readout for the page you're reading. > [!NOTE] > A verdict of `hardwareUnmeasured` is not a warning. It means no measurement covers this device, which is what `GlassHardware.detect()` returns everywhere except Apple platforms. ## Hardware and thermals - **`GlassHardware`** tells the package whose measurements apply: `appleMetal` (detected on iOS and macOS), `adrenoVulkan` (Adreno 830-class only, declare it yourself), or `unmeasured`. It changes reported costs, never rendering decisions. - **`GlassThermalState`** is the device's thermal pressure, which **your app reads natively** and passes to `GlassHost.thermal`. At `serious` and `critical`, the host may reuse a slightly stale capture for a frame or two while content changes. Blurry finishes tolerate that; `clear` gets no staleness. Tiers are never changed by thermals. - **`GlassThermalPolicy`** sets how much staleness each state may spend. `GlassThermalPolicy.never` keeps every frame fresh. ## Debugging Set the top-level `debugPaintGlassSurfaces = true` (debug builds only, like `debugPaintSizeEnabled`) to outline every registered glass surface. It's the quickest way to spot glass you didn't know was there. ## Complete example ```dart import 'dart:async'; import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A debug overlay that reads what the host's ledger says about the screen. /// Put it anywhere under the GlassHost. class GlassLoadBadge extends StatefulWidget { const GlassLoadBadge({super.key}); @override State createState() => _GlassLoadBadgeState(); } class _GlassLoadBadgeState extends State { late final Timer _poll; @override void initState() { super.initState(); // Geometry is read on demand (the ledger does not notify when glass // moves), so poll it rather than rebuild on every frame. _poll = Timer.periodic(const Duration(seconds: 1), (_) => setState(() {})); } @override void dispose() { _poll.cancel(); super.dispose(); } @override Widget build(BuildContext context) { final GlassLedger? ledger = GlassScope.maybeOf(context); if (ledger == null) { return const SizedBox.shrink(); } final GlassLoad load = ledger.read( viewSize: MediaQuery.sizeOf(context), model: GlassHardware.detect().surfaceCostModel, ); return Text( '${load.surfaceCount} surfaces · ' '${load.screensOfGlass.toStringAsFixed(2)} screens of glass · ' '${load.verdict.name}', ); } } /// The host, told what the package cannot read for itself. Widget performantHost(Widget child, {required GlassThermalState thermal, bool snapdragon = false}) => GlassHost( // Whose measurements apply: changes reported costs, never correctness. hardware: snapdragon ? GlassHardware.adrenoVulkan : GlassHardware.detect(), // Read natively by the app and passed in; under pressure the host may // reuse a slightly stale capture for a frame or two. thermal: thermal, thermalPolicy: const GlassThermalPolicy(), child: child, ); ``` ## API `GlassLedger` (reach it with `GlassScope.maybeOf(context)`; you never construct one): | Member | Type | Description | |---|---|---| | `read({required Size viewSize, required GlassSurfaceCostModel model})` | `GlassLoad` | Reads the register against one platform's measurements. | | `registeredCount` | `int` | How many surfaces are registered. | | `surfaces` | `Iterable` | Every surface that can say where it is, read now. | | `bounds` | `Rect?` | What one capture covering every surface would span. | `GlassLoad` (main fields): | Field | Type | Description | |---|---|---| | `surfaceCount` | `int` | Glass surfaces on the screen. | | `capturedSurfaceCount` | `int` | Of those, how many read a capture (full tier). | | `screensOfGlass` | `double` | Total glass area divided by the screen's area. | | `rectAreaLogical` / `shapeAreaLogical` | `double` | Glass area as boxes and as shapes, in logical px². | | `taxCycles` | `double?` | Estimated GPU cost, where measured (Adreno only). | | `verdict` | `GlassLoadVerdict` | `withinMeasured`, `pastMeasuredRange`, `betweenMeasuredPoints`, `overMeasuredCliff` or `hardwareUnmeasured`. | `GlassHost` parameters that matter here: | Parameter | Type | Default | Description | |---|---|---|---| | `hardware` | `GlassHardware?` | `null` (`GlassHardware.detect()`) | `appleMetal`, `adrenoVulkan` or `unmeasured`. Affects reported costs only. | | `thermal` | `GlassThermalState?` | `null` (nominal) | `nominal`, `fair`, `serious` or `critical`, as read by your app. | | `thermalPolicy` | `GlassThermalPolicy` | `const GlassThermalPolicy()` | How much staleness each thermal state may spend. | `const GlassThermalPolicy({double fairDeltaE = 0, double seriousDeltaE = 0.02 * kMaterialScaleDeltaE, double criticalDeltaE = 0.04 * kMaterialScaleDeltaE})`: the allowed staleness per state, as a colour difference (ΔE): about 0.70 at `serious` and 1.39 at `critical` by default. `GlassThermalPolicy.never` allows none. ### Debug flag | Name | Type | Default | Description | |---|---|---|---| | `debugPaintGlassSurfaces` | `bool` | `false` | Outlines every registered glass surface. Debug builds only. | --- # Bar > GlassBar is a floating glass capsule for navigation: a title bar, a bottom bar or a now-playing pill, with black or white labels picked for legibility. - Live: https://g1455.plugfox.dev/components/bar - API: [`GlassBar`](https://pub.dev/documentation/g1455/latest/g1455/GlassBar-class.html), [`kGlassCapsule`](https://pub.dev/documentation/g1455/latest/g1455/kGlassCapsule-constant.html) - Source: [`lib/src/surface/glass_components.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_components.dart) `GlassBar` is one glass surface with padding around its child. It is the navigation layer of an iOS 26 style screen: the top bar with a back button and a title, a floating bottom bar, a "now playing" pill over a feed. The bar picks the text and icon colour of everything inside it, black or white, whichever reads best on the glass over what is behind it. Its items are ordinary widgets, not glass, so a bar full of icons and text still costs one surface. ## When to use - Navigation chrome that floats over content: titles, back and search actions, a mini player. - A small pill that names the current screen or state. - **Not** for content panels: use a [Card](https://g1455.plugfox.dev/components/card.md) instead (same body, a corner instead of a capsule). - **Not** as a row of glass buttons. Every [`GlassButton`](https://g1455.plugfox.dev/components/button.md) inside a bar is one more surface. Use plain icons, or a [toolbar](https://g1455.plugfox.dev/components/toolbar.md) (`GlassButtonGroup`), which draws many actions as one surface. ## Usage ```dart Stack( children: [ Positioned.fill(child: content), // what the glass refracts const Positioned( top: 16, left: 16, right: 16, child: GlassBar(child: Center(child: Text('Library'))), ), ], ) ``` The bar sizes itself to its child. To stretch it across the screen, give it a width with `Positioned(left:, right:)`, a `SizedBox` or an `Expanded`. ## Layout - `borderRadius` defaults to [`kGlassCapsule`](https://pub.dev/documentation/g1455/latest/g1455/kGlassCapsule-constant.html), a full pill whatever the height. Pass a `BorderRadius` for a rounded rectangle. - `padding` defaults to 16 across and 8 down. Icon buttons usually want less, so their 44 px tap targets reach the edge of the glass. - No `SafeArea` is applied for you. Add `MediaQuery.paddingOf(context)` to your offsets. ## Labels Text and icons inside the bar get the label colour through `DefaultTextStyle` and `IconTheme`. Widgets that hard-code their own colour (for example Material's `IconButton`, which uses the colour scheme) ignore it, so pass `IconTheme.of(context).color` to them, or use plain `Icon`s in a `GestureDetector`. The choice of colour is only as good as what the host knows about the backdrop: see [Legibility](https://g1455.plugfox.dev/foundations/legibility.md). > [!WARNING] > A bar is not refracted by glass that is its sibling. If glass cards scroll under a bar, wrap the bar > in [`GlassAbove`](https://g1455.plugfox.dev/foundations/above.md), or use a [scroll edge](https://g1455.plugfox.dev/foundations/scroll-edge.md) instead. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A library screen: a top bar and a "now playing" bar over a list. /// Assumes a GlassHost above the navigator (MaterialApp.builder). class LibraryScreen extends StatelessWidget { const LibraryScreen({super.key}); @override Widget build(BuildContext context) { final EdgeInsets safe = MediaQuery.paddingOf(context); return Scaffold( body: Stack( children: [ ListView.builder( padding: EdgeInsets.fromLTRB(16, safe.top + 72, 16, safe.bottom + 96), itemCount: 30, itemBuilder: (BuildContext context, int i) => ListTile(leading: const Icon(Icons.album), title: Text('Album ${i + 1}')), ), Positioned( top: safe.top + 8, left: 16, right: 16, child: const GlassBar( padding: EdgeInsets.symmetric(horizontal: 12, vertical: 10), child: Row( children: [ Icon(Icons.arrow_back_ios_new), Expanded(child: Text('Library', textAlign: TextAlign.center)), Icon(Icons.search), ], ), ), ), Positioned( left: 16, right: 16, bottom: safe.bottom + 16, child: const GlassBar( padding: EdgeInsets.fromLTRB(16, 12, 16, 12), child: Row( children: [ Icon(Icons.music_note), SizedBox(width: 12), Expanded(child: Text('Heat Waves', maxLines: 1, overflow: TextOverflow.ellipsis)), Icon(Icons.pause), SizedBox(width: 16), Icon(Icons.fast_forward), ], ), ), ), ], ), ); } } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `child` | `Widget` | **required** | The bar's items. They are content, not glass, and get the bar's label colour. | | `borderRadius` | `BorderRadius` | `kGlassCapsule` | Corner radii. The default is a pill at any height. | | `padding` | `EdgeInsets` | `EdgeInsets.symmetric(horizontal: 16, vertical: 8)` | Space between the glass edge and the items. | | `finish` | `GlassFinish?` | `null` | The material. Null takes the theme's (normally the host's). | | `key` | `Key?` | `null` | | --- # Button > GlassButton is a tappable glass capsule that brightens while held, with a 44 × 44 minimum tap target and dimmed labels when disabled. - Live: https://g1455.plugfox.dev/components/button - API: [`GlassButton`](https://pub.dev/documentation/g1455/latest/g1455/GlassButton-class.html), [`kGlassMinTapTarget`](https://pub.dev/documentation/g1455/latest/g1455/kGlassMinTapTarget-constant.html), [`kGlassDisabledDarkLabel`](https://pub.dev/documentation/g1455/latest/g1455/kGlassDisabledDarkLabel-constant.html), [`kGlassDisabledLightLabel`](https://pub.dev/documentation/g1455/latest/g1455/kGlassDisabledLightLabel-constant.html) - Source: [`lib/src/surface/glass_components.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_components.dart) `GlassButton` is a glass capsule that takes a tap. While a finger is on it, the whole shape brightens by the finish's rim colour, so the material itself changes rather than a highlight being laid on top. The label (text, icon, or both) is centred and gets a legible colour, like a [Bar](https://g1455.plugfox.dev/components/bar.md). The whole capsule is the tap target, and it is never smaller than [`kGlassMinTapTarget`](https://pub.dev/documentation/g1455/latest/g1455/kGlassMinTapTarget-constant.html) (44 × 44, Apple's minimum). ## When to use - A standalone action on glass: a floating "add" button, the choices in a [sheet](https://g1455.plugfox.dev/components/sheet.md), the trigger of a [menu](https://g1455.plugfox.dev/components/menu.md) or a [popover](https://g1455.plugfox.dev/components/popover.md). - A primary action on a [Card](https://g1455.plugfox.dev/components/card.md). - **Not** for a row of actions in a toolbar: each button is a surface of its own. A [toolbar](https://g1455.plugfox.dev/components/toolbar.md) (`GlassButtonGroup`) draws all of them as one. - **Not** for every button of your app. Glass belongs to controls that float over content. ## Usage ```dart GlassButton( onPressed: () {}, child: const Text('Done'), ) ``` An icon-only button has nothing for a screen reader to say, so give it a `semanticLabel`. It replaces the child's semantics. Drop the padding so it stays a circle: ```dart GlassButton( onPressed: close, semanticLabel: 'Close', padding: EdgeInsets.zero, child: const Icon(Icons.close), ) ``` ## Disabled Pass `onPressed: null`. The glass stays exactly as it is and the label dims: to `kGlassDisabledDarkLabel` where the enabled label would be black, and to `kGlassDisabledLightLabel` where it would be white. That follows iOS 26, which also leaves the glass alone and only dims the title. Semantics report the button as disabled. ## Gotchas - Don't wrap a button in `Opacity`, `ColorFilter` or `ImageFilter`. The press highlight is added onto the glass, and those widgets make it add onto transparency instead, which looks wrong. A `RepaintBoundary` is fine. - `pressedOverlay: Color(0x00000000)` turns the highlight off. Any other colour replaces the rim's. - A button inside a bar or a card is glass on glass. It works, but it is one more surface and one more capture level. See [Performance](https://g1455.plugfox.dev/foundations/performance.md). ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A photo viewer's floating actions. /// Assumes a GlassHost above the navigator (MaterialApp.builder). class PhotoActions extends StatefulWidget { const PhotoActions({required this.canShare, super.key}); final bool canShare; @override State createState() => _PhotoActionsState(); } class _PhotoActionsState extends State { bool _liked = false; @override Widget build(BuildContext context) => Row( mainAxisSize: MainAxisSize.min, children: [ GlassButton( // Null disables the button: the glass stays, the label dims. onPressed: widget.canShare ? () {} : null, child: const Row( mainAxisSize: MainAxisSize.min, children: [Icon(Icons.ios_share, size: 18), SizedBox(width: 6), Text('Share')], ), ), const SizedBox(width: 12), GlassButton( onPressed: () => setState(() => _liked = !_liked), // Icon-only: say what it does, and keep it round. semanticLabel: _liked ? 'Unlike' : 'Like', padding: EdgeInsets.zero, child: Icon(_liked ? Icons.favorite : Icons.favorite_border), ), const SizedBox(width: 12), GlassButton( onPressed: () => Navigator.maybePop(context), semanticLabel: 'Close', padding: EdgeInsets.zero, child: const Icon(Icons.close), ), ], ); } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `child` | `Widget` | **required** | The label or icon, centred, in a legible colour. | | `onPressed` | `VoidCallback?` | `null` | Called on a tap. Null disables the button. | | `borderRadius` | `BorderRadius` | `kGlassCapsule` | Corner radii. | | `padding` | `EdgeInsets` | `EdgeInsets.symmetric(horizontal: 20, vertical: 10)` | Space between the glass and the label. | | `minSize` | `Size` | `kGlassMinTapTarget` | The smallest the button may be. | | `pressedOverlay` | `Color?` | `null` | Added over the whole shape while held. Null takes the finish's `rim`; `Color(0x00000000)` disables it. | | `finish` | `GlassFinish?` | `null` | The material. Null takes the theme's. | | `semanticLabel` | `String?` | `null` | What a screen reader says instead of the child. Set it on icon-only buttons. | | `key` | `Key?` | `null` | | ### Constants | Name | Value | Description | |---|---|---| | `kGlassMinTapTarget` | `Size(44, 44)` | Apple's minimum tap target; the default `minSize`. | | `kGlassDisabledDarkLabel` | `Color(0x4D3C3C43)` | Disabled label where the enabled one is black. | | `kGlassDisabledLightLabel` | `Color(0x4DEBEBF5)` | Disabled label where the enabled one is white. | --- # Card > GlassCard is a rounded glass panel for a few floating groups of content, such as widgets on a wallpaper, with a legible label colour for its children. - Live: https://g1455.plugfox.dev/components/card - API: [`GlassCard`](https://pub.dev/documentation/g1455/latest/g1455/GlassCard-class.html) - Source: [`lib/src/surface/glass_components.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_components.dart) `GlassCard` is the same panel as a [Bar](https://g1455.plugfox.dev/components/bar.md) with a 24 px corner instead of a capsule and 16 px of padding all round. Its children get a legible label colour, black or white, picked against the glass over what is behind it. ## When to use - A few floating panels over a wallpaper or a photo: weather and calendar widgets, a now-playing card, a settings group on a lock-screen style page. - A panel that holds controls: [switches](https://g1455.plugfox.dev/components/switch.md), [sliders](https://g1455.plugfox.dev/components/slider.md), a [button](https://g1455.plugfox.dev/components/button.md). - **Not** for every item of a list or a feed. Apple keeps Liquid Glass in the navigation and controls layers, out of the content layer. Each card is one more surface, and the cost grows faster than the number of surfaces. A feed of glass cards is the most expensive thing you can build with this package. If you really need it, check the count with [`GlassLedger`](https://g1455.plugfox.dev/foundations/performance.md). - **Not** over a plain, flat background. Glass over one flat colour looks like a grey box. Use an ordinary `Card` or `Container` there. ## Usage ```dart GlassCard( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Battery', style: TextStyle(fontWeight: FontWeight.w600)), const Text('82% - about 9 hours left'), GlassButton(onPressed: () {}, child: const Text('Details')), ], ), ) ``` A card sizes itself to its child, like a `Container` with padding. Give it a width with a `SizedBox`, `ConstrainedBox` or the layout around it. ## Choosing a finish The card uses the host's finish unless you pass one. Over a busy photo, `GlassFinish.frosted` blurs the most and is easiest to read; `GlassFinish.clear` shows the most of the photo and is the hardest to read on. Try them in the demo above, and see [Finishes](https://g1455.plugfox.dev/foundations/finishes.md). ## Gotchas - A bar or tab bar floating above glass cards does not show those cards unless it is wrapped in [`GlassAbove`](https://g1455.plugfox.dev/foundations/above.md). Controls written *inside* a card refract it automatically. - Secondary text in a card: derive it from `DefaultTextStyle.of(context).style.color` (for example at 70% alpha) rather than a fixed grey, so it follows the label colour the card chose. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A weather widget over a wallpaper. /// Assumes a GlassHost above the navigator (MaterialApp.builder). class WeatherCard extends StatelessWidget { const WeatherCard({super.key}); static const List<(String, IconData, int)> _hours = <(String, IconData, int)>[ ('Now', Icons.wb_sunny, 21), ('14', Icons.wb_sunny, 23), ('15', Icons.wb_cloudy, 24), ('16', Icons.water_drop, 19), ]; @override Widget build(BuildContext context) => SizedBox( width: 320, child: GlassCard( borderRadius: const BorderRadius.all(Radius.circular(28)), padding: const EdgeInsets.all(20), child: Builder( // Under the card, so it reads the label colour the card picked. builder: (BuildContext context) { final Color? ink = DefaultTextStyle.of(context).style.color; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Lisbon', style: TextStyle(fontSize: 17, fontWeight: FontWeight.w600)), const Text('21°', style: TextStyle(fontSize: 48, fontWeight: FontWeight.w300)), Text('Mostly sunny · H:24° L:16°', style: TextStyle(color: ink?.withValues(alpha: 0.7))), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ for (final (String hour, IconData icon, int temp) in _hours) Column(children: [Text(hour), Icon(icon, size: 20), Text('$temp°')]), ], ), ], ); }, ), ), ); } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `child` | `Widget` | **required** | The content. Gets the card's label colour. | | `borderRadius` | `BorderRadius` | `BorderRadius.all(Radius.circular(24))` | Corner radii. | | `padding` | `EdgeInsets` | `EdgeInsets.all(16)` | Space between the glass and the content. | | `finish` | `GlassFinish?` | `null` | The material. Null takes the theme's. | | `key` | `Key?` | `null` | | --- # Switch > GlassSwitch is the iOS 26 on/off switch: a white knob that lifts into a clear glass drop while you press or drag it, and costs nothing extra at rest. - Live: https://g1455.plugfox.dev/components/switch - API: [`GlassSwitch`](https://pub.dev/documentation/g1455/latest/g1455/GlassSwitch-class.html), [`kGlassSwitchSize`](https://pub.dev/documentation/g1455/latest/g1455/kGlassSwitchSize-constant.html), [`kGlassDropScale`](https://pub.dev/documentation/g1455/latest/g1455/kGlassDropScale-constant.html), [`kGlassSwitchDropWiden`](https://pub.dev/documentation/g1455/latest/g1455/kGlassSwitchDropWiden-constant.html), [`kGlassDropOptics`](https://pub.dev/documentation/g1455/latest/g1455/kGlassDropOptics-constant.html), [`kGlassDropDuration`](https://pub.dev/documentation/g1455/latest/g1455/kGlassDropDuration-constant.html), [`kGlassDisabledOpacity`](https://pub.dev/documentation/g1455/latest/g1455/kGlassDisabledOpacity-constant.html) - Source: [`lib/src/surface/glass_controls.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_controls.dart) `GlassSwitch` is an on/off switch drawn the way iOS 26 draws it: a 64 × 28 track and a white knob. When you press the knob, it lifts into a clear glass drop about 1.57 times its size, which bends what is under it. Drag it to the other side, or just tap; let go and the value commits. At rest the knob and the track are ordinary paint, not glass. The drop exists only while the switch is held, so a page with twenty switches costs the glass nothing until someone touches one. ## When to use - Boolean settings: Wi-Fi on or off, notifications, a feature flag in a settings group. - Over imagery or on a [Card](https://g1455.plugfox.dev/components/card.md), where an ordinary switch would look flat. - **Not** for choosing between more than two options: use a [segmented control](https://g1455.plugfox.dev/components/segmented-control.md). - **Not** for an action that happens right away and can't be undone. That is a [button](https://g1455.plugfox.dev/components/button.md). ## Usage The switch is controlled: you hold the value and pass it back in. ```dart bool _wifi = true; GlassSwitch( value: _wifi, onChanged: (bool v) => setState(() => _wifi = v), ) ``` Pass `onChanged: null` to disable it. A disabled switch is drawn at [`kGlassDisabledOpacity`](https://pub.dev/documentation/g1455/latest/g1455/kGlassDisabledOpacity-constant.html) (50%). ## Accessibility The switch reports itself as a toggle with its state. It has no label of its own. Either pass a `semanticLabel`, or put it in a row with a `Text` and wrap the row in `MergeSemantics`, so a screen reader says "Wi-Fi, switch, on" as one item: ```dart MergeSemantics( child: Row( children: [ const Expanded(child: Text('Wi-Fi')), GlassSwitch(value: wifi, onChanged: onWifi), ], ), ) ``` The switch is never smaller than 44 px tall, so it stays easy to hit. ## The drop - `dropScale` (default [`kGlassDropScale`](https://pub.dev/documentation/g1455/latest/g1455/kGlassDropScale-constant.html), 1.57) is how much bigger the held drop is than the knob. It must be at least 1. - `dropWiden` (default `kGlassSwitchDropWiden`, 5 px) makes the drop show a little more of what is around it, which looks slightly zoomed out, as on iOS. 0 turns that off. - The drop's refraction is `kGlassDropOptics`, and it grows in over `kGlassDropDuration` (180 ms). > [!NOTE] > The drop needs a [`GlassHost`](https://g1455.plugfox.dev/foundations/host.md) above it to be glass. Without one the switch still > works, but the held knob is not drawn as glass. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A settings group on a glass card. /// Assumes a GlassHost above the navigator (MaterialApp.builder). class ConnectivitySettings extends StatefulWidget { const ConnectivitySettings({super.key}); @override State createState() => _ConnectivitySettingsState(); } class _ConnectivitySettingsState extends State { bool _wifi = true; bool _bluetooth = false; bool _airplane = false; Widget _row(IconData icon, String label, bool value, ValueChanged? onChanged, {Color? color}) => MergeSemantics( // One item for a screen reader: "Wi-Fi, switch, on". child: SizedBox( height: 52, child: Row( children: [ Icon(icon), const SizedBox(width: 12), Expanded(child: Text(label)), GlassSwitch(value: value, onChanged: onChanged, activeColor: color ?? const Color(0xFF34C759)), ], ), ), ); @override Widget build(BuildContext context) => GlassCard( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Column( mainAxisSize: MainAxisSize.min, children: [ _row( Icons.flight, 'Airplane mode', _airplane, (bool v) => setState(() => _airplane = v), color: const Color(0xFFFF9F0A), ), // While airplane mode is on, the radios can't be changed: onChanged null disables them. _row(Icons.wifi, 'Wi-Fi', _wifi, _airplane ? null : (bool v) => setState(() => _wifi = v)), _row( Icons.bluetooth, 'Bluetooth', _bluetooth, _airplane ? null : (bool v) => setState(() => _bluetooth = v), color: const Color(0xFF0A84FF), ), ], ), ); } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `value` | `bool` | **required** | Whether the switch is on. | | `onChanged` | `ValueChanged?` | **required** | Called with the new value. Null disables the switch (drawn at 50% opacity). | | `activeColor` | `Color` | `Color(0xFF34C759)` | Track colour when on (iOS green). | | `trackColor` | `Color` | `Color(0x29787880)` | Track colour when off. | | `dropScale` | `double` | `kGlassDropScale` | Size of the held drop relative to the knob. At least 1. | | `dropWiden` | `double` | `kGlassSwitchDropWiden` | How many px of the surroundings the drop pulls in (a slight zoom-out). 0 for none. | | `dropMotion` | `GlassDropMotion?` | `null` | How the held drop stretches and squashes as it moves. Null takes the theme's; `GlassDropMotion.none` keeps it round. See [Drop motion](https://g1455.plugfox.dev/foundations/drop-motion.md). | | `semanticLabel` | `String?` | `null` | Screen-reader label. Or wrap the row in `MergeSemantics` with a `Text`. | | `key` | `Key?` | `null` | | ### Constants | Name | Value | Description | |---|---|---| | `kGlassSwitchSize` | `Size(64, 28)` | The track. The widget is at least 44 px tall. | | `kGlassDropScale` | `1.57` | Held drop size relative to the knob (switch and slider). | | `kGlassSwitchDropWiden` | `5` | The switch's default `dropWiden`, in px. | | `kGlassDropOptics` | `GlassOptics(thickness: 10, strength: -4.1)` | The drop's refraction. | | `kGlassDropDuration` | `Duration(milliseconds: 180)` | How long the drop takes to grow in and out. | | `kGlassDisabledOpacity` | `0.5` | Opacity of a disabled switch or slider. | --- # Slider > GlassSlider is a continuous 0 to 1 slider whose knob turns into a clear glass drop while you drag it, for volume, brightness or a scrubber. - Live: https://g1455.plugfox.dev/components/slider - API: [`GlassSlider`](https://pub.dev/documentation/g1455/latest/g1455/GlassSlider-class.html), [`SliderGeometry`](https://pub.dev/documentation/g1455/latest/g1455/SliderGeometry-class.html), [`kGlassDropScale`](https://pub.dev/documentation/g1455/latest/g1455/kGlassDropScale-constant.html), [`kGlassDisabledOpacity`](https://pub.dev/documentation/g1455/latest/g1455/kGlassDisabledOpacity-constant.html) - Source: [`lib/src/surface/glass_controls.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_controls.dart) `GlassSlider` picks a value between 0 and 1. It is 44 px tall and as wide as its parent lets it be. At rest the knob is a white capsule on a thin track; while you press or drag it, the knob lifts into a clear glass drop, like the [switch](https://g1455.plugfox.dev/components/switch.md)'s. Tapping anywhere on the track jumps the value there. Screen readers can step it up and down. ## When to use - Continuous values: volume, brightness, a playback scrubber, a blur radius. - **Not** for discrete steps. There is no snapping; if you need steps, round the value yourself or use a [segmented control](https://g1455.plugfox.dev/components/segmented-control.md). - **Not** for values you need to type exactly. Pair it with a readout, or use a text field. ## Usage The slider is controlled. It needs a bounded width, so in a `Row` put it in an `Expanded`: ```dart Row( children: [ const Icon(Icons.volume_down), Expanded( child: GlassSlider( value: _volume, onChanged: (double v) => setState(() => _volume = v), onChangeEnd: (double v) => save(v), semanticLabel: 'Volume', ), ), const Icon(Icons.volume_up), ], ) ``` `onChanged` fires on every frame of a drag. Use `onChangeStart` / `onChangeEnd` for work that should happen once, such as saving. `onChanged: null` disables the slider (50% opacity). ## Accessibility Give every slider a `semanticLabel` ("Volume", "Brightness"). `semanticStep` (default 0.1) is how far one increase or decrease moves it; 0.05 gives a screen reader 20 steps. ## Performance The fill of the track ends under the drop and follows it, so every frame of a drag changes what is under the glass, and the host captures again each frame. That is expected and is the price of a slider over glass. At rest it costs nothing. ## Lining things up with the fill `SliderGeometry.fillEnd(value, width)` returns the x position where the fill ends on a slider of that width. Use it to place a tick, a label or a buffered-range bar exactly under the knob's centre. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// Volume and brightness on a glass card, with a readout. /// Assumes a GlassHost above the navigator (MaterialApp.builder). class DisplayControls extends StatefulWidget { const DisplayControls({super.key}); @override State createState() => _DisplayControlsState(); } class _DisplayControlsState extends State { double _volume = 0.6; double _brightness = 0.8; Widget _slider(String label, IconData icon, double value, ValueChanged onChanged, Color color) => Row( children: [ Icon(icon, size: 20), const SizedBox(width: 8), Expanded( child: GlassSlider( value: value, onChanged: onChanged, onChangeEnd: (double v) => debugPrint('$label saved: $v'), activeColor: color, semanticLabel: label, semanticStep: 0.05, ), ), SizedBox(width: 44, child: Text('${(value * 100).round()}%', textAlign: TextAlign.end)), ], ); @override Widget build(BuildContext context) => SizedBox( width: 360, child: GlassCard( child: Column( mainAxisSize: MainAxisSize.min, children: [ _slider( 'Volume', Icons.volume_up, _volume, (double v) => setState(() => _volume = v), const Color(0xFF0A84FF), ), _slider( 'Brightness', Icons.light_mode, _brightness, (double v) => setState(() => _brightness = v), const Color(0xFFFFD60A), ), ], ), ), ); } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `value` | `double` | **required** | The value, from 0 to 1 (clamped). | | `onChanged` | `ValueChanged?` | **required** | Called while dragging. Null disables the slider (50% opacity). | | `onChangeStart` | `ValueChanged?` | `null` | A drag or tap began. | | `onChangeEnd` | `ValueChanged?` | `null` | A drag or tap ended. | | `activeColor` | `Color` | `Color(0xFF0A84FF)` | The filled part of the track. | | `trackColor` | `Color` | `Color(0x29787880)` | The rest of the track. | | `dropScale` | `double` | `kGlassDropScale` | Size of the held drop relative to the knob. At least 1. | | `dropWiden` | `double` | `0` | How many px of the surroundings the drop pulls in. | | `dropMotion` | `GlassDropMotion?` | `null` | How the held drop stretches and squashes as it moves. Null takes the theme's; `GlassDropMotion.none` keeps it round. See [Drop motion](https://g1455.plugfox.dev/foundations/drop-motion.md). | | `semanticLabel` | `String?` | `null` | Screen-reader label. | | `semanticStep` | `double` | `0.1` | How far one accessibility increase or decrease moves the value. Between 0 (exclusive) and 1. | | `key` | `Key?` | `null` | | ### Helpers | Name | Description | |---|---| | `SliderGeometry.fillEnd(double value, double width)` | The x position, in px from the slider's left edge, where the fill ends for `value` on a slider `width` wide. | --- # Segmented control > GlassSegmentedControl picks one of two to five options. The selected segment lifts into a clear glass drop you can slide to another segment. - Live: https://g1455.plugfox.dev/components/segmented-control - API: [`GlassSegmentedControl`](https://pub.dev/documentation/g1455/latest/g1455/GlassSegmentedControl-class.html), [`kGlassSegmentTrack`](https://pub.dev/documentation/g1455/latest/g1455/kGlassSegmentTrack-constant.html), [`kGlassSegmentDropGrow`](https://pub.dev/documentation/g1455/latest/g1455/kGlassSegmentDropGrow-constant.html), [`kGlassSegmentDropWiden`](https://pub.dev/documentation/g1455/latest/g1455/kGlassSegmentDropWiden-constant.html) - Source: [`lib/src/surface/glass_segmented_control.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_segmented_control.dart) `GlassSegmentedControl` is iOS 26's segmented control. The track is a flat translucent fill, and the selected segment sits on a white capsule. Press the selection and it lifts into a clear glass drop that stands out of the track; slide it along and let go over another segment to select that one. Tapping a segment selects it directly. The track and the capsule are paint, not glass, so at rest the control costs nothing. The drop exists only while it is held. ## When to use - Two to five mutually exclusive views or filters: Day / Week / Month, List / Grid / Map. - **Not** for navigation between the main sections of an app: that is a [tab bar](https://g1455.plugfox.dev/components/tab-bar.md). - **Not** for one on/off option: that is a [switch](https://g1455.plugfox.dev/components/switch.md). - **Not** for more than five options, or long labels. It asserts at least two segments. ## Usage ```dart SizedBox( width: 280, child: GlassSegmentedControl( segments: const [Text('Day'), Text('Week'), Text('Month')], selectedIndex: _range, onSelected: (int i) => setState(() => _range = i), ), ) ``` The control is 32 px tall (44 px with its tap area) and takes the width of its parent, divided evenly between segments. It needs a bounded width. ## Label colours The control lives in the content layer, so it does not pick a label colour for the unselected segments: they use the ambient `DefaultTextStyle` and `IconTheme`. Inside a [Card](https://g1455.plugfox.dev/components/card.md) that is already the card's legible colour. Elsewhere, set it yourself with `DefaultTextStyle.merge`. The **selected** segment's label is picked for you, black on a light `thumbColor` and white on a dark one. ## Accessibility Each segment is a button for a screen reader, with the selected one marked as selected. Text segments read their text. For icon segments, wrap each icon in `Semantics(label: ...)` or use `Icon(..., semanticLabel: ...)`. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A calendar header: a range picker on a glass card, and what it selects. /// Assumes a GlassHost above the navigator (MaterialApp.builder). class RangePicker extends StatefulWidget { const RangePicker({super.key}); @override State createState() => _RangePickerState(); } class _RangePickerState extends State { static const List _ranges = ['Day', 'Week', 'Month', 'Year']; int _range = 1; @override Widget build(BuildContext context) => SizedBox( width: 360, child: GlassCard( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Unselected labels take this style; the card has already set the colour. DefaultTextStyle.merge( style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), child: GlassSegmentedControl( segments: [for (final String r in _ranges) Text(r)], selectedIndex: _range, onSelected: (int i) => setState(() => _range = i), ), ), const SizedBox(height: 16), Text( 'This ${_ranges[_range].toLowerCase()}', style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600), ), ], ), ), ); } /// Icon segments with labels for screen readers, on a dark thumb. Widget layoutPicker(int index, ValueChanged onSelected) => SizedBox( width: 200, child: GlassSegmentedControl( segments: const [ Icon(Icons.list, semanticLabel: 'List'), Icon(Icons.grid_view, semanticLabel: 'Grid'), Icon(Icons.map, semanticLabel: 'Map'), ], selectedIndex: index, onSelected: onSelected, thumbColor: const Color(0xFF636366), ), ); ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `segments` | `List` | **required** | One widget per segment, usually a `Text` or an `Icon`. At least 2. | | `selectedIndex` | `int` | **required** | The selected segment. | | `onSelected` | `ValueChanged?` | **required** | Called with the new index. Null disables the control (half opacity). | | `trackColor` | `Color` | `kGlassSegmentTrack` | The track's fill. | | `thumbColor` | `Color` | `Color(0xFFFFFFFF)` | The capsule under the selected segment at rest. | | `dropMotion` | `GlassDropMotion?` | `null` | How the held drop stretches and squashes as it moves. Null takes the theme's; `GlassDropMotion.none` keeps it round. See [Drop motion](https://g1455.plugfox.dev/foundations/drop-motion.md). | | `key` | `Key?` | `null` | | ### Constants | Name | Value | Description | |---|---|---| | `kGlassSegmentTrack` | `Color.fromRGBO(118, 118, 128, 0.12)` | The default track fill (iOS's tertiary system fill). | | `kGlassSegmentDropGrow` | `Size(12, 8)` | How much larger the held drop is than the capsule, per side. | | `kGlassSegmentDropWiden` | `2.9` | How many px of the surroundings the drop pulls in (a slight zoom-out). | --- # Tab bar > GlassTabBar is the floating iOS 26 tab bar. The selected tab lifts into a glass drop that magnifies the bar and can be dragged from tab to tab. - Live: https://g1455.plugfox.dev/components/tab-bar - API: [`GlassTabBar`](https://pub.dev/documentation/g1455/latest/g1455/GlassTabBar-class.html), [`GlassTabItem`](https://pub.dev/documentation/g1455/latest/g1455/GlassTabItem-class.html), [`GlassTabItemLook`](https://pub.dev/documentation/g1455/latest/g1455/GlassTabItemLook-class.html), [`GlassTabItemBuilder`](https://pub.dev/documentation/g1455/latest/g1455/GlassTabItemBuilder.html), [`kGlassTabDropZoom`](https://pub.dev/documentation/g1455/latest/g1455/kGlassTabDropZoom-constant.html), [`kGlassTabDropGrow`](https://pub.dev/documentation/g1455/latest/g1455/kGlassTabDropGrow-constant.html) - Source: [`lib/src/surface/glass_tab_bar.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_tab_bar.dart) `GlassTabBar` is the floating tab bar of iOS 26: a [Bar](https://g1455.plugfox.dev/components/bar.md) holding two to five tabs, each an icon over a label. The selected tab sits on a grey pill in the `activeColor`. Press it and the pill lifts into a clear glass drop that magnifies the bar under it; drag the drop along the bar and the tab under your finger lights up; let go and that tab is selected. A plain tap on a tab works too. The layout adapts to the width: below 80 px per tab, the icon sits over the label (phones); above that, they sit side by side (tablets and desktop). ## When to use - The top-level sections of an app: Home, Search, Library, Profile. - **Not** for switching views inside one screen: that is a [segmented control](https://g1455.plugfox.dev/components/segmented-control.md). - **Not** for actions: tabs select a place, they don't do something. Use a [toolbar](https://g1455.plugfox.dev/components/toolbar.md) for actions. - **Not** for more than five sections. It asserts at least two. ## Usage ```dart const List tabs = [ GlassTabItem(icon: Icons.home, label: 'Home'), GlassTabItem(icon: Icons.search, label: 'Search'), GlassTabItem(icon: Icons.person, label: 'Profile'), ]; Positioned( left: 16, right: 16, bottom: MediaQuery.paddingOf(context).bottom + 12, child: GlassTabBar( items: tabs, selectedIndex: _tab, onSelected: (int i) => setState(() => _tab = i), ), ) ``` The bar takes its width from its parent, so it needs a bounded width: `Positioned(left:, right:)` is the usual way. Leave room at the bottom of your scrolling content so the last item is not hidden behind it. ## Behaviour - The drop magnifies by `dropZoom` (default `kGlassTabDropZoom`, 1.17, as on iOS). `1` means no magnification. - The drop is glass over glass (the bar), so while it is held the host captures one extra level. At rest, the bar is one surface. - Each tab is a button for screen readers, labelled with its `label` and marked selected. - The held drop stretches as it sets off and squashes as it lands. `dropMotion:` tunes it or turns it off; see [Drop motion](https://g1455.plugfox.dev/foundations/drop-motion.md). ## Custom icons and badges An `IconData` is drawn by the bar in the right colour: the accent when selected, otherwise the label colour its glass chose. For anything else, an SVG, an image, a badge, give `iconBuilder` (or `labelBuilder`), which is handed that colour and size in a `GlassTabItemLook`: ```dart GlassTabItem( label: 'Inbox', iconBuilder: (BuildContext context, GlassTabItemLook look) => Badge( label: const Text('3'), child: Icon(Icons.inbox, color: look.color, size: look.iconSize), ), ) ``` `label` stays what a screen reader says, whatever the builders draw. The items are built once and again only when their colour changes, which is when the drop moves onto them or off them. > [!WARNING] > If glass cards scroll under the tab bar, wrap it in [`GlassAbove`](https://g1455.plugfox.dev/foundations/above.md), or put a bottom > [scroll edge](https://g1455.plugfox.dev/foundations/scroll-edge.md) under it. Otherwise the bar shows the content but not the cards. > [!TIP] > [`GlassScaffold`](https://g1455.plugfox.dev/components/scaffold.md) places a tab bar as its `bottomBar`: lifted, at the bottom of the safe area, > with the list padded to end above it. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// An app shell: one page per tab, a floating glass tab bar on top. /// Assumes a GlassHost above the navigator (MaterialApp.builder). class AppShell extends StatefulWidget { const AppShell({super.key}); @override State createState() => _AppShellState(); } class _AppShellState extends State { static const List _tabs = [ GlassTabItem(icon: Icons.home, label: 'Home'), GlassTabItem(icon: Icons.search, label: 'Search'), GlassTabItem(icon: Icons.library_music, label: 'Library'), GlassTabItem(icon: Icons.person, label: 'Profile'), ]; int _tab = 0; @override Widget build(BuildContext context) { final double bottom = MediaQuery.paddingOf(context).bottom; return Scaffold( body: Stack( children: [ Positioned.fill( // Keeps every page alive; the bar floats over whichever is shown. child: IndexedStack( index: _tab, children: [ for (final GlassTabItem tab in _tabs) ListView.builder( // Room at the end so the last row is not under the bar. padding: EdgeInsets.only(bottom: bottom + 96), itemCount: 40, itemBuilder: (BuildContext context, int i) => ListTile(title: Text('${tab.label} ${i + 1}')), ), ], ), ), Positioned( left: 16, right: 16, bottom: bottom + 12, child: GlassTabBar(items: _tabs, selectedIndex: _tab, onSelected: (int i) => setState(() => _tab = i)), ), ], ), ); } } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `items` | `List` | **required** | The tabs. At least 2. | | `selectedIndex` | `int` | **required** | The selected tab. | | `onSelected` | `ValueChanged?` | **required** | Called with the new tab. Null disables the bar. | | `activeColor` | `Color` | `Color(0xFF007AFF)` | Icon and label colour of the selected tab, and of the tab under a held drop. | | `dropZoom` | `double` | `kGlassTabDropZoom` | How much the held drop magnifies the bar. `1` for none; must be above 0. | | `dropMotion` | `GlassDropMotion?` | `null` | How the held drop stretches and squashes as it moves. Null takes the theme's; `GlassDropMotion.none` keeps it round. See [Drop motion](https://g1455.plugfox.dev/foundations/drop-motion.md). | | `key` | `Key?` | `null` | | ### GlassTabItem | Parameter | Type | Default | Description | |---|---|---|---| | `label` | `String` | **required** | The tab's name: the text drawn unless `labelBuilder` is given, and what a screen reader says always. | | `icon` | `IconData?` | `null` | The tab's icon, drawn in the colour the bar resolved. Ignored when `iconBuilder` is given. | | `iconBuilder` | `GlassTabItemBuilder?` | `null` | Builds the icon instead of `icon`: an SVG, an image, a badge. Sized by you; the bar's own icons are `look.iconSize`. | | `labelBuilder` | `GlassTabItemBuilder?` | `null` | Builds the label instead of the text of `label`. | An item needs an `icon` or an `iconBuilder`; it asserts. `GlassTabItemBuilder` is `Widget Function(BuildContext context, GlassTabItemLook look)`. ### GlassTabItemLook What the bar resolved for one item, as it draws it. | Field | Type | Description | |---|---|---| | `index` | `int` | Which item. | | `color` | `Color` | The colour the item is drawn in now: `activeColor` when `highlighted`, otherwise the label colour the bar's glass chose. | | `iconSize` | `double` | The size the bar draws its own icons at: 26 stacked, 20 side by side. | | `labelStyle` | `TextStyle` | The label's style, `color` included. | | `selected` | `bool` | Whether this is `selectedIndex`. | | `highlighted` | `bool` | Whether this item takes the accent: the selected one at rest, the one under the drop while it is held. | | `inline` | `bool` | Whether the bar lays icon beside label (a wide bar) rather than icon over label. | ### Constants | Name | Value | Description | |---|---|---| | `kGlassTabDropZoom` | `1.17` | The default `dropZoom`, read off iOS 26. | | `kGlassTabDropGrow` | `10.5` | How much larger the held drop is than the resting pill, per side, in px. | --- # Text field > A single-line text field in its own glass capsule, like the search field of an iOS 26 bar. Text, placeholder and icons take a legible colour. - Live: https://g1455.plugfox.dev/components/text-field - API: [`GlassTextField`](https://pub.dev/documentation/g1455/latest/g1455/GlassTextField-class.html), [`GlassTextField.search`](https://pub.dev/documentation/g1455/latest/g1455/GlassTextField/GlassTextField.search.html), [`kGlassFieldHeight`](https://pub.dev/documentation/g1455/latest/g1455/kGlassFieldHeight-constant.html) - Source: [`lib/src/surface/glass_text_field.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_text_field.dart) `GlassTextField` is a single line of text in a 44 px glass capsule, built on Flutter's `EditableText`. The text, the placeholder and the icons are drawn in the label colour the theme picks for the glass, so they read over whatever is behind it. The placeholder is the label colour at 30% and the icons at 60%. Typing and the blinking caret repaint only the text inside the capsule, so neither makes the host capture again. `GlassTextField.search` is the preset for a bar's search field: a magnifier in front, "Search" as the placeholder, and the keyboard's search action. ## When to use - A search field that floats on its own: at the top of a list, in a toolbar, over a photo or a map. - A short single-line input that stands directly on the content, such as a caption or a name. - **Not** for fields that sit *on* a glass card or in a sheet. On iOS those are ordinary fields, and a glass field there is glass on glass, which costs an extra capture level. Use a plain field there. - **Not** for multi-line text or full forms. There is no `maxLines`, `decoration`, `inputFormatters` or validation. ## Usage ```dart Positioned( top: 16, left: 16, right: 16, // the field stretches, so give it a bounded width child: GlassTextField.search( onSubmitted: (String query) => search(query), ), ) ``` The main constructor takes everything the preset fixes: `placeholder`, `leading`, `keyboardType`, `textInputAction` and `obscureText` for a password. ```dart GlassTextField( placeholder: 'Password', leading: const Icon(Icons.lock_outline), obscureText: true, textInputAction: TextInputAction.done, onSubmitted: signIn, ) ``` ## Layout - The field **stretches to the width it is given**, so the width has to be bounded. In a `Row`, wrap it in `Expanded`; in a `Stack`, give the `Positioned` a `left` and a `right`. - Its height is fixed at [kGlassFieldHeight](https://pub.dev/documentation/g1455/latest/g1455/kGlassFieldHeight-constant.html) (44), which is also the minimum tap target. A tap anywhere on the capsule focuses the field. ## Behaviour - `controller` and `focusNode` are optional. Without them the field makes its own and disposes of them. - There is no built-in clear button. Pass one as `trailing` and wire it to `controller.clear()`, as the code example does. Icons in `leading` and `trailing` are themed at 20 px. - `cursorColor` defaults to iOS blue (`0xFF007AFF`); the selection is the same colour at 30%. - The field is an `EditableText`, so it needs what an app provides: `MediaQuery`, `Directionality` and an `Overlay` for the selection handles. Any `MaterialApp` or `WidgetsApp` has them. > [!TIP] > The label colour is only as right as what the host knows about the backdrop. Over photos or a scrolling feed, set > `richBackdrop: true` on the [GlassHost](https://g1455.plugfox.dev/foundations/host.md). See [Legibility](https://g1455.plugfox.dev/foundations/legibility.md). ## Accessibility - An icon-only `trailing` button says nothing to a screen reader on its own. Wrap it in `Semantics(button: true, label: 'Clear', ...)`. - Keep a visible label or a clear placeholder: the field has no separate label parameter. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A list of cities with a glass search field floating over it. /// Assumes a GlassHost above, e.g. in MaterialApp.builder. class CitySearch extends StatefulWidget { const CitySearch({super.key}); @override State createState() => _CitySearchState(); } class _CitySearchState extends State { static const List _cities = ['Amsterdam', 'Berlin', 'Kyoto', 'Lisbon', 'Oslo', 'Porto', 'Tbilisi']; final TextEditingController _query = TextEditingController(); @override void dispose() { _query.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final String q = _query.text.toLowerCase(); final List shown = _cities.where((String c) => c.toLowerCase().contains(q)).toList(); return Stack( children: [ // The content the glass floats over. ListView( padding: const EdgeInsets.fromLTRB(16, 76, 16, 16), children: [for (final String city in shown) ListTile(title: Text(city))], ), Positioned( top: 16, left: 16, right: 16, // a bounded width: the field stretches child: GlassTextField.search( controller: _query, onChanged: (_) => setState(() {}), trailing: _query.text.isEmpty ? null : Semantics( button: true, label: 'Clear', child: GestureDetector( onTap: () => setState(_query.clear), child: const Icon(Icons.cancel), ), ), ), ), ], ); } } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `controller` | `TextEditingController?` | `null` | The text. Null makes an internal one. | | `focusNode` | `FocusNode?` | `null` | The focus. Null makes an internal one. | | `placeholder` | `String?` | `null` (`'Search'` in `.search`) | Shown, dimmed to 30%, while the field is empty. | | `leading` | `Widget?` | `null` (a magnifier in `.search`) | Before the text, such as an icon. | | `trailing` | `Widget?` | `null` | After the text, such as a clear button. | | `onChanged` | `ValueChanged?` | `null` | Called on every change of the text. | | `onSubmitted` | `ValueChanged?` | `null` | Called on the keyboard's action. | | `keyboardType` | `TextInputType?` | `null` | Main constructor only; `.search` uses `TextInputType.text`. | | `textInputAction` | `TextInputAction?` | `null` | Main constructor only; `.search` uses `TextInputAction.search`. | | `autofocus` | `bool` | `false` | Focus the field when it first appears. | | `obscureText` | `bool` | `false` | Main constructor only. Hides the text, for passwords. | | `finish` | `GlassFinish?` | `null` | The glass. Null takes the theme's. | | `cursorColor` | `Color` | `Color(0xFF007AFF)` | The caret, and the selection at 30%. | ### Constants | Name | Value | Description | |---|---|---| | `kGlassFieldHeight` | `44` | The field's height. | --- # Toolbar > Several toolbar actions in one glass capsule, the way iOS 26 groups toolbar items. One surface for any number of actions, so it costs far less than a row of buttons. - Live: https://g1455.plugfox.dev/components/toolbar - API: [`GlassButtonGroup`](https://pub.dev/documentation/g1455/latest/g1455/GlassButtonGroup-class.html), [`GlassToolbarItem`](https://pub.dev/documentation/g1455/latest/g1455/GlassToolbarItem-class.html), [`kGlassToolbarHeight`](https://pub.dev/documentation/g1455/latest/g1455/kGlassToolbarHeight-constant.html), [`kGlassToolbarItemWidth`](https://pub.dev/documentation/g1455/latest/g1455/kGlassToolbarItemWidth-constant.html) - Source: [`lib/src/surface/glass_toolbar.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_toolbar.dart) iOS 26 doesn't put a glass behind each toolbar button. It groups adjacent items into one glass shape: a 48 px circle for an item on its own, and a capsule for several. `GlassButtonGroup` does the same. You give it a list of `GlassToolbarItem`s (an icon, an action and a label for screen readers), and it draws them in **one** glass surface. Pressing an item brightens just its cell, the same way a [GlassButton](https://g1455.plugfox.dev/components/button.md) brightens: the finish's rim colour is added over the cell, clipped to the capsule. Icons are themed at 22 px in the label colour the theme picks for the glass. ## When to use - Icon actions in a toolbar or over content: undo and redo, share, like, delete, previous and next. - Instead of a row of `GlassButton`s whenever the actions sit next to each other. - **Not** for text labels. The group is icon-first and its size is fixed. For a labelled action, use a [GlassButton](https://g1455.plugfox.dev/components/button.md). - **Not** for choosing one of several values. That's a [segmented control](https://g1455.plugfox.dev/components/segmented-control.md). ## Usage ```dart Row( children: [ GlassButtonGroup( items: [ GlassToolbarItem(icon: const Icon(Icons.undo), label: 'Undo', onPressed: canUndo ? undo : null), GlassToolbarItem(icon: const Icon(Icons.redo), label: 'Redo', onPressed: canRedo ? redo : null), ], ), const Spacer(), GlassButtonGroup( items: [ GlassToolbarItem(icon: const Icon(Icons.ios_share), label: 'Share', onPressed: share), ], ), ], ) ``` ## Why one surface Glass is paid for per surface, and the overhead grows faster than the number of surfaces. Three `GlassButton`s are three surfaces; a group of three is one. Grouping is how Apple draws it, and it's also the cheap way to build it. See [Performance](https://g1455.plugfox.dev/foundations/performance.md). ## Layout - The size is fixed: [kGlassToolbarHeight](https://pub.dev/documentation/g1455/latest/g1455/kGlassToolbarHeight-constant.html) (48) high, and 48 wide for one item or [kGlassToolbarItemWidth](https://pub.dev/documentation/g1455/latest/g1455/kGlassToolbarItemWidth-constant.html) (53) per item for more. Don't stretch it; place groups with a `Row` and `Spacer`s. - Four items are 212 px wide. On a narrow phone, a 4-item group plus two circles don't fit in 320 px: drop an action or move it into a [menu](https://g1455.plugfox.dev/components/menu.md). - Groups float over the content on their own, as in iOS. Putting them inside a [GlassBar](https://g1455.plugfox.dev/components/bar.md) works, but it is glass on glass and adds a capture level. ## Accessibility - **Always set `label`.** It is what a screen reader says for the item; the icon alone says nothing. - `onPressed: null` disables an item: its icon drops to 30% and it is announced as disabled. > [!WARNING] > Don't wrap a group in `Opacity`, `ColorFilter` or `ImageFilter`. The pressed highlight is added onto what's below, and > those widgets break it. `RepaintBoundary` is fine. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A photo viewer's bottom toolbar: share alone, three actions grouped, /// and delete alone. Three glass surfaces for five actions. /// Assumes a GlassHost above, e.g. in MaterialApp.builder. class PhotoViewer extends StatefulWidget { const PhotoViewer({required this.photo, super.key}); final ImageProvider photo; @override State createState() => _PhotoViewerState(); } class _PhotoViewerState extends State { bool _liked = false; void _toast(String what) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(what))); @override Widget build(BuildContext context) => Stack( children: [ Positioned.fill(child: Image(image: widget.photo, fit: BoxFit.cover)), Positioned( left: 16, right: 16, bottom: 16 + MediaQuery.paddingOf(context).bottom, child: Row( children: [ GlassButtonGroup( items: [ GlassToolbarItem(icon: const Icon(Icons.ios_share), label: 'Share', onPressed: () => _toast('Share')), ], ), const Spacer(), GlassButtonGroup( items: [ GlassToolbarItem( icon: Icon(_liked ? Icons.favorite : Icons.favorite_border), label: _liked ? 'Unlike' : 'Like', onPressed: () => setState(() => _liked = !_liked), ), GlassToolbarItem(icon: const Icon(Icons.info_outline), label: 'Info', onPressed: () => _toast('Info')), GlassToolbarItem(icon: const Icon(Icons.tune), label: 'Edit', onPressed: () => _toast('Edit')), ], ), const Spacer(), GlassButtonGroup( items: [ GlassToolbarItem( icon: const Icon(Icons.delete_outline), label: 'Delete', onPressed: () => _toast('Delete'), ), ], ), ], ), ), ], ); } ``` ## API `GlassButtonGroup` | Parameter | Type | Default | Description | |---|---|---|---| | `items` | `List` | **required** | The actions, left to right. Must not be empty. | | `finish` | `GlassFinish?` | `null` | The glass. Null takes the theme's. | | `pressedOverlay` | `Color?` | `null` | Added over a held item's cell. Null takes the finish's rim colour. | `GlassToolbarItem` | Parameter | Type | Default | Description | |---|---|---|---| | `icon` | `Widget` | **required** | The icon, themed at 22 px in the label colour. | | `onPressed` | `VoidCallback?` | **required** | The action. Null disables the item (icon at 30%). | | `label` | `String?` | `null` | What a screen reader says. Always set it. | ### Constants | Name | Value | Description | |---|---|---| | `kGlassToolbarHeight` | `48` | The group's height, and the size of a one-item circle. | | `kGlassToolbarItemWidth` | `53` | Each item's width in a group of two or more. | --- # Alert > iOS 26's alert on glass: a title, an optional message and capsule actions, shown over a dim with showGlassDialog. It materializes in, blur first and tint last. - Live: https://g1455.plugfox.dev/components/alert - API: [`GlassAlert`](https://pub.dev/documentation/g1455/latest/g1455/GlassAlert-class.html), [`GlassAlertAction`](https://pub.dev/documentation/g1455/latest/g1455/GlassAlertAction-class.html), [`showGlassDialog()`](https://pub.dev/documentation/g1455/latest/g1455/showGlassDialog.html), [`kGlassAlertRadius`](https://pub.dev/documentation/g1455/latest/g1455/kGlassAlertRadius-constant.html), [`kGlassAlertWidth`](https://pub.dev/documentation/g1455/latest/g1455/kGlassAlertWidth-constant.html), [`kGlassModalDim`](https://pub.dev/documentation/g1455/latest/g1455/kGlassModalDim-constant.html) - Source: [`lib/src/surface/glass_modal.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_modal.dart) `GlassAlert` is iOS 26's alert: a 320 px glass panel with 34 px corners, a title, an optional message and capsule action buttons. You show it with `showGlassDialog`, which pushes it on the navigator, centred over a 20% black dim, and returns a `Future` with the value the alert was closed with. The alert materializes in: blur first, tint last, with the text fading in on top. It lifts itself above page glass and glass bars, so you don't need a [GlassAbove](https://g1455.plugfox.dev/foundations/above.md) for it. ## When to use - Short confirmations and destructive-action prompts: "Delete this photo?", "Discard changes?". - A message the user must acknowledge before going on. - **Not** for forms, pickers or anything long. Use a [sheet](https://g1455.plugfox.dev/components/sheet.md). - **Not** for a list of actions on an item. Use a [menu](https://g1455.plugfox.dev/components/menu.md). ## Usage ```dart final bool? delete = await showGlassDialog( context: context, builder: (BuildContext context) => GlassAlert( title: const Text('Delete "Lisbon.jpg"?'), message: const Text('This cannot be undone.'), actions: [ GlassAlertAction(label: 'Cancel', onPressed: () => Navigator.pop(context, false)), GlassAlertAction(label: 'Delete', isDestructive: true, onPressed: () => Navigator.pop(context, true)), ], ), ); ``` > [!NOTE] > **Actions don't close the alert by themselves.** Call `Navigator.pop(context, value)` in each `onPressed`. The value > you pop with is what `showGlassDialog` returns. ## Behaviour - Exactly two actions sit side by side. One, or three and more, are stacked. - `isDefault` draws an action's label bold; `isDestructive` draws it in iOS red. `onPressed: null` disables it. - The title is 17 px semibold, the message 15 px at 60% of the label colour. Both start-aligned by default; pass `textAlign: TextAlign.center` to your `Text`s for the centred iOS look. - The panel is 320 px wide, or the screen width less 16 px on each side when that is narrower. - By default a tap on the dim does nothing, as in iOS. Set `barrierDismissible: true` to let it close the alert; the `Future` then completes with `null`. ## Setup > [!WARNING] > The [GlassHost](https://g1455.plugfox.dev/foundations/host.md) must be **above the navigator**, so put it in `MaterialApp(builder: ...)`. The > alert is built in the navigator's overlay; with the host inside a page instead, it finds no host and says so in debug. ## Accessibility - The dim is announced with `barrierLabel` ("Dismiss" by default) when it can close the alert. - Keep action labels short verbs ("Delete", "Keep"), not "Yes" and "No". ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A row that asks before deleting its file. /// Assumes a GlassHost above the navigator, e.g. in MaterialApp.builder. class FileTile extends StatefulWidget { const FileTile({required this.name, required this.onDelete, super.key}); final String name; final VoidCallback onDelete; @override State createState() => _FileTileState(); } class _FileTileState extends State { Future _confirm() async { final bool? delete = await showGlassDialog( context: context, builder: (BuildContext context) => GlassAlert( title: Text('Delete "${widget.name}"?', textAlign: TextAlign.center), message: const Text('It will be removed from all your devices.', textAlign: TextAlign.center), actions: [ // Actions don't close the alert: pop with the answer. GlassAlertAction(label: 'Cancel', onPressed: () => Navigator.pop(context, false)), GlassAlertAction(label: 'Delete', isDestructive: true, onPressed: () => Navigator.pop(context, true)), ], ), ); if (delete ?? false) { widget.onDelete(); } } @override Widget build(BuildContext context) => ListTile( leading: const Icon(Icons.image_outlined), title: Text(widget.name), trailing: GlassButton( onPressed: _confirm, semanticLabel: 'Delete ${widget.name}', padding: EdgeInsets.zero, child: const Icon(Icons.delete_outline), ), ); } ``` ## API `showGlassDialog` | Parameter | Type | Default | Description | |---|---|---|---| | `context` | `BuildContext` | **required** | Where to find the navigator. | | `builder` | `WidgetBuilder` | **required** | Builds the dialog, usually a `GlassAlert`. Centred in a `SafeArea`. | | `barrierDismissible` | `bool` | `false` | Whether a tap on the dim closes the dialog (with `null`). | | `barrierLabel` | `String` | `'Dismiss'` | What a screen reader says for the dim. | | `transitionDuration` | `Duration` | `Duration(milliseconds: 250)` | How long the alert takes to materialize. | | *returns* | `Future` | | The value passed to `Navigator.pop`, or `null`. | `GlassAlert` | Parameter | Type | Default | Description | |---|---|---|---| | `title` | `Widget` | **required** | The title, 17 px semibold. | | `message` | `Widget?` | `null` | The message under it, 15 px at 60% of the label colour. | | `actions` | `List` | `[]` | The buttons. Two sit side by side; other counts are stacked. | | `finish` | `GlassFinish?` | `null` | The glass. Null takes the theme's. | `GlassAlertAction` | Parameter | Type | Default | Description | |---|---|---|---| | `label` | `String` | **required** | The button's text. | | `onPressed` | `VoidCallback?` | `null` | The action. Null disables it. It does not close the alert. | | `isDefault` | `bool` | `false` | Draws the label bold, as the preferred action. | | `isDestructive` | `bool` | `false` | Draws the label in iOS red. | ### Constants | Name | Value | Description | |---|---|---| | `kGlassAlertWidth` | `320` | The alert's width. | | `kGlassAlertRadius` | `34` | The alert's corner radius. | | `kGlassModalDim` | `Color.fromRGBO(0, 0, 0, 0.20)` | The dim behind an alert and a sheet. | --- # Sheet > A floating glass bottom sheet with a grabber, inset from the screen edges. Drag it down or flick it to dismiss, and await the value it was closed with. - Live: https://g1455.plugfox.dev/components/sheet - API: [`showGlassSheet()`](https://pub.dev/documentation/g1455/latest/g1455/showGlassSheet.html), [`kGlassSheetInset`](https://pub.dev/documentation/g1455/latest/g1455/kGlassSheetInset-constant.html), [`kGlassSheetRadius`](https://pub.dev/documentation/g1455/latest/g1455/kGlassSheetRadius-constant.html) - Source: [`lib/src/surface/glass_modal.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_modal.dart) `showGlassSheet` shows your content in a glass sheet that rises from the bottom of the screen over a 20% dim, like an iOS 26 sheet at its medium height. The sheet floats 8 px in from the screen's sides and bottom, has 36 px corners and a grabber, and sizes itself to its content, up to the safe area. Drag it down past a third of its height, or flick it, and it goes; let go earlier and it springs back. A tap on the dim closes it too. Like `showDialog`, it returns a `Future` that completes with the value the sheet was popped with. On a narrow window, this site's own navigation (the menu button at the left of the top bar) opens in a glass sheet. ## When to use - Share sheets, action sheets, pickers and short forms. - Details about one item that the user glances at and dismisses. - **Not** for full-screen flows: there is only one height, the content's. Push a page instead. - **Not** for a yes/no confirmation. That's an [alert](https://g1455.plugfox.dev/components/alert.md). ## Usage ```dart final String? colour = await showGlassSheet( context: context, builder: (BuildContext context) => Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ for (final String c in ['Red', 'Green', 'Blue']) Padding( padding: const EdgeInsets.only(bottom: 8), child: GlassButton(onPressed: () => Navigator.pop(context, c), child: Text(c)), ), ], ), ), ); ``` ## Content > [!WARNING] > The sheet does **not** colour its content for legibility. Text inherits the style of the page that opened the sheet, > which may not read on the glass. Ask the theme for the label colour and apply it yourself: > > `final Color label = GlassTheme.of(context).legibility(finish).label;` > > Glass components inside the sheet, such as [GlassButton](https://g1455.plugfox.dev/components/button.md), pick their own colour. - The content is laid out in a `Flexible` under the grabber. Long content should scroll: a `SingleChildScrollView` works, and a `ListView` needs `shrinkWrap: true` or a bounded height. - The route has no `Material`. Widgets that need one, such as `InkWell` or `ListTile`, need a `Material(type: MaterialType.transparency)` around them. - `finish` sets the glass of this sheet only. Pass the same finish to `legibility` so the text matches it. ## Setup The [GlassHost](https://g1455.plugfox.dev/foundations/host.md) must be above the navigator (in `MaterialApp(builder: ...)`), as for every modal. The sheet lifts itself above page glass and bars on its own. ## Accessibility - With `barrierDismissible` on (the default), the dim is announced with `barrierLabel`. - Dragging is not the only way out: give the sheet a visible close or cancel action as well. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// Opens a glass sheet to choose who to share with, and returns the name. /// Assumes a GlassHost above the navigator, e.g. in MaterialApp.builder. Future shareWith(BuildContext context) => showGlassSheet( context: context, builder: (BuildContext context) { // The sheet doesn't colour its content: ask the theme what reads on it. final Color label = GlassTheme.of(context).legibility().label; return DefaultTextStyle.merge( style: TextStyle(color: label, fontSize: 17), child: IconTheme.merge( data: IconThemeData(color: label), child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text('Share with', style: TextStyle(fontWeight: FontWeight.w600)), const SizedBox(height: 12), for (final String name in ['Anna', 'Ben', 'Chiara']) Semantics( button: true, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => Navigator.pop(context, name), child: SizedBox( height: 48, child: Row( children: [ const Icon(Icons.person_outline), const SizedBox(width: 12), Text(name), ], ), ), ), ), const SizedBox(height: 12), GlassButton(onPressed: () => Navigator.pop(context), child: const Text('Cancel')), ], ), ), ), ); }, ); /// A button that opens the sheet and shows the choice. class ShareButton extends StatelessWidget { const ShareButton({super.key}); @override Widget build(BuildContext context) => GlassButton( onPressed: () async { final String? name = await shareWith(context); if (name != null && context.mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Sent to $name'))); } }, child: const Text('Share'), ); } ``` ## API `showGlassSheet` | Parameter | Type | Default | Description | |---|---|---|---| | `context` | `BuildContext` | **required** | Where to find the navigator. | | `builder` | `WidgetBuilder` | **required** | The sheet's content, laid out in a `Flexible` under the grabber. | | `finish` | `GlassFinish?` | `null` | The glass. Null takes the theme's. | | `showGrabber` | `bool` | `true` | Draws the 36 × 5 grabber at the top. | | `barrierDismissible` | `bool` | `true` | Whether a tap on the dim closes the sheet (with `null`). | | `barrierLabel` | `String` | `'Dismiss'` | What a screen reader says for the dim. | | *returns* | `Future` | | The value passed to `Navigator.pop`, or `null`. | ### Constants | Name | Value | Description | |---|---|---| | `kGlassSheetInset` | `8` | The sheet's distance from the screen's sides and bottom. | | `kGlassSheetRadius` | `36` | The sheet's corner radius. | | `kGlassModalDim` | `Color.fromRGBO(0, 0, 0, 0.20)` | The dim behind the sheet. | --- # Menu > iOS 26's pull-down menu: a glass list of actions that grows out of the button that opened it. Choosing a row runs it and closes the menu. - Live: https://g1455.plugfox.dev/components/menu - API: [`GlassMenuAnchor`](https://pub.dev/documentation/g1455/latest/g1455/GlassMenuAnchor-class.html), [`GlassMenuItem`](https://pub.dev/documentation/g1455/latest/g1455/GlassMenuItem-class.html), [`GlassMenuController`](https://pub.dev/documentation/g1455/latest/g1455/GlassMenuController-class.html), [`kGlassMenuRadius`](https://pub.dev/documentation/g1455/latest/g1455/kGlassMenuRadius-constant.html), [`kGlassMenuRowHeight`](https://pub.dev/documentation/g1455/latest/g1455/kGlassMenuRowHeight-constant.html), [`kGlassMenuWidth`](https://pub.dev/documentation/g1455/latest/g1455/kGlassMenuWidth-constant.html) - Source: [`lib/src/surface/glass_modal.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_modal.dart) `GlassMenuAnchor` wraps the widget that opens a menu, usually a "…" [GlassButton](https://g1455.plugfox.dev/components/button.md), and shows a glass menu of `GlassMenuItem`s when you call `open()` on the controller its `builder` is given. The menu grows out of the button the way iOS 26's does: from the button's own capsule to the menu's size, **over** the button rather than beside it. The button is hidden while the menu is open, because the menu is what it turned into. Choosing a row runs its action and closes the menu. A tap anywhere outside closes it too. There is no dim behind a menu. ## When to use - A "more" (…) button with a handful of actions on an item: rename, duplicate, share, delete. - A short list of one-shot choices from a button, such as "Sort by". - **Not** for controls you adjust in place, such as switches or sliders. Those belong in a [popover](https://g1455.plugfox.dev/components/popover.md), which stays open. - **Not** for a single confirmation. That's an [alert](https://g1455.plugfox.dev/components/alert.md). ## Usage ```dart GlassMenuAnchor( items: [ GlassMenuItem(label: 'Rename', icon: const Icon(Icons.edit_outlined), onPressed: rename), GlassMenuItem(label: 'Duplicate', icon: const Icon(Icons.copy), onPressed: duplicate), GlassMenuItem(label: 'Delete', icon: const Icon(Icons.delete_outline), isDestructive: true, onPressed: delete), ], builder: (BuildContext context, GlassMenuController menu) => GlassButton( onPressed: menu.open, semanticLabel: 'More actions', padding: EdgeInsets.zero, child: const Icon(Icons.more_horiz), ), ) ``` ## Placement - The menu opens **downward** from the button's top when there is room below, and **upward** from its bottom when not. When it opens upward, the items are reversed, so the first one stays nearest your finger. - It lines up with the button on the side of the screen the button is on, and stays at least 8 px inside the screen. - It's built in the nearest `Overlay`, so it stands over everything on the page, bars included. ## Items - Each row is [kGlassMenuRowHeight](https://pub.dev/documentation/g1455/latest/g1455/kGlassMenuRowHeight-constant.html) (42) high, with the icon in a 48 px column before the label. Labels are one line and end with an ellipsis. - `isDestructive` draws the row in iOS red. `onPressed: null` disables the row and dims it to 30%. - The menu is [kGlassMenuWidth](https://pub.dev/documentation/g1455/latest/g1455/kGlassMenuWidth-constant.html) (250) wide unless you pass `width`, with corners of [kGlassMenuRadius](https://pub.dev/documentation/g1455/latest/g1455/kGlassMenuRadius-constant.html) (31.5). ## Opening from code Pass your own `GlassMenuController` to open or close the menu from anywhere: `open()`, `close()` and `isOpen`. A controller drives one anchor at a time. ## Setup and accessibility - The [GlassHost](https://g1455.plugfox.dev/foundations/host.md) must be above the overlay the menu is built in. With the host in `MaterialApp(builder: ...)`, that's true everywhere. - Give an icon-only button a `semanticLabel`. The area around the open menu is announced with `barrierLabel`. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A note's header with a "…" menu of actions. /// Assumes a GlassHost above the navigator, e.g. in MaterialApp.builder. class NoteHeader extends StatefulWidget { const NoteHeader({required this.title, super.key}); final String title; @override State createState() => _NoteHeaderState(); } class _NoteHeaderState extends State { bool _pinned = false; void _toast(String what) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(what))); @override Widget build(BuildContext context) => Row( children: [ Expanded(child: Text(widget.title, style: Theme.of(context).textTheme.headlineSmall)), GlassMenuAnchor( width: 220, items: [ GlassMenuItem( label: _pinned ? 'Unpin' : 'Pin', icon: const Icon(Icons.push_pin_outlined, size: 20), onPressed: () => setState(() => _pinned = !_pinned), ), GlassMenuItem( label: 'Duplicate', icon: const Icon(Icons.copy, size: 20), onPressed: () => _toast('Duplicated'), ), const GlassMenuItem(label: 'Move to…', icon: Icon(Icons.folder_outlined, size: 20)), // disabled GlassMenuItem( label: 'Delete', icon: const Icon(Icons.delete_outline, size: 20), isDestructive: true, onPressed: () => _toast('Deleted'), ), ], builder: (BuildContext context, GlassMenuController menu) => GlassButton( onPressed: menu.open, semanticLabel: 'More actions', padding: EdgeInsets.zero, child: const Icon(Icons.more_horiz), ), ), ], ); } ``` ## API `GlassMenuAnchor` | Parameter | Type | Default | Description | |---|---|---|---| | `items` | `List` | **required** | The menu's rows, top to bottom. | | `builder` | `Widget Function(BuildContext, GlassMenuController)` | **required** | Builds the anchor, such as a button, and gets the controller that opens the menu. | | `controller` | `GlassMenuController?` | `null` | Your own controller, to open or close the menu from outside. Null makes an internal one. | | `finish` | `GlassFinish?` | `null` | The glass. Null takes the theme's. | | `width` | `double` | `kGlassMenuWidth` (250) | The menu's width. | | `barrierLabel` | `String` | `'Dismiss'` | What a screen reader says for the area around the open menu. | `GlassMenuItem` | Parameter | Type | Default | Description | |---|---|---|---| | `label` | `String` | **required** | The row's text, one line. | | `icon` | `Widget?` | `null` | Drawn before the label. | | `onPressed` | `VoidCallback?` | `null` | The action. Null disables the row. The menu closes after it runs. | | `isDestructive` | `bool` | `false` | Draws the row in iOS red. | `GlassMenuController`: `open()`, `close()`, `bool get isOpen`. ### Constants | Name | Value | Description | |---|---|---| | `kGlassMenuWidth` | `250` | The menu's default width. | | `kGlassMenuRowHeight` | `42` | Each row's height. | | `kGlassMenuRadius` | `31.5` | The menu's corner radius, and the popover's default. | --- # Popover > A glass panel of any content that grows out of its anchor and stays open while it is used: for quick settings and filters changed in place. - Live: https://g1455.plugfox.dev/components/popover - API: [`GlassPopoverAnchor`](https://pub.dev/documentation/g1455/latest/g1455/GlassPopoverAnchor-class.html) - Source: [`lib/src/surface/glass_modal.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_modal.dart) `GlassPopoverAnchor` is the [menu](https://g1455.plugfox.dev/components/menu.md)'s sibling for content you change in place. It grows a glass panel out of its anchor the same way, but the panel holds **any** widget you build, and it **stays open** until you tap outside it or call `close()`. Text and icons inside take the label colour the theme picks for the glass. The settings of this very site are a popover: the button with the tune icon at the right end of the top bar is a `GlassPopoverAnchor`. Its panel holds the preset, material, tint, rendering, ripple and contrast choices, and the page under it changes while you adjust them. ## When to use - Quick settings and controls adjusted in place: display options, sort and filter, a volume slider. - Small forms whose effect the user wants to see behind the panel as they change it. - **Not** for a list of one-shot actions. A [menu](https://g1455.plugfox.dev/components/menu.md) runs the action and closes for you. - **Not** for long or multi-step forms. Use a [sheet](https://g1455.plugfox.dev/components/sheet.md). ## Usage ```dart GlassPopoverAnchor( width: 280, popoverBuilder: (BuildContext context) => Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Brightness'), const SizedBox(height: 8), GlassSlider(value: brightness, onChanged: setBrightness, semanticLabel: 'Brightness'), ], ), ), builder: (BuildContext context, GlassMenuController popover) => GlassButton( onPressed: popover.open, semanticLabel: 'Display settings', padding: EdgeInsets.zero, child: const Icon(Icons.tune), ), ) ``` ## Placement - Its height isn't known before it's laid out, so the popover opens **down** from an anchor in the upper half of the screen and **up** from one in the lower half. - Like the menu, it lines up with the anchor on the side of the screen the anchor is on, and covers the anchor while open. Keep `width` within the screen: on a 320 px phone, a 320 px panel doesn't fit. - `radius` sets the corners; it defaults to the menu's 31.5. ## Content that changes The panel is rebuilt whenever the widget that builds the anchor rebuilds, so a `setState` there updates an open popover. When the values live somewhere else, such as a `ValueNotifier`, a store or an `InheritedWidget`, listen to them inside `popoverBuilder` so the open panel follows them too. The demo above keeps its filters in a `ValueNotifier` that both the popover and the list listen to. ## Cost - Glass controls inside the panel, such as a [slider](https://g1455.plugfox.dev/components/slider.md) or a [switch](https://g1455.plugfox.dev/components/switch.md), are glass on glass. That works, and costs one more capture level while the panel is open. - When the anchor already sits on a glass bar, a plain `GestureDetector` is a cheaper anchor than a `GlassButton`. The site's settings button does exactly that. ## Accessibility - The area around the open panel is announced with `barrierLabel` ("Dismiss"); it's how a screen reader closes it. - Label the controls inside, e.g. `semanticLabel` on a `GlassSlider`, or `MergeSemantics` around a text and a switch. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A "Sort & filter" button whose popover changes the list behind it. /// Assumes a GlassHost above the navigator, e.g. in MaterialApp.builder. class SortButton extends StatelessWidget { const SortButton({required this.sort, required this.openNow, super.key}); /// 0 = name, 1 = distance, 2 = rating. The list listens to these too. final ValueNotifier sort; final ValueNotifier openNow; @override Widget build(BuildContext context) => GlassPopoverAnchor( width: 300, radius: 26, // Listens, so the panel follows each change while it is open. popoverBuilder: (BuildContext context) => ListenableBuilder( listenable: Listenable.merge([sort, openNow]), builder: (BuildContext context, Widget? _) => Padding( padding: const EdgeInsets.all(18), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text('Sort by', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600)), const SizedBox(height: 8), GlassSegmentedControl( segments: const [Text('Name'), Text('Distance'), Text('Rating')], selectedIndex: sort.value, onSelected: (int i) => sort.value = i, ), const SizedBox(height: 8), MergeSemantics( child: Row( children: [ const Expanded(child: Text('Open now')), GlassSwitch(value: openNow.value, onChanged: (bool v) => openNow.value = v), ], ), ), ], ), ), ), builder: (BuildContext context, GlassMenuController popover) => GlassButton( onPressed: popover.open, child: const Row( mainAxisSize: MainAxisSize.min, children: [Icon(Icons.tune, size: 20), SizedBox(width: 6), Text('Sort & filter')], ), ), ); } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `popoverBuilder` | `WidgetBuilder` | **required** | The panel's content. Built under a text style and icon theme in the label colour. | | `builder` | `Widget Function(BuildContext, GlassMenuController)` | **required** | Builds the anchor, such as a button, and gets the controller that opens the panel. | | `controller` | `GlassMenuController?` | `null` | Your own controller, to open or close the panel from outside. Null makes an internal one. | | `finish` | `GlassFinish?` | `null` | The glass. Null takes the theme's. | | `width` | `double` | `320` | The panel's width. | | `radius` | `double` | `kGlassMenuRadius` (31.5) | The panel's corner radius. | | `barrierLabel` | `String` | `'Dismiss'` | What a screen reader says for the area around the open panel; a tap there closes it. | `GlassMenuController`: `open()`, `close()`, `bool get isOpen`. The panel closes only on a tap outside or `close()`. --- # Morph > One piece of glass that flows to the size of whatever child it holds: a button that becomes a panel, with a liquid neck while it moves. - Live: https://g1455.plugfox.dev/components/morph - API: [`GlassMorph`](https://pub.dev/documentation/g1455/latest/g1455/GlassMorph-class.html), [`GlassMorphMotion`](https://pub.dev/documentation/g1455/latest/g1455/GlassMorphMotion-class.html), [`kGlassMorphSpacing`](https://pub.dev/documentation/g1455/latest/g1455/kGlassMorphSpacing-constant.html) - Source: [`lib/src/surface/glass_morph.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_morph.dart) `GlassMorph` holds one child on glass. Give it a child of another identity, another type or another `Key`, and the glass flows to the new child's size while the old content fades out and the new fades in. It's how a button becomes its menu in iOS 26. You never type a size: the glass measures the child. `width` and `height` are overrides that pin one axis. ## When to use - A control that turns into the panel it opens, in place: a "+" into a list of actions, a pill into a search field. - A card whose content changes size, where the glass should follow rather than jump. - **Not** for a menu that floats over the page and closes on a tap outside. Use the [menu](https://g1455.plugfox.dev/components/menu.md) or the [popover](https://g1455.plugfox.dev/components/popover.md), which bring their own overlay and barrier. ## Usage ```dart Align( alignment: Alignment.topRight, child: GlassMorph( alignment: Alignment.topRight, borderRadius: open ? const BorderRadius.all(Radius.circular(28)) : kGlassCapsule, child: open ? ActionsPanel(key: const ValueKey('panel'), onDone: close) : PlusButton(key: const ValueKey('plus'), onTap: openPanel), ), ) ``` ## Alignment: what holds still `alignment` is the point of the glass that stays put **inside the morph's own box** while the size changes. The box is placed by the morph's parent, so to hold a corner on the screen, the parent has to hold the same corner: an `Align`, a `Positioned` with `top` and `right`, the end of a `Row`. Inside a `Center`, the glass grows from its centre whatever `alignment` says. Give the parent and the morph the same alignment, as the demo does. ## Identity The rule is `AnimatedSwitcher`'s. A child of the same type and key is updated in place: no morph, and the glass takes its new size at once. Changing `width`, `height` or `borderRadius` alone does morph. A child swapped back while it is still fading out keeps its state. ## Motion - `GlassMorphMotion.fluid`, the default: a spring with a little overshoot. - `GlassMorphMotion.calm`: no overshoot, a little slower, for large panels. - Or your own: `GlassMorphMotion(duration: ..., bounce: ...)`, SwiftUI's spring parameters. - With reduce motion on, there is no motion: the new child and its size arrive at once. ## Cost - At rest it's one plain glass surface. - While it grows, it's a [group](https://g1455.plugfox.dev/foundations/groups.md) of two shapes, and a capture on every frame, because the glass changes size. Wrap a fixed-size ancestor that holds every size the morph takes in a `GlassTravel`, and the motion is drawn from the proxy already held. - `spacing: 0` turns the neck off: a plain resize with the cross-fade, and no group even mid-morph. ## Accessibility - Only the new child is hit and read by a screen reader while the glass moves. - Label the controls inside the panel as you would anywhere else. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; /// A "+" in the corner that flows into a panel of actions. /// Assumes a GlassHost above, e.g. in MaterialApp.builder. class NewThing extends StatefulWidget { const NewThing({super.key}); @override State createState() => _NewThingState(); } class _NewThingState extends State { bool _open = false; @override Widget build(BuildContext context) => Align( // The parent holds the top-right corner, and so does the glass. alignment: Alignment.topRight, child: GlassMorph( alignment: Alignment.topRight, borderRadius: _open ? const BorderRadius.all(Radius.circular(28)) : kGlassCapsule, child: _open ? Column( key: const ValueKey('panel'), mainAxisSize: MainAxisSize.min, children: [ for (final String name in ['Note', 'List', 'Photo']) SizedBox( width: 200, child: TextButton(onPressed: () => setState(() => _open = false), child: Text(name)), ), ], ) : IconButton( key: const ValueKey('plus'), tooltip: 'New', onPressed: () => setState(() => _open = true), icon: const Icon(Icons.add), ), ), ); } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `child` | `Widget` | **required** | What the glass holds and measures. A child of another type or key starts a morph. | | `alignment` | `AlignmentGeometry` | `Alignment.center` | The point that holds still in the morph's box. The parent has to hold it on screen. | | `width` | `double?` | `null` | Pins the glass's width; the child is laid out at it. Null measures. | | `height` | `double?` | `null` | Pins the glass's height; the child is laid out at it. Null measures. | | `borderRadius` | `BorderRadius` | `24` all round | The corners around this child. `kGlassCapsule` is a pill at any size. | | `motion` | `GlassMorphMotion` | `GlassMorphMotion.fluid` | The spring. `GlassMorphMotion.calm` has no overshoot. | | `spacing` | `double` | `kGlassMorphSpacing` (16) | How far the neck reaches while the glass moves. Zero: no neck and no group. | | `finish` | `GlassFinish?` | `null` | The glass. Null takes the theme's. | | `labelled` | `bool` | `true` | Whether a label is drawn over the glass, so the theme's label floor applies. | | `onEnd` | `VoidCallback?` | `null` | Called when a morph has settled. | --- # Scaffold > GlassScaffold is a whole screen wired the recommended way: a top bar in a scroll edge, an optional bottom bar and floating action, and a body that scrolls under them. - Live: https://g1455.plugfox.dev/components/scaffold - API: [`GlassScaffold`](https://pub.dev/documentation/g1455/latest/g1455/GlassScaffold-class.html), [`kGlassScaffoldBarHeight`](https://pub.dev/documentation/g1455/latest/g1455/kGlassScaffoldBarHeight-constant.html), [`kGlassScaffoldBarMargin`](https://pub.dev/documentation/g1455/latest/g1455/kGlassScaffoldBarMargin-constant.html), [`kGlassScaffoldActionGap`](https://pub.dev/documentation/g1455/latest/g1455/kGlassScaffoldActionGap-constant.html) - Source: [`lib/src/surface/glass_scaffold.dart`](https://github.com/PlugFox/g1455/blob/master/lib/src/surface/glass_scaffold.dart) `GlassScaffold` lays out a screen of glass: a [bar](https://g1455.plugfox.dev/components/bar.md) at the top in a soft [scroll edge](https://g1455.plugfox.dev/foundations/scroll-edge.md), an optional bottom bar such as a [tab bar](https://g1455.plugfox.dev/components/tab-bar.md), an optional floating action, and a body that scrolls **under** all of them. It is composition and nothing else: every pixel is drawn by a widget you could place yourself. It writes the arrangement once, so you don't have to measure the bars and pad the list by hand. ## When to use - A screen with a scrolling list or grid under a top bar, with or without a tab bar. - **Not** for a screen whose glass doesn't sit at the edges, such as a full-screen map with a floating card. Use a `Stack` there. - **Not** instead of a host for dialogs and sheets: those are built in the navigator's overlay and need the host above the navigator (see below). ## Usage ```dart GlassScaffold( topBar: const GlassBar( child: Row( children: [Icon(Icons.arrow_back), SizedBox(width: 12), Text('Library')], ), ), bottomBar: GlassTabBar( items: const [ GlassTabItem(icon: Icons.home, label: 'Home'), GlassTabItem(icon: Icons.search, label: 'Search'), ], selectedIndex: tab, onSelected: (int i) => setState(() => tab = i), ), // No padding: the list takes it from the media query. body: ListView.builder(itemCount: 50, itemBuilder: buildRow), ) ``` ## The body The body is laid out under the whole scaffold, so content scrolls under the glass. It is told the bars' extents as `MediaQuery.padding`, the way Flutter's `Scaffold` does with `extendBody`. A `ListView`, `GridView` or `CustomScrollView` with no padding of its own takes it from there: its first row starts below the top bar and its last ends above the bottom bar. A body that isn't a scroll view can read the same padding, or wrap itself in a `SafeArea`. The keyboard is not handled: the body sees `MediaQuery.viewInsets` as it is. ## The bars - **The top bar** is laid out `topBarHeight` tall (`kGlassScaffoldBarHeight`, 56), inside `barMargin` and the safe area. It is declared rather than measured because the scroll edge is laid out from it. `scrollEdge: null` drops the edge and keeps the bar lifted. - **The bottom bar** takes its own height, which is measured: a tab bar is 60 tall on a phone and 44 on a wide screen. - **The floating action** sits `kGlassScaffoldActionGap` (16) from the end edge and above the bottom bar. It is on the left in a right-to-left app. - All three are [lifted](https://g1455.plugfox.dev/foundations/above.md), so glass cards scrolling under them show through. ## The host With `host: null`, the default, the scaffold mounts a [GlassHost](https://g1455.plugfox.dev/foundations/host.md) only when there is none above it, and that host takes every default. `host: true` always mounts one, `host: false` never does. > [!WARNING] > Dialogs, sheets, menus and popovers are built in the navigator's overlay, which a host inside the route does not > reach. For an app that uses any of them, put your own host in `MaterialApp(builder: ...)`, as in > [Installation](https://g1455.plugfox.dev/start/installation.md), and the scaffold uses it. That is also where you declare the backdrop, the > finish and the rest. The demo above is inside the site, whose host is above, so the scaffold mounts none. ## Cost It costs what the same screen built by hand costs. While the body scrolls, the content under the bars changes on every frame, so every frame of a scroll is one capture, for every glass on the screen at once. A still screen keeps its capture. Being lifted is free over plain content, and one more snapshot per recorded frame over glass cards. ## Complete example ```dart import 'package:flutter/material.dart'; import 'package:g1455/g1455.dart'; void main() => runApp(const LibraryApp()); class LibraryApp extends StatelessWidget { const LibraryApp({super.key}); @override Widget build(BuildContext context) => MaterialApp( theme: ThemeData.dark(), // The app's host, above the navigator: the scaffold uses it, and so do // dialogs and sheets. builder: (BuildContext context, Widget? child) => GlassHost( backdrop: const Color(0xFF101014), richBackdrop: true, minLabelContrast: kTextContrastAA, child: child!, ), home: const LibraryPage(), ); } class LibraryPage extends StatefulWidget { const LibraryPage({super.key}); @override State createState() => _LibraryPageState(); } class _LibraryPageState extends State { static const List _tabs = [ GlassTabItem(icon: Icons.photo_library, label: 'Library'), GlassTabItem(icon: Icons.favorite, label: 'Saved'), GlassTabItem(icon: Icons.search, label: 'Search'), ]; int _tab = 0; int _count = 30; @override Widget build(BuildContext context) => Scaffold( backgroundColor: const Color(0xFF101014), body: GlassScaffold( topBar: GlassBar( child: Row( children: [ Expanded( child: Text(_tabs[_tab].label, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600)), ), const Icon(Icons.more_horiz), ], ), ), bottomBar: GlassTabBar(items: _tabs, selectedIndex: _tab, onSelected: (int i) => setState(() => _tab = i)), floatingAction: GlassButton( onPressed: () => setState(() => _count++), semanticLabel: 'Add', padding: const EdgeInsets.all(14), child: const Icon(Icons.add), ), // Starts below the top bar, ends above the tab bar, scrolls under both. body: ListView.builder( itemCount: _count, itemBuilder: (BuildContext context, int i) => Container( height: 96, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), gradient: LinearGradient( colors: [ HSVColor.fromAHSV(1, (i * 37) % 360.0, 0.7, 0.9).toColor(), HSVColor.fromAHSV(1, (i * 37 + 60) % 360.0, 0.8, 0.5).toColor(), ], ), ), ), ), ), ); } ``` ## API | Parameter | Type | Default | Description | |---|---|---|---| | `body` | `Widget` | **required** | The content. Laid out under the whole scaffold and told the bars' extents through `MediaQuery.padding`. | | `topBar` | `Widget?` | `null` | The bar at the top, usually a `GlassBar`. Null for no top bar and no scroll edge. | | `topBarHeight` | `double` | `kGlassScaffoldBarHeight` (56) | The height the top bar is laid out in. Must be ≥ 0. | | `scrollEdge` | `GlassScrollEdgeStyle?` | `GlassScrollEdgeStyle.soft` | The scroll edge under the top bar. Null for none: the bar is only lifted. | | `bottomBar` | `Widget?` | `null` | The bar at the bottom, usually a `GlassTabBar`, at its own height. Lifted. | | `floatingAction` | `Widget?` | `null` | A control at the end edge above the bottom bar, usually a `GlassButton`, at its own size. Lifted. | | `barMargin` | `EdgeInsets` | `kGlassScaffoldBarMargin` | The space around each bar, inside the safe area. Under the bottom bar, the larger of this and the safe area. | | `host` | `bool?` | `null` | Whether to mount a `GlassHost`: null when there is none above, `true` always, `false` never. | | `key` | `Key?` | `null` | | `double topExtentFor(EdgeInsets safe)`: the top bar's extent from the top of the screen, which the body is told as its top padding: the safe area, `barMargin` above and below, and `topBarHeight`. ### Constants | Name | Value | Description | |---|---|---| | `kGlassScaffoldBarHeight` | `56` | The default `topBarHeight`. Material's toolbar height. | | `kGlassScaffoldBarMargin` | `EdgeInsets.fromLTRB(12, 8, 12, 8)` | The default `barMargin`. | | `kGlassScaffoldActionGap` | `16` | How far the floating action stands from the end edge and from the bottom bar. |