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({required this.child, super.alignment, super.baseline, 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 /// Helper function for extracting [WidgetSpan]s in preorder, from the given
90 /// [InlineSpan] as a list of widgets.
91 ///
92 /// The `textScaler` is the scaling strategy for scaling the content.
93 ///
94 /// This function is used by [EditableText] and [RichText] so calling it
95 /// directly is rarely necessary.
96 static List<Widget> extractFromInlineSpan(InlineSpan span, TextScaler textScaler) {
97 final List<Widget> widgets = <Widget>[];
98 // _kEngineDefaultFontSize is the default font size to use when none of the
99 // ancestor spans specifies one.
100 final List<double> fontSizeStack = <double>[kDefaultFontSize];
101 int index = 0;
102 // This assumes an InlineSpan tree's logical order is equivalent to preorder.
103 bool visitSubtree(InlineSpan span) {
104 final double? fontSizeToPush = switch (span.style?.fontSize) {
105 final double size when size != fontSizeStack.last => size,
106 _ => null,
107 };
108 if (fontSizeToPush != null) {
109 fontSizeStack.add(fontSizeToPush);
110 }
111 if (span is WidgetSpan) {
112 final double fontSize = fontSizeStack.last;
113 final double textScaleFactor = fontSize == 0 ? 0 : textScaler.scale(fontSize) / fontSize;
114 widgets.add(
115 _WidgetSpanParentData(
116 span: span,
117 child: Semantics(
118 tagForChildren: PlaceholderSpanIndexSemanticsTag(index++),
119 child: _AutoScaleInlineWidget(
120 span: span,
121 textScaleFactor: textScaleFactor,
122 child: span.child,
123 ),
124 ),
125 ),
126 );
127 }
128 assert(
129 span is WidgetSpan || span is! PlaceholderSpan,
130 '$span is a PlaceholderSpan but not a WidgetSpan subclass. This is currently not supported.',
131 );
132 span.visitDirectChildren(visitSubtree);
133 if (fontSizeToPush != null) {
134 final double poppedFontSize = fontSizeStack.removeLast();
135 assert(fontSizeStack.isNotEmpty);
136 assert(poppedFontSize == fontSizeToPush);
137 }
138 return true;
139 }
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(
158 ui.ParagraphBuilder builder, {
159 TextScaler textScaler = TextScaler.noScaling,
160 List<PlaceholderDimensions>? dimensions,
161 }) {
162 assert(debugAssertIsValid());
163 assert(dimensions != null);
164 final bool hasStyle = style != null;
165 if (hasStyle) {
166 builder.pushStyle(style!.getTextStyle(textScaler: textScaler));
167 }
168 assert(builder.placeholderCount < dimensions!.length);
169 final PlaceholderDimensions currentDimensions = dimensions![builder.placeholderCount];
170 builder.addPlaceholder(
171 currentDimensions.size.width,
172 currentDimensions.size.height,
173 alignment,
174 baseline: currentDimensions.baseline,
175 baselineOffset: currentDimensions.baselineOffset,
176 );
177 if (hasStyle) {
178 builder.pop();
179 }
180 }
181
182 /// Calls `visitor` on this [WidgetSpan]. There are no children spans to walk.
183 @override
184 bool visitChildren(InlineSpanVisitor visitor) => visitor(this);
185
186 @override
187 bool visitDirectChildren(InlineSpanVisitor visitor) => true;
188
189 @override
190 InlineSpan? getSpanForPositionVisitor(TextPosition position, Accumulator offset) {
191 if (position.offset == offset.value) {
192 return this;
193 }
194 offset.increment(1);
195 return null;
196 }
197
198 @override
199 int? codeUnitAtVisitor(int index, Accumulator offset) {
200 final int localOffset = index - offset.value;
201 assert(localOffset >= 0);
202 offset.increment(1);
203 return localOffset == 0 ? PlaceholderSpan.placeholderCodeUnit : null;
204 }
205
206 @override
207 RenderComparison compareTo(InlineSpan other) {
208 if (identical(this, other)) {
209 return RenderComparison.identical;
210 }
211 if (other.runtimeType != runtimeType) {
212 return RenderComparison.layout;
213 }
214 if ((style == null) != (other.style == null)) {
215 return RenderComparison.layout;
216 }
217 final WidgetSpan typedOther = other as WidgetSpan;
218 if (child != typedOther.child || alignment != typedOther.alignment) {
219 return RenderComparison.layout;
220 }
221 RenderComparison result = RenderComparison.identical;
222 if (style != null) {
223 final RenderComparison candidate = style!.compareTo(other.style!);
224 if (candidate.index > result.index) {
225 result = candidate;
226 }
227 if (result == RenderComparison.layout) {
228 return result;
229 }
230 }
231 return result;
232 }
233
234 @override
235 bool operator ==(Object other) {
236 if (identical(this, other)) {
237 return true;
238 }
239 if (other.runtimeType != runtimeType) {
240 return false;
241 }
242 if (super != other) {
243 return false;
244 }
245 return other is WidgetSpan &&
246 other.child == child &&
247 other.alignment == alignment &&
248 other.baseline == baseline;
249 }
250
251 @override
252 int get hashCode => Object.hash(super.hashCode, child, alignment, baseline);
253
254 /// Returns the text span that contains the given position in the text.
255 @override
256 InlineSpan? getSpanForPosition(TextPosition position) {
257 assert(debugAssertIsValid());
258 return null;
259 }
260
261 /// In debug mode, throws an exception if the object is not in a
262 /// valid configuration. Otherwise, returns true.
263 ///
264 /// This is intended to be used as follows:
265 ///
266 /// ```dart
267 /// assert(myWidgetSpan.debugAssertIsValid());
268 /// ```
269 @override
270 bool debugAssertIsValid() {
271 // WidgetSpans are always valid as asserts prevent invalid WidgetSpans
272 // from being constructed.
273 return true;
274 }
275
276 @override
277 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
278 super.debugFillProperties(properties);
279 properties.add(DiagnosticsProperty<Widget>('widget', child));
280 }
281}
282
283// A ParentDataWidget that sets TextParentData.span.
284class _WidgetSpanParentData extends ParentDataWidget<TextParentData> {
285 const _WidgetSpanParentData({required this.span, required super.child});
286
287 final WidgetSpan span;
288
289 @override
290 void applyParentData(RenderObject renderObject) {
291 final TextParentData parentData = renderObject.parentData! as TextParentData;
292 parentData.span = span;
293 }
294
295 @override
296 Type get debugTypicalAncestorWidgetClass => RichText;
297}
298
299// A RenderObjectWidget that automatically applies text scaling on inline
300// widgets.
301//
302// TODO(LongCatIsLooong): this shouldn't happen automatically, at least there
303// should be a way to opt out: https://github.com/flutter/flutter/issues/126962
304class _AutoScaleInlineWidget extends SingleChildRenderObjectWidget {
305 const _AutoScaleInlineWidget({
306 required this.span,
307 required this.textScaleFactor,
308 required super.child,
309 });
310
311 final WidgetSpan span;
312 final double textScaleFactor;
313
314 @override
315 _RenderScaledInlineWidget createRenderObject(BuildContext context) {
316 return _RenderScaledInlineWidget(span.alignment, span.baseline, textScaleFactor);
317 }
318
319 @override
320 void updateRenderObject(BuildContext context, _RenderScaledInlineWidget renderObject) {
321 renderObject
322 ..alignment = span.alignment
323 ..baseline = span.baseline
324 ..scale = textScaleFactor;
325 }
326}
327
328class _RenderScaledInlineWidget extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
329 _RenderScaledInlineWidget(this._alignment, this._baseline, this._scale);
330
331 double get scale => _scale;
332 double _scale;
333 set scale(double value) {
334 if (value == _scale) {
335 return;
336 }
337 assert(value > 0);
338 assert(value.isFinite);
339 _scale = value;
340 markNeedsLayout();
341 }
342
343 ui.PlaceholderAlignment get alignment => _alignment;
344 ui.PlaceholderAlignment _alignment;
345 set alignment(ui.PlaceholderAlignment value) {
346 if (_alignment == value) {
347 return;
348 }
349 _alignment = value;
350 markNeedsLayout();
351 }
352
353 TextBaseline? get baseline => _baseline;
354 TextBaseline? _baseline;
355 set baseline(TextBaseline? value) {
356 if (value == _baseline) {
357 return;
358 }
359 _baseline = value;
360 markNeedsLayout();
361 }
362
363 @override
364 double computeMaxIntrinsicHeight(double width) {
365 return (child?.getMaxIntrinsicHeight(width / scale) ?? 0.0) * scale;
366 }
367
368 @override
369 double computeMaxIntrinsicWidth(double height) {
370 return (child?.getMaxIntrinsicWidth(height / scale) ?? 0.0) * scale;
371 }
372
373 @override
374 double computeMinIntrinsicHeight(double width) {
375 return (child?.getMinIntrinsicHeight(width / scale) ?? 0.0) * scale;
376 }
377
378 @override
379 double computeMinIntrinsicWidth(double height) {
380 return (child?.getMinIntrinsicWidth(height / scale) ?? 0.0) * scale;
381 }
382
383 @override
384 double? computeDistanceToActualBaseline(TextBaseline baseline) {
385 return switch (child?.getDistanceToActualBaseline(baseline)) {
386 null => super.computeDistanceToActualBaseline(baseline),
387 final double childBaseline => scale * childBaseline,
388 };
389 }
390
391 @override
392 double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) {
393 final double? distance = child?.getDryBaseline(
394 BoxConstraints(maxWidth: constraints.maxWidth / scale),
395 baseline,
396 );
397 return distance == null ? null : scale * distance;
398 }
399
400 @override
401 Size computeDryLayout(BoxConstraints constraints) {
402 assert(!constraints.hasBoundedHeight);
403 final Size unscaledSize =
404 child?.getDryLayout(BoxConstraints(maxWidth: constraints.maxWidth / scale)) ?? Size.zero;
405 return constraints.constrain(unscaledSize * scale);
406 }
407
408 @override
409 void performLayout() {
410 final RenderBox? child = this.child;
411 if (child == null) {
412 return;
413 }
414 assert(!constraints.hasBoundedHeight);
415 // Only constrain the width to the maximum width of the paragraph.
416 // Leave height unconstrained, which will overflow if expanded past.
417 child.layout(BoxConstraints(maxWidth: constraints.maxWidth / scale), parentUsesSize: true);
418 size = constraints.constrain(child.size * scale);
419 }
420
421 @override
422 void applyPaintTransform(RenderBox child, Matrix4 transform) {
423 transform.scale(scale, scale);
424 }
425
426 @override
427 void paint(PaintingContext context, Offset offset) {
428 final RenderBox? child = this.child;
429 if (child == null) {
430 layer = null;
431 return;
432 }
433 if (scale == 1.0) {
434 context.paintChild(child, offset);
435 layer = null;
436 return;
437 }
438 layer = context.pushTransform(
439 needsCompositing,
440 offset,
441 Matrix4.diagonal3Values(scale, scale, 1.0),
442 (PaintingContext context, Offset offset) => context.paintChild(child, offset),
443 oldLayer: layer as TransformLayer?,
444 );
445 }
446
447 @override
448 bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
449 final RenderBox? child = this.child;
450 if (child == null) {
451 return false;
452 }
453 return result.addWithPaintTransform(
454 transform: Matrix4.diagonal3Values(scale, scale, 1.0),
455 position: position,
456 hitTest:
457 (BoxHitTestResult result, Offset transformedOffset) =>
458 child.hitTest(result, position: transformedOffset),
459 );
460 }
461}
462

Provided by KDAB

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