OptionalInsets.java
package swingtree.style;
import com.google.errorprone.annotations.Immutable;
import org.jspecify.annotations.Nullable;
import java.awt.Insets;
import java.util.Optional;
import java.util.function.Function;
/**
* OptionalInsets is an immutable value object holding four inset thicknesses, one for each
* side of a UI component, where any side may also be left unspecified. Unlike
* {@link java.awt.Insets}, a side is a {@code float} and does not have to have a value.
* <p>
* A side is optional in order to tell "the styling API asked for this thickness" apart from
* "the styling API said nothing about this side", so that a component's own defaults - the
* insets a layout manager wants, for example - survive being styled.
* <p>
* <b>A side is stored as a primitive float, and {@link Float#NaN} is what "unspecified"
* means.</b> The obvious spelling of an optional side is a boxed {@code Float}, and it is the
* wrong one here: a box model holds four of these objects - margin, padding, border widths
* and the insets a former border leaves behind - and they are built for every layer of every
* style gathered on every paint of every component, and {@code Float.valueOf} has no cache,
* so each side used to be a heap allocation in a former version of this class. Sides are
* compared by their bit patterns rather than with {@code ==}, which keeps every distinction
* {@code Float.equals} used to make - {@code 0.0} apart from {@code -0.0}, and one unspecified
* side equal to another. The one consequence is that a side which really is {@code NaN} now
* reads as unspecified instead of propagating a {@code NaN} into a layout.
*/
@Immutable
final class OptionalInsets
{
/** What a side holds when the styling API said nothing about it. */
private static final float UNSPECIFIED = Float.NaN;
private static final OptionalInsets _NONE = new OptionalInsets(UNSPECIFIED, UNSPECIFIED, UNSPECIFIED, UNSPECIFIED);
static OptionalInsets none() { return _NONE; }
static OptionalInsets of( float top, float right, float bottom, float left ) {
return new OptionalInsets(top, right, bottom, left);
}
static OptionalInsets of( float topAndBottom, float rightAndLeft ) {
return new OptionalInsets(topAndBottom, rightAndLeft, topAndBottom, rightAndLeft);
}
static OptionalInsets of( double top, double right, double bottom, double left ) {
return new OptionalInsets((float) top, (float) right, (float) bottom, (float) left);
}
static OptionalInsets of( float allSides ) {
return new OptionalInsets(allSides, allSides, allSides, allSides);
}
static OptionalInsets of( Insets insets ) {
return of(insets.top, insets.right, insets.bottom, insets.left);
}
private final float top;
private final float right;
private final float bottom;
private final float left;
private static OptionalInsets _of( float top, float right, float bottom, float left ) {
if ( _isUnset(top) && _isUnset(right) && _isUnset(bottom) && _isUnset(left) )
return _NONE;
return new OptionalInsets(top, right, bottom, left);
}
private static OptionalInsets _ofNullable( @Nullable Float top, @Nullable Float right, @Nullable Float bottom, @Nullable Float left ) {
return _of(_unbox(top), _unbox(right), _unbox(bottom), _unbox(left));
}
private static float _unbox( @Nullable Float value ) {
return value == null ? UNSPECIFIED : value;
}
private static boolean _isUnset( float value ) {
return Float.isNaN(value);
}
/**
* Whether two sides are the same side, which for an unspecified one means both are
* unspecified. Comparing the bit patterns rather than the numbers is what keeps
* {@code 0.0} and {@code -0.0} distinct, exactly as boxed {@code Float} equality did.
*
* @param a one side
* @param b the other side
* @return true when the two sides carry the same value, or are both unspecified
*/
private static boolean _same( float a, float b ) {
return Float.floatToIntBits(a) == Float.floatToIntBits(b);
}
private OptionalInsets( float top, float right, float bottom, float left ) {
this.top = top;
this.right = right;
this.bottom = bottom;
this.left = left;
}
/**
* The top inset in the form of an {@link Optional}, where {@link Optional#empty()}
* means that the top inset was not specified.
*
* @return An {@link Optional} containing the top inset if it was specified,
* {@link Optional#empty()} otherwise.
*/
Optional<Float> top() { return _isUnset(top) ? Optional.empty() : Optional.of(top); }
/**
* An optional value for the right inset.
*
* @return An {@link Optional} containing the right inset if it was specified,
* {@link Optional#empty()} otherwise.
*/
Optional<Float> right() { return _isUnset(right) ? Optional.empty() : Optional.of(right); }
/**
* The bottom inset in the form of an {@link Optional}, where {@link Optional#empty()}
* means that the bottom inset was not specified.
*
* @return An {@link Optional} containing the bottom inset if it was specified,
* {@link Optional#empty()} otherwise.
*/
Optional<Float> bottom() { return _isUnset(bottom) ? Optional.empty() : Optional.of(bottom); }
/**
* Returns an optional value for the left inset where {@link Optional#empty()}
* means that the left inset was not specified.
*
* @return An {@link Optional} containing the left inset if it was specified,
* {@link Optional#empty()} otherwise.
*/
Optional<Float> left() { return _isUnset(left) ? Optional.empty() : Optional.of(left); }
/**
* Creates an updated {@link OptionalInsets} with the specified {@code top} inset.
*
* @param top The top inset.
* @return A new {@link OptionalInsets} with the specified top inset.
*/
OptionalInsets withTop( float top ) { return _of(top, right, bottom, left); }
/**
* Creates an updated {@link OptionalInsets} with the specified {@code right} inset.
*
* @param right The right inset.
* @return A new {@link OptionalInsets} with the specified right inset.
*/
OptionalInsets withRight( float right ) { return _of(top, right, bottom, left); }
/**
* Creates an updated {@link OptionalInsets} with the specified {@code bottom} inset.
*
* @param bottom The bottom inset.
* @return A new {@link OptionalInsets} with the specified bottom inset.
*/
OptionalInsets withBottom( float bottom ) { return _of(top, right, bottom, left); }
/**
* Creates an updated {@link OptionalInsets} with the specified {@code left} inset.
* @param left The left inset.
* @return A new {@link OptionalInsets} with the specified left inset.
*/
OptionalInsets withLeft( float left ) { return _of(top, right, bottom, left); }
OptionalInsets minus( OptionalInsets other ) {
return _of(
_minus(top, other.top ),
_minus(right, other.right ),
_minus(bottom, other.bottom),
_minus(left, other.left )
);
}
private static float _minus( float a, float b ) {
if ( _isUnset(a) )
return UNSPECIFIED;
return _isUnset(b) ? a : a - b;
}
/**
* An {@link OptionalInsets} may be scaled by a factor to increase or decrease the thickness of every side.
* If any of the sides was not specified, it will remain unspecified.
*
* @param scale The scale factor.
* @return A new {@link OptionalInsets} with the insets scaled by the specified factor.
*/
OptionalInsets scale( double scale ) {
return _of(
_isUnset(top) ? UNSPECIFIED : (float) ( top * scale ),
_isUnset(right) ? UNSPECIFIED : (float) ( right * scale ),
_isUnset(bottom) ? UNSPECIFIED : (float) ( bottom * scale ),
_isUnset(left) ? UNSPECIFIED : (float) ( left * scale )
);
}
OptionalInsets simplified() {
if ( this.equals(_NONE) )
return _NONE;
return _of(
_same(this.top , 0f) ? UNSPECIFIED : this.top,
_same(this.right , 0f) ? UNSPECIFIED : this.right,
_same(this.bottom, 0f) ? UNSPECIFIED : this.bottom,
_same(this.left , 0f) ? UNSPECIFIED : this.left
);
}
/**
* Determines if any of the sides is specified and positive,
* which means that these insets take up room in the component's
* appearance or layout.
*
* @return {@code true} if any of the sides is specified and positive,
* {@code false} otherwise.
*/
public boolean isPositive() {
// An unspecified side is NaN, and no comparison against NaN is ever true:
return top > 0 || right > 0 || bottom > 0 || left > 0;
}
private static float _plus( float a, float b ) {
if ( _isUnset(a) )
return b;
return _isUnset(b) ? a : a + b;
}
/**
* Adds the sides of this {@link OptionalInsets} to the sides of the specified {@code other} {@link OptionalInsets}.
*
* @param other The other {@link OptionalInsets} to merge with.
* @return A new {@link OptionalInsets} with the summed insets.
*/
public OptionalInsets plus( OptionalInsets other ) {
if ( this.equals(_NONE) )
return other;
if ( other.equals(_NONE) )
return this;
return _of(
_plus(top, other.top ),
_plus(right, other.right ),
_plus(bottom, other.bottom),
_plus(left, other.left )
);
}
static OptionalInsets roundingCorrectionOf( OptionalInsets first, OptionalInsets second, OptionalInsets third ) {
return _of(
_roundingCorrectionOf(_plus(_plus(first.top, second.top ), third.top )),
_roundingCorrectionOf(_plus(_plus(first.right, second.right ), third.right )),
_roundingCorrectionOf(_plus(_plus(first.bottom, second.bottom), third.bottom)),
_roundingCorrectionOf(_plus(_plus(first.left, second.left ), third.left ))
);
}
private static float _roundingCorrectionOf( float sum ) {
if ( !(sum > 0f) )
return UNSPECIFIED;
float fraction = sum - (float) Math.floor(sum);
return fraction > 0f ? 1f - fraction : UNSPECIFIED;
}
public OptionalInsets or( OptionalInsets other ) {
if ( this.equals(_NONE) )
return other;
if ( other.equals(_NONE) )
return this;
return _of(
_isUnset(top) ? other.top : top,
_isUnset(right) ? other.right : right,
_isUnset(bottom) ? other.bottom : bottom,
_isUnset(left) ? other.left : left
);
}
/**
* Maps the specified sides of this {@link OptionalInsets} using the specified {@code mapper} function.
*
* @param mapper The mapper function.
* @return A new {@link OptionalInsets} with the mapped insets.
*/
public OptionalInsets map( Function<Float, @Nullable Float> mapper ) {
return _ofNullable(
_isUnset(top) ? null : mapper.apply(top),
_isUnset(right) ? null : mapper.apply(right),
_isUnset(bottom) ? null : mapper.apply(bottom),
_isUnset(left) ? null : mapper.apply(left)
);
}
@Override
public int hashCode() {
int hash = 7;
hash = 97 * hash + Float.floatToIntBits(this.top);
hash = 97 * hash + Float.floatToIntBits(this.right);
hash = 97 * hash + Float.floatToIntBits(this.bottom);
hash = 97 * hash + Float.floatToIntBits(this.left);
return hash;
}
@Override
public boolean equals( Object obj ) {
if ( obj == null ) return false;
if ( obj == this ) return true;
if ( obj.getClass() != getClass() ) return false;
OptionalInsets rhs = (OptionalInsets) obj;
return _same(top, rhs.top ) &&
_same(right, rhs.right ) &&
_same(bottom, rhs.bottom) &&
_same(left, rhs.left );
}
@Override
public String toString() {
return this.getClass().getSimpleName() + "[" +
"top=" + _toString( top ) + ", " +
"right=" + _toString( right ) + ", " +
"bottom=" + _toString( bottom ) + ", " +
"left=" + _toString( left ) +
"]";
}
private static String _toString( float value ) {
return _isUnset(value) ? "?" : String.valueOf(value).replace(".0", "");
}
}