LoboEvolution/LoboEvolution

View on GitHub
LoboHTML/src/main/java/org/loboevolution/html/renderer/RBlockViewport.java

Summary

Maintainability
F
3 wks
Test Coverage
/*
 * MIT License
 *
 * Copyright (c) 2014 - 2024 LoboEvolution
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * Contact info: ivan.difrancesco@yahoo.it
 */
package org.loboevolution.html.renderer;

import lombok.extern.slf4j.Slf4j;
import org.loboevolution.common.ArrayUtilities;
import org.loboevolution.common.Strings;
import org.loboevolution.html.HTMLTag;
import org.loboevolution.html.control.RUIControl;
import org.loboevolution.html.control.UIControl;
import org.loboevolution.html.dom.HTMLBodyElement;
import org.loboevolution.html.dom.HTMLDocument;
import org.loboevolution.html.dom.HTMLHtmlElement;
import org.loboevolution.html.dom.nodeimpl.DocumentFragmentImpl;
import org.loboevolution.html.dom.domimpl.HTMLElementImpl;
import org.loboevolution.html.dom.domimpl.HTMLTableElementImpl;
import org.loboevolution.html.dom.domimpl.UINode;
import org.loboevolution.html.dom.nodeimpl.ModelNode;
import org.loboevolution.html.dom.nodeimpl.NodeImpl;
import org.loboevolution.html.dom.nodeimpl.NodeListImpl;
import org.loboevolution.html.node.Node;
import org.loboevolution.css.CSSStyleDeclaration;
import org.loboevolution.html.renderer.RLayout.MiscLayout;
import org.loboevolution.html.renderer.info.RBlockInfo;
import org.loboevolution.html.renderer.info.RLayoutInfo;
import org.loboevolution.html.renderer.table.RTable;
import org.loboevolution.html.renderstate.RenderState;
import org.loboevolution.html.style.HtmlInsets;
import org.loboevolution.gui.HtmlRendererContext;
import org.loboevolution.html.style.HtmlValues;
import org.loboevolution.http.UserAgentContext;
import org.loboevolution.info.FloatingInfo;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.util.List;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A substantial portion of the HTML rendering logic of the package can be found
 * in this class. This class is in charge of laying out the DOM subtree of a
 * node, usually on behalf of an RBlock. It creates a renderer subtree
 * consisting of RLine's or RBlock's. RLine's in turn contain RWord's and so on.
 * This class also happens to be used as an RBlock scrollable viewport.
 */
@Slf4j
public class RBlockViewport extends BaseRCollection {
    
    private static final MarkupLayout miscLayout = new MiscLayout();
    
    private static final SizeExceededException SEE = new SizeExceededException();

    private BoundableRenderable armedRenderable;

    private int availContentHeight; // does not include insets

    private int availContentWidth; // does not include insets

    private int currentCollapsibleMargin;

    private RLine currentLine;

    private int desiredWidth; // includes insets

    private List<ExportableFloat> exportableFloats = null;

    private FloatingBounds floatBounds = null;

    private Boolean isFloatLimit = null;

    private BoundableRenderable lastSeqBlock;

    private int maxX;

    private int maxY;

    private boolean overrideNoWrap;

    private Insets paddingInsets;

    private Collection<RFloatInfo> pendingFloats = null;

    private int positionedOrdinal;

    private SortedSet<PositionedRenderable> positionedRenderables;

    private List<Renderable> seqRenderables = null;

    private boolean sizeOnly;
    
    private int yLimit;
    
    protected final HtmlRendererContext rendererContext;
    
    protected final UserAgentContext userAgentContext;
    
    protected final RenderableContainer container;
    
    protected final FrameContext frameContext;

    private final RBlockInfo info;

    private Integer cachedVisualWidth = null;

    private Integer cachedVisualHeight = null;

    /** Constant ZERO_INSETS */
    public static final Insets ZERO_INSETS = new Insets(0, 0, 0, 0);

    /**
     * Constructs an HtmlBlockLayout.
     *
     * @param info a {@link org.loboevolution.html.renderer.info.RBlockInfo} object.
     */
    public RBlockViewport(final RBlockInfo info, final RenderableContainer container, final RCollection parent) {
        super(container, info.getModelNode());
        this.parent = parent;
        this.userAgentContext = info.getPcontext();
        this.rendererContext = info.getRcontext();
        this.frameContext = info.getFrameContext();
        this.container = container;
        this.layoutUpTreeCanBeInvalidated = true;
        this.info = info;
    }

    private static int getPosition(final HTMLElementImpl element) {
        if(element == null) return RenderState.POSITION_STATIC;
        final RenderState rs = element.getRenderState();
        return rs == null ? RenderState.POSITION_STATIC : rs.getPosition();
    }

    private void addAsSeqBlock(final BoundableRenderable block, final boolean obeysFloats, final boolean informLineDone, final boolean addLine,
                               final boolean centerBlock) {
        final Insets insets = this.paddingInsets;
        final int insetsl = insets.left;
        List<Renderable> sr = this.seqRenderables;
        if (sr == null) {
            sr = new ArrayList<>();
            this.seqRenderables = sr;
        }
        final RLine prevLine = this.currentLine;
        final boolean initialAllowOverflow;
        if (prevLine != null) {
            initialAllowOverflow = prevLine.isAllowOverflow();
            if (informLineDone) {
                lineDone(prevLine);
            }
            if (prevLine.getX() + prevLine.getWidth() > this.maxX) {
                this.maxX = prevLine.getX() + prevLine.getWidth();
            }
            // Check height only with floats.
        } else {
            initialAllowOverflow = false;
        }
        final int prevLineHeight = prevLine == null ? 0 : prevLine.getHeight();
        int newLineY = prevLine == null ? insets.top : prevLine.getY() + prevLineHeight;
        int blockX;
        int blockY = prevLineHeight == 0 ? getNewBlockY(block, newLineY) : newLineY;
        final int blockWidth = block.getWidth();
        if (obeysFloats) {
            // TODO: execution of fetchLeftOffset done twice with positionRElement.
            final FloatingBounds floatBounds = this.floatBounds;
            final int actualAvailWidth;
            if (floatBounds != null) {
                final int blockOffset = fetchLeftOffset(newLineY);
                blockX = blockOffset;
                final int rightOffset = fetchRightOffset(newLineY);
                actualAvailWidth = this.desiredWidth - rightOffset - blockOffset;
                if (blockWidth > actualAvailWidth) {
                    blockY = floatBounds.getClearY(newLineY);
                }
            } else {
                actualAvailWidth = this.availContentWidth;
                blockX = insetsl;
            }
            if (centerBlock) {
                final int roomX = actualAvailWidth - blockWidth;
                if (roomX > 0) {
                    blockX += roomX / 2;
                }
            }
        } else {
            // Block does not obey alignment margins
            blockX = insetsl;
        }
        block.setOrigin(blockX, blockY);
        sr.add(block);
        block.setParent(this);
        if (blockX + blockWidth > this.maxX) {
            this.maxX = blockX + blockWidth;
        }
        this.lastSeqBlock = block;
        this.currentCollapsibleMargin = block instanceof RElement ? ((RElement) block).getMarginBottom() : 0;
        if (addLine) {
            newLineY = blockY + block.getHeight();
            checkY(newLineY);
            final int leftOffset = fetchLeftOffset(newLineY);
            final int newMaxWidth = this.desiredWidth - fetchRightOffset(newLineY) - leftOffset;
            final ModelNode lineNode = block.getModelNode().getParentModelNode();
            final RLine newLine = new RLine(lineNode, this.container, leftOffset, newLineY, newMaxWidth, 0,
                    initialAllowOverflow);
            newLine.setParent(this);
            sr.add(newLine);
            this.currentLine = newLine;
        }
    }

    private void addAsSeqBlock(final RElement block) {
        this.addAsSeqBlock(block, true, true, true, false);
    }

    private boolean addElsewhereIfFloat(final BoundableRenderable renderable, final HTMLElementImpl element,
                                        final boolean usesAlignAttribute, final CSSStyleDeclaration style) {

        String align = null;
        if (style != null) {
            align = style.getFloat();
            if (Strings.isBlank(align)) {
                align = null;
            }
        }
        if (align == null && usesAlignAttribute) {
            align = element.getAttribute("align");
        }
        if (align != null) {
            if ("left".equalsIgnoreCase(align)) {
                layoutFloat(renderable, true, true);
                return true;
            } else if ("right".equalsIgnoreCase(align)) {
                layoutFloat(renderable, true, false);
                return true;
            }
        }
        return false;
    }

