From fd9c61ea09d8e70dacc9b2fa89dcd3113c88d991 Mon Sep 17 00:00:00 2001 From: Henry-Hiles Date: Wed, 16 Sep 2026 16:09:56 -0400 Subject: [PATCH] adjust padding --- .widget_preview/.gitignore | 48 + .widget_preview/README.md | 4 + .widget_preview/analysis_options.yaml | 38 + .widget_preview/lib/main.dart | 9 + .widget_preview/lib/src/controls.dart | 593 ++++++++ .../lib/src/dtd/dtd_connection_info.dart | 9 + .widget_preview/lib/src/dtd/dtd_services.dart | 130 ++ .../lib/src/dtd/editor_service.dart | 395 ++++++ .widget_preview/lib/src/dtd/utils.dart | 39 + .../lib/src/generated_preview.dart | 6 + .widget_preview/lib/src/split.dart | 365 +++++ .../lib/src/theme/_ide_theme_desktop.dart | 10 + .../lib/src/theme/_ide_theme_web.dart | 43 + .widget_preview/lib/src/theme/ide_theme.dart | 47 + .widget_preview/lib/src/theme/theme.dart | 365 +++++ .widget_preview/lib/src/utils.dart | 202 +++ .../lib/src/utils/color_utils.dart | 70 + .../pointer_events/_pointer_events_stub.dart | 38 + .../pointer_events/_pointer_events_web.dart | 43 + .../utils/pointer_events/pointer_events.dart | 6 + .../lib/src/utils/url/_url_stub.dart | 24 + .../lib/src/utils/url/_url_web.dart | 36 + .widget_preview/lib/src/utils/url/url.dart | 7 + .widget_preview/lib/src/widget_preview.dart | 137 ++ .../src/widget_preview_inspector_service.dart | 89 ++ .../lib/src/widget_preview_rendering.dart | 1251 +++++++++++++++++ .../widget_preview_scaffold_controller.dart | 287 ++++ .widget_preview/preview_manifest.json | 1 + .widget_preview/pubspec.lock | 734 ++++++++++ .widget_preview/pubspec.yaml | 26 + .widget_preview/web/favicon.png | Bin 0 -> 917 bytes .widget_preview/web/icons/Icon-192.png | Bin 0 -> 5292 bytes .widget_preview/web/icons/Icon-512.png | Bin 0 -> 8252 bytes .../web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes .../web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes .widget_preview/web/index.html | 46 + .widget_preview/web/manifest.json | 35 + lib/src/widgets/emoji_picker.dart | 2 +- 38 files changed, 5134 insertions(+), 1 deletion(-) create mode 100644 .widget_preview/.gitignore create mode 100644 .widget_preview/README.md create mode 100644 .widget_preview/analysis_options.yaml create mode 100644 .widget_preview/lib/main.dart create mode 100644 .widget_preview/lib/src/controls.dart create mode 100644 .widget_preview/lib/src/dtd/dtd_connection_info.dart create mode 100644 .widget_preview/lib/src/dtd/dtd_services.dart create mode 100644 .widget_preview/lib/src/dtd/editor_service.dart create mode 100644 .widget_preview/lib/src/dtd/utils.dart create mode 100644 .widget_preview/lib/src/generated_preview.dart create mode 100644 .widget_preview/lib/src/split.dart create mode 100644 .widget_preview/lib/src/theme/_ide_theme_desktop.dart create mode 100644 .widget_preview/lib/src/theme/_ide_theme_web.dart create mode 100644 .widget_preview/lib/src/theme/ide_theme.dart create mode 100644 .widget_preview/lib/src/theme/theme.dart create mode 100644 .widget_preview/lib/src/utils.dart create mode 100644 .widget_preview/lib/src/utils/color_utils.dart create mode 100644 .widget_preview/lib/src/utils/pointer_events/_pointer_events_stub.dart create mode 100644 .widget_preview/lib/src/utils/pointer_events/_pointer_events_web.dart create mode 100644 .widget_preview/lib/src/utils/pointer_events/pointer_events.dart create mode 100644 .widget_preview/lib/src/utils/url/_url_stub.dart create mode 100644 .widget_preview/lib/src/utils/url/_url_web.dart create mode 100644 .widget_preview/lib/src/utils/url/url.dart create mode 100644 .widget_preview/lib/src/widget_preview.dart create mode 100644 .widget_preview/lib/src/widget_preview_inspector_service.dart create mode 100644 .widget_preview/lib/src/widget_preview_rendering.dart create mode 100644 .widget_preview/lib/src/widget_preview_scaffold_controller.dart create mode 100644 .widget_preview/preview_manifest.json create mode 100644 .widget_preview/pubspec.lock create mode 100644 .widget_preview/pubspec.yaml create mode 100644 .widget_preview/web/favicon.png create mode 100644 .widget_preview/web/icons/Icon-192.png create mode 100644 .widget_preview/web/icons/Icon-512.png create mode 100644 .widget_preview/web/icons/Icon-maskable-192.png create mode 100644 .widget_preview/web/icons/Icon-maskable-512.png create mode 100644 .widget_preview/web/index.html create mode 100644 .widget_preview/web/manifest.json diff --git a/.widget_preview/.gitignore b/.widget_preview/.gitignore new file mode 100644 index 0000000..79f7eca --- /dev/null +++ b/.widget_preview/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Widget Preview related +.widget_preview/ diff --git a/.widget_preview/README.md b/.widget_preview/README.md new file mode 100644 index 0000000..d09c93b --- /dev/null +++ b/.widget_preview/README.md @@ -0,0 +1,4 @@ +# Widget Preview Scaffold + +This project is generated by `flutter widget-preview` and is used to host Widgets +to be previewed in the widget previewer. diff --git a/.widget_preview/analysis_options.yaml b/.widget_preview/analysis_options.yaml new file mode 100644 index 0000000..cedcc10 --- /dev/null +++ b/.widget_preview/analysis_options.yaml @@ -0,0 +1,38 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/.widget_preview/lib/main.dart b/.widget_preview/lib/main.dart new file mode 100644 index 0000000..0dd047b --- /dev/null +++ b/.widget_preview/lib/main.dart @@ -0,0 +1,9 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'src/widget_preview_rendering.dart'; + +Future main() async { + await mainImpl(); +} diff --git a/.widget_preview/lib/src/controls.dart b/.widget_preview/lib/src/controls.dart new file mode 100644 index 0000000..ffe5f50 --- /dev/null +++ b/.widget_preview/lib/src/controls.dart @@ -0,0 +1,593 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; + +import 'theme/theme.dart'; +import 'widget_preview_scaffold_controller.dart'; + +enum _PreviewSearchFilter { + groupName('Group name'), + previewName('Preview name'), + containingScript('Containing script'), + containingPackage('Containing package'); + + const _PreviewSearchFilter(this.label); + + final String label; +} + +abstract class _SearchFilterConfig { + const _SearchFilterConfig(this.filter, this._controller); + + final _PreviewSearchFilter filter; + final WidgetPreviewScaffoldController _controller; + + String get label => filter.label; + + ValueListenable listenable(); + + bool onToggle(); +} + +class _GroupSearchFilter extends _SearchFilterConfig { + const _GroupSearchFilter(WidgetPreviewScaffoldController controller) + : super(_PreviewSearchFilter.groupName, controller); + + @override + ValueListenable listenable() => _controller.searchByGroupNameListenable; + + @override + bool onToggle() => _controller.toggleSearchByGroupName(); +} + +class _PreviewNameSearchFilter extends _SearchFilterConfig { + const _PreviewNameSearchFilter(WidgetPreviewScaffoldController controller) + : super(_PreviewSearchFilter.previewName, controller); + + @override + ValueListenable listenable() => + _controller.searchByPreviewNameListenable; + + @override + bool onToggle() => _controller.toggleSearchByPreviewName(); +} + +class _ContainingScriptSearchFilter extends _SearchFilterConfig { + const _ContainingScriptSearchFilter( + WidgetPreviewScaffoldController controller, + ) : super(_PreviewSearchFilter.containingScript, controller); + + @override + ValueListenable listenable() => + _controller.searchByContainingScriptListenable; + + @override + bool onToggle() => _controller.toggleSearchByContainingScript(); +} + +class _ContainingPackageSearchFilter extends _SearchFilterConfig { + const _ContainingPackageSearchFilter( + WidgetPreviewScaffoldController controller, + ) : super(_PreviewSearchFilter.containingPackage, controller); + + @override + ValueListenable listenable() => + _controller.searchByContainingPackageListenable; + + @override + bool onToggle() => _controller.toggleSearchByContainingPackage(); +} + +/// Provides controls to change the zoom level of a [WidgetPreview]. +class ZoomControls extends StatelessWidget { + /// Provides controls to change the zoom level of a [WidgetPreview]. + const ZoomControls({super.key, required this._transformationController}); + + static const double _minScale = 1.0; + static const double _maxScale = 4.0; + + final TransformationController _transformationController; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return _ControlDecorator( + child: ValueListenableBuilder( + valueListenable: _transformationController, + builder: (context, matrix, _) { + final double scale = matrix.entry(0, 0); + final String scalePercentage = '${(scale * 100).toStringAsFixed(0)}%'; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + tooltip: 'Zoom out', + style: theme.iconButtonTheme.style, + onPressed: scale > _minScale ? _zoomOut : null, + icon: const Icon(Icons.zoom_out), + color: theme.colorScheme.onSurface, + disabledColor: theme.colorScheme.onSurface.withValues( + alpha: 0.38, + ), + ), + SizedBox( + width: 100, + height: defaultButtonHeight, + child: Slider( + min: _minScale, + max: _maxScale, + value: scale.clamp(_minScale, _maxScale), + onChanged: _setScale, + ), + ), + IconButton( + tooltip: 'Zoom in', + style: theme.iconButtonTheme.style, + onPressed: scale < _maxScale ? _zoomIn : null, + icon: const Icon(Icons.zoom_in_sharp), + color: theme.colorScheme.onSurface, + disabledColor: theme.colorScheme.onSurface.withValues( + alpha: 0.38, + ), + ), + IconButton( + tooltip: 'Reset zoom', + style: theme.iconButtonTheme.style, + onPressed: scale != _minScale ? _reset : null, + icon: const Icon(Icons.zoom_out_map), + color: theme.colorScheme.onSurface, + disabledColor: theme.colorScheme.onSurface.withValues( + alpha: 0.38, + ), + ), + SizedBox( + width: 36, + child: Text( + scalePercentage, + textAlign: TextAlign.end, + style: TextStyle( + color: theme.colorScheme.onSurface, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ); + }, + ), + ); + } + + void _zoomIn() { + final double currentScale = _transformationController.value.entry(0, 0); + _setScale(currentScale + 0.25); + } + + void _zoomOut() { + final double currentScale = _transformationController.value.entry(0, 0); + _setScale(currentScale - 0.25); + } + + void _setScale(double scale) { + final double clampedScale = scale.clamp(_minScale, _maxScale); + _transformationController.value = Matrix4.diagonal3Values( + clampedScale, + clampedScale, + 1.0, + ); + } + + void _reset() { + _transformationController.value = Matrix4.identity(); + } +} + +class _ControlDecorator extends StatelessWidget { + const _ControlDecorator({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.all(densePadding), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: defaultBorderRadius, + ), + child: child, + ); + } +} + +/// Allows for controlling the grid vs layout view in the preview environment. +class LayoutTypeSelector extends StatelessWidget { + const LayoutTypeSelector({super.key, required this.controller}); + + final WidgetPreviewScaffoldController controller; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return _ControlDecorator( + child: ValueListenableBuilder( + valueListenable: controller.layoutTypeListenable, + builder: (context, selectedLayout, _) { + return Row( + children: [ + IconButton( + style: theme.iconButtonTheme.style, + visualDensity: VisualDensity.compact, + onPressed: () => controller.layoutType = LayoutType.gridView, + icon: Icon(Icons.grid_on), + color: selectedLayout == LayoutType.gridView + ? Colors.blue + : Colors.black, + ), + IconButton( + onPressed: () => controller.layoutType = LayoutType.listView, + visualDensity: VisualDensity.compact, + icon: Icon(Icons.view_list), + color: selectedLayout == LayoutType.listView + ? Colors.blue + : Colors.black, + ), + ], + ); + }, + ), + ); + } +} + +class WidgetInspectorToggle extends StatelessWidget { + const WidgetInspectorToggle({super.key, required this.controller}); + + final WidgetPreviewScaffoldController controller; + + @override + Widget build(BuildContext context) { + return _ControlDecorator( + child: ValueListenableBuilder( + valueListenable: controller.widgetInspectorVisible, + builder: (context, widgetInspectorVisible, _) { + final theme = Theme.of(context); + return IconButton( + style: theme.iconButtonTheme.style, + visualDensity: VisualDensity.compact, + onPressed: controller.toggleWidgetInspectorVisible, + // TODO(bkonyi): replace with widget inspector icon. + icon: Icon(Icons.image_search), + color: widgetInspectorVisible ? Colors.blue : Colors.black, + ); + }, + ), + ); + } +} + +/// A toggle button that enables / disables filtering previews by the currently +/// selected source file. +/// +/// This control is hidden if the DTD Editor service isn't available. +class FilterBySelectedFileToggle extends StatelessWidget { + const FilterBySelectedFileToggle({super.key, required this.controller}); + + @visibleForTesting + static const kTooltip = 'Filter previews by selected file'; + + final WidgetPreviewScaffoldController controller; + + @override + Widget build(BuildContext context) { + return _ControlDecorator( + child: ValueListenableBuilder( + valueListenable: controller.filterBySelectedFileListenable, + builder: (context, value, child) { + return IconButton( + onPressed: controller.toggleFilterBySelectedFile, + icon: Icon(Icons.file_open), + color: value ? Colors.blue : Colors.black, + tooltip: kTooltip, + ); + }, + ), + ); + } +} + +/// A button that triggers a "soft" restart of a previewed widget. +/// +/// A soft restart removes the previewed widget from the widget tree for a frame before +/// re-inserting it on the next frame. This has the effect of re-running local initializers in +/// State objects, which normally requires a hot restart to accomplish in a normal application. +class SoftRestartButton extends StatelessWidget { + const SoftRestartButton({super.key, required this.softRestartListenable}); + + final ValueNotifier softRestartListenable; + + @override + Widget build(BuildContext context) { + return _ControlDecorator( + child: IconButton( + tooltip: 'Hot restart', + onPressed: _onRestart, + icon: Icon(Icons.refresh), + color: Colors.black, + ), + ); + } + + void _onRestart() { + softRestartListenable.value = true; + } +} + +/// A button that triggers a restart of the widget previewer through a hot restart request made +/// through DTD. +class WidgetPreviewerRestartButton extends StatelessWidget { + const WidgetPreviewerRestartButton({super.key, required this.controller}); + + final WidgetPreviewScaffoldController controller; + + @override + Widget build(BuildContext context) { + return _ControlDecorator( + child: IconButton( + tooltip: 'Restart the Widget Previewer', + onPressed: controller.dtdServices.hotRestartPreviewer, + icon: Icon(Icons.restart_alt), + color: Colors.black, + ), + ); + } +} + +/// Controls for searching and filtering widget previews. +/// +/// This widget combines a text query field with a popup menu for selecting +/// which preview fields are included in search. +class PreviewSearchControls extends StatefulWidget { + const PreviewSearchControls({super.key, required this.controller}); + + final WidgetPreviewScaffoldController controller; + + @override + State createState() => _PreviewSearchControlsState(); +} + +class _PreviewSearchControlsState extends State { + late final TextEditingController _searchController; + late final List<_SearchFilterConfig> _searchFilters; + + @override + void initState() { + super.initState(); + _searchFilters = <_SearchFilterConfig>[ + _GroupSearchFilter(widget.controller), + _PreviewNameSearchFilter(widget.controller), + _ContainingScriptSearchFilter(widget.controller), + _ContainingPackageSearchFilter(widget.controller), + ]; + _searchController = TextEditingController( + text: widget.controller.searchQueryListenable.value, + ); + widget.controller.searchQueryListenable.addListener( + _syncControllerQueryToTextField, + ); + } + + @override + void didUpdateWidget(covariant PreviewSearchControls oldWidget) { + super.didUpdateWidget(oldWidget); + if (identical(oldWidget.controller, widget.controller)) { + return; + } + oldWidget.controller.searchQueryListenable.removeListener( + _syncControllerQueryToTextField, + ); + widget.controller.searchQueryListenable.addListener( + _syncControllerQueryToTextField, + ); + _searchFilters + ..clear() + ..addAll(<_SearchFilterConfig>[ + _GroupSearchFilter(widget.controller), + _PreviewNameSearchFilter(widget.controller), + _ContainingScriptSearchFilter(widget.controller), + _ContainingPackageSearchFilter(widget.controller), + ]); + _syncControllerQueryToTextField(); + } + + @override + void dispose() { + widget.controller.searchQueryListenable.removeListener( + _syncControllerQueryToTextField, + ); + _searchController.dispose(); + super.dispose(); + } + + void _syncControllerQueryToTextField() { + final query = widget.controller.searchQueryListenable.value; + if (_searchController.text == query) { + return; + } + + _searchController.value = _searchController.value.copyWith( + text: query, + selection: TextSelection.collapsed(offset: query.length), + composing: TextRange.empty, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return _ControlDecorator( + child: Row( + children: [ + Expanded( + child: SizedBox( + height: defaultButtonHeight, + child: TextField( + controller: _searchController, + style: theme.regularTextStyleWithColor(Colors.black), + cursorColor: Colors.black, + textAlignVertical: TextAlignVertical.center, + onChanged: widget.controller.updateSearchQuery, + decoration: InputDecoration( + isDense: true, + hintText: 'Search previews', + hintStyle: theme.regularTextStyleWithColor(Colors.black54), + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + vertical: densePadding, + horizontal: denseSpacing, + ), + prefixIcon: Icon( + Icons.search, + size: defaultIconSize, + color: Colors.black54, + ), + suffixIcon: _SearchClearButton(controller: widget.controller), + ), + ), + ), + ), + Container(height: 16, width: 1, color: Colors.black26), + _SearchFiltersMenuButton(searchFilters: _searchFilters), + ], + ), + ); + } +} + +class _SearchClearButton extends StatelessWidget { + const _SearchClearButton({required this.controller}); + + final WidgetPreviewScaffoldController controller; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ValueListenableBuilder( + valueListenable: controller.searchQueryListenable, + builder: (context, query, _) { + if (query.isEmpty) { + return const SizedBox.shrink(); + } + return IconButton( + tooltip: 'Clear search', + style: theme.iconButtonTheme.style, + visualDensity: VisualDensity.compact, + icon: Icon(Icons.clear, size: defaultIconSize), + color: Colors.black, + onPressed: () => controller.updateSearchQuery(''), + ); + }, + ); + } +} + +class _SearchFiltersMenuButton extends StatelessWidget { + const _SearchFiltersMenuButton({required this.searchFilters}); + + final List<_SearchFilterConfig> searchFilters; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ListenableBuilder( + listenable: Listenable.merge( + searchFilters + .map((filter) => filter.listenable()) + .toList(growable: false), + ), + builder: (context, _) { + final allFiltersEnabled = searchFilters.every( + (filter) => filter.listenable().value, + ); + return PopupMenuButton<_PreviewSearchFilter>( + tooltip: 'Search fields', + style: theme.iconButtonTheme.style, + iconColor: allFiltersEnabled ? Colors.black : Colors.blue, + iconSize: defaultIconSize, + icon: const Icon(Icons.filter_list), + onSelected: (_PreviewSearchFilter selected) { + final didToggle = searchFilters + .firstWhere((filter) => filter.filter == selected) + .onToggle(); + if (!didToggle) { + _showNoRemainingSearchFilterSnackBar(context); + } + }, + itemBuilder: (context) { + return searchFilters + .map( + (filter) => CheckedPopupMenuItem<_PreviewSearchFilter>( + value: filter.filter, + checked: filter.listenable().value, + child: Text(filter.label), + ), + ) + .toList(growable: false); + }, + ); + }, + ); + } + + void _showNoRemainingSearchFilterSnackBar(BuildContext context) { + final scaffoldMessenger = ScaffoldMessenger.of(context); + scaffoldMessenger + ..hideCurrentSnackBar() + ..showSnackBar( + const SnackBar( + content: Text('At least one search field must remain enabled.'), + ), + ); + } +} + +extension on Brightness { + Brightness get invert => isLight ? Brightness.dark : Brightness.light; + bool get isLight => this == Brightness.light; +} + +/// A button that toggles the current theme brightness. +class BrightnessToggleButton extends StatelessWidget { + const BrightnessToggleButton({super.key, required this.brightnessListenable}); + + final ValueNotifier brightnessListenable; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: brightnessListenable, + builder: (context, brightness, _) { + final brightness = brightnessListenable.value; + return _ControlDecorator( + child: IconButton( + tooltip: 'Switch to ${brightness.isLight ? 'dark' : 'light'} mode', + onPressed: _toggleBrightness, + icon: Icon(brightness.isLight ? Icons.dark_mode : Icons.light_mode), + color: Colors.black, + ), + ); + }, + ); + } + + void _toggleBrightness() { + brightnessListenable.value = brightnessListenable.value.invert; + } +} diff --git a/.widget_preview/lib/src/dtd/dtd_connection_info.dart b/.widget_preview/lib/src/dtd/dtd_connection_info.dart new file mode 100644 index 0000000..4cf65d2 --- /dev/null +++ b/.widget_preview/lib/src/dtd/dtd_connection_info.dart @@ -0,0 +1,9 @@ +// ignore_for_file: implementation_imports + +const String kWidgetPreviewDtdUri = 'ws://127.0.0.1:40395/5YUcVSkoXko='; +const String kWidgetPreviewService = + 'widget-preview-293db269-cca2-49da-8059-28e5b582b22e'; +const String kWidgetPreviewScaffoldStream = + 'WidgetPreviewScaffold-293db269-cca2-49da-8059-28e5b582b22e'; +const String kProjectRootPath = + r'/home/quadradical/Documents/Code/material_emoji_picker'; diff --git a/.widget_preview/lib/src/dtd/dtd_services.dart b/.widget_preview/lib/src/dtd/dtd_services.dart new file mode 100644 index 0000000..37551f5 --- /dev/null +++ b/.widget_preview/lib/src/dtd/dtd_services.dart @@ -0,0 +1,130 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:dtd/dtd.dart'; +import 'package:json_rpc_2/json_rpc_2.dart'; +import 'package:widget_preview_scaffold/src/dtd/dtd_connection_info.dart'; +import 'package:widget_preview_scaffold/src/dtd/editor_service.dart'; +import 'package:widget_preview_scaffold/src/dtd/utils.dart'; + +/// Provides services, streams, and RPC invocations to interact with Flutter developer tooling. +class WidgetPreviewScaffoldDtdServices with DtdEditorService { + // WARNING: Keep these constants and services in sync with those defined in the widget preview + // scaffold's dtd_services.dart. + // + // START KEEP SYNCED + + static const kIsWindows = 'isWindows'; + static const kHotRestartPreviewer = 'hotRestartPreviewer'; + static const kResolveUri = 'resolveUri'; + static const kSetPreference = 'setPreference'; + static const kGetPreference = 'getPreference'; + static const kGetDevToolsUri = 'getDevToolsUri'; + + /// Error code for RpcException thrown when attempting to load a key from + /// persistent preferences that doesn't have an entry. + static const kNoValueForKey = 200; + + // END KEEP SYNCED + + /// Connects to the Dart Tooling Daemon (DTD) specified by the Flutter tool. + /// + /// If the connection is successful, the Widget Preview Scaffold will register services and + /// subscribe to various streams to interact directly with other tooling (e.g., IDEs). + Future connect({Uri? dtdUri}) async { + final Uri dtdWsUri = dtdUri ?? Uri.parse(kWidgetPreviewDtdUri); + dtd = await DartToolingDaemon.connect(dtdWsUri); + unawaited( + dtd.postEvent( + kWidgetPreviewScaffoldStream, + 'Connected', + const {}, + ), + ); + await _determineIfWindows(); + await initializeEditorService(this); + } + + /// Disposes the DTD connection. + @override + Future dispose() async { + super.dispose(); + await dtd.close(); + } + + Future _call( + String methodName, { + Map? params, + }) => dtd.safeCall(kWidgetPreviewService, methodName, params: params); + + /// Returns `true` if the operating system is Windows. + late final bool isWindows; + + Future _determineIfWindows() async { + isWindows = (BoolResponse.fromDTDResponse( + (await _call(kIsWindows))!, + )).value!; + } + + /// Trigger a hot restart of the widget preview scaffold. + Future hotRestartPreviewer() => _call(kHotRestartPreviewer); + + /// Resolves a package:// URI to a file:// URI using the package_config. + /// + /// Returns null if [uri] can not be resolved. + Future resolveUri(Uri uri) async { + final response = await _call(kResolveUri, params: {'uri': uri.toString()}); + if (response == null) { + return null; + } + final result = StringResponse.fromDTDResponse(response).value; + return result == null ? null : Uri.parse(result); + } + + /// Retrieves an arbitrary value associated with [key] from the persistent + /// preferences map. + /// + /// Returns null if [key] is not in the map. + Future getPreference(String key) async { + try { + final response = await _call(kGetPreference, params: {'key': key}); + return switch (response?.type) { + 'StringResponse' => StringResponse.fromDTDResponse(response!).value, + 'BoolResponse' => BoolResponse.fromDTDResponse(response!).value, + _ => throw StateError('Unexpected response type: ${response?.type}'), + }; + } on RpcException catch (e) { + if (e.code == kNoValueForKey) { + return null; + } + rethrow; + } + } + + /// Retrieves the state of flag [key] from the persistent preferences map. + /// + /// If [key] is not set, [defaultValue] is returned. + Future getFlag(String key, {bool defaultValue = false}) async { + final result = await getPreference(key) as bool?; + return result ?? defaultValue; + } + + /// Sets [key] to [value] in the persistent preferences map. + Future setPreference(String key, Object? value) async { + await _call(kSetPreference, params: {'key': key, 'value': value}); + } + + /// Retrieves the DevTools URI for the previewer instance. + Future getDevToolsUri() async { + final result = StringResponse.fromDTDResponse( + (await _call(kGetDevToolsUri))!, + ); + return Uri.parse(result.value!); + } + + @override + late final DartToolingDaemon dtd; +} diff --git a/.widget_preview/lib/src/dtd/editor_service.dart b/.widget_preview/lib/src/dtd/editor_service.dart new file mode 100644 index 0000000..722c9db --- /dev/null +++ b/.widget_preview/lib/src/dtd/editor_service.dart @@ -0,0 +1,395 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:dtd/dtd.dart'; +import 'package:flutter/foundation.dart'; +import 'package:widget_preview_scaffold/src/dtd/dtd_services.dart'; +import 'package:widget_preview_scaffold/src/dtd/utils.dart'; + +/// Provides support for interacting with the Editor DTD service registered by IDE plugins. +mixin DtdEditorService { + DartToolingDaemon get dtd; + + /// The name of the Editor service. + static const String kEditorService = 'Editor'; + + /// The name of the Editor's getActiveLocation method. + static const String kGetActiveLocation = 'getActiveLocation'; + + /// The name of the Editor's navigateToCode method. + static const String kNavigateToCode = 'navigateToCode'; + + /// The name of the DTD Service stream. + static const String kServiceStream = 'Service'; + + /// The kind of the event sent over the [kServiceStream] stream when a new + /// service method is registered. + static const kServiceRegistered = 'ServiceRegistered'; + + /// The kind of the event sent over the [kServiceStream] stream when a + /// service method is unregistered. + static const kServiceUnregistered = 'ServiceUnregistered'; + + /// Whether or not the Editor service is available. + ValueListenable get editorServiceAvailable => _editorServiceAvailable; + static final _editorServiceAvailable = ValueNotifier(false); + + /// The currently selected source file in the IDE. + ValueListenable get selectedSourceFile => _selectedSourceFile; + static final _selectedSourceFile = ValueNotifier(null); + + /// The current theming set in the IDE. + ValueListenable get editorTheme => _editorTheme; + static final _editorTheme = ValueNotifier(null); + + /// Start listening for events on the Editor stream. + Future initializeEditorService( + WidgetPreviewScaffoldDtdServices dtdServices, + ) async { + final editorKindMap = EditorEventKind.values.asNameMap(); + dtd.onEvent(kEditorService).listen((data) { + final kind = editorKindMap[data.kind]; + switch (kind) { + // Unknown event. Use null here so we get exhaustiveness checking for + // the rest. + case null: + break; + case EditorEventKind.themeChanged: + _editorTheme.value = ThemeChangedEvent.fromJson(data.data).theme; + case EditorEventKind.activeLocationChanged: + _selectedSourceFile.value = ActiveLocationChangedEvent.fromJson( + data.data, + ).textDocument; + } + }); + await dtd.safeStreamListen(kEditorService); + + dtd.onEvent(kServiceStream).listen((data) async { + switch (data) { + case DTDEvent( + kind: kServiceRegistered, + data: { + DtdParameters.service: kEditorService, + DtdParameters.method: kGetActiveLocation, + }, + ): + // Manually retrieve the currently selected source file. + unawaited(_updateSelectedSourceFile()); + _editorServiceAvailable.value = true; + case DTDEvent( + kind: kServiceRegistered, + data: {DtdParameters.service: kEditorService}, + ): + _editorServiceAvailable.value = true; + case DTDEvent( + kind: kServiceUnregistered, + data: {DtdParameters.service: kEditorService}, + ): + _editorServiceAvailable.value = false; + } + }); + await dtd.safeStreamListen(kServiceStream); + } + + @mustCallSuper + void dispose() { + _selectedSourceFile.dispose(); + _editorServiceAvailable.dispose(); + _editorTheme.dispose(); + } + + Future _updateSelectedSourceFile() async { + final response = await dtd.safeCall(kEditorService, kGetActiveLocation); + if (response != null) { + _selectedSourceFile.value = ActiveLocation.fromJson( + response.result, + ).textDocument; + } + } + + /// Tells the editor to navigate to a given code [location]. + /// + /// Only locations with `file://` URIs are valid. + Future navigateToCode(CodeLocation location) async { + await dtd.safeCall( + kEditorService, + kNavigateToCode, + params: location.toJson(), + ); + } +} + +// TODO(bkonyi): much of the following code is copied from the DevTools codebase. We should publish +// a package containing these DTD services. See https://github.com/flutter/devtools/issues/9306. + +/// Known kinds of events that may come from the editor. +/// +/// This list is not guaranteed to match actual events from any given editor as +/// the editor might not implement all functionality or may be a future version +/// running against an older version of this code/DevTools. +enum EditorEventKind { + /// The kind for a [ThemeChangedEvent]. + themeChanged, + + /// The kind for an [ActiveLocationChangedEvent] event. + activeLocationChanged, +} + +/// A base class for all known events that an editor can produce. +/// +/// The set of subclasses is not guaranteed to match actual events from any +/// given editor as the editor might not implement all functionality or may be a +/// future version running against an older version of this code/DevTools. +sealed class EditorEvent { + EditorEventKind get kind; +} + +/// UI settings for an editor's theme. +class EditorTheme { + EditorTheme({ + required this.isDarkMode, + required this.backgroundColor, + required this.foregroundColor, + required this.fontSize, + }); + + EditorTheme.fromJson(Map map) + : this( + isDarkMode: map[Field.isDarkMode] as bool, + backgroundColor: map[Field.backgroundColor] as String?, + foregroundColor: map[Field.foregroundColor] as String?, + fontSize: map[Field.fontSize] as int?, + ); + + final bool isDarkMode; + final String? backgroundColor; + final String? foregroundColor; + final int? fontSize; + + Map toJson() => { + Field.isDarkMode: isDarkMode, + Field.backgroundColor: backgroundColor, + Field.foregroundColor: foregroundColor, + Field.fontSize: fontSize, + }; +} + +class ThemeChangedEvent extends EditorEvent { + ThemeChangedEvent({required this.theme}); + + ThemeChangedEvent.fromJson(Map map) + : this( + theme: EditorTheme.fromJson(map[Field.theme] as Map), + ); + + final EditorTheme theme; + + @override + EditorEventKind get kind => EditorEventKind.themeChanged; + + Map toJson() => {Field.theme: theme}; +} + +/// An event sent by an editor when the current cursor position/s change. +class ActiveLocationChangedEvent extends ActiveLocation implements EditorEvent { + ActiveLocationChangedEvent({required ActiveLocation activeLocation}) + : super( + selections: activeLocation.selections, + textDocument: activeLocation.textDocument, + ); + + ActiveLocationChangedEvent.fromJson(Map map) + : this(activeLocation: ActiveLocation.fromJson(map)); + + @override + EditorEventKind get kind => EditorEventKind.activeLocationChanged; +} + +class ActiveLocation { + ActiveLocation({required this.selections, required this.textDocument}); + + ActiveLocation.fromJson(Map map) + : this( + textDocument: map.containsKey(Field.textDocument) + ? TextDocument.fromJson( + map[Field.textDocument] as Map, + ) + : null, + selections: (map[Field.selections] as List) + .cast>() + .map(EditorSelection.fromJson) + .toList(), + ); + + final List selections; + final TextDocument? textDocument; + + Map toJson() => { + Field.selections: selections, + Field.textDocument: textDocument, + }; +} + +/// A reference to a text document in the editor. +/// +/// The [uriAsString] is a file URI to the text document. +/// +/// The [version] is an integer corresponding to LSP's +/// [VersionedTextDocumentIdentifier](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#versionedTextDocumentIdentifier) +class TextDocument { + TextDocument({required this.uriAsString, required this.version}); + + TextDocument.fromJson(Map map) + : this( + uriAsString: map[Field.uri] as String, + version: map[Field.version] as int?, + ); + + final String uriAsString; + final int? version; + + Map toJson() => { + Field.uri: uriAsString, + Field.version: version, + }; + + @override + bool operator ==(Object other) { + return other is TextDocument && + other.uriAsString == uriAsString && + other.version == version; + } + + @override + int get hashCode => Object.hash(uriAsString, version); +} + +/// The starting and ending cursor positions in the editor. +class EditorSelection { + EditorSelection({required this.active, required this.anchor}); + + EditorSelection.fromJson(Map map) + : this( + active: CursorPosition.fromJson( + map[Field.active] as Map, + ), + anchor: CursorPosition.fromJson( + map[Field.anchor] as Map, + ), + ); + + final CursorPosition active; + final CursorPosition anchor; + + Map toJson() => { + Field.active: active.toJson(), + Field.anchor: anchor.toJson(), + }; +} + +/// A range in the editor expressed as (zero-based) start and end positions. +class EditorRange { + EditorRange({required this.start, required this.end}); + + EditorRange.fromJson(Map map) + : this( + start: CursorPosition.fromJson( + map[Field.start] as Map, + ), + end: CursorPosition.fromJson(map[Field.end] as Map), + ); + + /// The range's start position. + final CursorPosition start; + + /// The range's end position. + final CursorPosition end; + + Map toJson() => { + Field.start: start.toJson(), + Field.end: end.toJson(), + }; +} + +/// Representation of a single cursor position in the editor. +/// +/// The cursor position is after the given [character] of the [line]. +class CursorPosition { + CursorPosition({required this.character, required this.line}); + + CursorPosition.fromJson(Map map) + : this( + character: map[Field.character] as int, + line: map[Field.line] as int, + ); + + /// The zero-based character number of this position. + final int character; + + /// The zero-based line number of this position. + final int line; + + Map toJson() => { + Field.character: character, + Field.line: line, + }; + + @override + bool operator ==(Object other) { + return other is CursorPosition && + other.character == character && + other.line == line; + } + + @override + int get hashCode => Object.hash(character, line); +} + +/// Parameters for the `navigateToCode` request. +class CodeLocation { + const CodeLocation({required this.uri, this.line, this.column}); + + /// The URI of the location to navigate to. Only `file://` URIs are supported + /// unless the service registration's `capabilities` indicate other schemes + /// are supported. + /// + /// Editors should return error code 144 if a caller passes a URI with an + /// unsupported scheme. + final String uri; + + /// Optional 1-based line number to navigate to. + final int? line; + + /// Optional 1-based column number to navigate to. + final int? column; + + Map toJson() => { + Field.uri: uri, + Field.line: ?line, + Field.column: ?column, + }; +} + +/// Constants for all fields used in JSON maps to avoid literal strings that +/// may have typos sprinkled throughout the API classes. +abstract class Field { + static const active = 'active'; + static const anchor = 'anchor'; + static const backgroundColor = 'backgroundColor'; + static const character = 'character'; + static const column = 'column'; + static const end = 'end'; + static const fontSize = 'fontSize'; + static const foregroundColor = 'foregroundColor'; + static const isDarkMode = 'isDarkMode'; + static const line = 'line'; + static const selections = 'selections'; + static const start = 'start'; + static const textDocument = 'textDocument'; + static const theme = 'theme'; + static const uri = 'uri'; + static const version = 'version'; +} diff --git a/.widget_preview/lib/src/dtd/utils.dart b/.widget_preview/lib/src/dtd/utils.dart new file mode 100644 index 0000000..292fedf --- /dev/null +++ b/.widget_preview/lib/src/dtd/utils.dart @@ -0,0 +1,39 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @docImport: package:dtd/dtd.dart +import 'package:dtd/dtd.dart'; +import 'package:json_rpc_2/json_rpc_2.dart'; + +extension WidgetPreviewScaffoldDtdUtils on DartToolingDaemon { + /// A [streamListen] implementation that ignores already subscribed exceptions. + Future safeStreamListen(String streamId) async { + try { + await streamListen(streamId); + } on RpcException catch (e) { + if (e.code != RpcErrorCodes.kStreamAlreadySubscribed) { + // TODO(bkonyi): consider logging an error. + rethrow; + } + } + } + + /// A [call] implementation that returns `null` if the service disappears or the method is not + /// found. + Future safeCall( + String? serviceName, + String methodName, { + Map? params, + }) async { + try { + return await call(serviceName, methodName, params: params); + } on RpcException catch (e) { + if (e.code != RpcErrorCodes.kMethodNotFound && + e.code != RpcErrorCodes.kServiceDisappeared) { + rethrow; + } + return null; + } + } +} diff --git a/.widget_preview/lib/src/generated_preview.dart b/.widget_preview/lib/src/generated_preview.dart new file mode 100644 index 0000000..0099e46 --- /dev/null +++ b/.widget_preview/lib/src/generated_preview.dart @@ -0,0 +1,6 @@ +// ignore_for_file: implementation_imports + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'widget_preview.dart' as _i1; + +List<_i1.WidgetPreview> previews() => []; diff --git a/.widget_preview/lib/src/split.dart b/.widget_preview/lib/src/split.dart new file mode 100644 index 0000000..0a07fff --- /dev/null +++ b/.widget_preview/lib/src/split.dart @@ -0,0 +1,365 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// NOTE: most of the code in this file was pulled from the DevTools `Split` +// implementation. + +import 'dart:math' as math; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import 'utils/pointer_events/pointer_events.dart'; + +// Method to convert degrees to radians +double degToRad(num deg) => deg * (math.pi / 180.0); + +/// A small double value, used to ensure that comparisons between double are +/// valid. +const defaultEpsilon = 1 / 1000; + +/// A widget that takes a list of children, lays them out along [axis], and +/// allows the user to resize them. +/// +/// The user can customize the amount of space allocated to each child by +/// dragging a divider between them. +/// +/// [initialFractions] defines how much space to give each child when building +/// this widget. +/// +/// [minSizes] defines the minimum size that each child can be set to when +/// adjusting the sizes of the children. +final class SplitPane extends StatefulWidget { + /// Builds a split oriented along [axis]. + SplitPane({ + super.key, + required this.axis, + required this.children, + required this.initialFractions, + this.minSizes, + this.splitters, + }) : assert(children.length >= 2), + assert(initialFractions.length >= 2), + assert(children.length == initialFractions.length) { + _verifyFractionsSumTo1(initialFractions); + if (minSizes != null) { + assert(minSizes!.length == children.length); + } + if (splitters != null) { + assert(splitters!.length == children.length - 1); + } + } + + /// The main axis the children will lay out on. + /// + /// If [Axis.horizontal], the children will be placed in a [Row] + /// and they will be horizontally resizable. + /// + /// If [Axis.vertical], the children will be placed in a [Column] + /// and they will be vertically resizable. + /// + /// Cannot be null. + final Axis axis; + + /// The children that will be laid out along [axis]. + final List children; + + /// The fraction of the layout to allocate to each child in [children]. + /// + /// The index of [initialFractions] corresponds to the child at index of + /// [children]. + final List initialFractions; + + /// The minimum size each child is allowed to be. + final List? minSizes; + + /// Splitter widgets to divide [children]. + /// + /// If this is null, a default splitter will be used to divide [children]. + final List? splitters; + + /// The key passed to the divider between children[index] and + /// children[index + 1]. + /// + /// Visible to grab it in tests. + @visibleForTesting + Key dividerKey(int index) => Key('$this dividerKey $index'); + + static Axis axisFor(BuildContext context, double horizontalAspectRatio) { + final screenSize = MediaQuery.of(context).size; + final aspectRatio = screenSize.width / screenSize.height; + if (aspectRatio >= horizontalAspectRatio) return Axis.horizontal; + return Axis.vertical; + } + + @override + State createState() => _SplitPaneState(); +} + +final class _SplitPaneState extends State { + late final List fractions; + bool _isDragging = false; + + bool get isHorizontal => widget.axis == Axis.horizontal; + + @override + void initState() { + super.initState(); + fractions = List.of(widget.initialFractions); + } + + @override + void dispose() { + if (_isDragging) { + toggleIframePointerEvents(false); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: _buildLayout); + } + + Widget _buildLayout(BuildContext _, BoxConstraints constraints) { + final width = constraints.maxWidth; + final height = constraints.maxHeight; + final axisSize = isHorizontal ? width : height; + + final availableSize = axisSize - _totalSplitterSize(); + + // Size calculation helpers. + double minSizeForIndex(int index) { + if (widget.minSizes == null) return 0.0; + + double totalMinSize = 0; + for (final minSize in widget.minSizes!) { + totalMinSize += minSize; + } + + // Reduce the min sizes gracefully if the total required min size for all + // children is greater than the available size for children. + return totalMinSize > availableSize + ? widget.minSizes![index] * availableSize / totalMinSize + : widget.minSizes![index]; + } + + double minFractionForIndex(int index) => + minSizeForIndex(index) / availableSize; + + void clampFraction(int index) { + fractions[index] = fractions[index].clamp( + minFractionForIndex(index), + 1.0, + ); + } + + double sizeForIndex(int index) => availableSize * fractions[index]; + + double fractionDeltaRequired = 0.0; + double fractionDeltaAvailable = 0.0; + + double deltaFromMinimumSize(int index) => + fractions[index] - minFractionForIndex(index); + + for (int i = 0; i < fractions.length; ++i) { + final delta = deltaFromMinimumSize(i); + if (delta < 0) { + fractionDeltaRequired -= delta; + } else { + fractionDeltaAvailable += delta; + } + } + if (fractionDeltaRequired > 0) { + // Likely due to a change in the available size, the current fractions for + // the children do not obey the min size constraints. + // The min size constraints for children are scaled so it is always + // possible to meet them. A scaleFactor greater than 1 would indicate that + // it is impossible to meet the constraints. + double scaleFactor = fractionDeltaRequired / fractionDeltaAvailable; + assert(scaleFactor <= 1 + defaultEpsilon); + scaleFactor = math.min(scaleFactor, 1.0); + for (int i = 0; i < fractions.length; ++i) { + final delta = deltaFromMinimumSize(i); + if (delta < 0) { + // This is equivalent to adding delta but avoids rounding error. + fractions[i] = minFractionForIndex(i); + } else { + // Reduce all fractions that are above their minimum size by an amount + // proportional to their ability to reduce their size without + // violating their minimum size constraints. + fractions[i] -= delta * scaleFactor; + } + } + } + + // Determine what fraction to give each child, including enough space to + // display the divider. + final sizes = List.generate(fractions.length, (i) => sizeForIndex(i)); + + void updateSpacing(DragUpdateDetails dragDetails, int splitterIndex) { + final dragDelta = isHorizontal + ? dragDetails.delta.dx + : dragDetails.delta.dy; + final fractionalDelta = dragDelta / axisSize; + + // Returns the actual delta applied to elements before the splitter. + double updateSpacingBeforeSplitterIndex(double delta) { + final startingDelta = delta; + var index = splitterIndex; + while (index >= 0) { + fractions[index] += delta; + final minFraction = minFractionForIndex(index); + if (fractions[index] >= minFraction) { + clampFraction(index); + return startingDelta; + } + delta = fractions[index] - minFraction; + clampFraction(index); + index--; + } + // At this point, we know that both [startingDelta] and [delta] are + // negative, and that [delta] represents the overflow that did not get + // applied. + return startingDelta - delta; + } + + // Returns the actual delta applied to elements after the splitter. + double updateSpacingAfterSplitterIndex(double delta) { + final startingDelta = delta; + var index = splitterIndex + 1; + while (index < fractions.length) { + fractions[index] += delta; + final minFraction = minFractionForIndex(index); + if (fractions[index] >= minFraction) { + clampFraction(index); + return startingDelta; + } + delta = fractions[index] - minFraction; + clampFraction(index); + index++; + } + // At this point, we know that both [startingDelta] and [delta] are + // negative, and that [delta] represents the overflow that did not get + // applied. + return startingDelta - delta; + } + + setState(() { + // Update the fraction of space consumed by the children. Always update + // the shrinking children first so that we do not over-increase the size + // of the growing children and cause layout overflow errors. + if (fractionalDelta <= 0.0) { + final appliedDelta = updateSpacingBeforeSplitterIndex( + fractionalDelta, + ); + updateSpacingAfterSplitterIndex(-appliedDelta); + } else { + final appliedDelta = updateSpacingAfterSplitterIndex( + -fractionalDelta, + ); + updateSpacingBeforeSplitterIndex(-appliedDelta); + } + }); + _verifyFractionsSumTo1(fractions); + } + + final children = []; + for (int i = 0; i < widget.children.length; i++) { + children.addAll([ + SizedBox( + width: isHorizontal ? sizes[i] : width, + height: isHorizontal ? height : sizes[i], + child: widget.children[i], + ), + if (i < widget.children.length - 1) + MouseRegion( + cursor: isHorizontal + ? SystemMouseCursors.resizeColumn + : SystemMouseCursors.resizeRow, + child: GestureDetector( + key: widget.dividerKey(i), + behavior: HitTestBehavior.translucent, + onPanStart: (details) { + _isDragging = true; + toggleIframePointerEvents(true); + }, + onPanUpdate: (details) => updateSpacing(details, i), + onPanEnd: (details) { + _isDragging = false; + toggleIframePointerEvents(false); + }, + onPanCancel: () { + _isDragging = false; + toggleIframePointerEvents(false); + }, + // DartStartBehavior.down is needed to keep the mouse pointer stuck to + // the drag bar. There still appears to be a few frame lag before the + // drag action triggers which is't ideal but isn't a launch blocker. + dragStartBehavior: DragStartBehavior.down, + child: widget.splitters != null + ? widget.splitters![i] + : DefaultSplitter(isHorizontal: isHorizontal), + ), + ), + ]); + } + return Flex( + direction: widget.axis, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ); + } + + double _totalSplitterSize() { + final numSplitters = widget.children.length - 1; + if (widget.splitters == null) { + return numSplitters * DefaultSplitter.splitterWidth; + } else { + var totalSize = 0.0; + for (final splitter in widget.splitters!) { + totalSize += isHorizontal + ? splitter.preferredSize.width + : splitter.preferredSize.height; + } + return totalSize; + } + } +} + +final class DefaultSplitter extends StatelessWidget { + const DefaultSplitter({super.key, required this.isHorizontal}); + + static const iconSize = 24.0; + static const splitterWidth = 12.0; + + final bool isHorizontal; + + @override + Widget build(BuildContext context) { + return Transform.rotate( + angle: isHorizontal ? degToRad(90.0) : degToRad(0.0), + child: Align( + widthFactor: 0.5, + heightFactor: 0.5, + child: Icon( + Icons.drag_handle, + size: iconSize, + color: Theme.of(context).focusColor, + ), + ), + ); + } +} + +void _verifyFractionsSumTo1(List fractions) { + var sumFractions = 0.0; + for (final fraction in fractions) { + sumFractions += fraction; + } + assert( + (1.0 - sumFractions).abs() < defaultEpsilon, + 'Fractions should sum to 1.0, but instead sum to $sumFractions:\n$fractions', + ); +} diff --git a/.widget_preview/lib/src/theme/_ide_theme_desktop.dart b/.widget_preview/lib/src/theme/_ide_theme_desktop.dart new file mode 100644 index 0000000..6c8d16f --- /dev/null +++ b/.widget_preview/lib/src/theme/_ide_theme_desktop.dart @@ -0,0 +1,10 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// NOTE: originally from package:devtools_app_shared + +import 'ide_theme.dart'; + +/// Load any IDE-supplied theming. +IdeTheme getIdeTheme() => IdeTheme(); diff --git a/.widget_preview/lib/src/theme/_ide_theme_web.dart b/.widget_preview/lib/src/theme/_ide_theme_web.dart new file mode 100644 index 0000000..d046142 --- /dev/null +++ b/.widget_preview/lib/src/theme/_ide_theme_web.dart @@ -0,0 +1,43 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// NOTE: originally from package:devtools_app_shared + +import 'dart:ui'; + +import 'package:web/web.dart'; + +import '../utils/url/url.dart'; +import 'ide_theme.dart'; + +/// Load any IDE-supplied theming. +IdeTheme getIdeTheme() { + final queryParams = IdeThemeQueryParams(loadQueryParams()); + + final overrides = IdeTheme( + backgroundColor: queryParams.backgroundColor, + foregroundColor: queryParams.foregroundColor, + isDarkMode: queryParams.darkMode, + ); + + // If the environment has provided a background color, set it immediately + // to avoid a white page until the first Flutter frame is rendered. + if (overrides.backgroundColor != null) { + document.body!.style.backgroundColor = toCssHexColor( + overrides.backgroundColor!, + ); + } + + return overrides; +} + +/// Converts a dart:ui Color into #RRGGBBAA format for use in CSS. +String toCssHexColor(Color color) { + // In CSS Hex, Alpha comes last, but in Flutter's `value` field, alpha is + // in the high bytes, so just using `value.toRadixString(16)` will put alpha + // in the wrong position. + String hex(double channelValue) => + (channelValue * 255).round().toRadixString(16).padLeft(2, '0'); + return '#${hex(color.r)}${hex(color.g)}${hex(color.b)}${hex(color.a)}'; +} diff --git a/.widget_preview/lib/src/theme/ide_theme.dart b/.widget_preview/lib/src/theme/ide_theme.dart new file mode 100644 index 0000000..ebdd293 --- /dev/null +++ b/.widget_preview/lib/src/theme/ide_theme.dart @@ -0,0 +1,47 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// NOTE: originally from package:devtools_app_shared + +import 'package:flutter/widgets.dart'; + +import '../utils/color_utils.dart'; +import 'theme.dart'; + +export '_ide_theme_desktop.dart' + if (dart.library.js_interop) '_ide_theme_web.dart'; + +/// IDE-supplied theming. +final class IdeTheme { + const IdeTheme({this.backgroundColor, this.foregroundColor, bool? isDarkMode}) + // ignore: prefer_initializing_formals + : _isDarkMode = isDarkMode; + + final Color? backgroundColor; + final Color? foregroundColor; + final bool? _isDarkMode; + + bool get isDarkMode => _isDarkMode ?? useDarkThemeAsDefault; + + /// Whether the IDE specified the DevTools color theme. + /// + /// If this returns false, that means the + /// [IdeThemeQueryParams.devToolsThemeKey] query parameter was not passed to + /// DevTools from the IDE. + bool get ideSpecifiedTheme => _isDarkMode != null; +} + +extension type IdeThemeQueryParams(Map params) { + Color? get backgroundColor => tryParseColor(params[backgroundColorKey]); + + Color? get foregroundColor => tryParseColor(params[foregroundColorKey]); + + bool get darkMode => params[devToolsThemeKey] != lightThemeValue; + + static const backgroundColorKey = 'backgroundColor'; + static const foregroundColorKey = 'foregroundColor'; + static const devToolsThemeKey = 'theme'; + static const lightThemeValue = 'light'; + static const darkThemeValue = 'dark'; +} diff --git a/.widget_preview/lib/src/theme/theme.dart b/.widget_preview/lib/src/theme/theme.dart new file mode 100644 index 0000000..332890f --- /dev/null +++ b/.widget_preview/lib/src/theme/theme.dart @@ -0,0 +1,365 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// NOTE: originally from package:devtools_app_shared + +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:widget_preview_scaffold/src/utils/color_utils.dart'; + +import 'ide_theme.dart'; + +// TODO(kenz): try to eliminate as many custom colors as possible, and pull +// colors only from the [lightColorScheme] and the [darkColorScheme]. + +/// Whether dark theme should be used as the default theme if none has been +/// explicitly set. +const useDarkThemeAsDefault = true; + +/// Constructs the light or dark theme for the app taking into account +/// IDE-supplied theming. +ThemeData themeFor({ + required bool isDarkTheme, + required IdeTheme ideTheme, + required ThemeData theme, +}) { + final colorTheme = isDarkTheme + ? _darkTheme(ideTheme: ideTheme, theme: theme) + : _lightTheme(ideTheme: ideTheme, theme: theme); + + return colorTheme.copyWith( + primaryTextTheme: theme.primaryTextTheme.merge(colorTheme.primaryTextTheme), + textTheme: theme.textTheme.merge(colorTheme.textTheme), + ); +} + +ThemeData _darkTheme({required IdeTheme ideTheme, required ThemeData theme}) { + final background = isValidDarkColor(ideTheme.backgroundColor) + ? ideTheme.backgroundColor! + : theme.colorScheme.surface; + return _baseTheme(theme: theme, backgroundColor: background); +} + +ThemeData _lightTheme({required IdeTheme ideTheme, required ThemeData theme}) { + final background = isValidLightColor(ideTheme.backgroundColor) + ? ideTheme.backgroundColor! + : theme.colorScheme.surface; + return _baseTheme(theme: theme, backgroundColor: background); +} + +ThemeData _baseTheme({ + required ThemeData theme, + required Color backgroundColor, +}) { + // TODO(kenz): do we need to pass in the foreground color from the [IdeTheme] + // as well as the background color? + const kCardRadius = Radius.circular(12); + return theme.copyWith( + tabBarTheme: theme.tabBarTheme.copyWith( + tabAlignment: TabAlignment.start, + labelStyle: theme.regularTextStyle, + labelPadding: const EdgeInsets.symmetric( + horizontal: defaultTabBarPadding, + ), + ), + canvasColor: backgroundColor, + scaffoldBackgroundColor: backgroundColor, + sliderTheme: theme.sliderTheme.copyWith( + trackHeight: 2.0, + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 5.0), + overlayShape: const RoundSliderOverlayShape(overlayRadius: 10.0), + ), + iconButtonTheme: IconButtonThemeData( + style: IconButton.styleFrom( + padding: const EdgeInsets.all(densePadding), + minimumSize: const Size(defaultButtonHeight, defaultButtonHeight), + fixedSize: const Size(defaultButtonHeight, defaultButtonHeight), + iconSize: defaultIconSize, + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + minimumSize: const Size(buttonMinWidth, defaultButtonHeight), + fixedSize: const Size.fromHeight(defaultButtonHeight), + foregroundColor: theme.colorScheme.onSurface, + padding: const EdgeInsets.symmetric(horizontal: denseSpacing), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + padding: const EdgeInsets.all(densePadding), + minimumSize: const Size(buttonMinWidth, defaultButtonHeight), + fixedSize: const Size.fromHeight(defaultButtonHeight), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + minimumSize: const Size(buttonMinWidth, defaultButtonHeight), + fixedSize: const Size.fromHeight(defaultButtonHeight), + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + padding: const EdgeInsets.symmetric(horizontal: denseSpacing), + ), + ), + menuButtonTheme: MenuButtonThemeData( + style: ButtonStyle( + textStyle: WidgetStatePropertyAll(theme.regularTextStyle), + fixedSize: const WidgetStatePropertyAll(Size.fromHeight(24.0)), + ), + ), + expansionTileTheme: ExpansionTileThemeData( + backgroundColor: backgroundColor.brighten(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(kCardRadius), + ), + collapsedShape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(kCardRadius), + ), + ), + listTileTheme: ListTileThemeData( + dense: true, + tileColor: backgroundColor.brighten(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(kCardRadius), + ), + ), + dropdownMenuTheme: DropdownMenuThemeData(textStyle: theme.regularTextStyle), + primaryTextTheme: _devToolsTextTheme(theme, theme.primaryTextTheme), + textTheme: _devToolsTextTheme(theme, theme.textTheme), + colorScheme: theme.colorScheme.copyWith(surface: backgroundColor), + ); +} + +TextTheme _devToolsTextTheme(ThemeData theme, TextTheme textTheme) { + return textTheme.copyWith( + displayLarge: theme.boldTextStyle.copyWith(fontSize: 24), + displayMedium: theme.boldTextStyle.copyWith(fontSize: 22), + displaySmall: theme.boldTextStyle.copyWith(fontSize: 20), + headlineLarge: theme.regularTextStyle.copyWith( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + headlineMedium: theme.regularTextStyle.copyWith( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + headlineSmall: theme.regularTextStyle.copyWith( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + titleLarge: theme._largeText.copyWith(fontWeight: FontWeight.w500), + titleMedium: theme.regularTextStyle.copyWith(fontWeight: FontWeight.w500), + titleSmall: theme._smallText.copyWith(fontWeight: FontWeight.w500), + bodyLarge: theme._largeText, + bodyMedium: theme.regularTextStyle, + bodySmall: theme._smallText, + labelLarge: theme._largeText, + labelMedium: theme.regularTextStyle, + labelSmall: theme._smallText, + ); +} + +/// Light theme color scheme generated from DevTools Figma file. +/// +/// Do not manually change these values. +const lightColorScheme = ColorScheme( + brightness: Brightness.light, + primary: Color(0xFF195BB9), + onPrimary: Color(0xFFFFFFFF), + primaryContainer: Color(0xFFD8E2FF), + onPrimaryContainer: Color(0xFF001A41), + secondary: Color(0xFF575E71), + onSecondary: Color(0xFFFFFFFF), + secondaryContainer: Color(0xFFDBE2F9), + onSecondaryContainer: Color(0xFF141B2C), + tertiary: Color(0xFF815600), + onTertiary: Color(0xFFFFFFFF), + tertiaryContainer: Color(0xFFFFDDB1), + onTertiaryContainer: Color(0xFF291800), + error: Color(0xFFBA1A1A), + errorContainer: Color(0xFFFFDAD5), + onError: Color(0xFFFFFFFF), + onErrorContainer: Color(0xFF410002), + surface: Color(0xFFFFFFFF), + onSurface: Color(0xFF1B1B1F), + surfaceContainerHighest: Color(0xFFE1E2EC), + onSurfaceVariant: Color(0xFF44474F), + outline: Color(0xFF75777F), + onInverseSurface: Color(0xFFF2F0F4), + inverseSurface: Color(0xFF303033), + inversePrimary: Color(0xFFADC6FF), + shadow: Color(0xFF000000), + surfaceTint: Color(0xFF195BB9), + outlineVariant: Color(0xFFC4C6D0), + scrim: Color(0xFF000000), +); + +/// Dark theme color scheme generated from DevTools Figma file. +/// +/// Do not manually change these values. +const darkColorScheme = ColorScheme( + brightness: Brightness.dark, + primary: Color(0xFFADC6FF), + onPrimary: Color(0xFF002E69), + primaryContainer: Color(0xFF004494), + onPrimaryContainer: Color(0xFFD8E2FF), + secondary: Color(0xFFBFC6DC), + onSecondary: Color(0xFF293041), + secondaryContainer: Color(0xFF3F4759), + onSecondaryContainer: Color(0xFFDBE2F9), + tertiary: Color(0xFFFEBA4B), + onTertiary: Color(0xFF442B00), + tertiaryContainer: Color(0xFF624000), + onTertiaryContainer: Color(0xFFFFDDB1), + error: Color(0xFFFFB4AB), + errorContainer: Color(0xFF930009), + onError: Color(0xFF690004), + onErrorContainer: Color(0xFFFFDAD5), + surface: Color(0xFF1B1B1F), + onSurface: Color(0xFFC7C6CA), + surfaceContainerHighest: Color(0xFF44474F), + onSurfaceVariant: Color(0xFFC4C6D0), + outline: Color(0xFF8E9099), + onInverseSurface: Color(0xFF1B1B1F), + inverseSurface: Color(0xFFE3E2E6), + inversePrimary: Color(0xFF195BB9), + shadow: Color(0xFF000000), + surfaceTint: Color(0xFFADC6FF), + outlineVariant: Color(0xFF44474F), + scrim: Color(0xFF000000), +); + +/// Threshold used to determine whether a colour is light/dark enough for us to +/// override the default DevTools themes with. +/// +/// A value of 0.5 would result in all colours being considered light/dark, and +/// a value of 0.12 allowing around only the 12% darkest/lightest colours by +/// Flutter's luminance calculation. +/// 12% was chosen because VS Code's default light background color is #f3f3f3 +/// which is a little under 11%. +const _lightDarkLuminanceThreshold = 0.12; + +bool isValidDarkColor(Color? color) { + if (color == null) { + return false; + } + return color.computeLuminance() <= _lightDarkLuminanceThreshold; +} + +bool isValidLightColor(Color? color) { + if (color == null) { + return false; + } + return color.computeLuminance() >= 1 - _lightDarkLuminanceThreshold; +} + +// Size constants: +const defaultButtonHeight = 26.0; +const buttonMinWidth = 26.0; + +const defaultIconSize = 14.0; + +// Padding / spacing constants: +const extraLargeSpacing = 32.0; +const largeSpacing = 16.0; +const defaultSpacing = 12.0; +const intermediateSpacing = 10.0; +const denseSpacing = 8.0; + +const defaultTabBarPadding = 14.0; +const tabBarSpacing = 8.0; +const denseRowSpacing = 6.0; + +const densePadding = 4.0; + +// Other UI related constants: +final defaultBorderRadius = BorderRadius.circular(_defaultBorderRadiusValue); +const defaultRadius = Radius.circular(_defaultBorderRadiusValue); +const _defaultBorderRadiusValue = 16.0; + +const defaultElevation = 4.0; + +// Font size constants: +const largeFontSize = 14.0; +const defaultFontSize = 12.0; +const smallFontSize = 10.0; + +extension DevToolsSharedColorScheme on ColorScheme { + bool get isLight => brightness == Brightness.light; + + bool get isDark => brightness == Brightness.dark; + + Color get subtleTextColor => const Color(0xFF919094); + + Color get _devtoolsLink => + isLight ? const Color(0xFF1976D2) : Colors.lightBlueAccent; + + Color get tooltipTextColor => isLight ? Colors.white : Colors.black; +} + +/// Utility extension methods to the [ThemeData] class. +extension ThemeDataExtension on ThemeData { + /// Returns whether we are currently using a dark theme. + bool get isDarkTheme => brightness == Brightness.dark; + + TextStyle get regularTextStyle => fixBlurryText( + TextStyle(color: colorScheme.onSurface, fontSize: defaultFontSize), + ); + + TextStyle regularTextStyleWithColor(Color? color, {Color? backgroundColor}) => + regularTextStyle.copyWith(color: color, backgroundColor: backgroundColor); + + TextStyle get _smallText => + regularTextStyle.copyWith(fontSize: smallFontSize); + + TextStyle get _largeText => + regularTextStyle.copyWith(fontSize: largeFontSize); + + TextStyle get errorTextStyle => regularTextStyleWithColor(colorScheme.error); + + TextStyle get boldTextStyle => + regularTextStyle.copyWith(fontWeight: FontWeight.bold); + + TextStyle get subtleTextStyle => + regularTextStyle.copyWith(color: colorScheme.subtleTextColor); + + TextStyle get fixedFontStyle => fixBlurryText( + regularTextStyle.copyWith( + fontFamily: GoogleFonts.robotoMono().fontFamily, + // Slightly smaller for fixes font text since it will appear larger + // to begin with. + fontSize: defaultFontSize - 1, + ), + ); + + TextStyle get subtleFixedFontStyle => + fixedFontStyle.copyWith(color: colorScheme.subtleTextColor); + + TextStyle get selectedSubtleTextStyle => + subtleTextStyle.copyWith(color: colorScheme.onSurface); + + TextStyle get tooltipFixedFontStyle => + fixedFontStyle.copyWith(color: colorScheme.tooltipTextColor); + + TextStyle get fixedFontLinkStyle => fixedFontStyle.copyWith( + color: colorScheme._devtoolsLink, + decoration: TextDecoration.underline, + ); + + TextStyle get linkTextStyle => fixBlurryText( + TextStyle( + color: colorScheme._devtoolsLink, + decoration: TextDecoration.underline, + fontSize: defaultFontSize, + ), + ); +} + +/// Returns a [TextStyle] with [FontFeature.proportionalFigures] applied to +/// fix blurry text. +TextStyle fixBlurryText(TextStyle style) { + return style.copyWith( + fontFeatures: [const FontFeature.proportionalFigures()], + ); +} diff --git a/.widget_preview/lib/src/utils.dart b/.widget_preview/lib/src/utils.dart new file mode 100644 index 0000000..32949af --- /dev/null +++ b/.widget_preview/lib/src/utils.dart @@ -0,0 +1,202 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/widget_previews.dart'; + +import 'widget_preview.dart'; + +Iterable buildMultiWidgetPreview({ + required String packageName, + required String scriptUri, + required int line, + required int column, + required MultiPreview preview, + required Object? Function() previewFunction, +}) { + return preview.transform().map( + (p) => buildWidgetPreview( + packageName: packageName, + scriptUri: scriptUri, + line: line, + column: column, + transformedPreview: p, + previewFunction: previewFunction, + ), + ); +} + +WidgetPreview buildWidgetPreview({ + required String packageName, + required String scriptUri, + required int line, + required int column, + required Preview transformedPreview, + required Object? Function() previewFunction, +}) { + Widget Function() previewBuilder; + if (previewFunction is WidgetBuilder Function()) { + previewBuilder = () { + return Builder(builder: previewFunction()); + }; + } else { + previewBuilder = previewFunction as Widget Function(); + } + return WidgetPreview( + builder: previewBuilder, + scriptUri: scriptUri, + line: line, + column: column, + previewData: transformedPreview, + packageName: packageName, + ); +} + +WidgetPreview buildWidgetPreviewError({ + required String packageName, + required String scriptUri, + required int line, + required int column, + required String packageUri, + required String functionName, + required bool dependencyHasErrors, +}) { + var errorMessage = '$packageUri has errors!'; + if (dependencyHasErrors) { + errorMessage = 'Dependency of $errorMessage'; + } + return WidgetPreview( + builder: () => Text('$functionName: $errorMessage'), + scriptUri: scriptUri, + line: line, + column: column, + previewData: const Preview(group: 'Invalid Previews'), + packageName: packageName, + ); +} + +/// A basic vertical spacer. +class VerticalSpacer extends StatelessWidget { + /// Creates a basic vertical spacer. + const VerticalSpacer({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox(height: 10); + } +} + +/// A basic horizontal spacer. +class HorizontalSpacer extends StatelessWidget { + /// Creates a basic vertical spacer. + const HorizontalSpacer({super.key}); + + @override + Widget build(BuildContext context) { + return const SizedBox(width: 10); + } +} + +/// A widget that explicitly responds to hot reload events. +/// +/// Hot reload will always result in [reassemble] being called. +class HotReloadListener extends StatefulWidget { + const HotReloadListener({ + super.key, + required this.onHotReload, + required this.child, + }); + + final VoidCallback onHotReload; + final Widget child; + + @override + HotReloadListenerState createState() => HotReloadListenerState(); +} + +class HotReloadListenerState extends State { + @override + void reassemble() { + super.reassemble(); + widget.onHotReload(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} + +/// Wraps [child] in a border with default styling. +/// +/// This border can optionally be made non-uniform by setting any of +/// [showTop], [showBottom], [showLeft] or [showRight] to false. +/// +/// Originally from DevTools. +final class OutlineDecoration extends StatelessWidget { + const OutlineDecoration({ + super.key, + this.child, + this.showTop = true, + this.showBottom = true, + this.showLeft = true, + this.showRight = true, + }); + + factory OutlineDecoration.onlyBottom({required Widget? child}) => + OutlineDecoration( + showTop: false, + showLeft: false, + showRight: false, + child: child, + ); + + factory OutlineDecoration.onlyTop({required Widget? child}) => + OutlineDecoration( + showBottom: false, + showLeft: false, + showRight: false, + child: child, + ); + + factory OutlineDecoration.onlyLeft({required Widget? child}) => + OutlineDecoration( + showBottom: false, + showTop: false, + showRight: false, + child: child, + ); + + factory OutlineDecoration.onlyRight({required Widget? child}) => + OutlineDecoration( + showBottom: false, + showTop: false, + showLeft: false, + child: child, + ); + + final bool showTop; + final bool showBottom; + final bool showLeft; + final bool showRight; + + final Widget? child; + + @override + Widget build(BuildContext context) { + final color = Theme.of(context).focusColor; + final border = BorderSide(color: color); + return Container( + decoration: BoxDecoration( + border: Border( + left: showLeft ? border : BorderSide.none, + right: showRight ? border : BorderSide.none, + top: showTop ? border : BorderSide.none, + bottom: showBottom ? border : BorderSide.none, + ), + ), + child: child, + ); + } +} diff --git a/.widget_preview/lib/src/utils/color_utils.dart b/.widget_preview/lib/src/utils/color_utils.dart new file mode 100644 index 0000000..03df6d7 --- /dev/null +++ b/.widget_preview/lib/src/utils/color_utils.dart @@ -0,0 +1,70 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +Color? tryParseColor(String? input) { + if (input == null) return null; + + try { + return parseCssHexColor(input); + } catch (e) { + return null; + } +} + +/// Parses a 3 or 6 digit CSS Hex Color into a dart:ui Color. +Color parseCssHexColor(String input) { + // Remove any leading # (and the escaped version to be lenient) + input = input.replaceAll('#', '').replaceAll('%23', ''); + + // Handle 3/4-digit hex codes (eg. #123 == #112233) + if (input.length == 3 || input.length == 4) { + input = input.split('').map((c) => '$c$c').join(); + } + + // Pad alpha with FF. + if (input.length == 6) { + input = '${input}ff'; + } + + // In CSS, alpha is in the lowest bits, but for Flutter's value, it's in the + // highest bits, so move the alpha from the end to the start before parsing. + if (input.length == 8) { + input = '${input.substring(6)}${input.substring(0, 6)}'; + } + final value = int.parse(input, radix: 16); + + return Color(value); +} + +/// Utility extension methods to the [Color] class. +extension ColorExtension on Color { + /// Return a slightly darker color than the current color. + Color darken([double percent = 0.05]) { + assert(0.0 <= percent && percent <= 1.0); + percent = 1.0 - percent; + + final c = this; + return Color.from( + alpha: c.a, + red: c.r * percent, + green: c.g * percent, + blue: c.b * percent, + ); + } + + /// Return a slightly brighter color than the current color. + Color brighten([double percent = 0.05]) { + assert(0.0 <= percent && percent <= 1.0); + + final c = this; + return Color.from( + alpha: c.a, + red: c.r + ((1.0 - c.r) * percent), + green: c.g + ((1.0 - c.g) * percent), + blue: c.b + ((1.0 - c.b) * percent), + ); + } +} diff --git a/.widget_preview/lib/src/utils/pointer_events/_pointer_events_stub.dart b/.widget_preview/lib/src/utils/pointer_events/_pointer_events_stub.dart new file mode 100644 index 0000000..8572078 --- /dev/null +++ b/.widget_preview/lib/src/utils/pointer_events/_pointer_events_stub.dart @@ -0,0 +1,38 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +bool _hasTestIframe = false; +String _pointerEventsStyle = ''; + +/// Toggles the `pointer-events` CSS property on all `iframe` elements in the document. +/// +/// Fakes the behavior on non-web platforms for testing. +void toggleIframePointerEvents(bool disable) { + if (_hasTestIframe) { + _pointerEventsStyle = disable ? 'none' : ''; + } +} + +/// Appends a test iframe to the document. +/// +/// Fakes the behavior on non-web platforms for testing. +void debugAppendTestIframe() { + _hasTestIframe = true; + _pointerEventsStyle = ''; +} + +/// Gets the pointer-events style of the test iframe. +/// +/// Fakes the behavior on non-web platforms for testing. +String? debugGetIframePointerEvents() { + return _hasTestIframe ? _pointerEventsStyle : null; +} + +/// Removes the test iframe. +/// +/// Fakes the behavior on non-web platforms for testing. +void debugRemoveTestIframe() { + _hasTestIframe = false; + _pointerEventsStyle = ''; +} diff --git a/.widget_preview/lib/src/utils/pointer_events/_pointer_events_web.dart b/.widget_preview/lib/src/utils/pointer_events/_pointer_events_web.dart new file mode 100644 index 0000000..bc137a0 --- /dev/null +++ b/.widget_preview/lib/src/utils/pointer_events/_pointer_events_web.dart @@ -0,0 +1,43 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:web/web.dart' as web; + +/// Toggles the `pointer-events` CSS property on all `iframe` elements in the document. +/// +/// On Flutter Web, platform views (like WebViews/iframes) reside in their own HTML +/// DOM trees above the WebGL canvas. This means they capture native mouse events +/// and prevent them from reaching the parent window, causing the resize drag +/// interaction to lose focus if the cursor passes over the iframe. +/// +/// Setting `pointer-events: none` on the iframe DOM elements during a drag +/// operation bypasses this issue, causing the browser to ignore the iframe and +/// deliver all mouse movements to the parent window where the splitter's drag +/// listener can continue smoothly. +void toggleIframePointerEvents(bool disable) { + final iframes = web.document.querySelectorAll('iframe'); + for (int i = 0; i < iframes.length; i++) { + final iframe = iframes.item(i) as web.HTMLElement; + iframe.style.pointerEvents = disable ? 'none' : ''; + } +} + +web.HTMLIFrameElement? _testIframe; + +/// Appends a test iframe to the document. +void debugAppendTestIframe() { + _testIframe = web.HTMLIFrameElement(); + web.document.body!.appendChild(_testIframe!); +} + +/// Gets the pointer-events style of the test iframe. +String? debugGetIframePointerEvents() { + return _testIframe?.style.pointerEvents; +} + +/// Removes the test iframe. +void debugRemoveTestIframe() { + _testIframe?.remove(); + _testIframe = null; +} diff --git a/.widget_preview/lib/src/utils/pointer_events/pointer_events.dart b/.widget_preview/lib/src/utils/pointer_events/pointer_events.dart new file mode 100644 index 0000000..657343b --- /dev/null +++ b/.widget_preview/lib/src/utils/pointer_events/pointer_events.dart @@ -0,0 +1,6 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export '_pointer_events_stub.dart' + if (dart.library.js_interop) '_pointer_events_web.dart'; diff --git a/.widget_preview/lib/src/utils/url/_url_stub.dart b/.widget_preview/lib/src/utils/url/_url_stub.dart new file mode 100644 index 0000000..51ebed0 --- /dev/null +++ b/.widget_preview/lib/src/utils/url/_url_stub.dart @@ -0,0 +1,24 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// NOTE: originally from package:devtools_app_shared + +Map loadQueryParams() => {}; + +/// Gets the URL from the browser. +/// +/// Returns null for non-web platforms. +String? getWebUrl() => null; + +/// Performs a web redirect using window.location.replace(). +/// +/// No-op for non-web platforms. +// Unused parameter lint doesn't make sense for stub files. +void webRedirect(String url) {} + +/// Updates the query parameter with [key] to the new [value], and optionally +/// reloads the page when [reload] is true. +/// +/// No-op for non-web platforms. +void updateQueryParameter(String key, String? value, {bool reload = false}) {} diff --git a/.widget_preview/lib/src/utils/url/_url_web.dart b/.widget_preview/lib/src/utils/url/_url_web.dart new file mode 100644 index 0000000..875a52b --- /dev/null +++ b/.widget_preview/lib/src/utils/url/_url_web.dart @@ -0,0 +1,36 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// NOTE: originally from package:devtools_app_shared + +import 'package:web/web.dart'; + +Map loadQueryParams({String Function(String)? urlModifier}) { + var url = getWebUrl()!; + url = urlModifier?.call(url) ?? url; + return Uri.parse(url).queryParameters; +} + +String? getWebUrl() => window.location.toString(); + +void webRedirect(String url) { + window.location.replace(url); +} + +void updateQueryParameter(String key, String? value, {bool reload = false}) { + final newQueryParams = Map.of(loadQueryParams()); + if (value == null) { + newQueryParams.remove(key); + } else { + newQueryParams[key] = value; + } + final newUri = Uri.parse( + window.location.toString(), + ).replace(queryParameters: newQueryParams); + window.history.replaceState(window.history.state, '', newUri.toString()); + + if (reload) { + window.location.reload(); + } +} diff --git a/.widget_preview/lib/src/utils/url/url.dart b/.widget_preview/lib/src/utils/url/url.dart new file mode 100644 index 0000000..7f15f82 --- /dev/null +++ b/.widget_preview/lib/src/utils/url/url.dart @@ -0,0 +1,7 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// NOTE: originally from package:devtools_app_shared + +export '_url_stub.dart' if (dart.library.js_interop) '_url_web.dart'; diff --git a/.widget_preview/lib/src/widget_preview.dart b/.widget_preview/lib/src/widget_preview.dart new file mode 100644 index 0000000..9f1e65d --- /dev/null +++ b/.widget_preview/lib/src/widget_preview.dart @@ -0,0 +1,137 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widget_previews.dart'; +import 'package:flutter/widgets.dart'; + +/// A group of [WidgetPreview] instances sharing the same group name. +class WidgetPreviewGroup { + const WidgetPreviewGroup({required this.name, required this.previews}); + + /// Returns `false` if the group has no previews. + /// + /// This can happen if a filter is applied that results in no previews matching + /// the filter being part of the group. + bool get hasPreviews => previews.isNotEmpty; + + /// The name of the group, as specified by the 'group' parameter in [Preview]. + final String name; + + /// The set of preview instances which are part of a group with a given [name]. + final List previews; +} + +/// Wraps a [Widget], initializing various state and properties to allow for +/// previewing of the [Widget] in the widget previewer. +class WidgetPreview { + /// Wraps [builder] in a [WidgetPreview] instance that applies some set of + /// properties. + const WidgetPreview({ + required this.builder, + required this.scriptUri, + required this.line, + required this.column, + required this.previewData, + required this.packageName, + }); + + @visibleForTesting + const WidgetPreview.test({ + required this.builder, + required this.previewData, + this.scriptUri = '', + this.line = -1, + this.column = -1, + this.packageName = '', + }); + + /// The absolute file:// URI pointing to the script containing this preview. + /// + /// This matches the URI format sent by IDEs for active location change events. + final String scriptUri; + + /// The line at which the Preview annotation was applied. + final int line; + + /// The column at which the Preview annotation was applied. + final int column; + + /// The name of the package in which a preview was defined. + /// + /// For example, if a preview is defined in 'package:foo/src/bar.dart', this + /// will have the value 'foo'. + final String packageName; + + /// A description to be displayed alongside the preview. + /// + /// If not provided, no name will be associated with the preview. + String? get name => previewData.name; + + /// A callback to build the [Widget] to be rendered in the preview. + final Widget Function() builder; + + Widget Function() get previewBuilder { + if (previewData.wrapper == null) { + return builder; + } + return switch (previewData) { + Preview(:final Widget Function(Widget) wrapper) => () => wrapper( + builder(), + ), + _ => builder, + }; + } + + /// Artificial constraints to be applied to the previewed widget. + /// + /// If not provided, the previewed widget will attempt to set its own + /// constraints. + /// + /// If a dimension has a value of `double.infinity`, the previewed widget + /// will attempt to set its own constraints in the relevant dimension. + Size? get size => previewData.size; + + /// Applies font scaling to text within the [Widget] returned by [builder]. + /// + /// If not provided, the default text scaling factor provided by [MediaQuery] + /// will be used. + double? get textScaleFactor => previewData.textScaleFactor; + + /// Material and Cupertino theming data to be applied to the previewed [Widget]. + /// + /// If not provided, the default theme will be used. + PreviewThemeData? get theme => previewData.theme?.call(); + + /// Sets the initial theme brightness. + /// + /// If not provided, the current system default brightness will be used. + Brightness? get brightness => previewData.brightness; + + /// A callback to return a localization configuration to be applied to the + /// previewed [Widget]. + /// + /// Note: this must be a reference to a static, public function defined as + /// either a top-level function or static member in a class. + PreviewLocalizationsData? get localizations => + previewData.localizations?.call(); + + final Preview previewData; + + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + ..add(DiagnosticsProperty('name', name, ifNull: 'not set')) + ..add(DiagnosticsProperty('group', previewData.group)) + ..add(DiagnosticsProperty('size', size)) + ..add(DiagnosticsProperty('textScaleFactor', textScaleFactor)) + ..add(DiagnosticsProperty('theme', theme)) + ..add(DiagnosticsProperty('brightness', brightness)) + ..add( + DiagnosticsProperty( + 'localizations', + localizations, + ), + ); + } +} diff --git a/.widget_preview/lib/src/widget_preview_inspector_service.dart b/.widget_preview/lib/src/widget_preview_inspector_service.dart new file mode 100644 index 0000000..224fb37 --- /dev/null +++ b/.widget_preview/lib/src/widget_preview_inspector_service.dart @@ -0,0 +1,89 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; +import 'package:widget_preview_scaffold/src/dtd/dtd_connection_info.dart'; +import 'package:widget_preview_scaffold/src/dtd/dtd_services.dart'; +import 'package:widget_preview_scaffold/src/dtd/editor_service.dart'; +import 'package:widget_preview_scaffold/src/widget_preview_rendering.dart'; + +/// A custom [WidgetInspectorService] responsible for routing navigation events +/// to the IDE. +/// +/// IMPORTANT NOTE: this **must** be called before WidgetsFlutterBinding.ensureInitialized() +/// is called, otherwise the inspector service extensions will be registered against +/// the default WidgetInspectorService, causing overrides to not be invoked. +class WidgetPreviewScaffoldInspectorService with WidgetInspectorService { + WidgetPreviewScaffoldInspectorService({required this.dtdServices}) { + WidgetInspectorService.instance = this; + addPubRootDirectories([kProjectRootPath]); + } + + /// The DTD services instance used to communicate with the tool. + final WidgetPreviewScaffoldDtdServices dtdServices; + + // Keys used to specify the creation location of a widget when serializing a + // DiagnosticsNode to JSON. This location is used by the widget inspector + // to jump to the creation location of a selected widget. + static const kFile = 'fileUri'; + static const kLine = 'line'; + static const kColumn = 'column'; + + CodeLocation? _nextNavigationLocation; + + @protected + @override + bool setSelection(Object? object, [String? groupName]) { + // The next navigation event sent to `postEvent` will be for this selection. + // Save the location of preview annotation applications so we can override + // the navigation target in `postEvent`. + if (object is PreviewWidgetElement) { + final previewData = (object.widget as PreviewWidget).preview; + _nextNavigationLocation = CodeLocation( + uri: previewData.scriptUri, + line: previewData.line, + column: previewData.column, + ); + } + final result = super.setSelection(object, groupName); + _nextNavigationLocation = null; + return result; + } + + @override + void postEvent( + String eventKind, + Map eventData, { + String stream = 'Extension', + }) { + // It's unlikely that the widget previewer will be connected to directly by + // an IDE via the VM service, so we forward navigation events via the + // Editor DTD service. + if (eventKind == 'navigate') { + CodeLocation? location = _nextNavigationLocation; + if (eventData case { + kFile: final String file, + kLine: final int line, + kColumn: final int column, + } when location == null) { + location = CodeLocation(uri: file, line: line, column: column); + } else if (location != null) { + // If a [PreviewWidgetElement] was selected, we're not navigating to the + // creation location of the widget. Override the location details in the + // event data, just in case an IDE is attached and listening for + // navigation events through the VM service. + // TODO(bkonyi): determine if this is necessary + eventData.addAll({ + kFile: location.uri, + kLine: location.line!, + kColumn: location.column!, + }); + } + if (location != null) { + dtdServices.navigateToCode(location); + } + } + super.postEvent(eventKind, eventData, stream: stream); + } +} diff --git a/.widget_preview/lib/src/widget_preview_rendering.dart b/.widget_preview/lib/src/widget_preview_rendering.dart new file mode 100644 index 0000000..b731021 --- /dev/null +++ b/.widget_preview/lib/src/widget_preview_rendering.dart @@ -0,0 +1,1251 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widget_previews.dart'; + +import 'package:stack_trace/stack_trace.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +import 'package:widget_preview_scaffold/src/dtd/editor_service.dart'; +import 'package:widget_preview_scaffold/src/split.dart'; +import 'package:widget_preview_scaffold/src/theme/ide_theme.dart'; +import 'package:widget_preview_scaffold/src/theme/theme.dart'; + +import 'package:widget_preview_scaffold/src/controls.dart'; +import 'package:widget_preview_scaffold/src/generated_preview.dart'; +import 'package:widget_preview_scaffold/src/utils.dart'; +import 'package:widget_preview_scaffold/src/widget_preview.dart'; +import 'package:widget_preview_scaffold/src/widget_preview_inspector_service.dart'; +import 'package:widget_preview_scaffold/src/widget_preview_scaffold_controller.dart'; + +/// Displayed when an unhandled exception is thrown when initializing the widget +/// tree for a preview (i.e., before the build phase). +/// +/// Provides users with details about the thrown exception, including the exception +/// contents and a scrollable stack trace. +class WidgetPreviewErrorWidget extends StatelessWidget { + WidgetPreviewErrorWidget({ + super.key, + required this.controller, + required this.error, + required StackTrace stackTrace, + required this.size, + }) : trace = Trace.from(stackTrace).terse; + + final WidgetPreviewScaffoldController controller; + + /// The [Object] that was thrown, resulting in an unhandled exception. + final Object error; + + /// The stack trace identifying where [error] was thrown from. + final Trace trace; + + /// The size of the error widget. + final Size size; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return SizedBox( + height: size.height, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: [ + TextSpan( + text: 'Failed to initialize widget tree: ', + style: theme.boldTextStyle, + ), + TextSpan(text: error.toString(), style: theme.fixedFontStyle), + ], + ), + ), + Text('Stacktrace:', style: theme.boldTextStyle), + ValueListenableBuilder( + valueListenable: controller.editorServiceAvailable, + builder: (context, editorServiceAvailable, child) { + return SelectableText.rich( + TextSpan( + children: _formatFrames( + theme, + trace.frames, + editorServiceAvailable, + ), + style: theme.fixedFontStyle, + ), + ); + }, + ), + ], + ), + ), + ); + } + + List _formatFrames( + ThemeData theme, + List frames, + bool editorServiceAvailable, + ) { + // Figure out the longest path so we know how much to pad. + final int longest = frames + .map((frame) => frame.location.length) + .fold(0, math.max); + + // Print out the stack trace nicely formatted. + return frames.map((frame) { + if (frame is UnparsedFrame) return TextSpan(text: '$frame\n'); + // The Editor.navigateToCode service can't handle Dart core library paths, + // so don't allow for navigation to them. Also disable navigation if the + // Editor service isn't available. + final isLinkable = + (frame.uri.isScheme('file') || frame.uri.isScheme('package')) && + editorServiceAvailable; + final style = isLinkable + ? theme.fixedFontLinkStyle + : theme.fixedFontStyle; + return TextSpan( + children: [ + TextSpan( + text: frame.location, + style: style, + recognizer: isLinkable + ? (TapGestureRecognizer() + ..onTap = () async { + final resolvedUri = await controller.dtdServices + .resolveUri(frame.uri); + controller.dtdServices.navigateToCode( + CodeLocation( + uri: resolvedUri.toString(), + line: frame.line, + column: frame.column, + ), + ); + }) + : null, + ), + TextSpan(text: ' ' * (longest - frame.location.length)), + const TextSpan(text: ' '), + TextSpan(text: '${frame.member}\n', style: style), + ], + ); + }).toList(); + } +} + +/// Displayed when no @Preview() annotations are detected in the project. +/// +/// Links to documentation. +class NoPreviewsDetectedWidget extends StatelessWidget { + const NoPreviewsDetectedWidget({super.key}); + + static Uri documentationUrl = Uri.https('flutter.dev', 'to/widget-previews'); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Center( + child: Column( + children: [ + Text('No previews detected', style: theme.boldTextStyle), + const VerticalSpacer(), + Text('Read more about getting started with widget previews at:'), + Text.rich( + TextSpan( + text: documentationUrl.toString(), + style: theme.linkTextStyle, + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl(documentationUrl); + }, + ), + ), + ], + ), + ); + } +} + +/// A wrapper that serves as the root entry for a single preview in the widget inspector. +class PreviewWidget extends StatelessWidget { + const PreviewWidget({super.key, required this.preview, required this.child}); + + final WidgetPreview preview; + final Widget child; + + @override + StatelessElement createElement() => PreviewWidgetElement(this); + + @override + Widget build(BuildContext context) { + return child; + } + + @override + String toStringShort() { + final StringBuffer buffer = StringBuffer( + '@${preview.previewData.runtimeType}', + ); + if (preview.name != null) { + buffer.write('(name: "${preview.name}")'); + } + return buffer.toString(); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + preview.debugFillProperties(properties); + } +} + +/// A custom [StatelessElement] with the sole purpose of simplifying identifying +/// selections of @Preview annotations in the widget inspector. +class PreviewWidgetElement extends StatelessElement { + PreviewWidgetElement(super.widget); +} + +class WidgetPreviewGroupWidget extends StatelessWidget { + const WidgetPreviewGroupWidget({ + super.key, + required this.controller, + required this.group, + }); + + final WidgetPreviewScaffoldController controller; + final WidgetPreviewGroup group; + + // Spacing values for the grid layout + static const _gridSpacing = 8.0; + static const _gridRunSpacing = 8.0; + + /// The default radius of a Material 3 `Card`, as per documentation for `Card.shape`. + // TODO(bkonyi): inherit this from the theme. + static const _kCardRadius = Radius.circular(12); + + Widget _buildGridViewFlex(List previews) { + return Wrap( + spacing: WidgetPreviewGroupWidget._gridSpacing, + runSpacing: WidgetPreviewGroupWidget._gridRunSpacing, + alignment: WrapAlignment.start, + children: [ + for (final WidgetPreview preview in previews) + WidgetPreviewWidget(controller: controller, preview: preview), + ], + ); + } + + Widget _buildVerticalListView(List previews) { + return Column( + children: [ + for (final preview in previews) + Center( + child: WidgetPreviewWidget( + controller: controller, + preview: preview, + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Card( + child: ListTileTheme( + data: ListTileTheme.of(context).copyWith( + dense: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(_kCardRadius), + ), + ), + child: Theme( + // Prevents divider lines appearing at the top and bottom of the + // expanded ExpansionTile. + data: theme.copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + key: PageStorageKey(group.name), + title: Text(group.name), + initiallyExpanded: true, + children: [ + ValueListenableBuilder( + valueListenable: controller.layoutTypeListenable, + builder: (context, selectedLayout, _) { + return switch (selectedLayout) { + LayoutType.gridView => _buildGridViewFlex(group.previews), + LayoutType.listView => _buildVerticalListView( + group.previews, + ), + }; + }, + ), + ], + ), + ), + ), + ); + } +} + +class WidgetPreviewWidget extends StatefulWidget { + const WidgetPreviewWidget({ + super.key, + required this.preview, + required this.controller, + }); + + final WidgetPreview preview; + + final WidgetPreviewScaffoldController controller; + + @override + State createState() => WidgetPreviewWidgetState(); +} + +class WidgetPreviewWidgetState extends State { + final transformationController = TransformationController(); + + // Set the initial preview brightness based on the platform default or the + // value explicitly specified for the preview. + late final brightnessListenable = ValueNotifier( + widget.preview.brightness ?? MediaQuery.platformBrightnessOf(context), + ); + + final softRestartListenable = ValueNotifier(false); + final key = GlobalKey(); + + /// Returns the last size of the previewed widget. + Size get lastChildSize => + (key.currentContext!.findRenderObject() as RenderBox).size; + + @override + void didUpdateWidget(WidgetPreviewWidget oldWidget) { + super.didUpdateWidget(oldWidget); + + final previousBrightness = oldWidget.preview.brightness; + final newBrightness = widget.preview.brightness; + final currentBrightness = brightnessListenable.value; + final systemBrightness = MediaQuery.platformBrightnessOf(context); + + // No initial brightness was previously defined. + if (previousBrightness == null && newBrightness != null) { + if (currentBrightness == systemBrightness) { + // If the current brightness is different than the system brightness, the user has manually + // changed the brightness through the UI, so don't change it automatically. + brightnessListenable.value = newBrightness; + } + } + // Changing the initial brightness to either a new initial brightness or system brightness. + else if (previousBrightness != null) { + // If the current brightness is different than the initial brightness, the user has manually + // changed the brightness through the UI, so don't change it automatically. + if (currentBrightness == previousBrightness) { + brightnessListenable.value = newBrightness ?? systemBrightness; + } + } + } + + @override + Widget build(BuildContext context) { + final previewerConstraints = + WidgetPreviewerWindowConstraints.getRootConstraints(context); + + final maxSizeConstraints = previewerConstraints.copyWith( + minHeight: previewerConstraints.maxHeight / 2.0, + maxHeight: previewerConstraints.maxHeight / 2.0, + ); + + bool errorThrownDuringTreeConstruction = false; + + // Wrap the previewed widget with a ValueListenableBuilder responsible for performing a "soft" + // restart. + // + // A soft restart simply removes the previewed widget from the widget tree for a frame before + // re-inserting it on the next frame. This has the effect of re-running local initializers in + // State objects, which normally requires a hot restart to accomplish in a normal application. + Widget preview = ValueListenableBuilder( + valueListenable: softRestartListenable, + builder: (context, performRestart, _) { + try { + final previewWidget = Container( + key: key, + child: WidgetPreviewTheming( + theme: widget.preview.theme, + child: EnableWidgetInspectorScope( + child: PreviewWidget( + preview: widget.preview, + child: widget.preview.previewBuilder(), + ), + ), + ), + ); + if (performRestart) { + WidgetsBinding.instance.addPostFrameCallback((_) { + // Trigger a rebuild on the next frame to re-insert previewWidget. + softRestartListenable.value = false; + }, debugLabel: 'Soft Restart'); + return SizedBox.fromSize(size: lastChildSize); + } + return previewWidget; + } on Object catch (error, stackTrace) { + // Catch any unhandled exceptions and display an error widget instead of taking + // down the entire preview environment. + errorThrownDuringTreeConstruction = true; + return WidgetPreviewErrorWidget( + controller: widget.controller, + error: error, + stackTrace: stackTrace, + size: maxSizeConstraints.biggest, + ); + } + }, + ); + + final Size? size = widget.preview.size; + + // Add support for selecting only previewed widgets via the widget + // inspector. + preview = ValueListenableBuilder( + valueListenable: + WidgetsBinding.instance.debugShowWidgetInspectorOverrideNotifier, + builder: (context, enableWidgetInspector, child) { + // Don't allow inspecting the error widget. + if (child is WidgetPreviewErrorWidget) { + return child; + } + if (enableWidgetInspector) { + return WidgetInspector( + // TODO(bkonyi): wire up inspector controls for individual previews or + // the entire preview environment. This currently requires users to + // to enable widget selection via the Widget Inspector tool in DevTools. + + // These buttons would be rendered on top of the previewed widget, so + // don't display them. + exitWidgetSelectionButtonBuilder: null, + moveExitWidgetSelectionButtonBuilder: null, + tapBehaviorButtonBuilder: null, + child: child!, + ); + } + return child!; + }, + child: _WidgetPreviewWrapper( + previewerConstraints: maxSizeConstraints, + child: SizedBox( + width: size?.width == double.infinity ? null : size?.width, + height: size?.height == double.infinity ? null : size?.height, + child: preview, + ), + ), + ); + + preview = WidgetPreviewMediaQueryOverride( + preview: widget.preview, + brightnessListenable: brightnessListenable, + child: preview, + ); + + preview = WidgetPreviewLocalizations( + localizationsData: widget.preview.localizations, + child: preview, + ); + + // Override the asset resolution behavior to automatically insert + // 'packages/$packageName/` in front of non-package paths as some previews + // may reference assets that are within the current project and wouldn't + // normally require a package specifier. + // TODO(bkonyi): this doesn't modify the behavior of asset loading logic in + // the engine implementation. This means that any asset loading done by + // APIs provided in dart:ui won't work correctly for non-package asset + // paths (e.g., shaders loaded by `FragmentProgram.fromAsset()`). + // + // See https://github.com/flutter/flutter/issues/171284 + preview = DefaultAssetBundle( + bundle: PreviewAssetBundle(packageName: widget.preview.packageName), + child: preview, + ); + + final hasName = widget.preview.name != null; + preview = Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (hasName) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + widget.preview.name!, + style: fixBlurryText( + TextStyle(fontSize: 16, fontWeight: FontWeight.w300), + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + // TODO(bkonyi): use theming or define global constants. + horizontal: 16.0, + ).add(hasName ? const EdgeInsets.only(top: 8.0) : EdgeInsets.zero), + decoration: hasName + ? BoxDecoration( + border: Border(top: Divider.createBorderSide(context)), + ) + : null, + child: Column( + children: [ + ZoomablePreviewArea( + transformationController: transformationController, + errorThrownDuringTreeConstruction: + errorThrownDuringTreeConstruction, + child: preview, + ), + const VerticalSpacer(), + Builder( + builder: (context) { + return _WidgetPreviewControlRow( + transformationController: transformationController, + errorThrownDuringTreeConstruction: + errorThrownDuringTreeConstruction, + brightnessListenable: brightnessListenable, + softRestartListenable: softRestartListenable, + ); + }, + ), + ], + ), + ), + ], + ); + + return Padding( + padding: const EdgeInsets.all(16.0), + child: Card.outlined( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: preview, + ), + ), + ); + } +} + +class _WidgetPreviewControlRow extends StatelessWidget { + const _WidgetPreviewControlRow({ + required this.transformationController, + required this.errorThrownDuringTreeConstruction, + required this.brightnessListenable, + required this.softRestartListenable, + }); + + final TransformationController transformationController; + final bool errorThrownDuringTreeConstruction; + final ValueNotifier brightnessListenable; + final ValueNotifier softRestartListenable; + + @override + Widget build(BuildContext context) { + // Don't show controls if an error occurred. + if (errorThrownDuringTreeConstruction) { + return Container(); + } + return Row( + mainAxisSize: MainAxisSize.min, + // If an unhandled exception was caught and we're displaying an error + // widget, these controls should be disabled. + // TODO(bkonyi): improve layout of controls. + children: [ + ZoomControls(transformationController: transformationController), + const SizedBox(width: 30), + BrightnessToggleButton(brightnessListenable: brightnessListenable), + const SizedBox(width: 10), + SoftRestartButton(softRestartListenable: softRestartListenable), + ], + ); + } +} + +/// Applies theming defined in [theme] to [child]. +class WidgetPreviewTheming extends StatelessWidget { + const WidgetPreviewTheming({ + super.key, + required this.theme, + required this.child, + }); + + final Widget child; + + /// The set of themes to be applied to [child]. + final PreviewThemeData? theme; + + @override + Widget build(BuildContext context) { + final themeData = theme; + if (themeData == null) { + return child; + } + return themeData.apply(context, child); + } +} + +/// Wraps the previewed [child] with the correct [MediaQueryData] overrides +/// based on [preview] and the current device [Brightness]. +class WidgetPreviewMediaQueryOverride extends StatelessWidget { + const WidgetPreviewMediaQueryOverride({ + super.key, + required this.preview, + required this.brightnessListenable, + required this.child, + }); + + /// The preview specification used to render the preview. + final WidgetPreview preview; + + /// The currently set brightness for this preview instance. + final ValueListenable brightnessListenable; + + final Widget child; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: brightnessListenable, + builder: (context, brightness, _) { + return MediaQuery( + data: _buildMediaQueryOverride( + context: context, + brightness: brightness, + ), + // Use mediaQueryPreview instead of preview to avoid capturing preview + // and creating an infinite loop. + child: child, + ); + }, + ); + } + + MediaQueryData _buildMediaQueryOverride({ + required BuildContext context, + required Brightness brightness, + }) { + var mediaQueryData = MediaQuery.of( + context, + ).copyWith(platformBrightness: brightness); + + if (preview.textScaleFactor != null) { + mediaQueryData = mediaQueryData.copyWith( + textScaler: TextScaler.linear(preview.textScaleFactor!), + ); + } + + var size = Size( + preview.size?.width ?? mediaQueryData.size.width, + preview.size?.height ?? mediaQueryData.size.height, + ); + + if (preview.size != null) { + mediaQueryData = mediaQueryData.copyWith(size: size); + } + + return mediaQueryData; + } +} + +/// Wraps [child] with a [Localizations] with localization data from +/// [localizationsData]. +class WidgetPreviewLocalizations extends StatefulWidget { + const WidgetPreviewLocalizations({ + super.key, + required this.localizationsData, + required this.child, + }); + + final PreviewLocalizationsData? localizationsData; + final Widget child; + + @override + State createState() => + _WidgetPreviewLocalizationsState(); +} + +class _WidgetPreviewLocalizationsState + extends State { + PreviewLocalizationsData get _localizationsData => widget.localizationsData!; + late final LocalizationsResolver _localizationsResolver = + LocalizationsResolver( + supportedLocales: _localizationsData.supportedLocales, + locale: _localizationsData.locale, + localeListResolutionCallback: + _localizationsData.localeListResolutionCallback, + localeResolutionCallback: _localizationsData.localeResolutionCallback, + localizationsDelegates: _localizationsData.localizationsDelegates, + ); + + @override + void didUpdateWidget(WidgetPreviewLocalizations oldWidget) { + super.didUpdateWidget(oldWidget); + final PreviewLocalizationsData? localizationsData = + widget.localizationsData; + if (localizationsData == null) { + return; + } + _localizationsResolver.update( + supportedLocales: localizationsData.supportedLocales, + locale: localizationsData.locale, + localeListResolutionCallback: + localizationsData.localeListResolutionCallback, + localeResolutionCallback: localizationsData.localeResolutionCallback, + localizationsDelegates: localizationsData.localizationsDelegates, + ); + } + + @override + Widget build(BuildContext context) { + if (widget.localizationsData == null) { + return widget.child; + } + return ListenableBuilder( + listenable: _localizationsResolver, + builder: (context, _) { + return Localizations( + locale: _localizationsResolver.locale, + delegates: _localizationsResolver.localizationsDelegates.toList(), + child: widget.child, + ); + }, + ); + } +} + +/// An [InheritedWidget] that propagates the current size of the +/// WidgetPreviewScaffold. +/// +/// This is needed when determining how to put constraints on previewed widgets +/// that would otherwise have infinite constraints. +class WidgetPreviewerWindowConstraints extends InheritedWidget { + const WidgetPreviewerWindowConstraints({ + super.key, + required super.child, + required this.constraints, + }); + + final BoxConstraints constraints; + + static BoxConstraints getRootConstraints(BuildContext context) { + final result = context + .dependOnInheritedWidgetOfExactType(); + assert( + result != null, + 'No WidgetPreviewerWindowConstraints founds in context', + ); + return result!.constraints; + } + + @override + bool updateShouldNotify(WidgetPreviewerWindowConstraints oldWidget) { + return oldWidget.constraints != constraints; + } +} + +class ZoomablePreviewArea extends StatelessWidget { + const ZoomablePreviewArea({ + super.key, + required this.child, + required this.transformationController, + required this.errorThrownDuringTreeConstruction, + }); + + final Widget child; + final TransformationController transformationController; + final bool errorThrownDuringTreeConstruction; + + @override + Widget build(BuildContext context) { + if (errorThrownDuringTreeConstruction) { + return child; + } + return ListenableBuilder( + listenable: transformationController, + builder: (context, _) { + final double scale = transformationController.value.entry(0, 0); + return SingleChildScrollView( + scrollDirection: Axis.vertical, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: _ScaledLayoutWrapper(scale: scale, child: child), + ), + ); + }, + ); + } +} + +class _ScaledLayoutWrapper extends SingleChildRenderObjectWidget { + const _ScaledLayoutWrapper({super.child, required this.scale}); + + final double scale; + + @override + RenderObject createRenderObject(BuildContext context) { + return _ScaledLayoutRenderObject(scale: scale); + } + + @override + void updateRenderObject( + BuildContext context, + _ScaledLayoutRenderObject renderObject, + ) { + renderObject.scale = scale; + } +} + +class _ScaledLayoutRenderObject extends RenderShiftedBox { + _ScaledLayoutRenderObject({required this._scale, RenderBox? child}) + : super(child); + + double _scale; + double get scale => _scale; + set scale(double value) { + if (_scale == value) { + return; + } + _scale = value; + markNeedsLayout(); + } + + @override + double computeMinIntrinsicWidth(double height) { + if (child == null) { + return 0.0; + } + return child!.getMinIntrinsicWidth(height / scale) * scale; + } + + @override + double computeMaxIntrinsicWidth(double height) { + if (child == null) { + return 0.0; + } + return child!.getMaxIntrinsicWidth(height / scale) * scale; + } + + @override + double computeMinIntrinsicHeight(double width) { + if (child == null) { + return 0.0; + } + return child!.getMinIntrinsicHeight(width / scale) * scale; + } + + @override + double computeMaxIntrinsicHeight(double width) { + if (child == null) { + return 0.0; + } + return child!.getMaxIntrinsicHeight(width / scale) * scale; + } + + @override + void performLayout() { + final child = this.child; + if (child == null) { + size = Size.zero; + return; + } + child.layout(constraints, parentUsesSize: true); + size = constraints.constrain(child.size * scale); + + final BoxParentData childParentData = child.parentData! as BoxParentData; + childParentData.offset = Offset.zero; + } + + @override + void paint(PaintingContext context, Offset offset) { + if (child == null) { + layer = null; + return; + } + if (scale == 1.0) { + super.paint(context, offset); + layer = null; + return; + } + final Matrix4 transform = Matrix4.diagonal3Values(scale, scale, 1.0); + layer = context.pushTransform( + needsCompositing, + offset, + transform, + super.paint, + oldLayer: layer is TransformLayer ? layer as TransformLayer? : null, + ); + } + + @override + void applyPaintTransform(RenderBox child, Matrix4 transform) { + if (scale != 1.0) { + transform.scaleByDouble(scale, scale, 1.0, 1.0); + } + super.applyPaintTransform(child, transform); + } + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + if (child == null) { + return false; + } + final Matrix4 transform = Matrix4.diagonal3Values(scale, scale, 1.0); + return result.addWithPaintTransform( + transform: transform, + position: position, + hitTest: (BoxHitTestResult result, Offset position) { + return super.hitTestChildren(result, position: position); + }, + ); + } +} + +// TODO(bkonyi): according to goderbauer@, this probably isn't the best approach to ensure we +// handle unconstrained widgets. This should be reworked. +/// Wrapper applying a custom render object to force constraints on +/// unconstrained widgets. +class _WidgetPreviewWrapper extends SingleChildRenderObjectWidget { + const _WidgetPreviewWrapper({ + super.child, + required this.previewerConstraints, + }); + + /// The size of the previewer render surface. + final BoxConstraints previewerConstraints; + + @override + RenderObject createRenderObject(BuildContext context) { + return _WidgetPreviewWrapperBox( + previewerConstraints: previewerConstraints, + child: null, + ); + } + + @override + void updateRenderObject( + BuildContext context, + _WidgetPreviewWrapperBox renderObject, + ) { + renderObject.setPreviewerConstraints(previewerConstraints); + } +} + +/// Custom render box that forces constraints onto unconstrained widgets. +class _WidgetPreviewWrapperBox extends RenderShiftedBox { + _WidgetPreviewWrapperBox({ + required RenderBox? child, + required this._previewerConstraints, + }) : super(child); + + BoxConstraints _constraintOverride = const BoxConstraints(); + BoxConstraints _previewerConstraints; + + void setPreviewerConstraints(BoxConstraints previewerConstraints) { + if (_previewerConstraints == previewerConstraints) { + return; + } + _previewerConstraints = previewerConstraints; + markNeedsLayout(); + } + + @override + void layout(Constraints constraints, {bool parentUsesSize = false}) { + if (child != null && constraints is BoxConstraints) { + double minInstrinsicHeight; + try { + minInstrinsicHeight = child!.getMinIntrinsicHeight( + constraints.maxWidth, + ); + } on Object { + minInstrinsicHeight = 0.0; + } + // Determine if the previewed widget is vertically constrained. If the + // widget has a minimum intrinsic height of zero given the widget's max + // width, it has an unconstrained height and will cause an overflow in + // the previewer. In this case, apply finite constraints (e.g., the + // constraints for the root of the previewer). Otherwise, use the + // widget's actual constraints. + _constraintOverride = minInstrinsicHeight == 0 + ? _previewerConstraints + : const BoxConstraints(); + } + super.layout(constraints, parentUsesSize: parentUsesSize); + } + + @override + void performLayout() { + final child = this.child; + if (child == null) { + size = Size.zero; + return; + } + final updatedConstraints = _constraintOverride.enforce(constraints); + child.layout(updatedConstraints, parentUsesSize: true); + size = constraints.constrain(child.size); + } +} + +/// Custom [AssetBundle] used to map original asset paths from the parent +/// projects to those in the preview project. +class PreviewAssetBundle extends PlatformAssetBundle { + PreviewAssetBundle({required this.packageName}); + + /// The name of the package in which a preview was defined. + /// + /// For example, if a preview is defined in 'package:foo/src/bar.dart', this + /// will have the value 'foo'. + final String packageName; + + // Assets shipped via package dependencies have paths that start with + // 'packages'. + static const String _kPackagesPrefix = 'packages'; + + // TODO(bkonyi): when loading an invalid asset path that doesn't start with + // 'packages', this throws a FlutterError referencing the modified key + // instead of the original. We should catch the error and rethrow one with + // the original key in the error message. + @override + Future load(String key) { + // These assets are always present or are shipped via a package and aren't + // actually located in the parent project, meaning their paths did not need + // to be modified. + if (key == 'AssetManifest.bin' || + key == 'AssetManifest.bin.json' || + key == 'FontManifest.json' || + key.startsWith(_kPackagesPrefix)) { + return super.load(key); + } + // Other assets are from the parent project. Map their keys to package + // paths corresponding to the package containing the preview. + return super.load(_toPackagePath(key)); + } + + @override + Future loadBuffer(String key) async { + if (kIsWeb) { + final ByteData bytes = await load(key); + return ImmutableBuffer.fromUint8List(Uint8List.sublistView(bytes)); + } + return await ImmutableBuffer.fromAsset( + key.startsWith(_kPackagesPrefix) ? key : _toPackagePath(key), + ); + } + + String _toPackagePath(String key) => '$_kPackagesPrefix/$packageName/$key'; +} + +/// Main entrypoint for the widget previewer. +/// +/// We don't actually define this as `main` to avoid copying this file into +/// the preview scaffold project which prevents us from being able to use hot +/// restart to iterate on this file. +Future mainImpl() async { + final controller = WidgetPreviewScaffoldController(previews: previews); + await controller.initialize(); + // WARNING: do not move this line. This constructor sets + // [WidgetInspectorService.instance] to the custom service for the widget + // previewer. If [WidgetsFlutterBinding.ensureInitialized()] is invoked before + // the custom service is set, inspector service extensions will be registered + // against the wrong service. + WidgetPreviewScaffoldInspectorService(dtdServices: controller.dtdServices); + final WidgetsBinding binding = WidgetsFlutterBinding.ensureInitialized(); + // Disable the injection of [WidgetInspector] into the widget tree built by + // [WidgetsApp]. [WidgetInspector] instances will be created for each + // individual preview so the widget inspector won't allow for users to select + // widgets that make up the widget preview scaffolding. + binding.debugExcludeRootWidgetInspector = true; + runWidget( + DisableWidgetInspectorScope( + child: binding.wrapWithDefaultView( + // Forces the set of previews to be recalculated after a hot reload. + HotReloadListener( + onHotReload: controller.onHotReload, + child: WidgetPreviewScaffold( + controller: controller, + ideTheme: getIdeTheme(), + ), + ), + ), + ), + ); +} + +class WidgetPreviewScaffold extends StatefulWidget { + const WidgetPreviewScaffold({ + super.key, + required this.controller, + this.ideTheme = const IdeTheme(), + this.enableWebView = true, + }); + + final WidgetPreviewScaffoldController controller; + final IdeTheme ideTheme; + final bool enableWebView; + + @override + State createState() => _WidgetPreviewScaffoldState(); +} + +class _WidgetPreviewScaffoldState extends State { + WebViewController? _webViewController; + + @override + void initState() { + super.initState(); + if (widget.enableWebView) { + _webViewController = WebViewController() + ..loadRequest(widget.controller.devToolsUri); + } + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: themeFor( + isDarkTheme: false, + ideTheme: widget.ideTheme, + theme: ThemeData(), + ), + darkTheme: themeFor( + isDarkTheme: true, + ideTheme: widget.ideTheme, + theme: ThemeData.dark(), + ), + themeMode: widget.ideTheme.isDarkMode ? ThemeMode.dark : ThemeMode.light, + home: Material( + child: OutlineDecoration.onlyTop( + child: ValueListenableBuilder( + valueListenable: widget.controller.widgetInspectorVisible, + builder: (context, widgetInspectorVisible, previewView) { + if (!widgetInspectorVisible) { + return previewView!; + } + return SplitPane( + axis: Axis.horizontal, + initialFractions: const [0.7, 0.3], + children: [ + OutlineDecoration.onlyRight(child: previewView!), + OutlineDecoration.onlyLeft( + child: widget.enableWebView + ? WebViewWidget(controller: _webViewController!) + : Container(), + ), + ], + ); + }, + // Display the previewer + child: Column( + children: [ + Expanded( + child: Container( + padding: const EdgeInsets.all(8.0), + child: WidgetPreviews(controller: widget.controller), + ), + ), + WidgetPreviewControls(controller: widget.controller), + ], + ), + ), + ), + ), + ); + } +} + +/// The set of controls used to control the preview environment. +class WidgetPreviewControls extends StatelessWidget { + const WidgetPreviewControls({super.key, required this.controller}); + + static const _controlsPadding = 20.0; + final WidgetPreviewScaffoldController controller; + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only( + bottom: _controlsPadding, + left: _controlsPadding, + right: _controlsPadding, + ), + child: Row( + children: [ + LayoutTypeSelector(controller: controller), + ValueListenableBuilder( + valueListenable: controller.editorServiceAvailable, + builder: (context, editorServiceAvailable, _) { + if (!editorServiceAvailable) { + return Container(); + } + return Row( + children: [ + HorizontalSpacer(), + FilterBySelectedFileToggle(controller: controller), + ], + ); + }, + ), + HorizontalSpacer(), + Expanded(child: PreviewSearchControls(controller: controller)), + HorizontalSpacer(), + WidgetInspectorToggle(controller: controller), + Spacer(), + WidgetPreviewerRestartButton(controller: controller), + ], + ), + ); + } +} + +/// Renders the set of currently selected widget previews. +class WidgetPreviews extends StatelessWidget { + const WidgetPreviews({super.key, required this.controller}); + + final WidgetPreviewScaffoldController controller; + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller.filteredPreviewSetListenable, + builder: (context, previewGroups, _) { + if (previewGroups.isEmpty) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [NoPreviewsDetectedWidget()], + ); + } + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + final previewGroupsList = previewGroups.toList(); + return WidgetPreviewerWindowConstraints( + constraints: constraints, + child: ListView.builder( + itemCount: previewGroups.length, + itemBuilder: (context, index) { + return WidgetPreviewGroupWidget( + controller: controller, + group: previewGroupsList[index], + ); + }, + ), + ); + }, + ); + }, + ); + } +} diff --git a/.widget_preview/lib/src/widget_preview_scaffold_controller.dart b/.widget_preview/lib/src/widget_preview_scaffold_controller.dart new file mode 100644 index 0000000..8b2cb60 --- /dev/null +++ b/.widget_preview/lib/src/widget_preview_scaffold_controller.dart @@ -0,0 +1,287 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart' as path; +import 'package:widget_preview_scaffold/src/widget_preview_rendering.dart'; +import 'dtd/dtd_services.dart'; +import 'widget_preview.dart'; + +/// Define the Enum for Layout Types +enum LayoutType { gridView, listView } + +typedef WidgetPreviews = Iterable; +typedef WidgetPreviewGroups = Iterable; +typedef PreviewsCallback = WidgetPreviews Function(); + +/// Controller used to process events and determine which previews should be +/// displayed and how they should be displayed in the [WidgetPreviewScaffold]. +class WidgetPreviewScaffoldController { + WidgetPreviewScaffoldController({ + required PreviewsCallback previews, + @visibleForTesting WidgetPreviewScaffoldDtdServices? dtdServicesOverride, + // ignore: prefer_initializing_formals + }) : _previews = previews, + dtdServices = dtdServicesOverride ?? WidgetPreviewScaffoldDtdServices(); + + @visibleForTesting + static const kFilterBySelectedFilePreference = 'filterBySelectedFile'; + + /// Initializes the controller by establishing a connection to DTD and + /// listening for events. + Future initialize() async { + await dtdServices.connect(); + context = path.Context( + style: dtdServices.isWindows ? path.Style.windows : path.Style.posix, + ); + _registerListeners(); + await Future.wait([ + dtdServices + .getFlag(kFilterBySelectedFilePreference, defaultValue: true) + .then((value) => _filterBySelectedFile.value = value), + dtdServices.getDevToolsUri().then((uri) { + devToolsUri = uri; + }), + ]); + } + + /// Cleanup internal controller state. + Future dispose() async { + await dtdServices.dispose(); + + _layoutType.dispose(); + _filterBySelectedFile.dispose(); + _searchQuery.dispose(); + for (final searchField in _searchFields) { + searchField.dispose(); + } + } + + /// Update state after the project has been reassembled due to a hot reload. + void onHotReload() => _updateFilteredPreviewSet(); + + /// The active DTD connection used to communicate with other developer tooling. + final WidgetPreviewScaffoldDtdServices dtdServices; + + final PreviewsCallback _previews; + + late final path.Context context; + + /// Specifies how the previews should be laid out. + ValueListenable get layoutTypeListenable => _layoutType; + final _layoutType = ValueNotifier(LayoutType.gridView); + + LayoutType get layoutType => _layoutType.value; + set layoutType(LayoutType type) => _layoutType.value = type; + + /// Set to true when the Editor service is available over DTD. + ValueListenable get editorServiceAvailable => + dtdServices.editorServiceAvailable; + + /// The DevTools instance that's used to display the widget inspector within the previewer. + late final Uri devToolsUri; + + /// Specifies if only previews from the currently selected source file should be rendered. + ValueListenable get filterBySelectedFileListenable => + _filterBySelectedFile; + final _filterBySelectedFile = ValueNotifier(true); + + /// Enable or disable filtering by selected source file. + Future toggleFilterBySelectedFile() async { + final updated = !_filterBySelectedFile.value; + await dtdServices.setPreference(kFilterBySelectedFilePreference, updated); + _filterBySelectedFile.value = updated; + } + + /// The current case-insensitive query used to search previews. + ValueListenable get searchQueryListenable => _searchQuery; + final _searchQuery = ValueNotifier(''); + + /// Update the search query used to filter previews. + void updateSearchQuery(String query) => _searchQuery.value = query; + + /// Whether to include group names when applying search filters. + ValueListenable get searchByGroupNameListenable => _searchByGroupName; + final _searchByGroupName = ValueNotifier(true); + + /// Whether to include preview names when applying search filters. + ValueListenable get searchByPreviewNameListenable => + _searchByPreviewName; + final _searchByPreviewName = ValueNotifier(true); + + /// Whether to include script URIs when applying search filters. + ValueListenable get searchByContainingScriptListenable => + _searchByContainingScript; + final _searchByContainingScript = ValueNotifier(true); + + /// Whether to include package names when applying search filters. + ValueListenable get searchByContainingPackageListenable => + _searchByContainingPackage; + final _searchByContainingPackage = ValueNotifier(true); + + /// Toggle inclusion of group names in search filters. + /// + /// Returns true if the filter state was changed. + bool toggleSearchByGroupName() => _toggleSearchField(_searchByGroupName); + + /// Toggle inclusion of preview names in search filters. + /// + /// Returns true if the filter state was changed. + bool toggleSearchByPreviewName() => _toggleSearchField(_searchByPreviewName); + + /// Toggle inclusion of script URIs in search filters. + /// + /// Returns true if the filter state was changed. + bool toggleSearchByContainingScript() => + _toggleSearchField(_searchByContainingScript); + + /// Toggle inclusion of package names in search filters. + /// + /// Returns true if the filter state was changed. + bool toggleSearchByContainingPackage() => + _toggleSearchField(_searchByContainingPackage); + + /// Specifies if the DevTools Widget Inspector should be visible. + ValueListenable get widgetInspectorVisible => _widgetInspectorVisible; + final _widgetInspectorVisible = ValueNotifier(false); + + /// Enable or disable the DevTools Widget Inspector. + void toggleWidgetInspectorVisible() => + _widgetInspectorVisible.value = !_widgetInspectorVisible.value; + + /// The current set of previews to be displayed. + ValueListenable get filteredPreviewSetListenable => + _filteredPreviewSet; + final _filteredPreviewSet = ValueNotifier([]); + + void _registerListeners() { + dtdServices.selectedSourceFile.addListener(_updateFilteredPreviewSet); + editorServiceAvailable.addListener( + () => _updateFilteredPreviewSet(editorServiceAvailabilityUpdated: true), + ); + filterBySelectedFileListenable.addListener(_updateFilteredPreviewSet); + searchQueryListenable.addListener(_updateFilteredPreviewSet); + for (final searchField in _searchFields) { + searchField.addListener(_updateFilteredPreviewSet); + } + // Set the initial state. + _updateFilteredPreviewSet(); + } + + late final _searchFields = >[ + _searchByGroupName, + _searchByPreviewName, + _searchByContainingScript, + _searchByContainingPackage, + ]; + + String _getSearchableValue( + WidgetPreview preview, + ValueNotifier searchField, + ) { + if (identical(searchField, _searchByGroupName)) { + return preview.previewData.group.toLowerCase(); + } + if (identical(searchField, _searchByPreviewName)) { + return (preview.name ?? '').toLowerCase(); + } + if (identical(searchField, _searchByContainingScript)) { + return preview.scriptUri.toLowerCase(); + } + if (identical(searchField, _searchByContainingPackage)) { + return preview.packageName.toLowerCase(); + } + + throw StateError('Unknown search field'); + } + + bool _toggleSearchField(ValueNotifier searchField) { + if (searchField.value && !_hasAnotherActiveSearchField(searchField)) { + return false; + } + searchField.value = !searchField.value; + return true; + } + + bool _hasAnotherActiveSearchField(ValueNotifier activeSearchField) => + _searchFields.any( + (field) => !identical(field, activeSearchField) && field.value, + ); + + bool _matchesSearchFilter(WidgetPreview preview, String searchQuery) { + if (searchQuery.isEmpty) { + return true; + } + + for (final searchField in _searchFields) { + if (!searchField.value) { + continue; + } + if (_getSearchableValue(preview, searchField).contains(searchQuery)) { + return true; + } + } + + return false; + } + + void _updateFilteredPreviewSet({ + bool editorServiceAvailabilityUpdated = false, + }) { + final previews = _previews(); + + final normalizedSearchQuery = _searchQuery.value.trim().toLowerCase(); + String? selectedSourcePath; + + if (editorServiceAvailable.value && _filterBySelectedFile.value) { + final selectedSourceFile = dtdServices.selectedSourceFile.value; + // If the Editor service has only just become available and we're filtering + // by selected file, we need to explicitly set the filtered preview set as + // empty, otherwise `selectedSourceFile` will interpreted as a non-source + // file being selected in the editor. + if (editorServiceAvailabilityUpdated && selectedSourceFile == null) { + _filteredPreviewSet.value = []; + return; + } + // If filtering by selected file, we don't update the filtered preview set + // if the currently selected file is null. This can happen when a non-source + // window is selected (e.g., the widget previewer itself in VSCode), so we + // ignore these updates. + if (selectedSourceFile == null) { + return; + } + // Convert to a file path for comparing to avoid issues with optional encoding in URIs. + // See https://github.com/flutter/flutter/issues/175524. + selectedSourcePath = context.fromUri(selectedSourceFile.uriAsString); + } + + final previewGroups = {}; + for (final preview in previews) { + if (selectedSourcePath != null && + !context.equals( + // TODO(bkonyi): we can probably save some cycles by caching the file path + // rather than computing it on each filter. + context.fromUri(preview.scriptUri), + selectedSourcePath, + )) { + continue; + } + if (!_matchesSearchFilter(preview, normalizedSearchQuery)) { + continue; + } + + final group = preview.previewData.group; + previewGroups + .putIfAbsent( + group, + () => WidgetPreviewGroup(name: group, previews: []), + ) + .previews + .add(preview); + } + _filteredPreviewSet.value = previewGroups.values.toList(); + } +} diff --git a/.widget_preview/preview_manifest.json b/.widget_preview/preview_manifest.json new file mode 100644 index 0000000..76b52c4 --- /dev/null +++ b/.widget_preview/preview_manifest.json @@ -0,0 +1 @@ +{"version":"0.0.2","sdk-version":"3.13.0","pubspec-hashes":{"/home/quadradical/Documents/Code/material_emoji_picker/pubspec.yaml":"240ad6569f7ed673949d65842675b8da"}} \ No newline at end of file diff --git a/.widget_preview/pubspec.lock b/.widget_preview/pubspec.lock new file mode 100644 index 0000000..a345f97 --- /dev/null +++ b/.widget_preview/pubspec.lock @@ -0,0 +1,734 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e + url: "https://pub.dev" + source: hosted + version: "1.1.3" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "828110d598123b5ea96c00c9f3c72105bf79f8ee36c20a39b26209ade421ec57" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_ui: + dependency: transitive + description: + name: cupertino_ui + sha256: c6747018d37d32feeeb858a1d7043831cb541460ebe1a890a995cdb8df8ea5a5 + url: "https://pub.dev" + source: hosted + version: "1.1.0" + dart_service_protocol_shared: + dependency: transitive + description: + name: dart_service_protocol_shared + sha256: "1737875c176d7e3d87bb3a359182828b542fe20a0b34198b8d31a81af5c7a76d" + url: "https://pub.dev" + source: hosted + version: "0.0.3" + dtd: + dependency: "direct main" + description: + name: dtd + sha256: "09ddb228b3d1478a093556357692a4c203ff4f9d5f8cda05dfdca0ff3fb7c5d3" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + fast_immutable_collections: + dependency: transitive + description: + name: fast_immutable_collections + sha256: "58cec99fc068427c71901e82d4b31b232240ebe6e61200993c2cb91bcada0ff6" + url: "https://pub.dev" + source: hosted + version: "11.2.0" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_hooks: + dependency: transitive + description: + name: flutter_hooks + sha256: "8ae1f090e5f4ef5cfa6670ce1ab5dddadd33f3533a7f9ba19d9f958aa2a89f42" + url: "https://pub.dev" + source: hosted + version: "0.21.3+1" + flutter_lints: + dependency: "direct main" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_riverpod: + dependency: transitive + description: + name: flutter_riverpod + sha256: "2b7f9d2a3c730ac1a98221b420cef966b990fca7ab546ede324642ca57a533e3" + url: "https://pub.dev" + source: hosted + version: "3.4.3" + flutter_test: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: e3cb3ee6b47fd2472c23de6da5744796a4da195137759ddb3fbcc9467b7b3c7d + url: "https://pub.dev" + source: hosted + version: "8.2.1" + hooks: + dependency: transitive + description: + name: hooks + sha256: eaac480a35ec0814146c2c48d96aaa829e0e44a7662c88ae84c9edf4bc35651f + url: "https://pub.dev" + source: hosted + version: "2.2.0" + hooks_riverpod: + dependency: transitive + description: + name: hooks_riverpod + sha256: d58e1e14aa112ca9c56d54effb7d0d717b2d3f96dad3cdd1fce16f8740450700 + url: "https://pub.dev" + source: hosted + version: "3.4.3" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: transitive + description: + name: intl + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" + url: "https://pub.dev" + source: hosted + version: "0.20.3" + jni: + dependency: transitive + description: + name: jni + sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: b2310cdd4c18c65c081ab141a41efa94aa26c65431803703ece51996f174f351 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + jni_util: + dependency: transitive + description: + name: jni_util + sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + json_rpc_2: + dependency: "direct main" + description: + name: json_rpc_2 + sha256: "82dfd37d3b2e5030ae4729e1d7f5538cbc45eb1c73d618b9272931facac3bec1" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + listen: + dependency: transitive + description: + name: listen + sha256: "47501a08016a43fcad79252439d723f50f14f88fa7bfd8a177e0a417e5c9e1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + url: "https://pub.dev" + source: hosted + version: "0.12.20" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + material_emoji_picker: + dependency: "direct main" + description: + path: "/home/quadradical/Documents/Code/material_emoji_picker" + relative: false + source: path + version: "1.0.2" + material_ui: + dependency: transitive + description: + name: material_ui + sha256: "6d056d60ce745335fcf1c726a4fc46a481a62229f202b65dbc2db5edbbabc7e2" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + meta: + dependency: transitive + description: + name: meta + sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" + url: "https://pub.dev" + source: hosted + version: "1.19.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: ad56fd53a78ff6b1472fa59ff2a4e8b8ccabafc586fc263a1dfad0b99b5553e3 + url: "https://pub.dev" + source: hosted + version: "9.6.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d + url: "https://pub.dev" + source: hosted + version: "3.0.0" + path: + dependency: "direct main" + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: a36d119c13416516a7b5913fbe8af8531e11633d784c550b2125f76c758524ec + url: "https://pub.dev" + source: hosted + version: "3.2.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + record_use: + dependency: transitive + description: + name: record_use + sha256: "1cb8564af8d43b464294411db9217f5ec04891c6f22ee2c32d73ae05e88a6bd2" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "484dfc873ea4c4f4240e5635444fa066f87b07862b7279ebd004a2b71fba4b7a" + url: "https://pub.dev" + source: hosted + version: "3.4.3" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: "direct main" + description: + name: stack_trace + sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490" + url: "https://pub.dev" + source: hosted + version: "1.12.2" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + super_sliver_list: + dependency: transitive + description: + name: super_sliver_list + sha256: b1e1e64d08ce40e459b9bb5d9f8e361617c26b8c9f3bb967760b0f436b6e3f56 + url: "https://pub.dev" + source: hosted + version: "0.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + url: "https://pub.dev" + source: hosted + version: "0.7.12" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unified_analytics: + dependency: transitive + description: + name: unified_analytics + sha256: "28bb11ef24567e720dc1397cba60df03cc5aded5bf6bef4d17b49934402c719b" + url: "https://pub.dev" + source: hosted + version: "8.0.18" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610" + url: "https://pub.dev" + source: hosted + version: "6.3.33" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a" + url: "https://pub.dev" + source: hosted + version: "6.4.2" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0" + url: "https://pub.dev" + source: hosted + version: "3.2.3" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201" + url: "https://pub.dev" + source: hosted + version: "3.2.6" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + uuid: + dependency: transitive + description: + name: uuid + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" + url: "https://pub.dev" + source: hosted + version: "4.6.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" + url: "https://pub.dev" + source: hosted + version: "15.3.0" + web: + dependency: "direct main" + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: d53e1ccf5516f25017e3c9d44c39034db352d20fa34fe200674270242c2c5111 + url: "https://pub.dev" + source: hosted + version: "4.14.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "4de8b3d1ff4ebe1bdb42e68a5e4f809194a3cb0117a8f495f590004f00da3964" + url: "https://pub.dev" + source: hosted + version: "4.14.1" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" + url: "https://pub.dev" + source: hosted + version: "2.15.1" + webview_flutter_web: + dependency: "direct main" + description: + name: webview_flutter_web + sha256: "18a7ccc1c31dd9a5c759a1b7217a2a1e04bd8f65712714a4070bfac19a23ca9e" + url: "https://pub.dev" + source: hosted + version: "0.2.3+4" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: fe359c7fac1002124b5b9e2ba3a41906bbb9b2d029ccb4a0067404d8f3704730 + url: "https://pub.dev" + source: hosted + version: "3.26.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea + url: "https://pub.dev" + source: hosted + version: "3.1.4" +sdks: + dart: ">=3.13.0 <4.0.0" + flutter: ">=3.44.0" diff --git a/.widget_preview/pubspec.yaml b/.widget_preview/pubspec.yaml new file mode 100644 index 0000000..3a3131c --- /dev/null +++ b/.widget_preview/pubspec.yaml @@ -0,0 +1,26 @@ +name: widget_preview_scaffold +description: Scaffolding for Flutter Widget Previews +publish_to: none +version: 0.0.1 +environment: + sdk: ^3.13.0 +dependencies: + dtd: ^4.0.0 + flutter: + sdk: flutter + flutter_lints: ^6.0.0 + google_fonts: ^8.2.1 + json_rpc_2: ^4.1.0 + material_emoji_picker: + path: /home/quadradical/Documents/Code/material_emoji_picker + path: ^1.9.1 + stack_trace: ^1.12.2 + url_launcher: ^6.3.2 + web: ^1.1.1 + webview_flutter: ^4.14.1 + webview_flutter_web: ^0.2.3+4 +flutter: + uses-material-design: true +dependency_overrides: + material_emoji_picker: + path: /home/quadradical/Documents/Code/material_emoji_picker diff --git a/.widget_preview/web/favicon.png b/.widget_preview/web/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..8aaa46ac1ae21512746f852a42ba87e4165dfdd1 GIT binary patch literal 917 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s77>k44ofy`glX=O&z`$AH z5n0T@pr;JNj1^1m%NQ6KBQrxHN+NuHtdjF{^%7I^lT!66atjzhz^1~gBDWwnwIorY zA~z?m*s8)-39P~@uhG~7qCqZhc3d|4;4lG&j~$oK zA@xWG2F9zNE{-7;ld7wO#(qnZWJZzI2E3_xA*Lsx!2?73+ET^w%+}I@67GR zPL~gKB+lnDthp%7_f?t4uW8c8|3WP+WgZ_FE&TA=jOG3EO9#GP7O}Aw)>E_$k#Ij{ zy7GD-^V?e9l7~+mufGc|5hy!fsr=)zztlt_?dh+%eqZG?+EVFvYJGT3%B4l+c?Nxv zA961J%WD)5K5(#z=Uq!-q}j6RjfU+9tC!e#p8CvEH~sU+6WhDDJxnp0o^W94eWCU1 zQd#DyR^MO$Yr>MKOsyrcjGjlI&9nHBnS0EEGw0Y$CX2WHmGZe!`~O@#c5SN5zDu26 zDI4BKt!FtLvCi9OcFDt_gv}BORd*bZl^1SM*A_yFxrSX8ds2x$lS21HT;^ zTCIz7I(nCKmV|Unv7TVUR`#dH$H;AN(TuMbbG%<%3CMZUe|J|QuJuIw1-!H;WNWB?$;ib zdFQ+9g^uhz>as0SX1QKp$Jx0PS=bUhTs~c2!d=hEU?gX@B64~UD6e?B`njxgN@xNA DfL3B_ literal 0 HcmV?d00001 diff --git a/.widget_preview/web/icons/Icon-192.png b/.widget_preview/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE6983%h40rea4q#y5Sy2@dQ4*9`u24{vpO%@E zs!&o{kgAYck(tK8P;u+(%;+*{TfUb2JG&y8JSVX1l!-e(Q})cK=gzB@Pxno>?|YuA z60IA!$234gL}UB^&+i}qJF4v2p_jUL>C{J+OQuD=*|YNB$BLL{|Ld>6&iEc)Q+NH{ zym*&cEvs$prqzCbzvcOM##!&aecX5VrETz~ zm=~A#{9pg)dEe&!dHj5dL~`o0$uAyFQYxRl8`#;OC3tu{U`l;UksSUQzds^jwyixP1R9J=9|C!&bY^Bei()F|1_TkwB%h(LXq_@IB5ewUe zzm?8y)IBOve&mkfG0CS{t7q&#T4||#(|FE`ODUE%LaC2T7h2kV7g|?(d+R-(sZ+xj zr1SWQNqoF~x%S!*^YhPLo=cQCfB2fI{Y{kja?W9!^z~m3DL%Yoc6NI0`sLLJd+Q6= zJUCc0b7xWW-Tvbu%I)SkC!eM-jQ@7>r+WrV?01$K9b1iOb2hQpa87dOj7z>Eup#-V z{oJ|ko`nw@CltpVl&w(?D)8&wW|nLcQ=8n^pSI$tlXI8Gy39+3VP2AdGFBz5NI!Y< z=^XECF~z6%dU&c%585K(UMlK4YwL=vtIPI!`I=r2+IsC)R{lm#k!jJHucC6-em%En z?n>*$*0xphg(p0e`;AJjTy&d%Abr`)^qE$#b2hKvks|nSwNB~kb=!XNNo%~_@>wss z{BG`d=4*X(NvpmI@3+G1*Vd(q?376f%rN^}q+n+CHR5M;|Nnol_E{hN|JP;n z8k4k3MZ3;zX6!!_cs^q586oSu(uzfWPfovD{VwQ9hV-5(f$2UDEEdx?CcJvu?l_NH#g>u)waF!_Dz~6$!+=U%;olZt?fV4rt$@4-QRLe z^xKtfG6yc)l88LK>U^QY(_EFuPjZs_-)~=MbI$JYuQwA-=S(?#@?dFX=s6Gd6Pxt+ z_GB-rp5508k!G{4O(F5usj*_rifv)tW%g3~00 zic|~w4V=9D&6geg_v}@i)TcYuf4KNcH7`nY-ITWCDE7PS-t}&8!n!Lm7M|9+ZzVr3 zVlR<7!ynK7sCN0rB=vvg^Y0kS-p&c&wXoeOH(&2SS?~gd-}7_ko@|MJ=Jb2w{jRhF zy`TOV9NWtyRj~BE?C(X_%u;^*OV&4u-PCUDH)~zobv&@w>mq6K3D6)Uj&qR=LB!?S`o2_L?YZ&l_&sq9Yqs@yd7U@ADz~X-UGB4^duq=t%n<)na_mIasT)%+ zYQ*)Pm|SAIqweYN-Mab(_owANF`RE`_G89ugM%2>(V%=^FeUO>n=bh|V#sbUZBtVddP>!vckci5j&)^+C9jN)6JVUruR9Hp7&-%OS+I3Zfe@Yp|9 zx4O7JX%$;n8%r-17_~Uj*ziXbdZ` z-s>bkMbkyOkN>I78xnh?jpAD%u z&iHyyeus#43HLG1cV9oBu-?!teDkWrrcC#V*ALa66%>1PxjboGsiek3zs7K7wOP8; zUfsI3OZ;f^;#;4$mNovgdSz3-MXNO;h55&JHYF*M2C=+Q$K2&pX0t`DKELX))dtPh z3`w5UJ~K}J-T=)rJa_oovXj(qRR=A&xXlg$e=U?-a!fxgQnS1B zK_%>HaIz4iM7E>dm77t=!({y9n4NYxZTu|ib(i_S5L5a!cI#aJu>TA*-i3RxuBd7i zcXX7s7Eaxz)bVZMzX&$-9RY>LB1}^g7GHeg;^g}xl)GciY}v>CjyvTPEGO=`nSMv$ zy@ZI4K=F@Gj=SPBChT8+AnLXAv%)2E83l5;l~ikH{buJ~R^qWC^=ISW);!AtUwp3m zv3gvYkh)=|TfmbYF`omjD=e0d+wzQaDc^}4ks_AQ{EIf+cquYF>`(vO8OxW1CRr*J z7VS5j^K6rl?QD+eCt7=4E*6Kh8}c%o%}UMiHhl2omiT5FuJu`&OD6jm1aHo}>1w@J z;jSCw2?id%+p9BF-X%`@X9F zlA3gpSWSJ2*WUTndqmDRwJs81m~hdFTer05^{i_xFHR;vOrw<_k%YzN!+IbZb<;pUuFvu^n# zeO7Z1%_n>_TYWB^HL*L_lYaeTNr_xqg11Vrh^w;Ktu@`rjS^NEs$A;Jm!Lrw)XTC92y0PiUy%SMR5)1j>b5(Y(KX&)` z^tpk`*W{Nd@K@ArYr1i4!3D3KAKl}c%{iky zC%TAto@*2n1^<~aUNc5K=EQz~_KoiFRVYMD)wCp_*HZkfiP z(bJjg5MI@>wXpB%%#_0(dMykAr#%ALJ06_qNpda_!DL)BxiDH3h!OZl3Zc-sPwyiQ)c z=#BeF-)nl_-g720xZj@qoRQV<8Pnb`liTvA?l`ykX1{XrL>J9d{9Tdmx5K7ezrD>@ z^jg#=#)xEH9Av- zqCU?5@GS0w17q2{R5|TJe!(vu`iG)$lU^l+zNUSzKS0 zvnywYyPm(K5%hxVsM_Ag2eWo;s5-n%<>kWW!z~*Nd72LOiFnYhW{YAVDIwD3=9mM1s;*b3=De8 zAk0{?)V_>?fq}im)7O>#2_w6Jq^`3676k?d_Et|9$B>F!Z|9!P7b%r#-*3Em^X>0i z9RV)fY6{0(gg*AoYuU_c7_&!1;154TvXVX*CwsDw!o?XH5eEcRCp3hwzr|##w2h@} z;p=IaCha?ucl+&|iW}PkZtLBhe)H!2_up;I^>lLP)yf$-z+gt>*fedLGyPwx&t`EKGrVbKk5iJESo^?ORB7W1D(Yy_)89I}rO;^qc0{ zcb~1=|0b!k=89Z(b?i%f-MjpM&sS|&&i3w5T;5judvDCbz9(u<58M7xI_j-|+}eA5 zPfv$_x;$V1y2v-3t?wKK&K4J^tCar;D?9Uio_Auk%Aw60w6^pVz0?V~p}RO{mb%WK zX@PpdVj4@P1?p{PS(S5IV|pRy>X6f?^so0i{=bpC&HW(jDv#5rbi&h5-M?#UDmBrr z>sD|y_mt=FJl+L-wBGVwb@`mhUF)X&fB1E!kHR+9*V1c#&1kiKrF-Gd*MPw1sZZw@ zm!7+&thCeN-O}bWB@MQ#xI%J3#@~}R{`TY}^UKxKoLAir?&j29l{!`Hx{`gw!|guI z)k~)BmtRqES}S^8Ys+0D+cy)X{`VbW(p_D#Q%orC^p=gwmkEb0NPReUHJ9$?lX8KN zqNC>LMBaGHwL5jz&Be!<&c3~{ygv1Qz2UaCJ(Ig?cJ6s(?-w;u_R@K|sVqVh-6uO* z%0GLnRaC9@dcw4C8fpQSCCmQYO}#Ps_{JrBRoY9ox^MZp_UzO9#k%d6-kmntcD#dC zYjaHMHlO_JFYyb`dEE}aQakh8w+y|2rKdupPWxf6XwsmpFIzReUX-Em;v&S_g&EHA&g=(|AedREs9 zdy}c2@z&L8DGI7>3#U3rhu?_bKQkiu>-*X>zDp)gKNq{-e9z|#Pv`ApWC^;xaB}qH zt0gDqfB2<8_cPmZ)q6MIvhMEtcj@x?12Tf+zy4whr}dJbnKcq1-JhPn-_s~qD1D@}bL*4h45!my>YXD~qo1W8n`~eEda0b} zp17R!ce|E1I9F{y(^0MxwkowQ&8G69^!xjYg|FqOXFoG5J2^XYZ**FVgJ|Z=`EvgM zDo(wwSIC=tY|L{?mS6=XJ+w^8TBrj$QF#Q5GQoXR00 zC>(kJ{L@8MCC4|te#FPrc&mVyg#!v393YSZrvL{FL`8zYf5xs!ljau8o1MnMz`)?? L>gTe~DWM4f2kRIC literal 0 HcmV?d00001 diff --git a/.widget_preview/web/icons/Icon-512.png b/.widget_preview/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/.widget_preview/web/icons/Icon-maskable-192.png b/.widget_preview/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 F695xY89M*~ literal 0 HcmV?d00001 diff --git a/.widget_preview/web/index.html b/.widget_preview/web/index.html new file mode 100644 index 0000000..df92389 --- /dev/null +++ b/.widget_preview/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + widget_preview_scaffold + + + + + + + diff --git a/.widget_preview/web/manifest.json b/.widget_preview/web/manifest.json new file mode 100644 index 0000000..3f44304 --- /dev/null +++ b/.widget_preview/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "widget_preview_scaffold", + "short_name": "widget_preview_scaffold", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/lib/src/widgets/emoji_picker.dart b/lib/src/widgets/emoji_picker.dart index 430533a..8dc9df6 100644 --- a/lib/src/widgets/emoji_picker.dart +++ b/lib/src/widgets/emoji_picker.dart @@ -65,7 +65,6 @@ final class const EmojiPicker({ ); return Column( - spacing: 4, children: [ SearchBar( hintText: "Search emoji", @@ -81,6 +80,7 @@ final class const EmojiPicker({ ), ], ), + SizedBox(height: 8), SizedBox( height: 48, child: Scrollbar(