StyleEngine.java
package swingtree.style;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import sprouts.Pair;
import swingtree.SwingTree;
import swingtree.UI;
import swingtree.layout.Bounds;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
/**
* Orchestrates the rendering of a component's style and animations. <br>
* Note that this class is immutable so that it is easier to reason about...
*/
final class StyleEngine
{
private static final Logger log = org.slf4j.LoggerFactory.getLogger(StyleEngine.class);
private static final UI.Layer[] ALL_LAYERS = UI.Layer.values();
static StyleEngine create() {
return new StyleEngine(new Pooled<>(BoxModelConf.none()), new Pooled<>(ComponentConf.none()), null);
}
static boolean IS_ANTIALIASING_ENABLED(){
return UI.scale() < 2.25;
}
private final Pooled<BoxModelConf> _boxModelConf;
private final Pooled<ComponentConf> _componentConf;
private final LayerCache[] _layerCaches;
private StyleEngine(
Pooled<BoxModelConf> boxModelConf,
Pooled<ComponentConf> componentConf,
@Nullable LayerCache[] layerCaches // Null when the style engine is freshly created
) {
_boxModelConf = Objects.requireNonNull(boxModelConf).intern();
_componentConf = Objects.requireNonNull(componentConf).intern();
if ( layerCaches == null ) {
layerCaches = new LayerCache[ALL_LAYERS.length];
for ( int i = 0; i < layerCaches.length; i++ )
layerCaches[i] = new LayerCache(ALL_LAYERS[i]);
}
_layerCaches = Objects.requireNonNull(layerCaches);
}
ComponentConf getComponentConf() { return _componentConf.get(); }
LayerCache[] getLayerCaches() { return _layerCaches; }
BoxModelConf getBoxModelConf() { return _boxModelConf.get(); }
Optional<Shape> componentAreaIfCalculated( UI.ComponentArea area ) {
ComponentAreas _areas = ComponentAreas.of(_boxModelConf);
if ( _areas.areaExists(area) )
return Optional.ofNullable(_areas.get(area));
if ( area == UI.ComponentArea.BODY ) {
if ( _componentConf.get().style().margin().isPositive() )
return Optional.ofNullable(_areas.get(area));
if ( _componentConf.get().style().border().hasAnyNonZeroArcs() )
return Optional.ofNullable(_areas.get(area));
}
return Optional.empty();
}
StyleEngine update(
final Bounds newBounds,
final StyleConf newStyle,
final Outline marginCorrection
) {
final StyleEngine engine = this;
final ComponentConf currentConf = engine.getComponentConf();
final Pair<BoxModelConf, ComponentConf> boxModelAndComponentConfs = _calculateBoxModelAndComponentConfs(newBounds, newStyle, marginCorrection, currentConf);
final BoxModelConf newBoxModelConf = boxModelAndComponentConfs.first();
final ComponentConf newConf = boxModelAndComponentConfs.second();
final LayerCache[] layerCaches = engine.getLayerCaches();
for ( LayerCache layerCache : layerCaches )
layerCache.validate(currentConf, newConf);
return new StyleEngine(new Pooled<>(newBoxModelConf), new Pooled<>(newConf), _layerCaches);
}
static sprouts.Pair<BoxModelConf, ComponentConf> _calculateBoxModelAndComponentConfs(
final Bounds newBounds,
final StyleConf newStyle,
final Outline marginCorrection,
final ComponentConf previousConf
) {
final boolean sameStyle = previousConf.style().equals(newStyle);
final boolean sameBounds = previousConf.currentBounds().equals(newBounds);
final boolean sameCorrection = previousConf.areaMarginCorrection().equals(marginCorrection);
ComponentConf newConf;
if ( sameStyle && sameBounds && sameCorrection )
newConf = previousConf;
else
newConf = new ComponentConf(newStyle, newBounds, marginCorrection);
BoxModelConf newBoxModelConf = BoxModelConf.of(newConf.style().border(), newConf.areaMarginCorrection(), newConf.currentBounds().size());
return Pair.of(newBoxModelConf, newConf);
}
void renderBackgroundStyle( Graphics2D g2d, @Nullable BufferedImage parentRendering, int x, int y ) {
// A component may have a filter on the parent:
if ( parentRendering != null ) {
FilterConf filter = _componentConf.get().style().layers().filter();
if ( !filter.equals(FilterConf.none()) ) {
// Location relative to the parent:
try {
StyleRenderer.renderParentFilter(filter, parentRendering, g2d, x, y, _boxModelConf);
} catch ( Exception ex ) {
log.error(SwingTree.get().logMarker(), "Exception while trying to apply and render parent filter!", ex);
}
}
}
_render(UI.Layer.BACKGROUND, g2d);
}
void paintBorder( Graphics2D g2d, Consumer<Graphics> formerBorderPainter ) {
_render(UI.Layer.CONTENT, g2d);
_render(UI.Layer.BORDER, g2d);
try {
formerBorderPainter.accept(g2d);
} catch ( Exception ex ) {
/*
Note that if any exceptions happen during the border style painting,
then we don't want to mess up how the rest of the component is painted...
Therefore, we catch any exceptions that happen in the above code.
*/
log.error(SwingTree.get().logMarker(), "Exception while painting former border!", ex);
}
}
void paintForeground( Graphics2D g2d ) {
_render(UI.Layer.FOREGROUND, g2d);
}
private void _render( UI.Layer layer, Graphics2D g2d ) {
final boolean antialiasingEnabled = IS_ANTIALIASING_ENABLED();
// We remember if antialiasing was enabled before we render (only when we will touch the hint):
final boolean antialiasingWasEnabled;
if ( antialiasingEnabled ) {
antialiasingWasEnabled = g2d.getRenderingHint( RenderingHints.KEY_ANTIALIASING ) == RenderingHints.VALUE_ANTIALIAS_ON;
g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
} else {
antialiasingWasEnabled = false;
}
LayerCache cache = null;
switch ( layer ) {
case BACKGROUND: cache = _layerCaches[0]; break;
case CONTENT: cache = _layerCaches[1]; break;
case BORDER: cache = _layerCaches[2]; break;
case FOREGROUND: cache = _layerCaches[3]; break;
}
if ( cache != null )
cache.paint(g2d, (conf, graphics) -> {
StyleRenderer.renderStyleOn(layer, conf, graphics);
});
else
log.error(SwingTree.get().logMarker(),
"Layer cache is null for layer: {}",
layer, new Throwable("Stack trace for debugging purposes.")
);
// Reset antialiasing to its previous state (only when we changed it):
if ( antialiasingEnabled )
g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING,
antialiasingWasEnabled
? RenderingHints.VALUE_ANTIALIAS_ON
: RenderingHints.VALUE_ANTIALIAS_OFF
);
}
}