    /**
     * Checks for position and float attributes.
     * 
     * @param renderable renderable
     * @param element element
     * @param usesAlignAttribute usesAlignAttribute
     * @return True if it was added elsewhere.
     */
    private boolean addElsewhereIfPositioned(final RElement renderable, final HTMLElementImpl element, final boolean usesAlignAttribute) {
        final CSSStyleDeclaration style = element.getCurrentStyle();
        final int position = getPosition(element);
        final boolean absolute = position == RenderState.POSITION_ABSOLUTE;
        final boolean fixed = position == RenderState.POSITION_FIXED;
        if (absolute || fixed) {
            if (renderable instanceof RBlock) {
                final RBlock block = (RBlock) renderable;

                block.layout(RLayoutInfo.builder()
                        .availWidth(this.availContentWidth)
                        .availHeight(this.availContentHeight)
                        .expandWidth(false)
                        .expandHeight(false)
                        .blockFloatBoundsSource(null)
                        .defaultOverflowX(block.defaultOverflowX)
                        .defaultOverflowY(block.defaultOverflowY)
                        .sizeOnly(this.sizeOnly)
                        .build());

            } else {
                renderable.layout(this.availContentWidth, this.availContentHeight, this.sizeOnly);
            }

            this.scheduleAbsDelayedPair(renderable, availContentWidth, availContentHeight, element, absolute, fixed);
            return true;
        } else {
            renderable.setupRelativePosition(container);
            return addElsewhereIfFloat(renderable, element, usesAlignAttribute, style);
        }
    }

    private void addExportableFloat(final BoundableRenderable element, final boolean leftFloat, final int origX, final int origY) {
        List<ExportableFloat> ep = this.exportableFloats;
        if (ep == null) {
            ep = new ArrayList<>(1);
            this.exportableFloats = ep;
        }
        final ExportableFloat ef = new ExportableFloat(element, leftFloat, origX, origY);
        ep.add(ef);
    }

    private RLine addLine(final ModelNode startNode, final RLine prevLine, final int newLineY) {
        // lineDone must be called before we try to
        // get float bounds.
        lineDone(prevLine);
        checkY(newLineY);
        final int leftOffset = fetchLeftOffset(newLineY);
        int newX = leftOffset;
        int newMaxWidth = this.desiredWidth - fetchRightOffset(newLineY) - leftOffset;
        final RLine rline;
        final boolean initialAllowOverflow;
        if (prevLine == null) {
            // Note: Assumes that prevLine == null means it's the first line.
            final RenderState rs = this.modelNode.getRenderState();
            initialAllowOverflow = rs != null && rs.getWhiteSpace() == RenderState.WS_NOWRAP;
            // Text indentation only applies to the first line in the block.
            final int textIndent = rs == null ? 0 : rs.getTextIndent(this.availContentWidth);
            if (textIndent != 0) {
                newX += textIndent;
                // Line width also changes!
                newMaxWidth += leftOffset - newX;
            }
        } else {
            final int prevLineHeight = prevLine.getHeight();
            if (prevLineHeight > 0) {
                this.currentCollapsibleMargin = 0;
            }
            initialAllowOverflow = prevLine.isAllowOverflow();
            if (prevLine.getX() + prevLine.getWidth() > this.maxX) {
                this.maxX = prevLine.getX() + prevLine.getWidth();
            }
        }
        rline = new RLine(startNode, this.container, newX, newLineY, newMaxWidth, 0, initialAllowOverflow);
        rline.setParent(this);
        List<Renderable> sr = this.seqRenderables;
        if (sr == null) {
            sr = new ArrayList<>();
            this.seqRenderables = sr;
        }
        sr.add(rline);
        this.currentLine = rline;
        return rline;
    }

    private void addLineAfterBlock(final RBlock block, final boolean informLineDone) {
        List<Renderable> sr = this.seqRenderables;
        if (sr == null) {
            sr = new ArrayList<>(1);
            this.seqRenderables = sr;
        }
        final RLine prevLine = this.currentLine;
        final boolean initialAllowOverflow;
        if (prevLine != null) {
            initialAllowOverflow = prevLine.isAllowOverflow();
            if (informLineDone) {
                lineDone(prevLine);
            }
            if (prevLine.getX() + prevLine.getWidth() > this.maxX) {
                this.maxX = prevLine.getX() + prevLine.getWidth();
            }
            // Check height only with floats.
        } else {
            initialAllowOverflow = false;
        }
        final ModelNode lineNode = block.getModelNode().getParentModelNode();
        final int newLineY = block.getY() + block.getHeight();
        checkY(newLineY);
        final int leftOffset = fetchLeftOffset(newLineY);
        final int newMaxWidth = this.desiredWidth - fetchRightOffset(newLineY) - leftOffset;
        final RLine newLine = new RLine(lineNode, this.container, leftOffset, newLineY, newMaxWidth, 0, initialAllowOverflow);
        newLine.setParent(this);
        sr.add(newLine);
        this.currentLine = newLine;
    }

    /**
     * <p>addLineBreak.</p>
     *
     * @param startNode a {@link org.loboevolution.html.dom.nodeimpl.ModelNode} object.
     * @param breakType a {@link java.lang.Integer} object.
     */
    protected void addLineBreak(final ModelNode startNode, final int breakType) {
        RLine line = this.currentLine;
        if (line == null) {
            final Insets insets = this.paddingInsets;
            addLine(startNode, null, insets.top);
            line = this.currentLine;
        }
        if (line.getHeight() == 0) {
            final RenderState rs = startNode.getRenderState();
            final int fontHeight = rs.getFontMetrics().getHeight();
            line.setHeight(fontHeight);
        }
        line.setLineBreak(new LineBreak(breakType, startNode));
        final int newLineY;
        final FloatingBounds fb = this.floatBounds;
        if (breakType == LineBreak.NONE || fb == null) {
            newLineY = line.getY() + line.getHeight();
        } else {
            final int prevY = line.getY() + line.getHeight();
            switch (breakType) {
                case LineBreak.BOTH:
                case LineBreak.LEFT:
                newLineY = fb.getLeftClearY(prevY);
                break;
            case LineBreak.RIGHT:
                newLineY = fb.getRightClearY(prevY);
                break;
            default:
                newLineY = fb.getClearY(prevY);
                break;
            }
        }
        this.currentLine = addLine(startNode, line, newLineY);
    }

    private void addPositionedRenderable(final BoundableRenderable renderable, final boolean verticalAlignable,
            final boolean isFloat, final boolean isFixed) {
        SortedSet<PositionedRenderable> others = this.positionedRenderables;
        if (others == null) {
            others = new TreeSet<>(new ZIndexComparator());
            this.positionedRenderables = others;
        }

        others.add(PositionedRenderable.builder().
                renderable(renderable).
                verticalAlignable(verticalAlignable).
                ordinal(positionedOrdinal++).
                isFloat(isFloat).
                isFixed(isFixed).build());

        renderable.setParent(this);
        if (renderable instanceof RUIControl) {
            this.container.addComponent(((RUIControl) renderable).widget.getComponent());
        }
    }

    private void addRenderableToLine(final Renderable renderable) {
        renderable.getModelNode().getRenderState();
        final RLine line = this.currentLine;
        final int liney = line.getY();
        final boolean emptyLine = line.isEmpty();
        final FloatingBounds floatBounds = this.floatBounds;
        final int cleary;
        if (floatBounds != null) {
            cleary = floatBounds.getFirstClearY(liney);
        } else {
            cleary = liney + line.getHeight();
        }
        try {
            line.add(renderable);
            // Check if the line goes into the float.
            if (floatBounds != null && cleary > liney) {
                final int rightOffset = fetchRightOffset(liney);
                final int topLineX = this.desiredWidth - rightOffset;
                if (line.getX() + line.getWidth() > topLineX) {
                    // Shift line down to clear area
                    line.setY(cleary);
                }
            }
        } catch (final OverflowException oe) {
            final int nextY = emptyLine ? cleary : liney + line.getHeight();
            addLine(renderable.getModelNode(), line, nextY);
            final Collection<Renderable> renderables = oe.getRenderables();
            for (final Renderable r : renderables) {
                addRenderableToLine(r);
            }
        }
        if (renderable instanceof RUIControl) {
            this.container.addComponent(((RUIControl) renderable).widget.getComponent());
        }
    }

    /**
     * Checks property 'float' and in some cases attribute 'align'.
     *
     * @param renderable a {@link org.loboevolution.html.renderer.RElement} object.
     * @param element a {@link org.loboevolution.html.dom.domimpl.HTMLElementImpl} object.
     */
    protected void addRenderableToLineCheckStyle(final RElement renderable, final HTMLElementImpl element) {
        if (!addElsewhereIfPositioned(renderable, element, true)) {
            renderable.layout(this.availContentWidth, this.availContentHeight, this.sizeOnly);
            addRenderableToLine(renderable);
        }
    }

