Popup draws one element above the rest of the screen and gets mouse events first; clicking or scrolling outside closes it. ElementContext now carries each element's position on screen so an input inside a scroll can open a popup beside itself. Select is a dropdown: the current value opens an OptionList with the current option ticked, scrolling for long lists and opening upward when there's no room below. The color picker moved out of ColorInput into a ColorPicker popup, so scroll areas no longer clip it and later rows no longer draw over it, and all three of its parts can be dragged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
126 lines
5.0 KiB
Java
126 lines
5.0 KiB
Java
package org.saturnclient.ui.components;
|
|
|
|
import java.util.function.BooleanSupplier;
|
|
|
|
import org.saturnclient.common.ref.asset.IdentifierRef;
|
|
import org.saturnclient.config.Theme;
|
|
import org.saturnclient.ui.Element;
|
|
import org.saturnclient.ui.ElementContext;
|
|
import org.saturnclient.ui.RenderScope;
|
|
import org.saturnclient.ui.resources.Fonts;
|
|
import org.saturnclient.ui.resources.Textures;
|
|
|
|
/**
|
|
* One cosmetic in the cosmetics grid: its preview and name. It shows whether
|
|
* it's equipped, and cosmetics not owned yet are dimmed with a "Get" label.
|
|
*/
|
|
public class CosmeticTile extends Element {
|
|
public static final int HEIGHT = 168;
|
|
|
|
private static final int INSET = 8;
|
|
private static final int IMAGE_AREA = 124;
|
|
private static final float NAME_SCALE = 0.62f;
|
|
private static final float BADGE_SCALE = 0.54f;
|
|
private static final int BADGE_HEIGHT = 20;
|
|
/** How long a claim may take before it can be tried again, in ms */
|
|
private static final long CLAIM_TIMEOUT = 5000;
|
|
|
|
private final String name;
|
|
private final IdentifierRef preview;
|
|
private final float previewAspect;
|
|
private final BooleanSupplier owned;
|
|
private final BooleanSupplier equipped;
|
|
private final Runnable onEquip;
|
|
private final Runnable onClaim;
|
|
|
|
private long claimedAt = 0;
|
|
|
|
/**
|
|
* @param previewAspect the preview texture's width divided by its height
|
|
*/
|
|
public CosmeticTile(String name, IdentifierRef preview, float previewAspect, int width, BooleanSupplier owned,
|
|
BooleanSupplier equipped, Runnable onEquip, Runnable onClaim) {
|
|
this.name = name;
|
|
this.preview = preview;
|
|
this.previewAspect = previewAspect;
|
|
this.owned = owned;
|
|
this.equipped = equipped;
|
|
this.onEquip = onEquip;
|
|
this.onClaim = onClaim;
|
|
this.width = width;
|
|
this.height = HEIGHT;
|
|
}
|
|
|
|
@Override
|
|
public void render(RenderScope renderScope, ElementContext ctx) {
|
|
boolean isOwned = owned.getAsBoolean();
|
|
boolean isEquipped = equipped.getAsBoolean();
|
|
boolean hovered = ctx.isHovering();
|
|
|
|
int border = isEquipped ? Theme.ACCENT.value
|
|
: Theme.withAlpha(hovered ? 0.22f : 0.06f, Theme.FOREGROUND.value);
|
|
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, isEquipped ? 2 : 1,
|
|
Theme.withAlpha(0.72f, Theme.PRIMARY.value), border);
|
|
|
|
// Preview, fitted into its area
|
|
int areaWidth = width - INSET * 2;
|
|
renderScope.drawRoundedRectangle(INSET, INSET, areaWidth, IMAGE_AREA, Theme.WIDGET_RADIUS.value - 2,
|
|
Theme.withAlpha(0.05f, Theme.FOREGROUND.value));
|
|
|
|
int imageHeight = IMAGE_AREA - 12;
|
|
int imageWidth = (int) (imageHeight * previewAspect);
|
|
if (imageWidth > areaWidth - 12) {
|
|
imageWidth = areaWidth - 12;
|
|
imageHeight = (int) (imageWidth / previewAspect);
|
|
}
|
|
|
|
renderScope.drawTexture(preview, INSET + (areaWidth - imageWidth) / 2, INSET + (IMAGE_AREA - imageHeight) / 2,
|
|
0, 0, imageWidth, imageHeight, isOwned ? 0xFFFFFFFF : Theme.withAlpha(0.35f, 0xFFFFFFFF));
|
|
|
|
// Name
|
|
int nameY = INSET + IMAGE_AREA + (HEIGHT - INSET - IMAGE_AREA - (int) (Fonts.getHeight() * NAME_SCALE)) / 2;
|
|
renderScope.drawText(NAME_SCALE, Fonts.ellipsize(name, Theme.FONT.value, NAME_SCALE, width - 24), 12, nameY,
|
|
Theme.FONT.value, isOwned ? Theme.FOREGROUND.value : Theme.getMutedFg());
|
|
|
|
// Badge in the preview's corner
|
|
if (isEquipped) {
|
|
drawBadge(renderScope, "On", Theme.ACCENT.value, Theme.ACCENT_FG.value, true);
|
|
} else if (!isOwned) {
|
|
drawBadge(renderScope, isClaiming() ? "Claiming" : "Get", hovered ? Theme.ACCENT.value : Theme.PRIMARY.value,
|
|
hovered ? Theme.ACCENT_FG.value : Theme.PRIMARY_FG.value, false);
|
|
}
|
|
}
|
|
|
|
private void drawBadge(RenderScope renderScope, String text, int background, int foreground, boolean check) {
|
|
int icon = check ? 10 : 0;
|
|
int badgeWidth = 14 + icon + (check ? 4 : 0) + (int) (Fonts.getWidth(text, Theme.FONT.value) * BADGE_SCALE);
|
|
int x = width - INSET - 6 - badgeWidth;
|
|
int y = INSET + 6;
|
|
|
|
renderScope.drawRoundedRectangle(x, y, badgeWidth, BADGE_HEIGHT, 6, background);
|
|
|
|
if (check) {
|
|
renderScope.drawTexture(Textures.ICON_CHECK, x + 7, y + (BADGE_HEIGHT - icon) / 2, 0, 0, icon, icon, foreground);
|
|
}
|
|
|
|
int textY = y + (BADGE_HEIGHT - (int) (Fonts.getHeight() * BADGE_SCALE)) / 2;
|
|
renderScope.drawText(BADGE_SCALE, text, x + 7 + icon + (check ? 4 : 0), textY, Theme.FONT.value, foreground);
|
|
}
|
|
|
|
@Override
|
|
public void click(int mouseX, int mouseY) {
|
|
if (owned.getAsBoolean()) {
|
|
if (!equipped.getAsBoolean()) {
|
|
onEquip.run();
|
|
}
|
|
} else if (!isClaiming()) {
|
|
claimedAt = System.currentTimeMillis();
|
|
onClaim.run();
|
|
}
|
|
}
|
|
|
|
private boolean isClaiming() {
|
|
return System.currentTimeMillis() - claimedAt < CLAIM_TIMEOUT;
|
|
}
|
|
}
|