Skip to content

Commit 08caa3e

Browse files
committed
fix(rendering): keep RTL abspos shrink-to-fit text visible in IFC
1 parent d60226b commit 08caa3e

File tree

4 files changed

+161
-12
lines changed

4 files changed

+161
-12
lines changed
3.33 KB
Loading
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
describe('RTL abspos shrink-to-fit badge text', () => {
2+
it('does not paint the inline text outside the badge (overflow-x hidden)', async () => {
3+
await resizeViewport(375, 756);
4+
5+
const prevHtmlDir = document.documentElement.getAttribute('dir');
6+
const prevBodyMargin = document.body.style.margin;
7+
const prevBodyPadding = document.body.style.padding;
8+
9+
document.documentElement.style.margin = '0';
10+
document.body.style.margin = '0';
11+
document.body.style.padding = '0';
12+
13+
const root = document.createElement('div');
14+
root.id = 'root';
15+
root.setAttribute('dir', 'rtl');
16+
root.setAttribute(
17+
'style',
18+
[
19+
'width: 375px',
20+
'height: 260px',
21+
'overflow-y: auto',
22+
'background: #ffffff',
23+
'padding: 16px',
24+
'box-sizing: border-box',
25+
].join(';'),
26+
);
27+
28+
const card = document.createElement('div');
29+
card.id = 'card';
30+
card.setAttribute(
31+
'style',
32+
[
33+
'position: relative',
34+
'width: 343px',
35+
'padding: 40px 24px',
36+
'overflow-x: hidden',
37+
'border-radius: 16px',
38+
'background: #f3f4f6',
39+
'box-sizing: border-box',
40+
].join(';'),
41+
);
42+
43+
const spacer = document.createElement('div');
44+
spacer.setAttribute('style', 'height: 120px; background: rgba(0,0,0,0.03)');
45+
46+
const badge = document.createElement('div');
47+
badge.id = 'badge';
48+
badge.textContent = '認證商家';
49+
badge.setAttribute(
50+
'style',
51+
[
52+
'position: absolute',
53+
'inset-inline-start: 0',
54+
'top: 0',
55+
'background: rgb(248 113 113)',
56+
'color: rgb(255 215 0)',
57+
'padding: 4px 12px',
58+
'border-radius: 16px 0 8px 0',
59+
'font-size: 14px',
60+
'font-weight: 600',
61+
'white-space: nowrap',
62+
].join(';'),
63+
);
64+
65+
try {
66+
card.appendChild(spacer);
67+
card.appendChild(badge);
68+
root.appendChild(card);
69+
document.body.appendChild(root);
70+
71+
await waitForOnScreen(root as any);
72+
await waitForFrame();
73+
await nextFrames(2);
74+
75+
const cardRect = card.getBoundingClientRect();
76+
const badgeRect = badge.getBoundingClientRect();
77+
expect(cardRect.width).toBeGreaterThan(300);
78+
expect(badgeRect.width).toBeGreaterThan(40);
79+
expect(badge.textContent).toBe('認證商家');
80+
81+
await snapshot(card);
82+
} finally {
83+
try {
84+
root.remove();
85+
} catch (_) {}
86+
87+
if (prevHtmlDir == null) {
88+
document.documentElement.removeAttribute('dir');
89+
} else {
90+
document.documentElement.setAttribute('dir', prevHtmlDir);
91+
}
92+
document.body.style.margin = prevBodyMargin;
93+
document.body.style.padding = prevBodyPadding;
94+
await resizeViewport(-1, -1);
95+
}
96+
});
97+
});
98+

