| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:ui' as ui show ParagraphBuilder, PlaceholderAlignment; |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/rendering.dart'; |
| |
| import 'basic.dart'; |
| import 'framework.dart'; |
| |
| const double _kEngineDefaultFontSize = 14.0; |
| |
| // Examples can assume: |
| // late WidgetSpan myWidgetSpan; |
| |
| /// An immutable widget that is embedded inline within text. |
| /// |
| /// The [child] property is the widget that will be embedded. Children are |
| /// constrained by the width of the paragraph. |
| /// |
| /// The [child] property may contain its own [Widget] children (if applicable), |
| /// including [Text] and [RichText] widgets which may include additional |
| /// [WidgetSpan]s. Child [Text] and [RichText] widgets will be laid out |
| /// independently and occupy a rectangular space in the parent text layout. |
| /// |
| /// [WidgetSpan]s will be ignored when passed into a [TextPainter] directly. |
| /// To properly layout and paint the [child] widget, [WidgetSpan] should be |
| /// passed into a [Text.rich] widget. |
| /// |
| /// {@tool snippet} |
| /// |
| /// A card with `Hello World!` embedded inline within a TextSpan tree. |
| /// |
| /// ```dart |
| /// const Text.rich( |
| /// TextSpan( |
| /// children: <InlineSpan>[ |
| /// TextSpan(text: 'Flutter is'), |
| /// WidgetSpan( |
| /// child: SizedBox( |
| /// width: 120, |
| /// height: 50, |
| /// child: Card( |
| /// child: Center( |
| /// child: Text('Hello World!') |
| /// ) |
| /// ), |
| /// ) |
| /// ), |
| /// TextSpan(text: 'the best!'), |
| /// ], |
| /// ) |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| /// [WidgetSpan] contributes the semantics of the [WidgetSpan.child] to the |
| /// semantics tree. |
| /// |
| /// See also: |
| /// |
| /// * [TextSpan], a node that represents text in an [InlineSpan] tree. |
| /// * [Text], a widget for showing uniformly-styled text. |
| /// * [RichText], a widget for finer control of text rendering. |
| /// * [TextPainter], a class for painting [InlineSpan] objects on a [Canvas]. |
| @immutable |
| class WidgetSpan extends PlaceholderSpan { |
| /// Creates a [WidgetSpan] with the given values. |
| /// |
| /// The [child] property must be non-null. [WidgetSpan] is a leaf node in |
| /// the [InlineSpan] tree. Child widgets are constrained by the width of the |
| /// paragraph they occupy. Child widget heights are unconstrained, and may |
| /// cause the text to overflow and be ellipsized/truncated. |
| /// |
| /// A [TextStyle] may be provided with the [style] property, but only the |
| /// decoration, foreground, background, and spacing options will be used. |
| const WidgetSpan({ |
| required this.child, |
| super.alignment, |
| super.baseline, |
| super.style, |
| }) : assert( |
| baseline != null || !( |
| identical(alignment, ui.PlaceholderAlignment.aboveBaseline) || |
| identical(alignment, ui.PlaceholderAlignment.belowBaseline) || |
| identical(alignment, ui.PlaceholderAlignment.baseline) |
| ), |
| ); |
| |
| /// Helper function for extracting [WidgetSpan]s in preorder, from the given |
| /// [InlineSpan] as a list of widgets. |
| /// |
| /// The `textScaler` is the scaling strategy for scaling the content. |
| /// |
| /// This function is used by [EditableText] and [RichText] so calling it |
| /// directly is rarely necessary. |
| static List<Widget> extractFromInlineSpan(InlineSpan span, TextScaler textScaler) { |
| final List<Widget> widgets = <Widget>[]; |
| // _kEngineDefaultFontSize is the default font size to use when none of the |
| // ancestor spans specifies one. |
| final List<double> fontSizeStack = <double>[_kEngineDefaultFontSize]; |
| int index = 0; |
| // This assumes an InlineSpan tree's logical order is equivalent to preorder. |
| bool visitSubtree(InlineSpan span) { |
| final double? fontSizeToPush = switch (span.style?.fontSize) { |
| final double size when size != fontSizeStack.last => size, |
| _ => null, |
| }; |
| if (fontSizeToPush != null) { |
| fontSizeStack.add(fontSizeToPush); |
| } |
| if (span is WidgetSpan) { |
| final double fontSize = fontSizeStack.last; |
| final double textScaleFactor = fontSize == 0 ? 0 : textScaler.scale(fontSize) / fontSize; |
| widgets.add( |
| _WidgetSpanParentData( |
| span: span, |
| child: Semantics( |
| tagForChildren: PlaceholderSpanIndexSemanticsTag(index++), |
| child: _AutoScaleInlineWidget(span: span, textScaleFactor: textScaleFactor, child: span.child), |
| ), |
| ), |
| ); |
| } |
| assert( |
| span is WidgetSpan || span is! PlaceholderSpan, |
| '$span is a PlaceholderSpan but not a WidgetSpan subclass. This is currently not supported.', |
| ); |
| span.visitDirectChildren(visitSubtree); |
| if (fontSizeToPush != null) { |
| final double poppedFontSize = fontSizeStack.removeLast(); |
| assert(fontSizeStack.isNotEmpty); |
| assert(poppedFontSize == fontSizeToPush); |
| } |
| return true; |
| } |
| visitSubtree(span); |
| return widgets; |
| } |
| |
| /// The widget to embed inline within text. |
| final Widget child; |
| |
| /// Adds a placeholder box to the paragraph builder if a size has been |
| /// calculated for the widget. |
| /// |
| /// Sizes are provided through `dimensions`, which should contain a 1:1 |
| /// in-order mapping of widget to laid-out dimensions. If no such dimension |
| /// is provided, the widget will be skipped. |
| /// |
| /// The `textScaler` will be applied to the laid-out size of the widget. |
| @override |
| void build(ui.ParagraphBuilder builder, { |
| TextScaler textScaler = TextScaler.noScaling, |
| List<PlaceholderDimensions>? dimensions, |
| }) { |
| assert(debugAssertIsValid()); |
| assert(dimensions != null); |
| final bool hasStyle = style != null; |
| if (hasStyle) { |
| builder.pushStyle(style!.getTextStyle(textScaler: textScaler)); |
| } |
| assert(builder.placeholderCount < dimensions!.length); |
| final PlaceholderDimensions currentDimensions = dimensions![builder.placeholderCount]; |
| builder.addPlaceholder( |
| currentDimensions.size.width, |
| currentDimensions.size.height, |
| alignment, |
| baseline: currentDimensions.baseline, |
| baselineOffset: currentDimensions.baselineOffset, |
| ); |
| if (hasStyle) { |
| builder.pop(); |
| } |
| } |
| |
| /// Calls `visitor` on this [WidgetSpan]. There are no children spans to walk. |
| @override |
| bool visitChildren(InlineSpanVisitor visitor) => visitor(this); |
| |
| @override |
| bool visitDirectChildren(InlineSpanVisitor visitor) => true; |
| |
| @override |
| InlineSpan? getSpanForPositionVisitor(TextPosition position, Accumulator offset) { |
| if (position.offset == offset.value) { |
| return this; |
| } |
| offset.increment(1); |
| return null; |
| } |
| |
| @override |
| int? codeUnitAtVisitor(int index, Accumulator offset) { |
| final int localOffset = index - offset.value; |
| assert(localOffset >= 0); |
| offset.increment(1); |
| return localOffset == 0 ? PlaceholderSpan.placeholderCodeUnit : null; |
| } |
| |
| @override |
| RenderComparison compareTo(InlineSpan other) { |
| if (identical(this, other)) { |
| return RenderComparison.identical; |
| } |
| if (other.runtimeType != runtimeType) { |
| return RenderComparison.layout; |
| } |
| if ((style == null) != (other.style == null)) { |
| return RenderComparison.layout; |
| } |
| final WidgetSpan typedOther = other as WidgetSpan; |
| if (child != typedOther.child || alignment != typedOther.alignment) { |
| return RenderComparison.layout; |
| } |
| RenderComparison result = RenderComparison.identical; |
| if (style != null) { |
| final RenderComparison candidate = style!.compareTo(other.style!); |
| if (candidate.index > result.index) { |
| result = candidate; |
| } |
| if (result == RenderComparison.layout) { |
| return result; |
| } |
| } |
| return result; |
| } |
| |
| @override |
| bool operator ==(Object other) { |
| if (identical(this, other)) { |
| return true; |
| } |
| if (other.runtimeType != runtimeType) { |
| return false; |
| } |
| if (super != other) { |
| return false; |
| } |
| return other is WidgetSpan |
| && other.child == child |
| && other.alignment == alignment |
| && other.baseline == baseline; |
| } |
| |
| @override |
| int get hashCode => Object.hash(super.hashCode, child, alignment, baseline); |
| |
| /// Returns the text span that contains the given position in the text. |
| @override |
| InlineSpan? getSpanForPosition(TextPosition position) { |
| assert(debugAssertIsValid()); |
| return null; |
| } |
| |
| /// In debug mode, throws an exception if the object is not in a |
| /// valid configuration. Otherwise, returns true. |
| /// |
| /// This is intended to be used as follows: |
| /// |
| /// ```dart |
| /// assert(myWidgetSpan.debugAssertIsValid()); |
| /// ``` |
| @override |
| bool debugAssertIsValid() { |
| // WidgetSpans are always valid as asserts prevent invalid WidgetSpans |
| // from being constructed. |
| return true; |
| } |
| |
| @override |
| void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| super.debugFillProperties(properties); |
| properties.add(DiagnosticsProperty<Widget>('widget', child)); |
| } |
| } |
| |
| // A ParentDataWidget that sets TextParentData.span. |
| class _WidgetSpanParentData extends ParentDataWidget<TextParentData> { |
| const _WidgetSpanParentData({ required this.span, required super.child }); |
| |
| final WidgetSpan span; |
| |
| @override |
| void applyParentData(RenderObject renderObject) { |
| final TextParentData parentData = renderObject.parentData! as TextParentData; |
| parentData.span = span; |
| } |
| |
| @override |
| Type get debugTypicalAncestorWidgetClass => RichText; |
| } |
| |
| // A RenderObjectWidget that automatically applies text scaling on inline |
| // widgets. |
| // |
| // TODO(LongCatIsLooong): this shouldn't happen automatically, at least there |
| // should be a way to opt out: https://github.com/flutter/flutter/issues/126962 |
| class _AutoScaleInlineWidget extends SingleChildRenderObjectWidget { |
| const _AutoScaleInlineWidget({ required this.span, required this.textScaleFactor, required super.child }); |
| |
| final WidgetSpan span; |
| final double textScaleFactor; |
| |
| @override |
| _RenderScaledInlineWidget createRenderObject(BuildContext context) { |
| return _RenderScaledInlineWidget(span.alignment, span.baseline, textScaleFactor); |
| } |
| |
| @override |
| void updateRenderObject(BuildContext context, _RenderScaledInlineWidget renderObject) { |
| renderObject |
| ..alignment = span.alignment |
| ..baseline = span.baseline |
| ..scale = textScaleFactor; |
| } |
| } |
| |
| class _RenderScaledInlineWidget extends RenderBox with RenderObjectWithChildMixin<RenderBox> { |
| _RenderScaledInlineWidget(this._alignment, this._baseline, this._scale); |
| |
| double get scale => _scale; |
| double _scale; |
| set scale(double value) { |
| if (value == _scale) { |
| return; |
| } |
| assert(value > 0); |
| assert(value.isFinite); |
| _scale = value; |
| markNeedsLayout(); |
| } |
| |
| ui.PlaceholderAlignment get alignment => _alignment; |
| ui.PlaceholderAlignment _alignment; |
| set alignment(ui.PlaceholderAlignment value) { |
| if (_alignment == value) { |
| return; |
| } |
| _alignment = value; |
| markNeedsLayout(); |
| } |
| |
| TextBaseline? get baseline => _baseline; |
| TextBaseline? _baseline; |
| set baseline(TextBaseline? value) { |
| if (value == _baseline) { |
| return; |
| } |
| _baseline = value; |
| markNeedsLayout(); |
| } |
| |
| @override |
| double computeMaxIntrinsicHeight(double width) { |
| return (child?.computeMaxIntrinsicHeight(width / scale) ?? 0.0) * scale; |
| } |
| |
| @override |
| double computeMaxIntrinsicWidth(double height) { |
| return (child?.computeMaxIntrinsicWidth(height / scale) ?? 0.0) * scale; |
| } |
| |
| @override |
| double computeMinIntrinsicHeight(double width) { |
| return (child?.computeMinIntrinsicHeight(width / scale) ?? 0.0) * scale; |
| } |
| |
| @override |
| double computeMinIntrinsicWidth(double height) { |
| return (child?.computeMinIntrinsicWidth(height / scale) ?? 0.0) * scale; |
| } |
| |
| @override |
| double? computeDistanceToActualBaseline(TextBaseline baseline) { |
| return switch (child?.getDistanceToActualBaseline(baseline)) { |
| null => super.computeDistanceToActualBaseline(baseline), |
| final double childBaseline => scale * childBaseline, |
| }; |
| } |
| |
| @override |
| Size computeDryLayout(BoxConstraints constraints) { |
| assert(!constraints.hasBoundedHeight); |
| final Size unscaledSize = child?.computeDryLayout(BoxConstraints(maxWidth: constraints.maxWidth / scale)) ?? Size.zero; |
| return constraints.constrain(unscaledSize * scale); |
| } |
| |
| @override |
| void performLayout() { |
| final RenderBox? child = this.child; |
| if (child == null) { |
| return; |
| } |
| assert(!constraints.hasBoundedHeight); |
| // Only constrain the width to the maximum width of the paragraph. |
| // Leave height unconstrained, which will overflow if expanded past. |
| child.layout(BoxConstraints(maxWidth: constraints.maxWidth / scale), parentUsesSize: true); |
| size = constraints.constrain(child.size * scale); |
| } |
| |
| @override |
| void applyPaintTransform(RenderBox child, Matrix4 transform) { |
| transform.scale(scale, scale); |
| } |
| |
| @override |
| void paint(PaintingContext context, Offset offset) { |
| final RenderBox? child = this.child; |
| if (child == null) { |
| layer = null; |
| return; |
| } |
| if (scale == 1.0) { |
| context.paintChild(child, offset); |
| layer = null; |
| return; |
| } |
| layer = context.pushTransform( |
| needsCompositing, |
| offset, |
| Matrix4.diagonal3Values(scale, scale, 1.0), |
| (PaintingContext context, Offset offset) => context.paintChild(child, offset), |
| oldLayer: layer as TransformLayer? |
| ); |
| } |
| |
| @override |
| bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { |
| final RenderBox? child = this.child; |
| if (child == null) { |
| return false; |
| } |
| return result.addWithPaintTransform( |
| transform: Matrix4.diagonal3Values(scale, scale, 1.0), |
| position: position, |
| hitTest: (BoxHitTestResult result, Offset transformedOffset) => child.hitTest(result, position: transformedOffset), |
| ); |
| } |
| } |