GradientConf.java
package swingtree.style;
import com.google.errorprone.annotations.Immutable;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import swingtree.UI;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import java.awt.Color;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Function;
/**
* An immutable config API for specifying a gradient style.
* as a sub-style of various other styles,
* like for example {@link BaseConf} or {@link BorderConf} accessed through the
* {@link ComponentStyleDelegate#gradient(String, swingtree.api.Configurator)}
* method.
* The state of a gradient style is immutable and can only be updated by
* wither like methods that return a new instance of the gradient style
* with the specified property updated.
* <p>
* The following properties with their respective purpose are available:
* <br>
* <ul>
* <li><b>Transition</b>
* The transition defines the direction of the gradient.
* <br>
* The following transitions are available:
* <ul>
* <li>{@link UI.Span#TOP_LEFT_TO_BOTTOM_RIGHT}</li>
* <li>{@link UI.Span#BOTTOM_LEFT_TO_TOP_RIGHT}</li>
* <li>{@link UI.Span#TOP_RIGHT_TO_BOTTOM_LEFT}</li>
* <li>{@link UI.Span#BOTTOM_RIGHT_TO_TOP_LEFT}</li>
* <li>{@link UI.Span#TOP_TO_BOTTOM}</li>
* <li>{@link UI.Span#LEFT_TO_RIGHT}</li>
* <li>{@link UI.Span#BOTTOM_TO_TOP}</li>
* <li>{@link UI.Span#RIGHT_TO_LEFT}</li>
* </ul>
* </li>
* <li><b>Type</b>
* The type defines the shape of the gradient
* which can be either linear or radial. <br>
* So the following types are available:
* <ul>
* <li>{@link UI.GradientType#LINEAR}</li>
* <li>{@link UI.GradientType#RADIAL}</li>
* <li>{@link UI.GradientType#CONIC}</li>
* </ul>
* </li>
* <li><b>Colors</b>
* An array of colors that will be used
* as a basis for the gradient transition.
* </li>
* <li><b>Offset</b>
* The offset defines the start position of the gradient
* on the x and y axis.
* This property, together with the {@link #span(UI.Span)}
* property, defines the start position and direction of the gradient.
* </li>
* <li><b>Size</b>
* The size defines the size of the gradient
* in terms of the distance from the start position of the gradient
* to the end position of the gradient.
* <br>
* If no size is specified, the size of the gradient will be
* based on the size of the component that the gradient is applied to.
* </li>
* <li><b>Area</b>
* The component are to which the gradient is clipped to.
* Which means that the gradient will only be visible within the
* specified area of the component.
* </li>
* <li><b>Boundary</b>
* The boundaries of a component define the outlines between the different
* {@link swingtree.UI.ComponentArea}s.
* Setting a particular boundary causes the gradient to start at that boundary.
* </li>
* <li><b>Focus Offset</b>
* An offset property consisting of a {@code x} and {@code y} value
* which will be used together with the gradients position to calculate
* a focus point.
* This is only relevant for radial gradients!
* </li>
* <li><b>Rotation</b>
* The rotation of the gradient in degrees.
* This is typically only relevant for a linear gradient.
* However it is also applicable to a radial gradient with a focus offset,
* where the rotation will be applied to the focus offset.
* </li>
* <li><b>Fractions</b>
* An array of values between 0 and 1 that defines the relative position
* of each color in the gradient.
* <br>
* Note that the number of fractions must match the number of colors in the gradient.
* However, if the number of fractions is less than the number of colors, then the remaining
* colors will be determined based on linear interpolation.
* If the number of fractions is greater than the number of colors, then the remaining
* fractions will be ignored.
* </li>
* <li><b>Cycle</b>
* The cycle of the gradient which can be one of the following constants:
* <ul>
* <li>{@link UI.Cycle#NONE} -
* The gradient is only rendered once, without repeating.
* The last color is used to fill the remaining area.
* This is the default cycle.
* </li>
* <li>{@link UI.Cycle#REFLECT} -
* The gradient is rendered once and then reflected.,
* which means that the gradient is rendered again in reverse order
* starting from the last color and ending with the first color.
* After that, the gradient is rendered again in the original order,
* starting from the first color and ending with the last color and so on.
* </li>
* <li>{@link UI.Cycle#REPEAT} -
* The gradient is rendered repeatedly, which means that it
* is rendered again and again in the original order, starting from the first color
* and ending with the last color.
* </li>
* </ul>
* Note that this property ultimately translates to the {@link java.awt.MultipleGradientPaint.CycleMethod}
* of the {@link java.awt.LinearGradientPaint} or {@link java.awt.RadialGradientPaint} that is used
* to render the gradient inside the SwingTree style engine.
* </li>
* </ul>
* <p>
* You can also use the {@link #none()} method to specify that no gradient should be used,
* as the instance returned by that method is a gradient without any colors, effectively
* making it a representation of the absence of a gradient style.
*/
@Immutable
@SuppressWarnings("Immutable")
public final class GradientConf implements Simplifiable<GradientConf>
{
private static final Logger log = org.slf4j.LoggerFactory.getLogger(GradientConf.class);
static final UI.Layer DEFAULT_LAYER = UI.Layer.BACKGROUND;
private static final GradientConf _NONE = new GradientConf(
UI.Span.TOP_TO_BOTTOM,
UI.GradientType.LINEAR,
new Color[0],
Offset.none(),
-1f,
UI.ComponentArea.BODY,
UI.ComponentBoundary.EXTERIOR_TO_BORDER,
Offset.none(),
0f,
new float[0],
UI.Cycle.NONE
);
/**
* Use the returned instance as a representation of the absence of a gradient.
*
* @return A gradient without any colors, effectively
* representing the absence of a gradient.
*/
public static GradientConf none() { return _NONE; }
static GradientConf of(
UI.Span span,
UI.GradientType type,
Color[] colors,
Offset offset,
float size,
UI.ComponentArea area,
UI.ComponentBoundary boundary,
Offset focus,
float rotation,
float[] fractions,
UI.Cycle cycle
) {
// The rotation may be any number
// which always has to be normalized to a value between -180 and 180
rotation = ( (((rotation+180f) % 360f + 360f) % 360f) - 180f );
GradientConf none = none();
if ( span == none._span &&
type == none._type &&
Arrays.equals(colors, none._colors) &&
Objects.equals(offset, none._offset) &&
size == none._size &&
area == none._area &&
boundary == none._boundary &&
Objects.equals(focus, none._focus) &&
rotation == none._rotation &&
Arrays.equals(fractions, none._fractions) &&
cycle == none._cycle
)
return none;
return new GradientConf(span, type, colors, offset, size, area, boundary, focus, rotation, fractions, cycle);
}
private final UI.Span _span;
private final UI.GradientType _type;
private final Color[] _colors;
private final Offset _offset;
private final float _size;
private final UI.ComponentArea _area;
private final UI.ComponentBoundary _boundary;
private final Offset _focus;
private final float _rotation;
private final float[] _fractions;
private final UI.Cycle _cycle;
private GradientConf(
UI.Span span,
UI.GradientType type,
Color[] colors,
Offset offset,
float size,
UI.ComponentArea area,
UI.ComponentBoundary boundary,
Offset focus,
float rotation,
float[] fractions,
UI.Cycle cycle
) {
_span = Objects.requireNonNull(span);
_type = Objects.requireNonNull(type);
_colors = Objects.requireNonNull(colors);
_offset = Objects.requireNonNull(offset);
_size = ( size < 0 ? -1 : size );
_area = Objects.requireNonNull(area);
_boundary = Objects.requireNonNull(boundary);
_focus = Objects.requireNonNull(focus);
_rotation = rotation;
_fractions = Objects.requireNonNull(fractions);
_cycle = Objects.requireNonNull(cycle);
}
UI.Span span() { return _span; }
UI.GradientType type() { return _type; }
Color[] colors() { return _colors; }
Offset offset() { return _offset; }
float size() { return _size; }
UI.ComponentArea area() { return _area; }
UI.ComponentBoundary boundary() { return _boundary; }
Offset focus() { return _focus; }
float rotation() { return _rotation; }
float[] fractions() { return _fractions; }
UI.Cycle cycle() { return _cycle; }
boolean isOpaque() {
if ( _colors.length == 0 )
return false;
boolean foundTransparentColor = false;
for ( Color c : _colors ) {
if ( c.getAlpha() < 255 ) {
foundTransparentColor = true;
break;
}
}
return !foundTransparentColor;
}
GradientConf _scale( double scale ) {
if ( _size > 0 && scale != 1 )
return of(
_span,
_type,
_colors,
_offset.scale(scale),
(float) (_size * scale),
_area,
_boundary,
_focus.scale(scale),
_rotation,
_fractions,
_cycle
);
else
return this;
}
/**
* Define a list of colors which will, as part of the gradient, transition from one
* to the next in the order they are specified.
* <p>
* Note that you need to specify at least two colors for a gradient to be visible.
*
* @param colors The colors in the gradient.
* @return A new gradient style with the specified colors.
* @throws NullPointerException if any of the colors is {@code null}.
*/
public GradientConf colors( Color... colors ) {
Objects.requireNonNull(colors);
for ( Color color : colors )
Objects.requireNonNull(color, "Use UI.Color.UNDEFINED instead of null to represent the absence of a color.");
return of(_span, _type, colors, _offset, _size, _area, _boundary, _focus, _rotation, _fractions, _cycle);
}
/**
* Define a list of {@link String} based colors which will, as part of the gradient, transition from one
* to the next in the order they are specified.
* <p>
* Note that you need to specify at least two colors for a gradient to be visible.
*
* @param colors The colors in the gradient in {@link String} format.
* @return A new gradient style with the specified colors.
* @throws NullPointerException if any of the colors is {@code null}.
*/
public GradientConf colors( String... colors ) {
Objects.requireNonNull(colors);
try {
Color[] actualColors = new Color[colors.length];
for ( int i = 0; i < colors.length; i++ )
actualColors[i] = UI.color(colors[i]);
return of(_span, _type, actualColors, _offset, _size, _area, _boundary, _focus, _rotation, _fractions, _cycle);
} catch ( Exception e ) {
log.error("Failed to parse color strings: " + Arrays.toString(colors), e);
return this; // We want to avoid side effects other than a wrong color
}
}
/**
* Define from where and to where the gradient should transition to
* within the {@link UI.ComponentBoundary} of the component.
* <ul>
* <li>{@link UI.Span#TOP_LEFT_TO_BOTTOM_RIGHT}</li>
* <li>{@link UI.Span#BOTTOM_LEFT_TO_TOP_RIGHT}</li>
* <li>{@link UI.Span#TOP_RIGHT_TO_BOTTOM_LEFT}</li>
* <li>{@link UI.Span#BOTTOM_RIGHT_TO_TOP_LEFT}</li>
* <li>{@link UI.Span#TOP_TO_BOTTOM}</li>
* <li>{@link UI.Span#LEFT_TO_RIGHT}</li>
* <li>{@link UI.Span#BOTTOM_TO_TOP}</li>
* <li>{@link UI.Span#RIGHT_TO_LEFT}</li>
* </ul>
*
* @param span The span policy of the gradient, which defines the direction of the gradient.
* @return A new gradient style with the specified alignment.
* @throws NullPointerException if the alignment is {@code null}.
*/
public GradientConf span( UI.Span span ) {
Objects.requireNonNull(span);
return of(span, _type, _colors, _offset, _size, _area, _boundary, _focus, _rotation, _fractions, _cycle);
}
/**
* Define the type of the gradient which is one of the following:
* <ul>
* <li>{@link UI.GradientType#LINEAR}</li>
* <li>{@link UI.GradientType#RADIAL}</li>
* <li>{@link UI.GradientType#CONIC}</li>
* </ul>
*
* @param type The type of the gradient.
* @return A new gradient style with the specified type.
* @throws NullPointerException if the type is {@code null}.
*/
public GradientConf type( UI.GradientType type ) {
Objects.requireNonNull(type);
return of(_span, type, _colors, _offset, _size, _area, _boundary, _focus, _rotation, _fractions, _cycle);
}
/**
* Define the offset of the gradient which is the start position of the gradient
* on the x and y-axis. <br>
* Note that the offset is relative to the component that the gradient is applied to.
* <p>
* @param x The gradient start offset on the x-axis.
* @param y The gradient start offset on the y-axis.
* @return A new gradient style with the specified offset.
*/
public GradientConf offset( double x, double y ) {
return of(_span, _type, _colors, Offset.of(x,y), _size, _area, _boundary, _focus, _rotation, _fractions, _cycle);
}
/**
* Define the size of the gradient which is the size of the gradient
* in terms of the distance from the start position of the gradient
* to the end position of the gradient.
* <p>
* Note that if no size is specified, the size of the gradient will be
* based on the size of the component that the gradient is applied to.
*
* @param size The gradient size.
* @return A new gradient style with the specified size.
*/
public GradientConf size( double size ) {
return of(_span, _type, _colors, _offset, (float) size, _area, _boundary, _focus, _rotation, _fractions, _cycle);
}
/**
* Define the area of the component to which the gradient is clipped to.
* Which means that the gradient will only be visible within the
* specified area of the component.
*
* @param area The area of the component to which the gradient is clipped to.
* @return A new gradient style with the specified area.
*/
public GradientConf clipTo( UI.ComponentArea area ) {
return of(_span, _type, _colors, _offset, _size, area, _boundary, _focus, _rotation, _fractions, _cycle);
}
/**
* Define the boundary at which the gradient should start in terms of its base position.
* So if the boundary is set to {@link UI.ComponentBoundary#EXTERIOR_TO_BORDER}
* then the gradient position will be determined by the margin of the component. <br>
* Here a complete list of the available boundaries:
* <ul>
* <li>{@link UI.ComponentBoundary#OUTER_TO_EXTERIOR} -
* The outermost boundary of the entire component, including any margin that might be applied.
* Using this boundary will cause the gradient to be positioned somewhere at
* the outer most edge of the component.
* </li>
* <li>{@link UI.ComponentBoundary#EXTERIOR_TO_BORDER} -
* The boundary located after the margin but before the border.
* This tightly wraps the entire {@link UI.ComponentArea#BODY}.
* Using this boundary will cause the gradient to be positioned somewhere at
* the outer most edge of the component's body, which is between the margin and the border.
* </li>
* <li>{@link UI.ComponentBoundary#BORDER_TO_INTERIOR} -
* The boundary located after the border but before the padding.
* It represents the edge of the component's interior.
* Using this boundary will cause the gradient to be positioned somewhere at
* the outer most edge of the component's interior, which is between the border and the padding area.
* </li>
* <li>{@link UI.ComponentBoundary#INTERIOR_TO_CONTENT} -
* The boundary located after the padding.
* It represents the innermost boundary of the component, where the actual content of the component begins,
* like for example the contents of a {@link JPanel} or {@link JScrollPane}.
* Using this boundary will cause the gradient to be positioned somewhere after the padding area
* and before the content area, which is where all of the child components are located.
* </li>
* </ul>
* <p>
* You can think of this property as a convenient way to define the base position of the gradient.
* So if you want to do the positioning yourself, then you may configure this property to
* {@link UI.ComponentBoundary#OUTER_TO_EXTERIOR}, which will cause the gradient to be positioned
* at the outermost edge of the component, and then use the {@link #offset(double, double)} method
* to define the exact position of the gradient.
* (You may also want to set the {@link #span(UI.Span)}
* property to {@link UI.Span#TOP_LEFT_TO_BOTTOM_RIGHT} to make sure that the gradient
* is positioned in the top left corner (origin position) of the component)
*
* @param boundary The boundary at which the gradient should start in terms of its offset.
* @return A new gradient style with the specified boundary.
*/
public GradientConf boundary( UI.ComponentBoundary boundary ) {
return of(_span, _type, _colors, _offset, _size, _area, boundary, _focus, _rotation, _fractions, _cycle);
}
/**
* Define the focus offset of a radial gradient as a second position relative
* to the main position of the gradient (see {@link #offset(double, double)} and {@link #boundary(UI.ComponentBoundary)}
* which is used to define the direction of the gradient.
* <p>
* Note that this property is only relevant for radial gradients.
*
* @param x The focus offset on the x-axis.
* @param y The focus offset on the y-axis.
*/
public GradientConf focus( double x, double y ) {
return of(_span, _type, _colors, _offset, _size, _area, _boundary, Offset.of(x,y), _rotation, _fractions, _cycle);
}
/**
* Define the rotation of the gradient in degrees.
*
* @param rotation The rotation of the gradient in degrees.
*/
public GradientConf rotation( float rotation ) {
return of(_span, _type, _colors, _offset, _size, _area, _boundary, _focus, rotation, _fractions, _cycle);
}
/**
* Define the fractions of the gradient in the dorm of an array of values between 0 and 1
* that each the relative position of each color in the gradient transition.
* <p>
* Note that the number of fractions must match the number of colors in the gradient.
* If the number of fractions is less than the number of colors, then the remaining
* colors will be evenly distributed between the last two fractions.
*
* @param fractions The fractions of the gradient.
* @return An updated gradient configuration with the specified fractions.
*/
public GradientConf fractions( double... fractions ) {
float[] actualFractions = new float[fractions.length];
for ( int i = 0; i < fractions.length; i++ )
actualFractions[i] = (float) fractions[i];
return of(_span, _type, _colors, _offset, _size, _area, _boundary, _focus, _rotation, actualFractions, _cycle);
}
/**
* Define the cycle of the gradient which is one of the following:
* <ul>
* <li>{@link UI.Cycle#NONE} -
* The gradient is only rendered once, without repeating.
* The last color is used to fill the remaining area.
* This is the default cycle.
* </li>
* <li>{@link UI.Cycle#REFLECT} -
* The gradient is rendered once and then reflected.,
* which means that the gradient is rendered again in reverse order
* starting from the last color and ending with the first color.
* After that, the gradient is rendered again in the original order,
* starting from the first color and ending with the last color and so on.
* </li>
* <li>{@link UI.Cycle#REPEAT} -
* The gradient is rendered repeatedly, which means that it
* is rendered again and again in the original order, starting from the first color
* and ending with the last color.
* </li>
* </ul>
* Note that this property ultimately translates to the {@link java.awt.MultipleGradientPaint.CycleMethod}
* of the {@link java.awt.LinearGradientPaint} or {@link java.awt.RadialGradientPaint} that is used
* to render the gradient inside the SwingTree style engine.
*
* @param cycle The cycle of the gradient.
* @return A new gradient style with the specified cycle method.
* @throws NullPointerException if the cycle is {@code null}.
*/
public GradientConf cycle(UI.Cycle cycle ) {
Objects.requireNonNull(cycle);
return of(_span, _type, _colors, _offset, _size, _area, _boundary, _focus, _rotation, _fractions, cycle);
}
@Override
public String toString() {
if ( this.equals(_NONE) )
return getClass().getSimpleName() + "[NONE]";
return getClass().getSimpleName() + "[" +
"transition=" + _span + ", " +
"type=" + _type + ", " +
"colors=" + Arrays.toString(_colors) + ", " +
"offset=" + _offset + ", " +
"size=" + _size + ", " +
"area=" + _area + ", " +
"boundary=" + _boundary + ", " +
"focus=" + _focus + ", " +
"rotation=" + _rotation + ", " +
"fractions=" + Arrays.toString(_fractions) + ", " +
"cycle=" + _cycle +
"]";
}
@Override
public boolean equals( @Nullable Object o ) {
if ( this == o ) return true;
if ( !(o instanceof GradientConf) ) return false;
GradientConf that = (GradientConf) o;
return _span == that._span &&
_type == that._type &&
Arrays.equals(_colors, that._colors) &&
Objects.equals(_offset, that._offset) &&
_size == that._size &&
_area == that._area &&
_boundary == that._boundary &&
Objects.equals(_focus, that._focus) &&
_rotation == that._rotation &&
Arrays.equals(_fractions, that._fractions) &&
_cycle == that._cycle;
}
@Override
public int hashCode() {
return Objects.hash(
_span,
_type,
Arrays.hashCode(_colors),
_offset,
_size,
_area,
_boundary,
_focus,
_rotation,
Arrays.hashCode(_fractions),
_cycle
);
}
@Override
@SuppressWarnings("ReferenceEquality")
public GradientConf simplified() {
if ( this.equals(_NONE) )
return _NONE;
if ( _colors.length == 0 )
return _NONE;
if ( Arrays.stream(_colors).allMatch( color -> color.getAlpha() == 0 || color == UI.Color.UNDEFINED) )
return _NONE;
int numberOfRealColors = Arrays.stream(_colors).mapToInt( color -> color == UI.Color.UNDEFINED ? 0 : 1 ).sum();
if ( numberOfRealColors == 0 )
return _NONE;
Offset focus = _focus;
float rotation = _rotation;
if ( _type != UI.GradientType.RADIAL )
focus = Offset.none();
else
if ( focus.equals(Offset.none()) )
rotation = 0f; // If the focus is not set, then the rotation is irrelevant
if ( numberOfRealColors != _colors.length ) {
Color[] realColors = new Color[numberOfRealColors];
int index = 0;
for ( Color color : _colors )
if ( color != UI.Color.UNDEFINED )
realColors[index++] = color;
return of(_span, _type, realColors, _offset, _size, _area, _boundary, focus, rotation, _fractions, _cycle);
}
if ( !focus.equals(_focus) )
return of(_span, _type, _colors, _offset, _size, _area, _boundary, focus, rotation, _fractions, _cycle);
return this;
}
}