Skip to content

Commit 1f84b91

Browse files
author
Vitali Prudnikovich
committed
Split flex container renderer
DEVSIX-6658 (cherry picked from commit 06c990e)
1 parent d9a9674 commit 1f84b91

13 files changed

+283
-26
lines changed

layout/src/main/java/com/itextpdf/layout/renderer/FlexContainerRenderer.java

Lines changed: 105 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ public LayoutResult layout(LayoutContext layoutContext) {
136136
UnitValue.createPointValue(rectangleWithoutBordersMarginsPaddings.getHeight()));
137137
}
138138
}
139+
139140
final LayoutResult result = super.layout(layoutContext);
140141

141142
// We must set back widths of the children because multiple layouts are possible
@@ -195,17 +196,25 @@ AbstractRenderer[] createSplitAndOverflowRenderers(int childPos, int layoutStatu
195196
List<IRenderer> waitingOverflowFloatRenderers) {
196197
final AbstractRenderer splitRenderer = createSplitRenderer(layoutStatus);
197198
final AbstractRenderer overflowRenderer = createOverflowRenderer(layoutStatus);
199+
198200
final IRenderer childRenderer = getChildRenderers().get(childPos);
199201
final boolean forcedPlacement = Boolean.TRUE.equals(this.<Boolean>getProperty(Property.FORCED_PLACEMENT));
200202
boolean metChildRenderer = false;
201203
for (final List<FlexItemInfo> line : lines) {
202-
metChildRenderer = metChildRenderer ||
203-
line.stream().anyMatch(flexItem -> flexItem.getRenderer() == childRenderer);
204-
for (final FlexItemInfo itemInfo : line) {
205-
if (metChildRenderer && !forcedPlacement) {
206-
overflowRenderer.addChildRenderer(itemInfo.getRenderer());
207-
} else {
208-
splitRenderer.addChildRenderer(itemInfo.getRenderer());
204+
final boolean isSplitLine = line.stream().anyMatch(flexItem -> flexItem.getRenderer() == childRenderer);
205+
metChildRenderer = metChildRenderer || isSplitLine;
206+
207+
// If the renderer to split is in the current line
208+
if (isSplitLine && !forcedPlacement && layoutStatus == LayoutResult.PARTIAL) {
209+
fillSplitOverflowRenderersForPartialResult(splitRenderer, overflowRenderer, line, childRenderer,
210+
childResult);
211+
} else {
212+
for (final FlexItemInfo itemInfo : line) {
213+
if (metChildRenderer && !forcedPlacement) {
214+
overflowRenderer.addChildRenderer(itemInfo.getRenderer());
215+
} else {
216+
splitRenderer.addChildRenderer(itemInfo.getRenderer());
217+
}
209218
}
210219
}
211220
}
@@ -224,22 +233,30 @@ LayoutResult processNotFullChildResult(LayoutContext layoutContext,
224233
List<Rectangle> areas, int currentAreaPos, Rectangle layoutBox,
225234
Set<Rectangle> nonChildFloatingRendererAreas, IRenderer causeOfNothing,
226235
boolean anythingPlaced, int childPos, LayoutResult result) {
227-
228236
final boolean keepTogether = isKeepTogether(causeOfNothing);
237+
if (Boolean.TRUE.equals(getPropertyAsBoolean(Property.FORCED_PLACEMENT)) || wasHeightClipped) {
238+
final AbstractRenderer splitRenderer = keepTogether ? null : createSplitRenderer(result.getStatus());
239+
if (splitRenderer != null) {
240+
splitRenderer.setChildRenderers(getChildRenderers());
241+
}
242+
243+
return new LayoutResult(LayoutResult.FULL,
244+
getOccupiedAreaInCaseNothingWasWrappedWithFull(result, splitRenderer), splitRenderer, null, null);
245+
}
229246

230247
final AbstractRenderer[] splitAndOverflowRenderers = createSplitAndOverflowRenderers(
231248
childPos, result.getStatus(), result, waitingFloatsSplitRenderers, waitingOverflowFloatRenderers);
232249

233250
AbstractRenderer splitRenderer = splitAndOverflowRenderers[0];
234251
final AbstractRenderer overflowRenderer = splitAndOverflowRenderers[1];
235252
overflowRenderer.deleteOwnProperty(Property.FORCED_PLACEMENT);
253+
updateHeightsOnSplit(wasHeightClipped, splitRenderer, overflowRenderer);
236254

237255
if (isRelativePosition() && !positionedRenderers.isEmpty()) {
238256
overflowRenderer.positionedRenderers = new ArrayList<>(positionedRenderers);
239257
}
240258

241259
// TODO DEVSIX-5086 When flex-wrap will be fully supported we'll need to update height on split
242-
243260
if (keepTogether) {
244261
splitRenderer = null;
245262
overflowRenderer.setChildRenderers(getChildRenderers());
@@ -249,23 +266,15 @@ LayoutResult processNotFullChildResult(LayoutContext layoutContext,
249266

250267
applyAbsolutePositionIfNeeded(layoutContext);
251268

252-
if (Boolean.TRUE.equals(getPropertyAsBoolean(Property.FORCED_PLACEMENT)) || wasHeightClipped) {
253-
if (splitRenderer != null) {
254-
splitRenderer.setChildRenderers(getChildRenderers());
255-
}
256-
return new LayoutResult(LayoutResult.FULL,
257-
getOccupiedAreaInCaseNothingWasWrappedWithFull(result, splitRenderer), splitRenderer, null, null);
269+
applyPaddings(occupiedArea.getBBox(), paddings, true);
270+
applyBorderBox(occupiedArea.getBBox(), borders, true);
271+
applyMargins(occupiedArea.getBBox(), true);
272+
if (splitRenderer == null || splitRenderer.getChildRenderers().isEmpty()) {
273+
return new LayoutResult(LayoutResult.NOTHING, null, null, overflowRenderer,
274+
result.getCauseOfNothing()).setAreaBreak(result.getAreaBreak());
258275
} else {
259-
applyPaddings(occupiedArea.getBBox(), paddings, true);
260-
applyBorderBox(occupiedArea.getBBox(), borders, true);
261-
applyMargins(occupiedArea.getBBox(), true);
262-
if (splitRenderer == null || splitRenderer.getChildRenderers().isEmpty()) {
263-
return new LayoutResult(LayoutResult.NOTHING, null, null, overflowRenderer,
264-
result.getCauseOfNothing()).setAreaBreak(result.getAreaBreak());
265-
} else {
266-
return new LayoutResult(LayoutResult.PARTIAL, layoutContext.getArea(), splitRenderer,
267-
overflowRenderer, null).setAreaBreak(result.getAreaBreak());
268-
}
276+
return new LayoutResult(LayoutResult.PARTIAL, layoutContext.getArea(), splitRenderer,
277+
overflowRenderer, null).setAreaBreak(result.getAreaBreak());
269278
}
270279
}
271280

@@ -354,7 +363,7 @@ private FlexItemInfo findFlexItemInfo(AbstractRenderer renderer) {
354363
}
355364
return null;
356365
}
357-
366+
358367
@Override
359368
void fixOccupiedAreaIfOverflowedX(OverflowPropertyValue overflowX, Rectangle layoutBox) {
360369
// TODO DEVSIX-5087 Support overflow visible/hidden property correctly
@@ -373,6 +382,71 @@ public void addChild(IRenderer renderer) {
373382
super.addChild(renderer);
374383
}
375384

385+
private void fillSplitOverflowRenderersForPartialResult(AbstractRenderer splitRenderer,
386+
AbstractRenderer overflowRenderer, List<FlexItemInfo> line, IRenderer childRenderer,
387+
LayoutResult childResult) {
388+
// If we split, we remove (override) Property.ALIGN_ITEMS for the overflow renderer.
389+
// because we have to layout the remaining part at the top of the layout context.
390+
// TODO DEVSIX-5086 When flex-wrap will be fully supported we'll need to reconsider this.
391+
// The question is what should be set/calculated for the next line
392+
overflowRenderer.setProperty(Property.ALIGN_ITEMS, null);
393+
394+
float occupiedSpace = 0;
395+
boolean metChildRendererInLine = false;
396+
for (final FlexItemInfo itemInfo : line) {
397+
// Split the line
398+
if (itemInfo.getRenderer() == childRenderer) {
399+
metChildRendererInLine = true;
400+
if (childResult.getSplitRenderer() != null) {
401+
splitRenderer.addChildRenderer(childResult.getSplitRenderer());
402+
}
403+
404+
if (childResult.getOverflowRenderer() != null) {
405+
overflowRenderer.addChildRenderer(childResult.getOverflowRenderer());
406+
}
407+
} else if (metChildRendererInLine) {
408+
// Process all following renderers in the current line
409+
// We have to layout them to understand what goes where
410+
final Rectangle neighbourBbox = getOccupiedAreaBBox().clone();
411+
// Move bbox by occupied space
412+
neighbourBbox.setX(neighbourBbox.getX() + occupiedSpace);
413+
neighbourBbox.setWidth(itemInfo.getRectangle().getWidth());
414+
415+
// Y of the renderer has been already calculated, move bbox accordingly
416+
neighbourBbox.setY(neighbourBbox.getY() - itemInfo.getRectangle().getY());
417+
418+
final LayoutResult neighbourLayoutResult = itemInfo.getRenderer().layout(new LayoutContext(
419+
new LayoutArea(childResult.getOccupiedArea().getPageNumber(), neighbourBbox)));
420+
// Handle result
421+
if (neighbourLayoutResult.getStatus() == LayoutResult.PARTIAL &&
422+
neighbourLayoutResult.getSplitRenderer() != null) {
423+
splitRenderer.addChildRenderer(neighbourLayoutResult.getSplitRenderer());
424+
} else if (neighbourLayoutResult.getStatus() == LayoutResult.FULL) {
425+
splitRenderer.addChildRenderer(itemInfo.getRenderer());
426+
} else {
427+
// LayoutResult.NOTHING
428+
}
429+
430+
if (neighbourLayoutResult.getOverflowRenderer() != null) {
431+
overflowRenderer.addChildRenderer(neighbourLayoutResult.getOverflowRenderer());
432+
} else {
433+
// Here we might need to still occupy the space on overflow renderer
434+
addSimulateDiv(overflowRenderer, itemInfo.getRectangle().getWidth());
435+
}
436+
} else {
437+
// Process all preceeding renderers in the current line
438+
// They all were layouted as FULL so add them into split renderer
439+
splitRenderer.addChildRenderer(itemInfo.getRenderer());
440+
441+
// But we also need to occupy the space on overflow renderer
442+
addSimulateDiv(overflowRenderer, itemInfo.getRectangle().getWidth());
443+
}
444+
445+
// X is nonzero only for the 1st renderer in line serving for alignment adjustments
446+
occupiedSpace += itemInfo.getRectangle().getX() + itemInfo.getRectangle().getWidth();
447+
}
448+
}
449+
376450
private void findMinMaxWidthIfCorrespondingPropertiesAreNotSet(MinMaxWidth minMaxWidth,
377451
AbstractWidthHandler minMaxWidthHandler) {
378452
// TODO DEVSIX-5086 When flex-wrap will be fully supported we'll find min/max width with respect to the lines
@@ -390,4 +464,9 @@ private void findMinMaxWidthIfCorrespondingPropertiesAreNotSet(MinMaxWidth minMa
390464
}
391465
}
392466

467+
private static void addSimulateDiv(AbstractRenderer overflowRenderer, float width) {
468+
final IRenderer fakeOverflowRenderer = new DivRenderer(
469+
new Div().setMinWidth(width).setMaxWidth(width));
470+
overflowRenderer.addChildRenderer(fakeOverflowRenderer);
471+
}
393472
}
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2022 iText Group NV
4+
Authors: iText Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.layout.element;
24+
25+
import com.itextpdf.kernel.colors.ColorConstants;
26+
import com.itextpdf.kernel.geom.PageSize;
27+
import com.itextpdf.kernel.pdf.PdfDocument;
28+
import com.itextpdf.kernel.pdf.PdfWriter;
29+
import com.itextpdf.kernel.utils.CompareTool;
30+
import com.itextpdf.layout.Document;
31+
import com.itextpdf.layout.borders.SolidBorder;
32+
import com.itextpdf.layout.logs.LayoutLogMessageConstant;
33+
import com.itextpdf.layout.properties.Background;
34+
import com.itextpdf.layout.properties.Property;
35+
import com.itextpdf.layout.properties.UnitValue;
36+
import com.itextpdf.test.ExtendedITextTest;
37+
import com.itextpdf.test.annotations.LogMessage;
38+
import com.itextpdf.test.annotations.LogMessages;
39+
import com.itextpdf.test.annotations.type.IntegrationTest;
40+
41+
import java.io.IOException;
42+
import org.junit.Assert;
43+
import org.junit.BeforeClass;
44+
import org.junit.Test;
45+
import org.junit.experimental.categories.Category;
46+
47+
@Category(IntegrationTest.class)
48+
public class FlexContainerSplitTest extends ExtendedITextTest {
49+
50+
private static final String SOURCE_FOLDER = "./src/test/resources/com/itextpdf/layout/FlexContainerSplitTest/";
51+
private static final String DESTINATION_FOLDER = "./target/test/com/itextpdf/layout/FlexContainerSplitTest/";
52+
53+
private static final String VERY_LONG_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do "
54+
+ "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud "
55+
+ "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in "
56+
+ "reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat "
57+
+ "cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ";
58+
private static final String SHORT_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit,?";
59+
60+
@BeforeClass
61+
public static void beforeClass() {
62+
createDestinationFolder(DESTINATION_FOLDER);
63+
}
64+
65+
@Test
66+
public void simpleTest() throws IOException, InterruptedException {
67+
String outFileName = DESTINATION_FOLDER + "simpleTest.pdf";
68+
String cmpFileName = SOURCE_FOLDER + "cmp_simpleTest.pdf";
69+
try (PdfDocument pdfDocument = new PdfDocument(new PdfWriter(outFileName))) {
70+
Document document = new Document(pdfDocument);
71+
pdfDocument.setDefaultPageSize(PageSize.A5);
72+
73+
Div flexContainer = createDefaultFlexContainer();
74+
document.add(flexContainer);
75+
}
76+
77+
Assert.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, DESTINATION_FOLDER, "diff"));
78+
}
79+
80+
@Test
81+
public void heightPropertyTest() throws IOException, InterruptedException {
82+
String outFileName = DESTINATION_FOLDER + "heightPropertyTest.pdf";
83+
String cmpFileName = SOURCE_FOLDER + "cmp_heightPropertyTest.pdf";
84+
85+
try (PdfDocument pdfDocument = new PdfDocument(new PdfWriter(outFileName))) {
86+
Document document = new Document(pdfDocument);
87+
pdfDocument.setDefaultPageSize(PageSize.A5);
88+
89+
Div flexContainer = createDefaultFlexContainer();
90+
((Paragraph) flexContainer.getChildren().get(0)).setHeight(250);
91+
document.add(flexContainer);
92+
}
93+
94+
Assert.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, DESTINATION_FOLDER, "diff"));
95+
}
96+
97+
@Test
98+
public void smallTrailingElementTest() throws IOException, InterruptedException {
99+
String outFileName = DESTINATION_FOLDER + "smallTrailingElementTest.pdf";
100+
String cmpFileName = SOURCE_FOLDER + "cmp_smallTrailingElementTest.pdf";
101+
102+
try (PdfDocument pdfDocument = new PdfDocument(new PdfWriter(outFileName))) {
103+
Document document = new Document(pdfDocument);
104+
pdfDocument.setDefaultPageSize(PageSize.A5);
105+
106+
Div flexContainer = createDefaultFlexContainer();
107+
((Paragraph) flexContainer.getChildren().get(0)).setHeight(250);
108+
Paragraph p3 = new Paragraph(SHORT_TEXT)
109+
.setWidth(UnitValue.createPercentValue(25))
110+
.setBackgroundColor(ColorConstants.BLUE)
111+
.setHeight(250);
112+
flexContainer.add(p3);
113+
document.add(flexContainer);
114+
}
115+
116+
Assert.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, DESTINATION_FOLDER, "diff"));
117+
}
118+
119+
@Test
120+
public void splitOverSeveralPagesTest() throws IOException, InterruptedException {
121+
String outFileName = DESTINATION_FOLDER + "splitOverSeveralPagesTest.pdf";
122+
String cmpFileName = SOURCE_FOLDER + "cmp_splitOverSeveralPagesTest.pdf";
123+
124+
try (PdfDocument pdfDocument = new PdfDocument(new PdfWriter(outFileName))) {
125+
Document document = new Document(pdfDocument);
126+
pdfDocument.setDefaultPageSize(PageSize.A6);
127+
128+
Div flexContainer = createDefaultFlexContainer();
129+
document.add(flexContainer);
130+
}
131+
132+
Assert.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, DESTINATION_FOLDER, "diff"));
133+
}
134+
135+
@Test
136+
@LogMessages(messages = @LogMessage(messageTemplate = LayoutLogMessageConstant.ELEMENT_DOES_NOT_FIT_AREA))
137+
public void keepTogetherIgnoredTest() throws IOException, InterruptedException {
138+
String outFileName = DESTINATION_FOLDER + "keepTogetherIgnoredTest.pdf";
139+
String cmpFileName = SOURCE_FOLDER + "cmp_keepTogetherIgnoredTest.pdf";
140+
141+
try (PdfDocument pdfDocument = new PdfDocument(new PdfWriter(outFileName))) {
142+
Document document = new Document(pdfDocument);
143+
pdfDocument.setDefaultPageSize(PageSize.A5);
144+
145+
Div flexContainer = new FlexContainer();
146+
flexContainer.add(new Div().setWidth(50).setHeight(600).setBackgroundColor(ColorConstants.YELLOW))
147+
.add(new Div().setWidth(50).setHeight(400).setBackgroundColor(ColorConstants.BLUE));
148+
flexContainer.setProperty(Property.KEEP_TOGETHER, true);
149+
document.add(flexContainer);
150+
}
151+
152+
Assert.assertNull(new CompareTool().compareByContent(outFileName, cmpFileName, DESTINATION_FOLDER, "diff"));
153+
}
154+
155+
private Div createDefaultFlexContainer() {
156+
Div flexContainer = new FlexContainer();
157+
flexContainer.setProperty(Property.BORDER, new SolidBorder(2));
158+
flexContainer.setProperty(Property.BACKGROUND, new Background(ColorConstants.LIGHT_GRAY));
159+
Paragraph p1 = new Paragraph(SHORT_TEXT)
160+
.setWidth(UnitValue.createPercentValue(25))
161+
.setBackgroundColor(ColorConstants.BLUE);
162+
p1.setProperty(Property.FLEX_GROW, 0f);
163+
p1.setProperty(Property.FLEX_SHRINK, 0f);
164+
flexContainer.add(p1);
165+
166+
Paragraph p2 = new Paragraph(VERY_LONG_TEXT + VERY_LONG_TEXT + VERY_LONG_TEXT + VERY_LONG_TEXT)
167+
.setWidth(UnitValue.createPercentValue(75))
168+
.setBackgroundColor(ColorConstants.YELLOW);
169+
p2.setProperty(Property.FLEX_GROW, 1f);
170+
p2.setProperty(Property.FLEX_SHRINK, 1f);
171+
flexContainer.add(p2);
172+
173+
return flexContainer;
174+
}
175+
}

0 commit comments

Comments
 (0)