1 /*
   2  * Copyright (c) 2012, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.layout;
  27 
  28 import javafx.beans.NamedArg;
  29 import javafx.geometry.Insets;
  30 import javafx.scene.paint.Color;
  31 import javafx.scene.paint.Paint;
  32 import javafx.scene.shape.StrokeType;
  33 
  34 /**
  35  * Defines the stroke to use on a {@link Border} for styling a {@code Region}.
  36  * The stroke is a vector-based rendering that outlines the border area.
  37  * It can be inset (or outset) from the {@code Region}'s edge, and the values
  38  * of the stroke are taken into account when computing the {@code Region}'s
  39  * insets (for defining the content area). The stroke visuals are not used when
  40  * any {@link BorderImage}s are in use.
  41  * <p>
  42  * When applied to a {@code Region} with a defined shape, the border width
  43  * and stroking information for the {@code top} is used, while the other
  44  * attributes are ignored.
  45  *
  46  * @since JavaFX 8.0
  47  */
  48 public class BorderStroke {
  49     /**
  50      * The default insets when "thin" is specified.
  51      */
  52     public static final BorderWidths THIN = new BorderWidths(1);
  53 
  54     /**
  55      * The default insets when "medium" is specified
  56      */
  57     public static final BorderWidths MEDIUM = new BorderWidths(3);
  58 
  59     /**
  60      * The default insets when "thick" is specified
  61      */
  62     public static final BorderWidths THICK = new BorderWidths(5);
  63 
  64     /**
  65      * The default Insets to be used with a BorderStroke that does not
  66      * otherwise define any.
  67      */
  68     public static final BorderWidths DEFAULT_WIDTHS = THIN;
  69 
  70     /**
  71      * Defines the fill of top side of this border.
  72      *
  73      * @return the fill of top side of this border
  74      * @defaultValue {@code Color.BLACK}
  75      */
  76     public final Paint getTopStroke() { return topStroke; }
  77     final Paint topStroke;
  78     // TODO: The spec says the default color is "currentColor", which appears to mean
  79     // by default the color is "inherit". So we should file a JIRA on this so that
  80     // we use inherit. But first I'd like a performance analysis.
  81 
  82     /**
  83      * Defines the fill of right side of this border. If {@code null}, then the
  84      * {@code topFill} is used.
  85      *
  86      * @return the fill of right side of this border
  87      * @defaultValue {@code null} (same as {@code topFill})
  88      */
  89     public final Paint getRightStroke() { return rightStroke; }
  90     final Paint rightStroke;
  91 
  92     /**
  93      * Defines the fill of bottom side of this border. If {@code null}, then the
  94      * {@code topFill} is used.
  95      *
  96      * @return the fill of bottom side of this border
  97      * @defaultValue {@code null} (same as {@code topFill})
  98      */
  99     public final Paint getBottomStroke() { return bottomStroke; }
 100     final Paint bottomStroke;
 101 
 102     /**
 103      * Defines the fill of left side of this border. If {@code null}, then the
 104      * {@code rightFill} is used.
 105      *
 106      * @return the fill of left side of this border
 107      * @defaultValue {@code null} (same as {@code rightFill})
 108      */
 109     public final Paint getLeftStroke() { return leftStroke; }
 110     final Paint leftStroke;
 111 
 112     /**
 113      * Defines the style of top side of this border.
 114      *
 115      * @return the style of top side of this border
 116      * @defaultValue {@code BorderStrokeStyle.NONE}
 117      */
 118     public final BorderStrokeStyle getTopStyle() { return topStyle; }
 119     final BorderStrokeStyle topStyle;
 120 
 121     /**
 122      * Defines the style of right side of this border. If {@code null}, then
 123      * {@code topStyle} is used;
 124      *
 125      * @return the style of right side of this border
 126      * @defaultValue {@code null} (same as {@code topStyle})
 127      */
 128     public final BorderStrokeStyle getRightStyle() { return rightStyle; }
 129     final BorderStrokeStyle rightStyle;
 130 
 131     /**
 132      * Defines the style of bottom side of this border. If {@code null}, then
 133      * {@code topStyle} is used;  Use {@code BorderStyle.NONE} to set the border to
 134      * have no border style.
 135      *
 136      * @return the style of bottom side of this border
 137      * @defaultValue {@code null} (same as {@code topStyle})
 138      */
 139     public final BorderStrokeStyle getBottomStyle() { return bottomStyle; }
 140     final BorderStrokeStyle bottomStyle;
 141 
 142     /**
 143      * Defines the style of left side of this border. If {@code null}, then
 144      * {@code rightStyle} is used. Use {@code BorderStyle.NONE} to set the border to
 145      * have no border style.
 146      *
 147      * @return the style of left side of this border
 148      * @defaultValue {@code null} (same as {@code rightStyle})
 149      */
 150     public final BorderStrokeStyle getLeftStyle() { return leftStyle; }
 151     final BorderStrokeStyle leftStyle;
 152 
 153     /**
 154      * Defines the thickness of each side of the {@code BorderStroke}. This will never
 155      * be {@code null}, and defaults to {@code DEFAULT_WIDTHS}.
 156      * @return the thickness of each side of the {@code BorderStroke}
 157      */
 158     public final BorderWidths getWidths() { return widths; }
 159     final BorderWidths widths;
 160 
 161     /**
 162      * Defines the insets of each side of the {@code BorderStroke}. This will never
 163      * be {@code null}, and defaults to {@code Insets.EMPTY}.
 164      * @return the insets of each side of the {@code BorderStroke}
 165      */
 166     public final Insets getInsets() { return insets; }
 167     final Insets insets;
 168 
 169     // These two are used by Border to compute the insets and outsets of the border
 170     final Insets innerEdge;
 171     final Insets outerEdge;
 172 
 173     /**
 174      * Defines the radii for each corner of this {@code BorderStroke}. This will never
 175      * be {@code null}, and defaults to {@code CornerRadii.EMPTY}.
 176      * @return the radii for each corner of this {@code BorderStroke}
 177      */
 178     public final CornerRadii getRadii() { return radii; }
 179      /* TODO I should change CornerRadii to be 4 properties, one for each corner,
 180      * where each corner is a horizontal / vertical radius! I think that would
 181      * be cleaner. */
 182     private final CornerRadii radii;
 183 
 184     /**
 185      * Checks if the stroke of this region is uniform. A uniform stroke has all its side
 186      * strokes (top, bottom, left, right) of same color, width and style.
 187      * @return {@code true} if border stroke is uniform
 188      */
 189     public final boolean isStrokeUniform() { return strokeUniform; }
 190     private final boolean strokeUniform;
 191 
 192     /**
 193      * A cached hash code
 194      */
 195     private final int hash;
 196 
 197     /**
 198      * Creates a new {@code BorderStroke}.
 199      *
 200      * @param stroke    The stroke to use for all sides. If {@code null}, defaults to {@code Color.BLACK}.
 201      * @param style     The style to use for all sides. If {@code null}, defaults to {@code BorderStrokeStyle.NONE}.
 202      * @param radii     The radii to use. If {@code null}, defaults to {@code CornerRadii.EMPTY}.
 203      * @param widths    The widths to use. If {@code null}, defaults to {@code DEFAULT_WIDTHS}.
 204      */
 205     public BorderStroke(@NamedArg("stroke") Paint stroke, @NamedArg("style") BorderStrokeStyle style, @NamedArg("radii") CornerRadii radii, @NamedArg("widths") BorderWidths widths) {
 206         // TODO: Note that we default to THIN, not to MEDIUM as the CSS spec says. So it will be
 207         // up to our CSS converter code to make sure the default is MEDIUM in that case.
 208         this.leftStroke = this.topStroke = this.rightStroke = this.bottomStroke = stroke == null ? Color.BLACK : stroke;
 209         this.topStyle = this.rightStyle = this.bottomStyle = this.leftStyle = style == null ? BorderStrokeStyle.NONE : style;
 210         this.radii = radii == null ? CornerRadii.EMPTY : radii;
 211         this.widths = widths == null ? DEFAULT_WIDTHS : widths;
 212         this.insets = Insets.EMPTY;
 213 
 214         // TODO: Our inside / outside should be 0 when stroke type is NONE in that dimension!
 215         // In fact, we could adjust the widths in such a case so that when you ask for the
 216         // widths, you get 0 instead of whatever was specified. See 4.3 of the CSS Spec.
 217 
 218         // Strokes can only differ in width
 219         strokeUniform = this.widths.left == this.widths.top &&
 220                         this.widths.left == this.widths.right &&
 221                         this.widths.left == this.widths.bottom;
 222 
 223         // Since insets are empty, don't have to worry about it
 224         innerEdge = new Insets(
 225                 computeInside(this.topStyle.getType(), this.widths.getTop()),
 226                 computeInside(this.rightStyle.getType(), this.widths.getRight()),
 227                 computeInside(this.bottomStyle.getType(), this.widths.getBottom()),
 228                 computeInside(this.leftStyle.getType(), this.widths.getLeft())
 229         );
 230         outerEdge = new Insets(
 231                 Math.max(0, computeOutside(this.topStyle.getType(), this.widths.getTop())),
 232                 Math.max(0, computeOutside(this.rightStyle.getType(), this.widths.getRight())),
 233                 Math.max(0, computeOutside(this.bottomStyle.getType(), this.widths.getBottom())),
 234                 Math.max(0, computeOutside(this.leftStyle.getType(), this.widths.getLeft()))
 235         );
 236         this.hash = preComputeHash();
 237     }
 238 
 239     /**
 240      * Creates a new {@code BorderStroke}.
 241      *
 242      * @param stroke    The stroke to use for all sides. If {@code null}, defaults to {@code Color.BLACK}.
 243      * @param style     The style to use for all sides. If {@code null}, defaults to {@code BorderStrokeStyle.NONE}.
 244      * @param radii     The radii to use. If {@code null}, defaults to {@code CornerRadii.EMPTY}.
 245      * @param widths    The widths to use. If {@code null}, defaults to {@code DEFAULT_WIDTHS}.
 246      * @param insets    The insets indicating where to draw the border relative to the region edges.
 247      *                  If {@code null}, defaults to {@code Insets.EMPTY}.
 248      */
 249     public BorderStroke(@NamedArg("stroke") Paint stroke, @NamedArg("style") BorderStrokeStyle style, @NamedArg("radii") CornerRadii radii, @NamedArg("widths") BorderWidths widths, @NamedArg("insets") Insets insets) {
 250         this(stroke, stroke, stroke, stroke, style, style, style, style, radii, widths, insets);
 251     }
 252 
 253     /**
 254      * Creates a new {@code BorderStroke}, specifying all construction parameters.
 255      *
 256      * @param topStroke       The fill to use on the top. If {@code null}, defaults to {@code Color.BLACK}.
 257      * @param rightStroke     The fill to use on the right. If {@code null}, defaults to the same value as {@code topStroke}.
 258      * @param bottomStroke    The fill to use on the bottom. If {@code null}, defaults to the same value as {@code topStroke}.
 259      * @param leftStroke      The fill to use on the left. If {@code null}, defaults to the same value as {@code rightStroke}.
 260      * @param topStyle        The style to use on the top. If {@code null}, defaults to {@code BorderStrokeStyle.NONE}.
 261      * @param rightStyle      The style to use on the right. If {@code null}, defaults to the same value as {@code topStyle}.
 262      * @param bottomStyle     The style to use on the bottom. If {@code null}, defaults to the same value as {@code topStyle}.
 263      * @param leftStyle       The style to use on the left. If {@code null}, defaults to the same value as {@code rightStyle}.
 264      * @param radii           The radii. If {@code null}, defaults to square corners by using {@code CornerRadii.EMPTY}.
 265      * @param widths          The thickness of each side. If {@code null}, defaults to {@code DEFAULT_WIDTHS}.
 266      * @param insets          The insets indicating where to draw the border relative to the region edges.
 267      *                        If {@code null}, defaults to {@code Insets.EMPTY}.
 268      */
 269     public BorderStroke(
 270             @NamedArg("topStroke") Paint topStroke, @NamedArg("rightStroke") Paint rightStroke, @NamedArg("bottomStroke") Paint bottomStroke, @NamedArg("leftStroke") Paint leftStroke,
 271             @NamedArg("topStyle") BorderStrokeStyle topStyle, @NamedArg("rightStyle") BorderStrokeStyle rightStyle,
 272             @NamedArg("bottomStyle") BorderStrokeStyle bottomStyle, @NamedArg("leftStyle") BorderStrokeStyle leftStyle,
 273             @NamedArg("radii") CornerRadii radii, @NamedArg("widths") BorderWidths widths, @NamedArg("insets") Insets insets)
 274     {
 275         this.topStroke = topStroke == null ? Color.BLACK : topStroke;
 276         this.rightStroke = rightStroke == null ? this.topStroke : rightStroke;
 277         this.bottomStroke = bottomStroke == null ? this.topStroke : bottomStroke;
 278         this.leftStroke = leftStroke == null ? this.rightStroke : leftStroke;
 279         this.topStyle = topStyle == null ? BorderStrokeStyle.NONE : topStyle;
 280         this.rightStyle = rightStyle == null ? this.topStyle : rightStyle;
 281         this.bottomStyle = bottomStyle == null ? this.topStyle : bottomStyle;
 282         this.leftStyle = leftStyle == null ? this.rightStyle : leftStyle;
 283         this.radii = radii == null ? CornerRadii.EMPTY : radii;
 284         this.widths = widths == null ? DEFAULT_WIDTHS : widths;
 285         this.insets = insets == null ? Insets.EMPTY : insets;
 286 
 287 
 288         final boolean colorsSame =
 289                 this.leftStroke.equals(this.topStroke) &&
 290                 this.leftStroke.equals(this.rightStroke) &&
 291                 this.leftStroke.equals(this.bottomStroke);
 292         final boolean widthsSame =
 293                 this.widths.left == this.widths.top &&
 294                 this.widths.left == this.widths.right &&
 295                 this.widths.left == this.widths.bottom;
 296         final boolean stylesSame =
 297                 this.leftStyle.equals(this.topStyle) &&
 298                 this.leftStyle.equals(this.rightStyle) &&
 299                 this.leftStyle.equals(this.bottomStyle);
 300 
 301         strokeUniform = colorsSame && widthsSame && stylesSame;
 302 
 303         // TODO these calculations are not accurate if we are stroking a shape. In such cases, we
 304         // need to account for the mitre limit
 305 
 306         innerEdge = new Insets(
 307                 this.insets.getTop() + computeInside(this.topStyle.getType(), this.widths.getTop()),
 308                 this.insets.getRight() + computeInside(this.rightStyle.getType(), this.widths.getRight()),
 309                 this.insets.getBottom() + computeInside(this.bottomStyle.getType(), this.widths.getBottom()),
 310                 this.insets.getLeft() + computeInside(this.leftStyle.getType(), this.widths.getLeft())
 311         );
 312         outerEdge = new Insets(
 313                 Math.max(0, computeOutside(this.topStyle.getType(), this.widths.getTop()) - this.insets.getTop()),
 314                 Math.max(0, computeOutside(this.rightStyle.getType(), this.widths.getRight()) - this.insets.getRight()),
 315                 Math.max(0, computeOutside(this.bottomStyle.getType(), this.widths.getBottom())- this.insets.getBottom()),
 316                 Math.max(0, computeOutside(this.leftStyle.getType(), this.widths.getLeft()) - this.insets.getLeft())
 317         );
 318         this.hash = preComputeHash();
 319     }
 320 
 321     private int preComputeHash() {
 322         int result;
 323         result = topStroke.hashCode();
 324         result = 31 * result + rightStroke.hashCode();
 325         result = 31 * result + bottomStroke.hashCode();
 326         result = 31 * result + leftStroke.hashCode();
 327         result = 31 * result + topStyle.hashCode();
 328         result = 31 * result + rightStyle.hashCode();
 329         result = 31 * result + bottomStyle.hashCode();
 330         result = 31 * result + leftStyle.hashCode();
 331         result = 31 * result + widths.hashCode();
 332         result = 31 * result + radii.hashCode();
 333         result = 31 * result + insets.hashCode();
 334         return result;
 335     }
 336 
 337     private double computeInside(StrokeType type, double width) {
 338         if (type == StrokeType.OUTSIDE) {
 339             return 0;
 340         } else if (type == StrokeType.CENTERED) {
 341             return width / 2.0;
 342         } else if (type == StrokeType.INSIDE) {
 343             return width;
 344         } else {
 345             throw new AssertionError("Unexpected Stroke Type");
 346         }
 347     }
 348 
 349     private double computeOutside(StrokeType type, double width) {
 350         if (type == StrokeType.OUTSIDE) {
 351             return width;
 352         } else if (type == StrokeType.CENTERED) {
 353             return width / 2.0;
 354         } else if (type == StrokeType.INSIDE) {
 355             return 0;
 356         } else {
 357             throw new AssertionError("Unexpected Stroke Type");
 358         }
 359     }
 360 
 361     /**
 362      * {@inheritDoc}
 363      */
 364     @Override public boolean equals(Object o) {
 365         if (this == o) return true;
 366         if (o == null || getClass() != o.getClass()) return false;
 367         BorderStroke that = (BorderStroke) o;
 368         if (this.hash != that.hash) return false;
 369         if (!bottomStroke.equals(that.bottomStroke)) return false;
 370         if (!bottomStyle.equals(that.bottomStyle)) return false;
 371         if (!leftStroke.equals(that.leftStroke)) return false;
 372         if (!leftStyle.equals(that.leftStyle)) return false;
 373         if (!radii.equals(that.radii)) return false;
 374         if (!rightStroke.equals(that.rightStroke)) return false;
 375         if (!rightStyle.equals(that.rightStyle)) return false;
 376         if (!topStroke.equals(that.topStroke)) return false;
 377         if (!topStyle.equals(that.topStyle)) return false;
 378         if (!widths.equals(that.widths)) return false;
 379         if (!insets.equals(that.insets)) return false;
 380 
 381         return true;
 382     }
 383 
 384     /**
 385      * {@inheritDoc}
 386      */
 387     @Override public int hashCode() {
 388         return hash;
 389     }
 390 }