adjust padding

This commit is contained in:
Henry Hiles 2026-09-16 16:09:56 -04:00
commit fd9c61ea09
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
38 changed files with 5134 additions and 1 deletions

48
.widget_preview/.gitignore vendored Normal file
View file

@ -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/

View file

@ -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.

View file

@ -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

View file

@ -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<void> main() async {
await mainImpl();
}

View file

@ -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<bool> listenable();
bool onToggle();
}
class _GroupSearchFilter extends _SearchFilterConfig {
const _GroupSearchFilter(WidgetPreviewScaffoldController controller)
: super(_PreviewSearchFilter.groupName, controller);
@override
ValueListenable<bool> listenable() => _controller.searchByGroupNameListenable;
@override
bool onToggle() => _controller.toggleSearchByGroupName();
}
class _PreviewNameSearchFilter extends _SearchFilterConfig {
const _PreviewNameSearchFilter(WidgetPreviewScaffoldController controller)
: super(_PreviewSearchFilter.previewName, controller);
@override
ValueListenable<bool> listenable() =>
_controller.searchByPreviewNameListenable;
@override
bool onToggle() => _controller.toggleSearchByPreviewName();
}
class _ContainingScriptSearchFilter extends _SearchFilterConfig {
const _ContainingScriptSearchFilter(
WidgetPreviewScaffoldController controller,
) : super(_PreviewSearchFilter.containingScript, controller);
@override
ValueListenable<bool> listenable() =>
_controller.searchByContainingScriptListenable;
@override
bool onToggle() => _controller.toggleSearchByContainingScript();
}
class _ContainingPackageSearchFilter extends _SearchFilterConfig {
const _ContainingPackageSearchFilter(
WidgetPreviewScaffoldController controller,
) : super(_PreviewSearchFilter.containingPackage, controller);
@override
ValueListenable<bool> 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<Matrix4>(
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<LayoutType>(
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<bool> 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<PreviewSearchControls> createState() => _PreviewSearchControlsState();
}
class _PreviewSearchControlsState extends State<PreviewSearchControls> {
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<String>(
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<Listenable>((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<Brightness> brightnessListenable;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<Brightness>(
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;
}
}

View file

@ -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';

View file

@ -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<void> connect({Uri? dtdUri}) async {
final Uri dtdWsUri = dtdUri ?? Uri.parse(kWidgetPreviewDtdUri);
dtd = await DartToolingDaemon.connect(dtdWsUri);
unawaited(
dtd.postEvent(
kWidgetPreviewScaffoldStream,
'Connected',
const <String, Object?>{},
),
);
await _determineIfWindows();
await initializeEditorService(this);
}
/// Disposes the DTD connection.
@override
Future<void> dispose() async {
super.dispose();
await dtd.close();
}
Future<DTDResponse?> _call(
String methodName, {
Map<String, Object?>? params,
}) => dtd.safeCall(kWidgetPreviewService, methodName, params: params);
/// Returns `true` if the operating system is Windows.
late final bool isWindows;
Future<void> _determineIfWindows() async {
isWindows = (BoolResponse.fromDTDResponse(
(await _call(kIsWindows))!,
)).value!;
}
/// Trigger a hot restart of the widget preview scaffold.
Future<void> hotRestartPreviewer() => _call(kHotRestartPreviewer);
/// Resolves a package:// URI to a file:// URI using the package_config.
///
/// Returns null if [uri] can not be resolved.
Future<Uri?> 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<Object?> 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<bool> 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<void> setPreference(String key, Object? value) async {
await _call(kSetPreference, params: {'key': key, 'value': value});
}
/// Retrieves the DevTools URI for the previewer instance.
Future<Uri> getDevToolsUri() async {
final result = StringResponse.fromDTDResponse(
(await _call(kGetDevToolsUri))!,
);
return Uri.parse(result.value!);
}
@override
late final DartToolingDaemon dtd;
}

View file

@ -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<bool> get editorServiceAvailable => _editorServiceAvailable;
static final _editorServiceAvailable = ValueNotifier<bool>(false);
/// The currently selected source file in the IDE.
ValueListenable<TextDocument?> get selectedSourceFile => _selectedSourceFile;
static final _selectedSourceFile = ValueNotifier<TextDocument?>(null);
/// The current theming set in the IDE.
ValueListenable<EditorTheme?> get editorTheme => _editorTheme;
static final _editorTheme = ValueNotifier<EditorTheme?>(null);
/// Start listening for events on the Editor stream.
Future<void> 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<void> _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<void> 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<String, Object?> 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<String, Object?> toJson() => {
Field.isDarkMode: isDarkMode,
Field.backgroundColor: backgroundColor,
Field.foregroundColor: foregroundColor,
Field.fontSize: fontSize,
};
}
class ThemeChangedEvent extends EditorEvent {
ThemeChangedEvent({required this.theme});
ThemeChangedEvent.fromJson(Map<String, Object?> map)
: this(
theme: EditorTheme.fromJson(map[Field.theme] as Map<String, Object?>),
);
final EditorTheme theme;
@override
EditorEventKind get kind => EditorEventKind.themeChanged;
Map<String, Object?> 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<String, Object?> map)
: this(activeLocation: ActiveLocation.fromJson(map));
@override
EditorEventKind get kind => EditorEventKind.activeLocationChanged;
}
class ActiveLocation {
ActiveLocation({required this.selections, required this.textDocument});
ActiveLocation.fromJson(Map<String, Object?> map)
: this(
textDocument: map.containsKey(Field.textDocument)
? TextDocument.fromJson(
map[Field.textDocument] as Map<String, Object?>,
)
: null,
selections: (map[Field.selections] as List<Object?>)
.cast<Map<String, Object?>>()
.map(EditorSelection.fromJson)
.toList(),
);
final List<EditorSelection> selections;
final TextDocument? textDocument;
Map<String, Object?> 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<String, Object?> map)
: this(
uriAsString: map[Field.uri] as String,
version: map[Field.version] as int?,
);
final String uriAsString;
final int? version;
Map<String, Object?> 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<String, Object?> map)
: this(
active: CursorPosition.fromJson(
map[Field.active] as Map<String, Object?>,
),
anchor: CursorPosition.fromJson(
map[Field.anchor] as Map<String, Object?>,
),
);
final CursorPosition active;
final CursorPosition anchor;
Map<String, Object?> 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<String, Object?> map)
: this(
start: CursorPosition.fromJson(
map[Field.start] as Map<String, Object?>,
),
end: CursorPosition.fromJson(map[Field.end] as Map<String, Object?>),
);
/// The range's start position.
final CursorPosition start;
/// The range's end position.
final CursorPosition end;
Map<String, Object?> 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<String, Object?> 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<String, Object?> 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<String, Object?> 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';
}

