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