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({ |
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. |
283 | class _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 |
303 | class _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 | |
323 | class _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 |
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