1// Copyright 2015 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'dart:ui' as ui show ParagraphBuilder, PlaceholderAlignment; 6 7import 'package:flutter/painting.dart'; 8 9import 'framework.dart'; 10 11/// An immutable widget that is embedded inline within text. 12/// 13/// The [child] property is the widget that will be embedded. Children are 14/// constrained by the width of the paragraph. 15/// 16/// The [child] property may contain its own [Widget] children (if applicable), 17/// including [Text] and [RichText] widgets which may include additional 18/// [WidgetSpan]s. Child [Text] and [RichText] widgets will be laid out 19/// independently and occupy a rectangular space in the parent text layout. 20/// 21/// [WidgetSpan]s will be ignored when passed into a [TextPainter] directly. 22/// To properly layout and paint the [child] widget, [WidgetSpan] should be 23/// passed into a [Text.rich] widget. 24/// 25/// {@tool sample} 26/// 27/// A card with `Hello World!` embedded inline within a TextSpan tree. 28/// 29/// ```dart 30/// Text.rich( 31/// TextSpan( 32/// children: <InlineSpan>[ 33/// TextSpan(text: 'Flutter is'), 34/// WidgetSpan( 35/// child: SizedBox( 36/// width: 120, 37/// height: 50, 38/// child: Card( 39/// child: Center( 40/// child: Text('Hello World!') 41/// ) 42/// ), 43/// ) 44/// ), 45/// TextSpan(text: 'the best!'), 46/// ], 47/// ) 48/// ) 49/// ``` 50/// {@end-tool} 51/// 52/// [WidgetSpan] contributes the semantics of the [WidgetSpan.child] to the 53/// semantics tree. 54/// 55/// See also: 56/// 57/// * [TextSpan], a node that represents text in an [InlineSpan] tree. 58/// * [Text], a widget for showing uniformly-styled text. 59/// * [RichText], a widget for finer control of text rendering. 60/// * [TextPainter], a class for painting [InlineSpan] objects on a [Canvas]. 61@immutable 62class WidgetSpan extends PlaceholderSpan { 63 /// Creates a [WidgetSpan] with the given values. 64 /// 65 /// The [child] property must be non-null. [WidgetSpan] is a leaf node in 66 /// the [InlineSpan] tree. Child widgets are constrained by the width of the 67 /// paragraph they occupy. Child widget heights are unconstrained, and may 68 /// cause the text to overflow and be ellipsized/truncated. 69 /// 70 /// A [TextStyle] may be provided with the [style] property, but only the 71 /// decoration, foreground, background, and spacing options will be used. 72 const WidgetSpan({ 73 @required this.child, 74 ui.PlaceholderAlignment alignment = ui.PlaceholderAlignment.bottom, 75 TextBaseline baseline, 76 TextStyle style, 77 }) : assert(child != null), 78 assert(baseline != null || !( 79 identical(alignment, ui.PlaceholderAlignment.aboveBaseline) || 80 identical(alignment, ui.PlaceholderAlignment.belowBaseline) || 81 identical(alignment, ui.PlaceholderAlignment.baseline) 82 )), 83 super( 84 alignment: alignment, 85 baseline: baseline, 86 style: style, 87 ); 88 89 /// The widget to embed inline within text. 90 final Widget child; 91 92 /// Adds a placeholder box to the paragraph builder if a size has been 93 /// calculated for the widget. 94 /// 95 /// Sizes are provided through `dimensions`, which should contain a 1:1 96 /// in-order mapping of widget to laid-out dimensions. If no such dimension 97 /// is provided, the widget will be skipped. 98 /// 99 /// The `textScaleFactor` will be applied to the laid-out size of the widget. 100 @override 101 void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, @required List<PlaceholderDimensions> dimensions }) { 102 assert(debugAssertIsValid()); 103 assert(dimensions != null); 104 final bool hasStyle = style != null; 105 if (hasStyle) { 106 builder.pushStyle(style.getTextStyle(textScaleFactor: textScaleFactor)); 107 } 108 assert(builder.placeholderCount < dimensions.length); 109 final PlaceholderDimensions currentDimensions = dimensions[builder.placeholderCount]; 110 builder.addPlaceholder( 111 currentDimensions.size.width, 112 currentDimensions.size.height, 113 alignment, 114 scale: textScaleFactor, 115 baseline: currentDimensions.baseline, 116 baselineOffset: currentDimensions.baselineOffset, 117 ); 118 if (hasStyle) { 119 builder.pop(); 120 } 121 } 122 123 /// Calls `visitor` on this [WidgetSpan]. There are no children spans to walk. 124 @override 125 bool visitChildren(InlineSpanVisitor visitor) { 126 return visitor(this); 127 } 128 129 @override 130 InlineSpan getSpanForPositionVisitor(TextPosition position, Accumulator offset) { 131 return null; 132 } 133 134 @override 135 int codeUnitAtVisitor(int index, Accumulator offset) { 136 return null; 137 } 138 139 @override 140 RenderComparison compareTo(InlineSpan other) { 141 if (identical(this, other)) 142 return RenderComparison.identical; 143 if (other.runtimeType != runtimeType) 144 return RenderComparison.layout; 145 if ((style == null) != (other.style == null)) 146 return RenderComparison.layout; 147 final WidgetSpan typedOther = other; 148 if (child != typedOther.child || alignment != typedOther.alignment) { 149 return RenderComparison.layout; 150 } 151 RenderComparison result = RenderComparison.identical; 152 if (style != null) { 153 final RenderComparison candidate = style.compareTo(other.style); 154 if (candidate.index > result.index) 155 result = candidate; 156 if (result == RenderComparison.layout) 157 return result; 158 } 159 return result; 160 } 161 162 @override 163 bool operator ==(dynamic other) { 164 if (identical(this, other)) 165 return true; 166 if (other.runtimeType != runtimeType) 167 return false; 168 if (super != other) 169 return false; 170 final WidgetSpan typedOther = other; 171 return typedOther.child == child 172 && typedOther.alignment == alignment 173 && typedOther.baseline == baseline; 174 } 175 176 @override 177 int get hashCode => hashValues(super.hashCode, child, alignment, baseline); 178 179 /// Returns the text span that contains the given position in the text. 180 @override 181 InlineSpan getSpanForPosition(TextPosition position) { 182 assert(debugAssertIsValid()); 183 return null; 184 } 185 186 /// In debug mode, throws an exception if the object is not in a 187 /// valid configuration. Otherwise, returns true. 188 /// 189 /// This is intended to be used as follows: 190 /// 191 /// ```dart 192 /// assert(myWidgetSpan.debugAssertIsValid()); 193 /// ``` 194 @override 195 bool debugAssertIsValid() { 196 // WidgetSpans are always valid as asserts prevent invalid WidgetSpans 197 // from being constructed. 198 return true; 199 } 200} 201