diff --git a/lib/app/app.dart b/lib/app/app.dart index 479d353e..88b7b2c9 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,4 +1,6 @@ import 'package:querya_desktop/core/theme/app_theme.dart'; +import 'package:querya_desktop/core/theme/querya_theme.dart'; +import 'package:querya_desktop/core/theme/querya_theme_scope.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'app_lifecycle_cleanup.dart'; @@ -19,8 +21,11 @@ class QueryaApp extends StatelessWidget { enableThemeAnimation: false, // Avoids scroll interception fighting nested Scrollbars in data views. enableScrollInterception: false, - home: const AppLifecycleCleanup( - child: MainScreen(), + home: const QueryaThemeScope( + data: QueryaTheme.darkDefault, + child: AppLifecycleCleanup( + child: MainScreen(), + ), ), ); } diff --git a/lib/core/theme/querya_theme_scope.dart b/lib/core/theme/querya_theme_scope.dart new file mode 100644 index 00000000..b35c72b5 --- /dev/null +++ b/lib/core/theme/querya_theme_scope.dart @@ -0,0 +1,41 @@ +import 'package:flutter/widgets.dart'; + +import 'querya_editor_theme.dart'; +import 'querya_theme.dart'; +import 'querya_workbench_theme.dart'; + +/// Provides [QueryaTheme] (workbench + editor tokens) below [ShadcnApp]. +class QueryaThemeScope extends InheritedWidget { + const QueryaThemeScope({ + super.key, + required this.data, + required super.child, + }); + + final QueryaTheme data; + + static QueryaTheme of(BuildContext context) { + final scope = context.dependOnInheritedWidgetOfExactType(); + assert(scope != null, 'QueryaThemeScope not found in context'); + return scope!.data; + } + + static QueryaTheme? maybeOf(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType() + ?.data; + } + + @override + bool updateShouldNotify(QueryaThemeScope oldWidget) => + data != oldWidget.data; +} + +/// Convenient access to [QueryaTheme] tokens from [BuildContext]. +extension QueryaThemeContext on BuildContext { + QueryaTheme get queryaTheme => QueryaThemeScope.of(this); + + QueryaWorkbenchTheme get workbench => queryaTheme.workbench; + + QueryaEditorTheme get editorTheme => queryaTheme.editor; +} diff --git a/test/core/theme/querya_theme_scope_test.dart b/test/core/theme/querya_theme_scope_test.dart new file mode 100644 index 00000000..b759edf4 --- /dev/null +++ b/test/core/theme/querya_theme_scope_test.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart' as material; +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/theme/app_theme.dart'; +import 'package:querya_desktop/core/theme/querya_theme.dart'; +import 'package:querya_desktop/core/theme/querya_theme_scope.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; + +void main() { + testWidgets('QueryaThemeScope provides workbench tokens', (tester) async { + late Color seenAccent; + + await tester.pumpWidget( + ShadcnApp( + theme: AppTheme.dark, + darkTheme: AppTheme.dark, + themeMode: ThemeMode.dark, + home: QueryaThemeScope( + data: QueryaTheme.darkDefault, + child: material.Builder( + builder: (context) { + seenAccent = context.workbench.accent; + return const material.SizedBox(); + }, + ), + ), + ), + ); + + expect(seenAccent, QueryaTheme.darkDefault.workbench.accent); + }); + + testWidgets('QueryaThemeScope rebuilds when data changes', (tester) async { + var canvas = QueryaTheme.darkDefault.workbench.canvas; + + await tester.pumpWidget( + _ScopeHost( + data: QueryaTheme.darkDefault, + onCanvas: (c) => canvas = c, + ), + ); + + const light = QueryaTheme.lightDefault; + await tester.pumpWidget( + _ScopeHost( + data: light, + onCanvas: (c) => canvas = c, + ), + ); + + expect(canvas, light.workbench.canvas); + expect(canvas, isNot(QueryaTheme.darkDefault.workbench.canvas)); + }); +} + +class _ScopeHost extends StatelessWidget { + const _ScopeHost({ + required this.data, + required this.onCanvas, + }); + + final QueryaTheme data; + final ValueChanged onCanvas; + + @override + Widget build(BuildContext context) { + return ShadcnApp( + theme: AppTheme.dark, + darkTheme: AppTheme.dark, + themeMode: ThemeMode.dark, + home: QueryaThemeScope( + data: data, + child: material.Builder( + builder: (context) { + onCanvas(context.workbench.canvas); + return const material.SizedBox(); + }, + ), + ), + ); + } +} \ No newline at end of file