LayerRenderConf.java
package swingtree.style;
import com.google.errorprone.annotations.Immutable;
import swingtree.UI;
import swingtree.layout.Size;
import java.awt.Graphics2D;
import java.util.Objects;
import java.util.Optional;
/**
* An immutable snapshot of essential component state needed for rendering
* the style of a particular component layer using the {@link StyleRenderer} and its
* {@link StyleRenderer#renderStyleOn(UI.Layer, LayerRenderConf, Graphics2D)} method. <br>
* This (and all of its parts) is immutable to use it as a basis for caching.
* When the config changes compared to the previous one, the image buffer based
* render cache is being invalidated and the component is rendered again
* (potentially with a new cached image buffer).
* <p>
* <b>Warning to maintainers:</b> the component {@link swingtree.layout.Size} carried by the
* {@link #boxModel()} must remain the <i>only</i> size dependent property reachable from here.
* {@link LayerPartitionCache} relies on it: for stretch tileable styles it derives a size independent
* cache key by swapping in a smaller size through {@link #withBoxModel(BoxModelConf)} and
* reconstructs any actual size from the resulting rendering. A newly added field whose value
* (or whose rendering) depends on the component size would silently break that reconstruction,
* producing subtly wrong pixels rather than a failure. Should such a field become necessary,
* {@link #_compactionFor(LayerRenderConf)} must return {@link Compaction#NONE} whenever it is
* set.
*/
@Immutable
@SuppressWarnings("Immutable")
final class LayerRenderConf
{
private static final int STRETCH_BAND = 2; // Freely stretchable band between the slice insets; one pixel suffices mathematically, two give slack.
private static final int SAFETY_MARGIN = 2; // Added to every slice inset to absorb antialiasing bleed and artifact adjustments in the renderer.
private static final LayerRenderConf _NONE = new LayerRenderConf(
BoxModelConf.none(),
BaseColorConf.none(),
StyleConfLayer.empty()
);
public static LayerRenderConf none() { return _NONE; }
private final Pooled<BoxModelConf> _boxModelConf;
private final BaseColorConf _baseColor;
private final StyleConfLayer _layer;
private final LazyRef<LayerRenderConf> _canonicalRepresentation;
private final LazyRef<Outline> _nineTileSliceInsets;
private LayerRenderConf(
BoxModelConf boxModelConf,
BaseColorConf base,
StyleConfLayer layers
) {
_boxModelConf = new Pooled<>(Objects.requireNonNull(boxModelConf)).intern();
_baseColor = Objects.requireNonNull(base);
_layer = Objects.requireNonNull(layers);
_canonicalRepresentation = new LazyRef<>(this, LayerRenderConf::_canonicalize);
_nineTileSliceInsets = new LazyRef<>(this, LayerRenderConf::_compute9PatchSliceInsets);
}
static LayerRenderConf of( UI.Layer layer, ComponentConf fullConf ) {
BoxModelConf boxModelConf = BoxModelConf.of(
fullConf.style().border(),
fullConf.areaMarginCorrection(),
fullConf.currentBounds().size()
);
BaseColorConf colorConf = BaseColorConf.of(
fullConf.style().base().foundationColor().filter( c -> layer == UI.Layer.BACKGROUND ).orElse(null),
fullConf.style().base().backgroundColor().filter( c -> layer == UI.Layer.BACKGROUND ).orElse(null),
layer == UI.Layer.BORDER ? fullConf.style().border().colors() : BorderColorsConf.none()
);
return of(
boxModelConf,
colorConf,
fullConf.style().layer(layer)
);
}
private static LayerRenderConf of(
final BoxModelConf boxModelConf,
final BaseColorConf base,
final StyleConfLayer layers
) {
if (
boxModelConf .equals( BoxModelConf.none() ) &&
base .equals( BaseColorConf.none() ) &&
layers .equals( _NONE._layer )
)
return _NONE;
else
return new LayerRenderConf(boxModelConf, base, layers);
}
BoxModelConf boxModel() { return _boxModelConf.get(); }
/**
* Returns a new {@link LayerRenderConf} with the supplied box model
* and everything else unchanged. Used by {@link LayerPartitionCache} to derive
* a size independent canonical render configuration from this one.
*/
LayerRenderConf withBoxModel( BoxModelConf boxModelConf ) {
if ( boxModelConf.equals(_boxModelConf.get()) )
return this;
return of(boxModelConf, _baseColor, _layer);
}
BaseColorConf baseColors() { return _baseColor; }
/**
* Returns a new {@link LayerRenderConf} with the supplied base colors and everything
* else unchanged. Used by {@link LayerRenderConfPartition} to narrow this configuration down to
* a single part of the layer.
*/
LayerRenderConf withBaseColors( BaseColorConf baseColors ) {
if ( baseColors.equals(_baseColor) )
return this;
return of(_boxModelConf.get(), baseColors, _layer);
}
StyleConfLayer layer() { return _layer; }
/**
* Returns a new {@link LayerRenderConf} with the supplied style layer and everything
* else unchanged. Used by {@link LayerRenderConfPartition} to narrow this configuration down to
* a single part of the layer.
*/
LayerRenderConf withLayer( StyleConfLayer layer ) {
if ( layer.equals(_layer) )
return this;
return of(_boxModelConf.get(), _baseColor, layer);
}
/** Whether handing this to the style renderer would put no pixels anywhere - a common
* outcome of narrowing a configuration down to a {@link LayerRenderConfPartition}. */
boolean rendersNothing() {
return _baseColor.equals(BaseColorConf.none()) && _layer.isNone();
}
ComponentAreas areas() { return ComponentAreas.of(_boxModelConf); }
LayerRenderConf canonicalRepresentation() {
return _canonicalRepresentation.get();
}
Outline nineTileSliceInsets() {
return _nineTileSliceInsets.get();
}
@Override
public int hashCode() {
return Objects.hash(_boxModelConf, _baseColor, _layer);
}
@Override
public boolean equals( Object o ) {
if ( o == this ) return true;
if ( o == null ) return false;
if ( o.getClass() != this.getClass() ) return false;
LayerRenderConf other = (LayerRenderConf) o;
return Objects.equals(_boxModelConf, other._boxModelConf) &&
Objects.equals(_baseColor, other._baseColor) &&
Objects.equals(_layer, other._layer);
}
@Override
public String toString() {
return getClass().getSimpleName()+"[" +
"boxModel=" + _boxModelConf.get() + ", " +
"baseColor=" + _baseColor + ", " +
"layer=" + _layer +
']';
}
// Canonical (size independent) representation for 9 patch caching:
/**
* Maps a configuration of any size onto its exemplar key. A dimension is compacted only if
* the layer repeats along it (see the {@link LayerPartitionCache} class documentation) and
* the component is larger than the exemplar in it, because a dimension the exemplar already
* fills has nothing left for the blit to stretch. A layer can repeat along the width while
* only the height is larger than the exemplar, and then neither dimension is compacted and
* the configuration comes back unchanged. Compacting a dimension also takes away its room
* to stretch, which is what makes this idempotent: a configuration already at the exemplar
* size maps onto itself.
*/
private static LayerRenderConf _canonicalize( LayerRenderConf conf ) {
Compaction compaction = _compactionFor(conf);
if ( compaction == Compaction.NONE )
return conf;
final Outline sliceInsets = conf.nineTileSliceInsets();
final Size exemplar = _exemplarSize(sliceInsets);
final Size actual = conf.boxModel().size();
if ( !_borderEdgeSeamsAreSizeIndependent(conf, sliceInsets, exemplar) )
return conf;
compaction = compaction.and(Compaction._of(
actual.widthOrElse(0f) > exemplar.widthOrElse(0f),
actual.heightOrElse(0f) > exemplar.heightOrElse(0f)
));
if ( compaction == Compaction.NONE )
return conf;
final Size canonical = Size.of(
compaction.includesWidth() ? exemplar.widthOrElse(0f) : actual.widthOrElse(0f),
compaction.includesHeight() ? exemplar.heightOrElse(0f) : actual.heightOrElse(0f)
);
return conf.withBoxModel(conf.boxModel().withSize(canonical));
}
/**
* Which of an exemplar's two dimensions we compacted to their minimum instead of taking
* them from the component, and stretch back on paint. A resize in a compacted dimension
* keeps the key; a resize in an uncompacted dimension gives a new one.
*/
enum Compaction
{
NONE(false, false), WIDTH(true, false), HEIGHT(false, true), BOTH(true, true);
static Compaction between( Size key, Size actual ) {
return _of(
key.widthOrElse(0f) != actual.widthOrElse(0f),
key.heightOrElse(0f) != actual.heightOrElse(0f)
);
}
private final boolean _width;
private final boolean _height;
Compaction( boolean width, boolean height ) {
_width = width;
_height = height;
}
boolean includesWidth() { return _width; }
boolean includesHeight() { return _height; }
/** The dimensions both of them allow, for a layer that has to satisfy two of these at once. */
Compaction and( Compaction other ) {
return _of(_width && other._width, _height && other._height);
}
private static Compaction _of( boolean width, boolean height ) {
if ( width )
return height ? BOTH : WIDTH;
else
return height ? HEIGHT : NONE;
}
}
/**
* How far the size dependent pixels reach into the component from each side; everything
* between opposite insets repeats and may be stretched freely. A pure function of the size
* independent parts of the configuration, so the blit can recompute it and is guaranteed
* to agree with the canonicalization.
*/
private static Outline _compute9PatchSliceInsets(LayerRenderConf conf ) {
final BoxModelConf box = conf.boxModel();
final float marginTop = _positive(box.margin().top());
final float marginRight = _positive(box.margin().right());
final float marginBottom = _positive(box.margin().bottom());
final float marginLeft = _positive(box.margin().left());
final float baseTop = _positive(box.baseOutline().top());
final float baseRight = _positive(box.baseOutline().right());
final float baseBottom = _positive(box.baseOutline().bottom());
final float baseLeft = _positive(box.baseOutline().left());
final float widthTop = _positive(box.widths().top());
final float widthRight = _positive(box.widths().right());
final float widthBottom = _positive(box.widths().bottom());
final float widthLeft = _positive(box.widths().left());
// Per side, the larger of the two adjacent corner arc extents:
final float arcTop = Math.max(_arcHeight(box.topLeftArc()), _arcHeight(box.topRightArc()) );
final float arcRight = Math.max(_arcWidth(box.topRightArc()), _arcWidth(box.bottomRightArc()) );
final float arcBottom = Math.max(_arcHeight(box.bottomLeftArc()), _arcHeight(box.bottomRightArc()));
final float arcLeft = Math.max(_arcWidth(box.topLeftArc()), _arcWidth(box.bottomLeftArc()) );
/*
A shadow's 2D-varying pixels extend beyond its geometric box: its gradients
fade over blur + spread + gradient start offset, and the whole shadow box is
displaced by the shadow offset. We conservatively use the same reach for all
four sides (overestimation only costs a few exemplar pixels).
*/
float shadowReachH = 0;
float shadowReachV = 0;
for ( ShadowConf shadow : conf.layer().shadows().sortedByNames() ) {
if ( shadow.equals(ShadowConf.none()) || !shadow.color().isPresent() )
continue;
final float blur = Math.max(0, shadow.blurRadius());
final float spread = Math.abs(shadow.spreadRadius());
final float fade = StyleRenderer.shadowGradientStartOffset(box, shadow);
shadowReachH = Math.max(shadowReachH, blur + spread + fade + Math.abs(shadow.horizontalOffset()));
shadowReachV = Math.max(shadowReachV, blur + spread + fade + Math.abs(shadow.verticalOffset()));
}
final float top = marginTop + baseTop + widthTop + arcTop + shadowReachV + SAFETY_MARGIN;
final float right = marginRight + baseRight + widthRight + arcRight + shadowReachH + SAFETY_MARGIN;
final float bottom = marginBottom + baseBottom + widthBottom + arcBottom + shadowReachV + SAFETY_MARGIN;
final float left = marginLeft + baseLeft + widthLeft + arcLeft + shadowReachH + SAFETY_MARGIN;
return Outline.of(
(float) Math.ceil(top),
(float) Math.ceil(right),
(float) Math.ceil(bottom),
(float) Math.ceil(left)
);
}
/** Which dimensions we may compact: we can shrink the width when every pixel strip along
* the y axis is identical, and the height when every pixel strip along the x axis is. */
private static Compaction _compactionFor( LayerRenderConf conf ) {
final StyleConfLayer layer = conf.layer();
for ( Pooled<NoiseConf> noise : layer.noises().sortedByNames() )
if ( !noise.get().equals(NoiseConf.none()) )
return Compaction.NONE; // Noise varies per pixel position.
for ( ImageConf image : layer.images().sortedByNames() )
if ( !image.equals(ImageConf.none()) )
return Compaction.NONE; // Image placement/fit depends on the component bounds.
for ( TextConf text : layer.texts().sortedByNames() )
if ( !text.equals(TextConf.none()) )
return Compaction.NONE; // Text layout depends on the component bounds.
for ( PainterConf painter : layer.painters().sortedByNames() )
if ( !painter.equals(PainterConf.none()) )
return Compaction.NONE; // We cannot know what a custom painter does.
Compaction compaction = Compaction.BOTH;
for ( GradientConf gradient : layer.gradients().sortedByNames() ) {
if ( gradient.equals(GradientConf.none()) )
continue;
compaction = compaction.and(_compactionAllowedBy(gradient));
if ( compaction == Compaction.NONE )
return compaction;
}
return compaction;
}
/**
* A linear gradient straight down a component is built by
* {@link StyleRenderer#createGradientPaint(BoxModelConf, GradientConf)} from two points
* sharing an x coordinate, so the component width never enters its rendering and we can
* compact the width; a gradient straight across is the same with the axes swapped. We turn
* down every other kind, because they vary with x and y at once -
* {@link UI.ComponentBoundary#CENTER_TO_CONTENT} because it derives its insets from the
* component size itself.
*/
private static Compaction _compactionAllowedBy( GradientConf gradient ) {
if ( gradient.type() != UI.GradientType.LINEAR )
return Compaction.NONE;
if ( gradient.rotation() % 360f != 0f )
return Compaction.NONE;
if ( gradient.boundary() == UI.ComponentBoundary.CENTER_TO_CONTENT )
return Compaction.NONE;
switch ( gradient.span() ) {
case TOP_TO_BOTTOM:
case BOTTOM_TO_TOP:
return Compaction.WIDTH;
case LEFT_TO_RIGHT:
case RIGHT_TO_LEFT:
return Compaction.HEIGHT;
default:
return Compaction.NONE;
}
}
/**
* Whether the seams of a border with a different color per edge fall in the same place in
* the exemplar as they do at any larger size, which is what lets such a border be
* reconstructed from an exemplar rather than re-rendered per size. <br>
* <br>
* Note that the border edges divide the <i>margin box</i> rather than the component: the
* seams are placed within, and the slice insets measured from, two different origins, so the
* margins have to be taken out of both before they can be compared.
* @see ComponentAreas#getEdgeAreas() For more context related code...
*/
private static boolean _borderEdgeSeamsAreSizeIndependent(
LayerRenderConf conf,
Outline sliceInsets,
Size exemplar
) {
final BorderColorsConf borderColors = conf.baseColors().borderColor();
if ( borderColors.equals(BorderColorsConf.none()) || borderColors.isHomogeneous() )
return true;
if ( !conf.boxModel().hasAnyNonZeroArcs() )
return true;
final Outline widths = conf.boxModel().widths();
final float top = _positive(widths.top());
final float right = _positive(widths.right());
final float bottom = _positive(widths.bottom());
final float left = _positive(widths.left());
if ( top <= 0 || right <= 0 || bottom <= 0 || left <= 0 )
return false;
final Outline margin = conf.boxModel().margin();
final float marginTop = _positive(margin.top());
final float marginRight = _positive(margin.right());
final float marginBottom = _positive(margin.bottom());
final float marginLeft = _positive(margin.left());
final float boxWidth = exemplar.widthOrElse(0f) - marginLeft - marginRight;
final float boxHeight = exemplar.heightOrElse(0f) - marginTop - marginBottom;
if ( boxWidth <= 0 || boxHeight <= 0 )
return false;
return boxHeight * top > ( _positive(sliceInsets.top()) - marginTop ) * ( top + bottom )
&& boxHeight * bottom > ( _positive(sliceInsets.bottom()) - marginBottom ) * ( top + bottom )
&& boxWidth * left > ( _positive(sliceInsets.left()) - marginLeft ) * ( left + right )
&& boxWidth * right > ( _positive(sliceInsets.right()) - marginRight ) * ( left + right );
}
/**
* The exemplar size for the supplied slice insets: {@code 2 * max(insetA, insetB) + band}
* per axis. Symmetric, because some rendering internals split their work at the component
* <i>center</i> - corner shadow clip boxes meet there, and so do the seams between two
* opposite border edges. The symmetric size guarantees the exemplar's center line falls
* into the repeating band, keeping those artifacts pixel-identical to a real rendering of
* any larger size.
*/
private static Size _exemplarSize( Outline sliceInsets ) {
final float maxHorizontal = Math.max(sliceInsets.left().orElse(0f), sliceInsets.right().orElse(0f));
final float maxVertical = Math.max(sliceInsets.top().orElse(0f), sliceInsets.bottom().orElse(0f));
return Size.of(
2 * maxHorizontal + STRETCH_BAND,
2 * maxVertical + STRETCH_BAND
);
}
private static float _positive( Optional<Float> value ) {
return Math.max(0f, value.orElse(0f));
}
private static float _arcWidth( Optional<Arc> arc ) {
return arc.map( a -> Math.max(0f, a.width()) ).orElse(0f);
}
private static float _arcHeight( Optional<Arc> arc ) {
return arc.map( a -> Math.max(0f, a.height()) ).orElse(0f);
}
}