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
20 changes: 17 additions & 3 deletions lib/core/editor/querya_code_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,18 @@ class _QueryaCodeEditorState extends State<QueryaCodeEditor> {
widget.onChanged?.call(_effectiveController.text);
}

void _syncThemeIfNeeded(QueryaTheme queryaTheme) {
void _syncThemeIfNeeded(
QueryaTheme queryaTheme, {
bool transitioning = false,
}) {
final controller = _effectiveController;
if (controller is! QueryaHighlightController) return;

// Mid-animation themes differ on every frame: building highlighters for
// them each time is wasted work and pushes real entries out of the pair
// cache. The final frame arrives with `transitioning` false (#1358).
if (transitioning && _lastEditorTheme != null) return;

final editor = queryaTheme.editor;
final tokenHash = queryaTheme.tokenColorsHash;
if (_lastEditorTheme == editor && _lastTokenColorsHash == tokenHash) {
Expand All @@ -122,7 +130,10 @@ class _QueryaCodeEditorState extends State<QueryaCodeEditor> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_syncThemeIfNeeded(context.queryaTheme);
_syncThemeIfNeeded(
context.queryaTheme,
transitioning: QueryaThemeScope.isTransitioning(context),
);
}

@override
Expand All @@ -139,7 +150,10 @@ class _QueryaCodeEditorState extends State<QueryaCodeEditor> {
_lastEditorTheme = null;
_lastTokenColorsHash = 0;
_attachListener();
_syncThemeIfNeeded(context.queryaTheme);
_syncThemeIfNeeded(
context.queryaTheme,
transitioning: QueryaThemeScope.isTransitioning(context),
);
}
}

Expand Down
3 changes: 3 additions & 0 deletions lib/core/theme/animated_querya_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ class _AnimatedQueryaThemeState
Widget build(BuildContext context) {
return QueryaThemeScope(
data: _data!.evaluate(animation),
// True only while the controller runs: the final frame, and a theme
// that never animates, are settled.
transitioning: animation.isAnimating,
child: widget.child,
);
}
Expand Down
18 changes: 17 additions & 1 deletion lib/core/theme/querya_theme_scope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@ class QueryaThemeScope extends InheritedWidget {
super.key,
required this.data,
required super.child,
this.transitioning = false,
});

final QueryaTheme data;

/// Whether [data] is an intermediate frame of a theme animation. Consumers
/// that are costly to update (syntax highlighters) can wait for the final
/// frame instead of following every interpolated colour (#1358).
final bool transitioning;

static QueryaTheme of(BuildContext context) {
final scope =
context.dependOnInheritedWidgetOfExactType<QueryaThemeScope>();
Expand All @@ -28,8 +34,18 @@ class QueryaThemeScope extends InheritedWidget {
return context.dependOnInheritedWidgetOfExactType<QueryaThemeScope>()?.data;
}

/// Whether the theme is mid-animation. Registers a dependency, so the caller
/// is rebuilt when the animation ends.
static bool isTransitioning(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<QueryaThemeScope>()
?.transitioning ??
false;
}

@override
bool updateShouldNotify(QueryaThemeScope oldWidget) => data != oldWidget.data;
bool updateShouldNotify(QueryaThemeScope oldWidget) =>
data != oldWidget.data || transitioning != oldWidget.transitioning;
}

/// Convenient access to [QueryaTheme] tokens and [Theme] / [ColorScheme] from [BuildContext].
Expand Down
99 changes: 99 additions & 0 deletions test/core/theme/animated_querya_theme_transition_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:querya_desktop/core/theme/animated_querya_theme.dart';
import 'package:querya_desktop/core/theme/querya_theme.dart';
import 'package:querya_desktop/core/theme/querya_theme_scope.dart';

void main() {
group('QueryaThemeScope.transitioning (#1358)', () {
testWidgets('is true only while the theme animation runs', (tester) async {
var theme = QueryaTheme.darkDefault;
var transitioning = false;
late StateSetter setOuterState;

await tester.pumpWidget(
StatefulBuilder(
builder: (context, setState) {
setOuterState = setState;
return Directionality(
textDirection: TextDirection.ltr,
child: AnimatedQueryaTheme(
data: theme,
duration: const Duration(milliseconds: 200),
child: Builder(
builder: (context) {
transitioning = QueryaThemeScope.isTransitioning(context);
return const SizedBox.shrink();
},
),
),
);
},
),
);
expect(transitioning, isFalse, reason: 'initial theme is settled');

setOuterState(() => theme = QueryaTheme.lightDefault);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(transitioning, isTrue, reason: 'mid-animation');

await tester.pumpAndSettle();
expect(transitioning, isFalse, reason: 'animation finished');
});

testWidgets('a consumer is rebuilt when the transition ends',
(tester) async {
var theme = QueryaTheme.darkDefault;
final seen = <bool>[];
late StateSetter setOuterState;

await tester.pumpWidget(
StatefulBuilder(
builder: (context, setState) {
setOuterState = setState;
return Directionality(
textDirection: TextDirection.ltr,
child: AnimatedQueryaTheme(
data: theme,
duration: const Duration(milliseconds: 200),
child: Builder(
builder: (context) {
seen.add(QueryaThemeScope.isTransitioning(context));
return const SizedBox.shrink();
},
),
),
);
},
),
);

setOuterState(() => theme = QueryaTheme.lightDefault);
await tester.pumpAndSettle();

expect(seen.last, isFalse);
expect(seen, contains(true));
});

testWidgets('defaults to settled for a plain scope', (tester) async {
var transitioning = true;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: QueryaThemeScope(
data: QueryaTheme.darkDefault,
child: Builder(
builder: (context) {
transitioning = QueryaThemeScope.isTransitioning(context);
return const SizedBox.shrink();
},
),
),
),
);

expect(transitioning, isFalse);
});
});
}
Loading