feat: Add support for styles propagating through the text node tree (#1915)

This PR continues the work for enabling rich text support within Flame.
Here I add support for different text fragments having different TextStyles, and allow those styles to be inheritable within the text node tree.
This commit is contained in:
Pasha Stetsenko
2022-10-08 04:08:18 -07:00
committed by GitHub
parent dfb94d1a34
commit b5780d4212
36 changed files with 775 additions and 406 deletions

View File

@ -11,7 +11,7 @@ class RichTextExample extends FlameGame {
@override @override
Future<void> onLoad() async { Future<void> 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), borderColor: const Color(0xFF000000),
borderWidth: 2.0, borderWidth: 2.0,
), ),
paragraphStyle: BlockStyle( paragraph: BlockStyle(
margin: const EdgeInsets.symmetric(vertical: 6),
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6), padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6),
background: BackgroundStyle( background: BackgroundStyle(
color: const Color(0xFFFFF0CB), color: const Color(0xFFFFF0CB),
@ -39,6 +38,7 @@ class MyTextComponent extends PositionComponent {
), ),
); );
final document = DocumentNode([ final document = DocumentNode([
HeaderNode.simple('1984', level: 1),
ParagraphNode.simple( ParagraphNode.simple(
'Anything could be true. The so-called laws of nature were nonsense.', '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 ' 'out. "If he thinks he floats off the floor, and I simultaneously '
'think I can see him do it, then the thing happens."', 'think I can see him do it, then the thing happens."',
), ),
ParagraphNode.simple( ParagraphNode.group([
'Suddenly, like a lump of submerged wreckage breaking the surface of ' PlainTextNode(
'water, the thought burst into his mind: "It doesn\'t really happen. ' 'Suddenly, like a lump of submerged wreckage breaking the surface '
'We imagine it. It is hallucination."', '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( ParagraphNode.simple(
'He pushed the thought under instantly. The fallacy was obvious. It ' 'He pushed the thought under instantly. The fallacy was obvious. It '
'presupposed that somewhere or other, outside oneself, there was a ' 'presupposed that somewhere or other, outside oneself, there was a '

View File

@ -46,8 +46,8 @@ class TextComponent<T extends TextRenderer> extends PositionComponent {
if (_textRenderer is FormatterTextRenderer) { if (_textRenderer is FormatterTextRenderer) {
_textElement = _textElement =
(_textRenderer as FormatterTextRenderer).formatter.format(_text); (_textRenderer as FormatterTextRenderer).formatter.format(_text);
final measurements = _textElement!.lastLine.metrics; final measurements = _textElement!.metrics;
_textElement!.lastLine.translate(0, measurements.ascent); _textElement!.translate(0, measurements.ascent);
size.setValues(measurements.width, measurements.height); size.setValues(measurements.width, measurements.height);
} else { } else {
final expectedSize = textRenderer.measureText(_text); final expectedSize = textRenderer.measureText(_text);

View File

@ -1,8 +1,6 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flame/src/text/common/text_line.dart'; /// The [LineMetrics] object contains measurements of a text line.
/// The [LineMetrics] object contains measurements of a [TextLine].
/// ///
/// A line of text can be thought of as surrounded by a box (rect) that outlines /// 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 /// the boundaries of the text, plus there is a [baseline] inside the box which

View File

@ -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);
}

View File

@ -1,5 +1,10 @@
import 'dart:math'; 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'; import 'package:meta/meta.dart';
@internal @internal
@ -10,3 +15,51 @@ double collapseMargin(double margin1, double margin2) {
return (margin2 < 0) ? min(margin1, margin2) : margin1 + 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 = <Element>[];
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);
}
}

View File

@ -7,6 +7,7 @@ import 'package:flame/src/text/elements/element.dart';
/// such as `<div>` or `<blockquote>`. /// such as `<div>` or `<blockquote>`.
abstract class BlockElement extends Element { abstract class BlockElement extends Element {
BlockElement(this.width, this.height); BlockElement(this.width, this.height);
double width;
double height; final double width;
final double height;
} }

View File

@ -1,6 +1,6 @@
import 'dart:ui'; 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 /// Replacement class for [TextPainterTextElement] which draws solid rectangles
/// instead of regular text. /// instead of regular text.

View File

@ -1,15 +1,17 @@
import 'dart:ui'; 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 /// 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 /// 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). /// arbitrary pieces of text ought to be rendered).
/// ///
/// Elements are at the final stage of the text rendering pipeline, they are /// Elements are at the final stage of the text rendering pipeline, they are
/// created during the layout step. /// created during the layout step.
abstract class Element { abstract class Element {
/// Moves the element by ([dx], [dy]) relative to its current location.
void translate(double dx, double dy); void translate(double dx, double dy);
/// Renders the element on the [canvas], at coordinates determined during the /// 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 /// In order to render the element at a different location, consider either
/// calling the [translate] method, or applying a translation transform to the /// calling the [translate] method, or applying a translation transform to the
/// canvas beforehand. /// canvas itself.
void render(Canvas canvas); void render(Canvas canvas);
} }

View File

@ -1,10 +1,13 @@
import 'dart:ui';
import 'package:flame/src/text/elements/block_element.dart'; import 'package:flame/src/text/elements/block_element.dart';
import 'package:flame/src/text/elements/element.dart'; import 'package:flame/src/text/elements/element.dart';
import 'package:flutter/rendering.dart' hide TextStyle;
class GroupElement extends BlockElement { 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<Element> children; final List<Element> children;

View File

@ -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<TextElement> children)
: assert(children.isNotEmpty, 'The children list cannot be empty'),
_children = children,
_metrics = _computeMetrics(children);
final List<TextElement> _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<TextElement> 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);
}
}

View File

@ -2,10 +2,9 @@ import 'dart:typed_data';
import 'dart:ui'; import 'dart:ui';
import 'package:flame/src/text/common/line_metrics.dart'; 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'; import 'package:flame/src/text/elements/text_element.dart';
class SpriteFontTextElement extends TextElement implements TextLine { class SpriteFontTextElement extends TextElement {
SpriteFontTextElement({ SpriteFontTextElement({
required this.source, required this.source,
required this.transforms, required this.transforms,
@ -20,9 +19,6 @@ class SpriteFontTextElement extends TextElement implements TextLine {
final Paint paint; final Paint paint;
final LineMetrics _box; final LineMetrics _box;
@override
TextLine get lastLine => this;
@override @override
LineMetrics get metrics => _box; LineMetrics get metrics => _box;

View File

@ -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'; import 'package:flame/src/text/elements/element.dart';
/// [TextElement] is the base class describing a span of text that has *inline* /// [TextElement] is the base class that represents a single line of text, laid
/// placement rules. /// out and prepared for rendering.
///
/// 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.
abstract class TextElement extends Element { abstract class TextElement extends Element {
TextLine get lastLine; /// The dimensions of this line.
LineMetrics get metrics;
} }

View File

@ -1,29 +1,25 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flame/src/text/common/line_metrics.dart'; 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: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) TextPainterTextElement(this._textPainter)
: _box = LineMetrics( : _box = LineMetrics(
ascent: _textPainter ascent: _textPainter.computeDistanceToActualBaseline(
.computeDistanceToActualBaseline(TextBaseline.alphabetic), flutter.TextBaseline.alphabetic,
),
width: _textPainter.width, width: _textPainter.width,
height: _textPainter.height, height: _textPainter.height,
); );
final TextPainter _textPainter; final flutter.TextPainter _textPainter;
final LineMetrics _box; final LineMetrics _box;
@override @override
LineMetrics get metrics => _box; LineMetrics get metrics => _box;
@override
TextLine get lastLine => this;
@override @override
void translate(double dx, double dy) => _box.translate(dx, dy); void translate(double dx, double dy) => _box.translate(dx, dy);

View File

@ -12,7 +12,7 @@ class FormatterTextRenderer<T extends TextFormatter> extends TextRenderer {
@override @override
Vector2 measureText(String text) { Vector2 measureText(String text) {
final box = formatter.format(text).lastLine.metrics; final box = formatter.format(text).metrics;
return Vector2(box.width, box.height); return Vector2(box.width, box.height);
} }
@ -24,8 +24,8 @@ class FormatterTextRenderer<T extends TextFormatter> extends TextRenderer {
Anchor anchor = Anchor.topLeft, Anchor anchor = Anchor.topLeft,
}) { }) {
final txt = formatter.format(text); final txt = formatter.format(text);
final box = txt.lastLine.metrics; final box = txt.metrics;
txt.lastLine.translate( txt.translate(
position.x - box.width * anchor.x, position.x - box.width * anchor.x,
position.y - box.height * anchor.y - box.top, position.y - box.height * anchor.y - box.top,
); );

View File

@ -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/glyph_data.dart';
import 'package:flame/src/text/common/line_metrics.dart'; import 'package:flame/src/text/common/line_metrics.dart';
import 'package:flame/src/text/common/sprite_font.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/elements/text_element.dart';
import 'package:flame/src/text/formatters/text_formatter.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 { class SpriteFontTextFormatter extends TextFormatter {
@Deprecated('Use SpriteFontTextFormatter.fromFont() instead; this ' @Deprecated('Use SpriteFontTextFormatter.fromFont() instead; this '

View File

@ -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/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'; import 'package:flutter/rendering.dart';
/// [TextPainterTextFormatter] applies a Flutter [TextStyle] to a string of /// [TextPainterTextFormatter] applies a Flutter [TextStyle] to a string of
@ -16,7 +16,7 @@ class TextPainterTextFormatter extends TextFormatter {
this.debugMode = false, this.debugMode = false,
}); });
final TextStyle style; final TextStyle style; // NOTE: this is a Flutter TextStyle
final TextDirection textDirection; final TextDirection textDirection;
@Deprecated('Use DebugTextFormatter instead. Will be removed in 1.5.0') @Deprecated('Use DebugTextFormatter instead. Will be removed in 1.5.0')
final bool debugMode; final bool debugMode;

View File

@ -1,41 +0,0 @@
import 'package:flame/src/text/nodes/block_node.dart';
class GroupBlockNode extends BlockNode {
GroupBlockNode(this.children);
final List<BlockNode> 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<TextNode> 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);
}

View File

@ -1,55 +1,20 @@
import 'package:flame/src/text/elements/element.dart'; import 'package:flame/src/text/elements/block_element.dart';
import 'package:flame/src/text/elements/group_element.dart'; import 'package:flame/src/text/styles/block_style.dart';
import 'package:flame/src/text/elements/rect_element.dart'; import 'package:flame/src/text/styles/document_style.dart';
import 'package:flame/src/text/elements/rrect_element.dart'; import 'package:flame/src/text/styles/flame_text_style.dart';
import 'package:flame/src/text/styles/background_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 `<div/>` 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 { abstract class BlockNode {
Element? makeBackground(BackgroundStyle? style, double width, double height) { /// The runtime style applied to this node, this will be set by [fillStyles].
if (style == null) { late BlockStyle style;
return null;
}
final out = <Element>[];
final backgroundPaint = style.backgroundPaint;
final borderPaint = style.borderPaint;
final borders = style.borderWidths;
final radius = style.borderRadius;
if (backgroundPaint != null) { BlockElement format(double availableWidth);
if (radius == 0) {
out.add(RectElement(width, height, backgroundPaint)); void fillStyles(DocumentStyle stylesheet, FlameTextStyle parentTextStyle);
} 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);
}
}
} }

View File

@ -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<TextNode> 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;
}

View File

@ -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<BlockNode> children;
@override
BlockElement format(double availableWidth) {
final out = <Element>[];
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);
}
}
}

View File

@ -3,13 +3,14 @@ import 'dart:math';
import 'package:flame/src/text/common/utils.dart'; import 'package:flame/src/text/common/utils.dart';
import 'package:flame/src/text/elements/element.dart'; import 'package:flame/src/text/elements/element.dart';
import 'package:flame/src/text/elements/group_element.dart'; import 'package:flame/src/text/elements/group_element.dart';
import 'package:flame/src/text/nodes.dart'; import 'package:flame/src/text/nodes/block_node.dart';
import 'package:flame/src/text/nodes/paragraph_node.dart';
import 'package:flame/src/text/styles/document_style.dart'; import 'package:flame/src/text/styles/document_style.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
class DocumentNode extends GroupBlockNode { class DocumentNode {
DocumentNode(super.children); DocumentNode(this.children);
final List<BlockNode> children;
/// Applies [style] to this document, producing an object that can be rendered /// Applies [style] to this document, producing an object that can be rendered
/// on a canvas. Parameters [width] and [height] serve as the fallback values /// 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', 'Width must be either provided explicitly or set in the stylesheet',
); );
final out = <Element>[]; final out = <Element>[];
final border = style.backgroundStyle?.borderWidths ?? EdgeInsets.zero; final border = style.background?.borderWidths ?? EdgeInsets.zero;
final padding = style.padding; final padding = style.padding;
final pageWidth = style.width ?? width!; final pageWidth = style.width ?? width!;
final contentWidth = pageWidth - padding.horizontal - border.horizontal; final contentWidth = pageWidth - padding.horizontal;
final horizontalOffset = padding.left + border.left; final horizontalOffset = padding.left;
var verticalOffset = border.top; var verticalOffset = border.top;
var currentMargin = padding.top; var currentMargin = padding.top;
for (final node in children) { 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); verticalOffset += collapseMargin(currentMargin, blockStyle.margin.top);
final nodeElement = (node as ParagraphNode).format( final nodeElement = node.format(contentWidth);
blockStyle,
parentWidth: contentWidth,
);
nodeElement.translate(horizontalOffset, verticalOffset); nodeElement.translate(horizontalOffset, verticalOffset);
out.add(nodeElement); out.add(nodeElement);
currentMargin = blockStyle.margin.bottom; currentMargin = blockStyle.margin.bottom;
@ -48,13 +47,13 @@ class DocumentNode extends GroupBlockNode {
border.bottom, border.bottom,
); );
final background = makeBackground( final background = makeBackground(
style.backgroundStyle, style.background,
pageWidth, pageWidth,
pageHeight, pageHeight,
); );
if (background != null) { if (background != null) {
out.insert(0, background); out.insert(0, background);
} }
return GroupElement(pageWidth, pageHeight, out); return GroupElement(width: pageWidth, height: pageHeight, children: out);
} }
} }

