diff --git a/infra/README.md b/infra/README.md index 16fc60b339..d0ae91f25f 100644 --- a/infra/README.md +++ b/infra/README.md @@ -53,7 +53,7 @@ The typical cycle for editing a recipe is: 1. Make your edits. 2. Run `build/scripts/slave/recipes.py simulation_test train flutter` to update expected files (remove the flutter if you need to do a global update). -3. Run `build/scripts/slave/recipes.py run flutter/flutter` (or flutter/engine) if something was strange during training and you need to run it locally. +3. Run `build/scripts/tools/run_recipe.py flutter/flutter` (or flutter/engine) if something was strange during training and you need to run it locally. 4. Upload the patch (`git commit`, `git cl upload`) and send it to someone in the `recipes/flutter/OWNERS` file for review. ## Editing the client.flutter buildbot master diff --git a/packages/flutter/lib/animation.dart b/packages/flutter/lib/animation.dart index c386df379e..ad8efec305 100644 --- a/packages/flutter/lib/animation.dart +++ b/packages/flutter/lib/animation.dart @@ -4,7 +4,7 @@ /// The Flutter animation system. /// -/// See [https://flutter.io/animations/] for an overview. +/// See for an overview. /// /// This library depends only on core Dart libraries and the `newton` package. library animation; diff --git a/packages/flutter/lib/shell.dart b/packages/flutter/lib/shell.dart new file mode 100644 index 0000000000..27256becc8 --- /dev/null +++ b/packages/flutter/lib/shell.dart @@ -0,0 +1,99 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Manages connections with embedder-provided services. +library shell; + +import 'dart:ui' as ui; + +import 'package:mojo/application.dart'; +import 'package:mojo/bindings.dart' as bindings; +import 'package:mojo/core.dart' as core; +import 'package:mojo/mojo/service_provider.mojom.dart' as mojom; +import 'package:mojo/mojo/shell.mojom.dart' as mojom; + +/// A replacement for shell.connectToService. Implementations should return true +/// if they handled the request, or false if the request should fall through +/// to the default requestService. +typedef bool OverrideConnectToService(String url, Object proxy); + +/// Manages connections with embedder-provided services. +class MojoShell { + MojoShell() { + assert(_instance == null); + _instance = this; + } + + /// The unique instance of this class. + static MojoShell get instance => _instance; + static MojoShell _instance; + + static mojom.ShellProxy _initShellProxy() { + core.MojoHandle shellHandle = new core.MojoHandle(ui.takeShellProxyHandle()); + if (!shellHandle.isValid) + return null; + return new mojom.ShellProxy.fromHandle(shellHandle); + } + final mojom.Shell _shell = _initShellProxy()?.ptr; + + static ApplicationConnection _initEmbedderConnection() { + core.MojoHandle servicesHandle = new core.MojoHandle(ui.takeServicesProvidedByEmbedder()); + core.MojoHandle exposedServicesHandle = new core.MojoHandle(ui.takeServicesProvidedToEmbedder()); + if (!servicesHandle.isValid || !exposedServicesHandle.isValid) + return null; + mojom.ServiceProviderProxy services = new mojom.ServiceProviderProxy.fromHandle(servicesHandle); + mojom.ServiceProviderStub exposedServices = new mojom.ServiceProviderStub.fromHandle(exposedServicesHandle); + return new ApplicationConnection(exposedServices, services); + } + final ApplicationConnection _embedderConnection = _initEmbedderConnection(); + + /// Attempts to connect to an application via the Mojo shell. + ApplicationConnection connectToApplication(String url) { + if (_shell == null) + return null; + mojom.ServiceProviderProxy services = new mojom.ServiceProviderProxy.unbound(); + mojom.ServiceProviderStub exposedServices = new mojom.ServiceProviderStub.unbound(); + _shell.connectToApplication(url, services, exposedServices); + return new ApplicationConnection(exposedServices, services); + } + + /// Set this to intercept calls to [connectToService()] and supply an + /// alternative implementation of a service (for example, a mock for testing). + OverrideConnectToService overrideConnectToService; + + /// Attempts to connect to a service implementing the interface for the given proxy. + /// If an application URL is specified, the service will be requested from that application. + /// Otherwise, it will be requested from the embedder (the Flutter engine). + void connectToService(String url, bindings.ProxyBase proxy) { + if (overrideConnectToService != null && overrideConnectToService(url, proxy)) + return; + if (url == null || _shell == null) { + // If the application URL is null, it means the service to connect + // to is one provided by the embedder. + // If the applircation URL isn't null but there's no shell, then + // ask the embedder in case it provides it. (For example, if you're + // running on Android without the Mojo shell, then you can obtain + // the media service from the embedder directly, instead of having + // to ask the media application for it.) + // This makes it easier to write an application that works both + // with and without a Mojo environment. + _embedderConnection?.requestService(proxy); + return; + } + mojom.ServiceProviderProxy services = new mojom.ServiceProviderProxy.unbound(); + _shell.connectToApplication(url, services, null); + core.MojoMessagePipe pipe = new core.MojoMessagePipe(); + proxy.impl.bind(pipe.endpoints[0]); + services.ptr.connectToService(proxy.serviceName, pipe.endpoints[1]); + services.close(); + } + + /// Registers a service to expose to the embedder. + void provideService(String interfaceName, ServiceFactory factory) { + _embedderConnection?.provideService(interfaceName, factory); + } +} + +/// The singleton object that manages connections with embedder-provided services. +MojoShell get shell => MojoShell.instance; diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart index f9832022a6..d958ee709b 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -2,13 +2,10 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:async'; -import 'dart:ui' as ui show WindowPadding, window; - import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'colors.dart'; import 'page.dart'; import 'theme.dart'; @@ -21,204 +18,75 @@ const TextStyle _errorTextStyle = const TextStyle( fontWeight: FontWeight.w900, textAlign: TextAlign.right, decoration: TextDecoration.underline, - decorationColor: const Color(0xFFFF00), + decorationColor: const Color(0xFFFFFF00), decorationStyle: TextDecorationStyle.double ); -AssetBundle _initDefaultBundle() { - if (rootBundle != null) - return rootBundle; - return new NetworkAssetBundle(Uri.base); -} -final AssetBundle _defaultBundle = _initDefaultBundle(); - -class RouteArguments { - const RouteArguments({ this.context }); - final BuildContext context; -} -typedef Widget RouteBuilder(RouteArguments args); - -typedef Future LocaleChangedCallback(Locale locale); - -class MaterialApp extends StatefulComponent { +class MaterialApp extends WidgetsApp { MaterialApp({ Key key, - this.title, - this.theme, - this.routes: const {}, - this.onGenerateRoute, - this.onLocaleChanged, + String title, + ThemeData theme, + Map routes: const {}, + RouteFactory onGenerateRoute, + LocaleChangedCallback onLocaleChanged, this.debugShowMaterialGrid: false, - this.showPerformanceOverlay: false, - this.showSemanticsDebugger: false, - this.debugShowCheckedModeBanner: true - }) : super(key: key) { - assert(routes != null); - assert(routes.containsKey(Navigator.defaultRouteName) || onGenerateRoute != null); + bool showPerformanceOverlay: false, + bool showSemanticsDebugger: false, + bool debugShowCheckedModeBanner: true + }) : theme = theme, + super( + key: key, + title: title, + textStyle: _errorTextStyle, + color: theme?.primaryColor ?? Colors.blue[500], // blue[500] is the primary color of the default theme + routes: routes, + onGenerateRoute: (RouteSettings settings) { + RouteBuilder builder = routes[settings.name]; + if (builder != null) { + return new MaterialPageRoute( + builder: (BuildContext context) { + return builder(new RouteArguments(context: context)); + }, + settings: settings + ); + } + if (onGenerateRoute != null) + return onGenerateRoute(settings); + return null; + }, + onLocaleChanged: onLocaleChanged, + showPerformanceOverlay: showPerformanceOverlay, + showSemanticsDebugger: showSemanticsDebugger, + debugShowCheckedModeBanner: debugShowCheckedModeBanner + ) { assert(debugShowMaterialGrid != null); - assert(showPerformanceOverlay != null); - assert(showSemanticsDebugger != null); } - /// A one-line description of this app for use in the window manager. - final String title; - /// The colors to use for the application's widgets. final ThemeData theme; - /// The default table of routes for the application. When the - /// [Navigator] is given a named route, the name will be looked up - /// in this table first. If the name is not available, then - /// [onGenerateRoute] will be called instead. - final Map routes; - - /// The route generator callback used when the app is navigated to a - /// named route but the name is not in the [routes] table. - final RouteFactory onGenerateRoute; - - /// Callback that is invoked when the operating system changes the - /// current locale. - final LocaleChangedCallback onLocaleChanged; - /// Turns on a [GridPaper] overlay that paints a baseline grid /// Material apps: /// https://www.google.com/design/spec/layout/metrics-keylines.html /// Only available in checked mode. final bool debugShowMaterialGrid; - /// Turns on a performance overlay. - /// https://flutter.io/debugging/#performanceoverlay - final bool showPerformanceOverlay; - - /// Turns on an overlay that shows the accessibility information - /// reported by the framework. - final bool showSemanticsDebugger; - - /// Turns on a "SLOW MODE" little banner in checked mode to indicate - /// that the app is in checked mode. This is on by default (in - /// checked mode), to turn it off, set the constructor argument to - /// false. In release mode this has no effect. - /// - /// To get this banner in your application if you're not using - /// MaterialApp, include a [CheckedModeBanner] widget in your app. - /// - /// This banner is intended to avoid people complaining that your - /// app is slow when it's in checked mode. In checked mode, Flutter - /// enables a large number of expensive diagnostics to aid in - /// development, and so performance in checked mode is not - /// representative of what will happen in release mode. - final bool debugShowCheckedModeBanner; - _MaterialAppState createState() => new _MaterialAppState(); } -EdgeDims _getPadding(ui.WindowPadding padding) { - return new EdgeDims.TRBL(padding.top, padding.right, padding.bottom, padding.left); -} - -class _MaterialAppState extends State implements BindingObserver { - - GlobalObjectKey _navigator; - - LocaleQueryData _localeData; - - void initState() { - super.initState(); - _navigator = new GlobalObjectKey(this); - didChangeLocale(ui.window.locale); - WidgetFlutterBinding.instance.addObserver(this); - } - - void dispose() { - WidgetFlutterBinding.instance.removeObserver(this); - super.dispose(); - } - - bool didPopRoute() { - assert(mounted); - NavigatorState navigator = _navigator.currentState; - assert(navigator != null); - bool result = false; - navigator.openTransaction((NavigatorTransaction transaction) { - result = transaction.pop(); - }); - return result; - } - - void didChangeMetrics() { - setState(() { - // The properties of ui.window have changed. We use them in our build - // function, so we need setState(), but we don't cache anything locally. - }); - } - - void didChangeLocale(Locale locale) { - if (config.onLocaleChanged != null) { - config.onLocaleChanged(locale).then((LocaleQueryData data) { - if (mounted) - setState(() { _localeData = data; }); - }); - } - } - - void didChangeAppLifecycleState(AppLifecycleState state) { } +class _MaterialAppState extends WidgetsAppState { final HeroController _heroController = new HeroController(); - - Route _generateRoute(RouteSettings settings) { - RouteBuilder builder = config.routes[settings.name]; - if (builder != null) { - return new MaterialPageRoute( - builder: (BuildContext context) { - return builder(new RouteArguments(context: context)); - }, - settings: settings - ); - } - if (config.onGenerateRoute != null) - return config.onGenerateRoute(settings); - return null; - } + NavigatorObserver get navigatorObserver => _heroController; Widget build(BuildContext context) { - if (config.onLocaleChanged != null && _localeData == null) { - // If the app expects a locale but we don't yet know the locale, then - // don't build the widgets now. - return new Container(); - } - ThemeData theme = config.theme ?? new ThemeData.fallback(); - Widget result = new MediaQuery( - data: new MediaQueryData( - size: ui.window.size, - devicePixelRatio: ui.window.devicePixelRatio, - padding: _getPadding(ui.window.padding) - ), - child: new LocaleQuery( - data: _localeData, - child: new AnimatedTheme( - data: theme, - duration: kThemeAnimationDuration, - child: new DefaultTextStyle( - style: _errorTextStyle, - child: new AssetVendor( - bundle: _defaultBundle, - devicePixelRatio: ui.window.devicePixelRatio, - child: new Title( - title: config.title, - color: theme.primaryColor, - child: new Navigator( - key: _navigator, - initialRoute: ui.window.defaultRouteName, - onGenerateRoute: _generateRoute, - observer: _heroController - ) - ) - ) - ) - ) - ) + Widget result = new AnimatedTheme( + data: theme, + duration: kThemeAnimationDuration, + child: super.build(context) ); assert(() { if (config.debugShowMaterialGrid) { @@ -232,27 +100,6 @@ class _MaterialAppState extends State implements BindingObserver { } return true; }); - if (config.showPerformanceOverlay) { - result = new Stack( - children: [ - result, - new Positioned(bottom: 0.0, left: 0.0, right: 0.0, child: new PerformanceOverlay.allEnabled()), - ] - ); - } - if (config.showSemanticsDebugger) { - result = new SemanticsDebugger( - child: result - ); - } - assert(() { - if (config.debugShowCheckedModeBanner) { - result = new CheckedModeBanner( - child: result - ); - } - return true; - }); return result; } diff --git a/packages/flutter/lib/src/material/bottom_sheet.dart b/packages/flutter/lib/src/material/bottom_sheet.dart index e636a81662..b92e671ba0 100644 --- a/packages/flutter/lib/src/material/bottom_sheet.dart +++ b/packages/flutter/lib/src/material/bottom_sheet.dart @@ -80,7 +80,7 @@ class _BottomSheetState extends State { onVerticalDragUpdate: _handleDragUpdate, onVerticalDragEnd: _handleDragEnd, child: new Material( - key: _childKey, + key: _childKey, child: config.builder(context) ) ); diff --git a/packages/flutter/lib/src/material/date_picker.dart b/packages/flutter/lib/src/material/date_picker.dart index 7b74a93258..85a1f18f67 100644 --- a/packages/flutter/lib/src/material/date_picker.dart +++ b/packages/flutter/lib/src/material/date_picker.dart @@ -10,6 +10,7 @@ import 'package:intl/date_symbols.dart'; import 'package:intl/intl.dart'; import 'colors.dart'; +import 'debug.dart'; import 'ink_well.dart'; import 'theme.dart'; import 'typography.dart'; @@ -403,6 +404,7 @@ class _YearPickerState extends State { } Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); return new ScrollableLazyList( itemExtent: _itemExtent, itemCount: config.lastDate.year - config.firstDate.year + 1, diff --git a/packages/flutter/lib/src/material/debug.dart b/packages/flutter/lib/src/material/debug.dart index 45e05c2320..a2ea646049 100644 --- a/packages/flutter/lib/src/material/debug.dart +++ b/packages/flutter/lib/src/material/debug.dart @@ -12,8 +12,17 @@ bool debugCheckHasMaterial(BuildContext context) { if (context.widget is! Material && context.ancestorWidgetOfExactType(Material) == null) { Element element = context; throw new WidgetError( - 'Missing Material widget.', - '${context.widget} needs to be placed inside a Material widget. Ownership chain:\n${element.debugGetOwnershipChain(10)}' + 'No Material widget found.\n' + '${context.widget.runtimeType} widgets require a Material widget ancestor.\n' + 'In material design, most widgets are conceptually "printed" on a sheet of material. In Flutter\'s material library, ' + 'that material is represented by the Material widget. It is the Material widget that renders ink splashes, for instance. ' + 'Because of this, many material library widgets require that there be a Material widget in the tree above them.\n' + 'To introduce a Material widget, you can either directly include one, or use a widget that contains Material itself, ' + 'such as a Card, Dialog, Drawer, or Scaffold.\n' + 'The specific widget that could not find a Material ancestor was:\n' + ' ${context.widget}' + 'The ownership chain for the affected widget is:\n' + ' ${element.debugGetOwnershipChain(10)}' ); } return true; @@ -27,8 +36,12 @@ bool debugCheckHasScaffold(BuildContext context) { if (Scaffold.of(context) == null) { Element element = context; throw new WidgetError( - 'Missing Scaffold widget.', - '${context.widget} needs to be placed inside the body of a Scaffold widget. Ownership chain:\n${element.debugGetOwnershipChain(10)}' + 'No Scaffold widget found.\n' + '${context.widget.runtimeType} widgets require a Scaffold widget ancestor.\n' + 'The specific widget that could not find a Scaffold ancestor was:\n' + ' ${context.widget}' + 'The ownership chain for the affected widget is:\n' + ' ${element.debugGetOwnershipChain(10)}' ); } return true; diff --git a/packages/flutter/lib/src/material/drawer_item.dart b/packages/flutter/lib/src/material/drawer_item.dart index 60b63e7d09..8f91ce7ca4 100644 --- a/packages/flutter/lib/src/material/drawer_item.dart +++ b/packages/flutter/lib/src/material/drawer_item.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.dart'; import 'colors.dart'; +import 'debug.dart'; import 'icon.dart'; import 'icons.dart'; import 'ink_well.dart'; @@ -55,6 +56,7 @@ class DrawerItem extends StatelessComponent { } Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); ThemeData themeData = Theme.of(context); List children = []; diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart index 84c976ec9e..cb79e557e1 100644 --- a/packages/flutter/lib/src/material/dropdown.dart +++ b/packages/flutter/lib/src/material/dropdown.dart @@ -182,6 +182,7 @@ class _DropDownRoute extends PopupRoute<_DropDownRouteResult> { ModalPosition getPosition(BuildContext context) { RenderBox overlayBox = Overlay.of(context).context.findRenderObject(); + assert(overlayBox != null); // can't be null; routes get inserted by Navigator which has its own Overlay Size overlaySize = overlayBox.size; RelativeRect menuRect = new RelativeRect.fromSize(rect, overlaySize); return new ModalPosition( diff --git a/packages/flutter/lib/src/material/icon_button.dart b/packages/flutter/lib/src/material/icon_button.dart index 8626974157..83e46e378b 100644 --- a/packages/flutter/lib/src/material/icon_button.dart +++ b/packages/flutter/lib/src/material/icon_button.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; +import 'debug.dart'; import 'icon.dart'; import 'icons.dart'; import 'ink_well.dart'; @@ -53,6 +54,7 @@ class IconButton extends StatelessComponent { final String tooltip; Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); Widget result = new Padding( padding: const EdgeDims.all(8.0), child: new Icon( diff --git a/packages/flutter/lib/src/material/ink_well.dart b/packages/flutter/lib/src/material/ink_well.dart index f30ba0d662..50393bdbe3 100644 --- a/packages/flutter/lib/src/material/ink_well.dart +++ b/packages/flutter/lib/src/material/ink_well.dart @@ -12,6 +12,18 @@ import 'debug.dart'; import 'material.dart'; import 'theme.dart'; +/// An area of a Material that responds to touch. Has a configurable shape and +/// can be configured to clip splashes that extend outside its bounds or not. +/// +/// For a variant of this widget that is specialised for rectangular areas that +/// always clip splashes, see [InkWell]. +/// +/// Must have an ancestor [Material] widget in which to cause ink reactions. +/// +/// If a Widget uses this class directly, it should include the following line +/// at the top of its [build] function to call [debugCheckHasMaterial]: +/// +/// assert(debugCheckHasMaterial(context)); class InkResponse extends StatefulComponent { InkResponse({ Key key, @@ -156,9 +168,14 @@ class _InkResponseState extends State { } -/// An area of a Material that responds to touch. +/// A rectangular area of a Material that responds to touch. /// -/// Must have an ancestor Material widget in which to cause ink reactions. +/// Must have an ancestor [Material] widget in which to cause ink reactions. +/// +/// If a Widget uses this class directly, it should include the following line +/// at the top of its [build] function to call [debugCheckHasMaterial]: +/// +/// assert(debugCheckHasMaterial(context)); class InkWell extends InkResponse { InkWell({ Key key, diff --git a/packages/flutter/lib/src/material/list_item.dart b/packages/flutter/lib/src/material/list_item.dart index 549345978e..84d4202ee2 100644 --- a/packages/flutter/lib/src/material/list_item.dart +++ b/packages/flutter/lib/src/material/list_item.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; +import 'debug.dart'; import 'ink_well.dart'; import 'theme.dart'; @@ -85,6 +86,7 @@ class ListItem extends StatelessComponent { } Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); final bool isTwoLine = !isThreeLine && secondary != null; final bool isOneLine = !isThreeLine && !isTwoLine; double itemHeight; diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index e1787f29cb..f606b41fe7 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -39,9 +39,8 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate { final EdgeDims padding; - void performLayout(Size size, BoxConstraints constraints) { - - BoxConstraints looseConstraints = constraints.loosen(); + void performLayout(Size size) { + BoxConstraints looseConstraints = new BoxConstraints.loose(size); // This part of the layout has the same effect as putting the toolbar and // body in a column and making the body flexible. What's different is that diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 794c266a31..0c216fd45d 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -10,6 +10,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; +import 'debug.dart'; import 'icon.dart'; import 'icons.dart'; import 'icon_theme.dart'; @@ -330,6 +331,7 @@ class _Tab extends StatelessComponent { } Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); Widget labelContent; if (label.icon == null && label.iconBuilder == null) { labelContent = _buildLabelText(); diff --git a/packages/flutter/lib/src/material/tooltip.dart b/packages/flutter/lib/src/material/tooltip.dart index 35757bc25c..66292f64db 100644 --- a/packages/flutter/lib/src/material/tooltip.dart +++ b/packages/flutter/lib/src/material/tooltip.dart @@ -46,6 +46,7 @@ class Tooltip extends StatefulComponent { assert(preferBelow != null); assert(fadeDuration != null); assert(showDuration != null); + assert(child != null); } final String message; @@ -150,7 +151,7 @@ class _TooltipState extends State { preferBelow: config.preferBelow ); }); - Overlay.of(context).insert(_entry); + Overlay.of(context, debugRequiredFor: config).insert(_entry); } _timer?.cancel(); if (_controller.status != AnimationStatus.completed) { @@ -175,7 +176,7 @@ class _TooltipState extends State { } Widget build(BuildContext context) { - assert(Overlay.of(context) != null); + assert(Overlay.of(context, debugRequiredFor: config) != null); return new GestureDetector( behavior: HitTestBehavior.opaque, onLongPress: showTooltip, diff --git a/packages/flutter/lib/src/painting/text_style.dart b/packages/flutter/lib/src/painting/text_style.dart index d03c1ade87..b832214aa7 100644 --- a/packages/flutter/lib/src/painting/text_style.dart +++ b/packages/flutter/lib/src/painting/text_style.dart @@ -31,22 +31,22 @@ class TextStyle { /// The color to use when painting the text. final Color color; - /// The name of the font to use when painting the text. + /// The name of the font to use when painting the text (e.g., Roboto). final String fontFamily; /// The size of gyphs (in logical pixels) to use when painting the text. final double fontSize; - /// The font weight to use when painting the text. + /// The typeface thickness to use when painting the text (e.g., bold). final FontWeight fontWeight; - /// The font style to use when painting the text. + /// The typeface variant to use when drawing the letters (e.g., italics). final FontStyle fontStyle; - /// The amount of space to add between each letter. + /// The amount of space (in logical pixels) to add between each letter. final double letterSpacing; - /// The amount of space to add at each sequence of white-space (i.e. between each word). + /// The amount of space (in logical pixels) to add at each sequence of white-space (i.e. between each word). final double wordSpacing; /// How the text should be aligned (applies only to the outermost @@ -59,13 +59,13 @@ class TextStyle { /// The distance between the text baselines, as a multiple of the font size. final double height; - /// The decorations to paint near the text. + /// The decorations to paint near the text (e.g., an underline). final TextDecoration decoration; /// The color in which to paint the text decorations. final Color decorationColor; - /// The style in which to paint the text decorations. + /// The style in which to paint the text decorations (e.g., dashed). final TextDecorationStyle decorationStyle; /// Returns a new text style that matches this text style but with the given diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 52105fd602..8fddbf5ce9 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -19,7 +19,7 @@ import 'semantics.dart'; export 'package:flutter/gestures.dart' show HitTestResult; /// The glue between the render tree and the Flutter engine. -abstract class Renderer extends Object with Scheduler, MojoShell +abstract class Renderer extends Object with Scheduler, Services implements HitTestable { void initInstances() { @@ -67,7 +67,7 @@ abstract class Renderer extends Object with Scheduler, MojoShell void initSemantics() { SemanticsNode.onSemanticsEnabled = renderView.scheduleInitialSemantics; - provideService(mojom.SemanticsServer.serviceName, (core.MojoMessagePipeEndpoint endpoint) { + shell.provideService(mojom.SemanticsServer.serviceName, (core.MojoMessagePipeEndpoint endpoint) { mojom.SemanticsServerStub server = new mojom.SemanticsServerStub.fromEndpoint(endpoint); server.impl = new SemanticsServer(); }); @@ -116,7 +116,7 @@ void debugDumpSemanticsTree() { /// A concrete binding for applications that use the Rendering framework /// directly. This is the glue that binds the framework to the Flutter engine. -class RenderingFlutterBinding extends BindingBase with Scheduler, Gesturer, MojoShell, Renderer { +class RenderingFlutterBinding extends BindingBase with Scheduler, Gesturer, Services, Renderer { RenderingFlutterBinding({ RenderBox root }) { assert(renderView != null); renderView.child = root; diff --git a/packages/flutter/lib/src/rendering/child_view.dart b/packages/flutter/lib/src/rendering/child_view.dart index 7e16099972..4bebb5d7ad 100644 --- a/packages/flutter/lib/src/rendering/child_view.dart +++ b/packages/flutter/lib/src/rendering/child_view.dart @@ -20,7 +20,7 @@ import 'object.dart'; mojom.ViewProxy _initViewProxy() { int viewHandle = ui.takeViewHandle(); assert(() { - if (viewHandle == 0) + if (viewHandle == core.MojoHandle.INVALID) debugPrint('Child view are supported only when running in Mojo shell.'); return true; }); @@ -35,8 +35,12 @@ mojom.ViewProxy _initViewProxy() { final mojom.ViewProxy _viewProxy = _initViewProxy(); final mojom.View _view = _viewProxy?.ptr; +/// (mojo-only) A connection with a child view. +/// +/// Used with the [ChildView] widget to display a child view. class ChildViewConnection { - ChildViewConnection({ this.url }) { + /// Establishes a connection to the app at the given URL. + ChildViewConnection({ String url }) { mojom.ViewProviderProxy viewProvider = new mojom.ViewProviderProxy.unbound(); shell.connectToService(url, viewProvider); mojom.ServiceProviderProxy incomingServices = new mojom.ServiceProviderProxy.unbound(); @@ -47,8 +51,16 @@ class ChildViewConnection { _connection = new ApplicationConnection(outgoingServices, incomingServices); } - final String url; + /// Wraps an already-established connection ot a child app. + ChildViewConnection.fromViewOwner({ + mojom.ViewOwnerProxy viewOwner, + ApplicationConnection connection + }) : _connection = connection, _viewOwner = viewOwner; + /// The underlying application connection to the child app. + /// + /// Useful for requesting services from the child app and for providing + /// services to the child app. ApplicationConnection get connection => _connection; ApplicationConnection _connection; @@ -120,18 +132,16 @@ class ChildViewConnection { ..devicePixelRatio = scale; return (await _view.layoutChild(_viewKey, layoutParams)).info; } - - String toString() { - return '$runtimeType(url: $url)'; - } } +/// (mojo-only) A view of a child application. class RenderChildView extends RenderBox { RenderChildView({ ChildViewConnection child, double scale }) : _child = child, _scale = scale; + /// The child to display. ChildViewConnection get child => _child; ChildViewConnection _child; void set child (ChildViewConnection value) { @@ -150,6 +160,7 @@ class RenderChildView extends RenderBox { } } + /// The device pixel ratio to provide the child. double get scale => _scale; double _scale; void set scale (double value) { diff --git a/packages/flutter/lib/src/rendering/custom_layout.dart b/packages/flutter/lib/src/rendering/custom_layout.dart index 8bf35ee157..f6ae9349dc 100644 --- a/packages/flutter/lib/src/rendering/custom_layout.dart +++ b/packages/flutter/lib/src/rendering/custom_layout.dart @@ -103,7 +103,7 @@ abstract class MultiChildLayoutDelegate { return '${childParentData.id}: $child'; } - void _callPerformLayout(Size size, BoxConstraints constraints, RenderBox firstChild) { + void _callPerformLayout(Size size, RenderBox firstChild) { // A particular layout delegate could be called reentrantly, e.g. if it used // by both a parent and a child. So, we must restore the _idToChild map when // we return. @@ -138,7 +138,7 @@ abstract class MultiChildLayoutDelegate { }); child = childParentData.nextSibling; } - performLayout(size, constraints); + performLayout(size); assert(() { if (_debugChildrenNeedingLayout.isNotEmpty) { if (_debugChildrenNeedingLayout.length > 1) { @@ -176,11 +176,10 @@ abstract class MultiChildLayoutDelegate { /// possible given the constraints. Size getSize(BoxConstraints constraints) => constraints.biggest; - /// Override this method to lay out and position all children given - /// this widget's size and the specified constraints. This method - /// must call [layoutChild] for each child. It should also specify - /// the final position of each child with [positionChild]. - void performLayout(Size size, BoxConstraints constraints); + /// Override this method to lay out and position all children given this + /// widget's size. This method must call [layoutChild] for each child. It + /// should also specify the final position of each child with [positionChild]. + void performLayout(Size size); /// Override this method to return true when the children need to be /// laid out. This should compare the fields of the current delegate @@ -257,7 +256,7 @@ class RenderCustomMultiChildLayoutBox extends RenderBox } void performLayout() { - delegate._callPerformLayout(size, constraints, firstChild); + delegate._callPerformLayout(size, firstChild); } void paint(PaintingContext context, Offset offset) { diff --git a/packages/flutter/lib/src/rendering/debug.dart b/packages/flutter/lib/src/rendering/debug.dart index 396b98bc83..a0950a68bc 100644 --- a/packages/flutter/lib/src/rendering/debug.dart +++ b/packages/flutter/lib/src/rendering/debug.dart @@ -53,9 +53,6 @@ bool debugPaintPointersEnabled = false; /// The color to use when reporting pointers. int debugPaintPointersColorValue = 0x00BBBB; -/// The color to use when painting [RenderErrorBox] objects in checked mode. -Color debugErrorBoxColor = const Color(0xFFFF0000); - /// Overlay a rotating set of colors when repainting layers in checked mode. bool debugRepaintRainbowEnabled = false; diff --git a/packages/flutter/lib/src/rendering/error.dart b/packages/flutter/lib/src/rendering/error.dart index 4e4b8c2e94..e6c3d00258 100644 --- a/packages/flutter/lib/src/rendering/error.dart +++ b/packages/flutter/lib/src/rendering/error.dart @@ -2,15 +2,56 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphStyle, TextStyle; + import 'box.dart'; -import 'debug.dart'; import 'object.dart'; const double _kMaxWidth = 100000.0; const double _kMaxHeight = 100000.0; -/// A render object used as a placeholder when an error occurs +/// A render object used as a placeholder when an error occurs. +/// +/// The box will be painted in the color given by the +/// [RenderErrorBox.backgroundColor] static property. +/// +/// A message can be provided. To simplify the class and thus help reduce the +/// likelihood of this class itself being the source of errors, the message +/// cannot be changed once the object has been created. If provided, the text +/// will be painted on top of the background, using the styles given by the +/// [RenderErrorBox.textStyle] and [RenderErrorBox.paragraphStyle] static +/// properties. +/// +/// Again to help simplify the class, this box tries to be 100000.0 pixels wide +/// and high, to approximate being infinitely high but without using infinities. class RenderErrorBox extends RenderBox { + /// Constructs a RenderErrorBox render object. + /// + /// A message can optionally be provided. If a message is provided, an attempt + /// will be made to render the message when the box paints. + RenderErrorBox([ this.message = '' ]) { + try { + if (message != '') { + // This class is intentionally doing things using the low-level + // primitives to avoid depending on any subsystems that may have ended + // up in an unstable state -- after all, this class is mainly used when + // things have gone wrong. + // + // Generally, the much better way to draw text in a RenderObject is to + // use the TextPainter class. If you're looking for code to crib from, + // see the paragraph.dart file and the RenderParagraph class. + ui.ParagraphBuilder builder = new ui.ParagraphBuilder(); + builder.pushStyle(textStyle); + builder.addText(message); + _paragraph = builder.build(paragraphStyle); + } + } catch (e) { } + } + + /// The message to attempt to display at paint time. + final String message; + + ui.Paragraph _paragraph; double getMinIntrinsicWidth(BoxConstraints constraints) { return constraints.constrainWidth(0.0); @@ -36,8 +77,36 @@ class RenderErrorBox extends RenderBox { size = constraints.constrain(const Size(_kMaxWidth, _kMaxHeight)); } - void paint(PaintingContext context, Offset offset) { - context.canvas.drawRect(offset & size, new Paint() .. color = debugErrorBoxColor); - } + /// The color to use when painting the background of [RenderErrorBox] objects. + static Color backgroundColor = const Color(0xF0900000); + /// The text style to use when painting [RenderErrorBox] objects. + static ui.TextStyle textStyle = new ui.TextStyle( + color: const Color(0xFFFFFF00), + fontFamily: 'monospace', + fontSize: 7.0 + ); + + /// The paragraph style to use when painting [RenderErrorBox] objects. + static ui.ParagraphStyle paragraphStyle = new ui.ParagraphStyle( + lineHeight: 0.25 // TODO(ianh): https://github.com/flutter/flutter/issues/2460 will affect this + ); + + void paint(PaintingContext context, Offset offset) { + try { + context.canvas.drawRect(offset & size, new Paint() .. color = backgroundColor); + if (_paragraph != null) { + // See the comment in the RenderErrorBox constructor. This is not the + // code you want to be copying and pasting. :-) + if (parent is RenderBox) { + RenderBox parentBox = parent; + _paragraph.maxWidth = parentBox.size.width; + } else { + _paragraph.maxWidth = size.width; + } + _paragraph.layout(); + _paragraph.paint(context.canvas, offset); + } + } catch (e) { } + } } diff --git a/packages/flutter/lib/src/services/asset_bundle.dart b/packages/flutter/lib/src/services/asset_bundle.dart index 0569939ae8..2b82857a7a 100644 --- a/packages/flutter/lib/src/services/asset_bundle.dart +++ b/packages/flutter/lib/src/services/asset_bundle.dart @@ -96,14 +96,11 @@ class MojoAssetBundle extends CachingAssetBundle { } AssetBundle _initRootBundle() { - try { - AssetBundleProxy bundle = new AssetBundleProxy.fromHandle( - new core.MojoHandle(ui.takeRootBundleHandle()) - ); - return new MojoAssetBundle(bundle); - } catch (e) { + int h = ui.takeRootBundleHandle(); + if (h == core.MojoHandle.INVALID) return null; - } + core.MojoHandle handle = new core.MojoHandle(h); + return new MojoAssetBundle(new AssetBundleProxy.fromHandle(handle)); } final AssetBundle rootBundle = _initRootBundle(); diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart index 51f71aa141..ff0023ca9b 100644 --- a/packages/flutter/lib/src/services/binding.dart +++ b/packages/flutter/lib/src/services/binding.dart @@ -1,14 +1,10 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. +// Copyright 2016 The Chromium 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' as ui; +import 'package:flutter/shell.dart'; -import 'package:mojo/application.dart'; -import 'package:mojo/bindings.dart' as bindings; -import 'package:mojo/core.dart' as core; -import 'package:mojo/mojo/service_provider.mojom.dart' as mojom; -import 'package:mojo/mojo/shell.mojom.dart' as mojom; +export 'package:flutter/shell.dart'; /// Base class for mixins that provide singleton services (also known as /// "bindings"). @@ -41,84 +37,9 @@ abstract class BindingBase { String toString() => '<$runtimeType>'; } -// A replacement for shell.connectToService. Implementations should return true -// if they handled the request, or false if the request should fall through -// to the default requestService. -typedef bool OverrideConnectToService(String url, Object proxy); - -abstract class MojoShell extends BindingBase { - +abstract class Services extends BindingBase { void initInstances() { super.initInstances(); - _instance = this; - } - - static MojoShell _instance; - static MojoShell get instance => _instance; - - static mojom.ShellProxy _initShellProxy() { - core.MojoHandle shellHandle = new core.MojoHandle(ui.takeShellProxyHandle()); - if (!shellHandle.isValid) - return null; - return new mojom.ShellProxy.fromHandle(shellHandle); - } - final mojom.Shell _shell = _initShellProxy()?.ptr; - - static ApplicationConnection _initEmbedderConnection() { - core.MojoHandle servicesHandle = new core.MojoHandle(ui.takeServicesProvidedByEmbedder()); - core.MojoHandle exposedServicesHandle = new core.MojoHandle(ui.takeServicesProvidedToEmbedder()); - if (!servicesHandle.isValid || !exposedServicesHandle.isValid) - return null; - mojom.ServiceProviderProxy services = new mojom.ServiceProviderProxy.fromHandle(servicesHandle); - mojom.ServiceProviderStub exposedServices = new mojom.ServiceProviderStub.fromHandle(exposedServicesHandle); - return new ApplicationConnection(exposedServices, services); - } - final ApplicationConnection _embedderConnection = _initEmbedderConnection(); - - /// Attempts to connect to an application via the Mojo shell. - ApplicationConnection connectToApplication(String url) { - if (_shell == null) - return null; - mojom.ServiceProviderProxy services = new mojom.ServiceProviderProxy.unbound(); - mojom.ServiceProviderStub exposedServices = new mojom.ServiceProviderStub.unbound(); - _shell.connectToApplication(url, services, exposedServices); - return new ApplicationConnection(exposedServices, services); - } - - /// Set this to intercept calls to [connectToService()] and supply an - /// alternative implementation of a service (for example, a mock for testing). - OverrideConnectToService overrideConnectToService; - - /// Attempts to connect to a service implementing the interface for the given proxy. - /// If an application URL is specified, the service will be requested from that application. - /// Otherwise, it will be requested from the embedder (the Flutter engine). - void connectToService(String url, bindings.ProxyBase proxy) { - if (overrideConnectToService != null && overrideConnectToService(url, proxy)) - return; - if (url == null || _shell == null) { - // If the application URL is null, it means the service to connect - // to is one provided by the embedder. - // If the applircation URL isn't null but there's no shell, then - // ask the embedder in case it provides it. (For example, if you're - // running on Android without the Mojo shell, then you can obtain - // the media service from the embedder directly, instead of having - // to ask the media application for it.) - // This makes it easier to write an application that works both - // with and without a Mojo environment. - _embedderConnection?.requestService(proxy); - return; - } - mojom.ServiceProviderProxy services = new mojom.ServiceProviderProxy.unbound(); - _shell.connectToApplication(url, services, null); - core.MojoMessagePipe pipe = new core.MojoMessagePipe(); - proxy.impl.bind(pipe.endpoints[0]); - services.ptr.connectToService(proxy.serviceName, pipe.endpoints[1]); - services.close(); - } - - /// Registers a service to expose to the embedder. - void provideService(String interfaceName, ServiceFactory factory) { - _embedderConnection?.provideService(interfaceName, factory); + new MojoShell(); } } -MojoShell get shell => MojoShell.instance; diff --git a/packages/flutter/lib/src/services/print.dart b/packages/flutter/lib/src/services/print.dart index 86dc01f386..4d594a6e01 100644 --- a/packages/flutter/lib/src/services/print.dart +++ b/packages/flutter/lib/src/services/print.dart @@ -46,9 +46,5 @@ void _debugPrintTask() { } void debugPrintStack() { - try { - throw new Exception(); - } catch (e, stack) { - debugPrint(stack.toString()); - } + debugPrint(StackTrace.current.toString()); } diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart new file mode 100644 index 0000000000..553676a3bb --- /dev/null +++ b/packages/flutter/lib/src/widgets/app.dart @@ -0,0 +1,223 @@ +// Copyright 2015 The Chromium 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 'dart:ui' as ui show Locale, WindowPadding, window; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +import 'asset_vendor.dart'; +import 'basic.dart'; +import 'binding.dart'; +import 'checked_mode_banner.dart'; +import 'framework.dart'; +import 'locale_query.dart'; +import 'media_query.dart'; +import 'navigator.dart'; +import 'performance_overlay.dart'; +import 'semantics_debugger.dart'; +import 'title.dart'; + +AssetBundle _initDefaultBundle() { + if (rootBundle != null) + return rootBundle; + return new NetworkAssetBundle(Uri.base); +} + +final AssetBundle _defaultBundle = _initDefaultBundle(); + +class RouteArguments { + const RouteArguments({ this.context }); + final BuildContext context; +} +typedef Widget RouteBuilder(RouteArguments args); + +typedef Future LocaleChangedCallback(Locale locale); + +class WidgetsApp extends StatefulComponent { + WidgetsApp({ + Key key, + this.title, + this.textStyle, + this.color, + this.routes: const {}, + this.onGenerateRoute, + this.onLocaleChanged, + this.showPerformanceOverlay: false, + this.showSemanticsDebugger: false, + this.debugShowCheckedModeBanner: true + }) : super(key: key) { + assert(routes != null); + assert(routes.containsKey(Navigator.defaultRouteName) || onGenerateRoute != null); + assert(showPerformanceOverlay != null); + assert(showSemanticsDebugger != null); + } + + /// A one-line description of this app for use in the window manager. + final String title; + + /// The default text style for [Text] in the application. + final TextStyle textStyle; + + /// The primary color to use for the application in the operating system + /// interface. + /// + /// For example, on Android this is the color used for the application in the + /// application switcher. + final Color color; + + /// The default table of routes for the application. When the + /// [Navigator] is given a named route, the name will be looked up + /// in this table first. If the name is not available, then + /// [onGenerateRoute] will be called instead. + final Map routes; + + /// The route generator callback used when the app is navigated to a + /// named route but the name is not in the [routes] table. + final RouteFactory onGenerateRoute; + + /// Callback that is invoked when the operating system changes the + /// current locale. + final LocaleChangedCallback onLocaleChanged; + + /// Turns on a performance overlay. + /// https://flutter.io/debugging/#performanceoverlay + final bool showPerformanceOverlay; + + /// Turns on an overlay that shows the accessibility information + /// reported by the framework. + final bool showSemanticsDebugger; + + /// Turns on a "SLOW MODE" little banner in checked mode to indicate + /// that the app is in checked mode. This is on by default (in + /// checked mode), to turn it off, set the constructor argument to + /// false. In release mode this has no effect. + /// + /// To get this banner in your application if you're not using + /// WidgetsApp, include a [CheckedModeBanner] widget in your app. + /// + /// This banner is intended to avoid people complaining that your + /// app is slow when it's in checked mode. In checked mode, Flutter + /// enables a large number of expensive diagnostics to aid in + /// development, and so performance in checked mode is not + /// representative of what will happen in release mode. + final bool debugShowCheckedModeBanner; + + WidgetsAppState createState() => new WidgetsAppState(); +} + +EdgeDims _getPadding(ui.WindowPadding padding) { + return new EdgeDims.TRBL(padding.top, padding.right, padding.bottom, padding.left); +} + +class WidgetsAppState extends State implements BindingObserver { + + GlobalObjectKey _navigator; + + LocaleQueryData _localeData; + + void initState() { + super.initState(); + _navigator = new GlobalObjectKey(this); + didChangeLocale(ui.window.locale); + WidgetFlutterBinding.instance.addObserver(this); + } + + void dispose() { + WidgetFlutterBinding.instance.removeObserver(this); + super.dispose(); + } + + bool didPopRoute() { + assert(mounted); + NavigatorState navigator = _navigator.currentState; + assert(navigator != null); + bool result = false; + navigator.openTransaction((NavigatorTransaction transaction) { + result = transaction.pop(); + }); + return result; + } + + void didChangeMetrics() { + setState(() { + // The properties of ui.window have changed. We use them in our build + // function, so we need setState(), but we don't cache anything locally. + }); + } + + void didChangeLocale(Locale locale) { + if (config.onLocaleChanged != null) { + config.onLocaleChanged(locale).then((LocaleQueryData data) { + if (mounted) + setState(() { _localeData = data; }); + }); + } + } + + void didChangeAppLifecycleState(AppLifecycleState state) { } + + NavigatorObserver get navigatorObserver => null; + + Widget build(BuildContext context) { + if (config.onLocaleChanged != null && _localeData == null) { + // If the app expects a locale but we don't yet know the locale, then + // don't build the widgets now. + // TODO(ianh): Make this unnecessary. See https://github.com/flutter/flutter/issues/1865 + return new Container(); + } + + Widget result = new MediaQuery( + data: new MediaQueryData( + size: ui.window.size, + devicePixelRatio: ui.window.devicePixelRatio, + padding: _getPadding(ui.window.padding) + ), + child: new LocaleQuery( + data: _localeData, + child: new DefaultTextStyle( + style: config.textStyle, + child: new AssetVendor( + bundle: _defaultBundle, + devicePixelRatio: ui.window.devicePixelRatio, + child: new Title( + title: config.title, + color: config.color, + child: new Navigator( + key: _navigator, + initialRoute: ui.window.defaultRouteName, + onGenerateRoute: config.onGenerateRoute, + observer: navigatorObserver + ) + ) + ) + ) + ) + ); + if (config.showPerformanceOverlay) { + result = new Stack( + children: [ + result, + new Positioned(bottom: 0.0, left: 0.0, right: 0.0, child: new PerformanceOverlay.allEnabled()), + ] + ); + } + if (config.showSemanticsDebugger) { + result = new SemanticsDebugger( + child: result + ); + } + assert(() { + if (config.debugShowCheckedModeBanner) { + result = new CheckedModeBanner( + child: result + ); + } + return true; + }); + return result; + } + +} diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index ab538b1feb..51759f7b42 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -999,6 +999,7 @@ class BlockBody extends MultiChildRenderObjectWidget { } } +/// A base class for widgets that accept [Positioned] children. abstract class StackRenderObjectWidgetBase extends MultiChildRenderObjectWidget { StackRenderObjectWidgetBase({ List children: _emptyWidgetList, diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index 58247e763f..0b353bc0dc 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -24,7 +24,7 @@ class BindingObserver { /// A concrete binding for applications based on the Widgets framework. /// This is the glue that binds the framework to the Flutter engine. -class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, MojoShell, Renderer { +class WidgetFlutterBinding extends BindingBase with Scheduler, Gesturer, Services, Renderer { /// Creates and initializes the WidgetFlutterBinding. This constructor is /// idempotent; calling it a second time will just return the diff --git a/packages/flutter/lib/src/widgets/dismissable.dart b/packages/flutter/lib/src/widgets/dismissable.dart index 05158c551f..08058a43cd 100644 --- a/packages/flutter/lib/src/widgets/dismissable.dart +++ b/packages/flutter/lib/src/widgets/dismissable.dart @@ -256,7 +256,7 @@ class _DismissableState extends State { assert(_resizeAnimation.status == AnimationStatus.completed); throw new WidgetError( 'Dismissable widget completed its resize animation without being removed from the tree.\n' - 'Make sure to implement the onDismissed handler and to immediately remove the Dismissable\n' + 'Make sure to implement the onDismissed handler and to immediately remove the Dismissable ' 'widget from the application once that handler has fired.' ); } diff --git a/packages/flutter/lib/src/widgets/drag_target.dart b/packages/flutter/lib/src/widgets/drag_target.dart index 015094ae77..8f129d099a 100644 --- a/packages/flutter/lib/src/widgets/drag_target.dart +++ b/packages/flutter/lib/src/widgets/drag_target.dart @@ -234,7 +234,7 @@ class _DraggableState extends State> { _activeCount += 1; }); return new _DragAvatar( - overlay: Overlay.of(context), + overlay: Overlay.of(context, debugRequiredFor: config), data: config.data, initialPosition: position, dragStartPoint: dragStartPoint, @@ -249,6 +249,7 @@ class _DraggableState extends State> { } Widget build(BuildContext context) { + assert(Overlay.of(context, debugRequiredFor: config) != null); final bool canDrag = config.maxSimultaneousDrags == null || _activeCount < config.maxSimultaneousDrags; final bool showChild = _activeCount == 0 || config.childWhenDragging == null; diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 1c65f8942f..35358bec45 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -149,8 +149,12 @@ abstract class GlobalKey> extends Key { message += 'The following GlobalKey was found multiple times among mounted elements: $key (${_debugDuplicates[key]} instances)\n'; message += 'The most recently registered instance is: ${_registry[key]}\n'; } - if (!_debugDuplicates.isEmpty) - throw new WidgetError('Incorrect GlobalKey usage.', message); + if (!_debugDuplicates.isEmpty) { + throw new WidgetError( + 'Incorrect GlobalKey usage.\n' + '$message' + ); + } return true; } @@ -1012,8 +1016,24 @@ abstract class Element implements BuildContext { } } +/// A widget that renders an exception's message. This widget is used when a +/// build function fails, to help with determining where the problem lies. +/// Exceptions are also logged to the console, which you can read using `flutter +/// logs`. The console will also include additional information such as the +/// stack trace for the exception. class ErrorWidget extends LeafRenderObjectWidget { - RenderBox createRenderObject() => new RenderErrorBox(); + ErrorWidget( + Object exception + ) : message = _stringify(exception), + super(key: new UniqueKey()); + final String message; + static String _stringify(Object exception) { + try { + return exception.toString(); + } catch (e) { } + return 'Error'; + } + RenderBox createRenderObject() => new RenderErrorBox(message); } typedef void BuildScheduler(BuildableElement element); @@ -1109,9 +1129,10 @@ abstract class BuildableElement extends Element { } if (_debugStateLocked && (!_debugAllowIgnoredCallsToMarkNeedsBuild || !dirty)) { throw new WidgetError( - 'Cannot mark this component as needing to build because the framework is ' - 'already in the process of building widgets. A widget can be marked as ' - 'needing to be built during the build phase only if one if its ancestor ' + 'setState() or markNeedsBuild() called during build.\n' + 'This component cannot be marked as needing to build because the framework ' + 'is already in the process of building widgets. A widget can be marked as ' + 'needing to be built during the build phase only if one if its ancestors ' 'is currently building. This exception is allowed because the framework ' 'builds parent widgets before children, which means a dirty descendant ' 'will always be built. Otherwise, the framework might not visit this ' @@ -1208,15 +1229,18 @@ abstract class ComponentElement extends BuildableElement { assert(() { if (built == null) { throw new WidgetError( - 'A build function returned null. Build functions must never return null.', - 'The offending widget is: $widget' + 'A build function returned null.\n' + 'The offending widget is: $widget\n' + 'Build functions must never return null. ' + 'To return an empty space that causes the building widget to fill available room, return "new Container()". ' + 'To return an empty space that takes as little room as possible, return "new Container(width: 0.0, height: 0.0)".' ); } return true; }); } catch (e, stack) { _debugReportException('building $_widget', e, stack); - built = new ErrorWidget(); + built = new ErrorWidget(e); } finally { // We delay marking the element as clean until after calling _builder so // that attempts to markNeedsBuild() during build() will be ignored. @@ -1228,7 +1252,7 @@ abstract class ComponentElement extends BuildableElement { assert(_child != null); } catch (e, stack) { _debugReportException('building $_widget', e, stack); - built = new ErrorWidget(); + built = new ErrorWidget(e); _child = updateChild(null, built, slot); } } @@ -1289,7 +1313,11 @@ class StatefulComponentElement> assert(() { if (_state._debugLifecycleState == _StateLifecycle.initialized) return true; - throw new WidgetError('${_state.runtimeType}.initState failed to call super.initState.'); + throw new WidgetError( + '${_state.runtimeType}.initState failed to call super.initState.\n' + 'initState() implementations must always call their superclass initState() method, to ensure ' + 'that the entire widget is initialized correctly.' + ); }); assert(() { _state._debugLifecycleState = _StateLifecycle.ready; return true; }); super._firstBuild(); @@ -1324,7 +1352,11 @@ class StatefulComponentElement> assert(() { if (_state._debugLifecycleState == _StateLifecycle.defunct) return true; - throw new WidgetError('${_state.runtimeType}.dispose failed to call super.dispose.'); + throw new WidgetError( + '${_state.runtimeType}.dispose failed to call super.dispose.\n' + 'dispose() implementations must always call their superclass dispose() method, to ensure ' + 'that all the resources used by the widget are fully released.' + ); }); assert(!dirty); // See BuildableElement.unmount for why this is important. _state._element = null; @@ -1381,12 +1413,15 @@ class ParentDataElement extends _ProxyElement { } if (ancestor != null && badAncestors.isEmpty) return true; - throw new WidgetError('Incorrect use of ParentDataWidget.', widget.debugDescribeInvalidAncestorChain( - description: "$this", - ownershipChain: parent.debugGetOwnershipChain(10), - foundValidAncestor: ancestor != null, - badAncestors: badAncestors - )); + throw new WidgetError( + 'Incorrect use of ParentDataWidget.\n' + + widget.debugDescribeInvalidAncestorChain( + description: "$this", + ownershipChain: parent.debugGetOwnershipChain(10), + foundValidAncestor: ancestor != null, + badAncestors: badAncestors + ) + ); }); super.mount(parent, slot); } @@ -1507,7 +1542,14 @@ abstract class RenderObjectElement extends Buildab // 'BuildContext' argument which you can pass to Theme.of() and other // InheritedWidget APIs which eventually trigger a rebuild.) assert(() { - throw new WidgetError('$runtimeType failed to implement reinvokeBuilders(), but got marked dirty.'); + throw new WidgetError( + '$runtimeType failed to implement reinvokeBuilders(), but got marked dirty.\n' + 'If a RenderObjectElement subclass supports being marked dirty, then the ' + 'reinvokeBuilders() method must be implemented.\n' + 'If a RenderObjectElement uses a builder callback, it must support being ' + 'marked dirty, because builder callbacks can register the object as having ' + 'an Inherited dependency.' + ); }); } @@ -1812,7 +1854,11 @@ class MultiChildRenderObjectElement exte continue; // when these nodes are reordered, we just reassign the data if (!idSet.add(child.key)) { - throw new WidgetError('If multiple keyed nodes exist as children of another node, they must have unique keys. $widget has multiple children with key "${child.key}".'); + throw new WidgetError( + 'Duplicate keys found.\n' + 'If multiple keyed nodes exist as children of another node, they must have unique keys.\n' + '$widget has multiple children with key "${child.key}".' + ); } } return false; @@ -1841,16 +1887,10 @@ class MultiChildRenderObjectElement exte } } -class WidgetError extends Error { - WidgetError(String message, [ String rawDetails = '' ]) { - rawDetails = rawDetails.trimRight(); // remove trailing newlines - if (rawDetails != '') - _message = '$message\n$rawDetails'; - else - _message = message; - } - String _message; - String toString() => _message; +class WidgetError extends AssertionError { + WidgetError(this.message); + final String message; + String toString() => message; } typedef void WidgetsExceptionHandler(String context, dynamic exception, StackTrace stack); diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index 58a4567af4..c59575c99f 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -71,11 +71,20 @@ class GestureDetector extends StatelessComponent { bool havePan = onPanStart != null || onPanUpdate != null || onPanEnd != null; bool haveScale = onScaleStart != null || onScaleUpdate != null || onScaleEnd != null; if (havePan || haveScale) { - if (havePan && haveScale) - throw new WidgetError('Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan. Just use the scale gesture recognizer.'); + if (havePan && haveScale) { + throw new WidgetError( + 'Incorrect GestureDetector arguments.\n' + 'Having both a pan gesture recognizer and a scale gesture recognizer is redundant; scale is a superset of pan. Just use the scale gesture recognizer.' + ); + } String recognizer = havePan ? 'pan' : 'scale'; - if (haveVerticalDrag && haveHorizontalDrag) - throw new WidgetError('Simultaneously having a vertical drag gesture recognizer, a horizontal drag gesture recognizer, and a $recognizer gesture recognizer will result in the $recognizer gesture recognizer being ignored, since the other two will catch all drags.'); + if (haveVerticalDrag && haveHorizontalDrag) { + throw new WidgetError( + 'Incorrect GestureDetector arguments.\n' + 'Simultaneously having a vertical drag gesture recognizer, a horizontal drag gesture recognizer, and a $recognizer gesture recognizer ' + 'will result in the $recognizer gesture recognizer being ignored, since the other two will catch all drags.' + ); + } } return true; }); @@ -279,8 +288,15 @@ class RawGestureDetectorState extends State { /// the gesture detector should be enabled. void replaceGestureRecognizers(Map gestures) { assert(() { - if (!RenderObject.debugDoingLayout) - throw new WidgetError('replaceGestureRecognizers() can only be called during the layout phase.'); + if (!RenderObject.debugDoingLayout) { + throw new WidgetError( + 'Unexpected call to replaceGestureRecognizers() method of RawGestureDetectorState.\n' + 'The replaceGestureRecognizers() method can only be called during the layout phase. ' + 'To set the gesture recognisers at other times, trigger a new build using setState() ' + 'and provide the new gesture recognisers as constructor arguments to the corresponding ' + 'RawGestureDetector or GestureDetector object.' + ); + } return true; }); _syncAll(gestures); diff --git a/packages/flutter/lib/src/widgets/heroes.dart b/packages/flutter/lib/src/widgets/heroes.dart index 8f13cdc36e..69ffc8bdfb 100644 --- a/packages/flutter/lib/src/widgets/heroes.dart +++ b/packages/flutter/lib/src/widgets/heroes.dart @@ -113,8 +113,8 @@ class Hero extends StatefulComponent { if (tagHeroes.containsKey(key)) { new WidgetError( 'There are multiple heroes that share the same key within the same subtree.\n' - 'Within each subtree for which heroes are to be animated (typically a PageRoute subtree),\n' - 'either each Hero must have a unique tag, or, all the heroes with a particular tag must\n' + 'Within each subtree for which heroes are to be animated (typically a PageRoute subtree), ' + 'either each Hero must have a unique tag, or, all the heroes with a particular tag must ' 'have different keys.\n' 'In this case, the tag "$tag" had multiple heroes with the key "$key".' ); diff --git a/packages/flutter/lib/src/widgets/mimic.dart b/packages/flutter/lib/src/widgets/mimic.dart index 5fb4818009..38fb20468f 100644 --- a/packages/flutter/lib/src/widgets/mimic.dart +++ b/packages/flutter/lib/src/widgets/mimic.dart @@ -137,6 +137,9 @@ class Mimic extends StatelessComponent { } /// A widget that can be copied by a [Mimic]. +/// +/// This widget's State, [MimicableState], contains an API for initiating the +/// mimic operation. class Mimicable extends StatefulComponent { Mimicable({ Key key, this.child }) : super(key: key); @@ -173,7 +176,18 @@ class MimicableState extends State { /// passing it to a [Mimic] widget. To mimic the child in the /// [Overlay], consider using [liftToOverlay()] instead. MimicableHandle startMimic() { - assert(_placeholderSize == null); + assert(() { + if (_placeholderSize != null) { + throw new WidgetError( + 'Mimicable started while already active.\n' + 'When startMimic() or liftToOverlay() is called on a MimicableState, the mimic becomes active. ' + 'While active, it cannot be reactivated until it is stopped. ' + 'To stop a Mimicable started with startMimic(), call the MimicableHandle object\'s stopMimic() method. ' + 'To stop a Mimicable started with liftToOverlay(), call dispose() on the MimicOverlayEntry.' + ); + } + return true; + }); RenderBox box = context.findRenderObject(); assert(box != null); assert(box.hasSize); @@ -193,8 +207,7 @@ class MimicableState extends State { /// had when the mimicking process started and (2) the child will be /// placed in the enclosing overlay. MimicOverlayEntry liftToOverlay() { - OverlayState overlay = Overlay.of(context); - assert(overlay != null); // You need an overlay to lift into. + OverlayState overlay = Overlay.of(context, debugRequiredFor: config); MimicOverlayEntry entry = new MimicOverlayEntry._(startMimic()); overlay.insert(entry._overlayEntry); return entry; diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index f293c960e2..a3941263ce 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -260,8 +260,12 @@ class Navigator extends StatefulComponent { static void openTransaction(BuildContext context, NavigatorTransactionCallback callback) { NavigatorState navigator = context.ancestorStateOfType(const TypeMatcher()); assert(() { - if (navigator == null) - throw new WidgetError('openTransaction called with a context that does not include a Navigator. The context passed to the Navigator.openTransaction() method must be that of a widget that is a descendant of a Navigator widget.'); + if (navigator == null) { + throw new WidgetError( + 'openTransaction called with a context that does not include a Navigator.\n' + 'The context passed to the Navigator.openTransaction() method must be that of a widget that is a descendant of a Navigator widget.' + ); + } return true; }); navigator.openTransaction(callback); diff --git a/packages/flutter/lib/src/widgets/overlay.dart b/packages/flutter/lib/src/widgets/overlay.dart index 015248b7f6..6b9bad218c 100644 --- a/packages/flutter/lib/src/widgets/overlay.dart +++ b/packages/flutter/lib/src/widgets/overlay.dart @@ -71,7 +71,32 @@ class Overlay extends StatefulComponent { final List initialEntries; /// The state from the closest instance of this class that encloses the given context. - static OverlayState of(BuildContext context) => context.ancestorStateOfType(const TypeMatcher()); + /// + /// In checked mode, if the [debugRequiredFor] argument is provided then this + /// function will assert that an overlay was found and will throw an exception + /// if not. The exception attempts to explain that the calling [Widget] (the + /// one given by the [debugRequiredFor] argument) needs an [Overlay] to be + /// present to function. + static OverlayState of(BuildContext context, { Widget debugRequiredFor }) { + OverlayState result = context.ancestorStateOfType(const TypeMatcher()); + assert(() { + if (debugRequiredFor != null && result == null) { + String additional = context.widget != debugRequiredFor + ? '\nThe context from which that widget was searching for an overlay was:\n $context' + : ''; + throw new WidgetError( + 'No Overlay widget found.\n' + '${debugRequiredFor.runtimeType} widgets require an Overlay widget ancestor for correct operation.\n' + 'The most common way to add an Overlay to an application is to include a MaterialApp or Navigator widget in the runApp() call.\n' + 'The specific widget that failed to find an overlay was:\n' + ' $debugRequiredFor' + '$additional' + ); + } + return true; + }); + return result; + } OverlayState createState() => new OverlayState(); } diff --git a/packages/flutter/lib/src/widgets/pageable_list.dart b/packages/flutter/lib/src/widgets/pageable_list.dart index 1b6b2c2af9..ab88721202 100644 --- a/packages/flutter/lib/src/widgets/pageable_list.dart +++ b/packages/flutter/lib/src/widgets/pageable_list.dart @@ -18,6 +18,10 @@ enum ItemsSnapAlignment { adjacentItem } +/// Scrollable widget that scrolls one "page" at a time. +/// +/// In a pageable list, one child is visible at a time. Scrolling the list +/// reveals either the next or previous child. class PageableList extends Scrollable { PageableList({ Key key, @@ -46,17 +50,34 @@ class PageableList extends Scrollable { snapOffsetCallback: snapOffsetCallback ); + /// Whether the first item should be revealed after scrolling past the last item. final bool itemsWrap; + + /// Controls whether a fling always reveals the adjacent item or whether flings can traverse many items. final ItemsSnapAlignment itemsSnapAlignment; + + /// Called when the currently visible page changes. final ValueChanged onPageChanged; + + /// Used to paint the scrollbar for this list. final ScrollableListPainter scrollableListPainter; + + /// The duration used when animating to a given page. final Duration duration; + + /// The animation curve to use when animating to a given page. final Curve curve; + + /// The list of pages themselves. final Iterable children; PageableListState createState() => new PageableListState(); } +/// State for a [PageableList] widget. +/// +/// Widgets that subclass [PageableList] can subclass this class to have +/// sensible default behaviors for pageable lists. class PageableListState extends ScrollableState { int get _itemCount => config.children?.length ?? 0; int _previousItemCount; diff --git a/packages/flutter/lib/src/widgets/placeholder.dart b/packages/flutter/lib/src/widgets/placeholder.dart index 534cec220b..f9ca3104bd 100644 --- a/packages/flutter/lib/src/widgets/placeholder.dart +++ b/packages/flutter/lib/src/widgets/placeholder.dart @@ -12,6 +12,9 @@ class Placeholder extends StatefulComponent { PlaceholderState createState() => new PlaceholderState(); } +/// State for a [Placeholder] widget. +/// +/// Useful for setting the child currently displayed by this placeholder widget. class PlaceholderState extends State { /// The child that this widget builds. /// diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart index 1b4aaf6ec7..2ecbc81f01 100644 --- a/packages/flutter/lib/src/widgets/scrollable.dart +++ b/packages/flutter/lib/src/widgets/scrollable.dart @@ -327,15 +327,9 @@ abstract class ScrollableState extends State { }); PageStorage.of(context)?.writeState(context, _scrollOffset); new ScrollNotification(this, _scrollOffset).dispatch(context); - final needsScrollStart = !_isBetweenOnScrollStartAndOnScrollEnd; - if (needsScrollStart) { - dispatchOnScrollStart(); - assert(_isBetweenOnScrollStartAndOnScrollEnd); - } + _startScroll(); dispatchOnScroll(); - assert(_isBetweenOnScrollStartAndOnScrollEnd); - if (needsScrollStart) - dispatchOnScrollEnd(); + _endScroll(); } /// Scroll this widget by the given scroll delta. @@ -372,8 +366,8 @@ abstract class ScrollableState extends State { Future _animateTo(double newScrollOffset, Duration duration, Curve curve) { _controller.stop(); _controller.value = scrollOffset; - _dispatchOnScrollStartIfNeeded(); - return _controller.animateTo(newScrollOffset, duration: duration, curve: curve).then(_dispatchOnScrollEndIfNeeded); + _startScroll(); + return _controller.animateTo(newScrollOffset, duration: duration, curve: curve).then(_endScroll); } /// Fling the scroll offset with the given velocity. @@ -401,8 +395,8 @@ abstract class ScrollableState extends State { Simulation simulation = _createSnapSimulation(scrollVelocity) ?? _createFlingSimulation(scrollVelocity); if (simulation == null) return new Future.value(); - _dispatchOnScrollStartIfNeeded(); - return _controller.animateWith(simulation).then(_dispatchOnScrollEndIfNeeded); + _startScroll(); + return _controller.animateWith(simulation).then(_endScroll); } /// Whether this scrollable should attempt to snap scroll offsets. @@ -453,14 +447,21 @@ abstract class ScrollableState extends State { return simulation; } - - bool _isBetweenOnScrollStartAndOnScrollEnd = false; + // When we start an scroll animation, we stop any previous scroll animation. + // However, the code that would deliver the onScrollEnd callback is watching + // for animations to end using a Future that resolves at the end of the + // microtask. That causes animations to "overlap" between the time we start a + // new animation and the end of the microtask. By the time the microtask is + // over and we check whether to deliver an onScrollEnd callback, we will have + // started the new animation (having skipped the onScrollStart) and therefore + // we won't deliver the onScrollEnd until the second animation is finished. + int _numberOfInProgressScrolls = 0; /// Calls the onScroll callback. /// /// Subclasses can override this function to hook the scroll callback. void dispatchOnScroll() { - assert(_isBetweenOnScrollStartAndOnScrollEnd); + assert(_numberOfInProgressScrolls > 0); if (config.onScroll != null) config.onScroll(_scrollOffset); } @@ -470,11 +471,12 @@ abstract class ScrollableState extends State { } void _handleDragStart(_) { - _dispatchOnScrollStartIfNeeded(); + _startScroll(); } - void _dispatchOnScrollStartIfNeeded() { - if (!_isBetweenOnScrollStartAndOnScrollEnd) + void _startScroll() { + _numberOfInProgressScrolls += 1; + if (_numberOfInProgressScrolls == 1) dispatchOnScrollStart(); } @@ -482,8 +484,7 @@ abstract class ScrollableState extends State { /// /// Subclasses can override this function to hook the scroll start callback. void dispatchOnScrollStart() { - assert(!_isBetweenOnScrollStartAndOnScrollEnd); - _isBetweenOnScrollStartAndOnScrollEnd = true; + assert(_numberOfInProgressScrolls == 1); if (config.onScrollStart != null) config.onScrollStart(_scrollOffset); } @@ -495,11 +496,12 @@ abstract class ScrollableState extends State { Future _handleDragEnd(Velocity velocity) { double scrollVelocity = pixelDeltaToScrollOffset(velocity.pixelsPerSecond) / Duration.MILLISECONDS_PER_SECOND; // The gesture velocity properties are pixels/second, config min,max limits are pixels/ms - return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then(_dispatchOnScrollEndIfNeeded); + return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then(_endScroll); } - void _dispatchOnScrollEndIfNeeded(_) { - if (_isBetweenOnScrollStartAndOnScrollEnd) + void _endScroll([_]) { + _numberOfInProgressScrolls -= 1; + if (_numberOfInProgressScrolls == 0) dispatchOnScrollEnd(); } @@ -507,8 +509,7 @@ abstract class ScrollableState extends State { /// /// Subclasses can override this function to hook the scroll end callback. void dispatchOnScrollEnd() { - assert(_isBetweenOnScrollStartAndOnScrollEnd); - _isBetweenOnScrollStartAndOnScrollEnd = false; + assert(_numberOfInProgressScrolls == 0); if (config.onScrollEnd != null) config.onScrollEnd(_scrollOffset); } diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 5e50f13468..fcea6581e2 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -5,6 +5,7 @@ /// The Flutter widget framework. library widgets; +export 'src/widgets/app.dart'; export 'src/widgets/asset_vendor.dart'; export 'src/widgets/auto_layout.dart'; export 'src/widgets/basic.dart'; diff --git a/packages/flutter/test/rendering/rendering_tester.dart b/packages/flutter/test/rendering/rendering_tester.dart index fb700b9cd4..6ac6287faf 100644 --- a/packages/flutter/test/rendering/rendering_tester.dart +++ b/packages/flutter/test/rendering/rendering_tester.dart @@ -26,7 +26,7 @@ enum EnginePhase { composite } -class TestRenderingFlutterBinding extends BindingBase with Scheduler, MojoShell, Renderer, Gesturer { +class TestRenderingFlutterBinding extends BindingBase with Scheduler, Services, Renderer, Gesturer { void initRenderView() { if (renderView == null) { renderView = new TestRenderView(); @@ -58,7 +58,7 @@ void layout(RenderBox box, { BoxConstraints constraints, EnginePhase phase: Engi _renderer ??= new TestRenderingFlutterBinding(); - renderer.renderView.child = null; + renderer.renderView.child = null; if (constraints != null) { box = new RenderPositionedBox( child: new RenderConstrainedBox( diff --git a/packages/flutter/test/widget/custom_multi_child_layout_test.dart b/packages/flutter/test/widget/custom_multi_child_layout_test.dart index 6145a77cce..900ec4d463 100644 --- a/packages/flutter/test/widget/custom_multi_child_layout_test.dart +++ b/packages/flutter/test/widget/custom_multi_child_layout_test.dart @@ -17,16 +17,15 @@ class TestMultiChildLayoutDelegate extends MultiChildLayoutDelegate { } Size performLayoutSize; - BoxConstraints performLayoutConstraints; Size performLayoutSize0; Size performLayoutSize1; bool performLayoutIsChild; - void performLayout(Size size, BoxConstraints constraints) { + void performLayout(Size size) { assert(!RenderObject.debugCheckingIntrinsics); expect(() { performLayoutSize = size; - performLayoutConstraints = constraints; + BoxConstraints constraints = new BoxConstraints.loose(size); performLayoutSize0 = layoutChild(0, constraints); performLayoutSize1 = layoutChild(1, constraints); performLayoutIsChild = isChild('fred'); @@ -68,10 +67,6 @@ void main() { expect(delegate.performLayoutSize.width, 200.0); expect(delegate.performLayoutSize.height, 300.0); - expect(delegate.performLayoutConstraints.minWidth, 0.0); - expect(delegate.performLayoutConstraints.maxWidth, 800.0); - expect(delegate.performLayoutConstraints.minHeight, 0.0); - expect(delegate.performLayoutConstraints.maxHeight, 600.0); expect(delegate.performLayoutSize0.width, 150.0); expect(delegate.performLayoutSize0.height, 100.0); expect(delegate.performLayoutSize1.width, 100.0); diff --git a/packages/flutter/test/widget/scroll_events_test.dart b/packages/flutter/test/widget/scroll_events_test.dart index 0bc09ae96e..ad08a14182 100644 --- a/packages/flutter/test/widget/scroll_events_test.dart +++ b/packages/flutter/test/widget/scroll_events_test.dart @@ -95,4 +95,45 @@ void main() { expect(log, equals(['scrollstart', 'scroll', 'scroll', 'scrollend'])); }); }); + + test('Scroll during animation', () { + testWidgets((WidgetTester tester) { + GlobalKey scrollKey = new GlobalKey(); + List log = []; + tester.pumpWidget(_buildScroller(key: scrollKey, log: log)); + + expect(log, equals([])); + scrollKey.currentState.scrollTo(100.0, duration: const Duration(seconds: 1)); + expect(log, equals(['scrollstart'])); + tester.pump(const Duration(milliseconds: 100)); + expect(log, equals(['scrollstart'])); + tester.pump(const Duration(milliseconds: 100)); + expect(log, equals(['scrollstart', 'scroll'])); + scrollKey.currentState.scrollTo(100.0, duration: const Duration(seconds: 1)); + expect(log, equals(['scrollstart', 'scroll'])); + tester.pump(const Duration(milliseconds: 100)); + expect(log, equals(['scrollstart', 'scroll'])); + tester.pump(const Duration(milliseconds: 1500)); + expect(log, equals(['scrollstart', 'scroll', 'scroll', 'scrollend'])); + }); + }); + + test('fling, fling generates one start/end pair', () { + testWidgets((WidgetTester tester) { + GlobalKey scrollKey = new GlobalKey(); + List log = []; + tester.pumpWidget(_buildScroller(key: scrollKey, log: log)); + + expect(log, equals([])); + tester.flingFrom(new Point(100.0, 100.0), new Offset(-50.0, -50.0), 500.0); + tester.pump(new Duration(seconds: 1)); + log.removeWhere((String value) => value == 'scroll'); + expect(log, equals(['scrollstart'])); + tester.flingFrom(new Point(100.0, 100.0), new Offset(-50.0, -50.0), 500.0); + tester.pump(new Duration(seconds: 1)); + tester.pump(new Duration(seconds: 1)); + log.removeWhere((String value) => value == 'scroll'); + expect(log, equals(['scrollstart', 'scrollend'])); + }); + }); } diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart index a47842e71f..50e8e30d43 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart @@ -78,7 +78,7 @@ class WidgetTester extends Instrumentation { super(binding: _SteppedWidgetFlutterBinding.ensureInitialized()) { timeDilation = 1.0; ui.window.onBeginFrame = null; - runApp(new ErrorWidget()); // flush out the last build entirely + runApp(new Container(key: new UniqueKey())); // flush out the last build entirely } final FakeAsync async; diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index d13f9cac05..8e6fc399da 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -128,21 +128,19 @@ All done! In order to run your application, type: void _renderTemplates(String projectName, String dirPath, String flutterPackagesDirectory, { bool renderDriverTest: false }) { - String relativePackagesDirectory = path.relative( - flutterPackagesDirectory, - from: path.join(dirPath, 'pubspec.yaml') - ); + new Directory(dirPath).createSync(recursive: true); + + flutterPackagesDirectory = path.normalize(flutterPackagesDirectory); + flutterPackagesDirectory = _relativePath(from: dirPath, to: flutterPackagesDirectory); printStatus('Creating project ${path.basename(projectName)}:'); - new Directory(dirPath).createSync(recursive: true); - Map templateContext = { 'projectName': projectName, 'androidIdentifier': _createAndroidIdentifier(projectName), 'iosIdentifier': _createUTIIdentifier(projectName), 'description': description, - 'flutterPackagesDirectory': relativePackagesDirectory, + 'flutterPackagesDirectory': flutterPackagesDirectory, 'androidMinApiLevel': android.minApiLevel }; @@ -211,3 +209,11 @@ String _validateProjectName(String projectName) { return null; } + +String _relativePath({ String from, String to }) { + String result = path.relative(to, from: from); + // `path.relative()` doesn't always return a correct result: dart-lang/path#12. + if (FileSystemEntity.isDirectorySync(path.join(from, result))) + return result; + return to; +} diff --git a/packages/newton/lib/src/simulation.dart b/packages/newton/lib/src/simulation.dart index def3b376c2..8e494ac724 100644 --- a/packages/newton/lib/src/simulation.dart +++ b/packages/newton/lib/src/simulation.dart @@ -4,19 +4,17 @@ import 'tolerance.dart'; -abstract class Simulatable { +/// The base class for all simulations. The user is meant to instantiate an +/// instance of a simulation and query the same for the position and velocity +/// of the body at a given interval. +abstract class Simulation { + Tolerance tolerance = toleranceDefault; + /// The current position of the object in the simulation double x(double time); /// The current velocity of the object in the simulation - double dx(double time); -} - -/// The base class for all simulations. The user is meant to instantiate an -/// instance of a simulation and query the same for the position and velocity -/// of the body at a given interval. -abstract class Simulation implements Simulatable { - Tolerance tolerance = toleranceDefault; + double dx(double time); // TODO(ianh): remove this; see https://github.com/flutter/flutter/issues/2092 /// Returns if the simulation is done at a given time bool isDone(double time); diff --git a/packages/newton/lib/src/simulation_group.dart b/packages/newton/lib/src/simulation_group.dart index 76fb440d61..fe36d739d9 100644 --- a/packages/newton/lib/src/simulation_group.dart +++ b/packages/newton/lib/src/simulation_group.dart @@ -10,7 +10,8 @@ import 'utils.dart'; /// must implement the appropriate methods to select the appropriate simulation /// at a given time interval. The simulation group takes care to call the `step` /// method at appropriate intervals. If more fine grained control over the the -/// step is necessary, subclasses may override `Simulatable` methods. +/// step is necessary, subclasses may override the [x], [dx], and [isDone] +/// methods. abstract class SimulationGroup extends Simulation { /// The currently active simulation diff --git a/packages/newton/lib/src/spring_simulation.dart b/packages/newton/lib/src/spring_simulation.dart index 8406a7930a..324a753b16 100644 --- a/packages/newton/lib/src/spring_simulation.dart +++ b/packages/newton/lib/src/spring_simulation.dart @@ -7,120 +7,154 @@ import 'dart:math' as math; import 'simulation.dart'; import 'utils.dart'; -abstract class _SpringSolution implements Simulatable { +enum SpringType { unknown, criticallyDamped, underDamped, overDamped } + +abstract class _SpringSolution { factory _SpringSolution( - SpringDescription desc, double initialPosition, double initialVelocity) { - double cmk = - desc.damping * desc.damping - 4 * desc.mass * desc.springConstant; - - if (cmk == 0.0) { + SpringDescription desc, + double initialPosition, + double initialVelocity + ) { + double cmk = desc.damping * desc.damping - 4 * desc.mass * desc.springConstant; + if (cmk == 0.0) return new _CriticalSolution(desc, initialPosition, initialVelocity); - } else if (cmk > 0.0) { + if (cmk > 0.0) return new _OverdampedSolution(desc, initialPosition, initialVelocity); - } else { - return new _UnderdampedSolution(desc, initialPosition, initialVelocity); - } - - return null; + return new _UnderdampedSolution(desc, initialPosition, initialVelocity); } + double x(double time); + double dx(double time); SpringType get type; } class _CriticalSolution implements _SpringSolution { - final double _r, _c1, _c2; - factory _CriticalSolution( - SpringDescription desc, double distance, double velocity) { + SpringDescription desc, + double distance, + double velocity + ) { final double r = -desc.damping / (2.0 * desc.mass); final double c1 = distance; final double c2 = velocity / (r * distance); return new _CriticalSolution.withArgs(r, c1, c2); } - SpringType get type => SpringType.criticallyDamped; - _CriticalSolution.withArgs(double r, double c1, double c2) - : _r = r, - _c1 = c1, - _c2 = c2; + : _r = r, + _c1 = c1, + _c2 = c2; - double x(double time) => (_c1 + _c2 * time) * math.pow(math.E, _r * time); + final double _r, _c1, _c2; + + double x(double time) { + return (_c1 + _c2 * time) * math.pow(math.E, _r * time); + } double dx(double time) { final double power = math.pow(math.E, _r * time); return _r * (_c1 + _c2 * time) * power + _c2 * power; } + + SpringType get type => SpringType.criticallyDamped; } class _OverdampedSolution implements _SpringSolution { - final double _r1, _r2, _c1, _c2; - factory _OverdampedSolution( - SpringDescription desc, double distance, double velocity) { - final double cmk = - desc.damping * desc.damping - 4 * desc.mass * desc.springConstant; - + SpringDescription desc, + double distance, + double velocity + ) { + final double cmk = desc.damping * desc.damping - 4 * desc.mass * desc.springConstant; final double r1 = (-desc.damping - math.sqrt(cmk)) / (2.0 * desc.mass); final double r2 = (-desc.damping + math.sqrt(cmk)) / (2.0 * desc.mass); final double c2 = (velocity - r1 * distance) / (r2 - r1); final double c1 = distance - c2; - return new _OverdampedSolution.withArgs(r1, r2, c1, c2); } _OverdampedSolution.withArgs(double r1, double r2, double c1, double c2) - : _r1 = r1, - _r2 = r2, - _c1 = c1, - _c2 = c2; + : _r1 = r1, + _r2 = r2, + _c1 = c1, + _c2 = c2; + + final double _r1, _r2, _c1, _c2; + + double x(double time) { + return _c1 * math.pow(math.E, _r1 * time) + + _c2 * math.pow(math.E, _r2 * time); + } + + double dx(double time) { + return _c1 * _r1 * math.pow(math.E, _r1 * time) + + _c2 * _r2 * math.pow(math.E, _r2 * time); + } SpringType get type => SpringType.overDamped; - - double x(double time) => - (_c1 * math.pow(math.E, _r1 * time) + _c2 * math.pow(math.E, _r2 * time)); - - double dx(double time) => (_c1 * _r1 * math.pow(math.E, _r1 * time) + - _c2 * _r2 * math.pow(math.E, _r2 * time)); } class _UnderdampedSolution implements _SpringSolution { - final double _w, _r, _c1, _c2; - factory _UnderdampedSolution( - SpringDescription desc, double distance, double velocity) { + SpringDescription desc, + double distance, + double velocity + ) { final double w = math.sqrt(4.0 * desc.mass * desc.springConstant - - desc.damping * desc.damping) / - (2.0 * desc.mass); + desc.damping * desc.damping) / (2.0 * desc.mass); final double r = -(desc.damping / 2.0 * desc.mass); final double c1 = distance; final double c2 = (velocity - r * distance) / w; - return new _UnderdampedSolution.withArgs(w, r, c1, c2); } _UnderdampedSolution.withArgs(double w, double r, double c1, double c2) - : _w = w, - _r = r, - _c1 = c1, - _c2 = c2; + : _w = w, + _r = r, + _c1 = c1, + _c2 = c2; - SpringType get type => SpringType.underDamped; + final double _w, _r, _c1, _c2; - double x(double time) => math.pow(math.E, _r * time) * - (_c1 * math.cos(_w * time) + _c2 * math.sin(_w * time)); + double x(double time) { + return math.pow(math.E, _r * time) * + (_c1 * math.cos(_w * time) + _c2 * math.sin(_w * time)); + } double dx(double time) { final double power = math.pow(math.E, _r * time); final double cosine = math.cos(_w * time); final double sine = math.sin(_w * time); - - return power * (_c2 * _w * cosine - _c1 * _w * sine) + - _r * power * (_c2 * sine + _c1 * cosine); + return power * (_c2 * _w * cosine - _c1 * _w * sine) + + _r * power * (_c2 * sine + _c1 * cosine); } + + SpringType get type => SpringType.underDamped; } class SpringDescription { + SpringDescription({ + this.mass, + this.springConstant, + this.damping + }) { + assert(mass != null); + assert(springConstant != null); + assert(damping != null); + } + + /// Create a spring given the mass, spring constant and the damping ratio. The + /// damping ratio is especially useful trying to determing the type of spring + /// to create. A ratio of 1.0 creates a critically damped spring, > 1.0 + /// creates an overdamped spring and < 1.0 an underdamped one. + SpringDescription.withDampingRatio({ + double mass, + double springConstant, + double ratio: 1.0 + }) : mass = mass, + springConstant = springConstant, + damping = ratio * 2.0 * math.sqrt(mass * springConstant); + /// The mass of the spring (m) final double mass; @@ -131,41 +165,23 @@ class SpringDescription { /// Not to be confused with the damping ratio. Use the separate /// constructor provided for this purpose final double damping; - - SpringDescription( - { this.mass, this.springConstant, this.damping } - ) { - assert(mass != null); - assert(springConstant != null); - assert(damping != null); - } - - /// Create a spring given the mass, spring constant and the damping ratio. The - /// damping ratio is especially useful trying to determing the type of spring - /// to create. A ratio of 1.0 creates a critically damped spring, > 1.0 - /// creates an overdamped spring and < 1.0 an underdamped one. - SpringDescription.withDampingRatio( - {double mass, double springConstant, double ratio: 1.0}) - : mass = mass, - springConstant = springConstant, - damping = ratio * 2.0 * math.sqrt(mass * springConstant); } -enum SpringType { unknown, criticallyDamped, underDamped, overDamped, } - /// Creates a spring simulation. Depending on the spring description, a /// critically, under or overdamped spring will be created. class SpringSimulation extends Simulation { - final double _endPosition; - - final _SpringSolution _solution; - /// A spring description with the provided spring description, start distance, /// end distance and velocity. SpringSimulation( - SpringDescription desc, double start, double end, double velocity) - : this._endPosition = end, - _solution = new _SpringSolution(desc, start - end, velocity); + SpringDescription desc, + double start, + double end, + double velocity + ) : _endPosition = end, + _solution = new _SpringSolution(desc, start - end, velocity); + + final double _endPosition; + final _SpringSolution _solution; SpringType get type => _solution.type; @@ -182,8 +198,12 @@ class SpringSimulation extends Simulation { /// A SpringSimulation where the value of x() is guaranteed to have exactly the /// end value when the simulation isDone(). class ScrollSpringSimulation extends SpringSimulation { - ScrollSpringSimulation(SpringDescription desc, double start, double end, double velocity) - : super(desc, start, end, velocity); + ScrollSpringSimulation( + SpringDescription desc, + double start, + double end, + double velocity + ) : super(desc, start, end, velocity); double x(double time) => isDone(time) ? _endPosition : super.x(time); }