ComponentAreas.java

package swingtree.style;

import org.jspecify.annotations.Nullable;
import swingtree.UI;
import swingtree.layout.Size;

import java.awt.Polygon;
import java.awt.geom.Arc2D;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.WeakHashMap;

/**
 *  A wrapper object for transient reference based caching of the various areas of a component.
 *  This is used to avoid recalculating the areas of a component over and over again
 *  if they don't change. (This is also shared between multiple components)
 */
final class ComponentAreas
{
    private static final Map<BoxModelConf, ComponentAreas> _CACHE = new WeakHashMap<>();

    private final LazyRef<Area>   _borderArea;
    private final LazyRef<Area>   _interiorArea;
    private final LazyRef<Area>   _exteriorArea;
    private final LazyRef<Area>   _bodyArea;
    private final LazyRef<Area[]> _borderEdgeAreas;
    private final WeakReference<BoxModelConf> _key;


    static ComponentAreas of( BoxModelConf state ) {
        return _CACHE.computeIfAbsent(state, conf -> new ComponentAreas(state));
    }

    static BoxModelConf intern(BoxModelConf state ) {
        ComponentAreas areas = _CACHE.get(state);
        if ( areas != null ) {
            BoxModelConf key = areas._key.get();
            if ( key != null )
                return key;
        }
        _CACHE.put(state, new ComponentAreas(state));
        return state;
    }

    private ComponentAreas(BoxModelConf conf) {
        this(
            conf,
            new LazyRef<>(new CacheProducerAndValidator<Area>(){
                @Override
                public Area produce(BoxModelConf currentState, ComponentAreas currentAreas) {
                    Area componentArea = currentAreas._interiorArea.getFor(currentState, currentAreas);
                    Area borderArea = new Area(currentAreas._bodyArea.getFor(currentState, currentAreas));
                    borderArea.subtract(componentArea);
                    return borderArea;
                }
            }),
            new LazyRef<>(new CacheProducerAndValidator<Area>(){
        
                @Override
                public Area produce(BoxModelConf currentState, ComponentAreas currentAreas) {
                    Outline widths = currentState.widths();
                    float leftBorderWidth   = widths.left().orElse(0f);
                    float topBorderWidth    = widths.top().orElse(0f);
                    float rightBorderWidth  = widths.right().orElse(0f);
                    float bottomBorderWidth = widths.bottom().orElse(0f);
                    return calculateComponentBodyArea(
                               currentState,
                               topBorderWidth,
                               leftBorderWidth,
                               bottomBorderWidth,
                               rightBorderWidth
                           );
                }
            }),
            new LazyRef<>(new CacheProducerAndValidator<Area>(){
                @Override
                public Area produce(BoxModelConf currentState, ComponentAreas currentAreas) {
                    Size size = currentState.size();
                    float width  = size.width().orElse(0f);
                    float height = size.height().orElse(0f);
                    Area exteriorComponentArea = new Area(new Rectangle2D.Float(0, 0, width, height));
                    exteriorComponentArea.subtract(currentAreas._bodyArea.getFor(currentState, currentAreas));
                    return exteriorComponentArea;
                }
            }),
            new LazyRef<>(new CacheProducerAndValidator<Area>(){
                @Override
                public Area produce(BoxModelConf currentState, ComponentAreas currentAreas) {
                    return calculateComponentBodyArea(currentState, 0, 0, 0, 0);
                }
            })
        );
    }
    
    public ComponentAreas(
        BoxModelConf conf,
        LazyRef<Area> borderArea,
        LazyRef<Area> interiorComponentArea,
        LazyRef<Area> exteriorComponentArea,
        LazyRef<Area> componentBodyArea
    ) {
        _key             = new WeakReference<>(conf);
        _borderArea      = Objects.requireNonNull(borderArea);
        _interiorArea    = Objects.requireNonNull(interiorComponentArea);
        _exteriorArea    = Objects.requireNonNull(exteriorComponentArea);
        _bodyArea        = Objects.requireNonNull(componentBodyArea);
        _borderEdgeAreas = new LazyRef<>((currentState, currentAreas) -> calculateEdgeBorderAreas(currentState));
    }


    public @Nullable Area get( UI.ComponentArea areaType ) {
        BoxModelConf boxModel = Optional.ofNullable(_key.get()).orElse(BoxModelConf.none());
        switch ( areaType ) {
            case ALL:
                return null; // No clipping
            case BODY:
                return _bodyArea.getFor(boxModel, this); // all - exterior == interior + border
            case INTERIOR:
                return _interiorArea.getFor(boxModel, this); // all - exterior - border == content - border
            case BORDER:
                return _borderArea.getFor(boxModel, this); // all - exterior - interior
            case EXTERIOR:
                return _exteriorArea.getFor(boxModel, this); // all - border - interior
            default:
                return null;
        }
    }

