Skip to content

Commit b9b9067

Browse files
committed
Implement overflowing of GridContainerRenderer to the next page
DEVSIX-8340
1 parent 3193c19 commit b9b9067

25 files changed

+281
-36
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ class GridCell {
3737
private final int spanRow;
3838
private final Rectangle layoutArea = new Rectangle(0.0f, 0.0f, 0.0f,0.0f);
3939

40+
/**
41+
* Cached track sizes for rows to use them during split.
42+
*/
43+
private float[] rowSizes;
44+
4045
/**
4146
* Create a grid cell and init value renderer position on a grid based on its properties.
4247
*
@@ -116,6 +121,14 @@ void setPos(int y, int x) {
116121
this.gridX = x;
117122
}
118123

124+
float[] getRowSizes() {
125+
return this.rowSizes;
126+
}
127+
128+
void setRowSizes(float[] rowSizes) {
129+
this.rowSizes = rowSizes;
130+
}
131+
119132
/**
120133
* Init axis placement values
121134
* if start > end values are swapped

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

Lines changed: 100 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ This file is part of the iText (R) project.
4141
/**
4242
* Represents a renderer for a grid.
4343
*/
44-
public class GridContainerRenderer extends DivRenderer {
44+
public class GridContainerRenderer extends BlockRenderer {
4545
private boolean isFirstLayout = true;
4646

4747
/**
@@ -110,7 +110,33 @@ public LayoutResult layout(LayoutContext layoutContext) {
110110
public void addChild(IRenderer renderer) {
111111
renderer.setProperty(Property.OVERFLOW_X, OverflowPropertyValue.VISIBLE);
112112
renderer.setProperty(Property.OVERFLOW_Y, OverflowPropertyValue.VISIBLE);
113-
super.addChild(renderer);
113+
renderer.setProperty(Property.COLLAPSING_MARGINS, determineCollapsingMargins(renderer));
114+
115+
GridItemRenderer itemRenderer = new GridItemRenderer();
116+
itemRenderer.setProperty(Property.COLLAPSING_MARGINS, Boolean.FALSE);
117+
itemRenderer.addChild(renderer);
118+
119+
super.addChild(itemRenderer);
120+
}
121+
122+
/**
123+
* Calculates collapsing margins value. It's based on browser behavior.
124+
* Always returning true somehow also almost works.
125+
*/
126+
private static Boolean determineCollapsingMargins(IRenderer renderer) {
127+
IRenderer currentRenderer = renderer;
128+
while (!currentRenderer.getChildRenderers().isEmpty()) {
129+
if (currentRenderer.getChildRenderers().size() > 1) {
130+
return Boolean.TRUE;
131+
} else {
132+
currentRenderer = currentRenderer.getChildRenderers().get(0);
133+
}
134+
}
135+
if (currentRenderer instanceof TableRenderer) {
136+
return Boolean.TRUE;
137+
}
138+
139+
return Boolean.FALSE;
114140
}
115141

116142
private AbstractRenderer createSplitRenderer(List<IRenderer> children) {
@@ -126,12 +152,13 @@ private AbstractRenderer createSplitRenderer(List<IRenderer> children) {
126152
}
127153

128154
private AbstractRenderer createOverflowRenderer(List<IRenderer> children) {
129-
// TODO DEVSIX-8340 - We put the original amount of rows into overflow container.
130155
GridContainerRenderer overflowRenderer = (GridContainerRenderer) getNextRenderer();
131156
overflowRenderer.isFirstLayout = false;
132157
overflowRenderer.parent = parent;
133158
overflowRenderer.modelElement = modelElement;
134159
overflowRenderer.addAllProperties(getOwnProperties());
160+
overflowRenderer.setProperty(Property.GRID_TEMPLATE_ROWS, null);
161+
overflowRenderer.setProperty(Property.GRID_AUTO_ROWS, null);
135162
overflowRenderer.setChildRenderers(children);
136163
ContinuousContainer.clearPropertiesFromOverFlowRenderer(overflowRenderer);
137164
return overflowRenderer;
@@ -141,54 +168,91 @@ private AbstractRenderer createOverflowRenderer(List<IRenderer> children) {
141168
private GridLayoutResult layoutGrid(LayoutContext layoutContext, Rectangle actualBBox, Grid grid) {
142169
GridLayoutResult layoutResult = new GridLayoutResult();
143170

171+
int notLayoutedRow = grid.getNumberOfRows();
144172
for (GridCell cell : grid.getUniqueGridCells(Grid.GridOrder.ROW)) {
145173
// Calculate cell layout context by getting actual x and y on parent layout area for it
146174
LayoutContext cellContext = getCellLayoutContext(layoutContext, actualBBox, cell);
147-
175+
Rectangle cellBBox = cellContext.getArea().getBBox();
148176
IRenderer cellToRender = cell.getValue();
149-
cellToRender.setProperty(Property.COLLAPSING_MARGINS, Boolean.FALSE);
150177

151178
// Now set the height for the individual items
152179
// We know cell height upfront and this way we tell the element what it can occupy
153-
Rectangle cellBBox = cellContext.getArea().getBBox();
154-
if (!cellToRender.hasProperty(Property.HEIGHT)) {
155-
final Rectangle rectangleWithoutBordersMarginsPaddings = cellBBox.clone();
156-
if (cellToRender instanceof AbstractRenderer) {
157-
final AbstractRenderer abstractCellRenderer = ((AbstractRenderer) cellToRender);
158-
// We subtract margins/borders/paddings because we should take into account that
159-
// borders/paddings/margins should also fit into a cell.
160-
if (AbstractRenderer.isBorderBoxSizing(cellToRender)) {
161-
abstractCellRenderer.applyMargins(rectangleWithoutBordersMarginsPaddings, false);
162-
} else {
163-
abstractCellRenderer.applyMarginsBordersPaddings(rectangleWithoutBordersMarginsPaddings, false);
164-
}
165-
}
180+
final float itemHeight = ((GridItemRenderer) cellToRender).calculateHeight(cellBBox.getHeight());
166181

167-
cellToRender.setProperty(Property.HEIGHT,
168-
UnitValue.createPointValue(rectangleWithoutBordersMarginsPaddings.getHeight()));
169-
}
182+
cellToRender.setProperty(Property.HEIGHT, UnitValue.createPointValue(itemHeight));
170183

171184
// Adjust cell BBox to the remaining part of the layout bbox
172185
// This way we can layout elements partially
173186
cellBBox.setHeight(cellBBox.getTop() - actualBBox.getBottom())
174187
.setY(actualBBox.getY());
175188

189+
cellToRender.setProperty(Property.FILL_AVAILABLE_AREA, Boolean.TRUE);
176190
LayoutResult cellResult = cellToRender.layout(cellContext);
191+
notLayoutedRow = Math.min(notLayoutedRow, processLayoutResult(layoutResult, cell, cellResult));
192+
}
177193

178-
if (cellResult.getStatus() == LayoutResult.NOTHING) {
179-
layoutResult.getOverflowRenderers().add(cellToRender);
180-
layoutResult.getCauseOfNothing().add(cellResult.getCauseOfNothing());
181-
} else {
182-
// PARTIAL + FULL result handling
183-
layoutResult.getSplitRenderers().add(cellToRender);
184-
if (cellResult.getStatus() == LayoutResult.PARTIAL) {
185-
layoutResult.getOverflowRenderers().add(cellResult.getOverflowRenderer());
186-
}
194+
for (IRenderer overflowRenderer : layoutResult.getOverflowRenderers()) {
195+
if (overflowRenderer.<Integer>getProperty(Property.GRID_ROW_START) != null) {
196+
overflowRenderer.setProperty(Property.GRID_ROW_START,
197+
(int) overflowRenderer.<Integer>getProperty(Property.GRID_ROW_START) - notLayoutedRow);
198+
overflowRenderer.setProperty(Property.GRID_ROW_END,
199+
(int) overflowRenderer.<Integer>getProperty(Property.GRID_ROW_END) - notLayoutedRow);
187200
}
188201
}
202+
189203
return layoutResult;
190204
}
191205

206+
private static int processLayoutResult(GridLayoutResult layoutResult, GridCell cell, LayoutResult cellResult) {
207+
IRenderer cellToRenderer = cell.getValue();
208+
if (cellResult.getStatus() == LayoutResult.NOTHING) {
209+
cellToRenderer.setProperty(Property.GRID_COLUMN_START, cell.getColumnStart() + 1);
210+
cellToRenderer.setProperty(Property.GRID_COLUMN_END, cell.getColumnEnd() + 1);
211+
cellToRenderer.setProperty(Property.GRID_ROW_START, cell.getRowStart() + 1);
212+
cellToRenderer.setProperty(Property.GRID_ROW_END, cell.getRowEnd() + 1);
213+
layoutResult.getOverflowRenderers().add(cellToRenderer);
214+
layoutResult.getCauseOfNothing().add(cellResult.getCauseOfNothing());
215+
216+
return cell.getRowStart();
217+
}
218+
219+
// PARTIAL + FULL result handling
220+
layoutResult.getSplitRenderers().add(cellToRenderer);
221+
if (cellResult.getStatus() == LayoutResult.PARTIAL) {
222+
IRenderer overflowRenderer = cellResult.getOverflowRenderer();
223+
overflowRenderer.setProperty(Property.GRID_COLUMN_START, cell.getColumnStart() + 1);
224+
overflowRenderer.setProperty(Property.GRID_COLUMN_END, cell.getColumnEnd() + 1);
225+
int rowStart = cell.getRowStart() + 1;
226+
int rowEnd = cell.getRowEnd() + 1;
227+
layoutResult.getOverflowRenderers().add(overflowRenderer);
228+
// Now let's find out where we split exactly
229+
float accumulatedRowSize = 0;
230+
final float layoutedHeight = cellResult.getOccupiedArea().getBBox().getHeight();
231+
int notLayoutedRow = rowStart - 1;
232+
for (int i = 0; i < cell.getRowSizes().length; ++i) {
233+
accumulatedRowSize += cell.getRowSizes()[i];
234+
if (accumulatedRowSize < layoutedHeight) {
235+
++rowStart;
236+
++notLayoutedRow;
237+
} else {
238+
break;
239+
}
240+
}
241+
242+
// We don't know what to do if rowStart is equal or more than rowEnd
243+
// Let's not try to guess by just take the 1st available space in a column
244+
// by leaving nulls for grid-row-start/end
245+
if (rowEnd > rowStart) {
246+
overflowRenderer.setProperty(Property.GRID_ROW_START, rowStart);
247+
overflowRenderer.setProperty(Property.GRID_ROW_END, rowEnd);
248+
}
249+
250+
return notLayoutedRow;
251+
}
252+
253+
return Integer.MAX_VALUE;
254+
}
255+
192256
//Init cell layout context based on a parent context and calculated cell layout area from grid sizing algorithm.
193257
private static LayoutContext getCellLayoutContext(LayoutContext layoutContext, Rectangle actualBBox, GridCell cell) {
194258
LayoutArea tempArea = layoutContext.getArea().clone();
@@ -209,10 +273,12 @@ private static LayoutContext getCellLayoutContext(LayoutContext layoutContext, R
209273
private LayoutArea calculateContainerOccupiedArea(LayoutContext layoutContext, Grid grid, boolean isFull) {
210274
LayoutArea area = layoutContext.getArea().clone();
211275
final float totalHeight = updateOccupiedHeight(grid.getHeight(), isFull);
212-
area.getBBox().setHeight(totalHeight);
213-
final Rectangle initialBBox = layoutContext.getArea().getBBox();
214-
area.getBBox().setY(initialBBox.getY() + initialBBox.getHeight() - area.getBBox().getHeight());
215-
recalculateHeightAndWidthAfterLayout(area.getBBox(), isFull);
276+
if (totalHeight < area.getBBox().getHeight() || isFull) {
277+
area.getBBox().setHeight(totalHeight);
278+
final Rectangle initialBBox = layoutContext.getArea().getBBox();
279+
area.getBBox().setY(initialBBox.getY() + initialBBox.getHeight() - area.getBBox().getHeight());
280+
recalculateHeightAndWidthAfterLayout(area.getBBox(), isFull);
281+
}
216282
return area;
217283
}
218284

@@ -300,7 +366,6 @@ private static Grid constructGrid(GridContainerRenderer renderer, Rectangle actu
300366
return grid;
301367
}
302368

303-
304369
private final static class GridLayoutResult {
305370
private final List<IRenderer> splitRenderers = new ArrayList<>();
306371
private final List<IRenderer> overflowRenderers = new ArrayList<>();
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2024 Apryse Group NV
4+
Authors: Apryse 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.renderer;
24+
25+
import com.itextpdf.kernel.geom.Rectangle;
26+
import com.itextpdf.layout.element.Div;
27+
import com.itextpdf.layout.properties.Property;
28+
29+
/**
30+
* Wrapper renderer around grid item. It's expected there is always exactly 1 child renderer.
31+
*/
32+
class GridItemRenderer extends BlockRenderer {
33+
34+
/**
35+
* A renderer to wrap.
36+
*/
37+
AbstractRenderer renderer;
38+
39+
/**
40+
* Flag saying that we updated height of the renderer we wrap.
41+
* It allows to remove that property on split.
42+
*/
43+
private boolean heightSet = false;
44+
45+
GridItemRenderer() {
46+
super(new Div());
47+
}
48+
49+
/**
50+
* {@inheritDoc}
51+
*/
52+
@Override
53+
public void addChild(IRenderer renderer) {
54+
this.renderer = (AbstractRenderer) renderer;
55+
super.addChild(renderer);
56+
}
57+
58+
/**
59+
* {@inheritDoc}
60+
*/
61+
@Override
62+
public IRenderer getNextRenderer() {
63+
logWarningIfGetNextRendererNotOverridden(GridItemRenderer.class, this.getClass());
64+
return new GridItemRenderer();
65+
}
66+
67+
/**
68+
* {@inheritDoc}
69+
*/
70+
@Override
71+
public <T1> T1 getProperty(int key) {
72+
// Handle only the props we are aware of
73+
switch (key) {
74+
case Property.GRID_COLUMN_START:
75+
case Property.GRID_COLUMN_END:
76+
case Property.GRID_COLUMN_SPAN:
77+
case Property.GRID_ROW_START:
78+
case Property.GRID_ROW_END:
79+
case Property.GRID_ROW_SPAN:
80+
return renderer.<T1>getProperty(key);
81+
82+
default:
83+
break;
84+
}
85+
86+
return super.<T1>getProperty(key);
87+
}
88+
89+
/**
90+
* {@inheritDoc}
91+
*/
92+
@Override
93+
public void setProperty(int property, Object value) {
94+
// Handle only the props we are aware of
95+
switch (property) {
96+
case Property.HEIGHT:
97+
if (!renderer.hasProperty(property) || heightSet) {
98+
renderer.setProperty(Property.HEIGHT, value);
99+
renderer.setProperty(Property.MIN_HEIGHT, value);
100+
heightSet = true;
101+
}
102+
break;
103+
104+
case Property.FILL_AVAILABLE_AREA:
105+
case Property.GRID_COLUMN_START:
106+
case Property.GRID_COLUMN_END:
107+
case Property.GRID_COLUMN_SPAN:
108+
case Property.GRID_ROW_START:
109+
case Property.GRID_ROW_END:
110+
case Property.GRID_ROW_SPAN:
111+
renderer.setProperty(property, value);
112+
break;
113+
114+
case Property.COLLAPSING_MARGINS:
115+
super.setProperty(property, value);
116+
break;
117+
118+
default:
119+
break;
120+
}
121+
}
122+
123+
/**
124+
* {@inheritDoc}
125+
*/
126+
@Override
127+
void updateHeightsOnSplit(float usedHeight, boolean wasHeightClipped, AbstractRenderer splitRenderer,
128+
AbstractRenderer overflowRenderer, boolean enlargeOccupiedAreaOnHeightWasClipped) {
129+
// If we set the height ourselves during layout, let's remove it while layouting on the next page
130+
// so that it is recalculated.
131+
if (heightSet) {
132+
// Always 1 child renderer
133+
overflowRenderer.childRenderers.get(0).deleteOwnProperty(Property.HEIGHT);
134+
}
135+
}
136+
137+
/**
138+
* {@inheritDoc}
139+
*/
140+
@Override
141+
void addChildRenderer(IRenderer child) {
142+
this.renderer = (AbstractRenderer) child;
143+
super.addChildRenderer(child);
144+
}
145+
146+
float calculateHeight(float initialHeight) {
147+
// We subtract margins/borders/paddings because we should take into account that
148+
// borders/paddings/margins should also fit into a cell.
149+
final Rectangle rectangle = new Rectangle(0, 0, 0, initialHeight);
150+
if (AbstractRenderer.isBorderBoxSizing(renderer)) {
151+
renderer.applyMargins(rectangle, false);
152+
} else {
153+
renderer.applyMarginsBordersPaddings(rectangle, false);
154+
}
155+
156+
return rectangle.getHeight();
157+
}
158+
}

0 commit comments

Comments
 (0)