Skip to content

Commit 30c1c8e

Browse files
committed
fix: Render linebreaks after blocks with fixed height
Consecutive line breaks following a block element get converted to vertical space with hte expected height. This avoids an issue where the RichText widget renders additional linebreaks after a block element with the same hight as the block element instead of returning to the default line height of the text.
1 parent 18c4b10 commit 30c1c8e

3 files changed

Lines changed: 93 additions & 9 deletions

File tree

packages/core/lib/src/internal/ops/tag_br.dart

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,44 @@ class TagBrBit extends BuildBit {
2525
BuildBit copyWith({BuildTree? parent}) => TagBrBit(parent ?? this.parent);
2626

2727
@override
28-
void flatten(Flattened f) => f.write(text: '\n');
28+
void flatten(Flattened f) {
29+
final next = _nextNonWhitespace;
30+
if (next != null && next.isInline == false) {
31+
// Keep behavior of skipping a single BR before a block, but convert
32+
// additional BRs to vertical spacing outside RichText to avoid
33+
// WidgetSpan line-metric artifacts.
34+
final prev = _prevNonWhitespace;
35+
if (prev is TagBrBit || prev?.isInline == false) {
36+
const oneEm = CssLength(1, CssLengthUnit.em);
37+
f.widget(
38+
HeightPlaceholder(
39+
oneEm,
40+
parent.inheritanceResolvers,
41+
debugLabel: '${parent.element.localName}--$oneEm',
42+
),
43+
);
44+
}
45+
return;
46+
}
47+
48+
f.write(text: '\n');
49+
}
50+
51+
BuildBit? get _nextNonWhitespace {
52+
var next = this.next;
53+
while (next is WhitespaceBit) {
54+
next = next.next;
55+
}
56+
return next;
57+
}
58+
59+
BuildBit? get _prevNonWhitespace {
60+
var prev = this.prev;
61+
while (prev is WhitespaceBit) {
62+
prev = prev.prev;
63+
}
64+
return prev;
65+
}
2966

3067
@override
3168
String toString() => '<BR />';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
4+
import 'package:html/dom.dart' as dom;
5+
6+
import '_.dart' as helper;
7+
8+
void main() {
9+
// Some special test cases for BR handling, note that most of the tests for BR are in `core_test.dart`
10+
group('BR', () {
11+
testWidgets(
12+
'avoids WidgetSpan newline artifact before block',
13+
(WidgetTester tester) async {
14+
Widget? customWidgetBuilder(dom.Element element) {
15+
if (element.localName != 'x-inline') {
16+
return null;
17+
}
18+
19+
return const InlineCustomWidget(
20+
child: SizedBox(width: 20, height: 20),
21+
);
22+
}
23+
24+
// Keep BR spacing outside RichText for inline widgets before a block:
25+
// if a trailing newline remains in the same paragraph as WidgetSpan,
26+
// paragraph line metrics may produce unexpectedly large vertical gaps.
27+
const html = '<x-inline></x-inline><br><br><div>Next line!</div>';
28+
final explained = await helper.explain(
29+
tester,
30+
null,
31+
hw: HtmlWidget(
32+
html,
33+
customWidgetBuilder: customWidgetBuilder,
34+
key: helper.hwKey,
35+
),
36+
);
37+
38+
expect(
39+
explained,
40+
allOf(
41+
startsWith('[Column:children=['),
42+
contains('[SizedBox:20.0x20.0],'),
43+
contains('[SizedBox:0.0x10.0],'),
44+
contains('[CssBlock:child=[RichText:(:Next line!)]]'),
45+
isNot(contains('[RichText:(:[SizedBox:20.0x20.0]')),
46+
),
47+
);
48+
},
49+
);
50+
});
51+
}

packages/core/test/tag_img_test.dart

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ void main() {
1313

1414
group('image.png', () {
1515
const src = 'http://domain.com/image.png';
16-
Future<String> explain(WidgetTester tester, String html) =>
17-
mockNetworkImages(() => helper.explain(tester, html));
16+
Future<String> explain(WidgetTester tester, String html) => mockNetworkImages(() => helper.explain(tester, html));
1817

1918
testWidgets('renders src', (WidgetTester tester) async {
2019
const html = '<img src="$src" />';
@@ -424,8 +423,7 @@ class _LoadingBuilderFactory extends WidgetFactory {
424423
_LoadingBuilderFactory(this.streamCompleter);
425424

426425
@override
427-
ImageProvider<Object> imageProviderFromNetwork(String url) =>
428-
_TestImageProvider(streamCompleter);
426+
ImageProvider<Object> imageProviderFromNetwork(String url) => _TestImageProvider(streamCompleter);
429427
}
430428

431429
class _TestImageProvider extends ImageProvider<Object> {
@@ -434,12 +432,10 @@ class _TestImageProvider extends ImageProvider<Object> {
434432
_TestImageProvider(this.streamCompleter);
435433

436434
@override
437-
Future<Object> obtainKey(ImageConfiguration configuration) =>
438-
SynchronousFuture<_TestImageProvider>(this);
435+
Future<Object> obtainKey(ImageConfiguration configuration) => SynchronousFuture<_TestImageProvider>(this);
439436

440437
@override
441-
ImageStreamCompleter loadImage(Object key, ImageDecoderCallback decode) =>
442-
streamCompleter;
438+
ImageStreamCompleter loadImage(Object key, ImageDecoderCallback decode) => streamCompleter;
443439
}
444440

445441
class _TestImageStreamCompleter extends ImageStreamCompleter {

0 commit comments

Comments
 (0)