diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index 0a7d28a1d5..3382ca4b4d 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -209,7 +209,13 @@ class AppBar extends StatelessWidget { toolBarRow.add(new Flexible( child: new Padding( padding: new EdgeInsets.only(left: 8.0), - child: title != null ? new DefaultTextStyle(style: centerStyle, child: title) : null + child: title != null ? + new DefaultTextStyle( + style: centerStyle, + softWrap: false, + overflow: TextOverflow.ellipsis, + child: title + ) : null ) )); if (actions != null) diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 02a9160139..15cb499872 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -345,7 +345,12 @@ class _Tab extends StatelessWidget { Widget _buildLabelText() { assert(label.text != null); TextStyle style = new TextStyle(color: color); - return new Text(label.text, style: style); + return new Text( + label.text, + style: style, + softWrap: false, + overflow: TextOverflow.fade + ); } Widget _buildLabelIcon(BuildContext context) { @@ -371,15 +376,15 @@ class _Tab extends StatelessWidget { labelContent = _buildLabelIcon(context); } else { labelContent = new Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ new Container( child: _buildLabelIcon(context), margin: const EdgeInsets.only(bottom: 10.0) ), _buildLabelText() - ], - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center + ] ); } diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart index 2a23a40d83..2c4366dc6c 100644 --- a/packages/flutter/lib/src/rendering/paragraph.dart +++ b/packages/flutter/lib/src/rendering/paragraph.dart @@ -2,20 +2,42 @@ // 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/gestures.dart'; import 'box.dart'; import 'object.dart'; import 'semantics.dart'; +/// How overflowing text should be handled. +enum TextOverflow { + /// Clip the overflowing text to fix its container. + clip, + + /// Fade the overflowing text to transparent. + fade, + + /// Use an ellipsis to indicate that the text has overflowed. + ellipsis, +} + /// A render object that displays a paragraph of text class RenderParagraph extends RenderBox { - + /// Creates a paragraph render object. + /// + /// The [text], [overflow], and [softWrap] arguments must not be null. RenderParagraph(TextSpan text, { - TextAlign textAlign - }) : _textPainter = new TextPainter(text: text, textAlign: textAlign) { + TextAlign textAlign, + TextOverflow overflow: TextOverflow.clip, + bool softWrap: true + }) : _softWrap = softWrap, + _overflow = overflow, + _textPainter = new TextPainter(text: text, textAlign: textAlign) { assert(text != null); assert(text.debugAssertValid()); + assert(overflow != null); + assert(softWrap != null); } final TextPainter _textPainter; @@ -27,6 +49,8 @@ class RenderParagraph extends RenderBox { if (_textPainter.text == value) return; _textPainter.text = value; + _overflowPainter = null; + _overflowShader = null; markNeedsLayout(); } @@ -39,10 +63,34 @@ class RenderParagraph extends RenderBox { markNeedsPaint(); } + /// Whether the text should break at soft line breaks. + /// + /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. + bool get softWrap => _softWrap; + bool _softWrap; + void set softWrap(bool value) { + assert(value != null); + if (_softWrap == value) + return; + _softWrap = value; + markNeedsLayout(); + } + + /// How visual overflow should be handled. + TextOverflow get overflow => _overflow; + TextOverflow _overflow; + void set overflow(TextOverflow value) { + assert(value != null); + if (_overflow == value) + return; + _overflow = value; + markNeedsPaint(); + } + void _layoutText(BoxConstraints constraints) { assert(constraints != null); assert(constraints.debugAssertIsValid()); - _textPainter.layout(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); + _textPainter.layout(minWidth: constraints.minWidth, maxWidth: _softWrap ? constraints.maxWidth : double.INFINITY); } @override @@ -95,10 +143,49 @@ class RenderParagraph extends RenderBox { span?.recognizer?.addPointer(event); } + bool _hasVisualOverflow = false; + TextPainter _overflowPainter; + ui.Shader _overflowShader; + @override void performLayout() { _layoutText(constraints); size = constraints.constrain(_textPainter.size); + + final bool didOverflowWidth = size.width < _textPainter.width; + // TODO(abarth): We're only measuring the sizes of the line boxes here. If + // the glyphs draw outside the line boxes, we might think that there isn't + // visual overflow when there actually is visual overflow. This can become + // a problem if we start having horizontal overflow and introduce a clip + // that affects the actual (but undetected) vertical overflow. + _hasVisualOverflow = didOverflowWidth || size.height < _textPainter.height; + if (didOverflowWidth) { + switch (_overflow) { + case TextOverflow.clip: + _overflowPainter = null; + _overflowShader = null; + break; + case TextOverflow.fade: + case TextOverflow.ellipsis: + _overflowPainter ??= new TextPainter( + text: new TextSpan(style: _textPainter.text.style, text: '\u2026') + )..layout(); + final double overflowUnit = _overflowPainter.width; + double fadeEnd = size.width; + if (_overflow == TextOverflow.ellipsis) + fadeEnd -= overflowUnit / 2.0; + final double fadeStart = fadeEnd - _overflowPainter.width; + // TODO(abarth): This shader has an LTR bias. + _overflowShader = new ui.Gradient.linear( + [new Point(fadeStart, 0.0), new Point(fadeEnd, 0.0)], + [const Color(0xFFFFFFFF), const Color(0x00FFFFFF)] + ); + break; + } + } else { + _overflowPainter = null; + _overflowShader = null; + } } @override @@ -114,7 +201,31 @@ class RenderParagraph extends RenderBox { // If you remove this call, make sure that changing the textAlign still // works properly. _layoutText(constraints); - _textPainter.paint(context.canvas, offset); + final Canvas canvas = context.canvas; + if (_hasVisualOverflow) { + final Rect bounds = offset & size; + if (_overflowPainter != null) + canvas.saveLayer(bounds, new Paint()); + else + canvas.save(); + canvas.clipRect(bounds); + } + _textPainter.paint(canvas, offset); + if (_hasVisualOverflow) { + if (_overflowShader != null) { + canvas.translate(offset.dx, offset.dy); + Paint paint = new Paint() + ..transferMode = TransferMode.modulate + ..shader = _overflowShader; + canvas.drawRect(Point.origin & size, paint); + if (_overflow == TextOverflow.ellipsis) { + // TODO(abarth): This paint offset has an LTR bias. + Offset ellipseOffset = new Offset(size.width - _overflowPainter.width, 0.0); + _overflowPainter.paint(canvas, ellipseOffset); + } + } + canvas.restore(); + } } @override diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 530f00b388..26b179862d 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -45,6 +45,7 @@ export 'package:flutter/rendering.dart' show RenderObjectPainter, ShaderCallback, SingleChildLayoutDelegate, + TextOverflow, ValueChanged, ViewportAnchor, ViewportDimensions, @@ -1904,8 +1905,16 @@ class RichText extends LeafRenderObjectWidget { /// Creates a paragraph of rich text. /// /// The [text] argument is required to be non-null. - RichText({ Key key, this.text, this.textAlign }) : super(key: key) { + RichText({ + Key key, + this.text, + this.textAlign, + this.softWrap: true, + this.overflow: TextOverflow.clip + }) : super(key: key) { assert(text != null); + assert(softWrap != null); + assert(overflow != null); } /// The text to display in this widget. @@ -1914,16 +1923,30 @@ class RichText extends LeafRenderObjectWidget { /// How the text should be aligned horizontally. final TextAlign textAlign; + /// Whether the text should break at soft line breaks. + /// + /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. + final bool softWrap; + + /// How visual overflow should be handled. + final TextOverflow overflow; + @override RenderParagraph createRenderObject(BuildContext context) { - return new RenderParagraph(text, textAlign: textAlign); + return new RenderParagraph(text, + textAlign: textAlign, + softWrap: softWrap, + overflow: overflow + ); } @override void updateRenderObject(BuildContext context, RenderParagraph renderObject) { renderObject ..text = text - ..textAlign = textAlign; + ..textAlign = textAlign + ..softWrap = softWrap + ..overflow = overflow; } } @@ -1937,16 +1960,24 @@ class DefaultTextStyle extends InheritedWidget { Key key, this.style, this.textAlign, + this.softWrap: true, + this.overflow: TextOverflow.clip, Widget child }) : super(key: key, child: child) { assert(style != null); + assert(softWrap != null); + assert(overflow != null); assert(child != null); } /// A const-constructible default text style that provides fallback values. /// /// Returned from [of] when the given [BuildContext] doesn't have an enclosing default text style. - const DefaultTextStyle.fallback() : style = const TextStyle(), textAlign = null; + const DefaultTextStyle.fallback() + : style = const TextStyle(), + textAlign = null, + softWrap = true, + overflow = TextOverflow.clip; /// Creates a default text style that inherits from the given [BuildContext]. /// @@ -1959,6 +1990,8 @@ class DefaultTextStyle extends InheritedWidget { BuildContext context, TextStyle style, TextAlign textAlign, + bool softWrap, + TextOverflow overflow, Widget child }) { DefaultTextStyle parent = DefaultTextStyle.of(context); @@ -1966,6 +1999,8 @@ class DefaultTextStyle extends InheritedWidget { key: key, style: parent.style.merge(style), textAlign: textAlign ?? parent.textAlign, + softWrap: softWrap ?? parent.softWrap, + overflow: overflow ?? parent.overflow, child: child ); } @@ -1976,6 +2011,14 @@ class DefaultTextStyle extends InheritedWidget { /// How the text should be aligned horizontally. final TextAlign textAlign; + /// Whether the text should break at soft line breaks. + /// + /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. + final bool softWrap; + + /// How visual overflow should be handled. + final TextOverflow overflow; + /// The closest instance of this class that encloses the given context. /// /// If no such instance exists, returns an instance created by @@ -2019,7 +2062,13 @@ class Text extends StatelessWidget { /// /// If the [style] argument is null, the text will use the style from the /// closest enclosing [DefaultTextStyle]. - Text(this.data, { Key key, this.style, this.textAlign }) : super(key: key) { + Text(this.data, { + Key key, + this.style, + this.textAlign, + this.softWrap, + this.overflow + }) : super(key: key) { assert(data != null); } @@ -2036,21 +2085,24 @@ class Text extends StatelessWidget { /// How the text should be aligned horizontally. final TextAlign textAlign; + /// Whether the text should break at soft line breaks. + /// + /// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space. + final bool softWrap; + + /// How visual overflow should be handled. + final TextOverflow overflow; + @override Widget build(BuildContext context) { - DefaultTextStyle defaultTextStyle; + DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); TextStyle effectiveTextStyle = style; - if (style == null || style.inherit) { - defaultTextStyle ??= DefaultTextStyle.of(context); + if (style == null || style.inherit) effectiveTextStyle = defaultTextStyle.style.merge(style); - } - TextAlign effectiveTextAlign = textAlign; - if (effectiveTextAlign == null) { - defaultTextStyle ??= DefaultTextStyle.of(context); - effectiveTextAlign = defaultTextStyle.textAlign; - } return new RichText( - textAlign: effectiveTextAlign, + textAlign: textAlign ?? defaultTextStyle.textAlign, + softWrap: softWrap ?? defaultTextStyle.softWrap, + overflow: overflow ?? defaultTextStyle.overflow, text: new TextSpan( style: effectiveTextStyle, text: data