1 /* 2 * Copyright (c) 2011, 2016, 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 com.sun.javafx.sg.prism; 27 28 import javafx.geometry.Insets; 29 import javafx.geometry.Side; 30 import javafx.scene.layout.Background; 31 import javafx.scene.layout.BackgroundFill; 32 import javafx.scene.layout.BackgroundImage; 33 import javafx.scene.layout.BackgroundPosition; 34 import javafx.scene.layout.BackgroundRepeat; 35 import javafx.scene.layout.BackgroundSize; 36 import javafx.scene.layout.Border; 37 import javafx.scene.layout.BorderImage; 38 import javafx.scene.layout.BorderRepeat; 39 import javafx.scene.layout.BorderStroke; 40 import javafx.scene.layout.BorderStrokeStyle; 41 import javafx.scene.layout.BorderWidths; 42 import javafx.scene.layout.CornerRadii; 43 import javafx.scene.paint.Color; 44 import javafx.scene.paint.LinearGradient; 45 import javafx.scene.shape.StrokeLineCap; 46 import javafx.scene.shape.StrokeLineJoin; 47 import javafx.scene.shape.StrokeType; 48 49 import java.util.Collections; 50 import java.util.List; 51 import java.util.WeakHashMap; 52 53 import com.sun.glass.ui.Screen; 54 import com.sun.javafx.PlatformUtil; 55 import com.sun.javafx.application.PlatformImpl; 56 import com.sun.javafx.geom.Path2D; 57 import com.sun.javafx.geom.RectBounds; 58 import com.sun.javafx.geom.Rectangle; 59 import com.sun.javafx.geom.Shape; 60 import com.sun.javafx.geom.transform.Affine2D; 61 import com.sun.javafx.geom.transform.BaseTransform; 62 import com.sun.javafx.geom.transform.GeneralTransform3D; 63 import com.sun.javafx.logging.PulseLogger; 64 import static com.sun.javafx.logging.PulseLogger.PULSE_LOGGING_ENABLED; 65 import com.sun.javafx.tk.Toolkit; 66 import com.sun.prism.BasicStroke; 67 import com.sun.prism.Graphics; 68 import com.sun.prism.Image; 69 import com.sun.prism.RTTexture; 70 import com.sun.prism.Texture; 71 import com.sun.prism.impl.PrismSettings; 72 import com.sun.prism.paint.ImagePattern; 73 import com.sun.prism.paint.Paint; 74 import com.sun.scenario.effect.Offset; 75 76 /** 77 * Implementation of the Region peer. This behaves like an NGGroup, in that 78 * it has children, but like a leaf node, in that it also draws itself if it has 79 * a Background or Border which contains non-transparent fills / strokes / images. 80 */ 81 public class NGRegion extends NGGroup { 82 /** 83 * This scratch transform is used when transforming shapes. Because this is 84 * a static variable, it is only intended to be used from a single thread, 85 * the render thread in this case. 86 */ 87 private static final Affine2D SCRATCH_AFFINE = new Affine2D(); 88 89 /** 90 * Temporary rect for general use. Because this is a static variable, 91 * it is only intended to be used from a single thread, the render thread 92 * in this case. 93 */ 94 private static final Rectangle TEMP_RECT = new Rectangle(); 95 96 /** 97 * Screen to RegionImageCache mapping. This mapping is required as textures 98 * are only valid in graphics context used to create them (relies on a one 99 * to one mapping between Screen and GraphicsContext). 100 */ 101 private static WeakHashMap<Screen, RegionImageCache> imageCacheMap = new WeakHashMap<>(); 102 103 /** 104 * Indicates the cached image can be sliced vertically. 105 */ 106 private static final int CACHE_SLICE_V = 0x1; 107 108 /** 109 * Indicates the cached image can be sliced horizontally. 110 */ 111 private static final int CACHE_SLICE_H = 0x2; 112 113 /** 114 * The background to use for drawing. Since this is an immutable object, I can simply refer to 115 * its fields / methods directly when rendering. I will make sure this is not ever null at 116 * the time that we do the sync, so that the code in this class can assume non-null. 117 */ 118 private Background background = Background.EMPTY; 119 120 /** 121 * The combined insets of all the backgrounds. As of right now, Background doesn't store 122 * this information itself, although it probably could (and probably should). 123 */ 124 private Insets backgroundInsets = Insets.EMPTY; 125 126 /** 127 * The border to use for drawing. Similar to background, this is not-null and immutable. 128 */ 129 private Border border = Border.EMPTY; 130 131 /** 132 * The normalized list of CornerRadii have been precomputed at the FX layer to 133 * properly account for percentages, insets and radii scaling to prevent 134 * the radii from overflowing the dimensions of the region. 135 * The List objects are shared with the FX layer and are therefore 136 * unmodifiable. If the normalized list is null then it means that all 137 * of the raw radii in the list were already absolute and non-overflowing 138 * and so the originals can be used from the arrays of strokes and fills. 139 */ 140 private List<CornerRadii> normalizedFillCorners; 141 private List<CornerRadii> normalizedStrokeCorners; 142 143 /** 144 * The shape of the region. Usually this will be null (except for things like check box 145 * checks, scroll bar down arrows / up arrows, etc). If this is not null, it determines 146 * the shape of the region to draw. If it is null, then the assumed shape of the region is 147 * one of a rounded rectangle. This shape is a com.sun.javafx.geom.Shape, and is not 148 * touched by the FX scene graph except during synchronization, so it is safe to access 149 * on the render thread. 150 */ 151 private Shape shape; 152 private NGShape ngShape; 153 154 /** 155 * Whether we should scale the shape to match the bounds of the region. Only applies 156 * if the shape is not null. 157 */ 158 private boolean scaleShape = true; 159 160 /** 161 * Whether we should center the shape within the bounds of the region. Only applies 162 * if the shape is not null. 163 */ 164 private boolean centerShape = true; 165 166 /** 167 * Whether we should attempt to use region caching for a region with a shape. 168 */ 169 private boolean cacheShape = false; 170 171 /** 172 * A cached set of the opaque insets as given to us during synchronization. We hold 173 * on to this so that we can determine the opaque insets in the computeOpaqueRegion method. 174 */ 175 private float opaqueTop = Float.NaN, 176 opaqueRight = Float.NaN, 177 opaqueBottom = Float.NaN, 178 opaqueLeft = Float.NaN; 179 180 /** 181 * The width and height of the region. 182 */ 183 private float width, height; 184 185 /** 186 * Determined when a background is set on the region, this flag indicates whether this 187 * background can be cached. As of this time, the only backgrounds which can be cached 188 * are those where there are only solid fills or linear gradients. 189 */ 190 private int cacheMode; 191 192 /** 193 * Is the key into the image cache that identifies the required background 194 * for the region. 195 */ 196 private Integer cacheKey; 197 198 /** 199 * Simple Helper Function for cleanup. 200 */ 201 static Paint getPlatformPaint(javafx.scene.paint.Paint paint) { 202 return (Paint)Toolkit.getPaintAccessor().getPlatformPaint(paint); 203 } 204 205 // We create a class instance of a no op. Effect internally to handle 3D 206 // transform if user didn't use Effect for 3D Transformed Region. This will 207 // automatically forces Region rendering path to use the Effect path. 208 private static final Offset nopEffect = new Offset(0, 0, null); 209 private EffectFilter nopEffectFilter; 210 211 /************************************************************************** 212 * * 213 * Methods used during synchronization only. * 214 * * 215 *************************************************************************/ 216 217 /** 218 * Called by the Region during synchronization. The Region *should* ensure that this is only 219 * called when one of these properties has changed. The cost of calling it excessively is 220 * only that the opaque region is invalidated excessively. Updating the shape and 221 * associated booleans is actually a very cheap operation. 222 * 223 * @param shape The shape, may be null. 224 * @param scaleShape whether to scale the shape 225 * @param positionShape whether to center the shape 226 */ 227 public void updateShape(Object shape, boolean scaleShape, boolean positionShape, boolean cacheShape) { 228 this.ngShape = shape == null ? null : ((javafx.scene.shape.Shape)shape).impl_getPeer(); 229 this.shape = shape == null ? null : ngShape.getShape(); 230 this.scaleShape = scaleShape; 231 this.centerShape = positionShape; 232 this.cacheShape = cacheShape; 233 // Technically I don't think this is needed because whenever the shape changes, setOpaqueInsets 234 // is also called, so this will get invalidated twice. 235 invalidateOpaqueRegion(); 236 cacheKey = null; 237 visualsChanged(); 238 } 239 240 /** 241 * Called by the Region whenever the width or height of the region has changed. 242 * The Region *should* only call this when the width or height have actually changed. 243 * 244 * @param width The width of the region, not including insets or outsets 245 * @param height The height of the region, not including insets or outsets 246 */ 247 public void setSize(float width, float height) { 248 this.width = width; 249 this.height = height; 250 invalidateOpaqueRegion(); 251 cacheKey = null; 252 // We only have to clear the background insets when the size changes if the 253 // background has fills who's insets are dependent on the size (as would be 254 // true only if a CornerRadii of any background fill on the background had 255 // a percentage based radius). 256 if (background != null && background.isFillPercentageBased()) { 257 backgroundInsets = null; 258 } 259 } 260 261 /** 262 * Called by Region whenever an image that was being loaded in the background has 263 * finished loading. Nothing changes in terms of metrics or sizes or caches, but 264 * we do need to repaint everything. 265 */ 266 public void imagesUpdated() { 267 visualsChanged(); 268 } 269 270 /** 271 * Called by the Region when the Border is changed. The Region *must* only call 272 * this method if the border object has actually changed, or excessive work may be done. 273 * 274 * @param b Border, of type javafx.scene.layout.Border 275 */ 276 public void updateBorder(Border b) { 277 // Make sure that the border instance we store on this NGRegion is never null 278 final Border old = border; 279 border = b == null ? Border.EMPTY : b; 280 281 // Determine whether the geometry has changed, or if only the visuals have 282 // changed. Geometry changes will require more work, and an equals check 283 // on the border objects is generally very fast (either for identity or 284 // for !equals. It is a bit longer for when they really are equal, but faster 285 // than a geometryChanged!) 286 if (!border.getOutsets().equals(old.getOutsets())) { 287 geometryChanged(); 288 } else { 289 visualsChanged(); 290 } 291 } 292 293 /** 294 * Called by the Region when any parameters are changed. 295 * It is only technically needed when a parameter that affects the size 296 * of any percentage or overflowing corner radii is changed, but since 297 * the data is not processed here in NGRegion, it is set on every update 298 * of the peers for any reason. 299 * A null value means that the raw radii in the BorderStroke objects 300 * themselves were already absolute and non-overflowing. 301 * 302 * @param normalizedStrokeCorners a precomputed copy of the radii in the 303 * BorderStroke objects that are not percentages and do not overflow 304 */ 305 public void updateStrokeCorners(List<CornerRadii> normalizedStrokeCorners) { 306 this.normalizedStrokeCorners = normalizedStrokeCorners; 307 } 308 309 /** 310 * Returns the normalized (non-percentage, non-overflowing) radii for the 311 * selected index into the BorderStroke objects. 312 * If a List was synchronized from the Region object, the value from that 313 * List, otherwise the raw radii are fetched from the indicated BorderStroke 314 * object. 315 * 316 * @param index the index of the BorderStroke object being processed 317 * @return the normalized radii for the indicated BorderStroke object 318 */ 319 private CornerRadii getNormalizedStrokeRadii(int index) { 320 return (normalizedStrokeCorners == null 321 ? border.getStrokes().get(index).getRadii() 322 : normalizedStrokeCorners.get(index)); 323 } 324 325 /** 326 * Called by the Region when the Background has changed. The Region *must* only call 327 * this method if the background object has actually changed, or excessive work may be done. 328 * 329 * @param b Background, of type javafx.scene.layout.Background. Can be null. 330 */ 331 public void updateBackground(Background b) { 332 // NOTE: We don't explicitly invalidate the opaque region in this method, because the 333 // Region will always call setOpaqueInsets whenever the background is changed, and 334 // setOpaqueInsets always invalidates the opaque region. So we don't have to do it 335 // again here. This wasn't immediately obvious and it might be better to combine 336 // the updateBackground and setOpaqueInsets methods into one call, so that we 337 // can more easily ensure that the opaque region is updated correctly. 338 339 // Make sure that the background instance we store on this NGRegion is never null 340 final Background old = background; 341 background = b == null ? Background.EMPTY : b; 342 343 final List<BackgroundFill> fills = background.getFills(); 344 cacheMode = 0; 345 if (!PrismSettings.disableRegionCaching && !fills.isEmpty() && (shape == null || cacheShape)) { 346 cacheMode = CACHE_SLICE_H | CACHE_SLICE_V; 347 for (int i=0, max=fills.size(); i<max && cacheMode != 0; i++) { 348 // We need to now inspect the paint to determine whether we can use a cache for this background. 349 // If a shape is being used, we don't care about gradients (we cache 'em both), but for a rectangle 350 // fill we omit these (so we can do 3-patch scaling). An ImagePattern is deadly to either 351 // (well, only deadly to a shape if it turns out to be a writable image). 352 final BackgroundFill fill = fills.get(i); 353 javafx.scene.paint.Paint paint = fill.getFill(); 354 if (shape == null) { 355 if (paint instanceof LinearGradient) { 356 LinearGradient linear = (LinearGradient) paint; 357 if (linear.getStartX() != linear.getEndX()) { 358 cacheMode &= ~CACHE_SLICE_H; 359 } 360 if (linear.getStartY() != linear.getEndY()) { 361 cacheMode &= ~CACHE_SLICE_V; 362 } 363 } else if (!(paint instanceof Color)) { 364 //Either radial gradient or image pattern 365 cacheMode = 0; 366 } 367 } else if (paint instanceof javafx.scene.paint.ImagePattern) { 368 cacheMode = 0; 369 } 370 } 371 } 372 backgroundInsets = null; 373 cacheKey = null; 374 375 // Only update the geom if the new background is geometrically different from the old 376 if (!background.getOutsets().equals(old.getOutsets())) { 377 geometryChanged(); 378 } else { 379 visualsChanged(); 380 } 381 } 382 383 /** 384 * Called by the Region when any parameters are changed. 385 * It is only technically needed when a parameter that affects the size 386 * of any percentage or overflowing corner radii is changed, but since 387 * the data is not processed here in NGRegion, it is set on every update 388 * of the peers for any reason. 389 * A null value means that the raw radii in the BackgroundFill objects 390 * themselves were already absolute and non-overflowing. 391 * 392 * @param normalizedStrokeCorners a precomputed copy of the radii in the 393 * BackgroundFill objects that are not percentages and do not overflow 394 */ 395 public void updateFillCorners(List<CornerRadii> normalizedFillCorners) { 396 this.normalizedFillCorners = normalizedFillCorners; 397 } 398 399 /** 400 * Returns the normalized (non-percentage, non-overflowing) radii for the 401 * selected index into the BackgroundFill objects. 402 * If a List was synchronized from the Region object, the value from that 403 * List, otherwise the raw radii are fetched from the indicated BackgroundFill 404 * object. 405 * 406 * @param index the index of the BackgroundFill object being processed 407 * @return the normalized radii for the indicated BackgroundFill object 408 */ 409 private CornerRadii getNormalizedFillRadii(int index) { 410 return (normalizedFillCorners == null 411 ? background.getFills().get(index).getRadii() 412 : normalizedFillCorners.get(index)); 413 } 414 415 /** 416 * Called by the Region whenever it knows that the opaque insets have changed. The 417 * Region <strong>must</strong> make sure that these opaque insets include the opaque 418 * inset information from the Border and Background as well, the NGRegion will not 419 * recompute this information. This is done because Border and Background are immutable, 420 * and as such this information is computed once and stored rather than recomputed 421 * each time we have to render. Any developer supplied opaque insets must be combined 422 * with the Border / Background intrinsic opaque insets prior to this call and passed 423 * as the arguments to this method. 424 * 425 * @param top The top, if NaN then there is no opaque inset at all 426 * @param right The right, must not be NaN or Infinity, etc. 427 * @param bottom The bottom, must not be NaN or Infinity, etc. 428 * @param left The left, must not be NaN or Infinity, etc. 429 */ 430 public void setOpaqueInsets(float top, float right, float bottom, float left) { 431 opaqueTop = top; 432 opaqueRight = right; 433 opaqueBottom = bottom; 434 opaqueLeft = left; 435 invalidateOpaqueRegion(); 436 } 437 438 /** 439 * When cleaning the dirty tree, we also have to keep in mind 440 * the NGShape used by the NGRegion 441 */ 442 @Override public void clearDirtyTree() { 443 super.clearDirtyTree(); 444 if (ngShape != null) { 445 ngShape.clearDirtyTree(); 446 } 447 } 448 449 /************************************************************************** 450 * * 451 * Implementations of methods defined in the parent classes, with the * 452 * exception of rendering methods. * 453 * * 454 *************************************************************************/ 455 456 private RegionImageCache getImageCache(final Graphics g) { 457 final Screen screen = g.getAssociatedScreen(); 458 RegionImageCache cache = imageCacheMap.get(screen); 459 if (cache != null) { 460 RTTexture tex = cache.getBackingStore(); 461 if (tex.isSurfaceLost()) { 462 imageCacheMap.remove(screen); 463 cache = null; 464 } 465 } 466 if (cache == null) { 467 cache = new RegionImageCache(g.getResourceFactory()); 468 imageCacheMap.put(screen, cache); 469 } 470 return cache; 471 } 472 473 private Integer getCacheKey(int w, int h) { 474 if (cacheKey == null) { 475 int key = 31 * w; 476 key = key * 37 + h; 477 key = key * 47 + background.hashCode(); 478 if (shape != null) { 479 key = key * 73 + shape.hashCode(); 480 } 481 cacheKey = key; 482 } 483 return cacheKey; 484 } 485 486 @Override protected boolean supportsOpaqueRegions() { return true; } 487 488 @Override 489 protected boolean hasOpaqueRegion() { 490 return super.hasOpaqueRegion() && 491 !Float.isNaN(opaqueTop) && !Float.isNaN(opaqueRight) && 492 !Float.isNaN(opaqueBottom) && !Float.isNaN(opaqueLeft); 493 } 494 495 /** 496 * The opaque region of an NGRegion takes into account the opaque insets 497 * specified by the Region during synchronization. It also takes into 498 * account the clip and the effect. 499 * 500 * @param opaqueRegion 501 * @return 502 */ 503 @Override protected RectBounds computeOpaqueRegion(RectBounds opaqueRegion) { 504 // TODO what to do if the opaqueRegion has negative width or height due to excessive opaque insets? (RT-26979) 505 return (RectBounds) opaqueRegion.deriveWithNewBounds(opaqueLeft, opaqueTop, 0, width - opaqueRight, height - opaqueBottom, 0); 506 } 507 508 @Override protected RenderRootResult computeRenderRoot(NodePath path, RectBounds dirtyRegion, 509 int cullingIndex, BaseTransform tx, 510 GeneralTransform3D pvTx) { 511 512 RenderRootResult result = super.computeRenderRoot(path, dirtyRegion, cullingIndex, tx, pvTx); 513 if (result == RenderRootResult.NO_RENDER_ROOT){ 514 result = computeNodeRenderRoot(path, dirtyRegion, cullingIndex, tx, pvTx); 515 } 516 return result; 517 } 518 519 @Override protected boolean hasVisuals() { 520 // This isn't entirely accurate -- the background might 521 // not be empty but still not draw anything since a BackgroundFill 522 // might be TRANSPARENT. The same is true of the border, which 523 // might have BorderStrokes but perhaps none of them draw. 524 return !border.isEmpty() || !background.isEmpty(); 525 } 526 527 @Override protected boolean hasOverlappingContents() { 528 // It may be that this can be optimized further, but I'm a bit 529 // worried about it as I would have to check that the children do not 530 // overlap with the strokes, and the strokes don't overlap each other, 531 // and there are no backgrounds, etc. So there are a few fast paths 532 // that could be used, but not sure it is really of any benefit in 533 // the real cases. 534 return true; 535 } 536 537 /************************************************************************** 538 * * 539 * Region drawing. * 540 * * 541 *************************************************************************/ 542 543 @Override protected void renderContent(Graphics g) { 544 // Use Effect to render a 3D transformed Region that does not contain 3D 545 // transformed children. This is done in order to render the Region's 546 // content and children into an image in local coordinates using the identity 547 // transform. The resulting image will then be correctly transformed in 3D by 548 // the composite transform used to render this Region. 549 // However, we avoid doing this for Regions whose children have a 3D 550 // transform, because it will flatten the transforms of those children 551 // and not look correct. 552 if (!g.getTransformNoClone().is2D() && this.isContentBounds2D()) { 553 assert (getEffectFilter() == null); 554 555 // Use Effect to render 3D transformed Region. 556 // We will need to use a no op. Effect internally since user 557 // didn't use Effect for this Region 558 if (nopEffectFilter == null) { 559 nopEffectFilter = new EffectFilter(nopEffect, this); 560 } 561 nopEffectFilter.render(g); 562 563 return; 564 } 565 566 // If the shape is not null, then the shape will define what we need to draw for 567 // this region. If the shape is null, then the "shape" of the region is just a 568 // rectangle (or rounded rectangle, depending on the Background). 569 if (shape != null) { 570 renderAsShape(g); 571 } else if (width > 0 && height > 0) { 572 renderAsRectangle(g); 573 } 574 575 // Paint the children 576 super.renderContent(g); 577 } 578 579 /************************************************************************** 580 * * 581 * Drawing a region background and borders when the Region has been * 582 * specified to have a shape. This is typically used to render some * 583 * portions of a UI Control, such as the tick on a CheckBox, the dot on a * 584 * RadioButton, or the disclosure node arrow on a TreeView. In these * 585 * cases, the overall region size is typically very small and can * 586 * therefore easily be cached. * 587 * * 588 *************************************************************************/ 589 590 private void renderAsShape(Graphics g) { 591 if (!background.isEmpty()) { 592 // Note: resizeShape is not cheap. This should be refactored so that we only invoke 593 // it if we absolutely have to. Specifically, if the background, shape, and size of the region 594 // has not changed since the last time we rendered we could skip all this and render 595 // directly out of a cache. 596 final Insets outsets = background.getOutsets(); 597 final Shape outsetShape = resizeShape((float) -outsets.getTop(), (float) -outsets.getRight(), 598 (float) -outsets.getBottom(), (float) -outsets.getLeft()); 599 final RectBounds outsetShapeBounds = outsetShape.getBounds(); 600 final int textureWidth = Math.round(outsetShapeBounds.getWidth()), 601 textureHeight = Math.round(outsetShapeBounds.getHeight()); 602 603 final int border = 1; 604 // See if we have a cached representation for this region background already. In UI controls, 605 // the arrow in a scroll bar button or the dot in a radio button or the tick in a check box are 606 // all examples of cases where we'd like to reuse a cached image for performance reasons rather 607 // than re-drawing everything each time. 608 609 RTTexture cached = null; 610 Rectangle rect = null; 611 // RT-25013: We need to make sure that we do not use a cached image in the case of a 612 // scaled region, or things won't look right (they'll looked scaled instead of vector-resized). 613 if (cacheMode != 0 && g.getTransformNoClone().isTranslateOrIdentity()) { 614 final RegionImageCache imageCache = getImageCache(g); 615 if (imageCache.isImageCachable(textureWidth, textureHeight)) { 616 final Integer key = getCacheKey(textureWidth, textureHeight); 617 rect = TEMP_RECT; 618 rect.setBounds(0, 0, textureWidth + border, textureHeight + border); 619 boolean render = imageCache.getImageLocation(key, rect, background, shape, g); 620 if (!rect.isEmpty()) { 621 // An empty rect indicates a failure occurred in the imageCache 622 cached = imageCache.getBackingStore(); 623 } 624 if (cached != null && render) { 625 Graphics cachedGraphics = cached.createGraphics(); 626 627 // Have to move the origin such that when rendering to x=0, we actually end up rendering 628 // at x=bounds.getMinX(). Otherwise anything rendered to the left of the origin would be lost 629 cachedGraphics.translate(rect.x - outsetShapeBounds.getMinX(), 630 rect.y - outsetShapeBounds.getMinY()); 631 renderBackgroundShape(cachedGraphics); 632 if (PULSE_LOGGING_ENABLED) { 633 PulseLogger.incrementCounter("Rendering region shape image to cache"); 634 } 635 } 636 } 637 } 638 639 // "cached" might not be null if either there was a cached image, or we just created one. 640 // In either case, we need to now render from the cached texture to the graphics 641 if (cached != null) { 642 // We just draw exactly what it was we have cached 643 final float dstX1 = outsetShapeBounds.getMinX(); 644 final float dstY1 = outsetShapeBounds.getMinY(); 645 final float dstX2 = outsetShapeBounds.getMaxX(); 646 final float dstY2 = outsetShapeBounds.getMaxY(); 647 648 final float srcX1 = rect.x; 649 final float srcY1 = rect.y; 650 final float srcX2 = srcX1 + textureWidth; 651 final float srcY2 = srcY1 + textureHeight; 652 653 g.drawTexture(cached, dstX1, dstY1, dstX2, dstY2, srcX1, srcY1, srcX2, srcY2); 654 if (PULSE_LOGGING_ENABLED) { 655 PulseLogger.incrementCounter("Cached region shape image used"); 656 } 657 } else { 658 // no cache, rendering backgrounds directly to graphics 659 renderBackgroundShape(g); 660 } 661 } 662 663 // Note that if you use borders, you're going to pay a premium in performance. 664 // I don't think this is strictly necessary (since we won't stretch a cached 665 // region shape anyway), so really this code should some how be combined 666 // with the caching code that happened above for backgrounds. 667 if (!border.isEmpty()) { 668 // We only deal with stroke borders, we never deal with ImageBorders when 669 // painting a shape on a Region. This is primarily because we don't know 670 // how to handle a 9-patch image on a random shape. We'll have to implement 671 // this at some point, but today is not that day. 672 final List<BorderStroke> strokes = border.getStrokes(); 673 for (int i = 0, max = strokes.size(); i < max; i++) { 674 // Get the BorderStroke. When stroking a shape, we only honor the 675 // topStroke, topStyle, widths.top, and insets. 676 final BorderStroke stroke = strokes.get(i); 677 // We're stroking a path, so there is no point trying to figure out the length. 678 // Instead, we just pass -1, telling setBorderStyle to just do a simple stroke 679 setBorderStyle(g, stroke, -1, false); 680 final Insets insets = stroke.getInsets(); 681 g.draw(resizeShape((float) insets.getTop(), (float) insets.getRight(), 682 (float) insets.getBottom(), (float) insets.getLeft())); 683 } 684 } 685 } 686 687 private void renderBackgroundShape(Graphics g) { 688 if (PULSE_LOGGING_ENABLED) { 689 PulseLogger.incrementCounter("NGRegion renderBackgroundShape slow path"); 690 PulseLogger.addMessage("Slow shape path for " + getName()); 691 } 692 693 // We first need to draw each background fill. We don't pay any attention 694 // to the radii of the BackgroundFill, but we do honor the insets and 695 // the fill paint itself. 696 final List<BackgroundFill> fills = background.getFills(); 697 for (int i = 0, max = fills.size(); i < max; i++) { 698 final BackgroundFill fill = fills.get(i); 699 // Get the paint for this BackgroundFill. It should not be possible 700 // for it to ever be null 701 final Paint paint = getPlatformPaint(fill.getFill()); 702 assert paint != null; 703 g.setPaint(paint); 704 // Adjust the box within which we will fit the shape based on the 705 // insets. The resize shape method will resize the shape to fit 706 final Insets insets = fill.getInsets(); 707 g.fill(resizeShape((float) insets.getTop(), (float) insets.getRight(), 708 (float) insets.getBottom(), (float) insets.getLeft())); 709 } 710 711 // We now need to draw each background image. Only the "cover" property 712 // of BackgroundImage, and the "image" property itself, have any impact 713 // on how the image is applied to a Shape. 714 final List<BackgroundImage> images = background.getImages(); 715 for (int i = 0, max = images.size(); i < max; i++) { 716 final BackgroundImage image = images.get(i); 717 final Image prismImage = (Image) image.getImage().impl_getPlatformImage(); 718 if (prismImage == null) { 719 // The prismImage might be null if the Image has not completed loading. 720 // In that case, we simply must skip rendering of that layer this 721 // time around. 722 continue; 723 } 724 // We need to translate the shape based on 0 insets. This will for example 725 // center and / or position the shape if necessary. 726 final Shape translatedShape = resizeShape(0, 0, 0, 0); 727 // Now ensure that the ImagePattern is based on the x/y position of the 728 // shape and not on the 0,0 position of the region. 729 final RectBounds bounds = translatedShape.getBounds(); 730 ImagePattern pattern = image.getSize().isCover() ? 731 new ImagePattern(prismImage, bounds.getMinX(), bounds.getMinY(), 732 bounds.getWidth(), bounds.getHeight(), false, false) : 733 new ImagePattern(prismImage, bounds.getMinX(), bounds.getMinY(), 734 prismImage.getWidth(), prismImage.getHeight(), false, false); 735 g.setPaint(pattern); 736 // Go ahead and finally fill! 737 g.fill(translatedShape); 738 } 739 } 740 741 /************************************************************************** 742 * * 743 * Drawing a region background and borders when the Region has no defined * 744 * shape, and is therefore treated as a rounded rectangle. This is the * 745 * most common code path for UI Controls. * 746 * * 747 *************************************************************************/ 748 749 private void renderAsRectangle(Graphics g) { 750 if (!background.isEmpty()) { 751 renderBackgroundRectangle(g); 752 } 753 754 if (!border.isEmpty()) { 755 renderBorderRectangle(g); 756 } 757 } 758 759 private void renderBackgroundRectangle(Graphics g) { 760 // TODO a big chunk of this only makes sense to do if there actually are background fills, 761 // and we should guard against that. 762 763 // cacheWidth is the width of the region used within the cached image. For example, 764 // perhaps normally the width of a region is 200px. But instead I will render the 765 // region as though it is 20px wide into the cached image. 20px in this case is 766 // the cache width. Although it may draw into more pixels than this (for example, 767 // drawing the focus rectangle extends beyond the width of the region). 768 // left + right background insets give us the left / right slice locations, plus 1 pixel for the center. 769 // Round the whole thing up to be a whole number. 770 if (backgroundInsets == null) updateBackgroundInsets(); 771 final double leftInset = backgroundInsets.getLeft() + 1; 772 final double rightInset = backgroundInsets.getRight() + 1; 773 final double topInset = backgroundInsets.getTop() + 1; 774 final double bottomInset = backgroundInsets.getBottom() + 1; 775 776 // If the insets are too large, then we want to use the width of the region instead of the 777 // computed cacheWidth. RadioButton, for example, enters this case 778 int cacheWidth = roundUp(width); 779 if ((cacheMode & CACHE_SLICE_H) != 0) { 780 cacheWidth = Math.min(cacheWidth, (int) (leftInset + rightInset)); 781 } 782 int cacheHeight = roundUp(height); 783 if ((cacheMode & CACHE_SLICE_V) != 0) { 784 cacheHeight = Math.min(cacheHeight, (int) (topInset + bottomInset)); 785 } 786 787 final Insets outsets = background.getOutsets(); 788 final int outsetsTop = roundUp(outsets.getTop()); 789 final int outsetsRight = roundUp(outsets.getRight()); 790 final int outsetsBottom = roundUp(outsets.getBottom()); 791 final int outsetsLeft = roundUp(outsets.getLeft()); 792 793 // The textureWidth / textureHeight is the width/height of the actual image. This needs to be rounded 794 // up to the next whole pixel value. 795 final int textureWidth = outsetsLeft + cacheWidth + outsetsRight; 796 final int textureHeight = outsetsTop + cacheHeight + outsetsBottom; 797 798 // See if we have a cached representation for this region background already. 799 // RT-25013: We need to make sure that we do not use a cached image in the case of a 800 // scaled region, or things won't look right (they'll looked scaled instead of vector-resized). 801 // RT-25049: Need to only use the cache for pixel aligned regions or the result 802 // will not look the same as though drawn by vector 803 final boolean cache = 804 background.getFills().size() > 1 && // Not worth the overhead otherwise 805 cacheMode != 0 && 806 g.getTransformNoClone().isTranslateOrIdentity(); 807 final int border = 1; 808 RTTexture cached = null; 809 Rectangle rect = null; 810 if (cache) { 811 RegionImageCache imageCache = getImageCache(g); 812 if (imageCache.isImageCachable(textureWidth, textureHeight)) { 813 final Integer key = getCacheKey(textureWidth, textureHeight); 814 rect = TEMP_RECT; 815 rect.setBounds(0, 0, textureWidth + border, textureHeight + border); 816 boolean render = imageCache.getImageLocation(key, rect, background, shape, g); 817 if (!rect.isEmpty()) { 818 // An empty rect indicates a failure occurred in the imageCache 819 cached = imageCache.getBackingStore(); 820 } 821 if (cached != null && render) { 822 Graphics cacheGraphics = cached.createGraphics(); 823 824 // Have to move the origin such that when rendering to x=0, we actually end up rendering 825 // at x=outsets.getLeft(). Otherwise anything rendered to the left of the origin would be lost 826 // Round up to the nearest pixel 827 cacheGraphics.translate(rect.x + outsetsLeft, rect.y + outsetsTop); 828 829 // Rendering backgrounds to the cache 830 renderBackgroundRectanglesDirectly(cacheGraphics, cacheWidth, cacheHeight); 831 832 if (PULSE_LOGGING_ENABLED) { 833 PulseLogger.incrementCounter("Rendering region background image to cache"); 834 } 835 } 836 } 837 } 838 839 // "cached" might not be null if either there was a cached image, or we just created one. 840 // In either case, we need to now render from the cached texture to the graphics 841 if (cached != null) { 842 renderBackgroundRectangleFromCache( 843 g, cached, rect, textureWidth, textureHeight, 844 topInset, rightInset, bottomInset, leftInset, 845 outsetsTop, outsetsRight, outsetsBottom, outsetsLeft); 846 } else { 847 // no cache, rendering backgrounds directly to graphics 848 renderBackgroundRectanglesDirectly(g, width, height); 849 } 850 851 final List<BackgroundImage> images = background.getImages(); 852 for (int i = 0, max = images.size(); i < max; i++) { 853 final BackgroundImage image = images.get(i); 854 Image prismImage = (Image) image.getImage().impl_getPlatformImage(); 855 if (prismImage == null) { 856 // The prismImage might be null if the Image has not completed loading. 857 // In that case, we simply must skip rendering of that layer this 858 // time around. 859 continue; 860 } 861 862 final int imgUnscaledWidth = (int)image.getImage().getWidth(); 863 final int imgUnscaledHeight = (int)image.getImage().getHeight(); 864 final int imgWidth = prismImage.getWidth(); 865 final int imgHeight = prismImage.getHeight(); 866 // TODO need to write tests where we use a writable image and draw to it a lot. (RT-26978) 867 if (imgWidth != 0 && imgHeight != 0) { 868 final BackgroundSize size = image.getSize(); 869 if (size.isCover()) { 870 // When "cover" is true, we can ignore most properties on the BackgroundSize and 871 // BackgroundRepeat and BackgroundPosition. Because the image will be stretched to 872 // fill the entire space, there is no need to know the repeat or position or 873 // size width / height. 874 final float scale = Math.max(width / imgWidth,height / imgHeight); 875 final Texture texture = 876 g.getResourceFactory().getCachedTexture(prismImage, Texture.WrapMode.CLAMP_TO_EDGE); 877 g.drawTexture(texture, 878 0, 0, width, height, 879 0, 0, width/scale, height/scale 880 ); 881 texture.unlock(); 882 } else { 883 // Other than "cover", all other modes need to pay attention to the repeat, 884 // size, and position in order to determine how to render. This next block 885 // of code is responsible for determining the width and height of the area 886 // that we are going to fill. The size might be percentage based, in which 887 // case we need to multiply by the width or height. 888 final double w = size.isWidthAsPercentage() ? size.getWidth() * width : size.getWidth(); 889 final double h = size.isHeightAsPercentage() ? size.getHeight() * height : size.getHeight(); 890 891 // Now figure out the width and height of each tile to be drawn. The actual image 892 // dimensions may be one thing, but we need to figure out what the size of the image 893 // in the destination is going to be. 894 final double tileWidth, tileHeight; 895 if (size.isContain()) { 896 // In the case of "contain", we compute the destination size based on the largest 897 // possible scale such that the aspect ratio is maintained, yet one side of the 898 // region is completely filled. 899 final float scaleX = width / imgUnscaledWidth; 900 final float scaleY = height / imgUnscaledHeight; 901 final float scale = Math.min(scaleX, scaleY); 902 tileWidth = Math.ceil(scale * imgUnscaledWidth); 903 tileHeight = Math.ceil(scale * imgUnscaledHeight); 904 } else if (size.getWidth() >= 0 && size.getHeight() >= 0) { 905 // The width and height have been expressly defined. Note that AUTO is -1, 906 // and all other negative values are disallowed, so by checking >= 0, we 907 // are essentially saying "if neither is AUTO" 908 tileWidth = w; 909 tileHeight = h; 910 } else if (w >= 0) { 911 // In this case, the width is specified, but the height is AUTO 912 tileWidth = w; 913 final double scale = tileWidth / imgUnscaledWidth; 914 tileHeight = imgUnscaledHeight * scale; 915 } else if (h >= 0) { 916 // Here the height is specified and the width is AUTO 917 tileHeight = h; 918 final double scale = tileHeight / imgUnscaledHeight; 919 tileWidth = imgUnscaledWidth * scale; 920 } else { 921 // Both are auto. 922 tileWidth = imgUnscaledWidth; 923 tileHeight = imgUnscaledHeight; 924 } 925 926 // Now figure out where we are going to place the images within the region. 927 // For example, the developer can ask for 20px or 20%, and we need to first 928 // determine where to place the image. This starts by figuring out the pixel 929 // based value for the position. 930 final BackgroundPosition pos = image.getPosition(); 931 final double tileX, tileY; 932 933 if (pos.getHorizontalSide() == Side.LEFT) { 934 final double position = pos.getHorizontalPosition(); 935 if (pos.isHorizontalAsPercentage()) { 936 tileX = (position * width) - (position * tileWidth); 937 } else { 938 tileX = position; 939 } 940 } else { 941 if (pos.isHorizontalAsPercentage()) { 942 final double position = 1 - pos.getHorizontalPosition(); 943 tileX = (position * width) - (position * tileWidth); 944 } else { 945 tileX = width - tileWidth- pos.getHorizontalPosition(); 946 } 947 } 948 949 if (pos.getVerticalSide() == Side.TOP) { 950 final double position = pos.getVerticalPosition(); 951 if (pos.isVerticalAsPercentage()) { 952 tileY = (position * height) - (position * tileHeight); 953 } else { 954 tileY = position; 955 } 956 } else { 957 if (pos.isVerticalAsPercentage()) { 958 final double position = 1 - pos.getVerticalPosition(); 959 tileY = (position * height) - (position * tileHeight); 960 } else { 961 tileY = height - tileHeight - pos.getVerticalPosition(); 962 } 963 } 964 965 // Now that we have acquired or computed all the data, we'll let paintTiles 966 // do the actual rendering operation. 967 paintTiles(g, prismImage, image.getRepeatX(), image.getRepeatY(), 968 pos.getHorizontalSide(), pos.getVerticalSide(), 969 0, 0, width, height, // the region area to fill with the image 970 0, 0, imgWidth, imgHeight, // The entire image is used 971 (float) tileX, (float) tileY, (float) tileWidth, (float) tileHeight); 972 } 973 } 974 } 975 } 976 977 private void renderBackgroundRectangleFromCache( 978 Graphics g, RTTexture cached, Rectangle rect, int textureWidth, int textureHeight, 979 double topInset, double rightInset, double bottomInset, double leftInset, 980 int outsetsTop, int outsetsRight, int outsetsBottom, int outsetsLeft) { 981 982 // All cache operations are padded by (just shy of) half a pixel so 983 // that as we are translated by sub-pixel amounts we continue to sample 984 // all of the cached pixels out until they become transparent at (or 985 // 1-bit worth of non-zero alhpa from) the center of the border pixel 986 // around the cache. If there is an integer translation, then our 987 // padding should come up just shy of including new rows/columns of 988 // pixels in the rendering and thus have no impact on pixel fill rates. 989 final float pad = 0.5f - 1f/256f; 990 final float dstWidth = outsetsLeft + width + outsetsRight; 991 final float dstHeight = outsetsTop + height + outsetsBottom; 992 final boolean sameWidth = textureWidth == dstWidth; 993 final boolean sameHeight = textureHeight == dstHeight; 994 final float dstX1 = -outsetsLeft - pad; 995 final float dstY1 = -outsetsTop - pad; 996 final float dstX2 = width + outsetsRight + pad; 997 final float dstY2 = height + outsetsBottom + pad; 998 final float srcX1 = rect.x - pad; 999 final float srcY1 = rect.y - pad; 1000 final float srcX2 = rect.x + textureWidth + pad; 1001 final float srcY2 = rect.y + textureHeight + pad; 1002 1003 // If total destination width is < the source width, then we need to start 1004 // shrinking the left and right sides to accommodate. Likewise in the other dimension. 1005 double adjustedLeftInset = leftInset; 1006 double adjustedRightInset = rightInset; 1007 double adjustedTopInset = topInset; 1008 double adjustedBottomInset = bottomInset; 1009 if (leftInset + rightInset > width) { 1010 double fraction = width / (leftInset + rightInset); 1011 adjustedLeftInset *= fraction; 1012 adjustedRightInset *= fraction; 1013 } 1014 if (topInset + bottomInset > height) { 1015 double fraction = height / (topInset + bottomInset); 1016 adjustedTopInset *= fraction; 1017 adjustedBottomInset *= fraction; 1018 } 1019 1020 if (sameWidth && sameHeight) { 1021 g.drawTexture(cached, dstX1, dstY1, dstX2, dstY2, srcX1, srcY1, srcX2, srcY2); 1022 } else if (sameHeight) { 1023 // We do 3-patch rendering fixed height 1024 final float left = pad + (float) (adjustedLeftInset + outsetsLeft); 1025 final float right = pad + (float) (adjustedRightInset + outsetsRight); 1026 1027 final float dstLeftX = dstX1 + left; 1028 final float dstRightX = dstX2 - right; 1029 final float srcLeftX = srcX1 + left; 1030 final float srcRightX = srcX2 - right; 1031 1032 g.drawTexture3SliceH(cached, 1033 dstX1, dstY1, dstX2, dstY2, 1034 srcX1, srcY1, srcX2, srcY2, 1035 dstLeftX, dstRightX, srcLeftX, srcRightX); 1036 } else if (sameWidth) { 1037 // We do 3-patch rendering fixed width 1038 final float top = pad + (float) (adjustedTopInset + outsetsTop); 1039 final float bottom = pad + (float) (adjustedBottomInset + outsetsBottom); 1040 1041 final float dstTopY = dstY1 + top; 1042 final float dstBottomY = dstY2 - bottom; 1043 final float srcTopY = srcY1 + top; 1044 final float srcBottomY = srcY2 - bottom; 1045 1046 g.drawTexture3SliceV(cached, 1047 dstX1, dstY1, dstX2, dstY2, 1048 srcX1, srcY1, srcX2, srcY2, 1049 dstTopY, dstBottomY, srcTopY, srcBottomY); 1050 } else { 1051 // We do 9-patch rendering 1052 final float left = pad + (float) (adjustedLeftInset + outsetsLeft); 1053 final float top = pad + (float) (adjustedTopInset + outsetsTop); 1054 final float right = pad + (float) (adjustedRightInset + outsetsRight); 1055 final float bottom = pad + (float) (adjustedBottomInset + outsetsBottom); 1056 1057 final float dstLeftX = dstX1 + left; 1058 final float dstRightX = dstX2 - right; 1059 final float srcLeftX = srcX1 + left; 1060 final float srcRightX = srcX2 - right; 1061 final float dstTopY = dstY1 + top; 1062 final float dstBottomY = dstY2 - bottom; 1063 final float srcTopY = srcY1 + top; 1064 final float srcBottomY = srcY2 - bottom; 1065 1066 g.drawTexture9Slice(cached, 1067 dstX1, dstY1, dstX2, dstY2, 1068 srcX1, srcY1, srcX2, srcY2, 1069 dstLeftX, dstTopY, dstRightX, dstBottomY, 1070 srcLeftX, srcTopY, srcRightX, srcBottomY); 1071 } 1072 1073 if (PULSE_LOGGING_ENABLED) { 1074 PulseLogger.incrementCounter("Cached region background image used"); 1075 } 1076 } 1077 1078 private void renderBackgroundRectanglesDirectly(Graphics g, float width, float height) { 1079 final List<BackgroundFill> fills = background.getFills(); 1080 for (int i = 0, max = fills.size(); i < max; i++) { 1081 final BackgroundFill fill = fills.get(i); 1082 final Insets insets = fill.getInsets(); 1083 final float t = (float) insets.getTop(), 1084 l = (float) insets.getLeft(), 1085 b = (float) insets.getBottom(), 1086 r = (float) insets.getRight(); 1087 // w and h is the width and height of the area to be filled (width and height less insets) 1088 float w = width - l - r; 1089 float h = height - t - b; 1090 // Only setup and paint for those areas which have positive width and height. This means, if 1091 // the insets are such that the right edge is left of the left edge, then we have a negative 1092 // width and will not paint it. TODO we need to document this fact (RT-26924) 1093 if (w > 0 && h > 0) { 1094 // Could optimize this such that if paint is transparent then we go no further. 1095 final Paint paint = getPlatformPaint(fill.getFill()); 1096 g.setPaint(paint); 1097 final CornerRadii radii = getNormalizedFillRadii(i); 1098 // This is a workaround for RT-28435 so we use path rasterizer for small radius's We are 1099 // keeping old rendering. We do not apply workaround when using Caspian or Embedded 1100 if (radii.isUniform() && 1101 !(!PlatformImpl.isCaspian() && !(PlatformUtil.isEmbedded() || PlatformUtil.isIOS()) && radii.getTopLeftHorizontalRadius() > 0 && radii.getTopLeftHorizontalRadius() <= 4)) { 1102 // If the radii is uniform then we know every corner matches, so we can do some 1103 // faster rendering paths. 1104 float tlhr = (float) radii.getTopLeftHorizontalRadius(); 1105 float tlvr = (float) radii.getTopLeftVerticalRadius(); 1106 if (tlhr == 0 && tlvr == 0) { 1107 // The edges are square, so we can do a simple fill rect 1108 g.fillRect(l, t, w, h); 1109 } else { 1110 // The edges are rounded, so we need to compute the arc width and arc height 1111 // and fill a round rect 1112 float arcWidth = tlhr + tlhr; 1113 float arcHeight = tlvr + tlvr; 1114 // If the arc width and arc height are so large as to exceed the width / height of 1115 // the region, then we clamp to the width / height of the region (which will give 1116 // the look of a circle on that corner) 1117 if (arcWidth > w) arcWidth = w; 1118 if (arcHeight > h) arcHeight = h; 1119 g.fillRoundRect(l, t, w, h, arcWidth, arcHeight); 1120 } 1121 } else { 1122 if (PULSE_LOGGING_ENABLED) { 1123 PulseLogger.incrementCounter("NGRegion renderBackgrounds slow path"); 1124 PulseLogger.addMessage("Slow background path for " + getName()); 1125 } 1126 // The edges are not uniform, so we have to render each edge independently 1127 // TODO document the issue number which will give us a fast path for rendering 1128 // non-uniform corners, and that we want to implement that instead of createPath2 1129 // below in such cases. (RT-26979) 1130 g.fill(createPath(width, height, t, l, b, r, radii)); 1131 } 1132 } 1133 } 1134 } 1135 1136 private void renderBorderRectangle(Graphics g) { 1137 final List<BorderImage> images = border.getImages(); 1138 final List<BorderStroke> strokes = images.isEmpty() ? border.getStrokes() : Collections.emptyList(); 1139 for (int i = 0, max = strokes.size(); i < max; i++) { 1140 final BorderStroke stroke = strokes.get(i); 1141 final BorderWidths widths = stroke.getWidths(); 1142 final CornerRadii radii = getNormalizedStrokeRadii(i); 1143 final Insets insets = stroke.getInsets(); 1144 1145 final javafx.scene.paint.Paint topStroke = stroke.getTopStroke(); 1146 final javafx.scene.paint.Paint rightStroke = stroke.getRightStroke(); 1147 final javafx.scene.paint.Paint bottomStroke = stroke.getBottomStroke(); 1148 final javafx.scene.paint.Paint leftStroke = stroke.getLeftStroke(); 1149 1150 final float topInset = (float) insets.getTop(); 1151 final float rightInset = (float) insets.getRight(); 1152 final float bottomInset = (float) insets.getBottom(); 1153 final float leftInset = (float) insets.getLeft(); 1154 1155 final float topWidth = (float) (widths.isTopAsPercentage() ? height * widths.getTop() : widths.getTop()); 1156 final float rightWidth = (float) (widths.isRightAsPercentage() ? width * widths.getRight() : widths.getRight()); 1157 final float bottomWidth = (float) (widths.isBottomAsPercentage() ? height * widths.getBottom() : widths.getBottom()); 1158 final float leftWidth = (float) (widths.isLeftAsPercentage() ? width * widths.getLeft() : widths.getLeft()); 1159 1160 final BorderStrokeStyle topStyle = stroke.getTopStyle(); 1161 final BorderStrokeStyle rightStyle = stroke.getRightStyle(); 1162 final BorderStrokeStyle bottomStyle = stroke.getBottomStyle(); 1163 final BorderStrokeStyle leftStyle = stroke.getLeftStyle(); 1164 1165 final StrokeType topType = topStyle.getType(); 1166 final StrokeType rightType = rightStyle.getType(); 1167 final StrokeType bottomType = bottomStyle.getType(); 1168 final StrokeType leftType = leftStyle.getType(); 1169 1170 // The Prism Graphics logic can stroke lines only CENTERED and doesn't know what to do with 1171 // INSIDE or OUTSIDE strokes for lines. The only way to deal with those is 1172 // to compensate for them here. So we will adjust the bounds that we are going 1173 // to stroke to take into account the insets (obviously), and also where we 1174 // want the stroked line to appear (inside, or outside, or centered). 1175 final float t = topInset + 1176 (topType == StrokeType.OUTSIDE ? -topWidth / 2 : 1177 topType == StrokeType.INSIDE ? topWidth / 2 : 0); 1178 final float l = leftInset + 1179 (leftType == StrokeType.OUTSIDE ? -leftWidth / 2 : 1180 leftType == StrokeType.INSIDE ? leftWidth / 2 : 0); 1181 final float b = bottomInset + 1182 (bottomType == StrokeType.OUTSIDE ? -bottomWidth / 2 : 1183 bottomType == StrokeType.INSIDE ? bottomWidth / 2 : 0); 1184 final float r = rightInset + 1185 (rightType == StrokeType.OUTSIDE ? -rightWidth / 2 : 1186 rightType == StrokeType.INSIDE ? rightWidth / 2 : 0); 1187 1188 // If the radii are uniform, then reading any one value is sufficient to 1189 // know what the radius is for all values 1190 final float radius = (float) radii.getTopLeftHorizontalRadius(); 1191 if (stroke.isStrokeUniform()) { 1192 // If the stroke is uniform, then that means that the style, width, and stroke of 1193 // all four sides is the same. 1194 if (!(topStroke instanceof Color && ((Color)topStroke).getOpacity() == 0f) && topStyle != BorderStrokeStyle.NONE) { 1195 float w = width - l - r; 1196 float h = height - t - b; 1197 // The length of each side of the path we're going to stroke 1198 final double di = 2 * radii.getTopLeftHorizontalRadius(); 1199 final double circle = di*Math.PI; 1200 final double totalLineLength = 1201 circle + 1202 2 * (w - di) + 1203 2 * (h - di); 1204 1205 if (w >= 0 && h >= 0) { 1206 setBorderStyle(g, stroke, totalLineLength, true); 1207 if (radii.isUniform() && radius == 0) { 1208 // We're just drawing a squared stroke on all four sides of the same style 1209 // and width and color, so a simple drawRect call is all that is needed. 1210 g.drawRect(l, t, w, h); 1211 } else if (radii.isUniform()) { 1212 // The radii are uniform, but are not squared up, so we have to 1213 // draw a rounded rectangle. 1214 float ar = radius + radius; 1215 if (ar > w) ar = w; 1216 if (ar > h) ar = h; 1217 g.drawRoundRect(l, t, w, h, ar, ar); 1218 } else { 1219 // We do not have uniform radii, so we need to create a path that represents 1220 // the stroke and then draw that. 1221 g.draw(createPath(width, height, t, l, b, r, radii)); 1222 } 1223 } 1224 } 1225 } else if (radii.isUniform() && radius == 0) { 1226 1227 // We have different styles, or widths, or strokes on one or more sides, and 1228 // therefore we have to draw each side independently. However, the corner radii 1229 // are all 0, so we don't have to go to the trouble of constructing some complicated 1230 // path to represent the border, we just draw each line independently. 1231 // Note that in each of these checks, if the stroke is identity equal to the TRANSPARENT 1232 // Color or the style is identity equal to BorderStrokeStyle.NONE, then we skip that 1233 // side. It is possible however to have a Color as the stroke which is effectively 1234 // TRANSPARENT and a style that is effectively NONE, but we are not checking for those 1235 // cases and will in those cases be doing more work than necessary. 1236 // TODO make sure CSS uses TRANSPARENT and NONE when possible (RT-26943) 1237 if (!(topStroke instanceof Color && ((Color)topStroke).getOpacity() == 0f) && topStyle != BorderStrokeStyle.NONE) { 1238 g.setPaint(getPlatformPaint(topStroke)); 1239 if (BorderStrokeStyle.SOLID == topStyle) { 1240 g.fillRect(leftInset, topInset, width - leftInset - rightInset, topWidth); 1241 } else { 1242 g.setStroke(createStroke(topStyle, topWidth, width, true)); 1243 g.drawLine(l, t, width - r, t); 1244 } 1245 } 1246 1247 if (!(rightStroke instanceof Color && ((Color)rightStroke).getOpacity() == 0f) && rightStyle != BorderStrokeStyle.NONE) { 1248 g.setPaint(getPlatformPaint(rightStroke)); 1249 if (BorderStrokeStyle.SOLID == rightStyle) { 1250 g.fillRect(width - rightInset - rightWidth, topInset, 1251 rightWidth, height - topInset - bottomInset); 1252 } else { 1253 g.setStroke(createStroke(rightStyle, rightWidth, height, true)); 1254 g.drawLine(width - r, t, width - r, height - b); 1255 } 1256 } 1257 1258 if (!(bottomStroke instanceof Color && ((Color)bottomStroke).getOpacity() == 0f) && bottomStyle != BorderStrokeStyle.NONE) { 1259 g.setPaint(getPlatformPaint(bottomStroke)); 1260 if (BorderStrokeStyle.SOLID == bottomStyle) { 1261 g.fillRect(leftInset, height - bottomInset - bottomWidth, 1262 width - leftInset - rightInset, bottomWidth); 1263 } else { 1264 g.setStroke(createStroke(bottomStyle, bottomWidth, width, true)); 1265 g.drawLine(l, height - b, width - r, height - b); 1266 } 1267 } 1268 1269 if (!(leftStroke instanceof Color && ((Color)leftStroke).getOpacity() == 0f) && leftStyle != BorderStrokeStyle.NONE) { 1270 g.setPaint(getPlatformPaint(leftStroke)); 1271 if (BorderStrokeStyle.SOLID == leftStyle) { 1272 g.fillRect(leftInset, topInset, leftWidth, height - topInset - bottomInset); 1273 } else { 1274 g.setStroke(createStroke(leftStyle, leftWidth, height, true)); 1275 g.drawLine(l, t, l, height - b); 1276 } 1277 } 1278 } else { 1279 // In this case, we have different styles and/or strokes and/or widths on one or 1280 // more sides, and either the radii are not uniform, or they are uniform but greater 1281 // than 0. In this case we have to take a much slower rendering path by turning this 1282 // stroke into a path (or in the current implementation, an array of paths). 1283 Shape[] paths = createPaths(t, l, b, r, radii); 1284 if (topStyle != BorderStrokeStyle.NONE) { 1285 double rsum = radii.getTopLeftHorizontalRadius() + radii.getTopRightHorizontalRadius(); 1286 double topLineLength = width + rsum * (Math.PI / 4 - 1); 1287 g.setStroke(createStroke(topStyle, topWidth, topLineLength, true)); 1288 g.setPaint(getPlatformPaint(topStroke)); 1289 g.draw(paths[0]); 1290 } 1291 if (rightStyle != BorderStrokeStyle.NONE) { 1292 double rsum = radii.getTopRightVerticalRadius() + radii.getBottomRightVerticalRadius(); 1293 double rightLineLength = height + rsum * (Math.PI / 4 - 1); 1294 g.setStroke(createStroke(rightStyle, rightWidth, rightLineLength, true)); 1295 g.setPaint(getPlatformPaint(rightStroke)); 1296 g.draw(paths[1]); 1297 } 1298 if (bottomStyle != BorderStrokeStyle.NONE) { 1299 double rsum = radii.getBottomLeftHorizontalRadius() + radii.getBottomRightHorizontalRadius(); 1300 double bottomLineLength = width + rsum * (Math.PI / 4 - 1); 1301 g.setStroke(createStroke(bottomStyle, bottomWidth, bottomLineLength, true)); 1302 g.setPaint(getPlatformPaint(bottomStroke)); 1303 g.draw(paths[2]); 1304 } 1305 if (leftStyle != BorderStrokeStyle.NONE) { 1306 double rsum = radii.getTopLeftVerticalRadius() + radii.getBottomLeftVerticalRadius(); 1307 double leftLineLength = height + rsum * (Math.PI / 4 - 1); 1308 g.setStroke(createStroke(leftStyle, leftWidth, leftLineLength, true)); 1309 g.setPaint(getPlatformPaint(leftStroke)); 1310 g.draw(paths[3]); 1311 } 1312 } 1313 } 1314 1315 for (int i = 0, max = images.size(); i < max; i++) { 1316 final BorderImage ib = images.get(i); 1317 final Image prismImage = (Image) ib.getImage().impl_getPlatformImage(); 1318 if (prismImage == null) { 1319 // The prismImage might be null if the Image has not completed loading. 1320 // In that case, we simply must skip rendering of that layer this 1321 // time around. 1322 continue; 1323 } 1324 final int imgWidth = prismImage.getWidth(); 1325 final int imgHeight = prismImage.getHeight(); 1326 final float imgScale = prismImage.getPixelScale(); 1327 final BorderWidths widths = ib.getWidths(); 1328 final Insets insets = ib.getInsets(); 1329 final BorderWidths slices = ib.getSlices(); 1330 1331 // we will get gaps if we don't round to pixel boundaries 1332 final int topInset = (int) Math.round(insets.getTop()); 1333 final int rightInset = (int) Math.round(insets.getRight()); 1334 final int bottomInset = (int) Math.round(insets.getBottom()); 1335 final int leftInset = (int) Math.round(insets.getLeft()); 1336 1337 final int topWidth = widthSize(widths.isTopAsPercentage(), widths.getTop(), height); 1338 final int rightWidth = widthSize(widths.isRightAsPercentage(), widths.getRight(), width); 1339 final int bottomWidth = widthSize(widths.isBottomAsPercentage(), widths.getBottom(), height); 1340 final int leftWidth = widthSize(widths.isLeftAsPercentage(), widths.getLeft(), width); 1341 1342 final int topSlice = sliceSize(slices.isTopAsPercentage(), slices.getTop(), imgHeight, imgScale); 1343 final int rightSlice = sliceSize(slices.isRightAsPercentage(), slices.getRight(), imgWidth, imgScale); 1344 final int bottomSlice = sliceSize(slices.isBottomAsPercentage(), slices.getBottom(), imgHeight, imgScale); 1345 final int leftSlice = sliceSize(slices.isLeftAsPercentage(), slices.getLeft(), imgWidth, imgScale); 1346 1347 // handle case where region is too small to fit in borders 1348 if ((leftInset + leftWidth + rightInset + rightWidth) > width 1349 || (topInset + topWidth + bottomInset + bottomWidth) > height) { 1350 continue; 1351 } 1352 1353 // calculate some things we can share 1354 final int centerMinX = leftInset + leftWidth; 1355 final int centerMinY = topInset + topWidth; 1356 final int centerW = Math.round(width) - rightInset - rightWidth - centerMinX; 1357 final int centerH = Math.round(height) - bottomInset - bottomWidth - centerMinY; 1358 final int centerMaxX = centerW + centerMinX; 1359 final int centerMaxY = centerH + centerMinY; 1360 final int centerSliceWidth = imgWidth - leftSlice - rightSlice; 1361 final int centerSliceHeight = imgHeight - topSlice - bottomSlice; 1362 // paint top left corner 1363 paintTiles(g, prismImage, BorderRepeat.STRETCH, BorderRepeat.STRETCH, Side.LEFT, Side.TOP, 1364 leftInset, topInset, leftWidth, topWidth, // target bounds 1365 0, 0, leftSlice, topSlice, // src image bounds 1366 0, 0, leftWidth, topWidth); // tile bounds 1367 // paint top slice 1368 float tileWidth = (ib.getRepeatX() == BorderRepeat.STRETCH) ? 1369 centerW : (topSlice > 0 ? (centerSliceWidth * topWidth) / topSlice : 0); 1370 float tileHeight = topWidth; 1371 paintTiles( 1372 g, prismImage, ib.getRepeatX(), BorderRepeat.STRETCH, Side.LEFT, Side.TOP, 1373 centerMinX, topInset, centerW, topWidth, 1374 leftSlice, 0, centerSliceWidth, topSlice, 1375 (centerW - tileWidth) / 2, 0, tileWidth, tileHeight); 1376 // paint top right corner 1377 paintTiles(g, prismImage, BorderRepeat.STRETCH, BorderRepeat.STRETCH, Side.LEFT, Side.TOP, 1378 centerMaxX, topInset, rightWidth, topWidth, 1379 (imgWidth - rightSlice), 0, rightSlice, topSlice, 1380 0, 0, rightWidth, topWidth); 1381 // paint left slice 1382 tileWidth = leftWidth; 1383 tileHeight = (ib.getRepeatY() == BorderRepeat.STRETCH) ? 1384 centerH : (leftSlice > 0 ? (leftWidth * centerSliceHeight) / leftSlice : 0); 1385 paintTiles(g, prismImage, BorderRepeat.STRETCH, ib.getRepeatY(), Side.LEFT, Side.TOP, 1386 leftInset, centerMinY, leftWidth, centerH, 1387 0, topSlice, leftSlice, centerSliceHeight, 1388 0, (centerH - tileHeight) / 2, tileWidth, tileHeight); 1389 // paint right slice 1390 tileWidth = rightWidth; 1391 tileHeight = (ib.getRepeatY() == BorderRepeat.STRETCH) ? 1392 centerH : (rightSlice > 0 ? (rightWidth * centerSliceHeight) / rightSlice : 0); 1393 paintTiles(g, prismImage, BorderRepeat.STRETCH, ib.getRepeatY(), Side.LEFT, Side.TOP, 1394 centerMaxX, centerMinY, rightWidth, centerH, 1395 imgWidth - rightSlice, topSlice, rightSlice, centerSliceHeight, 1396 0, (centerH - tileHeight) / 2, tileWidth, tileHeight); 1397 // paint bottom left corner 1398 paintTiles(g, prismImage, BorderRepeat.STRETCH, BorderRepeat.STRETCH, Side.LEFT, Side.TOP, 1399 leftInset, centerMaxY, leftWidth, bottomWidth, 1400 0, imgHeight - bottomSlice, leftSlice, bottomSlice, 1401 0, 0, leftWidth, bottomWidth); 1402 // paint bottom slice 1403 tileWidth = (ib.getRepeatX() == BorderRepeat.STRETCH) ? 1404 centerW : (bottomSlice > 0 ? (centerSliceWidth * bottomWidth) / bottomSlice : 0); 1405 tileHeight = bottomWidth; 1406 paintTiles(g, prismImage, ib.getRepeatX(), BorderRepeat.STRETCH, Side.LEFT, Side.TOP, 1407 centerMinX, centerMaxY, centerW, bottomWidth, 1408 leftSlice, imgHeight - bottomSlice, centerSliceWidth, bottomSlice, 1409 (centerW - tileWidth) / 2, 0, tileWidth, tileHeight); 1410 // paint bottom right corner 1411 paintTiles(g, prismImage, BorderRepeat.STRETCH, BorderRepeat.STRETCH, Side.LEFT, Side.TOP, 1412 centerMaxX, centerMaxY, rightWidth, bottomWidth, 1413 imgWidth - rightSlice, imgHeight - bottomSlice, rightSlice, bottomSlice, 1414 0, 0, rightWidth, bottomWidth); 1415 // paint the center slice 1416 if (ib.isFilled()) { 1417 // handle no repeat as stretch 1418 final float imgW = (ib.getRepeatX() == BorderRepeat.STRETCH) ? centerW : centerSliceWidth; 1419 final float imgH = (ib.getRepeatY() == BorderRepeat.STRETCH) ? centerH : centerSliceHeight; 1420 paintTiles(g, prismImage, ib.getRepeatX(), ib.getRepeatY(), Side.LEFT, Side.TOP, 1421 centerMinX, centerMinY, centerW, centerH, 1422 leftSlice, topSlice, centerSliceWidth, centerSliceHeight, 1423 0, 0, imgW, imgH); 1424 } 1425 } 1426 } 1427 1428 /** 1429 * Visits each of the background fills and takes their radii into account to determine the insets. 1430 * The backgroundInsets variable is cleared whenever the fills change, or whenever the size of the 1431 * region has changed (because if the size of the region changed and a radius is percentage based 1432 * then we need to recompute the insets). 1433 */ 1434 private void updateBackgroundInsets() { 1435 float top=0, right=0, bottom=0, left=0; 1436 final List<BackgroundFill> fills = background.getFills(); 1437 for (int i=0, max=fills.size(); i<max; i++) { 1438 // We need to now inspect the paint to determine whether we can use a cache for this background. 1439 // If a shape is being used, we don't care about gradients (we cache 'em both), but for a rectangle 1440 // fill we omit these (so we can do 3-patch scaling). An ImagePattern is deadly to either 1441 // (well, only deadly to a shape if it turns out to be a writable image). 1442 final BackgroundFill fill = fills.get(i); 1443 final Insets insets = fill.getInsets(); 1444 final CornerRadii radii = getNormalizedFillRadii(i); 1445 top = (float) Math.max(top, insets.getTop() + Math.max(radii.getTopLeftVerticalRadius(), radii.getTopRightVerticalRadius())); 1446 right = (float) Math.max(right, insets.getRight() + Math.max(radii.getTopRightHorizontalRadius(), radii.getBottomRightHorizontalRadius())); 1447 bottom = (float) Math.max(bottom, insets.getBottom() + Math.max(radii.getBottomRightVerticalRadius(), radii.getBottomLeftVerticalRadius())); 1448 left = (float) Math.max(left, insets.getLeft() + Math.max(radii.getTopLeftHorizontalRadius(), radii.getBottomLeftHorizontalRadius())); 1449 } 1450 backgroundInsets = new Insets(roundUp(top), roundUp(right), roundUp(bottom), roundUp(left)); 1451 } 1452 1453 private int widthSize(boolean isPercent, double sliceSize, float objSize) { 1454 //Not strictly correct. See RT-34051 1455 return (int) Math.round(isPercent ? sliceSize * objSize : sliceSize); 1456 } 1457 1458 private int sliceSize(boolean isPercent, double sliceSize, float objSize, float scale) { 1459 if (isPercent) sliceSize *= objSize; 1460 if (sliceSize > objSize) sliceSize = objSize; 1461 return (int) Math.round(sliceSize * scale); 1462 } 1463 1464 private int roundUp(double d) { 1465 return (d - (int)d) == 0 ? (int) d : (int) (d + 1); 1466 } 1467 1468 1469 /** 1470 * Creates a Prism BasicStroke based on the stroke style, width, and line length. 1471 * 1472 * @param sb The BorderStrokeStyle 1473 * @param strokeWidth The width of the stroke we're going to draw 1474 * @param lineLength The total linear length of this stroke. This is needed for 1475 * handling "dashed" and "dotted" cases, otherwise, it is ignored. 1476 * @param forceCentered When this is set to true, the stroke is always centered. 1477 * The "outer/inner" stroking has to be done by moving the line 1478 * @return A prism BasicStroke 1479 */ 1480 private BasicStroke createStroke(BorderStrokeStyle sb, 1481 double strokeWidth, 1482 double lineLength, 1483 boolean forceCentered) { 1484 int cap; 1485 if (sb.getLineCap() == StrokeLineCap.BUTT) { 1486 cap = BasicStroke.CAP_BUTT; 1487 } else if (sb.getLineCap() == StrokeLineCap.SQUARE) { 1488 cap = BasicStroke.CAP_SQUARE; 1489 } else { 1490 cap = BasicStroke.CAP_ROUND; 1491 } 1492 1493 int join; 1494 if (sb.getLineJoin() == StrokeLineJoin.BEVEL) { 1495 join = BasicStroke.JOIN_BEVEL; 1496 } else if (sb.getLineJoin() == StrokeLineJoin.MITER) { 1497 join = BasicStroke.JOIN_MITER; 1498 } else { 1499 join = BasicStroke.JOIN_ROUND; 1500 } 1501 1502 int type; 1503 if (forceCentered) { 1504 type = BasicStroke.TYPE_CENTERED; 1505 } else if (scaleShape) { 1506 // Note: this is just a workaround that allows us to avoid shape bounds computation with the given stroke. 1507 // By using inner stroke, we know the shape bounds and the shape will be scaled correctly, but the size of 1508 // the stroke after the scale will be slightly different, but this should be visible only with big stroke widths 1509 // See https://javafx-jira.kenai.com/browse/RT-38384 1510 type = BasicStroke.TYPE_INNER; 1511 } else { 1512 switch (sb.getType()) { 1513 case INSIDE: 1514 type = BasicStroke.TYPE_INNER; 1515 break; 1516 case OUTSIDE: 1517 type = BasicStroke.TYPE_OUTER; 1518 break; 1519 case CENTERED: 1520 default: 1521 type = BasicStroke.TYPE_CENTERED; 1522 break; 1523 } 1524 } 1525 1526 BasicStroke bs; 1527 if (sb == BorderStrokeStyle.NONE) { 1528 throw new AssertionError("Should never have been asked to draw a border with NONE"); 1529 } else if (strokeWidth <= 0) { 1530 // The stroke essentially disappears in this case, but some of the 1531 // dashing calculations below can produce degenerate dash arrays 1532 // that are problematic when the strokeWidth is 0. 1533 1534 // Ideally the calling code would not even be trying to perform a 1535 // stroke under these conditions, but there are so many unchecked 1536 // calls to createStroke() in the code that pass the result directly 1537 // to a Graphics and then use it, that we need to return something 1538 // valid, even if it represents a NOP. 1539 1540 bs = new BasicStroke((float) strokeWidth, cap, join, 1541 (float) sb.getMiterLimit()); 1542 } else if (sb.getDashArray().size() > 0) { 1543 List<Double> dashArray = sb.getDashArray(); 1544 double[] array; 1545 float dashOffset; 1546 if (dashArray == BorderStrokeStyle.DOTTED.getDashArray()) { 1547 // NOTE: IF line length is > 0, then we are going to do some math to try to make the resulting 1548 // dots look pleasing. It is set to -1 if we are stroking a random path (vs. a rounded rect), in 1549 // which case we are going to just scale the dotting pattern based on the stroke width, but we won't 1550 // try to adjust the phase to make it look better. 1551 if (lineLength > 0) { 1552 // For DOTTED we want the dash array to be 0, val, where the "val" is as close to strokewidth*2 as 1553 // possible, but we want the spacing to be such that we get an even spacing between all dots around 1554 // the edge. 1555 double remainder = lineLength % (strokeWidth * 2); 1556 double numSpaces = lineLength / (strokeWidth * 2); 1557 double spaceWidth = (strokeWidth * 2) + (remainder / numSpaces); 1558 array = new double[] {0, spaceWidth}; 1559 dashOffset = 0; 1560 } else { 1561 array = new double[] {0, strokeWidth * 2}; 1562 dashOffset = 0; 1563 } 1564 } else if (dashArray == BorderStrokeStyle.DASHED.getDashArray()) { 1565 // NOTE: IF line length is > 0, then we are going to do some math to try to make the resulting 1566 // dash look pleasing. It is set to -1 if we are stroking a random path (vs. a rounded rect), in 1567 // which case we are going to just scale the dashing pattern based on the stroke width, but we won't 1568 // try to adjust the phase to make it look better. 1569 if (lineLength > 0) { 1570 // For DASHED we want the dash array to be 2*strokewidth, val where "val" is as close to 1571 // 1.4*strokewidth as possible, but we want the spacing to be such that we get an even spacing between 1572 // all dashes around the edge. Maybe we can start with the dash phase at half the dash length. 1573 final double dashLength = strokeWidth * 2; 1574 double gapLength = strokeWidth * 1.4; 1575 final double segmentLength = dashLength + gapLength; 1576 final double divided = lineLength / segmentLength; 1577 final double numSegments = (int) divided; 1578 if (numSegments > 0) { 1579 final double dashCumulative = numSegments * dashLength; 1580 gapLength = (lineLength - dashCumulative) / numSegments; 1581 } 1582 array = new double[] {dashLength, gapLength}; 1583 dashOffset = (float) (dashLength*.6); 1584 } else { 1585 array = new double[] {2 * strokeWidth, 1.4 * strokeWidth}; 1586 dashOffset = 0; 1587 } 1588 } else { 1589 // If we are not DASHED or DOTTED or we're stroking a path and not a basic rounded rectangle 1590 // so we just take what we've been given. 1591 array = new double[dashArray.size()]; 1592 for (int i=0; i<array.length; i++) { 1593 array[i] = dashArray.get(i); 1594 } 1595 dashOffset = (float) sb.getDashOffset(); 1596 } 1597 1598 bs = new BasicStroke(type, (float) strokeWidth, cap, join, 1599 (float) sb.getMiterLimit(), 1600 array, dashOffset); 1601 } else { 1602 bs = new BasicStroke(type, (float) strokeWidth, cap, join, 1603 (float) sb.getMiterLimit()); 1604 } 1605 1606 return bs; 1607 } 1608 1609 private void setBorderStyle(Graphics g, BorderStroke sb, double length, boolean forceCentered) { 1610 // Any one of, or all of, the sides could be 'none'. 1611 // Take the first side that isn't. 1612 final BorderWidths widths = sb.getWidths(); 1613 BorderStrokeStyle bs = sb.getTopStyle(); 1614 double sbWidth = widths.isTopAsPercentage() ? height * widths.getTop() : widths.getTop(); 1615 Paint sbFill = getPlatformPaint(sb.getTopStroke()); 1616 if (bs == null) { 1617 bs = sb.getLeftStyle(); 1618 sbWidth = widths.isLeftAsPercentage() ? width * widths.getLeft() : widths.getLeft(); 1619 sbFill = getPlatformPaint(sb.getLeftStroke()); 1620 if (bs == null) { 1621 bs = sb.getBottomStyle(); 1622 sbWidth = widths.isBottomAsPercentage() ? height * widths.getBottom() : widths.getBottom(); 1623 sbFill = getPlatformPaint(sb.getBottomStroke()); 1624 if (bs == null) { 1625 bs = sb.getRightStyle(); 1626 sbWidth = widths.isRightAsPercentage() ? width * widths.getRight() : widths.getRight(); 1627 sbFill = getPlatformPaint(sb.getRightStroke()); 1628 } 1629 } 1630 } 1631 if (bs == null || bs == BorderStrokeStyle.NONE) { 1632 return; 1633 } 1634 1635 g.setStroke(createStroke(bs, sbWidth, length, forceCentered)); 1636 g.setPaint(sbFill); 1637 } 1638 1639 /** 1640 * Inserts geometry into the specified Path2D object for the specified 1641 * corner of a general rounded rectangle. 1642 * 1643 * The corner drawn is specified by the quadrant parameter, whose least 1644 * significant 2 bits specify the following corners and the associated 1645 * start, corner, and end points (which are always drawn clockwise): 1646 * 1647 * 0 - Top Left: X + 0 , Y + VR, X, Y, X + HR, Y + 0 1648 * 1 - Top Right: X - HR, Y + 0 , X, Y, X + 0 , Y + VR 1649 * 2 - Bottom Right: X + 0 , Y - VR, X, Y, X - HR, Y + 0 1650 * 3 - Bottom Left: X + HR, Y + 0 , X, Y, X + 0 , Y - VR 1651 * 1652 * The associated horizontal and vertical radii are fetched from the 1653 * indicated CornerRadii object which is assumed to be absolute (not 1654 * percentage based) and already scaled so that no pair of radii are 1655 * larger than the indicated width/height of the rounded rectangle being 1656 * expressed. 1657 * 1658 * The tstart and tend parameters specify what portion of the rounded 1659 * corner should be drawn with 0f => 1f being the entire rounded corner. 1660 * 1661 * The newPath parameter indicates whether the path should reach the 1662 * starting point with a moveTo() command or a lineTo() segment. 1663 * 1664 * @param path 1665 * @param radii 1666 * @param x 1667 * @param y 1668 * @param quadrant 1669 * @param tstart 1670 * @param tend 1671 * @param newPath 1672 */ 1673 private void doCorner(Path2D path, CornerRadii radii, 1674 float x, float y, int quadrant, 1675 float tstart, float tend, boolean newPath) 1676 { 1677 float dx0, dy0, dx1, dy1; 1678 float hr, vr; 1679 switch (quadrant & 0x3) { 1680 case 0: 1681 hr = (float) radii.getTopLeftHorizontalRadius(); 1682 vr = (float) radii.getTopLeftVerticalRadius(); 1683 // 0 - Top Left: X + 0 , Y + VR, X, Y, X + HR, Y + 0 1684 dx0 = 0f; dy0 = vr; dx1 = hr; dy1 = 0f; 1685 break; 1686 case 1: 1687 hr = (float) radii.getTopRightHorizontalRadius(); 1688 vr = (float) radii.getTopRightVerticalRadius(); 1689 // 1 - Top Right: X - HR, Y + 0 , X, Y, X + 0 , Y + VR 1690 dx0 = -hr; dy0 = 0f; dx1 = 0f; dy1 = vr; 1691 break; 1692 case 2: 1693 hr = (float) radii.getBottomRightHorizontalRadius(); 1694 vr = (float) radii.getBottomRightVerticalRadius(); 1695 // 2 - Bottom Right: X + 0 , Y - VR, X, Y, X - HR, Y + 0 1696 dx0 = 0f; dy0 = -vr; dx1 = -hr; dy1 = 0f; 1697 break; 1698 case 3: 1699 hr = (float) radii.getBottomLeftHorizontalRadius(); 1700 vr = (float) radii.getBottomLeftVerticalRadius(); 1701 // 3 - Bottom Left: X + HR, Y + 0 , X, Y, X + 0 , Y - VR 1702 dx0 = hr; dy0 = 0f; dx1 = 0f; dy1 = -vr; 1703 break; 1704 default: return; // Can never happen 1705 } 1706 if (hr > 0 && vr > 0) { 1707 path.appendOvalQuadrant(x + dx0, y + dy0, x, y, x + dx1, y + dy1, tstart, tend, 1708 (newPath) 1709 ? Path2D.CornerPrefix.MOVE_THEN_CORNER 1710 : Path2D.CornerPrefix.LINE_THEN_CORNER); 1711 } else if (newPath) { 1712 path.moveTo(x, y); 1713 } else { 1714 path.lineTo(x, y); 1715 } 1716 } 1717 1718 /** 1719 * Creates a rounded rectangle path with our width and height, different corner radii, 1720 * offset with given offsets 1721 */ 1722 private Path2D createPath(float width, float height, float t, float l, float bo, float ro, CornerRadii radii) { 1723 float r = width - ro; 1724 float b = height - bo; 1725 Path2D path = new Path2D(); 1726 doCorner(path, radii, l, t, 0, 0f, 1f, true); 1727 doCorner(path, radii, r, t, 1, 0f, 1f, false); 1728 doCorner(path, radii, r, b, 2, 0f, 1f, false); 1729 doCorner(path, radii, l, b, 3, 0f, 1f, false); 1730 path.closePath(); 1731 return path; 1732 } 1733 1734 private Path2D makeRoundedEdge(CornerRadii radii, 1735 float x0, float y0, float x1, float y1, 1736 int quadrant) 1737 { 1738 Path2D path = new Path2D(); 1739 doCorner(path, radii, x0, y0, quadrant, 0.5f, 1.0f, true); 1740 doCorner(path, radii, x1, y1, quadrant+1, 0.0f, 0.5f, false); 1741 return path; 1742 } 1743 1744 /** 1745 * Creates a rounded rectangle path with our width and height, different corner radii, offset with given offsets. 1746 * Each side as a separate path. The sides are returned in the CSS standard 1747 * order of top, right, bottom, left. 1748 */ 1749 private Path2D[] createPaths(float t, float l, float bo, float ro, CornerRadii radii) 1750 { 1751 float r = width - ro; 1752 float b = height - bo; 1753 return new Path2D[] { 1754 makeRoundedEdge(radii, l, t, r, t, 0), // top 1755 makeRoundedEdge(radii, r, t, r, b, 1), // right 1756 makeRoundedEdge(radii, r, b, l, b, 2), // bottom 1757 makeRoundedEdge(radii, l, b, l, t, 3), // left 1758 }; 1759 } 1760 1761 /** 1762 * Create a bigger or smaller version of shape. If not scaleShape then the shape is just centered rather 1763 * than resized. Proportions are not maintained when resizing. This is necessary so as to ensure 1764 * that the fill never looks scaled. For example, a tile-imaged based background will look stretched 1765 * if we were to render a scaled shape. Instead, we produce a new shape based on the scaled size and 1766 * then fill that shape without additional transforms. 1767 */ 1768 private Shape resizeShape(float topOffset, float rightOffset, float bottomOffset, float leftOffset) { 1769 // The bounds of the shape, before any centering / scaling takes place 1770 final RectBounds bounds = shape.getBounds(); 1771 if (scaleShape) { 1772 // First we need to modify the transform to scale the shape so that it will fit 1773 // within the insets. 1774 SCRATCH_AFFINE.setToIdentity(); 1775 SCRATCH_AFFINE.translate(leftOffset, topOffset); 1776 // width & height are the width and height of the region. w & h are the width and height 1777 // of the box within which the new shape must fit. 1778 final float w = width - leftOffset - rightOffset; 1779 final float h = height - topOffset - bottomOffset; 1780 SCRATCH_AFFINE.scale(w / bounds.getWidth(), h / bounds.getHeight()); 1781 // If we also need to center it, we need to adjust the transform so as to place 1782 // the shape in the center of the bounds 1783 if (centerShape) { 1784 SCRATCH_AFFINE.translate(-bounds.getMinX(), -bounds.getMinY()); 1785 } 1786 return SCRATCH_AFFINE.createTransformedShape(shape); 1787 } else if (centerShape) { 1788 // We are only centering. In this case, what we want is for the 1789 // original shape to be centered. If there are offsets (insets) 1790 // then we must pre-scale about the center to account for it. 1791 final float boundsWidth = bounds.getWidth(); 1792 final float boundsHeight = bounds.getHeight(); 1793 float newW = boundsWidth - leftOffset - rightOffset; 1794 float newH = boundsHeight - topOffset - bottomOffset; 1795 SCRATCH_AFFINE.setToIdentity(); 1796 SCRATCH_AFFINE.translate(leftOffset + (width - boundsWidth)/2 - bounds.getMinX(), 1797 topOffset + (height - boundsHeight)/2 - bounds.getMinY()); 1798 if (newH != boundsHeight || newW != boundsWidth) { 1799 SCRATCH_AFFINE.translate(bounds.getMinX(), bounds.getMinY()); 1800 SCRATCH_AFFINE.scale(newW / boundsWidth, newH / boundsHeight); 1801 SCRATCH_AFFINE.translate(-bounds.getMinX(), -bounds.getMinY()); 1802 } 1803 return SCRATCH_AFFINE.createTransformedShape(shape); 1804 } else if (topOffset != 0 || rightOffset != 0 || bottomOffset != 0 || leftOffset != 0) { 1805 // We are neither centering nor scaling, but we still have to resize the 1806 // shape because we have to fit within the bounds defined by the offsets 1807 float newW = bounds.getWidth() - leftOffset - rightOffset; 1808 float newH = bounds.getHeight() - topOffset - bottomOffset; 1809 SCRATCH_AFFINE.setToIdentity(); 1810 SCRATCH_AFFINE.translate(leftOffset, topOffset); 1811 SCRATCH_AFFINE.translate(bounds.getMinX(), bounds.getMinY()); 1812 SCRATCH_AFFINE.scale(newW / bounds.getWidth(), newH / bounds.getHeight()); 1813 SCRATCH_AFFINE.translate(-bounds.getMinX(), -bounds.getMinY()); 1814 return SCRATCH_AFFINE.createTransformedShape(shape); 1815 } else { 1816 // Nothing has changed, so we can simply return! 1817 return shape; 1818 } 1819 } 1820 1821 private void paintTiles(Graphics g, Image img, BorderRepeat repeatX, BorderRepeat repeatY, Side horizontalSide, Side verticalSide, 1822 final float regionX, final float regionY, final float regionWidth, final float regionHeight, 1823 final int srcX, final int srcY, final int srcW, final int srcH, 1824 float tileX, float tileY, float tileWidth, float tileHeight) 1825 { 1826 BackgroundRepeat rx = null; 1827 BackgroundRepeat ry = null; 1828 1829 switch (repeatX) { 1830 case REPEAT: rx = BackgroundRepeat.REPEAT; break; 1831 case STRETCH: rx = BackgroundRepeat.NO_REPEAT; break; 1832 case ROUND: rx = BackgroundRepeat.ROUND; break; 1833 case SPACE: rx = BackgroundRepeat.SPACE; break; 1834 } 1835 1836 switch (repeatY) { 1837 case REPEAT: ry = BackgroundRepeat.REPEAT; break; 1838 case STRETCH: ry = BackgroundRepeat.NO_REPEAT; break; 1839 case ROUND: ry = BackgroundRepeat.ROUND; break; 1840 case SPACE: ry = BackgroundRepeat.SPACE; break; 1841 } 1842 1843 paintTiles(g, img, rx, ry, horizontalSide, verticalSide, regionX, regionY, regionWidth, regionHeight, 1844 srcX, srcY, srcW, srcH, tileX, tileY, tileWidth, tileHeight); 1845 } 1846 1847 /** 1848 * Paints a subsection (srcX,srcY,srcW,srcH) of an image tiled or stretched to fill the destination area 1849 * (regionWidth,regionHeight). It is assumed we are pre-transformed to the correct origin, top left or destination area. When 1850 * tiling the first tile is positioned within the rectangle (tileX,tileY,tileW,tileH). 1851 * 1852 * Drawing two images next to each other on a non-pixel boundary can not be done simply so we use integers here. This 1853 * assumption may be wrong when drawing though a scale transform. 1854 * 1855 * @param g The graphics context to draw image into 1856 * @param img The image to draw 1857 * @param repeatX The horizontal repeat style for filling the area with the src image 1858 * @param repeatY The vertical repeat style for filling the area with the src image 1859 * @param horizontalSide The left or right 1860 * @param verticalSide The top or bottom 1861 * @param regionX The top left corner X of the area of the graphics context to fill with our img 1862 * @param regionY The top left corner Y of the area of the graphics context to fill with our img 1863 * @param regionWidth The width of the area of the graphics context to fill with our img 1864 * @param regionHeight The height of the area of the graphics context to fill with our img 1865 * @param srcX The top left corner X of the area of the image to paint with 1866 * @param srcY The top left corner Y of the area of the image to paint with 1867 * @param srcW The width of the area of the image to paint with, -1 to use the original image width 1868 * @param srcH The height of the area of the image to paint with, -1 to use the original image height 1869 * @param tileX The top left corner X of the area of the first tile within the destination rectangle. In some 1870 * cases we begin by drawing the center tile, and working to the left & right (for example), so 1871 * this value is not always the same as regionX. 1872 * @param tileY The top left corner Y of the area of the first tile within the destination rectangle 1873 * @param tileWidth The width of the area of the first tile within the destination rectangle, if <= 0, then the use intrinsic value 1874 * @param tileHeight The height of the area of the first tile within the destination rectangle, if <= 0, then the use intrinsic value 1875 */ 1876 private void paintTiles(Graphics g, Image img, BackgroundRepeat repeatX, BackgroundRepeat repeatY, Side horizontalSide, Side verticalSide, 1877 final float regionX, final float regionY, final float regionWidth, final float regionHeight, 1878 final int srcX, final int srcY, final int srcW, final int srcH, 1879 float tileX, float tileY, float tileWidth, float tileHeight) 1880 { 1881 // If the destination width/height is 0 or the src width / height is 0 then we have 1882 // nothing to draw, so we can just bail. 1883 if (regionWidth <= 0 || regionHeight <= 0 || srcW <= 0 || srcH <= 0) return; 1884 1885 // At this point we should have real values for the image source coordinates 1886 assert srcX >= 0 && srcY >= 0 && srcW > 0 && srcH > 0; 1887 1888 // If we are repeating in both the x & y directions, then we can take a fast path and just 1889 // use the ImagePattern directly instead of having to issue a large number of drawTexture calls. 1890 // This is the generally common case where we are tiling the background in both dimensions. 1891 // Note that this only works if the anchor point is the top-left, otherwise the ImagePattern would 1892 // not give the correct expected results. 1893 if (tileX == 0 && tileY == 0 && repeatX == BackgroundRepeat.REPEAT && repeatY == BackgroundRepeat.REPEAT) { 1894 if (srcX != 0 || srcY != 0 || srcW != img.getWidth() || srcH != img.getHeight()) { 1895 img = img.createSubImage(srcX, srcY, srcW, srcH); 1896 } 1897 g.setPaint(new ImagePattern(img, 0, 0, tileWidth, tileHeight, false, false)); 1898 g.fillRect(regionX, regionY, regionWidth, regionHeight); 1899 } else { 1900 // If SPACE repeat mode is being used, then we need to take special action if there is not enough 1901 // space to have more than one tile. Basically, it needs to act as NO_REPEAT in that case (see 1902 // section 3.4 of the spec for details under rules for SPACE). 1903 if (repeatX == BackgroundRepeat.SPACE && (regionWidth < (tileWidth * 2))) { 1904 repeatX = BackgroundRepeat.NO_REPEAT; 1905 } 1906 1907 if (repeatY == BackgroundRepeat.SPACE && (regionHeight < (tileHeight * 2))) { 1908 repeatY = BackgroundRepeat.NO_REPEAT; 1909 } 1910 1911 // The following variables are computed and used in order to lay out the tiles in the x and y directions. 1912 // "count" is used to keep track of the number of tiles to lay down in the x and y directions. 1913 final int countX, countY; 1914 // The amount to increment the dstX and dstY by during the rendering loop. This may be positive or 1915 //negative and will include any space between tiles. 1916 final float xIncrement, yIncrement; 1917 1918 // Based on the repeat mode, populate the above variables 1919 if (repeatX == BackgroundRepeat.REPEAT) { 1920 // In some cases we have a large positive offset but are in repeat mode. What we need 1921 // to do is tile, but we want to do so in such a way that we are "anchored" to the center, 1922 // or right, or whatnot. That is what offsetX will be used for. 1923 float offsetX = 0; 1924 if (tileX != 0) { 1925 float mod = tileX % tileWidth; 1926 tileX = mod == 0 ? 0 : tileX < 0 ? mod : mod - tileWidth; 1927 offsetX = tileX; 1928 } 1929 countX = (int) Math.max(1, Math.ceil((regionWidth - offsetX) / tileWidth)); 1930 xIncrement = horizontalSide == Side.RIGHT ? -tileWidth : tileWidth; 1931 } else if (repeatX == BackgroundRepeat.SPACE) { 1932 tileX = 0; // Space will always start from the top left 1933 countX = (int) (regionWidth / tileWidth); 1934 float remainder = (regionWidth % tileWidth); 1935 xIncrement = tileWidth + (remainder / (countX - 1)); 1936 } else if (repeatX == BackgroundRepeat.ROUND) { 1937 tileX = 0; // Round will always start from the top left 1938 countX = (int) (regionWidth / tileWidth); 1939 tileWidth = regionWidth / (int)(regionWidth / tileWidth); 1940 xIncrement = tileWidth; 1941 } else { // no repeat 1942 countX = 1; 1943 xIncrement = horizontalSide == Side.RIGHT ? -tileWidth : tileWidth; 1944 } 1945 1946 if (repeatY == BackgroundRepeat.REPEAT) { 1947 float offsetY = 0; 1948 if (tileY != 0) { 1949 float mod = tileY % tileHeight; 1950 tileY = mod == 0 ? 0 : tileY < 0 ? mod : mod - tileHeight; 1951 offsetY = tileY; 1952 } 1953 countY = (int) Math.max(1, Math.ceil((regionHeight - offsetY) / tileHeight)); 1954 yIncrement = verticalSide == Side.BOTTOM ? -tileHeight : tileHeight; 1955 } else if (repeatY == BackgroundRepeat.SPACE) { 1956 tileY = 0; // Space will always start from the top left 1957 countY = (int) (regionHeight / tileHeight); 1958 float remainder = (regionHeight % tileHeight); 1959 yIncrement = tileHeight + (remainder / (countY - 1)); 1960 } else if (repeatY == BackgroundRepeat.ROUND) { 1961 tileY = 0; // Round will always start from the top left 1962 countY = (int) (regionHeight / tileHeight); 1963 tileHeight = regionHeight / (int)(regionHeight / tileHeight); 1964 yIncrement = tileHeight; 1965 } else { // no repeat 1966 countY = 1; 1967 yIncrement = verticalSide == Side.BOTTOM ? -tileHeight : tileHeight; 1968 } 1969 1970 // paint loop 1971 final Texture texture = 1972 g.getResourceFactory().getCachedTexture(img, Texture.WrapMode.CLAMP_TO_EDGE); 1973 final int srcX2 = srcX + srcW; 1974 final int srcY2 = srcY + srcH; 1975 final float regionX2 = regionX + regionWidth; 1976 final float regionY2 = regionY + regionHeight; 1977 1978 float dstY = regionY + tileY; 1979 for (int y = 0; y < countY; y++) { 1980 float dstY2 = dstY + tileHeight; 1981 float dstX = regionX + tileX; 1982 for (int x = 0; x < countX; x++) { 1983 float dstX2 = dstX + tileWidth; 1984 // We don't want to end up rendering if we find that the destination rect is completely 1985 // off of the region rendering area 1986 boolean skipRender = false; 1987 float dx1 = dstX < regionX ? regionX : dstX; 1988 float dy1 = dstY < regionY ? regionY : dstY; 1989 if (dx1 > regionX2 || dy1 > regionY2) skipRender = true; 1990 1991 float dx2 = dstX2 > regionX2 ? regionX2 : dstX2; 1992 float dy2 = dstY2 > regionY2 ? regionY2 : dstY2; 1993 if (dx2 < regionX || dy2 < regionY) skipRender = true; 1994 1995 if (!skipRender) { 1996 // We know that dstX, dstY, dstX2, dstY2 overlap the region drawing area. Now we need 1997 // to compute the source rectangle, and then draw. 1998 float sx1 = dstX < regionX ? srcX + srcW * (-tileX / tileWidth) : srcX; 1999 float sy1 = dstY < regionY ? srcY + srcH * (-tileY / tileHeight) : srcY; 2000 float sx2 = dstX2 > regionX2 ? srcX2 - srcW * ((dstX2 - regionX2) / tileWidth) : srcX2; 2001 float sy2 = dstY2 > regionY2 ? srcY2 - srcH * ((dstY2 - regionY2) / tileHeight) : srcY2; 2002 // System.out.println("g.drawTexture(texture, " + dx1 + ", " + dy1 + ", " + dx2 + ", " + dy2 + ", " + sx1 + ", " + sy1 + ", " + sx2 + ", " + sy2 + ")"); 2003 g.drawTexture(texture, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2); 2004 } 2005 dstX += xIncrement; 2006 } 2007 dstY += yIncrement; 2008 } 2009 texture.unlock(); 2010 } 2011 } 2012 2013 final Border getBorder() { 2014 return border; 2015 } 2016 2017 final Background getBackground() { 2018 return background; 2019 } 2020 2021 final float getWidth() { 2022 return width; 2023 } 2024 2025 final float getHeight() { 2026 return height; 2027 } 2028 2029 }