409 lines
10 KiB
Dart
409 lines
10 KiB
Dart
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
|
|
import '../../core/config/app_frost.dart';
|
|
import '../../core/config/app_liquid_glass.dart';
|
|
import '../../core/config/app_visual_style.dart';
|
|
|
|
class LiquidGlass {
|
|
static const String _asset = 'shaders/liquid_glass.frag';
|
|
|
|
static ui.FragmentProgram? _program;
|
|
static bool _loadAttempted = false;
|
|
|
|
static bool get isSupported => _program != null;
|
|
|
|
static bool get active =>
|
|
isSupported && AppVisualStyle.current.value == VisualStyle.liquidGlass;
|
|
|
|
static Future<void> load() async {
|
|
if (_loadAttempted) return;
|
|
_loadAttempted = true;
|
|
if (!AppLiquidGlass.enabled) return;
|
|
if (!ui.ImageFilter.isShaderFilterSupported) return;
|
|
try {
|
|
_program = await ui.FragmentProgram.fromAsset(_asset);
|
|
} catch (_) {
|
|
_program = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
class GlassSurface extends StatelessWidget {
|
|
final bool liquid;
|
|
final BorderRadius borderRadius;
|
|
final Color frostTint;
|
|
final double frostSigma;
|
|
final Color liquidTint;
|
|
final BoxBorder? border;
|
|
final BackdropKey? backdropKey;
|
|
final Widget child;
|
|
|
|
const GlassSurface({
|
|
super.key,
|
|
this.liquid = false,
|
|
this.borderRadius = BorderRadius.zero,
|
|
required this.frostTint,
|
|
this.frostSigma = AppFrost.sigma,
|
|
this.liquidTint = Colors.transparent,
|
|
this.border,
|
|
this.backdropKey,
|
|
required this.child,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final glass = liquid && LiquidGlass.isSupported;
|
|
final decorated = DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
color: glass ? null : frostTint,
|
|
border: border,
|
|
),
|
|
child: child,
|
|
);
|
|
if (glass) {
|
|
return LiquidGlassSurface(
|
|
borderRadius: borderRadius,
|
|
tint: liquidTint,
|
|
child: decorated,
|
|
);
|
|
}
|
|
return ClipRRect(
|
|
borderRadius: borderRadius,
|
|
child: BackdropFilter(
|
|
filter: ui.ImageFilter.blur(sigmaX: frostSigma, sigmaY: frostSigma),
|
|
backdropGroupKey: backdropKey,
|
|
child: decorated,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class LiquidGlassSurface extends StatelessWidget {
|
|
final BorderRadius borderRadius;
|
|
final Color tint;
|
|
final double blurSigma;
|
|
final double spread;
|
|
final double refraction;
|
|
final double chroma;
|
|
final double specular;
|
|
final Offset light;
|
|
final double tintFeather;
|
|
final double rimWidth;
|
|
final Widget child;
|
|
|
|
const LiquidGlassSurface({
|
|
super.key,
|
|
required this.borderRadius,
|
|
required this.tint,
|
|
this.blurSigma = AppLiquidGlass.blurSigma,
|
|
this.spread = AppLiquidGlass.spread,
|
|
this.refraction = AppLiquidGlass.refraction,
|
|
this.chroma = AppLiquidGlass.chroma,
|
|
this.specular = AppLiquidGlass.specular,
|
|
this.light = AppLiquidGlass.light,
|
|
this.tintFeather = AppLiquidGlass.tintFeather,
|
|
this.rimWidth = AppLiquidGlass.rimWidth,
|
|
this.child = const SizedBox.expand(),
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!LiquidGlass.isSupported) return child;
|
|
return _LiquidGlassBackdrop(
|
|
borderRadius: borderRadius,
|
|
tint: tint,
|
|
blurSigma: blurSigma,
|
|
spread: spread,
|
|
refraction: refraction,
|
|
chroma: chroma,
|
|
specular: specular,
|
|
light: light,
|
|
tintFeather: tintFeather,
|
|
rimWidth: rimWidth,
|
|
devicePixelRatio: MediaQuery.devicePixelRatioOf(context),
|
|
child: child,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _LiquidGlassBackdrop extends SingleChildRenderObjectWidget {
|
|
final BorderRadius borderRadius;
|
|
final Color tint;
|
|
final double blurSigma;
|
|
final double spread;
|
|
final double refraction;
|
|
final double chroma;
|
|
final double specular;
|
|
final Offset light;
|
|
final double tintFeather;
|
|
final double rimWidth;
|
|
final double devicePixelRatio;
|
|
|
|
const _LiquidGlassBackdrop({
|
|
required this.borderRadius,
|
|
required this.tint,
|
|
required this.blurSigma,
|
|
required this.spread,
|
|
required this.refraction,
|
|
required this.chroma,
|
|
required this.specular,
|
|
required this.light,
|
|
required this.tintFeather,
|
|
required this.rimWidth,
|
|
required this.devicePixelRatio,
|
|
required super.child,
|
|
});
|
|
|
|
@override
|
|
_RenderLiquidGlass createRenderObject(BuildContext context) {
|
|
return _RenderLiquidGlass(
|
|
borderRadius: borderRadius,
|
|
tint: tint,
|
|
blurSigma: blurSigma,
|
|
spread: spread,
|
|
refraction: refraction,
|
|
chroma: chroma,
|
|
specular: specular,
|
|
light: light,
|
|
tintFeather: tintFeather,
|
|
rimWidth: rimWidth,
|
|
devicePixelRatio: devicePixelRatio,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(
|
|
BuildContext context,
|
|
_RenderLiquidGlass renderObject,
|
|
) {
|
|
renderObject
|
|
..borderRadius = borderRadius
|
|
..tint = tint
|
|
..blurSigma = blurSigma
|
|
..spread = spread
|
|
..refraction = refraction
|
|
..chroma = chroma
|
|
..specular = specular
|
|
..light = light
|
|
..tintFeather = tintFeather
|
|
..rimWidth = rimWidth
|
|
..devicePixelRatio = devicePixelRatio;
|
|
}
|
|
}
|
|
|
|
class _RenderLiquidGlass extends RenderProxyBox {
|
|
_RenderLiquidGlass({
|
|
required BorderRadius borderRadius,
|
|
required Color tint,
|
|
required double blurSigma,
|
|
required double spread,
|
|
required double refraction,
|
|
required double chroma,
|
|
required double specular,
|
|
required Offset light,
|
|
required double tintFeather,
|
|
required double rimWidth,
|
|
required double devicePixelRatio,
|
|
}) : _borderRadius = borderRadius,
|
|
_tint = tint,
|
|
_blurSigma = blurSigma,
|
|
_spread = spread,
|
|
_refraction = refraction,
|
|
_chroma = chroma,
|
|
_specular = specular,
|
|
_light = light,
|
|
_tintFeather = tintFeather,
|
|
_rimWidth = rimWidth,
|
|
_devicePixelRatio = devicePixelRatio;
|
|
|
|
final LayerHandle<ClipRRectLayer> _blurClipHandle =
|
|
LayerHandle<ClipRRectLayer>();
|
|
final LayerHandle<BackdropFilterLayer> _blurHandle =
|
|
LayerHandle<BackdropFilterLayer>();
|
|
final LayerHandle<ClipRRectLayer> _clipHandle = LayerHandle<ClipRRectLayer>();
|
|
final LayerHandle<BackdropFilterLayer> _backdropHandle =
|
|
LayerHandle<BackdropFilterLayer>();
|
|
|
|
ui.FragmentShader? _shader;
|
|
|
|
BorderRadius _borderRadius;
|
|
set borderRadius(BorderRadius value) {
|
|
if (_borderRadius == value) return;
|
|
_borderRadius = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
Color _tint;
|
|
set tint(Color value) {
|
|
if (_tint == value) return;
|
|
_tint = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
double _blurSigma;
|
|
set blurSigma(double value) {
|
|
if (_blurSigma == value) return;
|
|
_blurSigma = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
double _spread;
|
|
set spread(double value) {
|
|
if (_spread == value) return;
|
|
_spread = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
double _refraction;
|
|
set refraction(double value) {
|
|
if (_refraction == value) return;
|
|
_refraction = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
double _chroma;
|
|
set chroma(double value) {
|
|
if (_chroma == value) return;
|
|
_chroma = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
double _specular;
|
|
set specular(double value) {
|
|
if (_specular == value) return;
|
|
_specular = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
Offset _light;
|
|
set light(Offset value) {
|
|
if (_light == value) return;
|
|
_light = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
double _tintFeather;
|
|
set tintFeather(double value) {
|
|
if (_tintFeather == value) return;
|
|
_tintFeather = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
double _rimWidth;
|
|
set rimWidth(double value) {
|
|
if (_rimWidth == value) return;
|
|
_rimWidth = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
double _devicePixelRatio;
|
|
set devicePixelRatio(double value) {
|
|
if (_devicePixelRatio == value) return;
|
|
_devicePixelRatio = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
@override
|
|
bool get alwaysNeedsCompositing => true;
|
|
|
|
@override
|
|
void dispose() {
|
|
_blurClipHandle.layer = null;
|
|
_blurHandle.layer = null;
|
|
_clipHandle.layer = null;
|
|
_backdropHandle.layer = null;
|
|
_shader?.dispose();
|
|
_shader = null;
|
|
super.dispose();
|
|
}
|
|
|
|
ui.ImageFilter? _buildFilter() {
|
|
final program = LiquidGlass._program;
|
|
if (program == null) return null;
|
|
|
|
final shader = _shader ??= program.fragmentShader();
|
|
final dpr = _devicePixelRatio;
|
|
final topLeft = localToGlobal(Offset.zero);
|
|
final left = (topLeft.dx * dpr).roundToDouble();
|
|
final top = (topLeft.dy * dpr).roundToDouble();
|
|
final width = (size.width * dpr).roundToDouble();
|
|
final height = (size.height * dpr).roundToDouble();
|
|
final radius = _borderRadius.topLeft.x * dpr;
|
|
|
|
shader
|
|
..setFloat(2, left)
|
|
..setFloat(3, top)
|
|
..setFloat(4, width)
|
|
..setFloat(5, height)
|
|
..setFloat(6, radius)
|
|
..setFloat(7, _spread)
|
|
..setFloat(8, _refraction * dpr)
|
|
..setFloat(9, _chroma)
|
|
..setFloat(10, _specular)
|
|
..setFloat(11, _tint.r)
|
|
..setFloat(12, _tint.g)
|
|
..setFloat(13, _tint.b)
|
|
..setFloat(14, _tint.a)
|
|
..setFloat(15, _light.dx)
|
|
..setFloat(16, _light.dy)
|
|
..setFloat(17, _tintFeather * dpr)
|
|
..setFloat(18, _rimWidth * dpr);
|
|
|
|
return ui.ImageFilter.shader(shader);
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
final filter = size.isEmpty ? null : _buildFilter();
|
|
if (filter == null) {
|
|
_blurClipHandle.layer = null;
|
|
_blurHandle.layer = null;
|
|
_clipHandle.layer = null;
|
|
_backdropHandle.layer = null;
|
|
super.paint(context, offset);
|
|
return;
|
|
}
|
|
|
|
final bounds = Offset.zero & size;
|
|
final shape = _borderRadius.toRRect(bounds);
|
|
|
|
if (_blurSigma > 0) {
|
|
_blurClipHandle.layer = context.pushClipRRect(
|
|
needsCompositing,
|
|
offset,
|
|
bounds,
|
|
shape,
|
|
(PaintingContext innerContext, Offset innerOffset) {
|
|
final blur = _blurHandle.layer ??= BackdropFilterLayer();
|
|
blur.filter = ui.ImageFilter.blur(
|
|
sigmaX: _blurSigma,
|
|
sigmaY: _blurSigma,
|
|
tileMode: TileMode.mirror,
|
|
);
|
|
innerContext.pushLayer(blur, (_, _) {}, innerOffset);
|
|
},
|
|
oldLayer: _blurClipHandle.layer,
|
|
);
|
|
} else {
|
|
_blurClipHandle.layer = null;
|
|
_blurHandle.layer = null;
|
|
}
|
|
|
|
_clipHandle.layer = context.pushClipRRect(
|
|
needsCompositing,
|
|
offset,
|
|
bounds,
|
|
shape,
|
|
(PaintingContext innerContext, Offset innerOffset) {
|
|
final backdrop = _backdropHandle.layer ??= BackdropFilterLayer();
|
|
backdrop.filter = filter;
|
|
innerContext.pushLayer(backdrop, super.paint, innerOffset);
|
|
},
|
|
oldLayer: _clipHandle.layer,
|
|
);
|
|
}
|
|
}
|