Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions benchmark/grid_perf_bench.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// ignore_for_file: depend_on_referenced_packages
// ignore_for_file: depend_on_referenced_packages, use_build_context_synchronously

// Frame-timing benchmark for VirtualResultGrid. Run in profile mode:
// flutter run --profile -d linux -t benchmark/grid_perf_bench.dart --dart-define=MODE=scroll
// MODE: scroll (horizontal pan) | edit (staged cell edits on a sorted grid)
// | select (mouse-drag selection through ResultsTab, incl. stats)
// | dialog (open / close showAppDialog repeatedly over a busy grid)
import 'dart:async';
import 'dart:developer' as developer;
import 'dart:io';
Expand All @@ -17,6 +18,7 @@ import 'package:querya_desktop/core/theme/querya_theme.dart';
import 'package:querya_desktop/features/workspace/data_grid_staging_buffer.dart';
import 'package:querya_desktop/features/workspace/result_grid_view.dart';
import 'package:querya_desktop/features/workspace/results_tab.dart';
import 'package:querya_desktop/shared/widgets/app_dialog.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:vm_service/vm_service.dart' as vms;
import 'package:vm_service/vm_service_io.dart' as vms_io;
Expand All @@ -25,6 +27,7 @@ const mode = String.fromEnvironment('MODE', defaultValue: 'scroll');
const rowCount = int.fromEnvironment('ROWS', defaultValue: 5000);
const colCount = int.fromEnvironment('COLS', defaultValue: 120);
const seconds = int.fromEnvironment('SECS', defaultValue: 6);
final dialogHost = GlobalKey();
const profile = bool.fromEnvironment('PROFILE', defaultValue: false);

