Outline.java
package swingtree.style;
import com.google.errorprone.annotations.Immutable;
import org.jspecify.annotations.Nullable;
import java.awt.Insets;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
/**
* Outline is an immutable value object that represents the outline of a UI component
* where every side of the outline can have varying thicknesses and even be completely
* optional (null).
* <p>
* The values of this object are optional in order to determine if the outline
* was specified through the styling API or not so that the default properties of a component
* can be preserved (the insets of a layout manager, for example).
*/
@Immutable
final class Outline
{
private static final Outline _NONE = new Outline(null, null, null, null);
static Outline none() { return _NONE; }
static Outline of( float top, float right, float bottom, float left ) {
return new Outline(top, right, bottom, left);
}
static Outline of( float topAndBottom, float rightAndLeft ) {
return new Outline(topAndBottom, rightAndLeft, topAndBottom, rightAndLeft);
}
static Outline of( double top, double right, double bottom, double left ) {
return new Outline((float) top, (float) right, (float) bottom, (float) left);
}
static Outline of( float allSides ) {
return new Outline(allSides, allSides, allSides, allSides);
}
static Outline of( Insets insets ) {
return of(insets.top, insets.right, insets.bottom, insets.left);
}
private final @Nullable Float top;
private final @Nullable Float right;
private final @Nullable Float bottom;
private final @Nullable Float left;
static Outline ofNullable( @Nullable Float top, @Nullable Float right, @Nullable Float bottom, @Nullable Float left ) {
if ( top == null && right == null && bottom == null && left == null )
return _NONE;
return new Outline(top, right, bottom, left);
}
private Outline( @Nullable Float top, @Nullable Float right, @Nullable Float bottom, @Nullable Float left ) {
this.top = top;
this.right = right;
this.bottom = bottom;
this.left = left;
}
/**
* The top outline value in the form of an {@link Optional}, where {@link Optional#empty()}
* means that the top outline was not specified.
*
* @return An {@link Optional} containing the top outline value if it was specified,
* {@link Optional#empty()} otherwise.
*/
Optional<Float> top() { return Optional.ofNullable(top); }
/**
* An optional value for the right outline.
*
* @return An {@link Optional} containing the right outline value if it was specified,
* {@link Optional#empty()} otherwise.
*/
Optional<Float> right() { return Optional.ofNullable(right); }
/**
* The bottom outline value in the form of an {@link Optional}, where {@link Optional#empty()}
* means that the bottom outline was not specified.
*
* @return An {@link Optional} containing the bottom outline value if it was specified,
* {@link Optional#empty()} otherwise.
*/
Optional<Float> bottom() { return Optional.ofNullable(bottom); }
/**
* Returns an optional value for the left outline where {@link Optional#empty()}
* means that the left outline was not specified.
*
* @return An {@link Optional} containing the left outline value if it was specified,
* {@link Optional#empty()} otherwise.
*/
Optional<Float> left() { return Optional.ofNullable(left); }
/**
* Creates an updated {@link Outline} with the specified {@code top} outline value.
*
* @param top The top outline value.
* @return A new {@link Outline} with the specified top outline value.
*/
Outline withTop( float top ) { return Outline.ofNullable(top, right, bottom, left); }
/**
* Creates an updated {@link Outline} with the specified {@code right} outline value.
*
* @param right The right outline value.
* @return A new {@link Outline} with the specified right outline value.
*/
Outline withRight( float right ) { return Outline.ofNullable(top, right, bottom, left); }
/**
* Creates an updated {@link Outline} with the specified {@code bottom} outline value.
*
* @param bottom The bottom outline value.
* @return A new {@link Outline} with the specified bottom outline value.
*/
Outline withBottom( float bottom ) { return Outline.ofNullable(top, right, bottom, left); }
/**
* Creates an updated {@link Outline} with the specified {@code left} outline value.
* @param left The left outline value.
* @return A new {@link Outline} with the specified left outline value.
*/
Outline withLeft( float left ) { return Outline.ofNullable(top, right, bottom, left); }
Outline minus( Outline other ) {
return Outline.ofNullable(
top == null ? null : top - (other.top == null ? 0 : other.top),
right == null ? null : right - (other.right == null ? 0 : other.right),
bottom == null ? null : bottom - (other.bottom == null ? 0 : other.bottom),
left == null ? null : left - (other.left == null ? 0 : other.left)
);
}
/**
* An {@link Outline} may be scaled by a factor to increase or decrease the thickness of the outline.
* If any of the sides was not specified, it will remain unspecified.
*
* @param scale The scale factor.
* @return A new {@link Outline} with the outline values scaled by the specified factor.
*/
Outline scale( double scale ) {
return Outline.ofNullable(
top == null ? null : (float) ( top * scale ),
right == null ? null : (float) ( right * scale ),
bottom == null ? null : (float) ( bottom * scale ),
left == null ? null : (float) ( left * scale )
);
}
Outline simplified() {
if ( this.equals(_NONE) )
return _NONE;
Float top = Objects.equals(this.top , 0f) ? null : this.top;
Float right = Objects.equals(this.right , 0f) ? null : this.right;
Float bottom = Objects.equals(this.bottom, 0f) ? null : this.bottom;
Float left = Objects.equals(this.left , 0f) ? null : this.left;
if ( top == null && right == null && bottom == null && left == null )
return _NONE;
return Outline.ofNullable(top, right, bottom, left);
}
/**
* Determines if any of the outline values are not null and positive,
* which means that the outline is visible as part of the component's
* appearance or layout.
*
* @return {@code true} if any of the outline values are not null and positive,
* {@code false} otherwise.
*/
public boolean isPositive() {
return ( top != null && top > 0 ) ||
( right != null && right > 0 ) ||
( bottom != null && bottom > 0 ) ||
( left != null && left > 0 );
}
private static @Nullable Float _plus( @Nullable Float a, @Nullable Float b ) {
if ( a == null && b == null )
return null;
return a == null ? b : b == null ? a : a + b;
}
/**
* Adds the outline values of this {@link Outline} with the specified {@code other} {@link Outline} values.
*
* @param other The other {@link Outline} to merge with.
* @return A new {@link Outline} with the merged outline values.
*/
public Outline plus( Outline other ) {
if ( this.equals(_NONE) )
return other;
if ( other.equals(_NONE) )
return this;
return Outline.ofNullable(
_plus(top, other.top ),
_plus(right, other.right ),
_plus(bottom, other.bottom),
_plus(left, other.left )
);
}
public Outline or( Outline other ) {
if ( this.equals(_NONE) )
return other;
if ( other.equals(_NONE) )
return this;
return Outline.ofNullable(
top == null ? other.top : top,
right == null ? other.right : right,
bottom == null ? other.bottom : bottom,
left == null ? other.left : left
);
}
/**
* Maps the outline values of this {@link Outline} using the specified {@code mapper} function.
*
* @param mapper The mapper function.
* @return A new {@link Outline} with the mapped outline values.
*/
public Outline map( Function<Float, @Nullable Float> mapper ) {
return Outline.ofNullable(
top == null ? null : mapper.apply(top),
right == null ? null : mapper.apply(right),
bottom == null ? null : mapper.apply(bottom),
left == null ? null : mapper.apply(left)
);
}
@Override
public int hashCode() {
int hash = 7;
hash = 97 * hash + Objects.hashCode(this.top);
hash = 97 * hash + Objects.hashCode(this.right);
hash = 97 * hash + Objects.hashCode(this.bottom);
hash = 97 * hash + Objects.hashCode(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;
Outline rhs = (Outline) obj;
return Objects.equals(top, rhs.top ) &&
Objects.equals(right, rhs.right ) &&
Objects.equals(bottom, rhs.bottom) &&
Objects.equals(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( @Nullable Float value ) {
return value == null ? "?" : value.toString().replace(".0", "");
}
}