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, required this.child, }); final double strength; final Widget child; @override State createState() => _GlassRefractionState(); } class _GlassRefractionState extends State { static Future? _program; ui.FragmentShader? _shader; @override void initState() { super.initState(); if (ui.ImageFilter.isShaderFilterSupported) _load(); } Future _load() async { try { final program = await (_program ??= ui.FragmentProgram.fromAsset( 'shaders/liquid_glass.frag', )); if (mounted) setState(() => _shader = program.fragmentShader()); } catch (error) { _program = 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: 0.6, sigmaY: 0.6), child: widget.child, ) : _LiveGlassFilter( shader: _shader!, strength: widget.strength, pixelRatio: MediaQuery.devicePixelRatioOf(context), child: widget.child, ); } class _LiveGlassFilter extends SingleChildRenderObjectWidget { const _LiveGlassFilter({ required this.shader, required this.strength, required this.pixelRatio, required super.child, }); final ui.FragmentShader shader; final double strength, pixelRatio; @override _RenderLiveGlass createRenderObject(BuildContext context) => _RenderLiveGlass(shader, strength, pixelRatio); @override void updateRenderObject( BuildContext context, _RenderLiveGlass renderObject, ) => renderObject.update(shader, strength, pixelRatio); } class _RenderLiveGlass extends RenderProxyBox { _RenderLiveGlass(this._shader, this._strength, this._pixelRatio); ui.FragmentShader _shader; double _strength, _pixelRatio; final _layer = LayerHandle(); void update(ui.FragmentShader shader, double strength, double pixelRatio) { _shader = shader; _strength = strength; _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(); _layer.layer!.filter = ui.ImageFilter.shader(_shader); context.pushLayer(_layer.layer!, super.paint, offset); } @override void dispose() { _layer.layer = null; super.dispose(); } }