    private void addWordToLine(final RWord renderable) {
        final RLine line = this.currentLine;
        final int liney = line.getY();
        final boolean emptyLine = line.isEmpty();
        final FloatingBounds floatBounds = this.floatBounds;
        final int cleary;
        if (floatBounds != null) {
            cleary = floatBounds.getFirstClearY(liney);
        } else {
            cleary = liney + line.getHeight();
        }
        try {
            line.addWord(renderable);
            // Check if the line goes into the float.
            if (floatBounds != null && cleary > liney) {
                final int rightOffset = fetchRightOffset(liney);
                final int topLineX = this.desiredWidth - rightOffset;
                if (line.getX() + line.getWidth() > topLineX) {
                    // Shift line down to clear area
                    line.setY(cleary);
                }
            }
        } catch (final OverflowException oe) {
            final int nextY = emptyLine ? cleary : liney + line.getHeight();
            addLine(renderable.getModelNode(), line, nextY);
            final Collection<Renderable> renderables = oe.getRenderables();
            for (final Renderable r : renderables) {
                addRenderableToLine(r);
            }
        }
    }

    /**
     * Applies any horizonal aLignment. It may adjust height if necessary.
     *
     * @param canvasWidth   The new width of the viewport. It could be different to
     *                      the previously calculated width.
     * @param paddingInsets a {@link java.awt.Insets} object.
     * @param alignXPercent a {@link java.lang.Integer} object.
     */
    public void alignX(final int alignXPercent, final int canvasWidth, final Insets paddingInsets) {
        final int prevMaxY = this.maxY;
        if (alignXPercent > 0) {
            final List<Renderable> renderables = this.seqRenderables;
            if (renderables != null) {
                final Insets insets = this.paddingInsets;
                for (final Object r : renderables) {
                    if (r instanceof BoundableRenderable) {
                        final BoundableRenderable seqRenderable = (BoundableRenderable) r;
                        final int y = seqRenderable.getY();
                        final boolean isVisibleBlock = seqRenderable instanceof RBlock && ((RBlock) seqRenderable).isOverflowVisibleX();
                        final int leftOffset = isVisibleBlock ? insets.left : fetchLeftOffset(y);
                        final int rightOffset = isVisibleBlock ? insets.right : fetchRightOffset(y);
                        final int actualAvailWidth = canvasWidth - leftOffset - rightOffset;
                        final int difference = actualAvailWidth - seqRenderable.getWidth();
                        if (difference > 0) {
                            final int shift = difference * alignXPercent / 100;
                            if (!isVisibleBlock) {
                                final int newX = leftOffset + shift;
                                seqRenderable.setX(newX);
                            }
                        }
                    }
                }
            }
        }
        if (prevMaxY != this.maxY) {
            this.setHeight(getHeight() + this.maxY - prevMaxY);
        }
    }

    /**
     * Applies vertical alignment.
     *
     * @param canvasHeight a {@link java.lang.Integer} object.
     * @param paddingInsets a {@link java.awt.Insets} object.
     * @param alignYPercent a {@link java.lang.Integer} object.
     */
    public void alignY(final int alignYPercent, final int canvasHeight, final Insets paddingInsets) {
        final int prevMaxY = this.maxY;
        if (alignYPercent > 0) {
            final int availContentHeight = canvasHeight - paddingInsets.top - paddingInsets.bottom;
            final int usedHeight = this.maxY - paddingInsets.top;
            final int difference = availContentHeight - usedHeight;
            if (difference > 0) {
                final int shift = difference * alignYPercent / 100;
                final List<Renderable> rlist = this.seqRenderables;
                if (rlist != null) {
                    // Try sequential renderables first.
                    for (final Object r : rlist) {
                        if (r instanceof BoundableRenderable) {
                            final BoundableRenderable line = (BoundableRenderable) r;
                            final int newY = line.getY() + shift;
                            line.setY(newY);
                            if (newY + line.getHeight() > this.maxY) {
                                this.maxY = newY + line.getHeight();
                            }
                        }
                    }
                }

                // Now other renderables, but only those that can be
                // vertically aligned
                final Set<PositionedRenderable> others = this.positionedRenderables;
                if (others != null) {
                    for (final PositionedRenderable pr : others) {
                        if (pr.isVerticalAlignable()) {
                            final BoundableRenderable br = pr.getRenderable();
                            final int newY = br.getY() + shift;
                            br.setY(newY);
                            if (newY + br.getHeight() > this.maxY) {
                                this.maxY = newY + br.getHeight();
                            }
                        }
                    }
                }
            }
        }
        if (prevMaxY != this.maxY) {
            this.setHeight(getHeight() + this.maxY - prevMaxY);
        }
    }

    private void checkY(final int y) {
        if (this.yLimit != -1 && y > this.yLimit) {
            throw SEE;
        }
    }

    /**
     * Gets offset from the left due to floats. It includes padding.
     */
    private int fetchLeftOffset(final int newLineY) {
        final Insets paddingInsets = this.paddingInsets;
        final FloatingBounds floatBounds = this.floatBounds;
        if (floatBounds == null) {
            return paddingInsets.left;
        }
        final int left = floatBounds.getLeft(newLineY);
        return Math.max(left, paddingInsets.left);
    }

    /**
     * Gets offset from the right due to floats. It includes padding.
     */
    private int fetchRightOffset(final int newLineY) {
        final Insets paddingInsets = this.paddingInsets;
        final FloatingBounds floatBounds = this.floatBounds;
        if (floatBounds == null) {
            return paddingInsets.right;
        }
        final int right = floatBounds.getRight(newLineY);
        return Math.max(right, paddingInsets.right);
    }

    private int getEffectiveBlockHeight(final BoundableRenderable block) {
        // Assumes block is the last one in the sequence.
        if (!(block instanceof RElement)) {
            return block.getHeight();
        }
        final RCollection parent = getParent();
        if (!(parent instanceof RElement)) {
            return block.getHeight();
        }
        final int blockMarginBottom = ((RElement) block).getMarginBottom();
        final int parentMarginBottom = ((RElement) parent).getCollapsibleMarginBottom();
        return block.getHeight() - Math.min(blockMarginBottom, parentMarginBottom);
    }

    /**
     * <p>getExportableFloatingInfo.</p>
     *
     * @return a {@link org.loboevolution.info.FloatingInfo} object.
     */
    public FloatingInfo getExportableFloatingInfo() {
        final List<ExportableFloat> ef = this.exportableFloats;
        if (ef == null) {
            return null;
        }
        final ExportableFloat[] floats = ef.toArray(ExportableFloat.EMPTY_ARRAY);
        final FloatingInfo fInfo = new FloatingInfo();
        fInfo.setShiftX(0);
        fInfo.setShiftY(0);
        fInfo.setFloats(floats);
        return fInfo;
    }

    /**
     * <p>getFirstBaselineOffset.</p>
     *
     * @return a {@link java.lang.Integer} object.
     */
    public int getFirstBaselineOffset() {
        final List<Renderable> renderables = this.seqRenderables;
        if (renderables != null) {
            for (final Object r : renderables) {
                if (r instanceof RLine) {
                    final int blo = ((RLine) r).getBaselineOffset();
                    if (blo != 0) {
                        return blo;
                    }
                } else if (r instanceof RBlock) {
                    final RBlock block = (RBlock) r;
                    if (block.getHeight() > 0) {
                        final Insets insets = block.getInsetsMarginBorder(false, false);
                        final Insets paddingInsets = this.paddingInsets;
                        return block.getFirstBaselineOffset() + insets.top + (paddingInsets == null ? 0 : paddingInsets.top);
                    }
                }
            }
        }
        return 0;
    }

    /**
     * <p>getFirstLineHeight.</p>
     *
     * @return a {@link java.lang.Integer} object.
     */
    public int getFirstLineHeight() {
        final List<Renderable> renderables = this.seqRenderables;
        if (renderables != null) {
            final int size = renderables.size();
            if (size == 0) {
                return 0;
            }
            for (int i = 0; i < size; i++) {
                final BoundableRenderable br = (BoundableRenderable) renderables.get(0);
                final int height = br.getHeight();
                if (height != 0) {
                    return height;
                }
            }
        }
        // Not found!!
        return 1;
    }

    /** {@inheritDoc} */
    @Override
    public RenderableSpot getLowestRenderableSpot(final int x, final int y) {
        final BoundableRenderable br = this.getRenderable(new Point(x, y));
        if (br != null) {
            return br.getLowestRenderableSpot(x - br.getX(), y - br.getY());
        } else {
            return new RenderableSpot(this, x, y);
        }
    }

    private int getNewBlockY(final BoundableRenderable newBlock, final int expectedY) {
        // Assumes the previous block is not a line with height > 0.
        if (!(newBlock instanceof RElement)) {
            return expectedY;
        }
        final RElement block = (RElement) newBlock;
        final int ccm = this.currentCollapsibleMargin;
        final int topMargin = block.getMarginTop();
        if (topMargin == 0 && ccm == 0) {
            return expectedY;
        }
        return expectedY - Math.min(topMargin, ccm);
    }

