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