/* Copyright 2016 Software Freedom Conservancy Inc. * * This software is licensed under the GNU LGPL (version 2.1 or later). * See the COPYING file in this distribution. */ public enum BoxLocation { OUTSIDE, INSIDE, TOP_SIDE, LEFT_SIDE, RIGHT_SIDE, BOTTOM_SIDE, TOP_LEFT, BOTTOM_LEFT, TOP_RIGHT, BOTTOM_RIGHT } public enum BoxComplements { NONE, VERTICAL, HORIZONTAL, BOTH; public static BoxComplements derive(bool horizontal_complement, bool vertical_complement) { if (horizontal_complement && vertical_complement) return BOTH; else if(horizontal_complement) return HORIZONTAL; else if (vertical_complement) return VERTICAL; return NONE; } } public struct Box { public const int HAND_GRENADES = 12; public int left; public int top; public int right; public int bottom; public Box(int left = 0, int top = 0, int right = 0, int bottom = 0) { // Sanity check on top left vertex. left = left.clamp(0, int.MAX); top = top.clamp(0, int.MAX); // Sanity check on dimensions - force // box to be at least 1 px by 1 px. if (right <= left) right = left + 1; if (bottom <= top) bottom = top + 1; this.left = left; this.top = top; this.right = right; this.bottom = bottom; } public static Box from_rectangle(Gdk.Rectangle rect) { return Box(rect.x, rect.y, rect.x + rect.width - 1, rect.y + rect.height - 1); } public static Box from_allocation(Gtk.Allocation alloc) { return Box(alloc.x, alloc.y, alloc.x + alloc.width - 1, alloc.y + alloc.height - 1); } // This ensures a proper box is built from the points supplied, no matter the relationship // between the two points public static Box from_points(Gdk.Point corner1, Gdk.Point corner2) { return Box(int.min(corner1.x, corner2.x), int.min(corner1.y, corner2.y), int.max(corner1.x, corner2.x), int.max(corner1.y, corner2.y)); } public static Box from_center(Gdk.Point center, int width, int height) { return Box(center.x - (width / 2), center.y - (height / 2), center.x + (width / 2), center.y + (height / 2)); } public int get_width() { assert(right >= left); return right - left + 1; } public int get_height() { assert(bottom >= top); return bottom - top + 1; } public bool is_valid() { return (left >= 0) && (top >= 0) && (right >= left) && (bottom >= top); } public bool equals(Box box) { return (left == box.left && top == box.top && right == box.right && bottom == box.bottom); } // Adjust width, preserving the box's center. public void adjust_width(int width) { int center_x = (left + right) / 2; left = center_x - (width / 2); right = center_x + (width / 2); } // Adjust height, preserving the box's center. public void adjust_height(int height) { int center_y = (top + bottom) / 2; top = center_y - (height / 2); bottom = center_y + (height / 2); } public Box get_scaled(Dimensions scaled) { double x_scale, y_scale; get_dimensions().get_scale_ratios(scaled, out x_scale, out y_scale); int l = (int) Math.round((double) left * x_scale); int t = (int) Math.round((double) top * y_scale); // fix-up to match the scaled dimensions int r = l + scaled.width - 1; int b = t + scaled.height - 1; Box box = Box(l, t, r, b); assert(box.get_width() == scaled.width || box.get_height() == scaled.height); return box; } public Box get_scaled_similar(Dimensions original, Dimensions scaled) { double x_scale, y_scale; original.get_scale_ratios(scaled, out x_scale, out y_scale); int l = (int) Math.round((double) left * x_scale); int t = (int) Math.round((double) top * y_scale); int r = (int) Math.round((double) right * x_scale); int b = (int) Math.round((double) bottom * y_scale); // catch rounding errors if (r >= scaled.width) r = scaled.width - 1; if (b >= scaled.height) b = scaled.height - 1; return Box(l, t, r, b); } public Box get_offset(int xofs, int yofs) { return Box(left + xofs, top + yofs, right + xofs, bottom + yofs); } public Dimensions get_dimensions() { return Dimensions(get_width(), get_height()); } public void get_points(out Gdk.Point top_left, out Gdk.Point bottom_right) { top_left = { left, top }; bottom_right = { right, bottom }; } public Gdk.Rectangle get_rectangle() { Gdk.Rectangle rect = Gdk.Rectangle(); rect.x = left; rect.y = top; rect.width = get_width(); rect.height = get_height(); return rect; } public Gdk.Point get_center() { return { (left + right) / 2, (top + bottom) / 2 }; } public Box rotate_clockwise(Dimensions space) { int l = space.width - bottom - 1; int t = left; int r = space.width - top - 1; int b = right; return Box(l, t, r, b); } public Box rotate_counterclockwise(Dimensions space) { int l = top; int t = space.height - right - 1; int r = bottom; int b = space.height - left - 1; return Box(l, t, r, b); } public Box flip_left_to_right(Dimensions space) { int l = space.width - right - 1; int r = space.width - left - 1; return Box(l, top, r, bottom); } public Box flip_top_to_bottom(Dimensions space) { int t = space.height - bottom - 1; int b = space.height - top - 1; return Box(left, t, right, b); } public bool intersects(Box compare) { int left_intersect = int.max(left, compare.left); int top_intersect = int.max(top, compare.top); int right_intersect = int.min(right, compare.right); int bottom_intersect = int.min(bottom, compare.bottom); return (right_intersect >= left_intersect && bottom_intersect >= top_intersect); } public Box get_reduced(int amount) { return Box(left + amount, top + amount, right - amount, bottom - amount); } public Box get_expanded(int amount) { return Box(left - amount, top - amount, right + amount, bottom + amount); } public bool contains(Gdk.Point point) { return point.x >= left && point.x <= right && point.y >= top && point.y <= bottom; } // This specialized method is only concerned with resized comparisons between two Boxes, // of which one is altered in up to two dimensions: (top or bottom) and/or (left or right). // There may be overlap between the two returned Boxes. public BoxComplements resized_complements(Box resized, out Box horizontal, out bool horizontal_enlarged, out Box vertical, out bool vertical_enlarged) { bool horizontal_complement = true; if (resized.top < top) { // enlarged from top horizontal = Box(resized.left, resized.top, resized.right, top); horizontal_enlarged = true; } else if (resized.top > top) { // shrunk from top horizontal = Box(left, top, right, resized.top); horizontal_enlarged = false; } else if (resized.bottom < bottom) { // shrunk from bottom horizontal = Box(left, resized.bottom, right, bottom); horizontal_enlarged = false; } else if (resized.bottom > bottom) { // enlarged from bottom horizontal = Box(resized.left, bottom, resized.right, resized.bottom); horizontal_enlarged = true; } else { horizontal = Box(); horizontal_enlarged = false; horizontal_complement = false; } bool vertical_complement = true; if (resized.left < left) { // enlarged left vertical = Box(resized.left, resized.top, left, resized.bottom); vertical_enlarged = true; } else if (resized.left > left) { // shrunk left vertical = Box(left, top, resized.left, bottom); vertical_enlarged = false; } else if (resized.right < right) { // shrunk right vertical = Box(resized.right, top, right, bottom); vertical_enlarged = false; } else if (resized.right > right) { // enlarged right vertical = Box(right, resized.top, resized.right, resized.bottom); vertical_enlarged = true; } else { vertical = Box(); vertical_enlarged = false; vertical_complement = false; } return BoxComplements.derive(horizontal_complement, vertical_complement); } // This specialized method is only concerned with the complements of identical Boxes in two // different, spatial locations. There may be overlap between the four returned Boxes. However, // no portion of any of the four boxes will be outside the scope of the two compared boxes. public BoxComplements shifted_complements(Box shifted, out Box horizontal_this, out Box vertical_this, out Box horizontal_shifted, out Box vertical_shifted) { assert(get_width() == shifted.get_width()); assert(get_height() == shifted.get_height()); bool horizontal_complement = true; if (shifted.top < top && shifted.bottom > top) { // shifted up horizontal_this = Box(left, shifted.bottom, right, bottom); horizontal_shifted = Box(shifted.left, shifted.top, shifted.right, top); } else if (shifted.top > top && shifted.top < bottom) { // shifted down horizontal_this = Box(left, top, right, shifted.top); horizontal_shifted = Box(shifted.left, bottom, shifted.right, shifted.bottom); } else { // no vertical shift horizontal_this = Box(); horizontal_shifted = Box(); horizontal_complement = false; } bool vertical_complement = true; if (shifted.left < left && shifted.right > left) { // shifted left vertical_this = Box(shifted.right, top, right, bottom); vertical_shifted = Box(shifted.left, shifted.top, left, shifted.bottom); } else if (shifted.left > left && shifted.left < right) { // shifted right vertical_this = Box(left, top, shifted.left, bottom); vertical_shifted = Box(right, shifted.top, shifted.right, shifted.bottom); } else { // no horizontal shift vertical_this = Box(); vertical_shifted = Box(); vertical_complement = false; } return BoxComplements.derive(horizontal_complement, vertical_complement); } public Box rubber_band(Gdk.Point point) { assert(point.x >= 0); assert(point.y >= 0); int t = int.min(top, point.y); int b = int.max(bottom, point.y); int l = int.min(left, point.x); int r = int.max(right, point.x); return Box(l, t, r, b); } public string to_string() { return "%d,%d %d,%d (%s)".printf(left, top, right, bottom, get_dimensions().to_string()); } private static bool in_zone(double pos, int zone) { int top_zone = zone - HAND_GRENADES; int bottom_zone = zone + HAND_GRENADES; return in_between(pos, top_zone, bottom_zone); } private static bool in_between(double pos, int top, int bottom) { int ipos = (int) pos; return (ipos > top) && (ipos < bottom); } private static bool near_in_between(double pos, int top, int bottom) { int ipos = (int) pos; int top_zone = top - HAND_GRENADES; int bottom_zone = bottom + HAND_GRENADES; return (ipos > top_zone) && (ipos < bottom_zone); } public BoxLocation approx_location(int x, int y) { bool near_width = near_in_between(x, left, right); bool near_height = near_in_between(y, top, bottom); if (in_zone(x, left) && near_height) { if (in_zone(y, top)) { return BoxLocation.TOP_LEFT; } else if (in_zone(y, bottom)) { return BoxLocation.BOTTOM_LEFT; } else { return BoxLocation.LEFT_SIDE; } } else if (in_zone(x, right) && near_height) { if (in_zone(y, top)) { return BoxLocation.TOP_RIGHT; } else if (in_zone(y, bottom)) { return BoxLocation.BOTTOM_RIGHT; } else { return BoxLocation.RIGHT_SIDE; } } else if (in_zone(y, top) && near_width) { // if left or right was in zone, already caught top left & top right return BoxLocation.TOP_SIDE; } else if (in_zone(y, bottom) && near_width) { // if left or right was in zone, already caught bottom left & bottom right return BoxLocation.BOTTOM_SIDE; } else if (in_between(x, left, right) && in_between(y, top, bottom)) { return BoxLocation.INSIDE; } else { return BoxLocation.OUTSIDE; } } }