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