Outline.java

  1. package swingtree.style;

  2. import com.google.errorprone.annotations.Immutable;
  3. import org.jspecify.annotations.Nullable;

  4. import java.awt.Insets;
  5. import java.util.Objects;
  6. import java.util.Optional;
  7. import java.util.function.Function;

  8. /**
  9.  *  Outline is an immutable value object that represents the outline of a UI component
  10.  *  where every side of the outline can have varying thicknesses and even be completely
  11.  *  optional (null).
  12.  *  <p>
  13.  *  The values of this object are optional in order to determine if the outline
  14.  *  was specified through the styling API or not so that the default properties of a component
  15.  *  can be preserved (the insets of a layout manager, for example).
  16.  */
  17. @Immutable
  18. final class Outline
  19. {
  20.     private static final Outline _NONE = new Outline(null, null, null, null);

  21.     static Outline none() { return _NONE; }

  22.     static Outline of( float top, float right, float bottom, float left ) {
  23.         return new Outline(top, right, bottom, left);
  24.     }

  25.     static Outline of( float topAndBottom, float rightAndLeft ) {
  26.         return new Outline(topAndBottom, rightAndLeft, topAndBottom, rightAndLeft);
  27.     }

  28.     static Outline of( double top, double right, double bottom, double left ) {
  29.         return new Outline((float) top, (float) right, (float) bottom, (float) left);
  30.     }

  31.     static Outline of( float allSides ) {
  32.         return new Outline(allSides, allSides, allSides, allSides);
  33.     }

  34.     static Outline of( Insets insets ) {
  35.         return of(insets.top, insets.right, insets.bottom, insets.left);
  36.     }


  37.     private final @Nullable Float top;
  38.     private final @Nullable Float right;
  39.     private final @Nullable Float bottom;
  40.     private final @Nullable Float left;


  41.     static Outline ofNullable( @Nullable Float top, @Nullable Float right, @Nullable Float bottom, @Nullable Float left ) {
  42.         if ( top == null && right == null && bottom == null && left == null )
  43.             return _NONE;

  44.         return new Outline(top, right, bottom, left);
  45.     }
  46.    
  47.     private Outline( @Nullable Float top, @Nullable Float right, @Nullable Float bottom, @Nullable Float left ) {
  48.         this.top    = top;
  49.         this.right  = right;
  50.         this.bottom = bottom;
  51.         this.left   = left;
  52.     }

  53.     /**
  54.      *  The top outline value in the form of an {@link Optional}, where {@link Optional#empty()}
  55.      *  means that the top outline was not specified.
  56.      *
  57.      * @return An {@link Optional} containing the top outline value if it was specified,
  58.      *        {@link Optional#empty()} otherwise.
  59.      */
  60.     Optional<Float> top() { return Optional.ofNullable(top); }

  61.     /**
  62.      *  An optional value for the right outline.
  63.      *
  64.      * @return An {@link Optional} containing the right outline value if it was specified,
  65.      *        {@link Optional#empty()} otherwise.
  66.      */
  67.     Optional<Float> right() { return Optional.ofNullable(right); }

  68.     /**
  69.      *  The bottom outline value in the form of an {@link Optional}, where {@link Optional#empty()}
  70.      *  means that the bottom outline was not specified.
  71.      *
  72.      * @return An {@link Optional} containing the bottom outline value if it was specified,
  73.      *        {@link Optional#empty()} otherwise.
  74.      */
  75.     Optional<Float> bottom() { return Optional.ofNullable(bottom); }

  76.     /**
  77.      *  Returns an optional value for the left outline where {@link Optional#empty()}
  78.      *  means that the left outline was not specified.
  79.      *
  80.      * @return An {@link Optional} containing the left outline value if it was specified,
  81.      *        {@link Optional#empty()} otherwise.
  82.      */
  83.     Optional<Float> left() { return Optional.ofNullable(left); }

  84.     /**
  85.      *  Creates an updated {@link Outline} with the specified {@code top} outline value.
  86.      *
  87.      * @param top The top outline value.
  88.      * @return A new {@link Outline} with the specified top outline value.
  89.      */
  90.     Outline withTop( float top ) { return Outline.ofNullable(top, right, bottom, left); }

  91.     /**
  92.      *  Creates an updated {@link Outline} with the specified {@code right} outline value.
  93.      *
  94.      * @param right The right outline value.
  95.      * @return A new {@link Outline} with the specified right outline value.
  96.      */
  97.     Outline withRight( float right ) { return Outline.ofNullable(top, right, bottom, left); }

  98.     /**
  99.      *  Creates an updated {@link Outline} with the specified {@code bottom} outline value.
  100.      *
  101.      * @param bottom The bottom outline value.
  102.      * @return A new {@link Outline} with the specified bottom outline value.
  103.      */
  104.     Outline withBottom( float bottom ) { return Outline.ofNullable(top, right, bottom, left); }

  105.     /**
  106.      *  Creates an updated {@link Outline} with the specified {@code left} outline value.
  107.      * @param left The left outline value.
  108.      * @return A new {@link Outline} with the specified left outline value.
  109.      */
  110.     Outline withLeft( float left ) { return Outline.ofNullable(top, right, bottom, left); }

  111.     Outline minus( Outline other ) {
  112.         return Outline.ofNullable(
  113.                     top    == null ? null : top    - (other.top    == null ? 0 : other.top),
  114.                     right  == null ? null : right  - (other.right  == null ? 0 : other.right),
  115.                     bottom == null ? null : bottom - (other.bottom == null ? 0 : other.bottom),
  116.                     left   == null ? null : left   - (other.left   == null ? 0 : other.left)
  117.                 );
  118.     }

  119.     /**
  120.      *  An {@link Outline} may be scaled by a factor to increase or decrease the thickness of the outline.
  121.      *  If any of the sides was not specified, it will remain unspecified.
  122.      *
  123.      * @param scale The scale factor.
  124.      * @return A new {@link Outline} with the outline values scaled by the specified factor.
  125.      */
  126.     Outline scale( double scale ) {
  127.         return Outline.ofNullable(
  128.                     top    == null ? null : (float) ( top    * scale ),
  129.                     right  == null ? null : (float) ( right  * scale ),
  130.                     bottom == null ? null : (float) ( bottom * scale ),
  131.                     left   == null ? null : (float) ( left   * scale )
  132.                 );
  133.     }

  134.     Outline simplified() {
  135.         if ( this.equals(_NONE) )
  136.             return _NONE;

  137.         Float top    = Objects.equals(this.top   , 0f) ? null : this.top;
  138.         Float right  = Objects.equals(this.right , 0f) ? null : this.right;
  139.         Float bottom = Objects.equals(this.bottom, 0f) ? null : this.bottom;
  140.         Float left   = Objects.equals(this.left  , 0f) ? null : this.left;

  141.         if ( top == null && right == null && bottom == null && left == null )
  142.             return _NONE;

  143.         return Outline.ofNullable(top, right, bottom, left);
  144.     }

  145.     /**
  146.      *  Determines if any of the outline values are not null and positive,
  147.      *  which means that the outline is visible as part of the component's
  148.      *  appearance or layout.
  149.      *
  150.      * @return {@code true} if any of the outline values are not null and positive,
  151.      *         {@code false} otherwise.
  152.      */
  153.     public boolean isPositive() {
  154.         return ( top    != null && top    > 0 ) ||
  155.                ( right  != null && right  > 0 ) ||
  156.                ( bottom != null && bottom > 0 ) ||
  157.                ( left   != null && left   > 0 );
  158.     }

  159.     private static @Nullable Float _plus( @Nullable Float a, @Nullable Float b ) {
  160.         if ( a == null && b == null )
  161.             return null;
  162.         return a == null ? b : b == null ? a : a + b;
  163.     }

  164.     /**
  165.      *  Adds the outline values of this {@link Outline} with the specified {@code other} {@link Outline} values.
  166.      *
  167.      * @param other The other {@link Outline} to merge with.
  168.      * @return A new {@link Outline} with the merged outline values.
  169.      */
  170.     public Outline plus( Outline other ) {
  171.         if ( this.equals(_NONE) )
  172.             return other;
  173.         if ( other.equals(_NONE) )
  174.             return this;

  175.         return Outline.ofNullable(
  176.                     _plus(top,    other.top   ),
  177.                     _plus(right,  other.right ),
  178.                     _plus(bottom, other.bottom),
  179.                     _plus(left,   other.left  )
  180.                 );
  181.     }

  182.     public Outline or( Outline other ) {
  183.         if ( this.equals(_NONE) )
  184.             return other;
  185.         if ( other.equals(_NONE) )
  186.             return this;

  187.         return Outline.ofNullable(
  188.                     top    == null ? other.top    : top,
  189.                     right  == null ? other.right  : right,
  190.                     bottom == null ? other.bottom : bottom,
  191.                     left   == null ? other.left   : left
  192.                 );
  193.     }

  194.     /**
  195.      *  Maps the outline values of this {@link Outline} using the specified {@code mapper} function.
  196.      *
  197.      * @param mapper The mapper function.
  198.      * @return A new {@link Outline} with the mapped outline values.
  199.      */
  200.     public Outline map( Function<Float, @Nullable Float> mapper ) {
  201.         return Outline.ofNullable(
  202.                     top    == null ? null : mapper.apply(top),
  203.                     right  == null ? null : mapper.apply(right),
  204.                     bottom == null ? null : mapper.apply(bottom),
  205.                     left   == null ? null : mapper.apply(left)
  206.                 );
  207.     }

  208.     @Override
  209.     public int hashCode() {
  210.         int hash = 7;
  211.         hash = 97 * hash + Objects.hashCode(this.top);
  212.         hash = 97 * hash + Objects.hashCode(this.right);
  213.         hash = 97 * hash + Objects.hashCode(this.bottom);
  214.         hash = 97 * hash + Objects.hashCode(this.left);
  215.         return hash;
  216.     }

  217.     @Override
  218.     public boolean equals( Object obj ) {
  219.         if ( obj == null ) return false;
  220.         if ( obj == this ) return true;
  221.         if ( obj.getClass() != getClass() ) return false;
  222.         Outline rhs = (Outline) obj;
  223.         return Objects.equals(top,    rhs.top   ) &&
  224.                Objects.equals(right,  rhs.right ) &&
  225.                Objects.equals(bottom, rhs.bottom) &&
  226.                Objects.equals(left,   rhs.left  );
  227.     }

  228.     @Override
  229.     public String toString() {
  230.         return this.getClass().getSimpleName() + "[" +
  231.                     "top="    + _toString( top    ) + ", " +
  232.                     "right="  + _toString( right  ) + ", " +
  233.                     "bottom=" + _toString( bottom ) + ", " +
  234.                     "left="   + _toString( left   ) +
  235.                 "]";
  236.     }

  237.     private static String _toString( @Nullable Float value ) {
  238.         return value == null ? "?" : value.toString().replace(".0", "");
  239.     }

  240. }