ShadowConf.java
package swingtree.style;
import com.google.errorprone.annotations.Immutable;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import swingtree.UI;
import java.awt.Color;
import java.util.Objects;
import java.util.Optional;
/**
* An immutable config API
* designed for defining shadow styles
* as part of the full {@link StyleConf} configuration object.
* The state of this object can only be updated by using wither like update methods,
* like {@link #horizontalOffset(double)}, {@link #verticalOffset(double)}, {@link #blurRadius(double)}...
* which return a new instance of this class with the updated state.
* <p>
* The following properties with their respective purpose are available:
* <br>
* <ol>
* <li><b>Horizontal Offset</b>
* <p>
* The horizontal shadow offset, if positive the shadow will move to the right,
* if negative the shadow will move to the left.
* </p>
* </li>
* <li><b>Vertical Offset</b>
* <p>
* The vertical shadow offset, if positive the shadow will move down,
* if negative the shadow will move up.
* </p>
* </li>
* <li><b>Blur Radius</b>
* <p>
* The blur radius of the shadow, which defines the width of the blur effect.
* The higher the value, the bigger the blur, so the shadow transition will be
* stretched over a wider area.
* </p>
* </li>
* <li><b>Spread Radius</b>
* <p>
* The spread radius of the shadow defines how far inwards or
* outwards ({@link #isInset()}) the shadow begins.
* This offsets the start of the shadow similarly to the vertical and horizontal
* offsets, but instead of moving the shadow, it extends the shadow
* so that it either grows or shrinks in size.
* <br>
* You can imagine a shadow effect as a rectangular box, where the gradients of the shadow
* start at the edges of said box. The spread radius then defines the scale of the box,
* so that the shadow either grows or shrinks in size.
* </p>
* </li>
* <li><b>Color</b>
* <p>
* The color of the shadow.
* </p>
* </li>
* <li><b>Inset</b>
* <p>
* Whether the shadow is inset or outset.
* If true, the shadow is inset, otherwise it is outset.
* Inset shadows go inward, starting from the inner edge of the box (and its border),
* whereas outset shadows go outward, starting from the outer edge of the box's border.
* </p>
* </li>
* </ol>
* <p>
* Note that you can use the {@link #none()} method to specify that no shadow should be used,
* as the instance returned by that method is a shadow with no offset, no blur, no spread and no color,
* effectively making it a representation of the absence of a shadow.
*/
@Immutable
@SuppressWarnings("ReferenceEquality")
public final class ShadowConf implements Simplifiable<ShadowConf>
{
private static final Logger log = org.slf4j.LoggerFactory.getLogger(ShadowConf.class);
static final UI.Layer DEFAULT_LAYER = UI.Layer.CONTENT;
private static final ShadowConf _NONE = new ShadowConf(
Offset.none(),0, 0,
null, true
);
public static ShadowConf none() { return _NONE; }
static ShadowConf of(
Offset offset,
float shadowBlurRadius,
float shadowSpreadRadius,
@Nullable Color shadowColor,
boolean isOutset
) {
if (
offset == _NONE._offset &&
shadowBlurRadius == _NONE._blurRadius &&
shadowSpreadRadius == _NONE._spreadRadius &&
shadowColor == _NONE._color &&
isOutset == _NONE._isOutset
)
return _NONE;
else
return new ShadowConf(offset, shadowBlurRadius, shadowSpreadRadius, shadowColor, isOutset);
}
private final Offset _offset;
private final float _blurRadius;
private final float _spreadRadius;
private final @Nullable Color _color;
private final boolean _isOutset;
private ShadowConf(
Offset offset,
float shadowBlurRadius,
float shadowSpreadRadius,
@Nullable Color shadowColor,
boolean isOutset
) {
_offset = Objects.requireNonNull(offset);
_blurRadius = shadowBlurRadius;
_spreadRadius = shadowSpreadRadius;
_color = shadowColor;
_isOutset = isOutset;
}
float horizontalOffset() { return _offset.x(); }
float verticalOffset() { return _offset.y(); }
float blurRadius() { return _blurRadius; }
public float spreadRadius() { return _spreadRadius; }
Optional<Color> color() { return Optional.ofNullable(_color); }
boolean isOutset() { return _isOutset; }
boolean isInset() { return !_isOutset; }
/**
* Use this to offset the shadow position along the X axis.
* If the {@code horizontalShadowOffset} is positive, the shadow will move to the right,
* if negative the shadow will move to the left.
*
* @param horizontalShadowOffset The horizontal shadow offset, if positive the shadow will move to the right,
* if negative the shadow will move to the left.
* @return A new {@link ShadowConf} with the specified horizontal shadow offset.
*/
public ShadowConf horizontalOffset( double horizontalShadowOffset ) {
return ShadowConf.of(_offset.withX((float) horizontalShadowOffset), _blurRadius, _spreadRadius, _color, _isOutset);
}
/**
* Defines the shadow position along the Y axis in terms of the "vertical shadow offset".
* It will move the shadow up if negative and down if positive.
*
* @param verticalShadowOffset The vertical shadow offset, if positive the shadow will move down,
* if negative the shadow will move up.
* @return A new {@link ShadowConf} with the specified vertical shadow offset.
*/
public ShadowConf verticalOffset( double verticalShadowOffset ) {
return ShadowConf.of(_offset.withY((float) verticalShadowOffset), _blurRadius, _spreadRadius, _color, _isOutset);
}
/**
* Use this to offset the shadow position along the X or Y axis
* using the two supplied {@code horizontalShadowOffset} and {@code verticalShadowOffset} doubles.
* The {@code horizontalShadowOffset} will shift the shadow along the X axis,
* while the {@code verticalShadowOffset} will shift the shadow along the Y axis.
*
* @param horizontalShadowOffset The horizontal shadow offset, if positive the shadow will move to the right,
* if negative the shadow will move to the left.
* @param verticalShadowOffset The vertical shadow offset, if positive the shadow will move down,
* if negative the shadow will move up.
* @return A new {@link ShadowConf} with the specified horizontal and vertical shadow offsets.
*/
public ShadowConf offset( double horizontalShadowOffset, double verticalShadowOffset ) {
return ShadowConf.of(Offset.of(horizontalShadowOffset, verticalShadowOffset), _blurRadius, _spreadRadius, _color, _isOutset);
}
/**
* Use this to offset the shadow diagonally between the top left corner and the bottom right corner.
* This is effectively a diagonal shadow offset as it is applied to both the X and Y axis.
* (see {@link #offset(double, double)} for more information)
*
* @param shadowOffset The shadow offset, if positive the shadow will move to the right and down,
* if negative the shadow will move to the left and up.
* @return A new {@link ShadowConf} with the specified horizontal and vertical shadow offsets.
*/
public ShadowConf offset( double shadowOffset ) {
return ShadowConf.of(Offset.of(shadowOffset, shadowOffset), _blurRadius, _spreadRadius, _color, _isOutset);
}
/**
* The blur radius of a shadow defines the gap size between the start and end of the shadow gradient.
* The higher the value, the bigger the blur, so the shadow transition will extend further
* inwards or outwards ({@link #isInset()}) from the shadow center.
*
* @param shadowBlurRadius The blur radius of the shadow, which defines the width of the blur effect.
* The higher the value, the bigger the blur, so the shadow transition will be
* stretched over a wider area.
* @return A new {@link ShadowConf} with the specified blur radius.
*/
public ShadowConf blurRadius( double shadowBlurRadius ) {
return ShadowConf.of(_offset, (float) shadowBlurRadius, _spreadRadius, _color, _isOutset);
}
/**
* The spread radius of a shadow is a sort of scale for the shadow box.
* So when the spread radius is large the shadow will both begin and end further away from the shadow center.
* When the spread radius is small the shadow will be more concentrated around the shadow center.
*
* @param shadowSpreadRadius The spread radius of the shadow, which defines how far the shadow spreads
* outwards or inwards ({@link #isInset()}) from the element.
* This offsets the start of the shadow similarly to the vertical and horizontal
* offsets, but instead of moving the shadow, it extends the shadow
* so that it either grows or shrinks in size.
* @return A new {@link ShadowConf} with the specified spread radius.
*/
public ShadowConf spreadRadius( double shadowSpreadRadius ) {
return ShadowConf.of(_offset, _blurRadius, (float) shadowSpreadRadius, _color, _isOutset);
}
/**
* Use this to define the color of the visible shadow gradient.
* @param shadowColor The color of the shadow.
* @return A new {@link ShadowConf} with the specified color.
*/
public ShadowConf color( Color shadowColor ) {
Objects.requireNonNull(shadowColor, "Use UI.Color.UNDEFINED to specify no color instead of null");
if ( shadowColor == UI.Color.UNDEFINED)
shadowColor = null;
if ( Objects.equals(shadowColor, _color) )
return this;
return ShadowConf.of(_offset, _blurRadius, _spreadRadius, shadowColor, _isOutset);
}
/**
* Updates the color of the shadow using a color string
* which can be specified in various formats.
* (see {@link UI#color(String)} for more information)
*
* @param shadowColor The color of the shadow in the form of a String.
* The color can be specified in the following formats:
* <ul>
* <li>HTML color name - like "red"</li>
* <li>Hexadecimal RGB value - like "#ff0000"</li>
* <li>Hexadecimal RGBA value - like "#ff0000ff"</li>
* <li>RGB value - like "rgb(255, 0, 0)"</li>
* <li>RGBA value - like "rgba(255, 0, 0, 1.0)"</li>
* <li>HSB value - like "hsb(0, 100%, 100%)"</li>
* <li>HSBA value - like "hsba(0, 100%, 100%, 1.0)"</li>
* </ul>
* @return A new {@link ShadowConf} with the specified color.
*/
public ShadowConf color( String shadowColor ) {
Objects.requireNonNull(shadowColor);
Color newColor;
try {
newColor = UI.color(shadowColor);
} catch ( Exception e ) {
log.error("Failed to parse color string: '{}'", shadowColor, e);
return this; // We want to avoid side effects other than a wrong color
}
return ShadowConf.of(_offset, _blurRadius, _spreadRadius, newColor, _isOutset);
}
/**
* Use this to define the color of the visible shadow gradient
* in terms of the red, green and blue components
* consisting of three double values ranging from 0.0 to 1.0,
* where 0.0 represents the absence of the color component
* and 1.0 represents the color component at full strength.
*
* @param red The red component of the shadow color.
* @param green The green component of the shadow color.
* @param blue The blue component of the shadow color.
* @return A new {@link ShadowConf} with the specified color.
*/
public ShadowConf color( double red, double green, double blue ) {
return color(red, green, blue, 1.0);
}
/**
* Use this to define the color of the visible shadow gradient
* in terms of the red, green, blue and alpha components
* consisting of four double values ranging from 0.0 to 1.0,
* where 0.0 represents the absence of the color component
* and 1.0 represents the color component at full strength.
*
* @param red The red component of the shadow color.
* @param green The green component of the shadow color.
* @param blue The blue component of the shadow color.
* @param alpha The alpha component of the shadow color.
* @return A new {@link ShadowConf} with the specified color.
*/
public ShadowConf color( double red, double green, double blue, double alpha ) {
return color(new Color((float) red, (float) green, (float) blue, (float) alpha));
}
/**
* The {@code isInset} parameter determines whether the shadow is inset or outset,
* in terms of the direction of the shadow gradient either going inward or outward.
* If the {@code isInset} parameter is true, the shadow color will fade away
* towards the center of the shadow box, whereas if the {@code isInset} parameter is false,
* the shadow color will fade away towards the outer edge of the shadow box. <br>
* This is essentially the inverse of the {@link #isOutset(boolean)} method.
*
* @param shadowInset Whether the shadow is inset or outset.
* If true, the shadow is inset, otherwise it is outset.
* Inset shadows go inward, starting from the inner edge of the box (and its border),
* whereas outset shadows go outward, starting from the outer edge of the box's border.
* @return A new {@link ShadowConf} with the specified inset/outset state.
*/
public ShadowConf isInset(boolean shadowInset ) {
return ShadowConf.of(_offset, _blurRadius, _spreadRadius, _color, !shadowInset);
}
/**
* Use this to define whether the shadow is outset or inset,
* which will determine the direction of the shadow gradient.
* If the {@code isOutset} parameter is true, the shadow gradient
* color will fade away towards the outer edge of the shadow box.
* If the {@code isOutset} parameter is false on the other hand,
* the shadow color fades away towards the center of the shadow box. <br>
* This is essentially the inverse of the {@link #isInset(boolean)} method.
*
* @param shadowOutset Whether the shadow is outset or inset.
* If true, the shadow is outset, otherwise it is inset.
* Outset shadows go outward, starting from the outer edge of the box's border,
* whereas inset shadows go inward, starting from the inner edge of the box (and its border).
* @return A new {@link ShadowConf} with the specified outset/inset state.
*/
public ShadowConf isOutset( boolean shadowOutset ) {
return ShadowConf.of(_offset, _blurRadius, _spreadRadius, _color, shadowOutset);
}
ShadowConf _scale( double scaleFactor ) {
return ShadowConf.of(
_offset.scale(scaleFactor),
(float) (_blurRadius * scaleFactor),
(float) (_spreadRadius * scaleFactor),
_color,
_isOutset
);
}
@Override
public int hashCode() {
int hash = 7;
hash = 31 * hash + _offset.hashCode();
hash = 31 * hash + Float.hashCode(_blurRadius);
hash = 31 * hash + Float.hashCode(_spreadRadius);
hash = 31 * hash + Objects.hashCode(_color);
hash = 31 * hash + (_isOutset ? 1 : 0);
return hash;
}
@Override
public boolean equals(Object obj) {
if ( obj == null ) return false;
if ( obj == this ) return true;
if ( obj.getClass() != getClass() ) return false;
ShadowConf rhs = (ShadowConf) obj;
return Objects.equals(_offset, rhs._offset) &&
_blurRadius == rhs._blurRadius &&
_spreadRadius == rhs._spreadRadius &&
Objects.equals(_color, rhs._color) &&
_isOutset == rhs._isOutset;
}
@Override
public String toString() {
if ( this.equals(_NONE) )
return this.getClass().getSimpleName()+"[NONE]";
return this.getClass().getSimpleName()+"[" +
"horizontalOffset=" + _toString(_offset.x() ) + ", " +
"verticalOffset=" + _toString(_offset.y() ) + ", " +
"blurRadius=" + _toString(_blurRadius ) + ", " +
"spreadRadius=" + _toString(_spreadRadius) + ", " +
"color=" + StyleUtil.toString(_color) + ", " +
"isInset=" + !_isOutset +
"]";
}
private static String _toString( float value ) {
return value == 0 ? "0" : String.valueOf(value).replace(".0", "");
}
@Override
public ShadowConf simplified() {
if ( this.equals(_NONE) )
return _NONE;
if ( _color == null || _color.getAlpha() == 0 )
return _NONE;
if ( _color == UI.Color.UNDEFINED)
return _NONE;
return this;
}
}