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'; |
7 | library; |
8 | |
9 | import 'dart:ui' as ui show ParagraphBuilder, PlaceholderAlignment; |
10 | |
11 | import 'package:flutter/foundation.dart'; |
12 | import 'package:flutter/rendering.dart'; |
13 | |
14 | import 'basic.dart'; |
15 | import '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 |
71 | class 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. |
284 | class _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 |
304 | class _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 | |
328 | class _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 |
Definitions
- WidgetSpan
- WidgetSpan
- extractFromInlineSpan
- visitSubtree
- build
- visitChildren
- visitDirectChildren
- getSpanForPositionVisitor
- codeUnitAtVisitor
- compareTo
- ==
- hashCode
- getSpanForPosition
- debugAssertIsValid
- debugFillProperties
- _WidgetSpanParentData
- _WidgetSpanParentData
- applyParentData
- debugTypicalAncestorWidgetClass
- _AutoScaleInlineWidget
- _AutoScaleInlineWidget
- createRenderObject
- updateRenderObject
- _RenderScaledInlineWidget
- _RenderScaledInlineWidget
- scale
- scale
- alignment
- alignment
- baseline
- baseline
- computeMaxIntrinsicHeight
- computeMaxIntrinsicWidth
- computeMinIntrinsicHeight
- computeMinIntrinsicWidth
- computeDistanceToActualBaseline
- computeDryBaseline
- computeDryLayout
- performLayout
- applyPaintTransform
- paint
Learn more about Flutter for embedded and desktop on industrialflutter.com