Redesign the mod menu
The mod menu is now a translucent panel beside a navigation rail, with a header showing how many mods are enabled, a search box, category filters built from the mods' tags and a three-column grid of mod cards. Each card shows the mod's icon, name and description with a settings button and an on/off switch. The layout shrinks to fit smaller windows. New reusable pieces: Panel, Label, ChipGroup, SearchInput, Toggle.drawSwitch, Theme helpers for panel, border, muted text and body font, and Fonts.ellipsize. The sidebar and card icons are Lucide SVGs, and seven mod icons were redrawn in the same style. Scroll can skip its background and be cleared, and no longer passes clicks outside its bounds to children scrolled out of view. Input checks overflow and places the cursor at the scale its text is drawn at. Screens that are not redesigned yet move the wider sidebar so it keeps its gap. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -11,6 +11,11 @@ public abstract class Input extends Element {
|
||||
String text = "";
|
||||
public int cursorPosition = 0;
|
||||
|
||||
/** Scale the text is drawn at */
|
||||
protected float textScale = 0.6f;
|
||||
/** Horizontal inset of the text on both sides */
|
||||
protected int textX = 4;
|
||||
|
||||
public abstract void checkReset();
|
||||
|
||||
public abstract void backspace();
|
||||
@@ -41,6 +46,7 @@ public abstract class Input extends Element {
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
mouseX -= textX;
|
||||
int scrollOffset = getScrollOffset();
|
||||
String visibleText = text.substring(scrollOffset);
|
||||
|
||||
@@ -48,7 +54,7 @@ public abstract class Input extends Element {
|
||||
float textWidth = 0;
|
||||
|
||||
for (int i = 0; i < visibleText.length(); i++) {
|
||||
textWidth = Fonts.getWidth(visibleText.substring(0, i + 1), Theme.FONT.value) * 0.6f;
|
||||
textWidth = Fonts.getWidth(visibleText.substring(0, i + 1), Theme.FONT.value) * textScale;
|
||||
|
||||
if (textWidth > mouseX) {
|
||||
break;
|
||||
@@ -68,24 +74,32 @@ public abstract class Input extends Element {
|
||||
|
||||
int scrollOffset = getScrollOffset();
|
||||
String visibleText = getVisibleText(scrollOffset);
|
||||
renderScope.drawText(0.6f, visibleText, 4, 4, Theme.FONT.value, Theme.PRIMARY_FG.value);
|
||||
renderScope.drawText(textScale, visibleText, textX, 4, Theme.FONT.value, Theme.PRIMARY_FG.value);
|
||||
|
||||
if (focused) {
|
||||
int cursorX = 4 +
|
||||
(int) (Fonts.getWidth(visibleText.substring(0, cursorPosition - scrollOffset), Theme.FONT.value)
|
||||
* 0.6f);
|
||||
int cursorX = getCursorX(visibleText, scrollOffset);
|
||||
|
||||
renderScope.drawRectangle(cursorX, 2, 1, height - 4, Theme.ACCENT.value);
|
||||
}
|
||||
}
|
||||
|
||||
protected int getCursorX(String visibleText, int scrollOffset) {
|
||||
return textX + (int) (Fonts.getWidth(visibleText.substring(0, cursorPosition - scrollOffset),
|
||||
Theme.FONT.value) * textScale);
|
||||
}
|
||||
|
||||
/** Width available to the text, in unscaled font units */
|
||||
private float getTextSpace() {
|
||||
return (width - textX * 2) / textScale;
|
||||
}
|
||||
|
||||
protected int getScrollOffset() {
|
||||
if (text.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int offset = 0;
|
||||
int maxWidth = width - 4; // Available width for text
|
||||
float maxWidth = getTextSpace();
|
||||
while (offset < cursorPosition) { // Ensure cursor remains visible
|
||||
String visibleText = text.substring(offset, cursorPosition);
|
||||
if (Fonts.getWidth(visibleText, Theme.FONT.value) > maxWidth) {
|
||||
@@ -102,7 +116,7 @@ public abstract class Input extends Element {
|
||||
|
||||
// Ensure text does not overflow past the available width
|
||||
for (int i = 1; i <= visibleText.length(); i++) {
|
||||
if (Fonts.getWidth(visibleText.substring(0, i), Theme.FONT.value) > width - 4) {
|
||||
if (Fonts.getWidth(visibleText.substring(0, i), Theme.FONT.value) > getTextSpace()) {
|
||||
return visibleText.substring(0, i - 1); // Trim overflow
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.saturnclient.ui.components.inputs;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.saturnclient.config.Theme;
|
||||
import org.saturnclient.ui.ElementContext;
|
||||
import org.saturnclient.ui.RenderScope;
|
||||
import org.saturnclient.ui.resources.Fonts;
|
||||
import org.saturnclient.ui.resources.Textures;
|
||||
|
||||
/**
|
||||
* A free-text search box with an icon and placeholder, reporting every change.
|
||||
*/
|
||||
public class SearchInput extends Input {
|
||||
private static final int ICON_SIZE = 16;
|
||||
private static final int ICON_X = 12;
|
||||
|
||||
private final String placeholder;
|
||||
private final Consumer<String> onChange;
|
||||
|
||||
public SearchInput(String placeholder, Consumer<String> onChange) {
|
||||
this.placeholder = placeholder;
|
||||
this.onChange = onChange;
|
||||
this.textScale = 0.68f;
|
||||
this.textX = ICON_X + ICON_SIZE + 8;
|
||||
this.width = 240;
|
||||
this.height = 36;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void charTyped(char chr) {
|
||||
if (text.length() >= 64) {
|
||||
return;
|
||||
}
|
||||
|
||||
text = text.substring(0, cursorPosition) + chr + text.substring(cursorPosition);
|
||||
cursorPosition++;
|
||||
onChange.accept(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void backspace() {
|
||||
onChange.accept(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkReset() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(RenderScope renderScope, ElementContext ctx) {
|
||||
int border = focused ? Theme.withAlpha(0.6f, Theme.ACCENT.value)
|
||||
: ctx.isHovering() ? Theme.withAlpha(0.25f, Theme.FOREGROUND.value)
|
||||
: Theme.withAlpha(0.1f, Theme.FOREGROUND.value);
|
||||
|
||||
renderScope.drawRoundedRectangle(0, 0, width, height, Theme.WIDGET_RADIUS.value, 1,
|
||||
Theme.withAlpha(0.06f, Theme.FOREGROUND.value), border);
|
||||
|
||||
renderScope.drawTexture(Textures.ICON_SEARCH, ICON_X, (height - ICON_SIZE) / 2, 0, 0,
|
||||
ICON_SIZE, ICON_SIZE, Theme.getMutedFg());
|
||||
|
||||
int textHeight = (int) (Fonts.getHeight() * textScale);
|
||||
int textY = (height - textHeight) / 2;
|
||||
|
||||
if (text.isEmpty() && !focused) {
|
||||
renderScope.drawText(textScale, placeholder, textX, textY, Theme.getBodyFont(), Theme.getMutedFg());
|
||||
return;
|
||||
}
|
||||
|
||||
int scrollOffset = getScrollOffset();
|
||||
String visibleText = getVisibleText(scrollOffset);
|
||||
renderScope.drawText(textScale, visibleText, textX, textY, Theme.FONT.value, Theme.FOREGROUND.value);
|
||||
|
||||
if (focused) {
|
||||
renderScope.drawRectangle(getCursorX(visibleText, scrollOffset), textY - 1, 1, textHeight + 2,
|
||||
Theme.ACCENT.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,26 @@ public class Toggle extends Element {
|
||||
prop.value ? Theme.ACCENT.value : Theme.PRIMARY.value);
|
||||
}
|
||||
|
||||
public static final int SWITCH_WIDTH = 42;
|
||||
public static final int SWITCH_HEIGHT = 24;
|
||||
|
||||
/**
|
||||
* Draws an on/off switch: a pill track with a round knob on the right when on.
|
||||
*/
|
||||
public static void drawSwitch(RenderScope renderScope, int x, int y, boolean on) {
|
||||
int inset = 3;
|
||||
int knob = SWITCH_HEIGHT - inset * 2;
|
||||
|
||||
renderScope.drawRoundedRectangle(x, y, SWITCH_WIDTH, SWITCH_HEIGHT, SWITCH_HEIGHT / 2,
|
||||
on ? Theme.ACCENT.value : Theme.withAlpha(0.16f, Theme.FOREGROUND.value));
|
||||
|
||||
renderScope.drawRoundedRectangle(
|
||||
on ? x + SWITCH_WIDTH - inset - knob : x + inset,
|
||||
y + inset,
|
||||
knob, knob, knob / 2,
|
||||
on ? Theme.ACCENT_FG.value : Theme.withAlpha(0.8f, Theme.PRIMARY_FG.value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int mouseX, int mouseY) {
|
||||
prop.value = !prop.value;
|
||||
|
||||
Reference in New Issue
Block a user