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