    /** {@inheritDoc} */
    @Override
    public BoundableRenderable getRenderable(final int x, final int y) {
        final List<Renderable> renderables = this.getRenderables(x, y);
        return renderables == null ? null : renderables.stream().findFirst().isPresent() ? (BoundableRenderable) renderables.stream().findFirst().get() : null;
    }

    /**
     * <p>getRenderable.</p>
     *
     * @param point a {@link java.awt.Point} object.
     * @return a {@link org.loboevolution.html.renderer.BoundableRenderable} object.
     */
    public BoundableRenderable getRenderable(final Point point) {
        return this.getRenderable(point.x, point.y);
    }

    /** {@inheritDoc} */
    @Override
    public List<Renderable> getRenderables() {
        final SortedSet<PositionedRenderable> others = this.positionedRenderables;
        if (others == null || others.size() == 0) {
            final List<Renderable> sr = this.seqRenderables;
            return sr;
        } else {
            final List<Renderable> allRenderables = new ArrayList<>();
            this.populateZIndexGroupsIterator(others, this.seqRenderables, allRenderables);
            return allRenderables;
        }
    }

    /**
     * <p>getRenderables.</p>
     *
     * @param pointx a {@link java.lang.Integer} object.
     * @param pointy a {@link java.lang.Integer} object.
     * @return a {@link java.util.List} object.
     */
    public List<Renderable> getRenderables(final int pointx, final int pointy) {
        if (!SwingUtilities.isEventDispatchThread()) {
            log.warn("getRenderable(): Invoked outside GUI dispatch thread.");
        }
        List<Renderable> result = null;
        final SortedSet<PositionedRenderable> others = this.positionedRenderables;
        final int size = others == null ? 0 : others.size();
        final PositionedRenderable[] otherArray = size == 0 ? null
                : others.toArray(PositionedRenderable.EMPTY_ARRAY);
        // Try to find in other renderables with z-index >= 0 first.
        int index = 0;
        if (otherArray != null) {
            // Must go in reverse order
            for (index = size; --index >= 0;) {
                final PositionedRenderable pr = otherArray[index];
                final BoundableRenderable br = pr.getRenderable();
                if (br.getZIndex() < 0) {
                    break;
                }
                if (br.contains(pointx, pointy)) {
                    if (result == null) {
                        result = new LinkedList<>();
                    }
                    result.add(br);
                }
            }
        }

        final List<Renderable> sr = this.seqRenderables;
        if (sr != null) {
            final Renderable[] array = sr.toArray(Renderable.EMPTY_ARRAY);
            final List<BoundableRenderable> found = MarkupUtilities.findRenderables(array, pointx, pointy, true);
            if (found != null) {
                if (result == null) {
                    result = new LinkedList<>();
                }
                result.addAll(found);
            }
        }

        // Finally, try to find it in renderables with z-index < 0.
        if (otherArray != null) {
            for (; index >= 0; index--) {
                final PositionedRenderable pr = otherArray[index];
                final BoundableRenderable br = pr.getRenderable();
                if (br.contains(pointx, pointy)) {
                    if (result == null) {
                        result = new LinkedList<>();
                    }
                    result.add(br);
                }
            }
        }
        return result;
    }

    void importDelayedPair(final DelayedPair pair) {
        final BoundableRenderable r = pair.positionPairChild();
        this.addPositionedRenderable(r, false, false, pair.isFixed());
    }

    private void importFloat(final ExportableFloat ef, final int shiftX, final int shiftY) {
        final BoundableRenderable renderable = ef.element;
        final int newX = ef.origX + shiftX;
        final int newY = ef.origY + shiftY;
        renderable.setOrigin(newX, newY);
        final FloatingBounds prevBounds = this.floatBounds;
        final int offsetFromBorder;
        final boolean leftFloat = ef.leftFloat;
        if (leftFloat) {
            offsetFromBorder = newX + renderable.getWidth();
        } else {
            offsetFromBorder = this.desiredWidth - newX;
        }
        this.floatBounds = new FloatingViewportBounds(prevBounds, leftFloat, newY, offsetFromBorder, renderable.getHeight());
        if (isFloatLimit()) {
            addPositionedRenderable(renderable, true, true, false);
        } else {
              addExportableFloat(renderable, leftFloat, newX, newY);
        }
    }

    private void importFloatingInfo(final FloatingInfo floatingInfo, final BoundableRenderable block) {
        final int shiftX = floatingInfo.getShiftX() + block.getX();
        final int shiftY = floatingInfo.getShiftY() + block.getY();
        final ExportableFloat[] floats = (ExportableFloat[])floatingInfo.getFloats();
        for (final ExportableFloat ef : floats) {
            importFloat(ef, shiftX, shiftY);
        }
    }

    private int initCollapsibleMargin() {
        final Object parent = this.parent;
        if (!(parent instanceof RBlock)) {
            return 0;
        }
        final RBlock parentBlock = (RBlock) parent;
        return parentBlock.getCollapsibleMarginTop();
    }

    /** {@inheritDoc} */
    @Override
    public void invalidateLayoutLocal() {
        // Workaround for fact that RBlockViewport does not
        // get validated or invalidated.
        this.layoutUpTreeCanBeInvalidated = true;
    }

    /** {@inheritDoc} */
    @Override
    public boolean isContainedByNode() {
        return false;
    }

    private boolean isFloatLimit() {
        Boolean fl = this.isFloatLimit;
        if (fl == null) {
            fl = isFloatLimitImpl();
            this.isFloatLimit = fl;
        }
        return fl;
    }

    private Boolean isFloatLimitImpl() {
        final Object parent = getOriginalOrCurrentParent();
        if (!(parent instanceof RBlock)) {
            return Boolean.TRUE;
        }
        final RBlock blockParent = (RBlock) parent;
        final Object grandParent = blockParent.getOriginalOrCurrentParent();
        if (!(grandParent instanceof RBlockViewport)) {
            // Could be contained in a table, or it could
            // be a list item, for example.
            return Boolean.TRUE;
        }
        final ModelNode node = this.modelNode;
        if (!(node instanceof HTMLElementImpl)) {
            // Can only be a document here.
            return Boolean.TRUE;
        }
        final HTMLElementImpl element = (HTMLElementImpl) node;
        final int position = getPosition(element);
        if (position == RenderState.POSITION_ABSOLUTE || position == RenderState.POSITION_FIXED) {
            return Boolean.TRUE;
        }
        element.getCurrentStyle();
        final RenderState rs = element.getRenderState();
        final int floatValue = rs == null ? RenderState.FLOAT_NONE : rs.getFloat();
        if (floatValue != RenderState.FLOAT_NONE) {
            return Boolean.TRUE;
        }
        final int overflowX = rs == null ? RenderState.OVERFLOW_NONE : rs.getOverflowX();
        final int overflowY = rs == null ? RenderState.OVERFLOW_NONE : rs.getOverflowY();
        if ((overflowX == RenderState.OVERFLOW_AUTO) || (overflowX == RenderState.OVERFLOW_SCROLL)
                || (overflowY == RenderState.OVERFLOW_AUTO) || (overflowY == RenderState.OVERFLOW_SCROLL)) {
            return Boolean.TRUE;
        }
        return Boolean.FALSE;
    }

