Unverified Commit 9b2668a4 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Minor fix compendium (#107874)

parent da88c953
...@@ -4,4 +4,4 @@ The [snippets] tool uses the files in the `skeletons` directory to inject code ...@@ -4,4 +4,4 @@ The [snippets] tool uses the files in the `skeletons` directory to inject code
blocks generated from `{@tool dartpad}`, `{@tool sample}`, and `{@tool snippet}` blocks generated from `{@tool dartpad}`, `{@tool sample}`, and `{@tool snippet}`
sections found in doc comments into the API docs. sections found in doc comments into the API docs.
[snippet]: https://github.com/flutter/assets-for-api-docs/tree/master/packages/snippets [snippets]: https://github.com/flutter/assets-for-api-docs/tree/master/packages/snippets
...@@ -30,6 +30,10 @@ that on an Android device like so: ...@@ -30,6 +30,10 @@ that on an Android device like so:
## Naming ## Naming
> `lib/library/file/class_name.n.dart`
>
> `lib/library/file/class_name.member_name.n.dart`
The naming scheme for the files is similar to the hierarchy under The naming scheme for the files is similar to the hierarchy under
[packages/flutter/lib/src](../../packages/flutter/lib/src), except that the [packages/flutter/lib/src](../../packages/flutter/lib/src), except that the
files are represented as directories (without the `.dart` suffix), and each files are represented as directories (without the `.dart` suffix), and each
......
// 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.
// Flutter code sample for WidgetsApp
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return WidgetsApp(
title: 'Example',
color: const Color(0xFF000000),
home: const Center(child: Text('Hello World')),
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) => PageRouteBuilder<T>(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => builder(context),
),
);
}
}
// 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_api_samples/widgets/app/widgets_app.widgets_app.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('WidgetsApp test', (WidgetTester tester) async {
await tester.pumpWidget(
const example.MyApp(),
);
expect(find.text('Hello World'), findsOneWidget);
});
}
...@@ -19,6 +19,10 @@ import 'localizations.dart'; ...@@ -19,6 +19,10 @@ import 'localizations.dart';
/// assert(debugCheckHasCupertinoLocalizations(context)); /// assert(debugCheckHasCupertinoLocalizations(context));
/// ``` /// ```
/// ///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// Does nothing if asserts are disabled. Always returns true. /// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasCupertinoLocalizations(BuildContext context) { bool debugCheckHasCupertinoLocalizations(BuildContext context) {
assert(() { assert(() {
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import 'package:flutter/foundation.dart' show ValueListenable, clampDouble; import 'package:flutter/foundation.dart' show ValueListenable, clampDouble;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'button.dart'; import 'button.dart';
...@@ -32,8 +31,11 @@ const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.wit ...@@ -32,8 +31,11 @@ const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.wit
darkColor: Color(0xff302928), darkColor: Color(0xff302928),
); );
/// Desktop Cupertino styled text selection controls.
class _CupertinoDesktopTextSelectionControls extends TextSelectionControls { ///
/// The [cupertinoDesktopTextSelectionControls] global variable has a
/// suitable instance of this class.
class CupertinoDesktopTextSelectionControls extends TextSelectionControls {
/// Desktop has no text selection handles. /// Desktop has no text selection handles.
@override @override
Size getHandleSize(double textLineHeight) { Size getHandleSize(double textLineHeight) {
...@@ -87,7 +89,7 @@ class _CupertinoDesktopTextSelectionControls extends TextSelectionControls { ...@@ -87,7 +89,7 @@ class _CupertinoDesktopTextSelectionControls extends TextSelectionControls {
/// Text selection controls that follows Mac design conventions. /// Text selection controls that follows Mac design conventions.
final TextSelectionControls cupertinoDesktopTextSelectionControls = final TextSelectionControls cupertinoDesktopTextSelectionControls =
_CupertinoDesktopTextSelectionControls(); CupertinoDesktopTextSelectionControls();
// Generates the child that's passed into CupertinoDesktopTextSelectionToolbar. // Generates the child that's passed into CupertinoDesktopTextSelectionToolbar.
class _CupertinoDesktopTextSelectionControlsToolbar extends StatefulWidget { class _CupertinoDesktopTextSelectionControlsToolbar extends StatefulWidget {
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart' show ValueListenable, clampDouble; import 'package:flutter/foundation.dart' show ValueListenable, clampDouble;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'localizations.dart'; import 'localizations.dart';
...@@ -192,6 +191,9 @@ class _TextSelectionHandlePainter extends CustomPainter { ...@@ -192,6 +191,9 @@ class _TextSelectionHandlePainter extends CustomPainter {
} }
/// iOS Cupertino styled text selection controls. /// iOS Cupertino styled text selection controls.
///
/// The [cupertinoTextSelectionControls] global variable has a
/// suitable instance of this class.
class CupertinoTextSelectionControls extends TextSelectionControls { class CupertinoTextSelectionControls extends TextSelectionControls {
/// Returns the size of the Cupertino handle. /// Returns the size of the Cupertino handle.
@override @override
......
...@@ -23,6 +23,10 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger; ...@@ -23,6 +23,10 @@ import 'scaffold.dart' show Scaffold, ScaffoldMessenger;
/// assert(debugCheckHasMaterial(context)); /// assert(debugCheckHasMaterial(context));
/// ``` /// ```
/// ///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// This method can be expensive (it walks the element tree). /// This method can be expensive (it walks the element tree).
/// ///
/// Does nothing if asserts are disabled. Always returns true. /// Does nothing if asserts are disabled. Always returns true.
...@@ -67,6 +71,10 @@ bool debugCheckHasMaterial(BuildContext context) { ...@@ -67,6 +71,10 @@ bool debugCheckHasMaterial(BuildContext context) {
/// assert(debugCheckHasMaterialLocalizations(context)); /// assert(debugCheckHasMaterialLocalizations(context));
/// ``` /// ```
/// ///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// This function has the side-effect of establishing an inheritance /// This function has the side-effect of establishing an inheritance
/// relationship with the nearest [Localizations] widget (see /// relationship with the nearest [Localizations] widget (see
/// [BuildContext.dependOnInheritedWidgetOfExactType]). This is ok if the caller /// [BuildContext.dependOnInheritedWidgetOfExactType]). This is ok if the caller
...@@ -112,6 +120,10 @@ bool debugCheckHasMaterialLocalizations(BuildContext context) { ...@@ -112,6 +120,10 @@ bool debugCheckHasMaterialLocalizations(BuildContext context) {
/// assert(debugCheckHasScaffold(context)); /// assert(debugCheckHasScaffold(context));
/// ``` /// ```
/// ///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// This method can be expensive (it walks the element tree). /// This method can be expensive (it walks the element tree).
/// ///
/// Does nothing if asserts are disabled. Always returns true. /// Does nothing if asserts are disabled. Always returns true.
...@@ -145,6 +157,10 @@ bool debugCheckHasScaffold(BuildContext context) { ...@@ -145,6 +157,10 @@ bool debugCheckHasScaffold(BuildContext context) {
/// assert(debugCheckHasScaffoldMessenger(context)); /// assert(debugCheckHasScaffoldMessenger(context));
/// ``` /// ```
/// ///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// This method can be expensive (it walks the element tree). /// This method can be expensive (it walks the element tree).
/// ///
/// Does nothing if asserts are disabled. Always returns true. /// Does nothing if asserts are disabled. Always returns true.
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/foundation.dart' show ValueListenable, clampDouble; import 'package:flutter/foundation.dart' show ValueListenable, clampDouble;
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
...@@ -19,7 +18,11 @@ import 'theme.dart'; ...@@ -19,7 +18,11 @@ import 'theme.dart';
const double _kToolbarScreenPadding = 8.0; const double _kToolbarScreenPadding = 8.0;
const double _kToolbarWidth = 222.0; const double _kToolbarWidth = 222.0;
class _DesktopTextSelectionControls extends TextSelectionControls { /// Desktop Material styled text selection controls.
///
/// The [desktopTextSelectionControls] global variable has a
/// suitable instance of this class.
class DesktopTextSelectionControls extends TextSelectionControls {
/// Desktop has no text selection handles. /// Desktop has no text selection handles.
@override @override
Size getHandleSize(double textLineHeight) { Size getHandleSize(double textLineHeight) {
...@@ -83,7 +86,7 @@ class _DesktopTextSelectionControls extends TextSelectionControls { ...@@ -83,7 +86,7 @@ class _DesktopTextSelectionControls extends TextSelectionControls {
/// Text selection controls that loosely follows Material design conventions. /// Text selection controls that loosely follows Material design conventions.
final TextSelectionControls desktopTextSelectionControls = final TextSelectionControls desktopTextSelectionControls =
_DesktopTextSelectionControls(); DesktopTextSelectionControls();
// Generates the child that's passed into DesktopTextSelectionToolbar. // Generates the child that's passed into DesktopTextSelectionToolbar.
class _DesktopTextSelectionControlsToolbar extends StatefulWidget { class _DesktopTextSelectionControlsToolbar extends StatefulWidget {
...@@ -145,12 +148,14 @@ class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelect ...@@ -145,12 +148,14 @@ class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelect
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
assert(debugCheckHasMediaQuery(context));
// Don't render the menu until the state of the clipboard is known. // Don't render the menu until the state of the clipboard is known.
if (widget.handlePaste != null && widget.clipboardStatus?.value == ClipboardStatus.unknown) { if (widget.handlePaste != null && widget.clipboardStatus?.value == ClipboardStatus.unknown) {
return const SizedBox(width: 0.0, height: 0.0); return const SizedBox(width: 0.0, height: 0.0);
} }
assert(debugCheckHasMediaQuery(context));
final MediaQueryData mediaQuery = MediaQuery.of(context); final MediaQueryData mediaQuery = MediaQuery.of(context);
final Offset midpointAnchor = Offset( final Offset midpointAnchor = Offset(
...@@ -161,7 +166,6 @@ class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelect ...@@ -161,7 +166,6 @@ class _DesktopTextSelectionControlsToolbarState extends State<_DesktopTextSelect
widget.selectionMidpoint.dy - widget.globalEditableRegion.top, widget.selectionMidpoint.dy - widget.globalEditableRegion.top,
); );
assert(debugCheckHasMaterialLocalizations(context));
final MaterialLocalizations localizations = MaterialLocalizations.of(context); final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final List<Widget> items = <Widget>[]; final List<Widget> items = <Widget>[];
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'debug.dart';
import 'desktop_text_selection.dart'; import 'desktop_text_selection.dart';
import 'magnifier.dart'; import 'magnifier.dart';
import 'text_selection.dart'; import 'text_selection.dart';
...@@ -19,6 +20,10 @@ import 'theme.dart'; ...@@ -19,6 +20,10 @@ import 'theme.dart';
/// a specific screen, consider wrapping the body of the [Route] with a /// a specific screen, consider wrapping the body of the [Route] with a
/// [SelectionArea]. /// [SelectionArea].
/// ///
/// The [SelectionArea] widget must have a [Localizations] ancestor that
/// contains a [MaterialLocalizations] delegate; using the [MaterialApp] widget
/// ensures that such an ancestor is present.
///
/// {@tool dartpad} /// {@tool dartpad}
/// This example shows how to make a screen selectable. /// This example shows how to make a screen selectable.
/// ///
...@@ -85,6 +90,7 @@ class _SelectionAreaState extends State<SelectionArea> { ...@@ -85,6 +90,7 @@ class _SelectionAreaState extends State<SelectionArea> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMaterialLocalizations(context));
TextSelectionControls? controls = widget.selectionControls; TextSelectionControls? controls = widget.selectionControls;
switch (Theme.of(context).platform) { switch (Theme.of(context).platform) {
case TargetPlatform.android: case TargetPlatform.android:
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'debug.dart'; import 'debug.dart';
...@@ -22,6 +21,9 @@ const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0; ...@@ -22,6 +21,9 @@ const double _kToolbarContentDistanceBelow = _kHandleSize - 2.0;
const double _kToolbarContentDistance = 8.0; const double _kToolbarContentDistance = 8.0;
/// Android Material styled text selection controls. /// Android Material styled text selection controls.
///
/// The [materialTextSelectionControls] global variable has a
/// suitable instance of this class.
class MaterialTextSelectionControls extends TextSelectionControls { class MaterialTextSelectionControls extends TextSelectionControls {
/// Returns the size of the Material handle. /// Returns the size of the Material handle.
@override @override
......
...@@ -420,6 +420,8 @@ class RenderParagraph extends RenderBox ...@@ -420,6 +420,8 @@ class RenderParagraph extends RenderBox
} }
/// The color to use when painting the selection. /// The color to use when painting the selection.
///
/// Ignored if the text is not selectable (e.g. if [registrar] is null).
Color? get selectionColor => _selectionColor; Color? get selectionColor => _selectionColor;
Color? _selectionColor; Color? _selectionColor;
set selectionColor(Color? value) { set selectionColor(Color? value) {
......
...@@ -282,7 +282,7 @@ class WidgetsApp extends StatefulWidget { ...@@ -282,7 +282,7 @@ class WidgetsApp extends StatefulWidget {
/// ///
/// If [home] or [routes] are not null, the routing implementation needs to know how /// If [home] or [routes] are not null, the routing implementation needs to know how
/// appropriately build [PageRoute]s. This can be achieved by supplying the /// appropriately build [PageRoute]s. This can be achieved by supplying the
/// [pageRouteBuilder] parameter. The [pageRouteBuilder] is used by [MaterialApp] /// [pageRouteBuilder] parameter. The [pageRouteBuilder] is used by [MaterialApp]
/// and [CupertinoApp] to create [MaterialPageRoute]s and [CupertinoPageRoute], /// and [CupertinoApp] to create [MaterialPageRoute]s and [CupertinoPageRoute],
/// respectively. /// respectively.
/// ///
...@@ -296,15 +296,21 @@ class WidgetsApp extends StatefulWidget { ...@@ -296,15 +296,21 @@ class WidgetsApp extends StatefulWidget {
/// and [builder] are null, or if they fail to create a requested route, /// and [builder] are null, or if they fail to create a requested route,
/// [onGenerateRoute] will be invoked. If that fails, [onUnknownRoute] will be invoked. /// [onGenerateRoute] will be invoked. If that fails, [onUnknownRoute] will be invoked.
/// ///
/// The [pageRouteBuilder] will create a [PageRoute] that wraps newly built routes. /// The [pageRouteBuilder] is called to create a [PageRoute] that wraps newly built routes.
/// If the [builder] is non-null and the [onGenerateRoute] argument is null, then the /// If the [builder] is non-null and the [onGenerateRoute] argument is null, then the
/// [builder] will not be provided only with the context and the child widget, whereas /// [builder] will be provided only with the context and the child widget, whereas
/// the [pageRouteBuilder] will be provided with [RouteSettings]. If [onGenerateRoute] /// the [pageRouteBuilder] will be provided with [RouteSettings]; in that configuration,
/// is not provided, [navigatorKey], [onUnknownRoute], [navigatorObservers], and /// the [navigatorKey], [onUnknownRoute], [navigatorObservers], and
/// [initialRoute] must have their default values, as they will have no effect. /// [initialRoute] properties must have their default values, as they will have no effect.
/// ///
/// The `supportedLocales` argument must be a list of one or more elements. /// The `supportedLocales` argument must be a list of one or more elements.
/// By default supportedLocales is `[const Locale('en', 'US')]`. /// By default supportedLocales is `[const Locale('en', 'US')]`.
///
/// {@tool dartpad}
/// This sample shows a basic Flutter application using [WidgetsApp].
///
/// ** See code in examples/api/lib/widgets/app/widgets_app.widgets_app.0.dart **
/// {@end-tool}
WidgetsApp({ // can't be const because the asserts use methods on Iterable :-( WidgetsApp({ // can't be const because the asserts use methods on Iterable :-(
super.key, super.key,
this.navigatorKey, this.navigatorKey,
...@@ -530,8 +536,23 @@ class WidgetsApp extends StatefulWidget { ...@@ -530,8 +536,23 @@ class WidgetsApp extends StatefulWidget {
/// The [PageRoute] generator callback used when the app is navigated to a /// The [PageRoute] generator callback used when the app is navigated to a
/// named route. /// named route.
/// ///
/// A [PageRoute] represents the page in a [Navigator], so that it can
/// correctly animate between pages, and to represent the "return value" of
/// a route (e.g. which button a user selected in a modal dialog).
///
/// This callback can be used, for example, to specify that a [MaterialPageRoute] /// This callback can be used, for example, to specify that a [MaterialPageRoute]
/// or a [CupertinoPageRoute] should be used for building page transitions. /// or a [CupertinoPageRoute] should be used for building page transitions.
///
/// The [PageRouteFactory] type is generic, meaning the provided function must
/// itself be generic. For example (with special emphasis on the `<T>` at the
/// start of the closure):
///
/// ```dart
/// pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) => PageRouteBuilder<T>(
/// settings: settings,
/// pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => builder(context),
/// ),
/// ```
final PageRouteFactory? pageRouteBuilder; final PageRouteFactory? pageRouteBuilder;
/// {@template flutter.widgets.widgetsApp.routeInformationParser} /// {@template flutter.widgets.widgetsApp.routeInformationParser}
......
...@@ -5657,9 +5657,17 @@ class RichText extends MultiChildRenderObjectWidget { ...@@ -5657,9 +5657,17 @@ class RichText extends MultiChildRenderObjectWidget {
final ui.TextHeightBehavior? textHeightBehavior; final ui.TextHeightBehavior? textHeightBehavior;
/// The [SelectionRegistrar] this rich text is subscribed to. /// The [SelectionRegistrar] this rich text is subscribed to.
///
/// If this is set, [selectionColor] must be non-null.
final SelectionRegistrar? selectionRegistrar; final SelectionRegistrar? selectionRegistrar;
/// The color to use when painting the selection. /// The color to use when painting the selection.
///
/// This is ignored if [selectionRegistrar] is null.
///
/// See the section on selections in the [RichText] top-level API
/// documentation for more details on enabling selection in [RichText]
/// widgets.
final Color? selectionColor; final Color? selectionColor;
@override @override
......
...@@ -252,6 +252,10 @@ bool debugItemsHaveDuplicateKeys(Iterable<Widget> items) { ...@@ -252,6 +252,10 @@ bool debugItemsHaveDuplicateKeys(Iterable<Widget> items) {
/// assert(debugCheckHasTable(context)); /// assert(debugCheckHasTable(context));
/// ``` /// ```
/// ///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// This method can be expensive (it walks the element tree). /// This method can be expensive (it walks the element tree).
/// ///
/// Does nothing if asserts are disabled. Always returns true. /// Does nothing if asserts are disabled. Always returns true.
...@@ -282,6 +286,10 @@ bool debugCheckHasTable(BuildContext context) { ...@@ -282,6 +286,10 @@ bool debugCheckHasTable(BuildContext context) {
/// assert(debugCheckHasMediaQuery(context)); /// assert(debugCheckHasMediaQuery(context));
/// ``` /// ```
/// ///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// Does nothing if asserts are disabled. Always returns true. /// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasMediaQuery(BuildContext context) { bool debugCheckHasMediaQuery(BuildContext context) {
assert(() { assert(() {
...@@ -334,6 +342,10 @@ bool debugCheckHasMediaQuery(BuildContext context) { ...@@ -334,6 +342,10 @@ bool debugCheckHasMediaQuery(BuildContext context) {
/// If they are non-null, they are included in the order above, interspersed /// If they are non-null, they are included in the order above, interspersed
/// with the more generic advice regarding [Directionality]. /// with the more generic advice regarding [Directionality].
/// ///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// Does nothing if asserts are disabled. Always returns true. /// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasDirectionality(BuildContext context, { String? why, String? hint, String? alternative }) { bool debugCheckHasDirectionality(BuildContext context, { String? why, String? hint, String? alternative }) {
assert(() { assert(() {
...@@ -406,6 +418,10 @@ void debugWidgetBuilderValue(Widget widget, Widget? built) { ...@@ -406,6 +418,10 @@ void debugWidgetBuilderValue(Widget widget, Widget? built) {
/// assert(debugCheckHasWidgetsLocalizations(context)); /// assert(debugCheckHasWidgetsLocalizations(context));
/// ``` /// ```
/// ///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// Does nothing if asserts are disabled. Always returns true. /// Does nothing if asserts are disabled. Always returns true.
bool debugCheckHasWidgetsLocalizations(BuildContext context) { bool debugCheckHasWidgetsLocalizations(BuildContext context) {
assert(() { assert(() {
...@@ -443,6 +459,10 @@ bool debugCheckHasWidgetsLocalizations(BuildContext context) { ...@@ -443,6 +459,10 @@ bool debugCheckHasWidgetsLocalizations(BuildContext context) {
/// assert(debugCheckHasOverlay(context)); /// assert(debugCheckHasOverlay(context));
/// ``` /// ```
/// ///
/// Always place this before any early returns, so that the invariant is checked
/// in all cases. This prevents bugs from hiding until a particular codepath is
/// hit.
///
/// This method can be expensive (it walks the element tree). /// This method can be expensive (it walks the element tree).
/// ///
/// Does nothing if asserts are disabled. Always returns true. /// Does nothing if asserts are disabled. Always returns true.
......
...@@ -31,7 +31,7 @@ class DefaultSelectionStyle extends InheritedTheme { ...@@ -31,7 +31,7 @@ class DefaultSelectionStyle extends InheritedTheme {
}); });
/// A const-constructable default selection style that provides fallback /// A const-constructable default selection style that provides fallback
/// values. /// values (null).
/// ///
/// Returned from [of] when the given [BuildContext] doesn't have an enclosing /// Returned from [of] when the given [BuildContext] doesn't have an enclosing
/// default selection style. /// default selection style.
...@@ -43,6 +43,12 @@ class DefaultSelectionStyle extends InheritedTheme { ...@@ -43,6 +43,12 @@ class DefaultSelectionStyle extends InheritedTheme {
selectionColor = null, selectionColor = null,
super(child: const _NullWidget()); super(child: const _NullWidget());
/// The default cursor and selection color (semi-transparent grey).
///
/// This is the color that the [Text] widget uses when the specified selection
/// color is null.
static const Color defaultColor = Color(0x80808080);
/// The color of the text field's cursor. /// The color of the text field's cursor.
/// ///
/// The cursor indicates the current location of the text insertion point in /// The cursor indicates the current location of the text insertion point in
...@@ -88,9 +94,9 @@ class _NullWidget extends StatelessWidget { ...@@ -88,9 +94,9 @@ class _NullWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
throw FlutterError( throw FlutterError(
'A DefaultTextStyle constructed with DefaultTextStyle.fallback cannot be incorporated into the widget tree, ' 'A DefaultSelectionStyle constructed with DefaultSelectionStyle.fallback cannot be incorporated into the widget tree, '
'it is meant only to provide a fallback value returned by DefaultTextStyle.of() ' 'it is meant only to provide a fallback value returned by DefaultSelectionStyle.of() '
'when no enclosing default text style is present in a BuildContext.', 'when no enclosing default selection style is present in a BuildContext.',
); );
} }
} }
...@@ -572,7 +572,7 @@ class EditableText extends StatefulWidget { ...@@ -572,7 +572,7 @@ class EditableText extends StatefulWidget {
/// ///
/// The [controller], [focusNode], [obscureText], [autocorrect], [autofocus], /// The [controller], [focusNode], [obscureText], [autocorrect], [autofocus],
/// [showSelectionHandles], [enableInteractiveSelection], [forceLine], /// [showSelectionHandles], [enableInteractiveSelection], [forceLine],
/// [style], [cursorColor], [cursorOpacityAnimates],[backgroundCursorColor], /// [style], [cursorColor], [cursorOpacityAnimates], [backgroundCursorColor],
/// [enableSuggestions], [paintCursorAboveText], [selectionHeightStyle], /// [enableSuggestions], [paintCursorAboveText], [selectionHeightStyle],
/// [selectionWidthStyle], [textAlign], [dragStartBehavior], [scrollPadding], /// [selectionWidthStyle], [textAlign], [dragStartBehavior], [scrollPadding],
/// [dragStartBehavior], [toolbarOptions], [rendererIgnoresPointer], /// [dragStartBehavior], [toolbarOptions], [rendererIgnoresPointer],
......
...@@ -281,6 +281,10 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> { ...@@ -281,6 +281,10 @@ class _OverlayEntryWidgetState extends State<_OverlayEntryWidget> {
/// navigation and being able to insert widgets on top of the pages in an app. /// navigation and being able to insert widgets on top of the pages in an app.
/// To simply display a stack of widgets, consider using [Stack] instead. /// To simply display a stack of widgets, consider using [Stack] instead.
/// ///
/// An [Overlay] widget requires a [Directionality] widget to be in scope, so
/// that it can resolve direction-sensitive coordinates of any
/// [Positioned.directional] children.
///
/// {@tool dartpad} /// {@tool dartpad}
/// This example shows how to use the [Overlay] to highlight the [NavigationBar] /// This example shows how to use the [Overlay] to highlight the [NavigationBar]
/// destination. /// destination.
......
...@@ -7,6 +7,13 @@ import 'framework.dart'; ...@@ -7,6 +7,13 @@ import 'framework.dart';
import 'routes.dart'; import 'routes.dart';
/// A modal route that replaces the entire screen. /// A modal route that replaces the entire screen.
///
/// The [PageRouteBuilder] subclass provides a way to create a [PageRoute] using
/// callbacks rather than by defining a new class via subclassing.
///
/// See also:
///
/// * [Route], which documents the meaning of the `T` generic type argument.
abstract class PageRoute<T> extends ModalRoute<T> { abstract class PageRoute<T> extends ModalRoute<T> {
/// Creates a modal route that replaces the entire screen. /// Creates a modal route that replaces the entire screen.
PageRoute({ PageRoute({
...@@ -49,6 +56,13 @@ Widget _defaultTransitionsBuilder(BuildContext context, Animation<double> animat ...@@ -49,6 +56,13 @@ Widget _defaultTransitionsBuilder(BuildContext context, Animation<double> animat
/// ///
/// Callers must define the [pageBuilder] function which creates the route's /// Callers must define the [pageBuilder] function which creates the route's
/// primary contents. To add transitions define the [transitionsBuilder] function. /// primary contents. To add transitions define the [transitionsBuilder] function.
///
/// The `T` generic type argument corresponds to the type argument of the
/// created [Route] objects.
///
/// See also:
///
/// * [Route], which documents the meaning of the `T` generic type argument.
class PageRouteBuilder<T> extends PageRoute<T> { class PageRouteBuilder<T> extends PageRoute<T> {
/// Creates a route that delegates to builder callbacks. /// Creates a route that delegates to builder callbacks.
/// ///
...@@ -86,6 +100,8 @@ class PageRouteBuilder<T> extends PageRoute<T> { ...@@ -86,6 +100,8 @@ class PageRouteBuilder<T> extends PageRoute<T> {
/// ///
/// See [ModalRoute.buildTransitions] for complete definition of the parameters. /// See [ModalRoute.buildTransitions] for complete definition of the parameters.
/// {@endtemplate} /// {@endtemplate}
///
/// The default transition is a jump cut (i.e. no animation).
final RouteTransitionsBuilder transitionsBuilder; final RouteTransitionsBuilder transitionsBuilder;
@override @override
......
...@@ -34,6 +34,10 @@ import 'transitions.dart'; ...@@ -34,6 +34,10 @@ import 'transitions.dart';
// late StateSetter setState; // late StateSetter setState;
/// A route that displays widgets in the [Navigator]'s [Overlay]. /// A route that displays widgets in the [Navigator]'s [Overlay].
///
/// See also:
///
/// * [Route], which documents the meaning of the `T` generic type argument.
abstract class OverlayRoute<T> extends Route<T> { abstract class OverlayRoute<T> extends Route<T> {
/// Creates a route that knows how to interact with an [Overlay]. /// Creates a route that knows how to interact with an [Overlay].
OverlayRoute({ OverlayRoute({
...@@ -85,6 +89,10 @@ abstract class OverlayRoute<T> extends Route<T> { ...@@ -85,6 +89,10 @@ abstract class OverlayRoute<T> extends Route<T> {
} }
/// A route with entrance and exit transitions. /// A route with entrance and exit transitions.
///
/// See also:
///
/// * [Route], which documents the meaning of the `T` generic type argument.
abstract class TransitionRoute<T> extends OverlayRoute<T> { abstract class TransitionRoute<T> extends OverlayRoute<T> {
/// Creates a route that animates itself when it is pushed or popped. /// Creates a route that animates itself when it is pushed or popped.
TransitionRoute({ TransitionRoute({
...@@ -507,6 +515,10 @@ class LocalHistoryEntry { ...@@ -507,6 +515,10 @@ class LocalHistoryEntry {
/// pop internally if its list of local history entries is non-empty. Rather /// pop internally if its list of local history entries is non-empty. Rather
/// than being removed as the current route, the most recent [LocalHistoryEntry] /// than being removed as the current route, the most recent [LocalHistoryEntry]
/// is removed from the list and its [LocalHistoryEntry.onRemove] is called. /// is removed from the list and its [LocalHistoryEntry.onRemove] is called.
///
/// See also:
///
/// * [Route], which documents the meaning of the `T` generic type argument.
mixin LocalHistoryRoute<T> on Route<T> { mixin LocalHistoryRoute<T> on Route<T> {
List<LocalHistoryEntry>? _localHistory; List<LocalHistoryEntry>? _localHistory;
int _entriesImpliesAppBarDismissal = 0; int _entriesImpliesAppBarDismissal = 0;
...@@ -946,6 +958,10 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -946,6 +958,10 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
/// ///
/// The `T` type argument is the return value of the route. If there is no /// The `T` type argument is the return value of the route. If there is no
/// return value, consider using `void` as the return value. /// return value, consider using `void` as the return value.
///
/// See also:
///
/// * [Route], which further documents the meaning of the `T` generic type argument.
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> { abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
/// Creates a route that blocks interaction with previous routes. /// Creates a route that blocks interaction with previous routes.
ModalRoute({ ModalRoute({
......
...@@ -26,6 +26,10 @@ import 'selection_container.dart'; ...@@ -26,6 +26,10 @@ import 'selection_container.dart';
import 'text_editing_intents.dart'; import 'text_editing_intents.dart';
import 'text_selection.dart'; import 'text_selection.dart';
// Examples can assume:
// FocusNode _focusNode = FocusNode();
// late GlobalKey key;
const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{ const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{
PointerDeviceKind.touch, PointerDeviceKind.touch,
PointerDeviceKind.stylus, PointerDeviceKind.stylus,
...@@ -34,13 +38,22 @@ const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{ ...@@ -34,13 +38,22 @@ const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{
/// A widget that introduces an area for user selections. /// A widget that introduces an area for user selections.
/// ///
/// Flutter widgets are not selectable by default. To enable selection for /// Flutter widgets are not selectable by default. Wrapping a widget subtree
/// a Flutter application, consider wrapping a portion of widget subtree with /// with a [SelectableRegion] widget enables selection within that subtree (for
/// [SelectableRegion]. The wrapped subtree can be selected by users using mouse /// example, [Text] widgets automatically look for selectable regions to enable
/// or touch gestures, e.g. users can select widgets by holding the mouse /// selection). The wrapped subtree can be selected by users using mouse or
/// touch gestures, e.g. users can select widgets by holding the mouse
/// left-click and dragging across widgets, or they can use long press gestures /// left-click and dragging across widgets, or they can use long press gestures
/// to select words on touch devices. /// to select words on touch devices.
/// ///
/// A [SelectableRegion] widget requires configuration; in particular specific
/// [selectionControls] must be provided.
///
/// The [SelectionArea] widget from the [material] library configures a
/// [SelectableRegion] in a platform-specific manner (e.g. using a Material
/// toolbar on Android, a Cupertino toolbar on iOS), and it may therefore be
/// simpler to use that widget rather than using [SelectableRegion] directly.
///
/// ## An overview of the selection system. /// ## An overview of the selection system.
/// ///
/// Every [Selectable] under the [SelectableRegion] can be selected. They form a /// Every [Selectable] under the [SelectableRegion] can be selected. They form a
...@@ -77,7 +90,7 @@ const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{ ...@@ -77,7 +90,7 @@ const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{
/// MaterialApp( /// MaterialApp(
/// home: SelectableRegion( /// home: SelectableRegion(
/// selectionControls: materialTextSelectionControls, /// selectionControls: materialTextSelectionControls,
/// focusNode: FocusNode(), /// focusNode: _focusNode, // initialized to FocusNode()
/// child: Scaffold( /// child: Scaffold(
/// appBar: AppBar(title: const Text('Flutter Code Sample')), /// appBar: AppBar(title: const Text('Flutter Code Sample')),
/// body: ListView( /// body: ListView(
...@@ -160,6 +173,16 @@ const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{ ...@@ -160,6 +173,16 @@ const Set<PointerDeviceKind> _kLongPressSelectionDevices = <PointerDeviceKind>{
/// child selection area can not extend past its subtree, and the selection of /// child selection area can not extend past its subtree, and the selection of
/// the parent selection area can not extend inside the child selection area. /// the parent selection area can not extend inside the child selection area.
/// ///
/// ## Tests
///
/// In a test, a region can be selected either by faking drag events (e.g. using
/// [WidgetTester.dragFrom]) or by sending intents to a widget inside the region
/// that has been given a [GlobalKey], e.g.:
///
/// ```dart
/// Actions.invoke(key.currentContext!, const SelectAllTextIntent(SelectionChangedCause.keyboard));
/// ```
///
/// See also: /// See also:
/// * [SelectionArea], which creates a [SelectableRegion] with /// * [SelectionArea], which creates a [SelectableRegion] with
/// platform-adaptive selection controls. /// platform-adaptive selection controls.
...@@ -202,6 +225,9 @@ class SelectableRegion extends StatefulWidget { ...@@ -202,6 +225,9 @@ class SelectableRegion extends StatefulWidget {
/// The delegate to build the selection handles and toolbar for mobile /// The delegate to build the selection handles and toolbar for mobile
/// devices. /// devices.
///
/// The [emptyTextSelectionControls] global variable provides a default
/// [TextSelectionControls] implementation with no controls.
final TextSelectionControls selectionControls; final TextSelectionControls selectionControls;
@override @override
......
...@@ -14,7 +14,8 @@ import 'framework.dart'; ...@@ -14,7 +14,8 @@ import 'framework.dart';
/// ///
/// The state of this container is a single selectable and will register /// The state of this container is a single selectable and will register
/// itself to the [registrar] if provided. Otherwise, it will register to the /// itself to the [registrar] if provided. Otherwise, it will register to the
/// [SelectionRegistrar] from the context. /// [SelectionRegistrar] from the context. Consider using a [SelectionArea]
/// widget to provide a root registrar.
/// ///
/// The containers handle the [SelectionEvent]s from the registered /// The containers handle the [SelectionEvent]s from the registered
/// [SelectionRegistrar] and delegate the events to the [delegate]. /// [SelectionRegistrar] and delegate the events to the [delegate].
......
...@@ -533,6 +533,13 @@ class Text extends StatelessWidget { ...@@ -533,6 +533,13 @@ class Text extends StatelessWidget {
final ui.TextHeightBehavior? textHeightBehavior; final ui.TextHeightBehavior? textHeightBehavior;
/// The color to use when painting the selection. /// The color to use when painting the selection.
///
/// This is ignored if [SelectionContainer.maybeOf] returns null
/// in the [BuildContext] of the [Text] widget.
///
/// If null, the ambient [DefaultSelectionStyle] is used (if any); failing
/// that, the selection color defaults to [DefaultSelectionStyle.defaultColor]
/// (semi-transparent grey).
final Color? selectionColor; final Color? selectionColor;
@override @override
...@@ -558,7 +565,7 @@ class Text extends StatelessWidget { ...@@ -558,7 +565,7 @@ class Text extends StatelessWidget {
textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis, textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
textHeightBehavior: textHeightBehavior ?? defaultTextStyle.textHeightBehavior ?? DefaultTextHeightBehavior.of(context), textHeightBehavior: textHeightBehavior ?? defaultTextStyle.textHeightBehavior ?? DefaultTextHeightBehavior.of(context),
selectionRegistrar: registrar, selectionRegistrar: registrar,
selectionColor: selectionColor ?? DefaultSelectionStyle.of(context).selectionColor, selectionColor: selectionColor ?? DefaultSelectionStyle.of(context).selectionColor ?? DefaultSelectionStyle.defaultColor,
text: TextSpan( text: TextSpan(
style: effectiveTextStyle, style: effectiveTextStyle,
text: data, text: data,
......
...@@ -26,6 +26,7 @@ import 'tap_region.dart'; ...@@ -26,6 +26,7 @@ import 'tap_region.dart';
import 'ticker_provider.dart'; import 'ticker_provider.dart';
import 'transitions.dart'; import 'transitions.dart';
export 'package:flutter/rendering.dart' show TextSelectionPoint;
export 'package:flutter/services.dart' show TextSelectionDelegate; export 'package:flutter/services.dart' show TextSelectionDelegate;
/// A duration that controls how often the drag selection update callback is /// A duration that controls how often the drag selection update callback is
...@@ -76,6 +77,11 @@ class ToolbarItemsParentData extends ContainerBoxParentData<RenderBox> { ...@@ -76,6 +77,11 @@ class ToolbarItemsParentData extends ContainerBoxParentData<RenderBox> {
/// implementer of the toolbar widget. /// implementer of the toolbar widget.
/// ///
/// Override text operations such as [handleCut] if needed. /// Override text operations such as [handleCut] if needed.
///
/// See also:
///
/// * [SelectionArea], which selects appropriate text selection controls
/// based on the current platform.
abstract class TextSelectionControls { abstract class TextSelectionControls {
/// Builds a selection handle of the given `type`. /// Builds a selection handle of the given `type`.
/// ///
...@@ -97,20 +103,20 @@ abstract class TextSelectionControls { ...@@ -97,20 +103,20 @@ abstract class TextSelectionControls {
/// ///
/// Typically displays buttons for copying and pasting text. /// Typically displays buttons for copying and pasting text.
/// ///
/// [globalEditableRegion] is the TextField size of the global coordinate system /// The [globalEditableRegion] parameter is the TextField size of the global
/// in logical pixels. /// coordinate system in logical pixels.
/// ///
/// [textLineHeight] is the `preferredLineHeight` of the [RenderEditable] we /// The [textLineHeight] parameter is the [RenderEditable.preferredLineHeight]
/// are building a toolbar for. /// of the [RenderEditable] we are building a toolbar for.
/// ///
/// The [position] is a general calculation midpoint parameter of the toolbar. /// The [selectionMidpoint] parameter is a general calculation midpoint
/// If you want more detailed position information, can use [endpoints] /// parameter of the toolbar. More detailed position information
/// to calculate it. /// is computable from the [endpoints] parameter.
Widget buildToolbar( Widget buildToolbar(
BuildContext context, BuildContext context,
Rect globalEditableRegion, Rect globalEditableRegion,
double textLineHeight, double textLineHeight,
Offset position, Offset selectionMidpoint,
List<TextSelectionPoint> endpoints, List<TextSelectionPoint> endpoints,
TextSelectionDelegate delegate, TextSelectionDelegate delegate,
ValueListenable<ClipboardStatus>? clipboardStatus, ValueListenable<ClipboardStatus>? clipboardStatus,
...@@ -207,6 +213,55 @@ abstract class TextSelectionControls { ...@@ -207,6 +213,55 @@ abstract class TextSelectionControls {
} }
} }
/// Text selection controls that do not show any toolbars or handles.
///
/// This is a placeholder, suitable for temporary use during development, but
/// not practical for production. For example, it provides no way for the user
/// to interact with selections: no context menus on desktop, no toolbars or
/// drag handles on mobile, etc. For production, consider using
/// [MaterialTextSelectionControls] or creating a custom subclass of
/// [TextSelectionControls].
///
/// The [emptyTextSelectionControls] global variable has a
/// suitable instance of this class.
class EmptyTextSelectionControls extends TextSelectionControls {
@override
Size getHandleSize(double textLineHeight) => Size.zero;
@override
Widget buildToolbar(
BuildContext context,
Rect globalEditableRegion,
double textLineHeight,
Offset selectionMidpoint,
List<TextSelectionPoint> endpoints,
TextSelectionDelegate delegate,
ValueListenable<ClipboardStatus>? clipboardStatus,
Offset? lastSecondaryTapDownPosition,
) => const SizedBox.shrink();
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, [VoidCallback? onTap]) {
return const SizedBox.shrink();
}
@override
Offset getHandleAnchor(TextSelectionHandleType type, double textLineHeight) {
return Offset.zero;
}
}
/// Text selection controls that do not show any toolbars or handles.
///
/// This is a placeholder, suitable for temporary use during development, but
/// not practical for production. For example, it provides no way for the user
/// to interact with selections: no context menus on desktop, no toolbars or
/// drag handles on mobile, etc. For production, consider using
/// [materialTextSelectionControls] or creating a custom subclass of
/// [TextSelectionControls].
final TextSelectionControls emptyTextSelectionControls = EmptyTextSelectionControls();
/// An object that manages a pair of text selection handles for a /// An object that manages a pair of text selection handles for a
/// [RenderEditable]. /// [RenderEditable].
/// ///
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../widgets/editable_text_utils.dart' show textOffsetToPosition; import '../widgets/editable_text_utils.dart' show textOffsetToPosition;
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../widgets/editable_text_utils.dart' show textOffsetToPosition; import '../widgets/editable_text_utils.dart' show textOffsetToPosition;
......
// 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:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
const double _crispText = 100.0; // this font size is selected to avoid needing any antialiasing.
const String _expText = 'Éxp'; // renders in Ahem as:
// ########
// ########
// ########
// ########
//
// ÉÉÉÉxxxxpppp
void main() {
testWidgets('Default background', (WidgetTester tester) async {
await tester.pumpWidget(const Align(
alignment: Alignment.topLeft,
child: Text(_expText, textDirection: TextDirection.ltr, style: TextStyle(color: Color(0xFF345678), fontSize: _crispText))),
);
await _expectColors(
tester,
find.byType(Align),
<Color>{ const Color(0x00000000), const Color(0xFF345678) },
<Offset, Color>{
Offset.zero: const Color(0xFF345678), // the text
const Offset(10, 10): const Color(0xFF345678), // the text
const Offset(50, 95): const Color(0x00000000), // the background (under the É)
const Offset(250, 50): const Color(0x00000000), // the text (above the p)
const Offset(250, 95): const Color(0xFF345678), // the text (the p)
const Offset(400, 400): const Color(0x00000000), // the background
const Offset(799, 599): const Color(0x00000000), // the background
},
);
}, skip: !canCaptureImage); // [intended] Test relies on captureImage, which is not supported on web currently.
testWidgets('Default text color', (WidgetTester tester) async {
await tester.pumpWidget(const ColoredBox(
color: Color(0xFFABCDEF),
child: Align(
alignment: Alignment.topLeft,
child: Text('Éxp', textDirection: TextDirection.ltr, style: TextStyle(fontSize: _crispText)),
),
),
);
await _expectColors(
tester,
find.byType(Align),
<Color>{ const Color(0xFFABCDEF), const Color(0xFFFFFFFF) },
<Offset, Color>{
Offset.zero: const Color(0xFFFFFFFF), // the text
const Offset(10, 10): const Color(0xFFFFFFFF), // the text
const Offset(50, 95): const Color(0xFFABCDEF), // the background (under the É)
const Offset(250, 50): const Color(0xFFABCDEF), // the text (above the p)
const Offset(250, 95): const Color(0xFFFFFFFF), // the text (the p)
const Offset(400, 400): const Color(0xFFABCDEF), // the background
const Offset(799, 599): const Color(0xFFABCDEF), // the background
},
);
}, skip: !canCaptureImage); // [intended] Test relies on captureImage, which is not supported on web currently.
testWidgets('Default text selection color', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
await tester.pumpWidget(
ColoredBox(
color: const Color(0xFFFFFFFF),
child: Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) => SelectableRegion(
focusNode: FocusNode(),
selectionControls: emptyTextSelectionControls,
child: Align(
key: key,
alignment: Alignment.topLeft,
child: const Text('Éxp', textDirection: TextDirection.ltr, style: TextStyle(fontSize: _crispText)),
),
),
),
],
),
),
),
),
);
await _expectColors(
tester,
find.byType(Align),
<Color>{ const Color(0xFFFFFFFF) },
);
// fake a "select all" event to selecte the text
Actions.invoke(key.currentContext!, const SelectAllTextIntent(SelectionChangedCause.keyboard));
await tester.pump();
await _expectColors(
tester,
find.byType(Align),
<Color>{ const Color(0xFFFFFFFF), const Color(0xFFBFBFBF) }, // 0x80808080 blended with 0xFFFFFFFF
<Offset, Color>{
Offset.zero: const Color(0xFFBFBFBF), // the selected text
const Offset(10, 10): const Color(0xFFBFBFBF), // the selected text
const Offset(50, 95): const Color(0xFFBFBFBF), // the selected background (under the É)
const Offset(250, 50): const Color(0xFFBFBFBF), // the selected background (above the p)
const Offset(250, 95): const Color(0xFFBFBFBF), // the selected text (the p)
const Offset(400, 400): const Color(0xFFFFFFFF), // the background
const Offset(799, 599): const Color(0xFFFFFFFF), // the background
},
);
}, skip: !canCaptureImage); // [intended] Test relies on captureImage, which is not supported on web currently.
}
Color _getPixel(ByteData bytes, int x, int y, int width) {
final int offset = (x + y * width) * 4;
return Color.fromARGB(
bytes.getUint8(offset + 3),
bytes.getUint8(offset + 0),
bytes.getUint8(offset + 1),
bytes.getUint8(offset + 2),
);
}
Future<void> _expectColors(WidgetTester tester, Finder finder, Set<Color> allowedColors, [ Map<Offset, Color>? spotChecks ]) async {
final TestWidgetsFlutterBinding binding = tester.binding;
final ui.Image image = (await binding.runAsync<ui.Image>(() => captureImage(finder.evaluate().single)))!;
final ByteData bytes = (await binding.runAsync<ByteData?>(() => image.toByteData(format: ui.ImageByteFormat.rawStraightRgba)))!;
final Set<int> actualColorValues = <int>{};
for (int offset = 0; offset < bytes.lengthInBytes; offset += 4) {
actualColorValues.add((bytes.getUint8(offset + 3) << 24) +
(bytes.getUint8(offset + 0) << 16) +
(bytes.getUint8(offset + 1) << 8) +
(bytes.getUint8(offset + 2)));
}
final Set<Color> actualColors = actualColorValues.map((int value) => Color(value)).toSet();
expect(actualColors, allowedColors);
spotChecks?.forEach((Offset position, Color expected) {
assert(position.dx.round() >= 0);
assert(position.dx.round() < image.width);
assert(position.dy.round() >= 0);
assert(position.dy.round() < image.height);
final Offset precisePosition = position * binding.window.devicePixelRatio;
final Color actual = _getPixel(bytes, precisePosition.dx.round(), precisePosition.dy.round(), image.width);
expect(actual, expected, reason: 'Pixel at $position is $actual but expected $expected.');
});
}
...@@ -56,6 +56,7 @@ library flutter_test; ...@@ -56,6 +56,7 @@ library flutter_test;
export 'dart:async' show Future; export 'dart:async' show Future;
export 'src/_goldens_io.dart' if (dart.library.html) 'src/_goldens_web.dart'; export 'src/_goldens_io.dart' if (dart.library.html) 'src/_goldens_web.dart';
export 'src/_matchers_io.dart' if (dart.library.html) 'src/_matchers_web.dart';
export 'src/accessibility.dart'; export 'src/accessibility.dart';
export 'src/all_elements.dart'; export 'src/all_elements.dart';
export 'src/animation_sheet.dart'; export 'src/animation_sheet.dart';
......
...@@ -30,6 +30,15 @@ Future<ui.Image> captureImage(Element element) { ...@@ -30,6 +30,15 @@ Future<ui.Image> captureImage(Element element) {
return layer.toImage(renderObject.paintBounds); return layer.toImage(renderObject.paintBounds);
} }
/// Whether or not [captureImage] is supported.
///
/// This can be used to skip tests on platforms that don't support
/// capturing images.
///
/// Currently this is true except when tests are running in the context of a web
/// browser (`flutter test --platform chrome`).
const bool canCaptureImage = true;
/// The matcher created by [matchesGoldenFile]. This class is enabled when the /// The matcher created by [matchesGoldenFile]. This class is enabled when the
/// test is running on a VM using conditional import. /// test is running on a VM using conditional import.
class MatchesGoldenFile extends AsyncMatcher { class MatchesGoldenFile extends AsyncMatcher {
...@@ -84,7 +93,7 @@ class MatchesGoldenFile extends AsyncMatcher { ...@@ -84,7 +93,7 @@ class MatchesGoldenFile extends AsyncMatcher {
throw AssertionError('must provide a Finder, Image, Future<Image>, List<int>, or Future<List<int>>'); throw AssertionError('must provide a Finder, Image, Future<Image>, List<int>, or Future<List<int>>');
} }
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
return binding.runAsync<String?>(() async { return binding.runAsync<String?>(() async {
final ui.Image? image = await imageFuture; final ui.Image? image = await imageFuture;
if (image == null) { if (image == null) {
......
...@@ -18,6 +18,15 @@ Future<ui.Image> captureImage(Element element) { ...@@ -18,6 +18,15 @@ Future<ui.Image> captureImage(Element element) {
throw UnsupportedError('captureImage is not supported on the web.'); throw UnsupportedError('captureImage is not supported on the web.');
} }
/// Whether or not [captureImage] is supported.
///
/// This can be used to skip tests on platforms that don't support
/// capturing images.
///
/// Currently this is true except when tests are running in the context of a web
/// browser (`flutter test --platform chrome`).
const bool canCaptureImage = false;
/// The matcher created by [matchesGoldenFile]. This class is enabled when the /// The matcher created by [matchesGoldenFile]. This class is enabled when the
/// test is running in a web browser using conditional import. /// test is running in a web browser using conditional import.
class MatchesGoldenFile extends AsyncMatcher { class MatchesGoldenFile extends AsyncMatcher {
...@@ -47,7 +56,7 @@ class MatchesGoldenFile extends AsyncMatcher { ...@@ -47,7 +56,7 @@ class MatchesGoldenFile extends AsyncMatcher {
final Element element = elements.single; final Element element = elements.single;
final RenderObject renderObject = _findRepaintBoundary(element); final RenderObject renderObject = _findRepaintBoundary(element);
final Size size = renderObject.paintBounds.size; final Size size = renderObject.paintBounds.size;
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
final Element e = binding.renderViewElement!; final Element e = binding.renderViewElement!;
// Unlike `flutter_tester`, we don't have the ability to render an element // Unlike `flutter_tester`, we don't have the ability to render an element
......
...@@ -1131,28 +1131,45 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding { ...@@ -1131,28 +1131,45 @@ class AutomatedTestWidgetsFlutterBinding extends TestWidgetsFlutterBinding {
addTime(additionalTime); addTime(additionalTime);
return realAsyncZone.run<Future<T?>>(() async { return realAsyncZone.run<Future<T?>>(() {
final Completer<T?> result = Completer<T?>();
_pendingAsyncTasks = Completer<void>(); _pendingAsyncTasks = Completer<void>();
T? result;
try { try {
result = await callback(); callback().then(result.complete).catchError(
(Object exception, StackTrace stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'Flutter test framework',
context: ErrorDescription('while running async test code'),
informationCollector: () {
return <DiagnosticsNode>[
ErrorHint('The exception was caught asynchronously.'),
];
},
));
result.complete(null);
},
);
} catch (exception, stack) { } catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails( FlutterError.reportError(FlutterErrorDetails(
exception: exception, exception: exception,
stack: stack, stack: stack,
library: 'Flutter test framework', library: 'Flutter test framework',
context: ErrorDescription('while running async test code'), context: ErrorDescription('while running async test code'),
informationCollector: () {
return <DiagnosticsNode>[
ErrorHint('The exception was caught synchronously.'),
];
},
)); ));
} finally { result.complete(null);
// We complete the _pendingAsyncTasks future successfully regardless of }
// whether an exception occurred because in the case of an exception, result.future.whenComplete(() {
// we already reported the exception to FlutterError. Moreover,
// completing the future with an error would trigger an unhandled
// exception due to zone error boundaries.
_pendingAsyncTasks!.complete(); _pendingAsyncTasks!.complete();
_pendingAsyncTasks = null; _pendingAsyncTasks = null;
} });
return result; return result.future;
}); });
} }
......
...@@ -60,11 +60,11 @@ class _BufferGoldenMatcher extends AsyncMatcher { ...@@ -60,11 +60,11 @@ class _BufferGoldenMatcher extends AsyncMatcher {
/// golden files. This parameter is optional. /// golden files. This parameter is optional.
/// ///
/// {@tool snippet} /// {@tool snippet}
/// Sample invocations of [matchesGoldenFile]. /// Sample invocations of [bufferMatchesGoldenFile].
/// ///
/// ```dart /// ```dart
/// await expectLater( /// await expectLater(
/// const <int>[], /// const <int>[ /* bytes... */ ],
/// bufferMatchesGoldenFile('sample.png'), /// bufferMatchesGoldenFile('sample.png'),
/// ); /// );
/// ``` /// ```
......
...@@ -2030,7 +2030,7 @@ class _MatchesReferenceImage extends AsyncMatcher { ...@@ -2030,7 +2030,7 @@ class _MatchesReferenceImage extends AsyncMatcher {
imageFuture = captureImage(elements.single); imageFuture = captureImage(elements.single);
} }
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
return binding.runAsync<String?>(() async { return binding.runAsync<String?>(() async {
final ui.Image image = await imageFuture; final ui.Image image = await imageFuture;
final ByteData? bytes = await image.toByteData(); final ByteData? bytes = await image.toByteData();
......
...@@ -630,6 +630,23 @@ void main() { ...@@ -630,6 +630,23 @@ void main() {
key: 'abczed', key: 'abczed',
}); });
}); });
testWidgets('control test (return value)', (WidgetTester tester) async {
final String? result = await tester.binding.runAsync<String>(() async => 'Judy Turner');
expect(result, 'Judy Turner');
});
testWidgets('async throw', (WidgetTester tester) async {
final String? result = await tester.binding.runAsync<Never>(() async => throw Exception('Lois Dilettente'));
expect(result, isNull);
expect(tester.takeException(), isNotNull);
});
testWidgets('sync throw', (WidgetTester tester) async {
final String? result = await tester.binding.runAsync<Never>(() => throw Exception('Butch Barton'));
expect(result, isNull);
expect(tester.takeException(), isNotNull);
});
}); });
group('showKeyboard', () { group('showKeyboard', () {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment