Skip to content

Commit

Permalink
Ignore getMinIntrinsicWidth error during table rendering (#1067)
Browse files Browse the repository at this point in the history
Also:

* Apply middle vertical align for `DETAILS` marker
* Fix conflict between `vertical-align` and `display: inline-block`
* Update `BuildOp.inline` for better compatibility
* [core] v0.14.3
* [enhanced] v0.14.3
  • Loading branch information
daohoangson authored Oct 17, 2023
1 parent 9332ace commit 942ad5d
Show file tree
Hide file tree
Showing 22 changed files with 226 additions and 69 deletions.
Binary file modified demo_app/test/goldens/SUMMARY.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo_app/test/goldens/tag/DETAILS/close.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo_app/test/goldens/tag/DETAILS/open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demo_app/test/sizing/_guessChildSize/sized_inline_block.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.14.3

- Write migration docs from v0.10 to v0.14 (#1065)
- Fix Flutter badge SVG urls (#1065)
- Fix broken table (#1067)

## 0.14.2

- Add support for `display: table-cell` without row (#905)
Expand Down
2 changes: 1 addition & 1 deletion packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Add this to your app's `pubspec.yaml` file:

```yaml
dependencies:
flutter_widget_from_html_core: ^0.14.2
flutter_widget_from_html_core: ^0.14.3
```
## Usage
Expand Down
14 changes: 14 additions & 0 deletions packages/core/lib/src/data/build_bits.dart
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,17 @@ abstract class WidgetBit<T> extends BuildBit {

const WidgetBit._(this.parent, this.child);

/// How the placeholder aligns vertically with the text.
///
/// See [ui.PlaceholderAlignment] for details on each mode.
PlaceholderAlignment? get alignment => null;

/// The [TextBaseline] to align against when using [ui.PlaceholderAlignment.baseline],
/// [ui.PlaceholderAlignment.aboveBaseline], and [ui.PlaceholderAlignment.belowBaseline].
///
/// This is ignored when using other alignment modes.
TextBaseline? get baseline => null;

/// Creates a block widget.
static WidgetBit<Widget> block(BuildTree parent, Widget child) =>
_WidgetBitBlock(
Expand Down Expand Up @@ -348,7 +359,10 @@ class _WidgetBitBlock extends WidgetBit<Widget> {
}

class _WidgetBitInline extends WidgetBit<InlineSpan> {
@override
final PlaceholderAlignment alignment;

@override
final TextBaseline baseline;

const _WidgetBitInline(
Expand Down
14 changes: 14 additions & 0 deletions packages/core/lib/src/data/build_op.dart
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,20 @@ class BuildOp {
BuildOp.v2(
debugLabel: debugLabel,
onParsed: (tree) {
final bits = [...tree.bits];
if (bits.length == 1) {
final bit = bits.first;
if (bit is WidgetBit &&
bit.isInline == true &&
bit.alignment == alignment &&
bit.baseline == baseline) {
// tree has exactly 1 inline bit & all configurations match
// let's reuse the existing placeholder
bit.child.wrapWith((_, w) => onRenderInlineBlock(tree, w));
return tree;
}
}

final parent = tree.parent;
return parent.sub()
..append(
Expand Down
9 changes: 9 additions & 0 deletions packages/core/lib/src/internal/ops/style_display.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ extension StyleDisplayInlineBlock on WidgetFactory {
);

static BuildTree _onParsed(BuildTree tree) {
final bits = [...tree.bits];
if (bits.length == 1) {
final bit = bits.first;
if (bit is WidgetBit && bit.isInline == true) {
// tree has exactly 1 inline bit, nothing to do here
return tree;
}
}

final parent = tree.parent;
return parent.sub()
..append(
Expand Down
7 changes: 6 additions & 1 deletion packages/core/lib/src/internal/ops/tag_details.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ const kAttributeDetailsOpen = 'open';
const kTagDetails = 'details';
const kTagSummary = 'summary';

// use middle alignment as an early optimization
// baseline alignment may cause issue with getDrySize, getMinIntrinsicWidth, etc.
const _markerMarkerAlignment = PlaceholderAlignment.middle;

class TagDetails {
final WidgetFactory wf;

Expand All @@ -29,6 +33,7 @@ class TagDetails {
TextSpan(
children: [
WidgetSpan(
alignment: _markerMarkerAlignment,
child: HtmlDetailsMarker(style: textStyle),
),
// TODO: i18n
Expand Down Expand Up @@ -78,7 +83,7 @@ class TagDetails {
},
debugLabel: '$kTagSummary--inlineMarker',
),
alignment: PlaceholderAlignment.bottom,
alignment: _markerMarkerAlignment,
);
return summaryTree..prepend(marker);
},
Expand Down
14 changes: 11 additions & 3 deletions packages/core/lib/src/widgets/html_table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -494,9 +494,17 @@ class _TableRenderLayouter {
}
}

// table is too crowded
// get min width to avoid breaking line in the middle of a word
return child.getMinIntrinsicWidth(double.infinity);
try {
// table is too crowded
// get min width to avoid breaking line in the middle of a word
final w = child.getMinIntrinsicWidth(double.infinity);
return w;
} catch (error, stackTrace) {
// TODO: render horizontal scroll view
const message = "Could not measure child's min intrinsic width";
_logger.warning(message, error, stackTrace);
return null;
}
}

_TableDataStep4 step4ChildSizesAndRowHeights(_TableDataStep3 step3) {
Expand Down
12 changes: 12 additions & 0 deletions packages/core/lib/src/widgets/inline_custom_widget.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import 'package:flutter/widgets.dart';

/// A custom inline [Widget].
class InlineCustomWidget extends StatelessWidget {
/// How the placeholder aligns vertically with the text.
///
/// See [ui.PlaceholderAlignment] for details on each mode.
final PlaceholderAlignment alignment;

/// The [TextBaseline] to align against when using [ui.PlaceholderAlignment.baseline],
/// [ui.PlaceholderAlignment.aboveBaseline], and [ui.PlaceholderAlignment.belowBaseline].
///
/// This is ignored when using other alignment modes.
final TextBaseline baseline;

/// The custom [Widget].
final Widget child;

/// Creates a custom inline [Widget].
const InlineCustomWidget({
this.alignment = PlaceholderAlignment.baseline,
this.baseline = TextBaseline.alphabetic,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: flutter_widget_from_html_core
version: 0.14.2
version: 0.14.3
description: Flutter package to render html as widgets that focuses on correctness and extensibility.
homepage: https://github.com/daohoangson/flutter_widget_from_html/tree/master/packages/core

Expand Down
19 changes: 16 additions & 3 deletions packages/core/test/src/data/build_bits_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,17 @@ void main() {
expect(_data(text2), equals('1'));
});

test('WidgetBit.block returns same parent', () {
test('WidgetBit.block keeps fields', () {
final text = _text();
final child = WidgetPlaceholder();
final bit = WidgetBit.block(text, child);

final copied = bit.copyWith();
expect(copied.parent, equals(text));

final bit2 = copied as WidgetBit;
expect(bit2.alignment, isNull);
expect(bit2.baseline, isNull);
});

test('WidgetBit.block returns', () {
Expand All @@ -187,13 +191,22 @@ void main() {
expect(text2.build(), equals(child));
});

test('WidgetBit.inline returns same parent', () {
test('WidgetBit.inline keeps fields', () {
final text = _text();
final child = WidgetPlaceholder();
final bit = WidgetBit.inline(text, child);
final bit = WidgetBit.inline(
text,
child,
alignment: PlaceholderAlignment.top,
baseline: TextBaseline.ideographic,
);

final copied = bit.copyWith();
expect(copied.parent, equals(text));

final bit2 = copied as WidgetBit;
expect(bit2.alignment, PlaceholderAlignment.top);
expect(bit2.baseline, TextBaseline.ideographic);
});

test('BuildTree returns', () {
Expand Down
69 changes: 52 additions & 17 deletions packages/core/test/src/data/build_op_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,25 +99,60 @@ void main() {
);
});

testWidgets('works with inline-block', (WidgetTester tester) async {
group('works with inline-block', () {
const html = 'Hello <span class="inlineBuildOp" '
'style="display: inline-block">foo</span>';
final explained = await explain(
tester,
html,
inlineBuildOp: BuildOp.inline(
onRenderInlineBlock: (tree, child) {
return ColoredBox(
color: Colors.green,
child: child,
);
},
),
);
expect(
explained,
equals('[RichText:(:Hello [ColoredBox:child=[RichText:(:foo)]])]'),
);

testWidgets('reuse placeholder', (WidgetTester tester) async {
final e1 = await explain(
tester,
html,
inlineBuildOp: BuildOp.inline(
debugLabel: 'inlineBuildOp',
onRenderInlineBlock: (tree, child) {
return ColoredBox(
color: Colors.green,
child: child,
);
},
),
);
expect(
e1,
equals('[RichText:(:Hello [ColoredBox:child=[RichText:(:foo)]])]'),
);

final e2 = await helper.explainWithoutPumping(useExplainer: false);
expect(e2, isNot(contains('WidgetPlaceholder(inlineBuildOp)')));
expect(e2, contains('WidgetPlaceholder(inline-block)'));
});

testWidgets("don't reuse placeholder", (WidgetTester tester) async {
final e1 = await explain(
tester,
html,
inlineBuildOp: BuildOp.inline(
alignment: PlaceholderAlignment.top,
debugLabel: 'inlineBuildOp',
onRenderInlineBlock: (tree, child) {
return ColoredBox(
color: Colors.green,
child: child,
);
},
),
);
expect(
e1,
equals(
'[RichText:(:Hello [ColoredBox:child=[RichText:(:foo)]]@top)]',
),
);

final e2 = await helper.explainWithoutPumping(useExplainer: false);
expect(e2, contains('WidgetPlaceholder(inlineBuildOp)'));
expect(e2, isNot(contains('WidgetPlaceholder(inline-block)')));
});
});
});
});
Expand Down
19 changes: 0 additions & 19 deletions packages/core/test/style_sizing_test.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
Expand Down Expand Up @@ -813,24 +812,6 @@ Future<void> main() async {
'$kGoldenFilePrefix/sizing/100_percent/$name.png',
),
);

group('error handling', () {
testWidgets('skips _guessChildSize on sizeHeight error', (tester) async {
// the IMG tag is rendered as a `WidgetSpan` with a `baseline` alignment
// making it impossible for `RenderParagraph` to perform dry layout
const src = 'https://domain.com/image.jpg';
const html = '<div style="width: 100px; height: 100px">'
'Foo <img src="$src" />'
'</div>';
await explain(tester, html, useExplainer: false);
final render =
tester.firstRenderObject<RenderParagraph>(find.byType(RichText));
expect(
render.constraints,
equals(BoxConstraints.tight(const Size(100, 100))),
);
});
});
});
}

Expand Down
56 changes: 43 additions & 13 deletions packages/core/test/style_vertical_align_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,49 @@ void main() {
});

group('possible conflict', () {
testWidgets('display: inline', (WidgetTester tester) async {
const html = 'Foo <span style="display: inline; '
'vertical-align: top">bar</span>';
final explained = await explain(tester, html);
expect(explained, equals('[RichText:(:Foo [RichText:(:bar)]@top)]'));
});

testWidgets('display: inline-block', (WidgetTester tester) async {
const html = 'Foo <span style="display: inline-block; '
'vertical-align: top">bar</span>';
final explained = await explain(tester, html);
expect(explained, equals('[RichText:(:Foo [RichText:(:bar)]@top)]'));
});

testWidgets('display: block', (WidgetTester tester) async {
const html = 'Foo <span style="display: block; '
'vertical-align: top">bar</span>';
final explained = await explain(tester, html);
expect(
explained,
equals(
'[Column:children='
'[RichText:(:Foo)],'
'[Align:alignment=topLeft,widthFactor=1.0,child='
'[CssBlock:child=[RichText:(:bar)]]'
']]',
),
);
});

testWidgets('renders with A tag', (WidgetTester tester) async {
const html = '<sup><a href="http://domain.com/foo">foo</a></sup>';
final explained = await explain(tester, html);
expect(
explained,
equals(
'[Align:alignment=topCenter,widthFactor=1.0,child='
'[Padding:(0,0,3,0),child=[RichText:(#[email protected]+onTap:foo)]]'
']',
),
);
});

group('image', () {
const imgSrc = 'http://domain.com/image.png';
const imgRendered =
Expand Down Expand Up @@ -214,19 +257,6 @@ void main() {
);
});
});

testWidgets('renders with A tag', (WidgetTester tester) async {
const html = '<sup><a href="http://domain.com/foo">foo</a></sup>';
final explained = await explain(tester, html);
expect(
explained,
equals(
'[Align:alignment=topCenter,widthFactor=1.0,child='
'[Padding:(0,0,3,0),child=[RichText:(#[email protected]+onTap:foo)]]'
']',
),
);
});
});

group('error handling', () {
Expand Down
Loading

1 comment on commit 942ad5d

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.