    /**
     * Builds the layout/renderer tree from scratch. Note: Returned dimension needs
     * to be actual size needed for rendered content, not the available container
     * size. This is relied upon by table layout.
     *
     * @param yLimit If other than -1, layout will throw
     *               SizeExceededException in the event that the layout
     *               goes beyond this y-coordinate point.
     * @param desiredWidth a {@link java.lang.Integer} object.
     * @param desiredHeight a {@link java.lang.Integer} object.
     * @param paddingInsets a {@link java.awt.Insets} object.
     * @param floatBounds a {@link org.loboevolution.html.renderer.FloatingBounds} object.
     * @param sizeOnly a boolean.
     */
    public void layout(final int desiredWidth, final int desiredHeight, final Insets paddingInsets, final int yLimit,
                       final FloatingBounds floatBounds, final boolean sizeOnly) {
        // Expected in GUI thread. It's possible it may be invoked during pack()
        // outside of the GUI thread.
        if (!SwingUtilities.isEventDispatchThread()) {
            log.warn("layout(): Invoked outside GUI dispatch thread.");
        }
        this.paddingInsets = paddingInsets;
        this.yLimit = yLimit;
        this.desiredWidth = desiredWidth;
        this.floatBounds = floatBounds;
        this.isFloatLimit = null;
        this.pendingFloats = null;
        this.sizeOnly = sizeOnly;
        this.lastSeqBlock = null;
        this.currentCollapsibleMargin = initCollapsibleMargin();

        // maxX and maxY should not be reset by layoutPass.
        this.maxX = paddingInsets.left;
        this.maxY = paddingInsets.top;

        int availw = desiredWidth - paddingInsets.left - paddingInsets.right;
        if (availw < 0) {
            availw = 0;
        }
        int availh = desiredHeight - paddingInsets.top - paddingInsets.bottom;
        if (availh < 0) {
            availh = 0;
        }
        this.availContentHeight = availh;
        this.availContentWidth = availw;

        // New floating algorithm.
        layoutPass((NodeImpl) this.modelNode);

        positionDelayed();

        // Compute maxY according to last block.
        int maxY = this.maxY;
        int maxYWholeBlock = maxY;
        final BoundableRenderable lastSeqBlock = this.lastSeqBlock;
        if (lastSeqBlock != null) {
            final int effBlockHeight = getEffectiveBlockHeight(lastSeqBlock);
            if (lastSeqBlock.getY() + effBlockHeight > maxY) {
                this.maxY = maxY = lastSeqBlock.getY() + effBlockHeight;
                maxYWholeBlock = lastSeqBlock.getY() + lastSeqBlock.getHeight();
            }
        }

        // See if line should increase maxY. Empty
        // lines shouldn't, except in cases where
        // there was a BR.
        final RLine lastLine = this.currentLine;
        final Rectangle lastBounds = lastLine.getBounds();
        if (lastBounds.height > 0 || lastBounds.y > maxYWholeBlock) {
            final int lastTopX = lastBounds.x + lastBounds.width;
            if (lastTopX > this.maxX) {
                this.maxX = lastTopX;
            }
            final int lastTopY = lastBounds.y + lastBounds.height;
            if (lastTopY > maxY) {
                this.maxY = maxY = lastTopY;
            }
        }

        // Check positioned renderables for maxX and maxY
        final SortedSet<PositionedRenderable> posRenderables = this.positionedRenderables;
        if (posRenderables != null) {
            final boolean isFloatLimit = isFloatLimit();
            for (final PositionedRenderable pr : posRenderables) {
                final BoundableRenderable br = pr.getRenderable();
                if (br.getX() + br.getWidth() > this.maxX) {
                    this.maxX = br.getX() + br.getWidth();
                }
                if (isFloatLimit || !pr.isFloat()) {
                    if (br.getY() + br.getHeight() > maxY) {
                        this.maxY = maxY = br.getY() + br.getHeight();
                    }
                }
            }
        }

        this.setWidth(paddingInsets.right + this.maxX);
        this.setHeight(paddingInsets.bottom + maxY);
    }

    private void layoutChildren(final NodeImpl node) {
        final NodeListImpl nodeList = node.getNodeList();
        if (nodeList != null) {
            nodeList.forEach(nd -> {
                final NodeImpl child = (NodeImpl) nd;
                final int nodeType = child.getNodeType();
                switch (nodeType) {
                    case Node.TEXT_NODE:
                        layoutText(child);
                        break;
                    case Node.ELEMENT_NODE:
                        this.currentLine.addStyleChanger(new RStyleChanger(child));
                        final String nodeName = child.getNodeName().toUpperCase();
                        MarkupLayout ml = RLayout.elementLayout.get(HTMLTag.get(nodeName));
                        if (ml == null) {
                            ml = miscLayout;
                        }
                        ml.layoutMarkup(this, (HTMLElementImpl) child);
                        this.currentLine.addStyleChanger(new RStyleChanger(node));
                        break;
                    case Node.DOCUMENT_FRAGMENT_NODE:
                        final DocumentFragmentImpl fragment = (DocumentFragmentImpl) child;
                        fragment.getNodeList().forEach(fragNode -> {
                            final NodeImpl fragChild = (NodeImpl) fragNode;
                            layoutChildren(fragChild);
                        });
                        break;
                    case Node.COMMENT_NODE:
                    case Node.PROCESSING_INSTRUCTION_NODE:
                    default:
                        break;
                }
            });
        }
    }

    private void layoutFloat(final BoundableRenderable renderable, final boolean layout, final boolean leftFloat) {
        renderable.setOriginalParent(this);
        if (layout) {
            final int availWidth = this.availContentWidth;
            final int availHeight = this.availContentHeight;
            if (renderable instanceof RBlock) {
                final RBlock block = (RBlock) renderable;
                block.layout(RLayoutInfo.builder()
                        .availWidth(availWidth)
                        .availHeight(availHeight)
                        .expandWidth(false)
                        .expandHeight(false)
                        .blockFloatBoundsSource(null)
                        .defaultOverflowX(block.defaultOverflowX)
                        .defaultOverflowY(block.defaultOverflowY)
                        .sizeOnly(sizeOnly)
                        .build());
                this.lastSeqBlock = block;
            } else if (renderable instanceof RElement) {
                final RElement e = (RElement) renderable;
                e.layout(availWidth, availHeight, this.sizeOnly);
            }
        }
        final RFloatInfo floatInfo = new RFloatInfo(renderable.getModelNode(), renderable, leftFloat);
        this.currentLine.simplyAdd(floatInfo);
        scheduleFloat(floatInfo);
    }

    /**
     * <p>layoutList.</p>
     *
     * @param markupElement a {@link org.loboevolution.html.dom.domimpl.HTMLElementImpl} object.
     */
    protected final void layoutList(final HTMLElementImpl markupElement) {
        RList renderable = (RList) markupElement.getUINode();
        if (renderable == null) {
            info.setModelNode(markupElement);
            renderable = new RList(info);
            markupElement.setUINode(renderable);
        }
        renderable.setOriginalParent(this);
        positionRBlock(markupElement, renderable);
    }

    /**
     * <p>layoutListItem.</p>
     *
     * @param markupElement a {@link org.loboevolution.html.dom.domimpl.HTMLElementImpl} object.
     */
    protected final void layoutListItem(final HTMLElementImpl markupElement) {
        RListItem renderable = (RListItem) markupElement.getUINode();
        if (renderable == null) {
            info.setModelNode(markupElement);
            renderable = new RListItem(info);
            markupElement.setUINode(renderable);
        }
        renderable.setOriginalParent(this);
        positionRBlock(markupElement, renderable);
    }

    /**
     * <p>layoutMarkup.</p>
     *
     * @param node a {@link org.loboevolution.html.dom.nodeimpl.NodeImpl} object.
     */
    protected void layoutMarkup(final NodeImpl node) {
        // This is the "inline" layout of an element.
        // The difference with layoutChildren is that this
        // method checks for padding and margin insets.
        final RenderState rs = node.getRenderState();
        Insets marginInsets = null;
        Insets paddingInsets = null;
        if (rs != null) {
            final HtmlInsets mi = rs.getMarginInsets();
            marginInsets = mi == null ? null : mi.getAWTInsets(this.availContentWidth, this.availContentHeight, 0, 0);
            final HtmlInsets pi = rs.getPaddingInsets();
            paddingInsets = pi == null ? null : pi.getAWTInsets(this.availContentWidth, this.availContentHeight, 0, 0);
        }
        int leftSpacing = 0;
        int rightSpacing = 0;
        if (marginInsets != null) {
            leftSpacing += marginInsets.left;
            rightSpacing += marginInsets.right;
        }
        if (paddingInsets != null) {
            leftSpacing += paddingInsets.left;
            rightSpacing += paddingInsets.right;
        }
        if (leftSpacing > 0) {
            final RLine line = this.currentLine;
            line.addSpacing(new RSpacing(node, this.container, leftSpacing, line.getHeight()));
        }
        layoutChildren(node);
        if (rightSpacing > 0) {
            final RLine line = this.currentLine;
            line.addSpacing(new RSpacing(node, this.container, rightSpacing, line.getHeight()));
        }
    }

    private void layoutPass(final NodeImpl rootNode) {
        final RenderableContainer container = this.container;
        container.clearDelayedPairs();
        this.positionedOrdinal = 0;

        // Remove sequential renderables...
        this.seqRenderables = null;

        // Remove other renderables...
        this.positionedRenderables = null;

        // Remove exporatable floats...
        this.exportableFloats = null;

        // Call addLine after setting margins
        this.currentLine = addLine(rootNode, null, this.paddingInsets.top);

        // Start laying out...
        // The parent is expected to have set the RenderState already.
        layoutChildren(rootNode);

        // This adds last-line floats.
        lineDone(this.currentLine);
    }

    /**
     * <p>layoutRBlock.</p>
     *
     * @param markupElement a {@link org.loboevolution.html.dom.domimpl.HTMLElementImpl} object.
     */
    protected final void layoutRBlock(final HTMLElementImpl markupElement) {
        final UINode uiNode = markupElement.getUINode();
        RBlock renderable = null;
        if (uiNode instanceof RBlock) {
            renderable = (RBlock) markupElement.getUINode();
        }
            
        if (renderable == null) {
            info.setModelNode(markupElement);
            renderable = new RBlock(info);
            markupElement.setUINode(renderable);
        }
        renderable.setOriginalParent(this);
        positionRBlock(markupElement, renderable);
    }