vms.VmService? _vm;
Expand Down Expand Up @@ -57,7 +60,19 @@ void main() {
runApp(ShadcnApp(
theme: td,
home: material.Scaffold(
body: mode == 'select'
body: mode == 'dialog'
? KeyedSubtree(
key: dialogHost,
child: ListenableBuilder(
listenable: buffer,
builder: (_, __) => VirtualResultGrid(
columns: columns,
rows: buffer.effectiveRows,
stagingBuffer: buffer,
),
),
)
: mode == 'select'
? ListenableBuilder(
listenable: buffer,
builder: (_, __) => ResultsTab(
Expand Down Expand Up @@ -137,7 +152,25 @@ Future<void> _run(DataGridStagingBuffer buffer) async {
: null;
}
var dir = 1.0;
while (DateTime.now().isBefore(end)) {
if (mode == 'dialog') {
final ctx = dialogHost.currentContext!;
while (DateTime.now().isBefore(end)) {
unawaited(showAppDialog<void>(
context: ctx,
builder: (_) => const Center(
child: SizedBox(
width: 420,
height: 260,
child: Card(child: Center(child: Text('bench dialog'))),
),
),
));
await Future<void>.delayed(const Duration(milliseconds: 600));
Navigator.of(ctx).pop();
await Future<void>.delayed(const Duration(milliseconds: 600));
}
}
while (mode != 'dialog' && DateTime.now().isBefore(end)) {
await SchedulerBinding.instance.endOfFrame;
if (mode == 'scroll') {
final p = pos!;
Expand Down
49 changes: 34 additions & 15 deletions lib/shared/widgets/app_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,27 +101,16 @@ class _BlurredDialogScaffoldState extends State<_BlurredDialogScaffold> {
child: Stack(
fit: StackFit.expand,
children: [
// Backdrop: static blur; only opacity/dim animate (blur sigma is expensive).
// Backdrop: blur strength and tint animate directly. No Opacity wraps
// the BackdropFilter, so no extra full-screen offscreen layer is
// composited on every frame of the enter / exit transition.
Positioned.fill(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: widget.barrierDismissible ? widget.onDismiss : null,
child: AnimatedBuilder(
animation: curved,
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
child: const ColoredBox(
color: Color(0x52000000), // black @ 0.32
),
),
),
builder: (ctx, child) {
return Opacity(
opacity: curved.value.clamp(0.0, 1.0),
child: child,
);
},
builder: (ctx, _) => _DialogBackdrop(progress: curved.value),
),
),
),
Expand All @@ -140,3 +129,33 @@ class _BlurredDialogScaffoldState extends State<_BlurredDialogScaffold> {
);
}
}

/// Frosted, dimmed dialog backdrop at [progress] (0 = none, 1 = full).
///
/// Both blur sigma and tint alpha scale with [progress], so the frost fades in
/// and out without pop-in and without an enclosing `Opacity`.
class _DialogBackdrop extends StatelessWidget {
const _DialogBackdrop({required this.progress});

/// Full-strength blur sigma.
static const double sigma = 8;

/// Full-strength tint alpha (black @ 0.32).
static const double tintAlpha = 0.32;

final double progress;

@override
Widget build(BuildContext context) {
final t = progress.clamp(0.0, 1.0);
if (t <= 0) return const SizedBox.expand();
return ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: sigma * t, sigmaY: sigma * t),
child: ColoredBox(
color: const Color(0xFF000000).withValues(alpha: tintAlpha * t),
),
),
);
}
}
96 changes: 82 additions & 14 deletions test/shared/app_dialog_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -121,27 +121,95 @@ void main() {
builder: (c) => const SimpleDialog(title: Text('X')),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 40));
expect(find.byType(BackdropFilter), findsWidgets);
expect(find.byType(FadeTransition), findsWidgets);
expect(find.byType(SlideTransition), findsWidgets);
expect(find.byType(ScaleTransition), findsNothing);
});

testWidgets('backdrop keeps static blur sigma; opacity carries enter',
(tester) async {
final ctx = await pumpHost(tester);

showAppDialog<void>(
context: ctx,
builder: (c) => const SimpleDialog(title: Text('Blur')),
);
// Mid-enter: blur must stay sigma 8 (not sigma * t).
await tester.pump();
await tester.pump(const Duration(milliseconds: 40));
group('backdrop transition (#886)', () {
final fullFilter = ImageFilter.blur(sigmaX: 8, sigmaY: 8);
final fullTint = const Color(0xFF000000).withValues(alpha: 0.32);

final filter = tester.widget<BackdropFilter>(find.byType(BackdropFilter));
expect(filter.filter, ImageFilter.blur(sigmaX: 8, sigmaY: 8));
expect(find.byType(Opacity), findsWidgets);
Color tintOf(WidgetTester tester) {
final box = tester.widget<ColoredBox>(
find.descendant(
of: find.byType(BackdropFilter),
matching: find.byType(ColoredBox),
),
);
return box.color;
}

testWidgets('blur and tint scale in during enter; no Opacity wraps the filter',
(tester) async {
final ctx = await pumpHost(tester);

showAppDialog<void>(
context: ctx,
builder: (c) => const SimpleDialog(title: Text('Blur')),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 40));

final filter = tester.widget<BackdropFilter>(find.byType(BackdropFilter));
expect(filter.filter, isNot(fullFilter), reason: 'sigma is still ramping');
expect(tintOf(tester).a, greaterThan(0));
expect(tintOf(tester).a, lessThan(fullTint.a));

// The frost must not sit under an Opacity (that forces an offscreen layer).
expect(
find.ancestor(
of: find.byType(BackdropFilter),
matching: find.byType(Opacity),
),
findsNothing,
);
});

testWidgets('settles at full blur and tint', (tester) async {
final ctx = await pumpHost(tester);

showAppDialog<void>(
context: ctx,
builder: (c) => const SimpleDialog(title: Text('Settled')),
);
await tester.pumpAndSettle();

final filter = tester.widget<BackdropFilter>(find.byType(BackdropFilter));
expect(filter.filter, fullFilter);
expect(tintOf(tester), fullTint);
expect(
find.ancestor(
of: find.byType(BackdropFilter),
matching: find.byType(Opacity),
),
findsNothing,
);
});

testWidgets('blur and tint fade out again on dismiss', (tester) async {
final ctx = await pumpHost(tester);

final future = showAppDialog<void>(
context: ctx,
builder: (c) => const SimpleDialog(title: Text('Leaving')),
);
await tester.pumpAndSettle();

Navigator.of(ctx, rootNavigator: true).pop();
await tester.pump();
await tester.pump(const Duration(milliseconds: 40));

final filter = tester.widget<BackdropFilter>(find.byType(BackdropFilter));
expect(filter.filter, isNot(fullFilter));
expect(tintOf(tester).a, lessThan(fullTint.a));

await tester.pumpAndSettle();
expect(find.byType(BackdropFilter), findsNothing);
await future;
});
});

testWidgets('enter uses standard duration under full motion', (tester) async {
Expand Down
Loading