diff --git a/examples/lib/stories/rendering/rich_text_example.dart b/examples/lib/stories/rendering/rich_text_example.dart index 12fa21f0b..f91cf8c32 100644 --- a/examples/lib/stories/rendering/rich_text_example.dart +++ b/examples/lib/stories/rendering/rich_text_example.dart @@ -11,7 +11,7 @@ class RichTextExample extends FlameGame { @override Future onLoad() async { - add(MyTextComponent()..position = Vector2.all(100)); + add(MyTextComponent()..position = Vector2(100, 50)); } } @@ -29,8 +29,7 @@ class MyTextComponent extends PositionComponent { borderColor: const Color(0xFF000000), borderWidth: 2.0, ), - paragraphStyle: BlockStyle( - margin: const EdgeInsets.symmetric(vertical: 6), + paragraph: BlockStyle( padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6), background: BackgroundStyle( color: const Color(0xFFFFF0CB), @@ -39,6 +38,7 @@ class MyTextComponent extends PositionComponent { ), ); final document = DocumentNode([ + HeaderNode.simple('1984', level: 1), ParagraphNode.simple( 'Anything could be true. The so-called laws of nature were nonsense.', ), @@ -48,11 +48,16 @@ class MyTextComponent extends PositionComponent { 'out. "If he thinks he floats off the floor, and I simultaneously ' 'think I can see him do it, then the thing happens."', ), - ParagraphNode.simple( - 'Suddenly, like a lump of submerged wreckage breaking the surface of ' - 'water, the thought burst into his mind: "It doesn\'t really happen. ' - 'We imagine it. It is hallucination."', - ), + ParagraphNode.group([ + PlainTextNode( + 'Suddenly, like a lump of submerged wreckage breaking the surface ' + 'of water, the thought burst into his mind: '), + ItalicTextNode.group([ + PlainTextNode('"It doesn\'t really happen. We imagine it. It is '), + BoldTextNode.simple('hallucination'), + PlainTextNode('."'), + ]), + ]), ParagraphNode.simple( 'He pushed the thought under instantly. The fallacy was obvious. It ' 'presupposed that somewhere or other, outside oneself, there was a ' diff --git a/packages/flame/lib/src/components/text_component.dart b/packages/flame/lib/src/components/text_component.dart index 6ace1da6c..3fde23684 100644 --- a/packages/flame/lib/src/components/text_component.dart +++ b/packages/flame/lib/src/components/text_component.dart @@ -46,8 +46,8 @@ class TextComponent extends PositionComponent { if (_textRenderer is FormatterTextRenderer) { _textElement = (_textRenderer as FormatterTextRenderer).formatter.format(_text); - final measurements = _textElement!.lastLine.metrics; - _textElement!.lastLine.translate(0, measurements.ascent); + final measurements = _textElement!.metrics; + _textElement!.translate(0, measurements.ascent); size.setValues(measurements.width, measurements.height); } else { final expectedSize = textRenderer.measureText(_text); diff --git a/packages/flame/lib/src/text/common/line_metrics.dart b/packages/flame/lib/src/text/common/line_metrics.dart index 1d2e6c752..58e68a30f 100644 --- a/packages/flame/lib/src/text/common/line_metrics.dart +++ b/packages/flame/lib/src/text/common/line_metrics.dart @@ -1,8 +1,6 @@ import 'dart:ui'; -import 'package:flame/src/text/common/text_line.dart'; - -/// The [LineMetrics] object contains measurements of a [TextLine]. +/// The [LineMetrics] object contains measurements of a text line. /// /// A line of text can be thought of as surrounded by a box (rect) that outlines /// the boundaries of the text, plus there is a [baseline] inside the box which diff --git a/packages/flame/lib/src/text/common/text_line.dart b/packages/flame/lib/src/text/common/text_line.dart deleted file mode 100644 index 45856b27b..000000000 --- a/packages/flame/lib/src/text/common/text_line.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flame/src/text/common/line_metrics.dart'; -import 'package:flame/src/text/elements/text_element.dart'; - -/// [TextLine] is an abstract class describing a single line (or a fragment of -/// a line) of a laid-out text. -/// -/// More specifically, after any [TextElement] has been laid out, its layout -/// will be described by one or more [TextLine]s. -abstract class TextLine { - /// The dimensions of this line. - LineMetrics get metrics; - - /// Move the text within this [TextLine] by the specified offsets [dx], [dy]. - void translate(double dx, double dy); -} diff --git a/packages/flame/lib/src/text/common/utils.dart b/packages/flame/lib/src/text/common/utils.dart index a869f77c9..8b0c5f6bf 100644 --- a/packages/flame/lib/src/text/common/utils.dart +++ b/packages/flame/lib/src/text/common/utils.dart @@ -1,5 +1,10 @@ import 'dart:math'; +import 'package:flame/src/text/elements/element.dart'; +import 'package:flame/src/text/elements/group_element.dart'; +import 'package:flame/src/text/elements/rect_element.dart'; +import 'package:flame/src/text/elements/rrect_element.dart'; +import 'package:flame/src/text/styles/background_style.dart'; import 'package:meta/meta.dart'; @internal @@ -10,3 +15,51 @@ double collapseMargin(double margin1, double margin2) { return (margin2 < 0) ? min(margin1, margin2) : margin1 + margin2; } } + +@internal +Element? makeBackground(BackgroundStyle? style, double width, double height) { + if (style == null) { + return null; + } + final out = []; + final backgroundPaint = style.backgroundPaint; + final borderPaint = style.borderPaint; + final borders = style.borderWidths; + final radius = style.borderRadius; + + if (backgroundPaint != null) { + if (radius == 0) { + out.add(RectElement(width, height, backgroundPaint)); + } else { + out.add(RRectElement(width, height, radius, backgroundPaint)); + } + } + if (borderPaint != null) { + if (radius == 0) { + out.add( + RectElement( + width - borders.horizontal / 2, + height - borders.vertical / 2, + borderPaint, + )..translate(borders.left / 2, borders.top / 2), + ); + } else { + out.add( + RRectElement( + width - borders.horizontal / 2, + height - borders.vertical / 2, + radius, + borderPaint, + )..translate(borders.left / 2, borders.top / 2), + ); + } + } + if (out.isEmpty) { + return null; + } + if (out.length == 1) { + return out.first; + } else { + return GroupElement(width: width, height: height, children: out); + } +} diff --git a/packages/flame/lib/src/text/elements/block_element.dart b/packages/flame/lib/src/text/elements/block_element.dart index dccd955b5..238d256b8 100644 --- a/packages/flame/lib/src/text/elements/block_element.dart +++ b/packages/flame/lib/src/text/elements/block_element.dart @@ -7,6 +7,7 @@ import 'package:flame/src/text/elements/element.dart'; /// such as `
` or `
`. abstract class BlockElement extends Element { BlockElement(this.width, this.height); - double width; - double height; + + final double width; + final double height; } diff --git a/packages/flame/lib/src/text/inline/debug_text_painter_text_element.dart b/packages/flame/lib/src/text/elements/debug_text_painter_text_element.dart similarity index 90% rename from packages/flame/lib/src/text/inline/debug_text_painter_text_element.dart rename to packages/flame/lib/src/text/elements/debug_text_painter_text_element.dart index 0c8d81183..c6a378aa4 100644 --- a/packages/flame/lib/src/text/inline/debug_text_painter_text_element.dart +++ b/packages/flame/lib/src/text/elements/debug_text_painter_text_element.dart @@ -1,6 +1,6 @@ import 'dart:ui'; -import 'package:flame/src/text/inline/text_painter_text_element.dart'; +import 'package:flame/src/text/elements/text_painter_text_element.dart'; /// Replacement class for [TextPainterTextElement] which draws solid rectangles /// instead of regular text. diff --git a/packages/flame/lib/src/text/elements/element.dart b/packages/flame/lib/src/text/elements/element.dart index d4114a595..3bbfcb405 100644 --- a/packages/flame/lib/src/text/elements/element.dart +++ b/packages/flame/lib/src/text/elements/element.dart @@ -1,15 +1,17 @@ import 'dart:ui'; +import 'package:flame/src/text/styles/style.dart'; -/// An [Element] is a basic building block of a rich-text document. +/// An [Element] is a basic rendering block of a rich-text document. /// /// Elements are concrete and "physical": they are objects that are ready to be /// rendered on a canvas. This property distinguishes them from Nodes (which are -/// structured pieces of text), and from Styles (which are descriptors for how +/// structured pieces of text), and from [Style]s (which are descriptors for how /// arbitrary pieces of text ought to be rendered). /// /// Elements are at the final stage of the text rendering pipeline, they are /// created during the layout step. abstract class Element { + /// Moves the element by ([dx], [dy]) relative to its current location. void translate(double dx, double dy); /// Renders the element on the [canvas], at coordinates determined during the @@ -17,6 +19,6 @@ abstract class Element { /// /// In order to render the element at a different location, consider either /// calling the [translate] method, or applying a translation transform to the - /// canvas beforehand. + /// canvas itself. void render(Canvas canvas); } diff --git a/packages/flame/lib/src/text/elements/group_element.dart b/packages/flame/lib/src/text/elements/group_element.dart index 340d96a8b..1b317f277 100644 --- a/packages/flame/lib/src/text/elements/group_element.dart +++ b/packages/flame/lib/src/text/elements/group_element.dart @@ -1,10 +1,13 @@ -import 'dart:ui'; - import 'package:flame/src/text/elements/block_element.dart'; import 'package:flame/src/text/elements/element.dart'; +import 'package:flutter/rendering.dart' hide TextStyle; class GroupElement extends BlockElement { - GroupElement(super.width, super.height, this.children); + GroupElement({ + required double width, + required double height, + required this.children, + }) : super(width, height); final List children; diff --git a/packages/flame/lib/src/text/elements/group_text_element.dart b/packages/flame/lib/src/text/elements/group_text_element.dart new file mode 100644 index 000000000..1e9c71724 --- /dev/null +++ b/packages/flame/lib/src/text/elements/group_text_element.dart @@ -0,0 +1,48 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:flame/src/text/common/line_metrics.dart'; +import 'package:flame/src/text/elements/text_element.dart'; + +class GroupTextElement extends TextElement { + GroupTextElement(List children) + : assert(children.isNotEmpty, 'The children list cannot be empty'), + _children = children, + _metrics = _computeMetrics(children); + + final List _children; + final LineMetrics _metrics; + + @override + LineMetrics get metrics => _metrics; + + @override + void render(Canvas canvas) { + for (final child in _children) { + child.render(canvas); + } + } + + @override + void translate(double dx, double dy) { + _metrics.translate(dx, dy); + for (final child in _children) { + child.translate(dx, dy); + } + } + + static LineMetrics _computeMetrics(List elements) { + var width = 0.0; + var ascent = 0.0; + var descent = 0.0; + for (final element in elements) { + final metrics = element.metrics; + assert(metrics.left == width); + assert(metrics.baseline == 0); + width += metrics.width; + ascent = max(ascent, metrics.ascent); + descent = max(descent, metrics.descent); + } + return LineMetrics(width: width, ascent: ascent, descent: descent); + } +} diff --git a/packages/flame/lib/src/text/inline/sprite_font_text_element.dart b/packages/flame/lib/src/text/elements/sprite_font_text_element.dart similarity index 83% rename from packages/flame/lib/src/text/inline/sprite_font_text_element.dart rename to packages/flame/lib/src/text/elements/sprite_font_text_element.dart index 5c9d8ef1c..9f77b8323 100644 --- a/packages/flame/lib/src/text/inline/sprite_font_text_element.dart +++ b/packages/flame/lib/src/text/elements/sprite_font_text_element.dart @@ -2,10 +2,9 @@ import 'dart:typed_data'; import 'dart:ui'; import 'package:flame/src/text/common/line_metrics.dart'; -import 'package:flame/src/text/common/text_line.dart'; import 'package:flame/src/text/elements/text_element.dart'; -class SpriteFontTextElement extends TextElement implements TextLine { +class SpriteFontTextElement extends TextElement { SpriteFontTextElement({ required this.source, required this.transforms, @@ -20,9 +19,6 @@ class SpriteFontTextElement extends TextElement implements TextLine { final Paint paint; final LineMetrics _box; - @override - TextLine get lastLine => this; - @override LineMetrics get metrics => _box; diff --git a/packages/flame/lib/src/text/elements/text_element.dart b/packages/flame/lib/src/text/elements/text_element.dart index 7e45cdd4d..376cca937 100644 --- a/packages/flame/lib/src/text/elements/text_element.dart +++ b/packages/flame/lib/src/text/elements/text_element.dart @@ -1,12 +1,9 @@ -import 'package:flame/src/text/common/text_line.dart'; +import 'package:flame/src/text/common/line_metrics.dart'; import 'package:flame/src/text/elements/element.dart'; -/// [TextElement] is the base class describing a span of text that has *inline* -/// placement rules. -/// -/// Concrete implementations of this class must know how to perform own layout -/// (i.e. determine the exact placement and size of each internal piece), and -/// then render on a canvas afterwards. +/// [TextElement] is the base class that represents a single line of text, laid +/// out and prepared for rendering. abstract class TextElement extends Element { - TextLine get lastLine; + /// The dimensions of this line. + LineMetrics get metrics; } diff --git a/packages/flame/lib/src/text/inline/text_painter_text_element.dart b/packages/flame/lib/src/text/elements/text_painter_text_element.dart similarity index 55% rename from packages/flame/lib/src/text/inline/text_painter_text_element.dart rename to packages/flame/lib/src/text/elements/text_painter_text_element.dart index 9aee5bdc4..84df1e84c 100644 --- a/packages/flame/lib/src/text/inline/text_painter_text_element.dart +++ b/packages/flame/lib/src/text/elements/text_painter_text_element.dart @@ -1,29 +1,25 @@ import 'dart:ui'; import 'package:flame/src/text/common/line_metrics.dart'; -import 'package:flame/src/text/common/text_line.dart'; -import 'package:flame/src/text/elements/element.dart'; import 'package:flame/src/text/elements/text_element.dart'; -import 'package:flutter/rendering.dart' show TextBaseline, TextPainter; +import 'package:flutter/rendering.dart' as flutter; -class TextPainterTextElement extends TextElement implements TextLine, Element { +class TextPainterTextElement extends TextElement { TextPainterTextElement(this._textPainter) : _box = LineMetrics( - ascent: _textPainter - .computeDistanceToActualBaseline(TextBaseline.alphabetic), + ascent: _textPainter.computeDistanceToActualBaseline( + flutter.TextBaseline.alphabetic, + ), width: _textPainter.width, height: _textPainter.height, ); - final TextPainter _textPainter; + final flutter.TextPainter _textPainter; final LineMetrics _box; @override LineMetrics get metrics => _box; - @override - TextLine get lastLine => this; - @override void translate(double dx, double dy) => _box.translate(dx, dy); diff --git a/packages/flame/lib/src/text/formatter_text_renderer.dart b/packages/flame/lib/src/text/formatter_text_renderer.dart index 06ddce137..bb0ce4073 100644 --- a/packages/flame/lib/src/text/formatter_text_renderer.dart +++ b/packages/flame/lib/src/text/formatter_text_renderer.dart @@ -12,7 +12,7 @@ class FormatterTextRenderer extends TextRenderer { @override Vector2 measureText(String text) { - final box = formatter.format(text).lastLine.metrics; + final box = formatter.format(text).metrics; return Vector2(box.width, box.height); } @@ -24,8 +24,8 @@ class FormatterTextRenderer extends TextRenderer { Anchor anchor = Anchor.topLeft, }) { final txt = formatter.format(text); - final box = txt.lastLine.metrics; - txt.lastLine.translate( + final box = txt.metrics; + txt.translate( position.x - box.width * anchor.x, position.y - box.height * anchor.y - box.top, ); diff --git a/packages/flame/lib/src/text/formatters/sprite_font_text_formatter.dart b/packages/flame/lib/src/text/formatters/sprite_font_text_formatter.dart index c25c2931c..f88655d2d 100644 --- a/packages/flame/lib/src/text/formatters/sprite_font_text_formatter.dart +++ b/packages/flame/lib/src/text/formatters/sprite_font_text_formatter.dart @@ -5,9 +5,9 @@ import 'package:flame/src/text/common/glyph.dart'; import 'package:flame/src/text/common/glyph_data.dart'; import 'package:flame/src/text/common/line_metrics.dart'; import 'package:flame/src/text/common/sprite_font.dart'; +import 'package:flame/src/text/elements/sprite_font_text_element.dart'; import 'package:flame/src/text/elements/text_element.dart'; import 'package:flame/src/text/formatters/text_formatter.dart'; -import 'package:flame/src/text/inline/sprite_font_text_element.dart'; class SpriteFontTextFormatter extends TextFormatter { @Deprecated('Use SpriteFontTextFormatter.fromFont() instead; this ' diff --git a/packages/flame/lib/src/text/formatters/text_painter_text_formatter.dart b/packages/flame/lib/src/text/formatters/text_painter_text_formatter.dart index 40630d025..9168fe30d 100644 --- a/packages/flame/lib/src/text/formatters/text_painter_text_formatter.dart +++ b/packages/flame/lib/src/text/formatters/text_painter_text_formatter.dart @@ -1,6 +1,6 @@ +import 'package:flame/src/text/elements/debug_text_painter_text_element.dart'; +import 'package:flame/src/text/elements/text_painter_text_element.dart'; import 'package:flame/src/text/formatters/text_formatter.dart'; -import 'package:flame/src/text/inline/debug_text_painter_text_element.dart'; -import 'package:flame/src/text/inline/text_painter_text_element.dart'; import 'package:flutter/rendering.dart'; /// [TextPainterTextFormatter] applies a Flutter [TextStyle] to a string of @@ -16,7 +16,7 @@ class TextPainterTextFormatter extends TextFormatter { this.debugMode = false, }); - final TextStyle style; + final TextStyle style; // NOTE: this is a Flutter TextStyle final TextDirection textDirection; @Deprecated('Use DebugTextFormatter instead. Will be removed in 1.5.0') final bool debugMode; diff --git a/packages/flame/lib/src/text/nodes.dart b/packages/flame/lib/src/text/nodes.dart deleted file mode 100644 index 71e4f9341..000000000 --- a/packages/flame/lib/src/text/nodes.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:flame/src/text/nodes/block_node.dart'; - -class GroupBlockNode extends BlockNode { - GroupBlockNode(this.children); - - final List children; -} - -class BlockquoteNode extends GroupBlockNode { - BlockquoteNode(super.children); -} - -abstract class TextNode {} - -class PlainTextNode extends TextNode { - PlainTextNode(this.text); - - final String text; -} - -class GroupTextNode extends TextNode { - GroupTextNode(this.children); - - final List children; -} - -class BoldTextNode extends GroupTextNode { - BoldTextNode(super.children); -} - -class ItalicTextNode extends GroupTextNode { - ItalicTextNode(super.children); -} - -class StrikethroughTextNode extends GroupTextNode { - StrikethroughTextNode(super.children); -} - -class HighlightedTextNode extends GroupTextNode { - HighlightedTextNode(super.children); -} diff --git a/packages/flame/lib/src/text/nodes/block_node.dart b/packages/flame/lib/src/text/nodes/block_node.dart index 8d16b9e6d..5a1b3d3c5 100644 --- a/packages/flame/lib/src/text/nodes/block_node.dart +++ b/packages/flame/lib/src/text/nodes/block_node.dart @@ -1,55 +1,20 @@ -import 'package:flame/src/text/elements/element.dart'; -import 'package:flame/src/text/elements/group_element.dart'; -import 'package:flame/src/text/elements/rect_element.dart'; -import 'package:flame/src/text/elements/rrect_element.dart'; -import 'package:flame/src/text/styles/background_style.dart'; +import 'package:flame/src/text/elements/block_element.dart'; +import 'package:flame/src/text/styles/block_style.dart'; +import 'package:flame/src/text/styles/document_style.dart'; +import 'package:flame/src/text/styles/flame_text_style.dart'; -/// An abstract base class for all entities with "block" placement rules. +/// [BlockNode] is a base class for all nodes with "block" placement rules; it +/// roughly corresponds to `
` in HTML. +/// +/// A block node should be able to find its style in the root stylesheet, via +/// the method [fillStyles], and then based on that style build the +/// corresponding element in the [format] method. Both of these methods must be +/// implemented by subclasses. abstract class BlockNode { - Element? makeBackground(BackgroundStyle? style, double width, double height) { - if (style == null) { - return null; - } - final out = []; - final backgroundPaint = style.backgroundPaint; - final borderPaint = style.borderPaint; - final borders = style.borderWidths; - final radius = style.borderRadius; + /// The runtime style applied to this node, this will be set by [fillStyles]. + late BlockStyle style; - if (backgroundPaint != null) { - if (radius == 0) { - out.add(RectElement(width, height, backgroundPaint)); - } else { - out.add(RRectElement(width, height, radius, backgroundPaint)); - } - } - if (borderPaint != null) { - if (radius == 0) { - out.add( - RectElement( - width - borders.horizontal / 2, - height - borders.vertical / 2, - borderPaint, - )..translate(borders.left / 2, borders.top / 2), - ); - } else { - out.add( - RRectElement( - width - borders.horizontal / 2, - height - borders.vertical / 2, - radius, - borderPaint, - )..translate(borders.left / 2, borders.top / 2), - ); - } - } - if (out.isEmpty) { - return null; - } - if (out.length == 1) { - return out.first; - } else { - return GroupElement(width, height, out); - } - } + BlockElement format(double availableWidth); + + void fillStyles(DocumentStyle stylesheet, FlameTextStyle parentTextStyle); } diff --git a/packages/flame/lib/src/text/nodes/bold_text_node.dart b/packages/flame/lib/src/text/nodes/bold_text_node.dart new file mode 100644 index 000000000..02bb40204 --- /dev/null +++ b/packages/flame/lib/src/text/nodes/bold_text_node.dart @@ -0,0 +1,29 @@ +import 'dart:ui'; + +import 'package:flame/src/text/nodes/group_text_node.dart'; +import 'package:flame/src/text/nodes/plain_text_node.dart'; +import 'package:flame/src/text/nodes/text_node.dart'; +import 'package:flame/src/text/styles/document_style.dart'; +import 'package:flame/src/text/styles/flame_text_style.dart'; +import 'package:flame/src/text/styles/style.dart'; + +class BoldTextNode extends TextNode { + BoldTextNode(this.child); + + BoldTextNode.simple(String text) : child = PlainTextNode(text); + + BoldTextNode.group(List children) : child = GroupTextNode(children); + + final TextNode child; + + static final defaultStyle = FlameTextStyle(fontWeight: FontWeight.bold); + + @override + void fillStyles(DocumentStyle stylesheet, FlameTextStyle parentTextStyle) { + textStyle = Style.merge(parentTextStyle, stylesheet.boldText)!; + child.fillStyles(stylesheet, textStyle); + } + + @override + TextNodeLayoutBuilder get layoutBuilder => child.layoutBuilder; +} diff --git a/packages/flame/lib/src/text/nodes/column_node.dart b/packages/flame/lib/src/text/nodes/column_node.dart new file mode 100644 index 000000000..d01927b5c --- /dev/null +++ b/packages/flame/lib/src/text/nodes/column_node.dart @@ -0,0 +1,64 @@ +import 'package:flame/src/text/common/utils.dart'; +import 'package:flame/src/text/elements/block_element.dart'; +import 'package:flame/src/text/elements/element.dart'; +import 'package:flame/src/text/elements/group_element.dart'; +import 'package:flame/src/text/nodes/block_node.dart'; +import 'package:flame/src/text/styles/document_style.dart'; +import 'package:flame/src/text/styles/flame_text_style.dart'; +import 'package:meta/meta.dart'; + +/// [ColumnNode] is a block node containing other block nodes arranged as a +/// column. +/// +/// During formatting, produces an element which is as wide as the available +/// width, and tall enough to fit all the available children elements. +abstract class ColumnNode extends BlockNode { + ColumnNode(this.children); + + final List children; + + @override + BlockElement format(double availableWidth) { + final out = []; + final blockWidth = availableWidth; + final padding = style.padding; + + var verticalOffset = 0.0; + var currentMargin = padding.top; + for (final node in children) { + final nodeMargins = node.style.margin; + final marginLeft = collapseMargin(padding.left, nodeMargins.left); + final marginRight = collapseMargin(padding.right, nodeMargins.right); + final nodeAvailableWidth = blockWidth - marginLeft - marginRight; + final nodeElement = node.format(nodeAvailableWidth); + out.add(nodeElement); + + verticalOffset += collapseMargin(currentMargin, nodeMargins.top); + nodeElement.translate(marginLeft, verticalOffset); + verticalOffset += nodeElement.height; + currentMargin = nodeMargins.bottom; + } + // Do not collapse padding if there are no children. + final blockHeight = children.isEmpty + ? padding.vertical + : verticalOffset + collapseMargin(currentMargin, padding.bottom); + final background = + makeBackground(style.background, blockWidth, blockHeight); + if (background != null) { + out.insert(0, background); + } + return GroupElement( + width: blockWidth, + height: blockHeight, + children: out, + ); + } + + @mustCallSuper + @override + void fillStyles(DocumentStyle rootStyle, FlameTextStyle parentTextStyle) { + for (final node in children) { + node.fillStyles(rootStyle, parentTextStyle); + } + } +} diff --git a/packages/flame/lib/src/text/nodes/document_node.dart b/packages/flame/lib/src/text/nodes/document_node.dart index 53b6fc906..c693b8837 100644 --- a/packages/flame/lib/src/text/nodes/document_node.dart +++ b/packages/flame/lib/src/text/nodes/document_node.dart @@ -3,13 +3,14 @@ import 'dart:math'; import 'package:flame/src/text/common/utils.dart'; import 'package:flame/src/text/elements/element.dart'; import 'package:flame/src/text/elements/group_element.dart'; -import 'package:flame/src/text/nodes.dart'; -import 'package:flame/src/text/nodes/paragraph_node.dart'; +import 'package:flame/src/text/nodes/block_node.dart'; import 'package:flame/src/text/styles/document_style.dart'; import 'package:flutter/painting.dart'; -class DocumentNode extends GroupBlockNode { - DocumentNode(super.children); +class DocumentNode { + DocumentNode(this.children); + + final List children; /// Applies [style] to this document, producing an object that can be rendered /// on a canvas. Parameters [width] and [height] serve as the fallback values @@ -21,21 +22,19 @@ class DocumentNode extends GroupBlockNode { 'Width must be either provided explicitly or set in the stylesheet', ); final out = []; - final border = style.backgroundStyle?.borderWidths ?? EdgeInsets.zero; + final border = style.background?.borderWidths ?? EdgeInsets.zero; final padding = style.padding; final pageWidth = style.width ?? width!; - final contentWidth = pageWidth - padding.horizontal - border.horizontal; - final horizontalOffset = padding.left + border.left; + final contentWidth = pageWidth - padding.horizontal; + final horizontalOffset = padding.left; var verticalOffset = border.top; var currentMargin = padding.top; for (final node in children) { - final blockStyle = style.styleFor(node); + node.fillStyles(style, style.text); + final blockStyle = node.style; verticalOffset += collapseMargin(currentMargin, blockStyle.margin.top); - final nodeElement = (node as ParagraphNode).format( - blockStyle, - parentWidth: contentWidth, - ); + final nodeElement = node.format(contentWidth); nodeElement.translate(horizontalOffset, verticalOffset); out.add(nodeElement); currentMargin = blockStyle.margin.bottom; @@ -48,13 +47,13 @@ class DocumentNode extends GroupBlockNode { border.bottom, ); final background = makeBackground( - style.backgroundStyle, + style.background, pageWidth, pageHeight, ); if (background != null) { out.insert(0, background); } - return GroupElement(pageWidth, pageHeight, out); + return GroupElement(width: pageWidth, height: pageHeight, children: out); } } diff --git a/packages/flame/lib/src/text/nodes/group_text_node.dart b/packages/flame/lib/src/text/nodes/group_text_node.dart new file mode 100644 index 000000000..bc8724919 --- /dev/null +++ b/packages/flame/lib/src/text/nodes/group_text_node.dart @@ -0,0 +1,66 @@ +import 'package:flame/src/text/elements/group_text_element.dart'; +import 'package:flame/src/text/elements/text_element.dart'; +import 'package:flame/src/text/nodes/text_node.dart'; +import 'package:flame/src/text/styles/document_style.dart'; +import 'package:flame/src/text/styles/flame_text_style.dart'; + +class GroupTextNode extends TextNode { + GroupTextNode(this.children); + + final List children; + + @override + void fillStyles(DocumentStyle stylesheet, FlameTextStyle parentTextStyle) { + textStyle = parentTextStyle; + for (final node in children) { + node.fillStyles(stylesheet, textStyle); + } + } + + @override + TextNodeLayoutBuilder get layoutBuilder => _GroupTextLayoutBuilder(this); +} + +class _GroupTextLayoutBuilder extends TextNodeLayoutBuilder { + _GroupTextLayoutBuilder(this.node); + + final GroupTextNode node; + int _currentChildIndex = 0; + TextNodeLayoutBuilder? _currentChildBuilder; + + @override + bool get isDone => _currentChildIndex == node.children.length; + + @override + TextElement? layOutNextLine(double availableWidth) { + assert(!isDone); + final out = []; + var usedWidth = 0.0; + while (true) { + if (_currentChildBuilder?.isDone ?? false) { + _currentChildBuilder = null; + _currentChildIndex += 1; + if (_currentChildIndex == node.children.length) { + break; + } + } + _currentChildBuilder ??= node.children[_currentChildIndex].layoutBuilder; + + final maybeLine = + _currentChildBuilder!.layOutNextLine(availableWidth - usedWidth); + if (maybeLine == null) { + break; + } else { + assert(maybeLine.metrics.left == 0 && maybeLine.metrics.baseline == 0); + maybeLine.translate(usedWidth, 0); + out.add(maybeLine); + usedWidth += maybeLine.metrics.width; + } + } + if (out.isEmpty) { + return null; + } else { + return GroupTextElement(out); + } + } +} diff --git a/packages/flame/lib/src/text/nodes/header_node.dart b/packages/flame/lib/src/text/nodes/header_node.dart index eea46f12a..bc9ba59e4 100644 --- a/packages/flame/lib/src/text/nodes/header_node.dart +++ b/packages/flame/lib/src/text/nodes/header_node.dart @@ -1,9 +1,56 @@ -import 'package:flame/src/text/nodes.dart'; -import 'package:flame/src/text/nodes/block_node.dart'; +import 'dart:ui'; -class HeaderNode extends BlockNode { - HeaderNode(this.child, {required this.level}); +import 'package:flame/src/text/nodes/plain_text_node.dart'; +import 'package:flame/src/text/nodes/text_block_node.dart'; +import 'package:flame/src/text/styles/block_style.dart'; +import 'package:flame/src/text/styles/document_style.dart'; +import 'package:flame/src/text/styles/flame_text_style.dart'; +import 'package:flame/src/text/styles/style.dart'; +import 'package:flutter/rendering.dart' show EdgeInsets; + +class HeaderNode extends TextBlockNode { + HeaderNode(super.child, {required this.level}); + + HeaderNode.simple(String text, {required this.level}) + : super(PlainTextNode(text)); - final GroupTextNode child; final int level; + + static BlockStyle defaultStyleH1 = BlockStyle( + text: FlameTextStyle(fontScale: 2.0, fontWeight: FontWeight.bold), + margin: const EdgeInsets.fromLTRB(0, 24, 0, 12), + ); + static BlockStyle defaultStyleH2 = BlockStyle( + text: FlameTextStyle(fontScale: 1.5, fontWeight: FontWeight.bold), + margin: const EdgeInsets.fromLTRB(0, 24, 0, 8), + ); + static BlockStyle defaultStyleH3 = BlockStyle( + text: FlameTextStyle(fontScale: 1.25, fontWeight: FontWeight.bold), + ); + static BlockStyle defaultStyleH4 = BlockStyle( + text: FlameTextStyle(fontScale: 1.0, fontWeight: FontWeight.bold), + ); + static BlockStyle defaultStyleH5 = BlockStyle( + text: FlameTextStyle(fontScale: 0.875, fontWeight: FontWeight.bold), + ); + static BlockStyle defaultStyleH6 = BlockStyle( + text: FlameTextStyle(fontScale: 0.85, fontWeight: FontWeight.bold), + ); + + @override + void fillStyles(DocumentStyle stylesheet, FlameTextStyle parentTextStyle) { + style = level == 1 + ? stylesheet.header1 + : level == 2 + ? stylesheet.header2 + : level == 3 + ? stylesheet.header3 + : level == 4 + ? stylesheet.header4 + : level == 5 + ? stylesheet.header5 + : stylesheet.header6; + final textStyle = Style.merge(parentTextStyle, style.text)!; + super.fillStyles(stylesheet, textStyle); + } } diff --git a/packages/flame/lib/src/text/nodes/italic_text_node.dart b/packages/flame/lib/src/text/nodes/italic_text_node.dart new file mode 100644 index 000000000..d7eb18ab3 --- /dev/null +++ b/packages/flame/lib/src/text/nodes/italic_text_node.dart @@ -0,0 +1,30 @@ +import 'dart:ui'; + +import 'package:flame/src/text/nodes/group_text_node.dart'; +import 'package:flame/src/text/nodes/plain_text_node.dart'; +import 'package:flame/src/text/nodes/text_node.dart'; +import 'package:flame/src/text/styles/document_style.dart'; +import 'package:flame/src/text/styles/flame_text_style.dart'; +import 'package:flame/src/text/styles/style.dart'; + +class ItalicTextNode extends TextNode { + ItalicTextNode(this.child); + + ItalicTextNode.simple(String text) : child = PlainTextNode(text); + + ItalicTextNode.group(List children) + : child = GroupTextNode(children); + + final TextNode child; + + static final defaultStyle = FlameTextStyle(fontStyle: FontStyle.italic); + + @override + void fillStyles(DocumentStyle stylesheet, FlameTextStyle parentTextStyle) { + textStyle = Style.merge(parentTextStyle, stylesheet.italicText)!; + child.fillStyles(stylesheet, textStyle); + } + + @override + TextNodeLayoutBuilder get layoutBuilder => child.layoutBuilder; +} diff --git a/packages/flame/lib/src/text/nodes/paragraph_node.dart b/packages/flame/lib/src/text/nodes/paragraph_node.dart index 9a369fe10..c7ee485cf 100644 --- a/packages/flame/lib/src/text/nodes/paragraph_node.dart +++ b/packages/flame/lib/src/text/nodes/paragraph_node.dart @@ -1,59 +1,29 @@ -import 'package:flame/src/text/elements/block_element.dart'; -import 'package:flame/src/text/elements/group_element.dart'; -import 'package:flame/src/text/formatters/text_painter_text_formatter.dart'; -import 'package:flame/src/text/inline/text_painter_text_element.dart'; -import 'package:flame/src/text/nodes.dart'; -import 'package:flame/src/text/nodes/block_node.dart'; +import 'package:flame/src/text/nodes/group_text_node.dart'; +import 'package:flame/src/text/nodes/plain_text_node.dart'; +import 'package:flame/src/text/nodes/text_block_node.dart'; +import 'package:flame/src/text/nodes/text_node.dart'; import 'package:flame/src/text/styles/block_style.dart'; -import 'package:flutter/painting.dart' as painting; +import 'package:flame/src/text/styles/document_style.dart'; +import 'package:flame/src/text/styles/flame_text_style.dart'; +import 'package:flame/src/text/styles/style.dart'; +import 'package:flutter/rendering.dart' show EdgeInsets; -class ParagraphNode extends BlockNode { - ParagraphNode.simple(String text) - : child = GroupTextNode([PlainTextNode(text)]); +class ParagraphNode extends TextBlockNode { + ParagraphNode(super.child); - final GroupTextNode child; + ParagraphNode.simple(String text) : super(PlainTextNode(text)); - BlockElement format(BlockStyle style, {required double parentWidth}) { - final text = (child.children.first as PlainTextNode).text; - final formatter = TextPainterTextFormatter( - style: const painting.TextStyle(fontSize: 16), - ); - final contentWidth = parentWidth - style.padding.horizontal; - final horizontalOffset = style.padding.left; + ParagraphNode.group(List fragments) + : super(GroupTextNode(fragments)); - final words = text.split(' '); - final lines = []; - var verticalOffset = style.padding.top; - var i0 = 0; - var i1 = 1; - var startNewLine = true; - while (i1 <= words.length) { - final lineText = words.sublist(i0, i1).join(' '); - final formattedLine = formatter.format(lineText); - if (formattedLine.metrics.width <= contentWidth || i1 - i0 == 1) { - formattedLine.translate( - horizontalOffset, - verticalOffset + formattedLine.metrics.ascent, - ); - if (startNewLine) { - lines.add(formattedLine); - startNewLine = false; - } else { - lines[lines.length - 1] = formattedLine; - } - i1++; - } else { - i0 = i1 - 1; - startNewLine = true; - verticalOffset += lines.last.metrics.height; - } - } - if (!startNewLine) { - verticalOffset += lines.last.metrics.height; - } - verticalOffset += style.padding.bottom; - final bg = makeBackground(style.background, parentWidth, verticalOffset); - final elements = bg == null ? lines : [bg, ...lines]; - return GroupElement(parentWidth, verticalOffset, elements); + static const defaultStyle = BlockStyle( + margin: EdgeInsets.all(6), + ); + + @override + void fillStyles(DocumentStyle stylesheet, FlameTextStyle parentTextStyle) { + style = stylesheet.paragraph; + final textStyle = Style.merge(parentTextStyle, style.text)!; + super.fillStyles(stylesheet, textStyle); } } diff --git a/packages/flame/lib/src/text/nodes/plain_text_node.dart b/packages/flame/lib/src/text/nodes/plain_text_node.dart new file mode 100644 index 000000000..47624b9ba --- /dev/null +++ b/packages/flame/lib/src/text/nodes/plain_text_node.dart @@ -0,0 +1,58 @@ +import 'package:flame/src/text/elements/text_element.dart'; +import 'package:flame/src/text/formatters/text_formatter.dart'; +import 'package:flame/src/text/nodes/text_node.dart'; +import 'package:flame/src/text/styles/document_style.dart'; +import 'package:flame/src/text/styles/flame_text_style.dart'; + +class PlainTextNode extends TextNode { + PlainTextNode(this.text); + + final String text; + + @override + void fillStyles(DocumentStyle stylesheet, FlameTextStyle parentTextStyle) { + textStyle = parentTextStyle; + } + + @override + TextNodeLayoutBuilder get layoutBuilder => _PlainTextLayoutBuilder(this); +} + +class _PlainTextLayoutBuilder extends TextNodeLayoutBuilder { + _PlainTextLayoutBuilder(this.node) + : formatter = node.textStyle.asTextFormatter(), + words = node.text.split(' '); + + final PlainTextNode node; + final TextFormatter formatter; + final List words; + int index0 = 0; + int index1 = 1; + + @override + bool get isDone => index1 > words.length; + + @override + TextElement? layOutNextLine(double availableWidth) { + TextElement? tentativeLine; + int? tentativeIndex0; + while (index1 <= words.length) { + final textPiece = words.sublist(index0, index1).join(' '); + final formattedPiece = formatter.format(textPiece); + if (formattedPiece.metrics.width > availableWidth) { + break; + } else { + tentativeLine = formattedPiece; + tentativeIndex0 = index1; + index1 += 1; + } + } + if (tentativeLine != null) { + assert(tentativeIndex0 != 0 && tentativeIndex0! > index0); + index0 = tentativeIndex0!; + return tentativeLine; + } else { + return null; + } + } +} diff --git a/packages/flame/lib/src/text/nodes/text_block_node.dart b/packages/flame/lib/src/text/nodes/text_block_node.dart new file mode 100644 index 000000000..d8661f703 --- /dev/null +++ b/packages/flame/lib/src/text/nodes/text_block_node.dart @@ -0,0 +1,58 @@ +import 'package:flame/src/text/common/utils.dart'; +import 'package:flame/src/text/elements/block_element.dart'; +import 'package:flame/src/text/elements/group_element.dart'; +import 'package:flame/src/text/elements/text_element.dart'; +import 'package:flame/src/text/nodes/block_node.dart'; +import 'package:flame/src/text/nodes/text_node.dart'; +import 'package:flame/src/text/styles/document_style.dart'; +import 'package:flame/src/text/styles/flame_text_style.dart'; +import 'package:meta/meta.dart'; + +abstract class TextBlockNode extends BlockNode { + TextBlockNode(this.child); + + final TextNode child; + + @mustCallSuper + @override + void fillStyles(DocumentStyle stylesheet, FlameTextStyle parentTextStyle) { + child.fillStyles(stylesheet, parentTextStyle); + } + + /// Converts this node into a [BlockElement]. + /// + /// All late variables must be initialized prior to calling this method. + @override + BlockElement format(double availableWidth) { + final layoutBuilder = child.layoutBuilder; + final blockWidth = availableWidth; + final contentWidth = blockWidth - style.padding.horizontal; + + final lines = []; + final horizontalOffset = style.padding.left; + var verticalOffset = style.padding.top; + while (!layoutBuilder.isDone) { + final element = layoutBuilder.layOutNextLine(contentWidth); + if (element == null) { + // Not enough horizontal space to lay out. For now we just stop the + // layout altogether cutting off the remainder of the content. But is + // there a better alternative? + break; + } else { + final metrics = element.metrics; + assert(metrics.left == 0 && metrics.baseline == 0); + element.translate(horizontalOffset, verticalOffset + metrics.ascent); + lines.add(element); + verticalOffset += metrics.height; + } + } + verticalOffset += style.padding.bottom; + final bg = makeBackground(style.background, blockWidth, verticalOffset); + final elements = bg == null ? lines : [bg, ...lines]; + return GroupElement( + width: blockWidth, + height: verticalOffset, + children: elements, + ); + } +} diff --git a/packages/flame/lib/src/text/nodes/text_node.dart b/packages/flame/lib/src/text/nodes/text_node.dart new file mode 100644 index 000000000..dd8595347 --- /dev/null +++ b/packages/flame/lib/src/text/nodes/text_node.dart @@ -0,0 +1,16 @@ +import 'package:flame/src/text/elements/text_element.dart'; +import 'package:flame/src/text/styles/document_style.dart'; +import 'package:flame/src/text/styles/flame_text_style.dart'; + +abstract class TextNode { + late FlameTextStyle textStyle; + + void fillStyles(DocumentStyle stylesheet, FlameTextStyle parentTextStyle); + + TextNodeLayoutBuilder get layoutBuilder; +} + +abstract class TextNodeLayoutBuilder { + TextElement? layOutNextLine(double availableWidth); + bool get isDone; +} diff --git a/packages/flame/lib/src/text/styles/background_style.dart b/packages/flame/lib/src/text/styles/background_style.dart index 85d91165d..f265308a4 100644 --- a/packages/flame/lib/src/text/styles/background_style.dart +++ b/packages/flame/lib/src/text/styles/background_style.dart @@ -1,6 +1,8 @@ import 'package:flame/src/text/styles/style.dart'; import 'package:flutter/rendering.dart'; +import 'package:meta/meta.dart'; +@immutable class BackgroundStyle extends Style { BackgroundStyle({ Color? color, @@ -13,45 +15,28 @@ class BackgroundStyle extends Style { 'Parameters `paint` and `color` are exclusive', ), borderWidths = EdgeInsets.all(borderWidth ?? 0), - borderRadius = borderRadius ?? 0 { - if (paint != null) { - backgroundPaint = paint; - } else if (color != null) { - backgroundPaint = Paint()..color = color; - } else { - backgroundPaint = null; - } - if (borderColor != null) { - borderPaint = Paint() - ..color = borderColor - ..style = PaintingStyle.stroke - ..strokeWidth = borderWidth ?? 0; - } else { - borderPaint = null; - } - } + borderRadius = borderRadius ?? 0, + backgroundPaint = + paint ?? (color != null ? (Paint()..color = color) : null), + borderPaint = borderColor != null + ? (Paint() + ..color = borderColor + ..style = PaintingStyle.stroke + ..strokeWidth = borderWidth ?? 0) + : null; - late final Paint? backgroundPaint; - late final Paint? borderPaint; + final Paint? backgroundPaint; + final Paint? borderPaint; final double borderRadius; final EdgeInsets borderWidths; @override - BackgroundStyle clone() => copyWith(); - - BackgroundStyle copyWith({ - Color? color, - Paint? paint, - Color? borderColor, - double? borderRadius, - double? borderWidth, - }) { + BackgroundStyle copyWith(BackgroundStyle other) { return BackgroundStyle( - color: color ?? (paint == null ? backgroundPaint?.color : null), - paint: paint ?? backgroundPaint, - borderColor: borderColor ?? borderPaint?.color, - borderRadius: borderRadius ?? this.borderRadius, - borderWidth: borderWidth ?? borderWidths.left, + paint: other.backgroundPaint ?? backgroundPaint, + borderColor: other.borderPaint?.color ?? borderPaint?.color, + borderRadius: other.borderRadius, + borderWidth: other.borderWidths.top, ); } } diff --git a/packages/flame/lib/src/text/styles/block_style.dart b/packages/flame/lib/src/text/styles/block_style.dart index 1d73de233..4a93c0c67 100644 --- a/packages/flame/lib/src/text/styles/block_style.dart +++ b/packages/flame/lib/src/text/styles/block_style.dart @@ -1,30 +1,36 @@ import 'package:flame/src/text/styles/background_style.dart'; +import 'package:flame/src/text/styles/flame_text_style.dart'; import 'package:flame/src/text/styles/style.dart'; -import 'package:flutter/painting.dart'; +import 'package:flutter/painting.dart' hide TextStyle; +import 'package:meta/meta.dart'; +/// [BlockStyle] is a generic descriptor for a visual appearance of a block- +/// level element. +@immutable class BlockStyle extends Style { - BlockStyle({ - this.margin = EdgeInsets.zero, - this.padding = EdgeInsets.zero, - this.background, - }); - - EdgeInsets margin; - EdgeInsets padding; - BackgroundStyle? background; - - @override - BlockStyle clone() => copyWith(); - - BlockStyle copyWith({ + const BlockStyle({ EdgeInsets? margin, EdgeInsets? padding, - BackgroundStyle? background, - }) { + this.background, + this.text, + }) : _margin = margin, + _padding = padding; + + final EdgeInsets? _margin; + final EdgeInsets? _padding; + final BackgroundStyle? background; + final FlameTextStyle? text; + + EdgeInsets get margin => _margin ?? EdgeInsets.zero; + EdgeInsets get padding => _padding ?? EdgeInsets.zero; + + @override + BlockStyle copyWith(BlockStyle other) { return BlockStyle( - margin: margin ?? this.margin, - padding: padding ?? this.padding, - background: background ?? this.background, + margin: other._margin ?? _margin, + padding: other._padding ?? _padding, + background: other.background ?? background, + text: Style.merge(text, other.text), ); } } diff --git a/packages/flame/lib/src/text/styles/document_style.dart b/packages/flame/lib/src/text/styles/document_style.dart index c97c34b68..e1d4138db 100644 --- a/packages/flame/lib/src/text/styles/document_style.dart +++ b/packages/flame/lib/src/text/styles/document_style.dart @@ -1,12 +1,13 @@ -import 'package:flame/src/text/nodes/block_node.dart'; +import 'package:flame/src/text/nodes/bold_text_node.dart'; import 'package:flame/src/text/nodes/header_node.dart'; +import 'package:flame/src/text/nodes/italic_text_node.dart'; import 'package:flame/src/text/nodes/paragraph_node.dart'; import 'package:flame/src/text/styles/background_style.dart'; import 'package:flame/src/text/styles/block_style.dart'; +import 'package:flame/src/text/styles/flame_text_style.dart'; import 'package:flame/src/text/styles/overflow.dart'; import 'package:flame/src/text/styles/style.dart'; import 'package:flutter/painting.dart' show EdgeInsets; -import 'package:meta/meta.dart'; /// [DocumentStyle] is a user-facing description of how to render an entire /// body of text; it roughly corresponds to a stylesheet in HTML. @@ -21,25 +22,39 @@ class DocumentStyle extends Style { DocumentStyle({ this.width, this.height, - EdgeInsets? padding, - BackgroundStyle? background, - BlockStyle? paragraphStyle, - BlockStyle? header1Style, - BlockStyle? header2Style, - BlockStyle? header3Style, - BlockStyle? header4Style, - BlockStyle? header5Style, - BlockStyle? header6Style, - }) : padding = padding ?? EdgeInsets.zero { - backgroundStyle = acquire(background); - this.paragraphStyle = acquire(paragraphStyle ?? defaultParagraphStyle)!; - this.header1Style = acquire(header1Style ?? defaultHeader1Style)!; - this.header2Style = acquire(header2Style ?? defaultHeader2Style)!; - this.header3Style = acquire(header3Style ?? defaultHeader3Style)!; - this.header4Style = acquire(header4Style ?? defaultHeader4Style)!; - this.header5Style = acquire(header5Style ?? defaultHeader5Style)!; - this.header6Style = acquire(header6Style ?? defaultHeader6Style)!; - } + this.padding = EdgeInsets.zero, + this.background, + FlameTextStyle? text, + FlameTextStyle? boldText, + FlameTextStyle? italicText, + BlockStyle? paragraph, + BlockStyle? header1, + BlockStyle? header2, + BlockStyle? header3, + BlockStyle? header4, + BlockStyle? header5, + BlockStyle? header6, + }) : _text = Style.merge(text, DocumentStyle.defaultTextStyle), + _boldText = Style.merge(boldText, BoldTextNode.defaultStyle), + _italicText = Style.merge(italicText, ItalicTextNode.defaultStyle), + _paragraph = Style.merge(paragraph, ParagraphNode.defaultStyle), + _header1 = Style.merge(header1, HeaderNode.defaultStyleH1), + _header2 = Style.merge(header2, HeaderNode.defaultStyleH2), + _header3 = Style.merge(header3, HeaderNode.defaultStyleH3), + _header4 = Style.merge(header4, HeaderNode.defaultStyleH4), + _header5 = Style.merge(header5, HeaderNode.defaultStyleH5), + _header6 = Style.merge(header6, HeaderNode.defaultStyleH6); + + final FlameTextStyle? _text; + final FlameTextStyle? _boldText; + final FlameTextStyle? _italicText; + final BlockStyle? _paragraph; + final BlockStyle? _header1; + final BlockStyle? _header2; + final BlockStyle? _header3; + final BlockStyle? _header4; + final BlockStyle? _header5; + final BlockStyle? _header6; /// Outer width of the document page. /// @@ -65,7 +80,7 @@ class DocumentStyle extends Style { /// Behavior of the document when the amount of content that needs to be laid /// out exceeds the provided [height]. See the [Overflow] enum description for /// more details. - final Overflow overflow = Overflow.expand; + Overflow get overflow => Overflow.expand; /// The distance from the outer edges of the page to the inner content box of /// the document. @@ -79,89 +94,51 @@ class DocumentStyle extends Style { /// If present, describes what kind of background and borders to draw for the /// document page(s). - late final BackgroundStyle? backgroundStyle; + final BackgroundStyle? background; - /// Style for paragraph nodes in the document. - late final BlockStyle paragraphStyle; + FlameTextStyle get text => _text!; + FlameTextStyle get boldText => _boldText!; + FlameTextStyle get italicText => _italicText!; - /// Style for level-1 headers. - late final BlockStyle header1Style; + /// Style for [ParagraphNode]s. + BlockStyle get paragraph => _paragraph!; - /// Style for level-2 headers. - late final BlockStyle header2Style; + /// Styles for [HeaderNode]s, levels 1 to 6. + BlockStyle get header1 => _header1!; + BlockStyle get header2 => _header2!; + BlockStyle get header3 => _header3!; + BlockStyle get header4 => _header4!; + BlockStyle get header5 => _header5!; + BlockStyle get header6 => _header6!; - /// Style for level-3 headers. - late final BlockStyle header3Style; - - /// Style for level-4 headers. - late final BlockStyle header4Style; - - /// Style for level-5 headers. - late final BlockStyle header5Style; - - /// Style for level-6 headers. - late final BlockStyle header6Style; - - static BlockStyle defaultParagraphStyle = BlockStyle(); - static BlockStyle defaultHeader1Style = BlockStyle(); - static BlockStyle defaultHeader2Style = BlockStyle(); - static BlockStyle defaultHeader3Style = BlockStyle(); - static BlockStyle defaultHeader4Style = BlockStyle(); - static BlockStyle defaultHeader5Style = BlockStyle(); - static BlockStyle defaultHeader6Style = BlockStyle(); + static FlameTextStyle defaultTextStyle = FlameTextStyle(fontSize: 16.0); @override - DocumentStyle clone() => copyWith(); - - DocumentStyle copyWith({ - double? width, - double? height, - EdgeInsets? padding, - BackgroundStyle? background, - BlockStyle? paragraphStyle, - BlockStyle? header1Style, - BlockStyle? header2Style, - BlockStyle? header3Style, - BlockStyle? header4Style, - BlockStyle? header5Style, - BlockStyle? header6Style, - }) { + DocumentStyle copyWith(DocumentStyle other) { return DocumentStyle( - width: width ?? this.width, - height: height ?? this.height, - padding: padding ?? this.padding, - background: background ?? backgroundStyle, - paragraphStyle: paragraphStyle ?? this.paragraphStyle, - header1Style: header1Style ?? this.header1Style, - header2Style: header2Style ?? this.header2Style, - header3Style: header3Style ?? this.header3Style, - header4Style: header4Style ?? this.header4Style, - header5Style: header5Style ?? this.header5Style, - header6Style: header6Style ?? this.header6Style, + width: other.width ?? width, + height: other.height ?? height, + padding: other.padding, + background: merge(other.background, background)! as BackgroundStyle, + paragraph: merge(other.paragraph, paragraph)! as BlockStyle, + header1: merge(other.header1, header1)! as BlockStyle, + header2: merge(other.header2, header2)! as BlockStyle, + header3: merge(other.header3, header3)! as BlockStyle, + header4: merge(other.header4, header4)! as BlockStyle, + header5: merge(other.header5, header5)! as BlockStyle, + header6: merge(other.header6, header6)! as BlockStyle, ); } - @internal - BlockStyle styleFor(BlockNode node) { - if (node is ParagraphNode) { - return paragraphStyle; + final Map> _mergedStylesCache = {}; + Style? merge(Style? style1, Style? style2) { + if (style1 == null) { + return style2; + } else if (style2 == null) { + return style1; + } else { + return (_mergedStylesCache[style1] ??= {})[style2] ??= + style1.copyWith(style2); } - if (node is HeaderNode) { - switch (node.level) { - case 1: - return header1Style; - case 2: - return header2Style; - case 3: - return header3Style; - case 4: - return header4Style; - case 5: - return header5Style; - default: - return header6Style; - } - } - return BlockStyle(); } } diff --git a/packages/flame/lib/src/text/styles/flame_text_style.dart b/packages/flame/lib/src/text/styles/flame_text_style.dart new file mode 100644 index 000000000..1da932bc5 --- /dev/null +++ b/packages/flame/lib/src/text/styles/flame_text_style.dart @@ -0,0 +1,55 @@ +import 'package:flame/src/text/formatters/text_formatter.dart'; +import 'package:flame/src/text/formatters/text_painter_text_formatter.dart'; +import 'package:flame/src/text/styles/style.dart'; +import 'package:flutter/rendering.dart'; +import 'package:meta/meta.dart'; + +@immutable +class FlameTextStyle extends Style { + FlameTextStyle({ + this.color, + this.fontFamily, + this.fontSize, + this.fontScale, + this.fontWeight, + this.fontStyle, + this.letterSpacing, + }); + + final Color? color; + final String? fontFamily; + final double? fontSize; + final double? fontScale; + final FontWeight? fontWeight; + final FontStyle? fontStyle; + final double? letterSpacing; + + late final TextFormatter formatter = asTextFormatter(); + + @override + FlameTextStyle copyWith(FlameTextStyle other) { + return FlameTextStyle( + color: color ?? other.color, + fontFamily: fontFamily ?? other.fontFamily, + fontSize: fontSize ?? other.fontSize, + fontScale: fontScale ?? other.fontScale, + fontWeight: fontWeight ?? other.fontWeight, + fontStyle: fontStyle ?? other.fontStyle, + letterSpacing: letterSpacing ?? other.letterSpacing, + ); + } + + @internal + TextPainterTextFormatter asTextFormatter() { + return TextPainterTextFormatter( + style: TextStyle( + color: color, + fontFamily: fontFamily, + fontSize: fontSize! * (fontScale ?? 1.0), + fontWeight: fontWeight, + fontStyle: fontStyle, + letterSpacing: letterSpacing, + ), + ); + } +} diff --git a/packages/flame/lib/src/text/styles/style.dart b/packages/flame/lib/src/text/styles/style.dart index 94d33fa41..698ad7a9f 100644 --- a/packages/flame/lib/src/text/styles/style.dart +++ b/packages/flame/lib/src/text/styles/style.dart @@ -1,5 +1,4 @@ import 'package:flame/src/text/styles/document_style.dart'; -import 'package:meta/meta.dart'; /// A [Style] is a base class for several classes that collectively describe /// the desired visual appearance of a "rich-text" document. @@ -14,27 +13,18 @@ import 'package:meta/meta.dart'; /// /// The tree of [Style]s is roughly equivalent to a CSS stylesheet. abstract class Style { - /// The owner of the current style. - /// - /// Usually, styles are organized into a tree, and this property allows - /// traversing up this tree. This property can be null when the style hasn't - /// been put into a tree yet, or when it is the root of the tree. - Style? get parent => _parent; - Style? _parent; + const Style(); - /// Creates and returns a copy of the current object. - Style clone(); + Style copyWith(covariant Style other); - /// Marks [style] as being owned by the current object and returns it. - /// However, if the [style] is already owned by some other object, then clones - /// the [style], marks the copy as being owned, and returns it. - @protected - S? acquire(S? style) { - if (style == null) { - return null; + static T? merge(T? style1, T? style2) { + if (style1 == null) { + return style2; + } else if (style2 == null) { + return style1; + } else { + assert(style1.runtimeType == style2.runtimeType); + return style1.copyWith(style2) as T; } - final useStyle = style._parent == null ? style : style.clone() as S; - useStyle._parent = this; - return useStyle; } } diff --git a/packages/flame/lib/text.dart b/packages/flame/lib/text.dart index 5ee588b3c..1f2496fc0 100644 --- a/packages/flame/lib/text.dart +++ b/packages/flame/lib/text.dart @@ -3,21 +3,36 @@ export 'src/text/common/glyph.dart' show Glyph; export 'src/text/common/glyph_data.dart' show GlyphData; export 'src/text/common/line_metrics.dart' show LineMetrics; export 'src/text/common/sprite_font.dart' show SpriteFont; -export 'src/text/common/text_line.dart' show TextLine; +export 'src/text/elements/block_element.dart' show BlockElement; export 'src/text/elements/element.dart' show Element; +export 'src/text/elements/rect_element.dart' show RectElement; +export 'src/text/elements/rrect_element.dart' show RRectElement; +export 'src/text/elements/sprite_font_text_element.dart' + show SpriteFontTextElement; export 'src/text/elements/text_element.dart' show TextElement; +export 'src/text/elements/text_painter_text_element.dart' + show TextPainterTextElement; export 'src/text/formatters/sprite_font_text_formatter.dart' show SpriteFontTextFormatter; export 'src/text/formatters/text_formatter.dart' show TextFormatter; export 'src/text/formatters/text_painter_text_formatter.dart' show TextPainterTextFormatter; export 'src/text/nodes/block_node.dart' show BlockNode; +export 'src/text/nodes/bold_text_node.dart' show BoldTextNode; +export 'src/text/nodes/column_node.dart' show ColumnNode; export 'src/text/nodes/document_node.dart' show DocumentNode; +export 'src/text/nodes/group_text_node.dart' show GroupTextNode; export 'src/text/nodes/header_node.dart' show HeaderNode; +export 'src/text/nodes/italic_text_node.dart' show ItalicTextNode; export 'src/text/nodes/paragraph_node.dart' show ParagraphNode; +export 'src/text/nodes/plain_text_node.dart' show PlainTextNode; +export 'src/text/nodes/text_block_node.dart' show TextBlockNode; +export 'src/text/nodes/text_node.dart' show TextNode; export 'src/text/sprite_font_renderer.dart' show SpriteFontRenderer; export 'src/text/styles/background_style.dart' show BackgroundStyle; export 'src/text/styles/block_style.dart' show BlockStyle; export 'src/text/styles/document_style.dart' show DocumentStyle; +export 'src/text/styles/flame_text_style.dart' show FlameTextStyle; +export 'src/text/styles/style.dart' show Style; export 'src/text/text_paint.dart' show TextPaint; export 'src/text/text_renderer.dart' show TextRenderer; diff --git a/packages/flame/test/text/text_paint_test.dart b/packages/flame/test/text/text_paint_test.dart index ba1930c68..51b97317e 100644 --- a/packages/flame/test/text/text_paint_test.dart +++ b/packages/flame/test/text/text_paint_test.dart @@ -1,12 +1,11 @@ -import 'package:flame/components.dart'; import 'package:flame/text.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart' as flutter; import 'package:test/test.dart'; void main() { group('TextPaint', () { test('copyWith returns a new instance with the new values', () { - const style = TextStyle(fontSize: 12, fontFamily: 'Times'); + const style = flutter.TextStyle(fontSize: 12, fontFamily: 'Times'); final tp = TextPaint(style: style) .copyWith((t) => t.copyWith(fontFamily: 'Helvetica')); expect(tp.style.fontSize, 12); diff --git a/packages/flame_test/lib/src/debug_text_formatter.dart b/packages/flame_test/lib/src/debug_text_formatter.dart index 626231e0c..5838ea408 100644 --- a/packages/flame_test/lib/src/debug_text_formatter.dart +++ b/packages/flame_test/lib/src/debug_text_formatter.dart @@ -27,7 +27,7 @@ class DebugTextFormatter extends TextFormatter { TextElement format(String text) => _DebugTextElement(this, text); } -class _DebugTextElement extends TextElement implements TextLine { +class _DebugTextElement extends TextElement { _DebugTextElement(this.style, this.text) { final charWidth = style.fontSize * 1.0; final charHeight = style.fontSize; @@ -52,9 +52,6 @@ class _DebugTextElement extends TextElement implements TextLine { @override late final LineMetrics metrics; - @override - TextLine get lastLine => this; - @override void render(Canvas canvas) { canvas.save();