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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com