7c9627fffb
Changelog: Optional two-tier Liquid Glass and slide-to-pick menus
445 lines
13 KiB
Dart
445 lines
13 KiB
Dart
import 'dart:ui' as ui;
|
|
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
|
|
const _red = Color(0xFFFF0000);
|
|
const _green = Color(0xFF00FF00);
|
|
const _blue = Color(0xFF0000FF);
|
|
const _clear = LiquidGlassStyle(tint: Colors.transparent, blur: 0);
|
|
|
|
typedef _Pane = ({
|
|
Rect pane,
|
|
Rect slot,
|
|
Rect place,
|
|
double scale,
|
|
int level,
|
|
ui.Image atlas,
|
|
});
|
|
|
|
Widget _app(
|
|
Widget child, {
|
|
bool enabled = true,
|
|
LiquidGlassQuality quality = LiquidGlassQuality.ultra,
|
|
}) => MaterialApp(
|
|
home: LiquidGlassScope(
|
|
enabled: true,
|
|
quality: quality,
|
|
child: LiquidGlassHost(enabled: enabled, child: child),
|
|
),
|
|
);
|
|
|
|
Widget _pane(Rect rect, {LiquidGlassStyle style = _clear, Widget? child}) =>
|
|
Positioned.fromRect(
|
|
rect: rect,
|
|
child: LiquidGlass(style: style, child: child),
|
|
);
|
|
|
|
LiquidGlassCapture _capture() => LiquidGlassHost.debugLastCapture!;
|
|
|
|
class _HeldPane extends LeafRenderObjectWidget {
|
|
const _HeldPane(this.rect, this.hold);
|
|
|
|
final Rect rect;
|
|
final Rect hold;
|
|
|
|
@override
|
|
RenderObject createRenderObject(BuildContext context) =>
|
|
_RenderHeldPane(rect, hold);
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, _RenderHeldPane renderObject) {
|
|
renderObject
|
|
..rect = rect
|
|
..hold = hold
|
|
..markNeedsPaint();
|
|
}
|
|
}
|
|
|
|
class _RenderHeldPane extends RenderBox {
|
|
_RenderHeldPane(this.rect, this.hold);
|
|
|
|
Rect rect;
|
|
Rect hold;
|
|
late final _pane = LiquidGlassPane(markNeedsPaint);
|
|
|
|
@override
|
|
bool get sizedByParent => true;
|
|
|
|
@override
|
|
bool get alwaysNeedsCompositing => true;
|
|
|
|
@override
|
|
Size computeDryLayout(BoxConstraints constraints) => constraints.biggest;
|
|
|
|
@override
|
|
void dispose() {
|
|
_pane.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
_pane.paint(
|
|
context,
|
|
rect.shift(offset),
|
|
shape: LiquidGlassCorners.circular(0),
|
|
style: _clear,
|
|
hold: hold.shift(offset),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<Color> _behind(WidgetTester tester, _Pane pane, Offset point) async {
|
|
final texel = pane.place.topLeft + (point - pane.slot.topLeft) * pane.scale;
|
|
final bytes = (await tester.runAsync(pane.atlas.toByteData))!;
|
|
final offset = (texel.dy.floor() * pane.atlas.width + texel.dx.floor()) * 4;
|
|
return Color.fromARGB(
|
|
bytes.getUint8(offset + 3),
|
|
bytes.getUint8(offset),
|
|
bytes.getUint8(offset + 1),
|
|
bytes.getUint8(offset + 2),
|
|
);
|
|
}
|
|
|
|
void main() {
|
|
setUp(() => LiquidGlassHost.debugLastCapture = null);
|
|
|
|
testWidgets('a pane reads what lies behind it and not what it holds', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
_app(
|
|
Stack(
|
|
children: [
|
|
const Positioned.fill(child: ColoredBox(color: _red)),
|
|
_pane(
|
|
const Rect.fromLTWH(100, 100, 200, 100),
|
|
child: const ColoredBox(color: _blue),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
final pane = _capture().panes.single;
|
|
expect(pane.pane, const Rect.fromLTWH(100, 100, 200, 100));
|
|
expect(pane.level, 0);
|
|
expect(await _behind(tester, pane, const Offset(200, 150)), _red);
|
|
});
|
|
|
|
testWidgets('a frame that leaves the backdrop as it was keeps the capture', (
|
|
tester,
|
|
) async {
|
|
Widget scene(Color behind) => _app(
|
|
Stack(
|
|
children: [
|
|
Positioned.fill(child: ColoredBox(color: behind)),
|
|
_pane(const Rect.fromLTWH(100, 100, 200, 100)),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(scene(_red));
|
|
final first = _capture().panes.single.atlas;
|
|
expect(_capture().snapshots, 1);
|
|
|
|
tester.binding.scheduleFrame();
|
|
await tester.pump();
|
|
expect(_capture().snapshots, 0);
|
|
expect(_capture().panes.single.atlas, same(first));
|
|
|
|
await tester.pumpWidget(scene(_green));
|
|
expect(_capture().snapshots, 1);
|
|
final pane = _capture().panes.single;
|
|
expect(await _behind(tester, pane, const Offset(200, 150)), _green);
|
|
});
|
|
|
|
testWidgets('a pane moving within its hold keeps one capture', (
|
|
tester,
|
|
) async {
|
|
const hold = Rect.fromLTWH(50, 50, 400, 300);
|
|
Widget scene(Rect rect) => _app(
|
|
Stack(
|
|
children: [
|
|
const Positioned.fill(
|
|
child: RepaintBoundary(child: ColoredBox(color: _red)),
|
|
),
|
|
Positioned.fill(child: _HeldPane(rect, hold)),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.pumpWidget(scene(const Rect.fromLTWH(100, 100, 50, 50)));
|
|
expect(_capture().snapshots, 1);
|
|
final first = _capture().panes.single;
|
|
expect(first.slot.contains(hold.bottomRight - const Offset(1, 1)), isTrue);
|
|
|
|
await tester.pumpWidget(scene(const Rect.fromLTWH(200, 150, 200, 150)));
|
|
expect(_capture().snapshots, 0);
|
|
expect(_capture().panes.single.slot, first.slot);
|
|
expect(_capture().panes.single.atlas, same(first.atlas));
|
|
|
|
await tester.pumpWidget(scene(const Rect.fromLTWH(500, 500, 50, 50)));
|
|
expect(_capture().snapshots, 1);
|
|
});
|
|
|
|
testWidgets('a pane over another reads a capture with that one drawn in', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
_app(
|
|
Stack(
|
|
children: [
|
|
const Positioned.fill(child: ColoredBox(color: _red)),
|
|
_pane(
|
|
const Rect.fromLTWH(100, 100, 200, 100),
|
|
style: const LiquidGlassStyle(tint: _blue, blur: 0),
|
|
),
|
|
_pane(const Rect.fromLTWH(250, 150, 100, 100)),
|
|
_pane(const Rect.fromLTWH(500, 400, 100, 100)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
final [lower, upper, apart] = _capture().panes;
|
|
expect([lower.level, upper.level, apart.level], [0, 1, 0]);
|
|
expect(upper.atlas, isNot(same(lower.atlas)));
|
|
expect(apart.atlas, same(lower.atlas));
|
|
final over = await _behind(tester, upper, const Offset(275, 175));
|
|
expect(over.b, greaterThan(over.r));
|
|
expect(await _behind(tester, upper, const Offset(325, 225)), _red);
|
|
});
|
|
|
|
testWidgets('a pane stays on its level beside one past its frost', (
|
|
tester,
|
|
) async {
|
|
const frosted = LiquidGlassStyle(tint: Colors.transparent);
|
|
Future<int> levelBeside(double gap) async {
|
|
await tester.pumpWidget(
|
|
_app(
|
|
Stack(
|
|
children: [
|
|
const Positioned.fill(child: ColoredBox(color: _red)),
|
|
_pane(const Rect.fromLTWH(100, 100, 200, 100), style: frosted),
|
|
_pane(Rect.fromLTWH(300 + gap, 100, 100, 100), style: frosted),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
return _capture().panes.last.level;
|
|
}
|
|
|
|
expect(await levelBeside(8), 0);
|
|
expect(await levelBeside(4), 1);
|
|
});
|
|
|
|
Widget bandsScene(Color changing) => _app(
|
|
Stack(
|
|
children: [
|
|
Positioned(
|
|
left: 0,
|
|
top: 0,
|
|
width: 400,
|
|
height: 300,
|
|
child: RepaintBoundary(child: ColoredBox(color: changing)),
|
|
),
|
|
const Positioned(
|
|
left: 0,
|
|
top: 300,
|
|
width: 400,
|
|
height: 300,
|
|
child: RepaintBoundary(child: ColoredBox(color: _blue)),
|
|
),
|
|
_pane(const Rect.fromLTWH(100, 100, 200, 100)),
|
|
_pane(
|
|
const Rect.fromLTWH(100, 420, 200, 100),
|
|
style: const LiquidGlassStyle(tint: Colors.transparent, blur: 12),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
testWidgets('a level redraws only the bands whose backdrop changed', (
|
|
tester,
|
|
) async {
|
|
LiquidGlassHost.debugCarryBandsOverride = true;
|
|
addTearDown(() => LiquidGlassHost.debugCarryBandsOverride = null);
|
|
|
|
await tester.pumpWidget(bandsScene(_red));
|
|
expect(_capture().snapshots, 1);
|
|
expect(_capture().keptBands, 0);
|
|
|
|
await tester.pumpWidget(bandsScene(_green));
|
|
expect(_capture().snapshots, 1);
|
|
expect(_capture().keptBands, 1);
|
|
final [changed, kept] = _capture().panes;
|
|
expect(await _behind(tester, changed, const Offset(200, 150)), _green);
|
|
expect(await _behind(tester, kept, const Offset(200, 470)), _blue);
|
|
});
|
|
|
|
testWidgets(
|
|
'without Impeller a level redraws every band, copying from no capture',
|
|
(tester) async {
|
|
await tester.pumpWidget(bandsScene(_red));
|
|
await tester.pumpWidget(bandsScene(_green));
|
|
expect(_capture().snapshots, 1);
|
|
expect(_capture().keptBands, 0);
|
|
final [changed, kept] = _capture().panes;
|
|
expect(await _behind(tester, changed, const Offset(200, 150)), _green);
|
|
expect(await _behind(tester, kept, const Offset(200, 470)), _blue);
|
|
},
|
|
skip: ui.ImageFilter.isShaderFilterSupported,
|
|
);
|
|
|
|
testWidgets('the standard tier frosts a hazed pane through its haze alone', (
|
|
tester,
|
|
) async {
|
|
const hazed = LiquidGlassStyle(
|
|
tint: Colors.transparent,
|
|
blur: 0,
|
|
haze: 0.72,
|
|
hazeBlur: 15.6,
|
|
);
|
|
Future<double> scaleAt(LiquidGlassQuality quality) async {
|
|
await tester.pumpWidget(
|
|
_app(
|
|
Stack(
|
|
children: [
|
|
const Positioned.fill(child: ColoredBox(color: _red)),
|
|
_pane(const Rect.fromLTWH(100, 100, 200, 100), style: hazed),
|
|
],
|
|
),
|
|
quality: quality,
|
|
),
|
|
);
|
|
return _capture().panes.single.scale;
|
|
}
|
|
|
|
expect(
|
|
await scaleAt(LiquidGlassQuality.ultra),
|
|
tester.view.devicePixelRatio,
|
|
);
|
|
expect(await scaleAt(LiquidGlassQuality.standard), lessThan(1));
|
|
});
|
|
|
|
testWidgets('a pane under a layer of its own reads only that layer', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
_app(
|
|
Stack(
|
|
children: [
|
|
const Positioned.fill(child: ColoredBox(color: _red)),
|
|
Positioned.fill(
|
|
child: Opacity(
|
|
opacity: 0.5,
|
|
child: Stack(
|
|
children: [
|
|
const Positioned(
|
|
left: 100,
|
|
top: 100,
|
|
width: 100,
|
|
height: 100,
|
|
child: ColoredBox(color: _green),
|
|
),
|
|
_pane(const Rect.fromLTWH(100, 100, 200, 100)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
final pane = _capture().panes.single;
|
|
expect(await _behind(tester, pane, const Offset(150, 150)), _green);
|
|
expect((await _behind(tester, pane, const Offset(250, 150))).a, 0);
|
|
});
|
|
|
|
testWidgets('a pane under a glass fade reads through it', (tester) async {
|
|
await tester.pumpWidget(
|
|
_app(
|
|
Stack(
|
|
children: [
|
|
const Positioned.fill(child: ColoredBox(color: _red)),
|
|
Positioned.fill(
|
|
child: LiquidGlassFade(
|
|
opacity: const AlwaysStoppedAnimation(0.5),
|
|
child: Stack(
|
|
children: [
|
|
const Positioned(
|
|
left: 100,
|
|
top: 100,
|
|
width: 100,
|
|
height: 100,
|
|
child: ColoredBox(color: _green),
|
|
),
|
|
_pane(const Rect.fromLTWH(100, 100, 200, 100)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
final pane = _capture().panes.single;
|
|
final faded = await _behind(tester, pane, const Offset(150, 150));
|
|
expect(faded.r, inExclusiveRange(0, 1));
|
|
expect(faded.g, inExclusiveRange(0, 1));
|
|
expect(await _behind(tester, pane, const Offset(250, 150)), _red);
|
|
});
|
|
|
|
testWidgets('a frosted pane reads a capture that holds its blur', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
_app(
|
|
Stack(
|
|
children: [
|
|
const Positioned.fill(child: ColoredBox(color: _red)),
|
|
const Positioned(
|
|
left: 200,
|
|
top: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
child: ColoredBox(color: _blue),
|
|
),
|
|
_pane(
|
|
const Rect.fromLTWH(100, 100, 200, 100),
|
|
style: const LiquidGlassStyle(tint: Colors.transparent, blur: 12),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
|
|
final pane = _capture().panes.single;
|
|
expect(pane.scale, lessThan(1));
|
|
final near = await _behind(tester, pane, const Offset(194, 150));
|
|
expect(near.b, inExclusiveRange(0.15, 0.5));
|
|
expect(await _behind(tester, pane, const Offset(120, 150)), _red);
|
|
});
|
|
|
|
testWidgets('a host turned off captures nothing', (tester) async {
|
|
await tester.pumpWidget(
|
|
_app(
|
|
Stack(
|
|
children: [
|
|
const Positioned.fill(child: ColoredBox(color: _red)),
|
|
_pane(const Rect.fromLTWH(100, 100, 200, 100)),
|
|
],
|
|
),
|
|
enabled: false,
|
|
),
|
|
);
|
|
|
|
expect(LiquidGlassHost.debugLastCapture, isNull);
|
|
});
|
|
}
|