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