    public Area[] getEdgeAreas() {
        BoxModelConf boxModel = Optional.ofNullable(_key.get()).orElse(BoxModelConf.none());
        return _borderEdgeAreas.getFor(boxModel, this);
    }

    public boolean bodyAreaExists() {
        return _bodyArea.exists();
    }

    static Area calculateComponentBodyArea(BoxModelConf state, float insTop, float insLeft, float insBottom, float insRight )
    {
        return _calculateComponentBodyArea(
                    state,
                    insTop,
                    insLeft,
                    insBottom,
                    insRight
                );
    }

    private static Area _calculateComponentBodyArea(
        final BoxModelConf border,
        float insTop,
        float insLeft,
        float insBottom,
        float insRight
    ) {
        final Outline margin  = border.margin();
        final Size    size    = border.size();
        final Outline outline = border.baseOutline();

        if ( BoxModelConf.none().equals(border) ) {
            Outline insets = outline.plus(margin).plus(Outline.of(insTop, insLeft, insBottom, insRight));
            // If there is no style, we just return the component's bounds:
            return new Area(new Rectangle2D.Float(
                            insets.left().orElse(0f),
                            insets.top().orElse(0f),
                            size.width().orElse(0f) - insets.left().orElse(0f) - insets.right().orElse(0f),
                            size.height().orElse(0f) - insets.top().orElse(0f) - insets.bottom().orElse(0f)
                        ));
        }

        insTop    += outline.top().orElse(0f);
        insLeft   += outline.left().orElse(0f);
        insBottom += outline.bottom().orElse(0f);
        insRight  += outline.right().orElse(0f);

        // The background box is calculated from the margins and border radius:
        float left   = Math.max(margin.left().orElse(0f), 0)   + insLeft  ;
        float top    = Math.max(margin.top().orElse(0f), 0)    + insTop   ;
        float right  = Math.max(margin.right().orElse(0f), 0)  + insRight ;
        float bottom = Math.max(margin.bottom().orElse(0f), 0) + insBottom;
        float width  = size.width().orElse(0f);
        float height = size.height().orElse(0f);

        boolean insAllTheSame = insTop == insLeft && insLeft == insBottom && insBottom == insRight;

        if ( border.allCornersShareTheSameArc() && insAllTheSame ) {
            float arcWidth  = border.topLeftArc().map( a -> Math.max(0,a.width() ) ).orElse(0f);
            float arcHeight = border.topLeftArc().map( a -> Math.max(0,a.height()) ).orElse(0f);
            arcWidth  = Math.max(0, arcWidth  - insTop);
            arcHeight = Math.max(0, arcHeight - insTop);
            if ( arcWidth == 0 || arcHeight == 0 )
                return new Area(new Rectangle2D.Float(left, top, width - left - right, height - top - bottom));

            // We can return a simple round rectangle:
            return new Area(new RoundRectangle2D.Float(
                                left, top,
                                width - left - right, height - top - bottom,
                                arcWidth, arcHeight
                            ));
        } else {
            Arc topLeftArc     = border.topLeftArc().orElse(null);
            Arc topRightArc    = border.topRightArc().orElse(null);
            Arc bottomRightArc = border.bottomRightArc().orElse(null);
            Arc bottomLeftArc  = border.bottomLeftArc().orElse(null);
            Area area = new Area();

            float topLeftRoundnessAdjustment     = Math.min(insLeft,   insTop  );
            float topRightRoundnessAdjustment    = Math.min(insTop,    insRight);
            float bottomRightRoundnessAdjustment = Math.min(insBottom, insRight);
            float bottomLeftRoundnessAdjustment  = Math.min(insBottom, insLeft );

            float arcWidthTL  = Math.max(0, topLeftArc     == null ? 0 : topLeftArc.width()      - topLeftRoundnessAdjustment);
            float arcHeightTL = Math.max(0, topLeftArc     == null ? 0 : topLeftArc.height()     - topLeftRoundnessAdjustment);
            float arcWidthTR  = Math.max(0, topRightArc    == null ? 0 : topRightArc.width()     - topRightRoundnessAdjustment);
            float arcHeightTR = Math.max(0, topRightArc    == null ? 0 : topRightArc.height()    - topRightRoundnessAdjustment);
            float arcWidthBR  = Math.max(0, bottomRightArc == null ? 0 : bottomRightArc.width()  - bottomRightRoundnessAdjustment);
            float arcHeightBR = Math.max(0, bottomRightArc == null ? 0 : bottomRightArc.height() - bottomRightRoundnessAdjustment);
            float arcWidthBL  = Math.max(0, bottomLeftArc  == null ? 0 : bottomLeftArc.width()   - bottomLeftRoundnessAdjustment);
            float arcHeightBL = Math.max(0, bottomLeftArc  == null ? 0 : bottomLeftArc.height()  - bottomLeftRoundnessAdjustment);

            // Top left:
            if ( topLeftArc != null ) {
                area.add(new Area(new Arc2D.Float(
                        left, top,
                        arcWidthTL, arcHeightTL,
                        90, 90, Arc2D.PIE
                )));
            }
            // Top right:
            if ( topRightArc != null ) {
                area.add(new Area(new Arc2D.Float(
                        width - right - topRightArc.width() + topRightRoundnessAdjustment,
                        top,
                        arcWidthTR, arcHeightTR,
                        0, 90, Arc2D.PIE
                )));
            }
            // Bottom right:
            if ( bottomRightArc != null ) {
                area.add(new Area(new Arc2D.Float(
                        width  - right  - bottomRightArc.width()  + bottomRightRoundnessAdjustment,
                        height - bottom - bottomRightArc.height() + bottomRightRoundnessAdjustment,
                        arcWidthBR, arcHeightBR,
                        270, 90, Arc2D.PIE
                )));
            }
            // Bottom left:
            if ( bottomLeftArc != null ) {
                area.add(new Area(new Arc2D.Float(
                        left,
                        height - bottom - bottomLeftArc.height() + bottomLeftRoundnessAdjustment,
                        arcWidthBL, arcHeightBL,
                        180, 90, Arc2D.PIE
                )));
            }
            /*
                Now we are going to have to fill four rectangles for each side of the partially rounded background box
                and then a single rectangle for the center.
                The four outer rectangles are calculated from the arcs and the margins.
             */
            float topDistance    = 0;
            float rightDistance  = 0;
            float bottomDistance = 0;
            float leftDistance   = 0;
            // top:
            if ( topLeftArc != null || topRightArc != null ) {
                float arcWidthLeft   = (arcWidthTL  / 2f);
                float arcHeightLeft  = (arcHeightTL / 2f);
                float arcWidthRight  = (arcWidthTR  / 2f);
                float arcHeightRight = (arcHeightTR / 2f);
                topDistance = Math.max(arcHeightLeft, arcHeightRight);// This is where the center rectangle will start!
                float innerLeft   = left + arcWidthLeft;
                float innerRight  = width - right - arcWidthRight;
                float edgeRectangleHeight = topDistance;
                area.add(new Area(new Rectangle2D.Float(
                        innerLeft, top, innerRight - innerLeft, edgeRectangleHeight
                    )));
            }
            // right:
            if ( topRightArc != null || bottomRightArc != null ) {
                float arcWidthTop    = (arcWidthTR  / 2f);
                float arcHeightTop   = (arcHeightTR / 2f);
                float arcWidthBottom = (arcWidthBR  / 2f);
                float arcHeightBottom= (arcHeightBR / 2f);
                rightDistance = Math.max(arcWidthTop, arcWidthBottom);// This is where the center rectangle will start!
                float innerTop    = top + arcHeightTop;
                float innerBottom = height - bottom - arcHeightBottom;
                float edgeRectangleWidth = rightDistance;
                area.add(new Area(new Rectangle2D.Float(
                        width - right - edgeRectangleWidth, innerTop, edgeRectangleWidth, innerBottom - innerTop
                    )));
            }
            // bottom:
            if ( bottomRightArc != null || bottomLeftArc != null ) {
                float arcWidthRight  = (arcWidthBR  / 2f);
                float arcHeightRight = (arcHeightBR / 2f);
                float arcWidthLeft   = (arcWidthBL  / 2f);
                float arcHeightLeft  = (arcHeightBL / 2f);
                bottomDistance = Math.max(arcHeightRight, arcHeightLeft);// This is where the center rectangle will start!
                float innerLeft   = left + arcWidthLeft;
                float innerRight  = width - right - arcWidthRight;
                float edgeRectangleHeight = bottomDistance;
                area.add(new Area(new Rectangle2D.Float(
                        innerLeft, height - bottom - edgeRectangleHeight, innerRight - innerLeft, edgeRectangleHeight
                    )));
            }
            // left:
            if ( bottomLeftArc != null || topLeftArc != null ) {
                float arcWidthBottom = (arcWidthBL  / 2f);
                float arcHeightBottom= (arcHeightBL / 2f);
                float arcWidthTop    = (arcWidthTL  / 2f);
                float arcHeightTop   = (arcHeightTL / 2f);
                leftDistance = Math.max(arcWidthBottom, arcWidthTop);// This is where the center rectangle will start!
                float innerTop    = top + arcHeightTop;
                float innerBottom = height - bottom - arcHeightBottom;
                float edgeRectangleWidth = leftDistance;
                area.add(new Area(new Rectangle2D.Float(
                        left, innerTop, edgeRectangleWidth, innerBottom - innerTop
                    )));
            }
            // Now we add the center:
            area.add(new Area(
                        new Rectangle2D.Float(
                            left + leftDistance, top + topDistance,
                            width - left - leftDistance - right - rightDistance,
                            height - top - topDistance - bottom - bottomDistance
                        )
                    ));
            return area;
        }
    }