View file

@ -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<void> 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<DTDResponse?> safeCall(
String? serviceName,
String methodName, {
Map<String, Object?>? 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;
}
}
}

View file

@ -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() => [];

View file

@ -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<Widget> 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<double> initialFractions;
/// The minimum size each child is allowed to be.
final List<double>? minSizes;
/// Splitter widgets to divide [children].
///
/// If this is null, a default splitter will be used to divide [children].
final List<PreferredSizeWidget>? 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<StatefulWidget> createState() => _SplitPaneState();
}
final class _SplitPaneState extends State<SplitPane> {
late final List<double> 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 = <Widget>[];
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<double> 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',
);
}

View file

@ -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();

View file

@ -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)}';
}

View file

@ -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<String, String?> 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';
}

View file

@ -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<TextStyle>(theme.regularTextStyle),
fixedSize: const WidgetStatePropertyAll<Size>(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()],
);
}

View file

@ -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<WidgetPreview> 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<HotReloadListener> {
@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,
);
}
}

View file

@ -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),
);
}
}

View file

@ -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 = '';
}

View file

@ -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;
}

View file

@ -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';

View file

@ -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<String, String> 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}) {}

View file

@ -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<String, String> 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();
}
}

View file

@ -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';

View file

@ -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<WidgetPreview> 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<String>('name', name, ifNull: 'not set'))
..add(DiagnosticsProperty<String>('group', previewData.group))
..add(DiagnosticsProperty<Size>('size', size))
..add(DiagnosticsProperty<double>('textScaleFactor', textScaleFactor))
..add(DiagnosticsProperty<PreviewThemeData>('theme', theme))
..add(DiagnosticsProperty<Brightness>('brightness', brightness))
..add(
DiagnosticsProperty<PreviewLocalizationsData>(
'localizations',
localizations,
),
);
}
}