View File

@ -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<TextNode> 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 = <TextElement>[];
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);
}
}
}

View File

@ -1,9 +1,56 @@
import 'package:flame/src/text/nodes.dart'; import 'dart:ui';
import 'package:flame/src/text/nodes/block_node.dart';
class HeaderNode extends BlockNode { import 'package:flame/src/text/nodes/plain_text_node.dart';
HeaderNode(this.child, {required this.level}); 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; 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);
}
} }

View File

@ -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<TextNode> 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;
}

View File

@ -1,59 +1,29 @@
import 'package:flame/src/text/elements/block_element.dart'; import 'package:flame/src/text/nodes/group_text_node.dart';
import 'package:flame/src/text/elements/group_element.dart'; import 'package:flame/src/text/nodes/plain_text_node.dart';
import 'package:flame/src/text/formatters/text_painter_text_formatter.dart'; import 'package:flame/src/text/nodes/text_block_node.dart';
import 'package:flame/src/text/inline/text_painter_text_element.dart'; import 'package:flame/src/text/nodes/text_node.dart';
import 'package:flame/src/text/nodes.dart';
import 'package:flame/src/text/nodes/block_node.dart';
import 'package:flame/src/text/styles/block_style.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 { class ParagraphNode extends TextBlockNode {
ParagraphNode.simple(String text) ParagraphNode(super.child);
: child = GroupTextNode([PlainTextNode(text)]);
final GroupTextNode child; ParagraphNode.simple(String text) : super(PlainTextNode(text));
BlockElement format(BlockStyle style, {required double parentWidth}) { ParagraphNode.group(List<TextNode> fragments)
final text = (child.children.first as PlainTextNode).text; : super(GroupTextNode(fragments));
final formatter = TextPainterTextFormatter(
style: const painting.TextStyle(fontSize: 16),
);
final contentWidth = parentWidth - style.padding.horizontal;
final horizontalOffset = style.padding.left;
final words = text.split(' '); static const defaultStyle = BlockStyle(
final lines = <TextPainterTextElement>[]; margin: EdgeInsets.all(6),
var verticalOffset = style.padding.top; );
var i0 = 0;
var i1 = 1; @override
var startNewLine = true; void fillStyles(DocumentStyle stylesheet, FlameTextStyle parentTextStyle) {
while (i1 <= words.length) { style = stylesheet.paragraph;
final lineText = words.sublist(i0, i1).join(' '); final textStyle = Style.merge(parentTextStyle, style.text)!;
final formattedLine = formatter.format(lineText); super.fillStyles(stylesheet, textStyle);
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);
} }
} }

