1 /* 2 * Copyright (c) 2012, 2018, 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 }