import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; /// Filters live pixels beneath the glass; symbols above it remain undistorted. class GlassRefraction extends StatefulWidget { const GlassRefraction({ super.key, required this.strength, this.blurSigma = 2, required this.child, }); final double strength; /// Optical softness in logical pixels, independent of display density. final double blurSigma; final Widget child; @override State createState() => _GlassRefractionState(); } class _GlassRefractionState extends State { static Future? _program; static ui.FragmentProgram? _loadedProgram; ui.FragmentShader? _shader; @override void initState() { super.initState(); if (ui.ImageFilter.isShaderFilterSupported) _load(); } Future _load() async { try { final program = _loadedProgram ??= await (_program ??= ui.FragmentProgram.fromAsset('shaders/liquid_glass.frag')); if (mounted) setState(() => _shader = program.fragmentShader()); } catch (error) { _program = null; _loadedProgram = null; debugPrint('Glass refraction unavailable: $error'); } } @override void dispose() { _shader?.dispose(); super.dispose(); } @override Widget build(BuildContext context) => _shader == null ? BackdropFilter( filter: ui.ImageFilter.blur( sigmaX: widget.blurSigma, sigmaY: widget.blurSigma, tileMode: TileMode.clamp, ), child: widget.child, ) : _LiveGlassFilter( shader: _shader!, strength: widget.strength, blurSigma: widget.blurSigma, pixelRatio: MediaQuery.devicePixelRatioOf(context), child: widget.child, ); } class _LiveGlassFilter extends SingleChildRenderObjectWidget { const _LiveGlassFilter({ required this.shader, required this.strength, required this.blurSigma, required this.pixelRatio, required super.child, }); final ui.FragmentShader shader; final double strength, blurSigma, pixelRatio; @override _RenderLiveGlass createRenderObject(BuildContext context) => _RenderLiveGlass(shader, strength, blurSigma, pixelRatio); @override void updateRenderObject( BuildContext context, _RenderLiveGlass renderObject, ) => renderObject.update(shader, strength, blurSigma, pixelRatio); } class _RenderLiveGlass extends RenderProxyBox { _RenderLiveGlass( this._shader, this._strength, this._blurSigma, this._pixelRatio, ); ui.FragmentShader _shader; double _strength, _blurSigma, _pixelRatio; final _layer = LayerHandle(); void update( ui.FragmentShader shader, double strength, double blurSigma, double pixelRatio, ) { _shader = shader; _strength = strength; _blurSigma = blurSigma; _pixelRatio = pixelRatio; markNeedsPaint(); } @override bool get alwaysNeedsCompositing => child != null; @override void paint(PaintingContext context, Offset offset) { if (child == null) return; // Sample coordinates are physical pixels. Resolve the actual transform // during paint so the glass also follows the Dock's hide/show animation. final origin = localToGlobal(Offset.zero) * _pixelRatio; _shader ..setFloat(2, origin.dx) ..setFloat(3, origin.dy) ..setFloat(4, size.width * _pixelRatio) ..setFloat(5, size.height * _pixelRatio) ..setFloat(6, _strength); _layer.layer ??= BackdropFilterLayer(); // Refract the original backdrop first, then diffuse it with the engine's // separable Gaussian blur. Keeping the shader inside the composition also // keeps its source texture coordinates independent of blur padding. _layer.layer!.filter = ui.ImageFilter.compose( outer: ui.ImageFilter.blur( sigmaX: _blurSigma, sigmaY: _blurSigma, tileMode: TileMode.clamp, ), inner: ui.ImageFilter.shader(_shader), ); context.pushLayer(_layer.layer!, super.paint, offset); } @override void dispose() { _layer.layer = null; super.dispose(); } }