package org.saturnclient.ui; import java.util.List; /** * A single element drawn above the rest of the screen, such as a dropdown's * options or a color picker. It gets mouse events before anything else, and a * click or scroll outside it closes it. * * Only one screen is open at a time, so the popup is kept here rather than on * the screen; {@link SaturnScreen} forwards to it and clears it when it's * rebuilt. */ public final class Popup { private static final int SCREEN_MARGIN = 8; private static final int OWNER_GAP = 4; private static Element popup; private static Element owner; private static int screenWidth, screenHeight; private Popup() { } /** * Opens {@code popup} with its top-left corner at ({@code x}, {@code y}) on * the screen, moved as needed to stay on it. * * @param owner the element that opened it, for {@link #isOpenFor} */ public static void open(Element owner, Element popup, int x, int y) { Popup.owner = owner; Popup.popup = popup; popup.x = clamp(x, popup.width, screenWidth); popup.y = clamp(y, popup.height, screenHeight); } /** * Opens {@code popup} just below its owner if it fits in the part of the * screen where the owner can be seen, such as a scroll's box; otherwise * above it if it fits there, or else on the side with more room. * * @param ownerContext the owner's context from its last render */ public static void openBelow(Element owner, Element popup, ElementContext ownerContext) { openBelow(owner, popup, ownerContext, 0); } /** * Like {@link #openBelow(Element, Element, ElementContext)}, with the popup * moved {@code offsetX} from the owner's left edge. */ public static void openBelow(Element owner, Element popup, ElementContext ownerContext, int offsetX) { int top = Math.max(ownerContext.visibleTop, SCREEN_MARGIN); int bottom = Math.min(ownerContext.visibleBottom, screenHeight - SCREEN_MARGIN); int below = ownerContext.originY + owner.height + OWNER_GAP; int above = ownerContext.originY - OWNER_GAP - popup.height; boolean fitsBelow = below + popup.height <= bottom; boolean fitsAbove = above >= top; boolean moreRoomBelow = bottom - below >= ownerContext.originY - OWNER_GAP - top; int y = fitsBelow || (!fitsAbove && moreRoomBelow) ? below : above; open(owner, popup, ownerContext.originX + offsetX, y); } public static void close() { popup = null; owner = null; } public static boolean isOpenFor(Element element) { return popup != null && owner == element; } // --------------------------------------------------------------- // Called by SaturnScreen // --------------------------------------------------------------- static void reset(int width, int height) { close(); screenWidth = width; screenHeight = height; } static void render(RenderScope renderScope, long elapsed, int mouseX, int mouseY) { if (popup != null) { ElementRenderer.render(List.of(popup), elapsed, renderScope, mouseX, mouseY); } } /** * @return whether the popup took the click; a click outside it closes it * and is taken too, so it doesn't reach what's underneath */ static boolean mouseClicked(double mouseX, double mouseY, int button) { if (popup == null) { return false; } if (isInside(mouseX, mouseY)) { ElementRenderer.mouseClicked(List.of(popup), mouseX, mouseY, button); } else { close(); } return true; } static boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { if (popup == null) { return false; } ElementRenderer.mouseDragged(List.of(popup), mouseX, mouseY, button, deltaX, deltaY); return true; } static void mouseReleased(double mouseX, double mouseY, int button) { if (popup != null) { ElementRenderer.mouseReleased(List.of(popup), mouseX, mouseY, button); } } /** * @return whether the popup took the scroll; scrolling outside it closes it, * since it wouldn't move with what's underneath */ static boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { if (popup == null) { return false; } if (isInside(mouseX, mouseY)) { ElementRenderer.mouseScrolled(List.of(popup), mouseX, mouseY, horizontalAmount, verticalAmount); } else { close(); } return true; } private static boolean isInside(double mouseX, double mouseY) { return mouseX >= popup.x && mouseX <= popup.x + popup.width && mouseY >= popup.y && mouseY <= popup.y + popup.height; } private static int clamp(int position, int size, int screenSize) { return Math.max(SCREEN_MARGIN, Math.min(position, screenSize - SCREEN_MARGIN - size)); } }