    /**
     * <p>layoutRTable.</p>
     *
     * @param markupElement a {@link org.loboevolution.html.dom.domimpl.HTMLElementImpl} object.
     */
    protected final void layoutRTable(final HTMLElementImpl markupElement) {
        RElement renderable = (RElement) markupElement.getUINode();
        if (renderable == null) {
            info.setModelNode(markupElement);
            renderable = new RTable(info);
            markupElement.setUINode(renderable);
        }
        renderable.setOriginalParent(this);
        positionRElement(markupElement, renderable, markupElement instanceof HTMLTableElementImpl, true, true);
    }
    
    /**
     * <p>layoutRInlineBlock.</p>
     *
     * @param markupElement a {@link org.loboevolution.html.dom.domimpl.HTMLElementImpl} object.
     */
    public void layoutRInlineBlock(final HTMLElementImpl markupElement) {
        final UINode uINode = markupElement.getUINode();
        final RInlineBlock inlineBlock;
        if (uINode instanceof RInlineBlock) {
            inlineBlock = (RInlineBlock) uINode;
        } else {
            info.setModelNode(markupElement);
            final RInlineBlock newInlineBlock = new RInlineBlock(container, info);
            markupElement.setUINode(newInlineBlock);
            inlineBlock = newInlineBlock;
        }
        inlineBlock.doLayout(availContentWidth, availContentHeight, sizeOnly);
        addRenderableToLine(inlineBlock);
    }
    
    /**
     * <p>layoutRFlex.</p>
     *
     * @param markupElement a {@link org.loboevolution.html.dom.domimpl.HTMLElementImpl} object.
     */
    public void layoutRFlex(final HTMLElementImpl markupElement) {
        final RenderState renderState = markupElement.getRenderState();
        final RFlex flex = new RFlex(renderState);
        if (flex.isFlexTable()) {
            layoutRTable(markupElement);
        } else {
            flex.flexAlign(markupElement);
            layoutRBlock(markupElement);    
        }
    }

    /**
     * <p>layoutChildFlex.</p>
     *
     * @param markupElement a {@link org.loboevolution.html.dom.domimpl.HTMLElementImpl} object.
     */
    public void layoutChildFlex(final HTMLElementImpl markupElement) {
        final RenderState renderState = markupElement.getRenderState();
        final RFlexChild flex = new RFlexChild(renderState);
        
        if (flex.isInlineBlock()) {
            layoutRInlineBlock(markupElement);
        } else {
            flex.flexAlign(markupElement);
            layoutRBlock(markupElement);
        }
    }

    private void layoutText(final NodeImpl textNode) {
        final RenderState renderState = textNode.getRenderState();
        if (renderState != null) {


        final FontMetrics fm = renderState.getFontMetrics();
        final int descent = fm.getDescent();
        final int ascentPlusLeading = fm.getAscent() + fm.getLeading();
        final int wordHeight = fm.getHeight();
        final int blankWidth = fm.charWidth(' ');
        final int whiteSpace = this.overrideNoWrap ? RenderState.WS_NOWRAP : renderState.getWhiteSpace();
        final int textTransform = renderState.getTextTransform();
        final String text = textNode.getNodeValue();
        if (whiteSpace != RenderState.WS_PRE) {
            final boolean prevAllowOverflow = this.currentLine.isAllowOverflow();
            final boolean allowOverflow = whiteSpace == RenderState.WS_NOWRAP;
            this.currentLine.setAllowOverflow(allowOverflow);
            try {
                final int length = text.length();
                boolean firstWord = true;
                final StringBuilder word = new StringBuilder(12);
                for (int i = 0; i < length; i++) {
                    char ch = text.charAt(i);
                    if (Character.isWhitespace(ch)) {
                        if (firstWord) {
                            firstWord = false;
                        }
                        final int wlen = word.length();
                        if (wlen > 0) {
                            final RWord rword = new RWord(textNode, word.toString(), this.container, fm, descent,
                                    ascentPlusLeading, wordHeight, textTransform);
                            addWordToLine(rword);
                            word.delete(0, wlen);
                        }
                        final RLine line = this.currentLine;
                        if (line.getWidth() > 0) {
                            final RBlank rblank = new RBlank(textNode, fm, this.container, ascentPlusLeading,
                                    blankWidth, wordHeight);
                            line.addBlank(rblank);
                        }
                        for (i++; i < length; i++) {
                            ch = text.charAt(i);
                            if (!Character.isWhitespace(ch)) {
                                word.append(ch);
                                break;
                            }
                        }
                    } else {
                        word.append(ch);
                    }
                }
                if (word.length() > 0) {
                    final RWord rword = new RWord(textNode, word.toString(), this.container, fm, descent,
                            ascentPlusLeading, wordHeight, textTransform);
                    addWordToLine(rword);
                }
            } finally {
                this.currentLine.setAllowOverflow(prevAllowOverflow);
            }
        } else {
            final int length = text.length();
            boolean lastCharSlashR = false;
            final StringBuilder line = new StringBuilder();
            for (int i = 0; i < length; i++) {
                final char ch = text.charAt(i);
                switch (ch) {
                case '\r':
                    lastCharSlashR = true;
                    break;
                case '\n':
                    final RWord rword = new RWord(textNode, line.toString(), container, fm, descent, ascentPlusLeading, wordHeight, textTransform);
                    this.addWordToLine(rword);
                    line.delete(0, line.length());
                    final RLine prevLine = this.currentLine;
                    prevLine.setLineBreak(new LineBreak(LineBreak.NONE, textNode));
                    addLine(textNode, prevLine, prevLine.getY() + prevLine.getHeight());
                    break;
                default:
                    if (lastCharSlashR) {
                        line.append('\r');
                        lastCharSlashR = false;
                    }
                    line.append(ch);
                    break;
                }
            }
            if (line.length() > 0) {
                final RWord rword = new RWord(textNode, line.toString(), this.container, fm, descent, ascentPlusLeading,
                        wordHeight, textTransform);
                addWordToLine(rword);
            }
        }} else{
            log.error("RenderState is null for node {} with parent {} ", textNode, textNode.getParentNode());
        }
    }

    private void lineDone(final RLine line) {
        final int yAfterLine = line == null ? this.paddingInsets.top : line.getY() + line.getHeight();
        final Collection<RFloatInfo> pfs = this.pendingFloats;
        if (pfs != null) {
            this.pendingFloats = null;
            for (final RFloatInfo pf : pfs) {
                placeFloat(pf.getRenderable(), yAfterLine, pf.isLeftFloat());
            }
        }
    }

    /** {@inheritDoc} */
    @Override
    public boolean onDoubleClick(final MouseEvent event, final int x, final int y) {
        final List<Renderable> renderables = getRenderables();
        final AtomicBoolean result = new AtomicBoolean(true);
        if (renderables != null) {
            renderables.forEach(rn -> {
                if (rn instanceof BoundableRenderable) {
                    final BoundableRenderable br = (BoundableRenderable) rn;
                    final Rectangle bounds = br.getVisualBounds();
                    if (!br.onDoubleClick(event, x - bounds.x, y - bounds.y)) {
                        result.set(false);
                    }
                }
            });
        }
        return result.get();
    }

    /** {@inheritDoc} */
    @Override
    public boolean onMouseClick(final MouseEvent event, final int x, final int y) {
        final List<Renderable> renderables = getRenderables();
        final AtomicBoolean result = new AtomicBoolean(true);
        if (renderables != null) {
            renderables.forEach(rn -> {
                if (rn instanceof BoundableRenderable) {
                    final BoundableRenderable br = (BoundableRenderable) rn;
                    final Rectangle bounds = br.getVisualBounds();
                    if (!br.onMouseClick(event, x - bounds.x, y - bounds.y)) {
                        result.set(false);
                    }
                }
            });
        }
        return result.get();
    }

    /** {@inheritDoc} */
    @Override
    public boolean onMouseDisarmed(final MouseEvent event) {
        final BoundableRenderable br = this.armedRenderable;
        if (br != null) {
            try {
                return br.onMouseDisarmed(event);
            } finally {
                this.armedRenderable = null;
            }
        } else {
            return true;
        }
    }
    /** {@inheritDoc} */
    @Override
    public boolean onMousePressed(final MouseEvent event, final int x, final int y) {
        final List<Renderable> renderables = getRenderables();
        final AtomicBoolean result = new AtomicBoolean(true);
        if (renderables != null) {
            renderables.forEach(rn -> {
                if (rn instanceof BoundableRenderable) {
                    final BoundableRenderable br = (BoundableRenderable) rn;
                    final Rectangle bounds = br.getVisualBounds();
                    if (!br.onMousePressed(event, x - bounds.x, y - bounds.y)) {
                        this.armedRenderable = br;
                        result.set(false);
                    }
                }
            });
        }
        return result.get();
    }