View file

@ -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(<String>[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<Object, Object?> 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(<String, Object>{
kFile: location.uri,
kLine: location.line!,
kColumn: location.column!,
});
}
if (location != null) {
dtdServices.navigateToCode(location);
}
}
super.postEvent(eventKind, eventData, stream: stream);
}
}

File diff suppressed because it is too large Load diff

View file

@ -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<WidgetPreview>;
typedef WidgetPreviewGroups = Iterable<WidgetPreviewGroup>;
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<void> initialize() async {
await dtdServices.connect();
context = path.Context(
style: dtdServices.isWindows ? path.Style.windows : path.Style.posix,
);
_registerListeners();
await Future.wait<void>([
dtdServices
.getFlag(kFilterBySelectedFilePreference, defaultValue: true)
.then((value) => _filterBySelectedFile.value = value),
dtdServices.getDevToolsUri().then((uri) {
devToolsUri = uri;
}),
]);
}
/// Cleanup internal controller state.
Future<void> 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<LayoutType> get layoutTypeListenable => _layoutType;
final _layoutType = ValueNotifier<LayoutType>(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<bool> 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<bool> get filterBySelectedFileListenable =>
_filterBySelectedFile;
final _filterBySelectedFile = ValueNotifier<bool>(true);
/// Enable or disable filtering by selected source file.
Future<void> toggleFilterBySelectedFile() async {
final updated = !_filterBySelectedFile.value;
await dtdServices.setPreference(kFilterBySelectedFilePreference, updated);
_filterBySelectedFile.value = updated;
}
/// The current case-insensitive query used to search previews.
ValueListenable<String> get searchQueryListenable => _searchQuery;
final _searchQuery = ValueNotifier<String>('');
/// 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<bool> get searchByGroupNameListenable => _searchByGroupName;
final _searchByGroupName = ValueNotifier<bool>(true);
/// Whether to include preview names when applying search filters.
ValueListenable<bool> get searchByPreviewNameListenable =>
_searchByPreviewName;
final _searchByPreviewName = ValueNotifier<bool>(true);
/// Whether to include script URIs when applying search filters.
ValueListenable<bool> get searchByContainingScriptListenable =>
_searchByContainingScript;
final _searchByContainingScript = ValueNotifier<bool>(true);
/// Whether to include package names when applying search filters.
ValueListenable<bool> get searchByContainingPackageListenable =>
_searchByContainingPackage;
final _searchByContainingPackage = ValueNotifier<bool>(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<bool> get widgetInspectorVisible => _widgetInspectorVisible;
final _widgetInspectorVisible = ValueNotifier<bool>(false);
/// Enable or disable the DevTools Widget Inspector.
void toggleWidgetInspectorVisible() =>
_widgetInspectorVisible.value = !_widgetInspectorVisible.value;
/// The current set of previews to be displayed.
ValueListenable<WidgetPreviewGroups> get filteredPreviewSetListenable =>
_filteredPreviewSet;
final _filteredPreviewSet = ValueNotifier<WidgetPreviewGroups>([]);
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 = <ValueNotifier<bool>>[
_searchByGroupName,
_searchByPreviewName,
_searchByContainingScript,
_searchByContainingPackage,
];
String _getSearchableValue(
WidgetPreview preview,
ValueNotifier<bool> 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<bool> searchField) {
if (searchField.value && !_hasAnotherActiveSearchField(searchField)) {
return false;
}
searchField.value = !searchField.value;
return true;
}
bool _hasAnotherActiveSearchField(ValueNotifier<bool> 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 = <String, WidgetPreviewGroup>{};
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();
}
}

View file

@ -0,0 +1 @@
{"version":"0.0.2","sdk-version":"3.13.0","pubspec-hashes":{"/home/quadradical/Documents/Code/material_emoji_picker/pubspec.yaml":"240ad6569f7ed673949d65842675b8da"}}

View file

@ -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"

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="widget_preview_scaffold">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>widget_preview_scaffold</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<!--
You can customize the "flutter_bootstrap.js" script.
This is useful to provide a custom configuration to the Flutter loader
or to give the user feedback during the initialization process.
For more details:
* https://docs.flutter.dev/platform-integration/web/initialization
-->
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

View file

@ -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"
}
]
}