1 /* 2 * Rectangle.java 3 * 4 * Copyright (C) 2021 by RStudio, PBC 5 * 6 * Unless you have received this program directly from RStudio pursuant 7 * to the terms of a commercial license agreement with RStudio, then 8 * this program is licensed to you under the terms of version 3 of the 9 * GNU Affero General Public License. This program is distributed WITHOUT 10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT, 11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the 12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details. 13 * 14 */ 15 package org.rstudio.core.client; 16 17 public class Rectangle 18 { Rectangle(int x, int y, int width, int height)19 public Rectangle(int x, int y, int width, int height) 20 { 21 this.x = x; 22 this.y = y; 23 this.width = width; 24 this.height = height; 25 } 26 27 // Eclipse auto-generated 28 @Override hashCode()29 public int hashCode() 30 { 31 final int prime = 31; 32 int result = 1; 33 result = prime * result + height; 34 result = prime * result + width; 35 result = prime * result + x; 36 result = prime * result + y; 37 return result; 38 } 39 40 // Eclipse auto-generated 41 @Override equals(Object obj)42 public boolean equals(Object obj) 43 { 44 if (this == obj) 45 return true; 46 if (obj == null) 47 return false; 48 if (getClass() != obj.getClass()) 49 return false; 50 Rectangle other = (Rectangle) obj; 51 if (height != other.height) 52 return false; 53 if (width != other.width) 54 return false; 55 if (x != other.x) 56 return false; 57 return y == other.y; 58 } 59 60 @Override toString()61 public String toString() 62 { 63 return "Rectangle(x=" + x + ",y=" + y + 64 ",w=" + width + ",h=" + height + ")"; 65 } 66 getLeft()67 public int getLeft() 68 { 69 return x; 70 } 71 getTop()72 public int getTop() 73 { 74 return y; 75 } 76 getWidth()77 public int getWidth() 78 { 79 return width; 80 } 81 getHeight()82 public int getHeight() 83 { 84 return height; 85 } 86 getRight()87 public int getRight() 88 { 89 return x + width; 90 } 91 getBottom()92 public int getBottom() 93 { 94 return y + height; 95 } 96 getLocation()97 public Point getLocation() 98 { 99 return Point.create(x, y); 100 } 101 getSize()102 public Size getSize() 103 { 104 return new Size(width, height); 105 } 106 getCorner(boolean left, boolean top)107 public Point getCorner(boolean left, boolean top) 108 { 109 return Point.create( 110 left ? getLeft() : getRight(), 111 top ? getTop() : getBottom()); 112 } 113 move(int x, int y)114 public Rectangle move(int x, int y) 115 { 116 return new Rectangle(x, y, getWidth(), getHeight()); 117 } 118 119 /** 120 * Returns the rectangular intersection of the two rectangles, or null if 121 * the rectangles do not touch anywhere. 122 * @param other The rectangle to intersect with this. 123 * @param canReturnEmptyRect If true, in cases where one of the 124 * rectangles has zero width or height OR cases where the rectangles share 125 * an edge, it's possible for a zero-width or zero-height rectangle to be 126 * returned. If false, then it's guaranteed that the return value will 127 * either be null or a rectangle with a positive area. Note that 128 * regardless of true or false, null can always be returned. 129 * @return The intersection, or null if none. 130 */ intersect(Rectangle other, boolean canReturnEmptyRect)131 public Rectangle intersect(Rectangle other, boolean canReturnEmptyRect) 132 { 133 int left = Math.max(x, other.x); 134 int right = Math.min(getRight(), other.getRight()); 135 int top = Math.max(getTop(), other.getTop()); 136 int bottom = Math.min(getBottom(), other.getBottom()); 137 138 if ((canReturnEmptyRect && left <= right && top <= bottom) 139 || (!canReturnEmptyRect && left < right && top < bottom)) 140 { 141 return new Rectangle(left, top, right-left, bottom-top); 142 } 143 else 144 return null; 145 } 146 147 /** 148 * Enlarge the rectangle in the given directions by the given amounts. 149 */ inflate(int left, int top, int right, int bottom)150 public Rectangle inflate(int left, int top, int right, int bottom) 151 { 152 return new Rectangle(x - left, y - top, 153 width + left + right, height + top + bottom); 154 } 155 156 /** 157 * Enlarge each side of the rectangle by the given amount. Note that e.g. 158 * 10 will result in 20-unit-greater width and height. 159 * @param inflateBy 160 * @return 161 */ inflate(int inflateBy)162 public Rectangle inflate(int inflateBy) 163 { 164 return inflate(inflateBy, inflateBy, inflateBy, inflateBy); 165 } 166 167 /** 168 * Return the center point 169 */ center()170 public Point center() 171 { 172 return Point.create( 173 (getLeft() + getRight()) / 2, 174 (getTop() + getBottom()) / 2); 175 } 176 177 /** 178 * Create a new rectangle of the given width and height that is centered 179 * relative to this rectangle. 180 */ createCenteredRect(int width, int height)181 public Rectangle createCenteredRect(int width, int height) 182 { 183 return new Rectangle( 184 (getWidth() - width) / 2, 185 (getHeight() - height) / 2, 186 width, 187 height); 188 } 189 190 /** 191 * Returns true if this rectangle ENTIRELY contains the given rectangle. 192 */ contains(Rectangle other)193 public boolean contains(Rectangle other) 194 { 195 return getLeft() <= other.getLeft() && 196 getTop() <= other.getTop() && 197 getRight() >= other.getRight() && 198 getBottom() >= other.getBottom(); 199 } 200 201 /** 202 * Intelligently figures out where to move this rectangle within a container 203 * to avoid another rectangle (the avoidee). 204 * @param avoidee The rectangle we're trying to avoid 205 * @param container The rectangle we need to try to stay within, if possible 206 * @return The new location for the rectangle 207 */ avoidBounds(final Rectangle avoidee, final Rectangle container)208 public Point avoidBounds(final Rectangle avoidee, 209 final Rectangle container) 210 { 211 // Check for nothing to avoid 212 if (avoidee == null) 213 return this.getLocation(); 214 215 // Check for no collision 216 if (this.intersect(avoidee, false) == null) 217 return this.getLocation(); 218 219 // Figure out whether the avoidee is in the top or bottom half of the 220 // container. vertDir < 0 means top half, vertDir > 0 means bottom half. 221 int vertDir = avoidee.center().getY() - container.center().getY(); 222 // Create new bounds that are just below or just above the avoidee, 223 // depending on whether the avoidee is in the top or bottom half of 224 // the container, respectively. 225 Rectangle vertShift = this.move( 226 this.getLeft(), 227 vertDir > 0 ? avoidee.getTop() - this.getHeight() 228 : avoidee.getBottom()); 229 // If that resulted in bounds that fit entirely in the container, then 230 // use it. (We prefer vertical shifting to horizontal shifting.) 231 if (container.contains(vertShift)) 232 return vertShift.getLocation(); 233 234 // Now repeat the algorithm in the horizontal dimension. 235 int horizDir = avoidee.center().getX() - container.center().getX(); 236 Rectangle horizShift = this.move( 237 horizDir > 0 ? avoidee.getLeft() - this.getWidth() 238 : avoidee.getRight(), 239 this.getTop()); 240 if (container.contains(horizShift)) 241 return horizShift.getLocation(); 242 243 // Both vertical and horizontal options go off the container. Combine 244 // their effects, then move to within the screen, if possible; or if all 245 // else fails, just center. 246 Rectangle hvShift = new Rectangle(horizShift.getLeft(), 247 vertShift.getTop(), 248 this.getWidth(), 249 this.getHeight()); 250 return hvShift.attemptToMoveInto(container, 251 FailureMode.CENTER).getLocation(); 252 } 253 254 public enum FailureMode 255 { 256 /** 257 * Center the rect's position in this dimension 258 */ 259 CENTER, 260 /** 261 * Don't change the rect's position in this dimension 262 */ 263 NO_CHANGE 264 } 265 266 /** 267 * Attempt to move this rectangle so that it fits inside the given container. 268 * If this rectangle is taller and/or wider than the container, then the 269 * failureMode parameter can be used to dictate what the fallback behavior 270 * should be. 271 */ attemptToMoveInto(Rectangle container, FailureMode failureMode)272 public Rectangle attemptToMoveInto(Rectangle container, 273 FailureMode failureMode) 274 { 275 int newX = this.x; 276 int newY = this.y; 277 278 if (getWidth() <= container.getWidth()) 279 { 280 newX = Math.min(Math.max(newX, container.getLeft()), 281 container.getRight() - getWidth()); 282 } 283 else if (failureMode == FailureMode.CENTER) 284 { 285 newX = container.getLeft() - (getWidth()-container.getWidth())/2; 286 } 287 288 if (getHeight() <= container.getHeight()) 289 { 290 newY = Math.min(Math.max(newY, container.getTop()), 291 container.getBottom() - getHeight()); 292 } 293 else if (failureMode == FailureMode.CENTER) 294 { 295 newY = container.getTop() - (getHeight()-container.getHeight())/2; 296 } 297 298 return new Rectangle(newX, newY, getWidth(), getHeight()); 299 } 300 301 private final int x; 302 private final int y; 303 private final int width; 304 private final int height; 305 } 306