    /** {@inheritDoc} */
    @Override
    public boolean onMouseReleased(final MouseEvent event, final int x, final int y) {
        final List<Renderable> renderables = getRenderables();
        final AtomicBoolean result = new AtomicBoolean(true);
        if (renderables != null) {
            renderables.forEach(rn -> {
                if (rn instanceof BoundableRenderable) {
                    final BoundableRenderable br = (BoundableRenderable) rn;
                    final Rectangle bounds = br.getVisualBounds();
                    if (!br.onMouseReleased(event, x - bounds.x, y - bounds.y)) {
                        final BoundableRenderable oldArmedRenderable = this.armedRenderable;
                        if (oldArmedRenderable != null && br != oldArmedRenderable) {
                            oldArmedRenderable.onMouseDisarmed(event);
                            this.armedRenderable = null;
                        }
                        result.set(false);
                    }
                }
            });
        }
        final BoundableRenderable oldArmedRenderable = this.armedRenderable;
        if (oldArmedRenderable != null) {
            oldArmedRenderable.onMouseDisarmed(event);
            this.armedRenderable = null;
        }
        return result.get();
    }

    /** {@inheritDoc} */
    @Override
    public void paint(final Graphics gIn) {
        paint(gIn, gIn);
    }

    private void paint(final Graphics gIn, final Graphics gInUnClipped) {
        final boolean translationRequired = (getX() | getY()) != 0;
        final Graphics g = translationRequired ? gIn.create() : gIn;
        if (translationRequired) {
            g.translate(getX(), getY());
        }

        final Graphics gUnClipped = translationRequired ? gInUnClipped.create() : gInUnClipped;
        if (translationRequired) {
            gUnClipped.translate(getX(), getY());
        }

        try {
            final List<Renderable> renderables = getRenderables();
            if (renderables != null) {
                renderables.forEach(robj -> {
                    if (robj instanceof BoundableRenderable) {
                        final BoundableRenderable renderable = (BoundableRenderable) robj;
                        if (!renderable.isDelegated()) {
                            renderable.paintTranslated(g);
                        }
                    } else {
                        final Graphics selectedG = robj.isFixed() ? gIn : g;
                        boolean paintRendable = false;

                        if (getModelNode() instanceof HTMLDocument) {
                            Renderable htmlRenderable = robj.findHtmlRenderable(this);

                            if (htmlRenderable instanceof PositionedRenderable) {
                                final PositionedRenderable htmlPR = (PositionedRenderable) htmlRenderable;
                                htmlRenderable = htmlPR.getRenderable();
                            }

                            if (htmlRenderable instanceof RBlock) {
                                final Rectangle htmlBounds = ((RBlock) htmlRenderable).getClipBoundsWithoutInsets();
                                if (htmlBounds != null) {
                                    paintRendable = true;
                                    final Graphics clippedG = selectedG.create(0, 0, htmlBounds.width, htmlBounds.height);
                                    try {
                                        robj.paint(clippedG);
                                    } finally {
                                        clippedG.dispose();
                                    }
                                }
                            }
                        }
                        if(!paintRendable) robj.paint(selectedG);
                    }
                });
            }
        } finally {
            if (translationRequired) {
                g.dispose();
                gUnClipped.dispose();
            }
        }
    }

    private void placeFloat(final BoundableRenderable element, final int y, final boolean leftFloat) {
        final Insets insets = this.paddingInsets;
        int boxY = y;
        int boxWidth = element.getWidth();
        int boxHeight = element.getHeight();
        final int desiredWidth = this.desiredWidth;
        int boxX;
        for (;;) {
            final int leftOffset = fetchLeftOffset(boxY);
            final int rightOffset = fetchRightOffset(boxY);
            boxX = leftFloat ? leftOffset : desiredWidth - rightOffset - boxWidth;
            if (leftOffset == insets.left && rightOffset == insets.right) {
                // Probably typical scenario. If it's overflowing to the left,
                // we need to correct.
                if (!leftFloat && boxX < leftOffset) {
                    boxX = leftOffset;
                }
                break;
            }
            if ((desiredWidth <= 0) || boxWidth <= (desiredWidth - rightOffset - leftOffset)) {
                break;
            }
            // At this point the float doesn't fit at the current Y position.
            if (element instanceof RBlock) {
                // Try shrinking it.
                final RBlock relement = (RBlock) element;
                if (!relement.hasDeclaredWidth()) {
                    final int availableBoxWidth = desiredWidth - rightOffset - leftOffset;
                    relement.layout(availableBoxWidth, this.availContentHeight, this.sizeOnly);
                    if (relement.getWidth() < boxWidth) {
                        if (relement.getWidth() > desiredWidth - rightOffset - leftOffset) {
                            // Didn't work out. Put it back the way it was.
                            relement.layout(this.availContentWidth, this.availContentHeight, this.sizeOnly);
                        } else {
                            // Retry
                            boxWidth = relement.getWidth();
                            boxHeight = relement.getHeight();
                            continue;
                        }
                    }
                }
            }
            final FloatingBounds fb = this.floatBounds;
            final int newY = fb == null ? boxY + boxHeight : fb.getFirstClearY(boxY);
            if (newY == boxY) {
                // Possible if prior box has height zero?
                break;
            }
            boxY = newY;
        }
        // Position element
        element.setOrigin(boxX, boxY);
        // Update float bounds accordingly
        final int offsetFromBorder = leftFloat ? boxX + boxWidth : desiredWidth - boxX;
        this.floatBounds = new FloatingViewportBounds(this.floatBounds, leftFloat, boxY, offsetFromBorder, boxHeight);
        // Add element to collection
        final boolean isFloatLimit = isFloatLimit();
        if (isFloatLimit) {
            addPositionedRenderable(element, true, true, false);
        } else {
            addExportableFloat(element, leftFloat, boxX, boxY);
        }
        // Adjust maxX based on float.
        if (boxX + boxWidth > this.maxX) {
            this.maxX = boxX + boxWidth;
        }
        // Adjust maxY based on float, but only if this viewport is the float limit.
        if (isFloatLimit()) {
            if (boxY + boxHeight > this.maxY) {
                this.maxY = boxY + boxHeight;
            }
        }
    }

    private void populateZIndexGroupsIterator(final Collection<PositionedRenderable> others,
            final Collection<Renderable> seqRenderables, final List<Renderable> destination) {
        populateZIndexGroups(others, seqRenderables == null ? null : seqRenderables.iterator(), destination);
    }

    private static void populateZIndexGroups(final Collection<PositionedRenderable> others,
            final Iterator<Renderable> seqRenderablesIterator, final List<Renderable> destination) {
        // First, others with z-index < 0
        final Iterator<PositionedRenderable> i1 = others.iterator();
        Renderable pending = null;
        while (i1.hasNext()) {
            final PositionedRenderable pr = i1.next();
            final BoundableRenderable r = pr.getRenderable();
            if (r.getZIndex() >= 0) {
                pending = pr;
                break;
            }
            destination.add(pr);
        }

        // Second, sequential renderables
        if (seqRenderablesIterator != null) {
            while (seqRenderablesIterator.hasNext()) {
                destination.add(seqRenderablesIterator.next());
            }
        }

        // Third, other renderables with z-index >= 0.
        if (pending != null) {
            destination.add(pending);
            while (i1.hasNext()) {
                final PositionedRenderable pr = i1.next();
                destination.add(pr);
            }
        }
    }

    private void positionRBlock(final HTMLElementImpl markupElement, final RBlock renderable) {
        
        final RenderState rs = renderable.getModelNode().getRenderState();
        final int clear = rs.getClear();
        if (clear != LineBreak.NONE) {
            addLineBreak(renderable.getModelNode(), clear);
        }
        
        if (!addElsewhereIfPositioned(renderable, markupElement, false)) {
            final int availContentHeight = this.availContentHeight;
            final RLine line = this.currentLine;
            // Inform line done before layout so floats are considered.
            lineDone(line);
            final Insets paddingInsets = this.paddingInsets;
            final int newLineY = line == null ? paddingInsets.top : line.getY() + line.getHeight();
            final int availContentWidth = this.availContentWidth;
            final int expectedWidth = availContentWidth;
            final int blockShiftRight = paddingInsets.right;
            final int newX = paddingInsets.left;
            final FloatingBounds floatBounds = this.floatBounds;
            FloatingBoundsSource floatBoundsSource = null;
            if (floatBounds != null) {
                floatBoundsSource = new ParentFloatingBoundsSource(blockShiftRight, expectedWidth, newX, newLineY, floatBounds);
            }

            renderable.layout(RLayoutInfo.builder()
                    .availWidth(availContentWidth)
                    .availHeight(availContentHeight)
                    .expandWidth(true)
                    .expandHeight(false)
                    .blockFloatBoundsSource(floatBoundsSource)
                    .defaultOverflowX(renderable.defaultOverflowX)
                    .defaultOverflowY(renderable.defaultOverflowY)
                    .sizeOnly(sizeOnly)
                    .build());

            this.addAsSeqBlock(renderable, false, false, false, false);
            // Calculate new floating bounds after block has been put in place.
            final FloatingInfo floatingInfo = renderable.getExportableFloatingInfo();
            if (floatingInfo != null) {
                importFloatingInfo(floatingInfo, renderable);
            }
            // Now add line, after float is set.
            addLineAfterBlock(renderable, false);
        }
    }