webf/lib/src/rendering/inline_formatting_context.dart

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3433,6 +3433,15 @@ class InlineFormattingContext {
34333433
bool shapedWithHugeWidth = false;
34343434
bool shapedWithZeroWidth = false; // Track when we intentionally shape with 0 width
34353435

3436+
final CSSRenderStyle containerStyle = (container as RenderBoxModel).renderStyle;
3437+
final CSSPositionType posType = containerStyle.position;
3438+
final bool containerIsOutOfFlow = posType == CSSPositionType.absolute || posType == CSSPositionType.fixed;
3439+
// For out-of-flow positioned blocks with auto width, CSS uses shrink-to-fit sizing.
3440+
// In these cases, shaping/layouting the paragraph to an ancestor "fallback" width can
3441+
// cause RTL start alignment to place glyphs far to the right (outside the shrink-to-fit
3442+
// box), where they may then be clipped by an ancestor overflow.
3443+
final bool outOfFlowShrinkToFit = containerIsOutOfFlow && containerStyle.width.isAuto;
3444+
34363445
final bool hasAtomicInlines = _items.any((it) => it.isAtomicInline);
34373446
final bool hasExplicitBreaks =
34383447
_items.any((it) => it.type == InlineItemType.control || it.type == InlineItemType.lineBreakOpportunity);
@@ -3467,10 +3476,15 @@ class InlineFormattingContext {
34673476
final bool preferZeroWidthShaping =
34683477
hasAtomicInlines || hasExplicitBreaks || hasWhitespaceInText || hasCJKBreaks || breakAll;
34693478
if (!constraints.hasBoundedWidth) {
3470-
initialWidth =
3471-
(fallbackContentMaxWidth != null && fallbackContentMaxWidth > 0) ? fallbackContentMaxWidth : 1000000.0;
3472-
if (initialWidth >= 1000000.0) {
3479+
if (outOfFlowShrinkToFit) {
3480+
initialWidth = 1000000.0;
34733481
shapedWithHugeWidth = true;
3482+
} else {
3483+
initialWidth =
3484+
(fallbackContentMaxWidth != null && fallbackContentMaxWidth > 0) ? fallbackContentMaxWidth : 1000000.0;
3485+
if (initialWidth >= 1000000.0) {
3486+
shapedWithHugeWidth = true;
3487+
}
34743488
}
34753489
} else {
34763490
if (constraints.maxWidth > 0) {
@@ -3511,15 +3525,13 @@ class InlineFormattingContext {
35113525
}
35123526
paragraph.layout(ui.ParagraphConstraints(width: initialWidth));
35133527

3514-
final CSSDisplay display = (container as RenderBoxModel).renderStyle.effectiveDisplay;
3528+
final CSSDisplay display = containerStyle.effectiveDisplay;
35153529
final bool isBlockLike = display == CSSDisplay.block || display == CSSDisplay.inlineBlock;
35163530

35173531
if (shapedWithHugeWidth &&
35183532
constraints.hasBoundedWidth &&
35193533
constraints.maxWidth.isFinite &&
35203534
constraints.maxWidth > 0) {
3521-
final CSSPositionType posType = (container as RenderBoxModel).renderStyle.position;
3522-
final bool containerIsOutOfFlow = posType == CSSPositionType.absolute || posType == CSSPositionType.fixed;
35233535
if (!containerIsOutOfFlow) {
35243536
final double naturalSingleLine = paragraph.longestLine;
35253537
if (constraints.maxWidth + 0.5 >= naturalSingleLine) {
@@ -3534,15 +3546,19 @@ class InlineFormattingContext {
35343546

35353547
if (isBlockLike) {
35363548
if (!constraints.hasBoundedWidth) {
3537-
final double targetWidth = (fallbackContentMaxWidth != null && fallbackContentMaxWidth > 0)
3538-
? fallbackContentMaxWidth
3539-
: paragraph.longestLine;
3549+
final double targetWidth = outOfFlowShrinkToFit
3550+
? paragraph.longestLine
3551+
: (fallbackContentMaxWidth != null && fallbackContentMaxWidth > 0)
3552+
? fallbackContentMaxWidth
3553+
: paragraph.longestLine;
35403554
paragraph.layout(ui.ParagraphConstraints(width: targetWidth));
35413555
} else if (constraints.maxWidth <= 0) {
35423556
if (!shapedWithZeroWidth) {
3543-
final double targetWidth = (fallbackContentMaxWidth != null && fallbackContentMaxWidth > 0)
3544-
? fallbackContentMaxWidth
3545-
: paragraph.longestLine;
3557+
final double targetWidth = outOfFlowShrinkToFit
3558+
? paragraph.longestLine
3559+
: (fallbackContentMaxWidth != null && fallbackContentMaxWidth > 0)
3560+
? fallbackContentMaxWidth
3561+
: paragraph.longestLine;
35463562
paragraph.layout(ui.ParagraphConstraints(width: targetWidth));
35473563
}
35483564
}

webf/test/src/rendering/inline_formatting_context_test.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart';
77
import 'package:webf/webf.dart';
88
import 'package:webf/dom.dart' as dom;
99
import 'package:webf/css.dart';
10+
import 'package:webf/rendering.dart';
1011
import '../../setup.dart';
1112
import '../widget/test_utils.dart';
1213

@@ -186,6 +187,40 @@ void main() {
186187
expect(btn.attachedRenderer!.size.width, lessThan(host.attachedRenderer!.size.width));
187188
});
188189

190+
testWidgets('should paint RTL abspos inline text at origin in shrink-to-fit boxes', (WidgetTester tester) async {
191+
final prepared = await WebFWidgetTestUtils.prepareWidgetTest(
192+
tester: tester,
193+
controllerName: 'rtl-abspos-shrink-to-fit-${DateTime.now().millisecondsSinceEpoch}',
194+
html: '''
195+
<div id="card" style="position: relative; width: 343px; padding: 40px 24px; overflow-x: hidden; direction: rtl; background: #eee;">
196+
<div id="badge" style="position: absolute; inset-inline-start: 0; top: 0; background: rgb(248 113 113); padding: 4px 12px; border-radius: 16px 0 0 8px;">
197+
認證商家
198+
</div>
199+
</div>
200+
''',
201+
);
202+
203+
final controller = prepared.controller;
204+
await tester.pump();
205+
206+
final badge = controller.view.document.getElementById(['badge']) as dom.Element;
207+
final renderer = badge.attachedRenderer!;
208+
expect(renderer, isA<RenderFlowLayout>());
209+
210+
final flow = renderer as RenderFlowLayout;
211+
expect(flow.establishIFC, isTrue);
212+
final ifc = flow.inlineFormattingContext;
213+
expect(ifc, isNotNull);
214+
215+
final lines = ifc!.paragraphLineMetrics;
216+
expect(lines, isNotEmpty);
217+
218+
// In shrink-to-fit abspos containers under RTL, the paragraph should be
219+
// laid out to intrinsic width so the line's left is near 0 (not shifted
220+
// far right by an ancestor constraint width).
221+
expect(lines.first.left.abs(), lessThan(1.0));
222+
});
223+
189224
testWidgets('should handle nested inline elements', (WidgetTester tester) async {
190225
final prepared = await WebFWidgetTestUtils.prepareWidgetTest(
191226
tester: tester,

0 commit comments

Comments
 (0)