mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 09:39:12 +08:00
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:
@ -11,7 +11,7 @@ class RichTextExample extends FlameGame {
|
||||
|
||||
@override
|
||||
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),
|
||||
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 '
|
||||
|
||||
@ -46,8 +46,8 @@ class TextComponent<T extends TextRenderer> 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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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 = <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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import 'package:flame/src/text/elements/element.dart';
|
||||
/// such as `<div>` or `<blockquote>`.
|
||||
abstract class BlockElement extends Element {
|
||||
BlockElement(this.width, this.height);
|
||||
double width;
|
||||
double height;
|
||||
|
||||
final double width;
|
||||
final double height;
|
||||
}
|
||||
|
||||
@ -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.
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<Element> children;
|
||||
|
||||
|
||||
48
packages/flame/lib/src/text/elements/group_text_element.dart
Normal file
48
packages/flame/lib/src/text/elements/group_text_element.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -12,7 +12,7 @@ class FormatterTextRenderer<T extends TextFormatter> 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<T extends TextFormatter> 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,
|
||||
);
|
||||
|
||||
@ -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 '
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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 `<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 {
|
||||
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;
|
||||
/// 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);
|
||||
}
|
||||
|
||||
29
packages/flame/lib/src/text/nodes/bold_text_node.dart
Normal file
29
packages/flame/lib/src/text/nodes/bold_text_node.dart
Normal 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;
|
||||
}
|
||||
64
packages/flame/lib/src/text/nodes/column_node.dart
Normal file
64
packages/flame/lib/src/text/nodes/column_node.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<BlockNode> 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 = <Element>[];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
66
packages/flame/lib/src/text/nodes/group_text_node.dart
Normal file
66
packages/flame/lib/src/text/nodes/group_text_node.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
30
packages/flame/lib/src/text/nodes/italic_text_node.dart
Normal file
30
packages/flame/lib/src/text/nodes/italic_text_node.dart
Normal 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;
|
||||
}
|
||||
@ -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),
|
||||
ParagraphNode.group(List<TextNode> fragments)
|
||||
: super(GroupTextNode(fragments));
|
||||
|
||||
static const defaultStyle = BlockStyle(
|
||||
margin: EdgeInsets.all(6),
|
||||
);
|
||||
final contentWidth = parentWidth - style.padding.horizontal;
|
||||
final horizontalOffset = style.padding.left;
|
||||
|
||||
final words = text.split(' ');
|
||||
final lines = <TextPainterTextElement>[];
|
||||
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);
|
||||
@override
|
||||
void fillStyles(DocumentStyle stylesheet, FlameTextStyle parentTextStyle) {
|
||||
style = stylesheet.paragraph;
|
||||
final textStyle = Style.merge(parentTextStyle, style.text)!;
|
||||
super.fillStyles(stylesheet, textStyle);
|
||||
}
|
||||
}
|
||||
|
||||
58
packages/flame/lib/src/text/nodes/plain_text_node.dart
Normal file
58
packages/flame/lib/src/text/nodes/plain_text_node.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
packages/flame/lib/src/text/nodes/text_block_node.dart
Normal file
58
packages/flame/lib/src/text/nodes/text_block_node.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
16
packages/flame/lib/src/text/nodes/text_node.dart
Normal file
16
packages/flame/lib/src/text/nodes/text_node.dart
Normal 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;
|
||||
}
|
||||
@ -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()
|
||||
borderRadius = borderRadius ?? 0,
|
||||
backgroundPaint =
|
||||
paint ?? (color != null ? (Paint()..color = color) : null),
|
||||
borderPaint = borderColor != null
|
||||
? (Paint()
|
||||
..color = borderColor
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = borderWidth ?? 0;
|
||||
} else {
|
||||
borderPaint = null;
|
||||
}
|
||||
}
|
||||
..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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
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;
|
||||
final Map<Style, Map<Style, Style>> _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);
|
||||
}
|
||||
}
|
||||
return BlockStyle();
|
||||
}
|
||||
}
|
||||
|
||||
55
packages/flame/lib/src/text/styles/flame_text_style.dart
Normal file
55
packages/flame/lib/src/text/styles/flame_text_style.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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 extends Style>(S? style) {
|
||||
if (style == null) {
|
||||
return null;
|
||||
}
|
||||
final useStyle = style._parent == null ? style : style.clone() as S;
|
||||
useStyle._parent = this;
|
||||
return useStyle;
|
||||
static T? merge<T extends Style>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
Reference in New Issue
Block a user