    /**
     *  Calculates the border-edge areas of the components box model in the form of
     *  an array of 4 {@link Area} objects, each representing the area of a single edge.
     *  So the top, right, bottom and left edge areas are returned in that order.
     *  <p>
     *  Each area is essentially just a polygon which consists of 5 points,
     *  two of which are the margin based border corners and the other three
     *  are the inner border width based corners as well as a center point.
     *
     * @param boxModel The box model of the component
     * @return An array of 4 {@link Area} objects representing the border-edge areas
     */
    private static Area[] calculateEdgeBorderAreas( BoxModelConf boxModel) {
        final Size    size   = boxModel.size();
        final Outline margin = boxModel.margin();
        final Outline widths = boxModel.widths();
        final float   width  = size.width().orElse(0f);
        final float   height = size.height().orElse(0f);

        final float topLeftX     = margin.left().orElse(0f);
        final float topLeftY     = margin.top().orElse(0f);
        final float topRightX    = width - margin.right().orElse(0f);
        final float topRightY    = topLeftY;
        final float bottomLeftX  = topLeftX;
        final float bottomLeftY  = height - margin.bottom().orElse(0f);
        final float bottomRightX = topRightX;
        final float bottomRightY = bottomLeftY;

        final float innerTopLeftX     = topLeftX + widths.left().orElse(0f);
        final float innerTopLeftY     = topLeftY + widths.top().orElse(0f);
        final float innerTopRightX    = topRightX - widths.right().orElse(0f);
        final float innerTopRightY    = innerTopLeftY;
        final float innerBottomLeftX  = bottomLeftX + widths.left().orElse(0f);
        final float innerBottomLeftY  = bottomLeftY - widths.bottom().orElse(0f);
        final float innerBottomRightX = bottomRightX - widths.right().orElse(0f);
        final float innerBottomRightY = innerBottomLeftY;

        final float innerCenterX = (innerTopLeftX + innerTopRightX) / 2f;
        final float innerCenterY = (innerTopLeftY + innerBottomLeftY) / 2f;

        Area[] edgeAreas = new Area[4];
        { // TOP:
            edgeAreas[0] = new Area(new Polygon(
                new int[] {(int)innerCenterX, (int)innerTopLeftX, (int)topLeftX, (int)topRightX, (int)innerTopRightX},
                new int[] {(int)innerCenterY, (int)innerTopLeftY, (int)topLeftY, (int)topRightY, (int)innerTopRightY},
                5
            ));
        }
        { // RIGHT:
            edgeAreas[1] = new Area(new Polygon(
                new int[] {(int)innerCenterX, (int)innerTopRightX, (int)topRightX, (int)bottomRightX, (int)innerBottomRightX},
                new int[] {(int)innerCenterY, (int)innerTopRightY, (int)topRightY, (int)bottomRightY, (int)innerBottomRightY},
                5
            ));
        }
        { // BOTTOM:
            edgeAreas[2] = new Area(new Polygon(
                new int[] {(int)innerCenterX, (int)innerBottomRightX, (int)bottomRightX, (int)bottomLeftX, (int)innerBottomLeftX},
                new int[] {(int)innerCenterY, (int)innerBottomRightY, (int)bottomRightY, (int)bottomLeftY, (int)innerBottomLeftY},
                5
            ));
        }
        { // LEFT:
            edgeAreas[3] = new Area(new Polygon(
                new int[] {(int)innerCenterX, (int)innerBottomLeftX, (int)bottomLeftX, (int)topLeftX, (int)innerTopLeftX},
                new int[] {(int)innerCenterY, (int)innerBottomLeftY, (int)bottomLeftY, (int)topLeftY, (int)innerTopLeftY},
                5
            ));
        }
        return edgeAreas;
    }

}