package org.saturnclient.ui.elements; import java.util.ArrayList; import java.util.List; import org.saturnclient.config.Theme; import org.saturnclient.ui.Element; import org.saturnclient.ui.ElementContext; import org.saturnclient.ui.ElementRenderer; import org.saturnclient.ui.RenderScope; /** * A vertically scrolling container. It scrolls with the mouse wheel or by * dragging its scrollbar, which shows along its right edge when the children * don't fit. */ public class Scroll extends Element { /** Units scrolled per mouse wheel notch */ private static final double SCROLL_SPEED = 24; /** Space left below the last child when scrolled to the end */ private static final int END_SPACE = 10; private static final int BAR_WIDTH = 5; private static final int BAR_INSET = 5; private static final int MIN_THUMB = 20; /** Width to keep clear at the right of the content for the scrollbar and its click area */ public static final int SCROLLBAR_SPACE = BAR_WIDTH + BAR_INSET * 2 + 4; int padding = 0; boolean background = true; protected List children = new ArrayList<>(); /** Kept fractional so small trackpad deltas add up instead of being dropped */ private double scroll = 0; private int maxScroll = 0; /** Where in the thumb it was grabbed, while it's being dragged */ private Integer thumbGrab = null; public Scroll(int padding) { this.padding = padding; } public void draw(Element element) { ElementRenderer.draw(children, element); } /** * Skips the scroll's own background, for when it sits on a {@link Panel}. */ public Scroll noBackground() { this.background = false; return this; } /** * Removes every child and scrolls back to the top. */ public void clear() { children.clear(); scroll = 0; maxScroll = 0; } @Override public void render(RenderScope renderScope, ElementContext ctx) { updateMaxScroll(); int offset = getOffset(); if (background) { renderScope.drawRoundedRectangle(0, 0, width, height, Theme.BG_RADIUS.value, Theme.BACKGROUND.value); } renderScope.enableScissor(padding, padding, width - padding, height - padding); renderScope.getMatrixStack().push(); renderScope.getMatrixStack().translate(padding, -offset + padding); ElementRenderer.render(children, ctx.elapsed, renderScope, ctx.mouseX - padding, ctx.mouseY - padding + offset, ctx.childFrame(padding, padding - offset, padding, height - padding)); renderScope.getMatrixStack().pop(); renderScope.disableScissor(); if (maxScroll > 0) { boolean active = thumbGrab != null || ctx.isHovering(getBarX() - BAR_INSET, 0, BAR_WIDTH + BAR_INSET * 2, height); renderScope.drawRoundedRectangle(getBarX(), getThumbY(), BAR_WIDTH, getThumbHeight(), BAR_WIDTH / 2, active ? Theme.SCROLL.value : Theme.withAlpha(0.7f, Theme.SCROLL.value)); } } @Override public void scroll(int mouseX, int mouseY, double horizontalAmount, double verticalAmount) { setScroll(scroll - verticalAmount * SCROLL_SPEED); } @Override public void keyPressed(int keyCode, int scanCode, int modifiers) { ElementRenderer.keyPressed(children, keyCode, scanCode, modifiers); } @Override public void mouseClicked(double mouseX, double mouseY, int button) { // Children scrolled out of view still have positions under the rest of the // screen, so only clicks inside the scroll's own bounds may reach them. if (mouseX < 0 || mouseY < 0 || mouseX > width || mouseY > height) { for (Element child : children) { child.focused = false; } return; } if (maxScroll > 0 && mouseX >= getBarX() - BAR_INSET) { int thumbY = getThumbY(); int thumbHeight = getThumbHeight(); // Grab the thumb where it was clicked, or center it on a click in the track. boolean onThumb = mouseY >= thumbY && mouseY <= thumbY + thumbHeight; thumbGrab = onThumb ? (int) mouseY - thumbY : thumbHeight / 2; dragThumb(mouseY); return; } ElementRenderer.mouseClicked(children, mouseX - padding, mouseY - padding + getOffset(), button); } @Override public void mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) { if (thumbGrab != null) { dragThumb(mouseY); return; } ElementRenderer.mouseDragged(children, mouseX - padding, mouseY - padding + getOffset(), button, deltaX, deltaY); } @Override public void mouseReleased(double mouseX, double mouseY, int button) { thumbGrab = null; ElementRenderer.mouseReleased(children, mouseX - padding, mouseY - padding + getOffset(), button); } private void dragThumb(double mouseY) { int travel = getTrackHeight() - getThumbHeight(); if (travel > 0) { setScroll((mouseY - thumbGrab - BAR_INSET) / travel * maxScroll); } } private void setScroll(double value) { scroll = Math.max(0, Math.min(maxScroll, value)); } private int getOffset() { return (int) Math.round(scroll); } private void updateMaxScroll() { int bottom = 0; for (Element element : children) { bottom = Math.max(bottom, element.y + element.height); } int overflow = bottom + padding * 2 - height; maxScroll = overflow > 0 ? overflow + END_SPACE : 0; // The content may have shrunk, such as after filtering. setScroll(scroll); } private int getBarX() { return width - BAR_WIDTH - BAR_INSET; } private int getTrackHeight() { return height - BAR_INSET * 2; } private int getThumbHeight() { int track = getTrackHeight(); return Math.max(MIN_THUMB, (int) ((long) track * height / (height + maxScroll))); } private int getThumbY() { int travel = getTrackHeight() - getThumbHeight(); return BAR_INSET + (int) (travel * (scroll / maxScroll)); } }