adjust padding
This commit is contained in:
parent
e2c171adec
commit
fd9c61ea09
38 changed files with 5134 additions and 1 deletions
48
.widget_preview/.gitignore
vendored
Normal file
48
.widget_preview/.gitignore
vendored
Normal 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/
|
||||||
4
.widget_preview/README.md
Normal file
4
.widget_preview/README.md
Normal 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.
|
||||||
38
.widget_preview/analysis_options.yaml
Normal file
38
.widget_preview/analysis_options.yaml
Normal 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
|
||||||
9
.widget_preview/lib/main.dart
Normal file
9
.widget_preview/lib/main.dart
Normal 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();
|
||||||
|
}
|
||||||
593
.widget_preview/lib/src/controls.dart
Normal file
593
.widget_preview/lib/src/controls.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
.widget_preview/lib/src/dtd/dtd_connection_info.dart
Normal file
9
.widget_preview/lib/src/dtd/dtd_connection_info.dart
Normal 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';
|
||||||
130
.widget_preview/lib/src/dtd/dtd_services.dart
Normal file
130
.widget_preview/lib/src/dtd/dtd_services.dart
Normal 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;
|
||||||
|
}
|
||||||
395
.widget_preview/lib/src/dtd/editor_service.dart
Normal file
395
.widget_preview/lib/src/dtd/editor_service.dart
Normal 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';
|
||||||
|
}
|
||||||
39
.widget_preview/lib/src/dtd/utils.dart
Normal file
39
.widget_preview/lib/src/dtd/utils.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
.widget_preview/lib/src/generated_preview.dart
Normal file
6
.widget_preview/lib/src/generated_preview.dart
Normal 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() => [];
|
||||||
365
.widget_preview/lib/src/split.dart
Normal file
365
.widget_preview/lib/src/split.dart
Normal 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',
|
||||||
|
);
|
||||||
|
}
|
||||||
10
.widget_preview/lib/src/theme/_ide_theme_desktop.dart
Normal file
10
.widget_preview/lib/src/theme/_ide_theme_desktop.dart
Normal 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();
|
||||||
43
.widget_preview/lib/src/theme/_ide_theme_web.dart
Normal file
43
.widget_preview/lib/src/theme/_ide_theme_web.dart
Normal 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)}';
|
||||||
|
}
|
||||||
47
.widget_preview/lib/src/theme/ide_theme.dart
Normal file
47
.widget_preview/lib/src/theme/ide_theme.dart
Normal 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';
|
||||||
|
}
|
||||||
365
.widget_preview/lib/src/theme/theme.dart
Normal file
365
.widget_preview/lib/src/theme/theme.dart
Normal 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()],
|
||||||
|
);
|
||||||
|
}
|
||||||
202
.widget_preview/lib/src/utils.dart
Normal file
202
.widget_preview/lib/src/utils.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
70
.widget_preview/lib/src/utils/color_utils.dart
Normal file
70
.widget_preview/lib/src/utils/color_utils.dart
Normal 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 = '';
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue