mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 19:12:31 +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
|
@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 '
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 '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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
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 '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;
|
||||||
|
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 '
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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/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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
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/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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 '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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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/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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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: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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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: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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user