mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 15:02:21 +02:00
fix(mornye): match tab highlights to the expanding glass outline
This commit is contained in:
1 parent
6550104df9
commit
82068263f2
5 files changed
+357
-50
No files matched your search
@@ -0,0 +1,19 @@
|
||||
#include <flutter/runtime_effect.glsl>
|
||||
|
||||
uniform vec4 uRect;
|
||||
uniform vec2 uScale;
|
||||
uniform float uRadius;
|
||||
uniform vec4 uColor;
|
||||
|
||||
out vec4 fragColor;
|
||||
|
||||
void main() {
|
||||
vec2 center = uRect.xy + uRect.zw * 0.5;
|
||||
vec2 halfSize = uRect.zw / uScale * 0.5;
|
||||
vec2 position = (FlutterFragCoord().xy - center) / uScale;
|
||||
vec2 corner = abs(position) - halfSize + vec2(uRadius);
|
||||
float distance = length(max(corner, vec2(0.0)))
|
||||
+ min(max(corner.x, corner.y), 0.0) - uRadius;
|
||||
float coverage = 1.0 - smoothstep(-0.5, 0.5, distance);
|
||||
fragColor = vec4(uColor.rgb * uColor.a, uColor.a) * coverage;
|
||||
}
|
||||
@@ -17,6 +17,98 @@ import 'package:spotiflac_android/providers/runtime_profile_provider.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('held foreground reaches the curved glass edge', (tester) async {
|
||||
final capture = GlobalKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: MornyeTheme.build(Brightness.dark),
|
||||
home: Center(
|
||||
child: RepaintBoundary(
|
||||
key: capture,
|
||||
child: SizedBox(
|
||||
width: 360,
|
||||
child: MornyeSelectionPill(
|
||||
labels: const ['Home', 'Library', 'Repo', 'Search'],
|
||||
selectedIndex: 0,
|
||||
onChanged: (_) {},
|
||||
padding: EdgeInsets.zero,
|
||||
liquidInteraction: true,
|
||||
selectionColor: Colors.red,
|
||||
itemBuilder: (_, _, _) => const SizedBox(
|
||||
height: 64,
|
||||
child: ColoredBox(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.runAsync(() async {
|
||||
await LiquidGlassShaders.ensureLoaded();
|
||||
await ui.FragmentProgram.fromAsset(
|
||||
'assets/shaders/mornye_selection_mask.frag',
|
||||
);
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
final row = tester.getRect(find.byType(MornyeSelectionPill));
|
||||
final gesture = await tester.startGesture(
|
||||
row.topLeft + const Offset(45, 32),
|
||||
);
|
||||
await gesture.moveTo(row.topLeft + const Offset(90, 32));
|
||||
await tester.pump();
|
||||
final lens = find.byWidgetPredicate(
|
||||
(widget) => widget is MornyeLiquidBackdrop && widget.interaction,
|
||||
);
|
||||
for (final duration in [
|
||||
const Duration(milliseconds: 60),
|
||||
const Duration(milliseconds: 240),
|
||||
]) {
|
||||
await tester.pump(duration);
|
||||
final bounds = tester.getRect(lens).shift(-row.topLeft);
|
||||
final pixels = (await tester.runAsync(() async {
|
||||
final boundary = tester.renderObject<RenderRepaintBoundary>(
|
||||
find.byKey(capture),
|
||||
);
|
||||
final image = await boundary.toImage();
|
||||
try {
|
||||
final bytes = (await image.toByteData())!;
|
||||
return [
|
||||
for (final point in [
|
||||
Offset(bounds.left + 2, 32),
|
||||
Offset(bounds.right - 3, 32),
|
||||
Offset(bounds.left + 2, 2),
|
||||
])
|
||||
Color.fromARGB(
|
||||
bytes.getUint8(
|
||||
(point.dy.toInt() * image.width + point.dx.toInt()) * 4 + 3,
|
||||
),
|
||||
bytes.getUint8(
|
||||
(point.dy.toInt() * image.width + point.dx.toInt()) * 4,
|
||||
),
|
||||
bytes.getUint8(
|
||||
(point.dy.toInt() * image.width + point.dx.toInt()) * 4 + 1,
|
||||
),
|
||||
bytes.getUint8(
|
||||
(point.dy.toInt() * image.width + point.dx.toInt()) * 4 + 2,
|
||||
),
|
||||
),
|
||||
];
|
||||
} finally {
|
||||
image.dispose();
|
||||
}
|
||||
}))!;
|
||||
expect(
|
||||
pixels.map((color) => color.toARGB32()),
|
||||
[Colors.red, Colors.red, Colors.white].map((color) => color.toARGB32()),
|
||||
);
|
||||
}
|
||||
await gesture.cancel();
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.binding.hasScheduledFrame, isFalse);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Impeller lens refracts its own bounds and preserves foreground',
|
||||
(tester) async {
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:liquid_glass_easy/liquid_glass_easy.dart';
|
||||
import 'package:spotiflac_android/widgets/mornye_liquid_backdrop.dart';
|
||||
|
||||
Future<ui.FragmentProgram>? _selectionMaskProgram;
|
||||
const _heldWidthGrowth = 0.28;
|
||||
const _heldHeightGrowth = 0.24;
|
||||
const _pillRadius = 32.0;
|
||||
|
||||
/// Selection with a small, transient refractive lens while held. Labels are
|
||||
/// rendered once above the lens; it samples the bar's backdrop on the GPU.
|
||||
class MornyeSelectionPill extends StatefulWidget {
|
||||
@@ -37,7 +44,8 @@ class MornyeSelectionPill extends StatefulWidget {
|
||||
State<MornyeSelectionPill> createState() => _MornyeSelectionPillState();
|
||||
}
|
||||
|
||||
class _MornyeSelectionPillState extends State<MornyeSelectionPill> {
|
||||
class _MornyeSelectionPillState extends State<MornyeSelectionPill>
|
||||
with SingleTickerProviderStateMixin {
|
||||
int? _dragIndex;
|
||||
bool _dragCancelled = false;
|
||||
bool _pressed = false;
|
||||
@@ -45,10 +53,50 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill> {
|
||||
double? _dragAlignment;
|
||||
int? _activePointer;
|
||||
final _foregroundKey = GlobalKey();
|
||||
late final AnimationController _pressController;
|
||||
late final CurvedAnimation _pressAnimation;
|
||||
ui.FragmentProgram? _maskProgram;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pressController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 240),
|
||||
);
|
||||
_pressAnimation = CurvedAnimation(
|
||||
parent: _pressController,
|
||||
curve: Curves.easeOutCubic,
|
||||
reverseCurve: Curves.easeInCubic,
|
||||
)..addListener(() => setState(() {}));
|
||||
if (widget.selectionColor != null) unawaited(_loadMaskProgram());
|
||||
}
|
||||
|
||||
Future<void> _loadMaskProgram() async {
|
||||
try {
|
||||
final program = await (_selectionMaskProgram ??=
|
||||
ui.FragmentProgram.fromAsset(
|
||||
'assets/shaders/mornye_selection_mask.frag',
|
||||
));
|
||||
if (mounted) setState(() => _maskProgram = program);
|
||||
} catch (error) {
|
||||
debugPrint('Mornye selection mask unavailable: $error');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pressAnimation.dispose();
|
||||
_pressController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MornyeSelectionPill oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.selectionColor == null && widget.selectionColor != null) {
|
||||
unawaited(_loadMaskProgram());
|
||||
}
|
||||
if (oldWidget.labels.length != widget.labels.length ||
|
||||
oldWidget.selectedIndex != widget.selectedIndex) {
|
||||
_dragIndex = null;
|
||||
@@ -113,10 +161,12 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill> {
|
||||
? count - visual - 1
|
||||
: visual;
|
||||
});
|
||||
_pressController.forward();
|
||||
},
|
||||
onPointerUp: (event) {
|
||||
if (event.pointer != _activePointer) return;
|
||||
_activePointer = null;
|
||||
_pressController.reverse();
|
||||
setState(() {
|
||||
_pressed = false;
|
||||
_pressedIndex = null;
|
||||
@@ -126,6 +176,7 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill> {
|
||||
onPointerCancel: (event) {
|
||||
if (event.pointer != _activePointer) return;
|
||||
_activePointer = null;
|
||||
_pressController.reverse();
|
||||
setState(() {
|
||||
_dragCancelled = true;
|
||||
_pressed = false;
|
||||
@@ -187,7 +238,7 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill> {
|
||||
? 0.12
|
||||
: 0.08,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
borderRadius: BorderRadius.circular(_pillRadius),
|
||||
border: Border.all(
|
||||
color: scheme.onSurface.withValues(alpha: 0.05),
|
||||
width: 0.5,
|
||||
@@ -206,10 +257,11 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill> {
|
||||
),
|
||||
),
|
||||
if (lensEnabled && selected >= 0 && selected < count)
|
||||
_interactionLens(selected, count),
|
||||
_interactionLens(selected, count, _pressAnimation.value),
|
||||
_foreground(
|
||||
selected,
|
||||
count,
|
||||
lensEnabled ? _pressAnimation.value : 0,
|
||||
Row(
|
||||
key: _foregroundKey,
|
||||
children: [
|
||||
@@ -222,7 +274,9 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill> {
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
borderRadius: BorderRadius.circular(
|
||||
_pillRadius,
|
||||
),
|
||||
// The moving pill supplies press feedback. An
|
||||
// ink highlight would leave a colored patch on
|
||||
// the tab where the drag started.
|
||||
@@ -289,7 +343,12 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill> {
|
||||
return (1 - (index - position).abs()).clamp(0.0, 1.0);
|
||||
}
|
||||
|
||||
Widget _foreground(int selected, int count, Widget child) {
|
||||
Widget _foreground(
|
||||
int selected,
|
||||
int count,
|
||||
double pressProgress,
|
||||
Widget child,
|
||||
) {
|
||||
final color = widget.selectionColor;
|
||||
if (color == null) return child;
|
||||
return TweenAnimationBuilder<double>(
|
||||
@@ -309,6 +368,9 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill> {
|
||||
? -alignment
|
||||
: alignment,
|
||||
count: count,
|
||||
scaleX: 1 + _heldWidthGrowth * pressProgress,
|
||||
scaleY: 1 + _heldHeightGrowth * pressProgress,
|
||||
program: _maskProgram,
|
||||
color: selected >= 0 && selected < count
|
||||
? color
|
||||
: color.withValues(alpha: 0),
|
||||
@@ -318,7 +380,11 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _interactionLens(int selected, int count) => Positioned.fill(
|
||||
Widget _interactionLens(
|
||||
int selected,
|
||||
int count,
|
||||
double progress,
|
||||
) => Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: AnimatedAlign(
|
||||
duration: _dragAlignment != null
|
||||
@@ -329,42 +395,37 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill> {
|
||||
_dragAlignment ?? (count == 1 ? 0 : -1 + 2 * selected / (count - 1)),
|
||||
0,
|
||||
),
|
||||
child: TweenAnimationBuilder<double>(
|
||||
tween: Tween(end: _pressed ? 1 : 0),
|
||||
duration: const Duration(milliseconds: 240),
|
||||
curve: Curves.easeOutCubic,
|
||||
builder: (context, progress, child) => FractionallySizedBox(
|
||||
widthFactor: 1 / count,
|
||||
heightFactor: 1,
|
||||
child: Transform.scale(
|
||||
scaleX: 1 + 0.28 * progress,
|
||||
scaleY: 1 + 0.24 * progress,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
child: LiquidGlassBatch.exclude(
|
||||
child: MornyeLiquidBackdrop(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
clarity: 1,
|
||||
interaction: true,
|
||||
progress: progress,
|
||||
blurSigma: 0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
color: Colors.white.withValues(
|
||||
alpha:
|
||||
progress *
|
||||
(Theme.of(context).brightness == Brightness.light
|
||||
? 0.24
|
||||
: 0.08),
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.6 * progress),
|
||||
width: 0.75,
|
||||
),
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: 1 / count,
|
||||
heightFactor: 1,
|
||||
child: Transform.scale(
|
||||
scaleX: 1 + _heldWidthGrowth * progress,
|
||||
scaleY: 1 + _heldHeightGrowth * progress,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(_pillRadius),
|
||||
child: LiquidGlassBatch.exclude(
|
||||
child: MornyeLiquidBackdrop(
|
||||
borderRadius: BorderRadius.circular(_pillRadius),
|
||||
clarity: 1,
|
||||
interaction: true,
|
||||
progress: progress,
|
||||
blurSigma: 0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(_pillRadius),
|
||||
color: Colors.white.withValues(
|
||||
alpha:
|
||||
progress *
|
||||
(Theme.of(context).brightness == Brightness.light
|
||||
? 0.24
|
||||
: 0.08),
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.6 * progress),
|
||||
width: 0.75,
|
||||
),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -378,16 +439,36 @@ class _MornyeSelectionPillState extends State<MornyeSelectionPill> {
|
||||
|
||||
/// Paint only icons and labels with the sliding selection color. Badge fills
|
||||
/// and counters can remain outside this widget and retain their own contrast.
|
||||
class MornyeSelectionForeground extends StatelessWidget {
|
||||
class MornyeSelectionForeground extends StatefulWidget {
|
||||
const MornyeSelectionForeground({super.key, required this.child});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<MornyeSelectionForeground> createState() =>
|
||||
_MornyeSelectionForegroundState();
|
||||
}
|
||||
|
||||
class _MornyeSelectionForegroundState extends State<MornyeSelectionForeground> {
|
||||
ui.FragmentProgram? _program;
|
||||
ui.FragmentShader? _shader;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_shader?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scope = context
|
||||
.dependOnInheritedWidgetOfExactType<_SelectionForegroundScope>();
|
||||
if (scope == null) return child;
|
||||
if (scope == null) return widget.child;
|
||||
if (_program != scope.program) {
|
||||
_shader?.dispose();
|
||||
_program = scope.program;
|
||||
_shader = _program?.fragmentShader();
|
||||
}
|
||||
return ShaderMask(
|
||||
blendMode: BlendMode.srcATop,
|
||||
shaderCallback: (bounds) {
|
||||
@@ -396,21 +477,46 @@ class MornyeSelectionForeground extends StatelessWidget {
|
||||
final box = context.findRenderObject()! as RenderBox;
|
||||
final width = row.size.width / scope.count;
|
||||
final left = (scope.alignment + 1) * (row.size.width - width) / 2;
|
||||
// Convert from the bar so scaled icons and nested badges still share
|
||||
// exactly the same moving selection boundary.
|
||||
final start = box.globalToLocal(row.localToGlobal(Offset(left, 0)));
|
||||
final end = box.globalToLocal(
|
||||
row.localToGlobal(Offset(left + width, 0)),
|
||||
final lens = Rect.fromCenter(
|
||||
center: Offset(left + width / 2, row.size.height / 2),
|
||||
width: width * scope.scaleX,
|
||||
height: row.size.height * scope.scaleY,
|
||||
);
|
||||
// Use the same rounded, expanding bounds as the glass, including the
|
||||
// inverse scale of each icon. A one-tab rectangular mask cuts through
|
||||
// the foreground before it reaches the held lens's curved edge.
|
||||
final local = Rect.fromPoints(
|
||||
box.globalToLocal(row.localToGlobal(lens.topLeft)),
|
||||
box.globalToLocal(row.localToGlobal(lens.bottomRight)),
|
||||
);
|
||||
final shader = _shader;
|
||||
if (shader != null) {
|
||||
shader
|
||||
..setFloat(0, local.left)
|
||||
..setFloat(1, local.top)
|
||||
..setFloat(2, local.width)
|
||||
..setFloat(3, local.height)
|
||||
..setFloat(4, local.width / width)
|
||||
..setFloat(5, local.height / row.size.height)
|
||||
..setFloat(
|
||||
6,
|
||||
math.min(_pillRadius, math.min(width, row.size.height) / 2),
|
||||
)
|
||||
..setFloat(7, scope.color.r)
|
||||
..setFloat(8, scope.color.g)
|
||||
..setFloat(9, scope.color.b)
|
||||
..setFloat(10, scope.color.a);
|
||||
return shader;
|
||||
}
|
||||
return ui.Gradient.linear(
|
||||
Offset(start.dx, 0),
|
||||
Offset(end.dx, 0),
|
||||
Offset(local.left, 0),
|
||||
Offset(local.right, 0),
|
||||
[scope.color, scope.color],
|
||||
null,
|
||||
TileMode.decal,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -420,6 +526,9 @@ class _SelectionForegroundScope extends InheritedWidget {
|
||||
required this.rowKey,
|
||||
required this.alignment,
|
||||
required this.count,
|
||||
required this.scaleX,
|
||||
required this.scaleY,
|
||||
required this.program,
|
||||
required this.color,
|
||||
required super.child,
|
||||
});
|
||||
@@ -427,12 +536,18 @@ class _SelectionForegroundScope extends InheritedWidget {
|
||||
final GlobalKey rowKey;
|
||||
final double alignment;
|
||||
final int count;
|
||||
final double scaleX;
|
||||
final double scaleY;
|
||||
final ui.FragmentProgram? program;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_SelectionForegroundScope oldWidget) =>
|
||||
alignment != oldWidget.alignment ||
|
||||
count != oldWidget.count ||
|
||||
scaleX != oldWidget.scaleX ||
|
||||
scaleY != oldWidget.scaleY ||
|
||||
program != oldWidget.program ||
|
||||
color != oldWidget.color ||
|
||||
rowKey != oldWidget.rowKey;
|
||||
}
|
||||
@@ -120,6 +120,9 @@ flutter:
|
||||
uses-material-design: true
|
||||
generate: true
|
||||
|
||||
shaders:
|
||||
- assets/shaders/mornye_selection_mask.frag
|
||||
|
||||
assets:
|
||||
- assets/images/
|
||||
- assets/fonts/Inter-LICENSE.txt
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -9,6 +11,82 @@ import 'package:spotiflac_android/widgets/mornye_chrome.dart';
|
||||
import 'package:spotiflac_android/widgets/mornye_selection_pill.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('selection color follows rounded pill corners', (tester) async {
|
||||
final capture = GlobalKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Center(
|
||||
child: RepaintBoundary(
|
||||
key: capture,
|
||||
child: SizedBox(
|
||||
width: 200,
|
||||
child: MornyeSelectionPill(
|
||||
labels: const ['A', 'B'],
|
||||
selectedIndex: 0,
|
||||
onChanged: (_) {},
|
||||
padding: EdgeInsets.zero,
|
||||
selectionColor: Colors.red,
|
||||
itemBuilder: (_, _, _) => const SizedBox(
|
||||
height: 64,
|
||||
child: ColoredBox(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.runAsync(
|
||||
() => ui.FragmentProgram.fromAsset(
|
||||
'assets/shaders/mornye_selection_mask.frag',
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final colors = (await tester.runAsync(() async {
|
||||
final boundary = tester.renderObject<RenderRepaintBoundary>(
|
||||
find.byKey(capture),
|
||||
);
|
||||
final image = await boundary.toImage();
|
||||
try {
|
||||
final bytes = (await image.toByteData())!;
|
||||
return [
|
||||
for (final point in [
|
||||
const Offset(2, 2),
|
||||
const Offset(20, 32),
|
||||
const Offset(98, 2),
|
||||
const Offset(150, 32),
|
||||
])
|
||||
Color.fromARGB(
|
||||
bytes.getUint8(
|
||||
(point.dy.toInt() * image.width + point.dx.toInt()) * 4 + 3,
|
||||
),
|
||||
bytes.getUint8(
|
||||
(point.dy.toInt() * image.width + point.dx.toInt()) * 4,
|
||||
),
|
||||
bytes.getUint8(
|
||||
(point.dy.toInt() * image.width + point.dx.toInt()) * 4 + 1,
|
||||
),
|
||||
bytes.getUint8(
|
||||
(point.dy.toInt() * image.width + point.dx.toInt()) * 4 + 2,
|
||||
),
|
||||
),
|
||||
];
|
||||
} finally {
|
||||
image.dispose();
|
||||
}
|
||||
}))!;
|
||||
expect(
|
||||
colors.map((color) => color.toARGB32()),
|
||||
[
|
||||
Colors.white,
|
||||
Colors.red,
|
||||
Colors.white,
|
||||
Colors.white,
|
||||
].map((color) => color.toARGB32()),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
for (final direction in TextDirection.values) {
|
||||
testWidgets('tab dragging previews, commits and cancels ($direction)', (
|
||||
tester,
|
||||
|
||||
Reference in new issue
Block a user