ElementContext carries the part of the screen where an element can be seen, passed down in a Frame and narrowed by each Scroll to its own box. A dropdown opens below only if its list fits in that part, otherwise above if it fits there, or else on the side with more room, so one at the bottom of a scrolled list no longer opens past the list's edge. Co-Authored-By: Claude Opus 5.5 <[email protected]>
196 lines
6.3 KiB
Java
196 lines
6.3 KiB
Java
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<Element> 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));
|
|
}
|
|
}
|