    /**
     * <p>positionRElement.</p>
     *
     * @param markupElement a {@link org.loboevolution.html.dom.domimpl.HTMLElementImpl} object.
     * @param renderable a {@link org.loboevolution.html.renderer.RElement} object.
     * @param usesAlignAttribute a boolean.
     * @param obeysFloats a boolean.
     * @param alignCenterAttribute a boolean.
     */
    protected final void positionRElement(final HTMLElementImpl markupElement, final RElement renderable, final boolean usesAlignAttribute,
                                          final boolean obeysFloats, final boolean alignCenterAttribute) {
        if (!addElsewhereIfPositioned(renderable, markupElement, usesAlignAttribute)) {
            int availContentWidth = this.availContentWidth;
            final int availContentHeight = this.availContentHeight;
            final RLine line = this.currentLine;
            // Inform line done before layout so floats are considered.
            lineDone(line);
            if (obeysFloats) {
                final int newLineY = line == null ? this.paddingInsets.top : line.getY() + line.getHeight();
                final int leftOffset = fetchLeftOffset(newLineY);
                final int rightOffset = fetchRightOffset(newLineY);
                availContentWidth = this.desiredWidth - leftOffset - rightOffset;
            }
            renderable.layout(availContentWidth, availContentHeight, this.sizeOnly);
            boolean centerBlock = false;
            if (alignCenterAttribute) {
                final String align = markupElement.getAttribute("align");
                centerBlock = align != null && align.equalsIgnoreCase("center");
            }
            addAsSeqBlock(renderable, obeysFloats, false, true, centerBlock);
        }
    }

    /**
     * @param absolute if true, then position is absolute, else fixed
     */
    private void scheduleAbsDelayedPair(final BoundableRenderable renderable, final int availContentWidth, final int availContentHeight,
                                        final HTMLElementImpl element, final boolean absolute, final boolean fixed) {

        final RenderableContainer containingBlock = absolute ? getPositionedAncestor(this.container) : getRootContainer(container);

        final CSSStyleDeclaration style = element.getCurrentStyle();

        this.container.addDelayedPair(DelayedPair.builder().
                modelNode(element).
                immediateContainingBlock(container).
                containingBlock(containingBlock).
                child(renderable).
                availContentHeight(availContentHeight).
                availContentWidth(availContentWidth).
                left(style.getLeft()).
                right(style.getRight()).
                top(style.getTop()).
                bottom(style.getBottom()).
                width(HtmlValues.getPixelSize(style.getWidth(), element.getRenderState(), element.getDocumentNode().getDefaultView(), null, availContentWidth)).
                height(HtmlValues.getPixelSize(style.getHeight(), element.getRenderState(), element.getDocumentNode().getDefaultView(), null, availContentHeight)).
                rs(element.getRenderState()).
                initY(currentLine.getY() + currentLine.getHeight()).
                initX(currentLine.getX()).
                isFixed(fixed).
                isRelative(!absolute && !fixed).
                build());
    }

    private static RenderableContainer getRootContainer(final RenderableContainer container) {
        RenderableContainer c = container.getParentContainer();
        RenderableContainer prevC = container;
        for (;;) {
            if (c != null) {
                final RenderableContainer newContainer = c.getParentContainer();
                if (newContainer == null) {
                    break;
                }
                prevC = c;
                c = newContainer;
            } else {
                break;
            }
        }
        return prevC;
    }

    private static RenderableContainer getPositionedAncestor(final RenderableContainer block) {
        RenderableContainer containingBlock = block;
        for (;;) {
            if (containingBlock instanceof Renderable) {
                final ModelNode node = ((Renderable) containingBlock).getModelNode();
                if (node instanceof HTMLElementImpl) {
                    final HTMLElementImpl element = (HTMLElementImpl) node;
                    if(element instanceof HTMLHtmlElement || element instanceof HTMLBodyElement || element.hasChildNodes()) break;
                    final int position = getPosition(element);
                    if (position != RenderState.POSITION_STATIC) {
                        break;
                    }
                    final RenderableContainer newContainer = containingBlock.getParentContainer();
                    if (newContainer == null) {
                        break;
                    }
                    containingBlock = newContainer;
                } else {
                    break;
                }
            } else {
                break;
            }
        }
        return containingBlock;
    }

    private void scheduleFloat(final RFloatInfo floatInfo) {
        final RLine line = this.currentLine;
        if (line == null) {
            final int y = this.paddingInsets.top;
            placeFloat(floatInfo.getRenderable(), y, floatInfo.isLeftFloat());
        } else if (line.getWidth() == 0) {
            final int y = line.getY();
            placeFloat(floatInfo.getRenderable(), y, floatInfo.isLeftFloat());
            final int leftOffset = fetchLeftOffset(y);
            final int rightOffset = fetchRightOffset(y);
            line.changeLimits(leftOffset, this.desiredWidth - leftOffset - rightOffset);
        } else {
            // These pending floats are positioned when
            // lineDone() is called.
            Collection<RFloatInfo> c = this.pendingFloats;
            if (c == null) {
                c = new LinkedList<>();
                this.pendingFloats = c;
            }
            c.add(floatInfo);
        }
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return "RBlockViewport[node=" + this.modelNode + "]";
    }

    /** {@inheritDoc} */
    @Override
    public Rectangle getClipBounds() {
        return ((RBlock) container).getClipBounds();
    }

    /** {@inheritDoc} */
    @Override
    public int getVisualHeight() {
        if (cachedVisualHeight != null) {
            return cachedVisualHeight;
        }

        final AtomicInteger maxY = new AtomicInteger(getHeight());
        final List<Renderable> renderables = getRenderables();
        if (renderables != null) {
            renderables.forEach(r -> {
                if (r instanceof BoundableRenderable) {
                    final BoundableRenderable br = (BoundableRenderable) r;
                    final double brMaxY = br.getVisualBounds().getMaxY();
                    if (brMaxY > maxY.get()) {
                        maxY.set((int) brMaxY);
                    }
                } else if (r instanceof RenderableContainer) {
                    final RenderableContainer rc = (RenderableContainer) r;
                    final double rcMaxY = rc.getVisualBounds().getMaxY();
                    if (rcMaxY > maxY.get()) {
                        maxY.set((int) rcMaxY);
                    }
                } else if (r instanceof PositionedRenderable) {
                    final PositionedRenderable rc = (PositionedRenderable) r;
                    final double rcMaxY = rc.getRenderable().getVisualBounds().getMaxY();
                    if (rcMaxY > maxY.get()) {
                        maxY.set((int) rcMaxY);
                    }
                }
            });
        }
        cachedVisualHeight = maxY.get();
        return cachedVisualHeight;
    }

    /** {@inheritDoc} */
    @Override
    public int getVisualWidth() {
        if (cachedVisualWidth != null) {
            return cachedVisualWidth;
        }

        final AtomicInteger maxX = new AtomicInteger(getWidth());
        final List<Renderable> renderables = getRenderables();
        if (renderables != null) {
            renderables.forEach(r -> {
                if (r instanceof BoundableRenderable) {
                    final BoundableRenderable br = (BoundableRenderable) r;
                    final double brMaxX = br.getVisualBounds().getMaxX();
                    if (brMaxX > maxX.get()) {
                        maxX.set((int) brMaxX);
                    }
                } else if (r instanceof RenderableContainer) {
                    final RenderableContainer rc = (RenderableContainer) r;
                    final double rcMaxX = rc.getVisualBounds().getMaxX();
                    if (rcMaxX > maxX.get()) {
                        maxX.set((int) rcMaxX);
                    }
                } else if (r instanceof PositionedRenderable) {
                    final PositionedRenderable rc = (PositionedRenderable) r;
                    final double rcMaxX = rc.getRenderable().getVisualBounds().getMaxX();
                    if (rcMaxX >maxX.get()) {
                        maxX.set((int) rcMaxX);
                    }
                }
            });
        }
        cachedVisualWidth = maxX.get();
        return cachedVisualWidth;
    }

    /**
     * <p>positionDelayed.</p>
     */
    public void positionDelayed() {
        final List<DelayedPair> delayedPairs = container.getDelayedPairs();
        if (ArrayUtilities.isNotBlank(delayedPairs)) {
            delayedPairs.stream().filter(pair -> pair.getContainingBlock() == container).forEach(this::importDelayedPair);
        }
    }
}