1 /*
   2  * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.layout;
  27 
  28 import javafx.beans.NamedArg;
  29 import javafx.css.CssMetaData;
  30 import javafx.css.Styleable;
  31 import javafx.geometry.Insets;
  32 import javafx.scene.Node;
  33 import javafx.scene.image.Image;
  34 import javafx.scene.paint.Color;
  35 import javafx.scene.paint.Paint;
  36 import java.util.Arrays;
  37 import java.util.Collections;
  38 import java.util.List;
  39 import com.sun.javafx.UnmodifiableArrayList;
  40 import com.sun.javafx.css.SubCssMetaData;
  41 import com.sun.javafx.css.converters.InsetsConverter;
  42 import com.sun.javafx.css.converters.PaintConverter;
  43 import com.sun.javafx.css.converters.URLConverter;
  44 import com.sun.javafx.scene.layout.region.LayeredBackgroundPositionConverter;
  45 import com.sun.javafx.scene.layout.region.LayeredBackgroundSizeConverter;
  46 import com.sun.javafx.scene.layout.region.CornerRadiiConverter;
  47 import com.sun.javafx.scene.layout.region.RepeatStruct;
  48 import com.sun.javafx.scene.layout.region.RepeatStructConverter;
  49 import com.sun.javafx.tk.Toolkit;
  50 
  51 /**
  52  * The Background of a {@link Region}. A Background is an immutable object which
  53  * encapsulates the entire set of data required to render the background
  54  * of a Region. Because this class is immutable, you can freely reuse the same
  55  * Background on many different Regions. Please refer to
  56  * {@link ../doc-files/cssref.html JavaFX CSS Reference} for a complete description
  57  * of the CSS rules for styling the background of a Region.
  58  * <p/>
  59  * Every Background is comprised of {@link #getFills() fills} and / or
  60  * {@link #getImages() images}. Neither list will ever be null, but either or
  61  * both may be empty. Each defined {@link BackgroundFill} is rendered in order,
  62  * followed by each defined {@link BackgroundImage}.
  63  * <p/>
  64  * The Background's {@link #getOutsets() outsets} define any extension of the drawing area of a Region
  65  * which is necessary to account for all background drawing. These outsets are strictly
  66  * defined by the BackgroundFills that are specified on this Background, if any, because
  67  * all BackgroundImages are clipped to the drawing area, and do not define it. The
  68  * outsets values are strictly non-negative.
  69  *
  70  * @since JavaFX 8.0
  71  */
  72 @SuppressWarnings("unchecked")
  73 public final class Background {
  74     static final CssMetaData<Node,Paint[]> BACKGROUND_COLOR =
  75             new SubCssMetaData<>("-fx-background-color",
  76                     PaintConverter.SequenceConverter.getInstance(),
  77                     new Paint[] {Color.TRANSPARENT});
  78 
  79     static final CssMetaData<Node,CornerRadii[]> BACKGROUND_RADIUS =
  80             new SubCssMetaData<>("-fx-background-radius",
  81                     CornerRadiiConverter.getInstance(),
  82                     new CornerRadii[] {CornerRadii.EMPTY});
  83 
  84     static final CssMetaData<Node,Insets[]> BACKGROUND_INSETS =
  85             new SubCssMetaData<>("-fx-background-insets",
  86                     InsetsConverter.SequenceConverter.getInstance(),
  87                     new Insets[] {Insets.EMPTY});
  88 
  89     static final CssMetaData<Node,Image[]> BACKGROUND_IMAGE =
  90             new SubCssMetaData<>("-fx-background-image",
  91                     URLConverter.SequenceConverter.getInstance());
  92 
  93     static final CssMetaData<Node,RepeatStruct[]> BACKGROUND_REPEAT =
  94             new SubCssMetaData<>("-fx-background-repeat",
  95                     RepeatStructConverter.getInstance(),
  96                     new RepeatStruct[] {new RepeatStruct(BackgroundRepeat.REPEAT,
  97                                                          BackgroundRepeat.REPEAT) });
  98 
  99     static final CssMetaData<Node,BackgroundPosition[]> BACKGROUND_POSITION =
 100             new SubCssMetaData<>("-fx-background-position",
 101                     LayeredBackgroundPositionConverter.getInstance(),
 102                     new BackgroundPosition[] { BackgroundPosition.DEFAULT });
 103 
 104     static final CssMetaData<Node,BackgroundSize[]> BACKGROUND_SIZE =
 105             new SubCssMetaData<>("-fx-background-size",
 106                     LayeredBackgroundSizeConverter.getInstance(),
 107                     new BackgroundSize[] { BackgroundSize.DEFAULT } );
 108 
 109     private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES =
 110             (List<CssMetaData<? extends Styleable, ?>>) (List) Collections.unmodifiableList(
 111                     // Unchecked!
 112                     Arrays.asList(BACKGROUND_COLOR,
 113                             BACKGROUND_INSETS,
 114                             BACKGROUND_RADIUS,
 115                             BACKGROUND_IMAGE,
 116                             BACKGROUND_REPEAT,
 117                             BACKGROUND_POSITION,
 118                             BACKGROUND_SIZE));
 119 
 120     /**
 121      * @return The CssMetaData associated with this class, which may include the
 122      * CssMetaData of its super classes.
 123      */
 124     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
 125         return STYLEABLES;
 126     }
 127 
 128     /**
 129      * An empty Background, useful to use instead of null.
 130      */
 131     public static final Background EMPTY = new Background((BackgroundFill[])null, null);
 132 
 133     /**
 134      * The list of BackgroundFills which together define the filled portion
 135      * of this Background. This List is unmodifiable and immutable. It
 136      * will never be null. The elements of this list will also never be null.
 137      */
 138     public final List<BackgroundFill> getFills() { return fills; }
 139     final List<BackgroundFill> fills;
 140 
 141     /**
 142      * The list of BackgroundImages which together define the image portion
 143      * of this Background. This List is unmodifiable and immutable. It
 144      * will never be null. The elements of this list will also never be null.
 145      */
 146     public final List<BackgroundImage> getImages() { return images; }
 147     final List<BackgroundImage> images;
 148 
 149     /**
 150      * The outsets of this Background. This represents the largest
 151      * bounding rectangle within which all drawing for the Background
 152      * will take place. The outsets will never be negative, and represent
 153      * the distance from the edge of the Region outward. Any BackgroundImages
 154      * which would extend beyond the outsets will be clipped. Only the
 155      * BackgroundFills contribute to the outsets.
 156      */
 157     public final Insets getOutsets() { return outsets; }
 158     final Insets outsets;
 159 
 160     /**
 161      * Gets whether the background is empty. It is empty if there are no fills or images.
 162      * @return true if the Background is empty, false otherwise.
 163      */
 164     public final boolean isEmpty() {
 165         return fills.isEmpty() && images.isEmpty();
 166     }
 167 
 168     /**
 169      * Specifies whether the Background has at least one opaque fill.
 170      */
 171     private final boolean hasOpaqueFill;
 172 
 173     /**
 174      * Package-private immutable fields referring to the opaque insets
 175      * of this Background.
 176      */
 177     private final double opaqueFillTop, opaqueFillRight, opaqueFillBottom, opaqueFillLeft;
 178     final boolean hasPercentageBasedOpaqueFills;
 179 
 180     /**
 181      * True if there are any fills that are in some way based on the size of the region.
 182      * For example, if a CornerRadii on the fill is percentage based in either or both
 183      * dimensions.
 184      */
 185     final boolean hasPercentageBasedFills;
 186 
 187     /**
 188      * The cached hash code computation for the Background. One very big
 189      * reason for making Background immutable was to make it possible to
 190      * cache and reuse the same Background instance for multiple
 191      * Regions (for example, every un-hovered Button should have the same
 192      * Background instance). To enable efficient caching, we cache the hash.
 193      */
 194     private final int hash;
 195 
 196     /**
 197      * Create a new Background by supplying an array of BackgroundFills.
 198      * This array may be null, or may contain null values. Any null values
 199      * will be ignored and will not contribute to the {@link #getFills() fills}
 200      * or {@link #getOutsets() outsets}.
 201      *
 202      * @param fills     The fills. This may be null, and may contain nulls. Any
 203      *                  contained nulls are filtered out and not included in the
 204      *                  final List of fills. A null array becomes an empty List.
 205      */
 206     public Background(final @NamedArg("fills") BackgroundFill... fills) {
 207         this(fills, null);
 208     }
 209 
 210     /**
 211      * Create a new Background by supplying an array of BackgroundImages.
 212      * This array may be null, or may contain null values. Any null values will
 213      * be ignored and will not contribute to the {@link #getImages() images}.
 214      *
 215      * @param images    The images. This may be null, and may contain nulls. Any
 216      *                  contained nulls are filtered out and not included in the
 217      *                  final List of images. A null array becomes an empty List.
 218      */
 219     public Background(final @NamedArg("images") BackgroundImage... images) {
 220         this(null, images);
 221     }
 222 
 223     /**
 224      * Create a new Background supply two Lists, one for background fills and
 225      * one for background images. Either list may be null, and may contain nulls.
 226      * Any null values in these lists will be ignored and will not
 227      * contribute to the {@link #getFills() fills}, {@link #getImages() images}, or
 228      * {@link #getOutsets() outsets}.
 229      *
 230      * @param fills     The fills. This may be null, and may contain nulls. Any
 231      *                  contained nulls are filtered out and not included in the
 232      *                  final List of fills. A null List becomes an empty List.
 233      * @param images    The images. This may be null, and may contain nulls. Any
 234      *                  contained nulls are filtered out and not included in the
 235      *                  final List of images. A null List becomes an empty List.
 236      */
 237     public Background(final @NamedArg("fills") List<BackgroundFill> fills, final @NamedArg("images") List<BackgroundImage> images) {
 238         // NOTE: This constructor had to be supplied in order to cause a Builder
 239         // to be auto-generated, because otherwise the types of the fills and images
 240         // properties didn't match the types of the array based constructor parameters.
 241         // So a Builder will use this constructor, while the CSS engine uses the
 242         // array based constructor (for speed).
 243         this(fills == null ? null : fills.toArray(new BackgroundFill[fills.size()]),
 244              images == null ? null : images.toArray(new BackgroundImage[images.size()]));
 245     }
 246 
 247     /**
 248      * Create a new Background by supplying two arrays, one for background fills,
 249      * and one for background images. Either array may be null, and may contain null
 250      * values. Any null values in these arrays will be ignored and will not
 251      * contribute to the {@link #getFills() fills}, {@link #getImages() images}, or
 252      * {@link #getOutsets() outsets}.
 253      *
 254      * @param fills     The fills. This may be null, and may contain nulls. Any
 255      *                  contained nulls are filtered out and not included in the
 256      *                  final List of fills. A null array becomes an empty List.
 257      * @param images    The images. This may be null, and may contain nulls. Any
 258      *                  contained nulls are filtered out and not included in the
 259      *                  final List of images. A null array becomes an empty List.
 260      */
 261     public Background(final @NamedArg("fills") BackgroundFill[] fills, final @NamedArg("images") BackgroundImage[] images) {
 262         // The cumulative insets
 263         double outerTop = 0, outerRight = 0, outerBottom = 0, outerLeft = 0;
 264         boolean hasPercentOpaqueInsets = false;
 265         boolean hasPercentFillRadii = false;
 266         boolean opaqueFill = false;
 267 
 268         // If the fills is empty or null then we know we can just use the shared
 269         // immutable empty list from Collections.
 270         if (fills == null || fills.length == 0) {
 271             this.fills = Collections.emptyList();
 272         } else {
 273             // We need to iterate over all of the supplied elements in the fills array.
 274             // Each null element is ignored. Each non-null element is inspected to
 275             // see if it contributes to the outsets.
 276             final BackgroundFill[] noNulls = new BackgroundFill[fills.length];
 277             int size = 0;
 278             for (int i=0; i<fills.length; i++) {
 279                 final BackgroundFill fill = fills[i];
 280                 if (fill != null) {
 281                     noNulls[size++] = fill;
 282                     final Insets fillInsets = fill.getInsets();
 283                     final double fillTop = fillInsets.getTop();
 284                     final double fillRight = fillInsets.getRight();
 285                     final double fillBottom = fillInsets.getBottom();
 286                     final double fillLeft = fillInsets.getLeft();
 287                     outerTop = outerTop <= fillTop ? outerTop : fillTop; // min
 288                     outerRight = outerRight <= fillRight ? outerRight : fillRight; // min
 289                     outerBottom = outerBottom <= fillBottom ? outerBottom : fillBottom; // min
 290                     outerLeft = outerLeft <= fillLeft ? outerLeft : fillLeft; // min
 291 
 292                     // The common case is to NOT have percent based radii
 293                     final boolean b = fill.getRadii().hasPercentBasedRadii;
 294                     hasPercentFillRadii |= b;
 295                     if (fill.fill.isOpaque()) {
 296                         opaqueFill = true;
 297                         if (b) {
 298                             hasPercentOpaqueInsets = true;
 299                         }
 300                     }
 301                 }
 302             }
 303             this.fills = new UnmodifiableArrayList<>(noNulls, size);
 304         }
 305         hasPercentageBasedFills = hasPercentFillRadii;
 306 
 307         // This ensures that we either have outsets of 0, if all the insets were positive,
 308         // or a value greater than zero if they were negative.
 309         outsets = new Insets(
 310                 Math.max(0, -outerTop),
 311                 Math.max(0, -outerRight),
 312                 Math.max(0, -outerBottom),
 313                 Math.max(0, -outerLeft));
 314 
 315         // An null or empty images array results in an empty list
 316         if (images == null || images.length == 0) {
 317             this.images = Collections.emptyList();
 318         } else {
 319             // Filter out any  null values and create an immutable array list
 320             final BackgroundImage[] noNulls = new BackgroundImage[images.length];
 321             int size = 0;
 322             for (int i=0; i<images.length; i++) {
 323                 final BackgroundImage image = images[i];
 324                 if (image != null) noNulls[size++] = image;
 325             }
 326             this.images = new UnmodifiableArrayList<>(noNulls, size);
 327         }
 328 
 329         hasOpaqueFill = opaqueFill;
 330         if (hasPercentOpaqueInsets) {
 331             opaqueFillTop = Double.NaN;
 332             opaqueFillRight = Double.NaN;
 333             opaqueFillBottom = Double.NaN;
 334             opaqueFillLeft = Double.NaN;
 335         } else {
 336             double[] trbl = new double[4];
 337             computeOpaqueInsets(1, 1, true, trbl);
 338             opaqueFillTop = trbl[0];
 339             opaqueFillRight = trbl[1];
 340             opaqueFillBottom = trbl[2];
 341             opaqueFillLeft = trbl[3];
 342         }
 343         hasPercentageBasedOpaqueFills = hasPercentOpaqueInsets;
 344 
 345         // Pre-compute the hash code. NOTE: all variables are prefixed with "this" so that we
 346         // do not accidentally compute the hash based on the constructor arguments rather than
 347         // based on the fields themselves!
 348         int result = this.fills.hashCode();
 349         result = 31 * result + this.images.hashCode();
 350         hash = result;
 351     }
 352 
 353     /**
 354      * Gets whether the fill of this Background is based on percentages (that is, relative to the
 355      * size of the region being styled). Specifically, this returns true if any of the CornerRadii
 356      * on any of the fills on this Background has a radius that is based on percentages.
 357      *
 358      * @return True if any CornerRadii of any BackgroundFill on this background would return true, false otherwise.
 359      * @since JavaFX 8.0
 360      */
 361     public boolean isFillPercentageBased() {
 362         return hasPercentageBasedFills;
 363     }
 364 
 365     /**
 366      * Computes the opaque insets for a region with the specified width and height. This call
 367      * must be made whenever the width or height of the region change, because the opaque insets
 368      * are based on background fills, and the corner radii of a background fill can be percentage
 369      * based. Thus, we need to potentially recompute the opaque insets whenever the width or
 370      * height of the region change. On the other hand, if there are no percentage based corner
 371      * radii, then we can simply return the pre-computed and cached answers.
 372      *
 373      * @param width     The width of the region
 374      * @param height    The height of the region
 375      * @param trbl      A four-element array of doubles in order: top, right, bottom, left.
 376      */
 377     void computeOpaqueInsets(double width, double height, double[] trbl) {
 378         computeOpaqueInsets(width, height, false, trbl);
 379     }
 380 
 381     /**
 382      * Computes the opaque insets. The first time this is called from the constructor
 383      * we want to take the long route through and compute everything, whether there are
 384      * percentage based insets or not (the constructor ensures not to call it in the case
 385      * that it has percentage based insets!). All other times, this is called by the other
 386      * computeOpaqueInsets method with "firstTime" set to false, such that if we have
 387      * percentage based insets, then we will bail early.
 388      *
 389      * This method takes into account both fills and images. Because images can be
 390      * lazy loaded, we cannot pre-compute a bunch of things in the constructor for images
 391      * the way we can with fills. Instead, each time the method is called, we have to
 392      * inspect the images. However, we do have fast paths for cases where fills are used
 393      * and not images.
 394      *
 395      * @param width        The width of the region
 396      * @param height       The height of the region
 397      * @param firstTime    Whether this is being called from the constructor
 398      * @param trbl         A four-element array of doubles in order: top, right, bottom, left.
 399      */
 400     private void computeOpaqueInsets(double width, double height, boolean firstTime, double[] trbl) {
 401         double opaqueRegionTop = Double.NaN,
 402                opaqueRegionRight = Double.NaN,
 403                opaqueRegionBottom = Double.NaN,
 404                opaqueRegionLeft = Double.NaN;
 405 
 406         // If during object construction we determined that there is an opaque fill, then we need
 407         // to visit the fills and figure out which ones contribute to the opaque insets
 408         if (hasOpaqueFill) {
 409             // If during construction time we determined that none of the fills had a percentage based
 410             // opaque inset, then we can just use the pre-computed values. This is worth doing since
 411             // at this time all CSS based radii for BackgroundFills are literal values!
 412             if (!firstTime && !hasPercentageBasedOpaqueFills) {
 413                 opaqueRegionTop = opaqueFillTop;
 414                 opaqueRegionRight = opaqueFillRight;
 415                 opaqueRegionBottom = opaqueFillBottom;
 416                 opaqueRegionLeft = opaqueFillLeft;
 417             } else {
 418                 // NOTE: We know at this point that there is an opaque fill, and that at least one
 419                 // of them uses a percentage for at least one corner radius. Iterate over each
 420                 // BackgroundFill. If the fill is opaque, then we will compute the largest rectangle
 421                 // which will fit within its opaque area, taking the corner radii into account.
 422                 // Initialize them to the "I Don't Know" answer.
 423 
 424                 for (int i=0, max=fills.size(); i<max; i++) {
 425                     final BackgroundFill fill = fills.get(i);
 426                     final Insets fillInsets = fill.getInsets();
 427                     final double fillTop = fillInsets.getTop();
 428                     final double fillRight = fillInsets.getRight();
 429                     final double fillBottom = fillInsets.getBottom();
 430                     final double fillLeft = fillInsets.getLeft();
 431 
 432                     if (fill.fill.isOpaque()) {
 433                         // Some possible configurations:
 434                         //     (a) rect1 is completely contained by rect2
 435                         //     (b) rect2 is completely contained by rect1
 436                         //     (c) rect1 is the same height as rect 2 and they overlap on the left or right
 437                         //     (d) rect1 is the same width as rect 2 and they overlap on the top or bottom
 438                         //     (e) they are disjoint or overlap in an unsupported manner.
 439                         final CornerRadii radii = fill.getRadii();
 440                         final double topLeftHorizontalRadius = radii.isTopLeftHorizontalRadiusAsPercentage() ?
 441                                 width * radii.getTopLeftHorizontalRadius() : radii.getTopLeftHorizontalRadius();
 442                         final double topLeftVerticalRadius = radii.isTopLeftVerticalRadiusAsPercentage() ?
 443                                 height * radii.getTopLeftVerticalRadius() : radii.getTopLeftVerticalRadius();
 444                         final double topRightVerticalRadius = radii.isTopRightVerticalRadiusAsPercentage() ?
 445                                 height * radii.getTopRightVerticalRadius() : radii.getTopRightVerticalRadius();
 446                         final double topRightHorizontalRadius = radii.isTopRightHorizontalRadiusAsPercentage() ?
 447                                 width * radii.getTopRightHorizontalRadius() : radii.getTopRightHorizontalRadius();
 448                         final double bottomRightHorizontalRadius = radii.isBottomRightHorizontalRadiusAsPercentage() ?
 449                                 width * radii.getBottomRightHorizontalRadius() : radii.getBottomRightHorizontalRadius();
 450                         final double bottomRightVerticalRadius = radii.isBottomRightVerticalRadiusAsPercentage() ?
 451                                 height * radii.getBottomRightVerticalRadius() : radii.getBottomRightVerticalRadius();
 452                         final double bottomLeftVerticalRadius = radii.isBottomLeftVerticalRadiusAsPercentage() ?
 453                                 height * radii.getBottomLeftVerticalRadius() : radii.getBottomLeftVerticalRadius();
 454                         final double bottomLeftHorizontalRadius = radii.isBottomLeftHorizontalRadiusAsPercentage() ?
 455                                 width * radii.getBottomLeftHorizontalRadius() : radii.getBottomLeftHorizontalRadius();
 456 
 457                         final double t = fillTop + (Math.max(topLeftVerticalRadius, topRightVerticalRadius) / 2);
 458                         final double r = fillRight + (Math.max(topRightHorizontalRadius, bottomRightHorizontalRadius) / 2);
 459                         final double b = fillBottom + (Math.max(bottomLeftVerticalRadius, bottomRightVerticalRadius) / 2);
 460                         final double l = fillLeft + (Math.max(topLeftHorizontalRadius, bottomLeftHorizontalRadius) / 2);
 461                         if (Double.isNaN(opaqueRegionTop)) {
 462                             // This only happens for the first opaque fill we encounter
 463                             opaqueRegionTop = t;
 464                             opaqueRegionRight = r;
 465                             opaqueRegionBottom = b;
 466                             opaqueRegionLeft = l;
 467                         } else {
 468                             final boolean largerTop = t >= opaqueRegionTop;
 469                             final boolean largerRight = r >= opaqueRegionRight;
 470                             final boolean largerBottom = b >= opaqueRegionBottom;
 471                             final boolean largerLeft = l >= opaqueRegionLeft;
 472                             if (largerTop && largerRight && largerBottom && largerLeft) {
 473                                 // The new fill is completely contained within the existing rect, so no change
 474                                 continue;
 475                             } else if (!largerTop && !largerRight && !largerBottom && !largerLeft) {
 476                                 // The new fill completely contains the existing rect, so use these
 477                                 // new values for our opaque region
 478                                 opaqueRegionTop = fillTop;
 479                                 opaqueRegionRight = fillRight;
 480                                 opaqueRegionBottom = fillBottom;
 481                                 opaqueRegionLeft = fillLeft;
 482                             } else if (l == opaqueRegionLeft && r == opaqueRegionRight) {
 483                                 // The left and right insets are the same between the two rects, so just pick
 484                                 // the smallest top and bottom
 485                                 opaqueRegionTop = Math.min(t, opaqueRegionTop);
 486                                 opaqueRegionBottom = Math.min(b, opaqueRegionBottom);
 487                             } else if (t == opaqueRegionTop && b == opaqueRegionBottom) {
 488                                 // The top and bottom are the same between the two rects so just pick
 489                                 // the smallest left and right
 490                                 opaqueRegionLeft = Math.min(l, opaqueRegionLeft);
 491                                 opaqueRegionRight = Math.min(r, opaqueRegionRight);
 492                             } else {
 493                                 // They are disjoint or overlap in some other manner. So we will just
 494                                 // ignore this region.
 495                                 continue;
 496                             }
 497                         }
 498                     }
 499                 }
 500             }
 501         }
 502 
 503         // Check the background images. Since the image of a BackgroundImage might load asynchronously
 504         // and since we must inspect the image to check for opacity, we just have to visit all the
 505         // images each time this method is called rather than pre-computing results. With some work
 506         // we could end up caching the result eventually.
 507         final Toolkit.ImageAccessor acc = Toolkit.getImageAccessor();
 508         for (BackgroundImage bi : images) {
 509             if (bi.opaque == null) {
 510                 // If the image is not yet loaded, just skip it
 511                 // Note: Unit test wants this to be com.sun.javafx.tk.PlatformImage, not com.sun.prism.Image
 512                 final com.sun.javafx.tk.PlatformImage platformImage = acc.getImageProperty(bi.image).get();
 513                 if (platformImage == null) continue;
 514 
 515                 // The image has been loaded, so update the opaque flag
 516                 if (platformImage instanceof com.sun.prism.Image) {
 517                     bi.opaque = ((com.sun.prism.Image)platformImage).isOpaque();
 518                 } else {
 519                     continue;
 520                 }
 521             }
 522 
 523             // At this point we know that we're processing an image which has already been resolved
 524             // and we know whether it is opaque or not. Of course, we only care about processing
 525             // opaque images.
 526             if (bi.opaque) {
 527                 if (bi.size.cover ||
 528                         (bi.size.height == BackgroundSize.AUTO && bi.size.width == BackgroundSize.AUTO &&
 529                         bi.size.widthAsPercentage && bi.size.heightAsPercentage)) {
 530                     // If the size mode is "cover" or AUTO, AUTO, and percentage based, then we're done -- we can simply
 531                     // accumulate insets of "0"
 532                     opaqueRegionTop = Double.isNaN(opaqueRegionTop) ? 0 : Math.min(0, opaqueRegionTop);
 533                     opaqueRegionRight = Double.isNaN(opaqueRegionRight) ? 0 : Math.min(0, opaqueRegionRight);
 534                     opaqueRegionBottom = Double.isNaN(opaqueRegionBottom) ? 0 : Math.min(0, opaqueRegionBottom);
 535                     opaqueRegionLeft = Double.isNaN(opaqueRegionLeft) ? 0 : Math.min(0, opaqueRegionLeft);
 536                     break;
 537                 } else {
 538                     // Here we are taking into account all potential tiling cases including "contain". Basically,
 539                     // as long as the repeat is *not* SPACE, we know that we'll be touching every pixel, and we
 540                     // don't really care how big the tiles end up being. The only case where we care about the
 541                     // actual tile size is in the NO_REPEAT modes.
 542 
 543                     // If the repeatX or repeatY includes "SPACE" Then we bail, because we can't be happy about
 544                     // spaces strewn about within the region.
 545                     if (bi.repeatX == BackgroundRepeat.SPACE || bi.repeatY == BackgroundRepeat.SPACE) {
 546                         bi.opaque = false; // We'll treat it as false in the future
 547                         continue;
 548                     }
 549 
 550                     // If the repeatX and repeatY are "REPEAT" and/or "ROUND" (any combination thereof) then
 551                     // we know all pixels within the region width / height are being touched, so we can just
 552                     // set the opaqueRegion variables and we're done.
 553                     final boolean filledX = bi.repeatX == BackgroundRepeat.REPEAT || bi.repeatX == BackgroundRepeat.ROUND;
 554                     final boolean filledY = bi.repeatY == BackgroundRepeat.REPEAT || bi.repeatY == BackgroundRepeat.ROUND;
 555                     if (filledX && filledY) {
 556                         opaqueRegionTop = Double.isNaN(opaqueRegionTop) ? 0 : Math.min(0, opaqueRegionTop);
 557                         opaqueRegionRight = Double.isNaN(opaqueRegionRight) ? 0 : Math.min(0, opaqueRegionRight);
 558                         opaqueRegionBottom = Double.isNaN(opaqueRegionBottom) ? 0 : Math.min(0, opaqueRegionBottom);
 559                         opaqueRegionLeft = Double.isNaN(opaqueRegionLeft) ? 0 : Math.min(0, opaqueRegionLeft);
 560                         break;
 561                     }
 562 
 563                     // We know that one or the other dimension is not filled, so we have to compute the right
 564                     // width / height. This is basically a big copy/paste from NGRegion! Blah!
 565                     final double w = bi.size.widthAsPercentage ? bi.size.width * width : bi.size.width;
 566                     final double h = bi.size.heightAsPercentage ? bi.size.height * height : bi.size.height;
 567                     final double imgUnscaledWidth = bi.image.getWidth();
 568                     final double imgUnscaledHeight = bi.image.getHeight();
 569 
 570                     // Now figure out the width and height of each tile to be drawn. The actual image
 571                     // dimensions may be one thing, but we need to figure out what the size of the image
 572                     // in the destination is going to be.
 573                     final double tileWidth, tileHeight;
 574                     if (bi.size.contain) {
 575                         // In the case of "contain", we compute the destination size based on the largest
 576                         // possible scale such that the aspect ratio is maintained, yet one side of the
 577                         // region is completely filled.
 578                         final double scaleX = width / imgUnscaledWidth;
 579                         final double scaleY = height / imgUnscaledHeight;
 580                         final double scale = Math.min(scaleX, scaleY);
 581                         tileWidth = Math.ceil(scale * imgUnscaledWidth);
 582                         tileHeight = Math.ceil(scale * imgUnscaledHeight);
 583                     } else if (bi.size.width >= 0 && bi.size.height >= 0) {
 584                         // The width and height have been expressly defined. Note that AUTO is -1,
 585                         // and all other negative values are disallowed, so by checking >= 0, we
 586                         // are essentially saying "if neither is AUTO"
 587                         tileWidth = w;
 588                         tileHeight = h;
 589                     } else if (w >= 0) {
 590                         // In this case, the width is specified, but the height is AUTO
 591                         tileWidth = w;
 592                         final double scale = tileWidth / imgUnscaledWidth;
 593                         tileHeight = imgUnscaledHeight * scale;
 594                     } else if (h >= 0) {
 595                         // Here the height is specified and the width is AUTO
 596                         tileHeight = h;
 597                         final double scale = tileHeight / imgUnscaledHeight;
 598                         tileWidth = imgUnscaledWidth * scale;
 599                     } else {
 600                         // Both are auto.
 601                         tileWidth = imgUnscaledWidth;
 602                         tileHeight = imgUnscaledHeight;
 603                     }
 604 
 605                     opaqueRegionTop = Double.isNaN(opaqueRegionTop) ? 0 : Math.min(0, opaqueRegionTop);
 606                     opaqueRegionRight = Double.isNaN(opaqueRegionRight) ? (width - tileWidth) : Math.min(width - tileWidth, opaqueRegionRight);
 607                     opaqueRegionBottom = Double.isNaN(opaqueRegionBottom) ? (height - tileHeight) : Math.min(height - tileHeight, opaqueRegionBottom);
 608                     opaqueRegionLeft = Double.isNaN(opaqueRegionLeft) ? 0 : Math.min(0, opaqueRegionLeft);
 609                 }
 610             }
 611         }
 612 
 613         trbl[0] = opaqueRegionTop;
 614         trbl[1] = opaqueRegionRight;
 615         trbl[2] = opaqueRegionBottom;
 616         trbl[3] = opaqueRegionLeft;
 617     }
 618 
 619     /**
 620      * @inheritDoc
 621      */
 622     @Override public boolean equals(Object o) {
 623         if (this == o) return true;
 624         if (o == null || getClass() != o.getClass()) return false;
 625         Background that = (Background) o;
 626         // Because the hash is cached, this can be a very fast check
 627         if (hash != that.hash) return false;
 628         if (!fills.equals(that.fills)) return false;
 629         if (!images.equals(that.images)) return false;
 630 
 631         return true;
 632     }
 633 
 634     /**
 635      * @inheritDoc
 636      */
 637     @Override public int hashCode() {
 638         return hash;
 639     }
 640 }