1// Copyright 2014 The Flutter 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/foundation.dart';
8import 'package:flutter/rendering.dart';
9
10import 'basic.dart';
11import 'framework.dart';
12
13// Examples can assume:
14// late WidgetSpan myWidgetSpan;
15
16/// An immutable widget that is embedded inline within text.
17///
18/// The [child] property is the widget that will be embedded. Children are
19/// constrained by the width of the paragraph.
20///
21/// The [child] property may contain its own [Widget] children (if applicable),
22/// including [Text] and [RichText] widgets which may include additional
23/// [WidgetSpan]s. Child [Text] and [RichText] widgets will be laid out
24/// independently and occupy a rectangular space in the parent text layout.
25///
26/// [WidgetSpan]s will be ignored when passed into a [TextPainter] directly.
27/// To properly layout and paint the [child] widget, [WidgetSpan] should be
28/// passed into a [Text.rich] widget.
29///
30/// {@tool snippet}
31///
32/// A card with `Hello World!` embedded inline within a TextSpan tree.
33///
34/// ```dart
35/// const Text.rich(
36/// TextSpan(
37/// children: <InlineSpan>[
38/// TextSpan(text: 'Flutter is'),
39/// WidgetSpan(
40/// child: SizedBox(
41/// width: 120,
42/// height: 50,
43/// child: Card(
44/// child: Center(
45/// child: Text('Hello World!')
46/// )
47/// ),
48/// )
49/// ),
50/// TextSpan(text: 'the best!'),
51/// ],
52/// )
53/// )
54/// ```
55/// {@end-tool}
56///
57/// [WidgetSpan] contributes the semantics of the [WidgetSpan.child] to the
58/// semantics tree.
59///
60/// See also:
61///
62/// * [TextSpan], a node that represents text in an [InlineSpan] tree.
63/// * [Text], a widget for showing uniformly-styled text.
64/// * [RichText], a widget for finer control of text rendering.
65/// * [TextPainter], a class for painting [InlineSpan] objects on a [Canvas].
66@immutable
67class WidgetSpan extends PlaceholderSpan {
68 /// Creates a [WidgetSpan] with the given values.
69 ///
70 /// [WidgetSpan] is a leaf node in the [InlineSpan] tree. Child widgets are
71 /// constrained by the width of the paragraph they occupy. Child widget
72 /// heights are unconstrained, and may cause the text to overflow and be
73 /// ellipsized/truncated.
74 ///
75 /// A [TextStyle] may be provided with the [style] property, but only the
76 /// decoration, foreground, background, and spacing options will be used.
77 const WidgetSpan({
78 required this.child,
79 super.alignment,
80 super.baseline,
81 super.style,
82 }) : assert(
83 baseline != null || !(
84 identical(alignment, ui.PlaceholderAlignment.aboveBaseline) ||
85 identical(alignment, ui.PlaceholderAlignment.belowBaseline) ||
86 identical(alignment, ui.PlaceholderAlignment.baseline)
87 ),
88 );
89
90 /// Helper function for extracting [WidgetSpan]s in preorder, from the given
91 /// [InlineSpan] as a list of widgets.
92 ///
93 /// The `textScaler` is the scaling strategy for scaling the content.
94 ///
95 /// This function is used by [EditableText] and [RichText] so calling it
96 /// directly is rarely necessary.
97 static List<Widget> extractFromInlineSpan(InlineSpan span, TextScaler textScaler) {
98 final List<Widget> widgets = <Widget>[];
99 // _kEngineDefaultFontSize is the default font size to use when none of the
100 // ancestor spans specifies one.
101 final List<double> fontSizeStack = <double>[kDefaultFontSize];
102 int index = 0;
103 // This assumes an InlineSpan tree's logical order is equivalent to preorder.
104 bool visitSubtree(InlineSpan span) {
105 final double? fontSizeToPush = switch (span.style?.fontSize) {
106 final double size when size != fontSizeStack.last => size,
107 _ => null,
108 };
109 if (fontSizeToPush != null) {
110 fontSizeStack.add(fontSizeToPush);
111 }
112 if (span is WidgetSpan) {
113 final double fontSize = fontSizeStack.last;
114 final double textScaleFactor = fontSize == 0 ? 0 : textScaler.scale(fontSize) / fontSize;
115 widgets.add(
116 _WidgetSpanParentData(
117 span: span,
118 child: Semantics(
119 tagForChildren: PlaceholderSpanIndexSemanticsTag(index++),
120 child: _AutoScaleInlineWidget(span: span, textScaleFactor: textScaleFactor, child: span.child),
121 ),
122 ),
123 );
124 }
125 assert(
126 span is WidgetSpan || span is! PlaceholderSpan,
127 '$span is a PlaceholderSpan but not a WidgetSpan subclass. This is currently not supported.',
128 );
129 span.visitDirectChildren(visitSubtree);
130 if (fontSizeToPush != null) {
131 final double poppedFontSize = fontSizeStack.removeLast();
132 assert(fontSizeStack.isNotEmpty);
133 assert(poppedFontSize == fontSizeToPush);
134 }
135 return true;
136 }
137 visitSubtree(span);
138 return widgets;
139 }
140
141 /// The widget to embed inline within text.
142 final Widget child;
143
144 /// Adds a placeholder box to the paragraph builder if a size has been
145 /// calculated for the widget.
146 ///
147 /// Sizes are provided through `dimensions`, which should contain a 1:1
148 /// in-order mapping of widget to laid-out dimensions. If no such dimension
149 /// is provided, the widget will be skipped.
150 ///
151 /// The `textScaler` will be applied to the laid-out size of the widget.
152 @override
153 void build(ui.ParagraphBuilder builder, {
154 TextScaler textScaler = TextScaler.noScaling,
155 List<PlaceholderDimensions>? dimensions,
156 }) {
157 assert(debugAssertIsValid());
158 assert(dimensions != null);
159 final bool hasStyle = style != null;
160 if (hasStyle) {
161 builder.pushStyle(style!.getTextStyle(textScaler: textScaler));
162 }
163 assert(builder.placeholderCount < dimensions!.length);
164 final PlaceholderDimensions currentDimensions = dimensions![builder.placeholderCount];
165 builder.addPlaceholder(
166 currentDimensions.size.width,
167 currentDimensions.size.height,
168 alignment,
169 baseline: currentDimensions.baseline,
170 baselineOffset: currentDimensions.baselineOffset,
171 );
172 if (hasStyle) {
173 builder.pop();
174 }
175 }
176
177 /// Calls `visitor` on this [WidgetSpan]. There are no children spans to walk.
178 @override
179 bool visitChildren(InlineSpanVisitor visitor) => visitor(this);
180
181 @override
182 bool visitDirectChildren(InlineSpanVisitor visitor) => true;
183
184 @override
185 InlineSpan? getSpanForPositionVisitor(TextPosition position, Accumulator offset) {
186 if (position.offset == offset.value) {
187 return this;
188 }
189 offset.increment(1);
190 return null;
191 }
192
193 @override
194 int? codeUnitAtVisitor(int index, Accumulator offset) {
195 final int localOffset = index - offset.value;
196 assert(localOffset >= 0);
197 offset.increment(1);
198 return localOffset == 0 ? PlaceholderSpan.placeholderCodeUnit : null;
199 }
200
201 @override
202 RenderComparison compareTo(InlineSpan other) {
203 if (identical(this, other)) {
204 return RenderComparison.identical;
205 }
206 if (other.runtimeType != runtimeType) {
207 return RenderComparison.layout;
208 }
209 if ((style == null) != (other.style == null)) {
210 return RenderComparison.layout;
211 }
212 final WidgetSpan typedOther = other as WidgetSpan;
213 if (child != typedOther.child || alignment != typedOther.alignment) {
214 return RenderComparison.layout;
215 }
216 RenderComparison result = RenderComparison.identical;
217 if (style != null) {
218 final RenderComparison candidate = style!.compareTo(other.style!);
219 if (candidate.index > result.index) {
220 result = candidate;
221 }
222 if (result == RenderComparison.layout) {
223 return result;
224 }
225 }
226 return result;
227 }
228
229 @override
230 bool operator ==(Object other) {
231 if (identical(this, other)) {
232 return true;
233 }
234 if (other.runtimeType != runtimeType) {
235 return false;
236 }
237 if (super != other) {
238 return false;
239 }
240 return other is WidgetSpan
241 && other.child == child
242 && other.alignment == alignment
243 && other.baseline == baseline;
244 }
245
246 @override
247 int get hashCode => Object.hash(super.hashCode, child, alignment, baseline);
248
249 /// Returns the text span that contains the given position in the text.
250 @override
251 InlineSpan? getSpanForPosition(TextPosition position) {
252 assert(debugAssertIsValid());
253 return null;
254 }
255
256 /// In debug mode, throws an exception if the object is not in a
257 /// valid configuration. Otherwise, returns true.
258 ///
259 /// This is intended to be used as follows:
260 ///
261 /// ```dart
262 /// assert(myWidgetSpan.debugAssertIsValid());
263 /// ```
264 @override
265 bool debugAssertIsValid() {
266 // WidgetSpans are always valid as asserts prevent invalid WidgetSpans
267 // from being constructed.
268 return true;
269 }
270
271 @override
272 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
273 super.debugFillProperties(properties);
274 properties.add(DiagnosticsProperty<Widget>('widget', child));
275 }
276}
277
278// A ParentDataWidget that sets TextParentData.span.
279class _WidgetSpanParentData extends ParentDataWidget<TextParentData> {
280 const _WidgetSpanParentData({ required this.span, required super.child });
281
282 final WidgetSpan span;
283
284 @override
285 void applyParentData(RenderObject renderObject) {
286 final TextParentData parentData = renderObject.parentData! as TextParentData;
287 parentData.span = span;
288 }
289
290 @override
291 Type get debugTypicalAncestorWidgetClass => RichText;
292}
293
294// A RenderObjectWidget that automatically applies text scaling on inline
295// widgets.
296//
297// TODO(LongCatIsLooong): this shouldn't happen automatically, at least there
298// should be a way to opt out: https://github.com/flutter/flutter/issues/126962
299class _AutoScaleInlineWidget extends SingleChildRenderObjectWidget {
300 const _AutoScaleInlineWidget({ required this.span, required this.textScaleFactor, required super.child });
301
302 final WidgetSpan span;
303 final double textScaleFactor;
304
305 @override
306 _RenderScaledInlineWidget createRenderObject(BuildContext context) {
307 return _RenderScaledInlineWidget(span.alignment, span.baseline, textScaleFactor);
308 }
309
310 @override
311 void updateRenderObject(BuildContext context, _RenderScaledInlineWidget renderObject) {
312 renderObject
313 ..alignment = span.alignment
314 ..baseline = span.baseline
315 ..scale = textScaleFactor;
316 }
317}
318
319class _RenderScaledInlineWidget extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
320 _RenderScaledInlineWidget(this._alignment, this._baseline, this._scale);
321
322 double get scale => _scale;
323 double _scale;
324 set scale(double value) {
325 if (value == _scale) {
326 return;
327 }
328 assert(value > 0);
329 assert(value.isFinite);
330 _scale = value;
331 markNeedsLayout();
332 }
333
334 ui.PlaceholderAlignment get alignment => _alignment;
335 ui.PlaceholderAlignment _alignment;
336 set alignment(ui.PlaceholderAlignment value) {
337 if (_alignment == value) {
338 return;
339 }
340 _alignment = value;
341 markNeedsLayout();
342 }
343
344 TextBaseline? get baseline => _baseline;
345 TextBaseline? _baseline;
346 set baseline(TextBaseline? value) {
347 if (value == _baseline) {
348 return;
349 }
350 _baseline = value;
351 markNeedsLayout();
352 }
353
354 @override
355 double computeMaxIntrinsicHeight(double width) {
356 return (child?.computeMaxIntrinsicHeight(width / scale) ?? 0.0) * scale;
357 }
358
359 @override
360 double computeMaxIntrinsicWidth(double height) {
361 return (child?.computeMaxIntrinsicWidth(height / scale) ?? 0.0) * scale;
362 }
363
364 @override
365 double computeMinIntrinsicHeight(double width) {
366 return (child?.computeMinIntrinsicHeight(width / scale) ?? 0.0) * scale;
367 }
368
369 @override
370 double computeMinIntrinsicWidth(double height) {
371 return (child?.computeMinIntrinsicWidth(height / scale) ?? 0.0) * scale;
372 }
373
374 @override
375 double? computeDistanceToActualBaseline(TextBaseline baseline) {
376 return switch (child?.getDistanceToActualBaseline(baseline)) {
377 null => super.computeDistanceToActualBaseline(baseline),
378 final double childBaseline => scale * childBaseline,
379 };
380 }
381
382 @override
383 Size computeDryLayout(BoxConstraints constraints) {
384 assert(!constraints.hasBoundedHeight);
385 final Size unscaledSize = child?.computeDryLayout(BoxConstraints(maxWidth: constraints.maxWidth / scale)) ?? Size.zero;
386 return constraints.constrain(unscaledSize * scale);
387 }
388
389 @override
390 void performLayout() {
391 final RenderBox? child = this.child;
392 if (child == null) {
393 return;
394 }
395 assert(!constraints.hasBoundedHeight);
396 // Only constrain the width to the maximum width of the paragraph.
397 // Leave height unconstrained, which will overflow if expanded past.
398 child.layout(BoxConstraints(maxWidth: constraints.maxWidth / scale), parentUsesSize: true);
399 size = constraints.constrain(child.size * scale);
400 }
401
402 @override
403 void applyPaintTransform(RenderBox child, Matrix4 transform) {
404 transform.scale(scale, scale);
405 }
406
407 @override
408 void paint(PaintingContext context, Offset offset) {
409 final RenderBox? child = this.child;
410 if (child == null) {
411 layer = null;
412 return;
413 }
414 if (scale == 1.0) {
415 context.paintChild(child, offset);
416 layer = null;
417 return;
418 }
419 layer = context.pushTransform(
420 needsCompositing,
421 offset,
422 Matrix4.diagonal3Values(scale, scale, 1.0),
423 (PaintingContext context, Offset offset) => context.paintChild(child, offset),
424 oldLayer: layer as TransformLayer?
425 );
426 }
427
428 @override
429 bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
430 final RenderBox? child = this.child;
431 if (child == null) {
432 return false;
433 }
434 return result.addWithPaintTransform(
435 transform: Matrix4.diagonal3Values(scale, scale, 1.0),
436 position: position,
437 hitTest: (BoxHitTestResult result, Offset transformedOffset) => child.hitTest(result, position: transformedOffset),
438 );
439 }
440}
441