View File

@ -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<String> 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;
}
}
}

View File

@ -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 = <TextElement>[];
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,
);
}
}

View File

@ -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;
}

View File

@ -1,6 +1,8 @@
import 'package:flame/src/text/styles/style.dart'; import 'package:flame/src/text/styles/style.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
@immutable
class BackgroundStyle extends Style { class BackgroundStyle extends Style {
BackgroundStyle({ BackgroundStyle({
Color? color, Color? color,
@ -13,45 +15,28 @@ class BackgroundStyle extends Style {
'Parameters `paint` and `color` are exclusive', 'Parameters `paint` and `color` are exclusive',
), ),
borderWidths = EdgeInsets.all(borderWidth ?? 0), borderWidths = EdgeInsets.all(borderWidth ?? 0),
borderRadius = borderRadius ?? 0 { borderRadius = borderRadius ?? 0,
if (paint != null) { backgroundPaint =
backgroundPaint = paint; paint ?? (color != null ? (Paint()..color = color) : null),
} else if (color != null) { borderPaint = borderColor != null
backgroundPaint = Paint()..color = color; ? (Paint()
} else { ..color = borderColor
backgroundPaint = null; ..style = PaintingStyle.stroke
} ..strokeWidth = borderWidth ?? 0)
if (borderColor != null) { : null;
borderPaint = Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = borderWidth ?? 0;
} else {
borderPaint = null;
}
}
late final Paint? backgroundPaint; final Paint? backgroundPaint;
late final Paint? borderPaint; final Paint? borderPaint;
final double borderRadius; final double borderRadius;
final EdgeInsets borderWidths; final EdgeInsets borderWidths;
@override @override
BackgroundStyle clone() => copyWith(); BackgroundStyle copyWith(BackgroundStyle other) {
BackgroundStyle copyWith({
Color? color,
Paint? paint,
Color? borderColor,
double? borderRadius,
double? borderWidth,
}) {
return BackgroundStyle( return BackgroundStyle(
color: color ?? (paint == null ? backgroundPaint?.color : null), paint: other.backgroundPaint ?? backgroundPaint,
paint: paint ?? backgroundPaint, borderColor: other.borderPaint?.color ?? borderPaint?.color,
borderColor: borderColor ?? borderPaint?.color, borderRadius: other.borderRadius,
borderRadius: borderRadius ?? this.borderRadius, borderWidth: other.borderWidths.top,
borderWidth: borderWidth ?? borderWidths.left,
); );
} }
} }

View File

@ -1,30 +1,36 @@
import 'package:flame/src/text/styles/background_style.dart'; 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: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 { class BlockStyle extends Style {
BlockStyle({ const BlockStyle({
this.margin = EdgeInsets.zero,
this.padding = EdgeInsets.zero,
this.background,
});
EdgeInsets margin;
EdgeInsets padding;
BackgroundStyle? background;
@override
BlockStyle clone() => copyWith();
BlockStyle copyWith({
EdgeInsets? margin, EdgeInsets? margin,
EdgeInsets? padding, 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( return BlockStyle(
margin: margin ?? this.margin, margin: other._margin ?? _margin,
padding: padding ?? this.padding, padding: other._padding ?? _padding,
background: background ?? this.background, background: other.background ?? background,
text: Style.merge(text, other.text),
); );
} }
} }

View File

@ -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/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/nodes/paragraph_node.dart';
import 'package:flame/src/text/styles/background_style.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/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/overflow.dart';
import 'package:flame/src/text/styles/style.dart'; import 'package:flame/src/text/styles/style.dart';
import 'package:flutter/painting.dart' show EdgeInsets; import 'package:flutter/painting.dart' show EdgeInsets;
import 'package:meta/meta.dart';
/// [DocumentStyle] is a user-facing description of how to render an entire /// [DocumentStyle] is a user-facing description of how to render an entire
/// body of text; it roughly corresponds to a stylesheet in HTML. /// body of text; it roughly corresponds to a stylesheet in HTML.
@ -21,25 +22,39 @@ class DocumentStyle extends Style {
DocumentStyle({ DocumentStyle({
this.width, this.width,
this.height, this.height,
EdgeInsets? padding, this.padding = EdgeInsets.zero,
BackgroundStyle? background, this.background,
BlockStyle? paragraphStyle, FlameTextStyle? text,
BlockStyle? header1Style, FlameTextStyle? boldText,
BlockStyle? header2Style, FlameTextStyle? italicText,
BlockStyle? header3Style, BlockStyle? paragraph,
BlockStyle? header4Style, BlockStyle? header1,
BlockStyle? header5Style, BlockStyle? header2,
BlockStyle? header6Style, BlockStyle? header3,
}) : padding = padding ?? EdgeInsets.zero { BlockStyle? header4,
backgroundStyle = acquire(background); BlockStyle? header5,
this.paragraphStyle = acquire(paragraphStyle ?? defaultParagraphStyle)!; BlockStyle? header6,
this.header1Style = acquire(header1Style ?? defaultHeader1Style)!; }) : _text = Style.merge(text, DocumentStyle.defaultTextStyle),
this.header2Style = acquire(header2Style ?? defaultHeader2Style)!; _boldText = Style.merge(boldText, BoldTextNode.defaultStyle),
this.header3Style = acquire(header3Style ?? defaultHeader3Style)!; _italicText = Style.merge(italicText, ItalicTextNode.defaultStyle),
this.header4Style = acquire(header4Style ?? defaultHeader4Style)!; _paragraph = Style.merge(paragraph, ParagraphNode.defaultStyle),
this.header5Style = acquire(header5Style ?? defaultHeader5Style)!; _header1 = Style.merge(header1, HeaderNode.defaultStyleH1),
this.header6Style = acquire(header6Style ?? defaultHeader6Style)!; _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. /// 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 /// 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 /// out exceeds the provided [height]. See the [Overflow] enum description for
/// more details. /// 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 distance from the outer edges of the page to the inner content box of
/// the document. /// the document.
@ -79,89 +94,51 @@ class DocumentStyle extends Style {
/// If present, describes what kind of background and borders to draw for the /// If present, describes what kind of background and borders to draw for the
/// document page(s). /// document page(s).
late final BackgroundStyle? backgroundStyle; final BackgroundStyle? background;
/// Style for paragraph nodes in the document. FlameTextStyle get text => _text!;
late final BlockStyle paragraphStyle; FlameTextStyle get boldText => _boldText!;
FlameTextStyle get italicText => _italicText!;
/// Style for level-1 headers. /// Style for [ParagraphNode]s.
late final BlockStyle header1Style; BlockStyle get paragraph => _paragraph!;
/// Style for level-2 headers. /// Styles for [HeaderNode]s, levels 1 to 6.
late final BlockStyle header2Style; 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. static FlameTextStyle defaultTextStyle = FlameTextStyle(fontSize: 16.0);
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();
@override @override
DocumentStyle clone() => copyWith(); DocumentStyle copyWith(DocumentStyle other) {
DocumentStyle copyWith({
double? width,
double? height,
EdgeInsets? padding,
BackgroundStyle? background,
BlockStyle? paragraphStyle,
BlockStyle? header1Style,
BlockStyle? header2Style,
BlockStyle? header3Style,
BlockStyle? header4Style,
BlockStyle? header5Style,
BlockStyle? header6Style,
}) {
return DocumentStyle( return DocumentStyle(
width: width ?? this.width, width: other.width ?? width,
height: height ?? this.height, height: other.height ?? height,
padding: padding ?? this.padding, padding: other.padding,
background: background ?? backgroundStyle, background: merge(other.background, background)! as BackgroundStyle,
paragraphStyle: paragraphStyle ?? this.paragraphStyle, paragraph: merge(other.paragraph, paragraph)! as BlockStyle,
header1Style: header1Style ?? this.header1Style, header1: merge(other.header1, header1)! as BlockStyle,
header2Style: header2Style ?? this.header2Style, header2: merge(other.header2, header2)! as BlockStyle,
header3Style: header3Style ?? this.header3Style, header3: merge(other.header3, header3)! as BlockStyle,
header4Style: header4Style ?? this.header4Style, header4: merge(other.header4, header4)! as BlockStyle,
header5Style: header5Style ?? this.header5Style, header5: merge(other.header5, header5)! as BlockStyle,
header6Style: header6Style ?? this.header6Style, header6: merge(other.header6, header6)! as BlockStyle,
); );
} }
@internal final Map<Style, Map<Style, Style>> _mergedStylesCache = {};
BlockStyle styleFor(BlockNode node) { Style? merge(Style? style1, Style? style2) {
if (node is ParagraphNode) { if (style1 == null) {
return paragraphStyle; 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();
} }
} }

View File

@ -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,
),
);
}
}

View File

@ -1,5 +1,4 @@
import 'package:flame/src/text/styles/document_style.dart'; 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 /// A [Style] is a base class for several classes that collectively describe
/// the desired visual appearance of a "rich-text" document. /// 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. /// The tree of [Style]s is roughly equivalent to a CSS stylesheet.
abstract class Style { abstract class Style {
/// The owner of the current style. const 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;
/// Creates and returns a copy of the current object. Style copyWith(covariant Style other);
Style clone();
/// Marks [style] as being owned by the current object and returns it. static T? merge<T extends Style>(T? style1, T? style2) {
/// However, if the [style] is already owned by some other object, then clones if (style1 == null) {
/// the [style], marks the copy as being owned, and returns it. return style2;
@protected } else if (style2 == null) {
S? acquire<S extends Style>(S? style) { return style1;
if (style == null) { } else {
return null; 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;
} }
} }

View File

@ -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/glyph_data.dart' show GlyphData;
export 'src/text/common/line_metrics.dart' show LineMetrics; export 'src/text/common/line_metrics.dart' show LineMetrics;
export 'src/text/common/sprite_font.dart' show SpriteFont; 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/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_element.dart' show TextElement;
export 'src/text/elements/text_painter_text_element.dart'
show TextPainterTextElement;
export 'src/text/formatters/sprite_font_text_formatter.dart' export 'src/text/formatters/sprite_font_text_formatter.dart'
show SpriteFontTextFormatter; show SpriteFontTextFormatter;
export 'src/text/formatters/text_formatter.dart' show TextFormatter; export 'src/text/formatters/text_formatter.dart' show TextFormatter;
export 'src/text/formatters/text_painter_text_formatter.dart' export 'src/text/formatters/text_painter_text_formatter.dart'
show TextPainterTextFormatter; show TextPainterTextFormatter;
export 'src/text/nodes/block_node.dart' show BlockNode; 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/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/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/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/sprite_font_renderer.dart' show SpriteFontRenderer;
export 'src/text/styles/background_style.dart' show BackgroundStyle; export 'src/text/styles/background_style.dart' show BackgroundStyle;
export 'src/text/styles/block_style.dart' show BlockStyle; export 'src/text/styles/block_style.dart' show BlockStyle;
export 'src/text/styles/document_style.dart' show DocumentStyle; 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_paint.dart' show TextPaint;
export 'src/text/text_renderer.dart' show TextRenderer; export 'src/text/text_renderer.dart' show TextRenderer;

View File

@ -1,12 +1,11 @@
import 'package:flame/components.dart';
import 'package:flame/text.dart'; import 'package:flame/text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' as flutter;
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
group('TextPaint', () { group('TextPaint', () {
test('copyWith returns a new instance with the new values', () { 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) final tp = TextPaint(style: style)
.copyWith((t) => t.copyWith(fontFamily: 'Helvetica')); .copyWith((t) => t.copyWith(fontFamily: 'Helvetica'));
expect(tp.style.fontSize, 12); expect(tp.style.fontSize, 12);

View File

@ -27,7 +27,7 @@ class DebugTextFormatter extends TextFormatter {
TextElement format(String text) => _DebugTextElement(this, text); TextElement format(String text) => _DebugTextElement(this, text);
} }
class _DebugTextElement extends TextElement implements TextLine { class _DebugTextElement extends TextElement {
_DebugTextElement(this.style, this.text) { _DebugTextElement(this.style, this.text) {
final charWidth = style.fontSize * 1.0; final charWidth = style.fontSize * 1.0;
final charHeight = style.fontSize; final charHeight = style.fontSize;
@ -52,9 +52,6 @@ class _DebugTextElement extends TextElement implements TextLine {
@override @override
late final LineMetrics metrics; late final LineMetrics metrics;
@override
TextLine get lastLine => this;
@override @override
void render(Canvas canvas) { void render(Canvas canvas) {
canvas.save(); canvas.save();