Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 158 additions & 93 deletions src/client/java/com/example/itemtweaks/client/SwingSettingsScreen.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,85 +5,102 @@

import com.example.itemtweaks.ItemTweaksConfig;

import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.AbstractSliderButton;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.components.StringWidget;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import net.minecraft.util.Mth;

/** Dark in-game panel: sliders + typed values for swing speed and held-item scale. */
/** Settings panel for swing speed and held-item scale. Non-pausing so the player sees changes live. */
public class SwingSettingsScreen extends Screen {

// --- Layout ---
private static final int PANEL_W = 320;
private static final int PANEL_H = 196;
private static final int SLIDER_W = 200;
private static final int BOX_W = 52;
private static final int ROW_H = 20;
private static final int BACKDROP = 0xC8080810;
private static final int PANEL = 0xFF16161C;
private static final int PANEL_EDGE = 0xFF3D3D4A;

private boolean syncing;
private FloatSlider swingSlider;
private FloatSlider scaleSlider;
private EditBox swingBox;
private EditBox scaleBox;
private static final int PANEL_H = 178;
private static final int PAD = 18; // horizontal inset on both sides
// Slider + 8px gap + box fills the inner width exactly: 320 - 2*18 = 284 = 224 + 8 + 52
private static final int SLIDER_W = 224;
private static final int BOX_W = 52;
private static final int CTRL_H = 20;
private static final int BTN_W = 84;
private static final int BTN_GAP = 8;

// Vertical offsets from panel top
private static final int TITLE_Y = 14;
private static final int ROW1_LABEL_Y = 38;
private static final int ROW1_CTRL_Y = 52;
private static final int ROW2_LABEL_Y = 88;
private static final int ROW2_CTRL_Y = 102;
private static final int SEPARATOR_Y = 138;
private static final int BTN_Y = 148;

// --- Colors (ARGB) ---
private static final int COL_BACKDROP = 0xB0060609;
private static final int COL_PANEL = 0xFF0F0F14;
private static final int COL_PANEL_INNER = 0xFF18181E; // 1-shade lighter inset, gives depth
private static final int COL_ACCENT = 0xFF8BDDFF; // soft cyan — the only hue on the panel
private static final int COL_SEPARATOR = 0xFF262630;
private static final int COL_TITLE = 0xFFFFFFFF;
private static final int COL_LABEL = 0xFF8888A0; // muted so labels step back from controls
private static final int COL_TEXT = 0xFFE8E8EE;
private static final int COL_TRACK_BG = 0xFF1A1A22;
private static final int COL_TRACK_FILL = 0xFF2C6A8A;
private static final int COL_HANDLE = 0xFF8BDDFF;
private static final int COL_HANDLE_HOV = 0xFFBDEEFF;
private static final int COL_SLIDER_TEXT = 0xFFD0D0DC;
private static final int HANDLE_W = 6;

// --- State ---
private boolean syncing; // prevents slider <-> box feedback loops
private boolean resetArmed; // true after first Reset click; second click confirms
private FloatSlider swingSlider, scaleSlider;
private EditBox swingBox, scaleBox;
private Button resetBtn;

public SwingSettingsScreen() {
super(Component.literal("Swing"));
super(Component.literal("Swing Settings"));
}

@Override
protected void init() {
int x = panelX();
int y = panelY();
int inner = x + 16;

addRenderableWidget(new StringWidget(x, y + 12, PANEL_W, 12,
Component.literal("Swing"), this.font));

int row1 = y + 44;
addRenderableWidget(new StringWidget(inner, row1 - 14, SLIDER_W, 12,
Component.literal("Swing speed"), this.font));
this.swingSlider = addRenderableWidget(new FloatSlider(inner, row1, SLIDER_W, ROW_H,
"Swing speed", ItemTweaksConfig.SWING_SPEED_MULTIPLIER, this::applySwing));
this.swingBox = addBox(inner + SLIDER_W + 8, row1, ItemTweaksConfig.SWING_SPEED_MULTIPLIER, this::applySwing);

int row2 = y + 96;
addRenderableWidget(new StringWidget(inner, row2 - 14, SLIDER_W, 12,
Component.literal("Held item scale"), this.font));
this.scaleSlider = addRenderableWidget(new FloatSlider(inner, row2, SLIDER_W, ROW_H,
"Held item scale", ItemTweaksConfig.HELD_ITEM_SCALE, this::applyScale));
this.scaleBox = addBox(inner + SLIDER_W + 8, row2, ItemTweaksConfig.HELD_ITEM_SCALE, this::applyScale);

int by = y + PANEL_H - 36;
int bw = 88;
addRenderableWidget(Button.builder(Component.literal("Reset"), b -> reset())
.bounds(x + 16, by, bw, ROW_H).build());
addRenderableWidget(Button.builder(Component.literal("Apply"), b -> ItemTweaksConfig.save())
.bounds(x + (PANEL_W - bw) / 2, by, bw, ROW_H).build());
int px = panelX(), py = panelY();
int inner = px + PAD;

int r1y = py + ROW1_CTRL_Y;
this.swingSlider = addRenderableWidget(new FloatSlider(inner, r1y, SLIDER_W, CTRL_H,
"Swing speed", ItemTweaksConfig.SWING_SPEED_MULTIPLIER, this::applySwing, this.font));
this.swingBox = addBox(inner + SLIDER_W + 8, r1y, ItemTweaksConfig.SWING_SPEED_MULTIPLIER, this::applySwing);

int r2y = py + ROW2_CTRL_Y;
this.scaleSlider = addRenderableWidget(new FloatSlider(inner, r2y, SLIDER_W, CTRL_H,
"Held item scale", ItemTweaksConfig.HELD_ITEM_SCALE, this::applyScale, this.font));
this.scaleBox = addBox(inner + SLIDER_W + 8, r2y, ItemTweaksConfig.HELD_ITEM_SCALE, this::applyScale);

// Two buttons centered: Reset and Close (Apply removed — onClose auto-saves)
int totalBtnW = BTN_W * 2 + BTN_GAP;
int btnLeft = px + (PANEL_W - totalBtnW) / 2;
int by = py + BTN_Y;
this.resetBtn = addRenderableWidget(Button.builder(Component.literal("Reset"), b -> onResetClick())
.bounds(btnLeft, by, BTN_W, CTRL_H).build());
addRenderableWidget(Button.builder(Component.literal("Close"), b -> this.onClose())
.bounds(x + PANEL_W - 16 - bw, by, bw, ROW_H).build());
.bounds(btnLeft + BTN_W + BTN_GAP, by, BTN_W, CTRL_H).build());
}

/** Creates a numeric text box that feeds into {@code apply} on every keystroke. */
private EditBox addBox(int x, int y, float value, DoubleConsumer apply) {
EditBox box = new EditBox(this.font, x, y, BOX_W, ROW_H, Component.literal("value"));
EditBox box = new EditBox(this.font, x, y, BOX_W, CTRL_H, Component.literal("value"));
box.setMaxLength(4);
box.setTextColor(0xFFE8E8EE);
box.setTextColor(COL_TEXT);
this.syncing = true;
box.setValue(fmt(value));
this.syncing = false;
box.setResponder(text -> {
if (this.syncing) {
return;
}
try {
apply.accept(Float.parseFloat(text));
} catch (NumberFormatException ignored) {
// Keep typing; last valid value stays applied.
}
if (this.syncing) return;
try { apply.accept(Float.parseFloat(text)); }
catch (NumberFormatException ignored) {} // user still typing, wait it out
});
return addRenderableWidget(box);
}
Expand All @@ -101,20 +118,23 @@ private void applyScale(double raw) {
}

private void sync(FloatSlider slider, EditBox box, float v) {
if (this.syncing) {
return;
}
if (this.syncing) return;
this.syncing = true;
if (slider != null) {
slider.setFromFloat(v);
}
if (box != null && !box.isFocused()) {
box.setValue(fmt(v));
}
if (slider != null) slider.setFromFloat(v);
if (box != null && !box.isFocused()) box.setValue(fmt(v)); // don't interrupt active typing
this.syncing = false;
}

private void reset() {
/** First click arms Reset ("Confirm?"); second click within the same session resets. */
private void onResetClick() {
if (!this.resetArmed) {
this.resetArmed = true;
this.resetBtn.setMessage(Component.literal("Confirm?"));
return;
}
// Second click — do the actual reset
this.resetArmed = false;
this.resetBtn.setMessage(Component.literal("Reset"));
ItemTweaksConfig.resetToVanilla();
this.syncing = true;
this.swingSlider.setFromFloat(ItemTweaksConfig.SWING_SPEED_MULTIPLIER);
Expand All @@ -130,63 +150,108 @@ public void onClose() {
super.onClose();
}

@Override
public boolean mouseClicked(net.minecraft.client.input.MouseButtonEvent event, boolean b) {
// If the player clicks anywhere other than the reset button while it's armed, disarm it.
if (this.resetArmed && !this.resetBtn.isMouseOver(event.x(), event.y())) {
this.resetArmed = false;
this.resetBtn.setMessage(Component.literal("Reset"));
}
return super.mouseClicked(event, b);
}

@Override
public boolean isPauseScreen() {
return false;
return false; // game keeps ticking so the player sees changes live
}

@Override
public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) {
if (this.minecraft.level != null) {
extractBlurredBackground(graphics);
}
graphics.fill(0, 0, this.width, this.height, BACKDROP);
int x = panelX();
int y = panelY();
graphics.fill(x, y, x + PANEL_W, y + PANEL_H, PANEL);
graphics.fill(x, y, x + PANEL_W, y + 1, PANEL_EDGE);
if (this.minecraft.level != null) extractBlurredBackground(graphics);
// Translucent veil — dark enough to frame the panel, light enough to see the world.
graphics.fill(0, 0, this.width, this.height, COL_BACKDROP);
renderPanel(graphics);
renderLabels(graphics);
this.minecraft.gui.hud.extractDeferredSubtitles();
}

private int panelX() {
return (this.width - PANEL_W) / 2;
}
/** Three-layer panel: dark shell → lighter inner zone → 2px cyan accent stripe → separator. */
private void renderPanel(GuiGraphicsExtractor graphics) {
int px = panelX(), py = panelY();
int pr = px + PANEL_W, pb = py + PANEL_H;

private int panelY() {
return (this.height - PANEL_H) / 2;
graphics.fill(px, py, pr, pb, COL_PANEL);
// Inner zone inset 1px; top starts after accent stripe so it isn't overdrawn.
graphics.fill(px + 1, py + 3, pr - 1, pb - 1, COL_PANEL_INNER);
graphics.fill(px, py, pr, py + 2, COL_ACCENT);
// Thin separator between the settings area and the button row.
graphics.fill(px + PAD, py + SEPARATOR_Y, pr - PAD, py + SEPARATOR_Y + 1, COL_SEPARATOR);
}

private static String fmt(float v) {
return String.format(Locale.US, "%.2f", v);
/** Title (centered) and the two muted section labels drawn directly in the background pass. */
private void renderLabels(GuiGraphicsExtractor graphics) {
int px = panelX(), py = panelY();
String title = "Swing Settings";
graphics.text(this.font, title, px + (PANEL_W - this.font.width(title)) / 2,
py + TITLE_Y, COL_TITLE, true);
graphics.text(this.font, "Swing Speed", px + PAD, py + ROW1_LABEL_Y, COL_LABEL, false);
graphics.text(this.font, "Held Item Scale", px + PAD, py + ROW2_LABEL_Y, COL_LABEL, false);
}

private int panelX() { return (this.width - PANEL_W) / 2; }
private int panelY() { return (this.height - PANEL_H) / 2; }

private static String fmt(float v) { return String.format(Locale.US, "%.2f", v); }


/**
* Flat custom-rendered slider
* Track is three rects: dark background, cyan active fill, bright handle knob.
* Handle brightens on hover. Value is drawn centered on the track.
*/
private static final class FloatSlider extends AbstractSliderButton {
private final String label;
private final String label;
private final DoubleConsumer onChange;
private final Font font; // needed for text width + rendering inside extractWidgetRenderState

FloatSlider(int x, int y, int w, int h, String label, float current, DoubleConsumer onChange) {
FloatSlider(int x, int y, int w, int h, String label, float current,
DoubleConsumer onChange, Font font) {
super(x, y, w, h, Component.literal(label + ": " + fmt(current)),
Mth.clamp((current - ItemTweaksConfig.MIN) / (ItemTweaksConfig.MAX - ItemTweaksConfig.MIN), 0.0, 1.0));
this.label = label;
this.onChange = onChange;
Mth.clamp((double)(current - ItemTweaksConfig.MIN)
/ (ItemTweaksConfig.MAX - ItemTweaksConfig.MIN), 0.0, 1.0));
this.label = label; this.onChange = onChange; this.font = font;
}

private float toFloat() {
return ItemTweaksConfig.MIN + (float)(this.value * (ItemTweaksConfig.MAX - ItemTweaksConfig.MIN));
}

void setFromFloat(float v) {
setValue((v - ItemTweaksConfig.MIN) / (ItemTweaksConfig.MAX - ItemTweaksConfig.MIN));
setValue((double)(v - ItemTweaksConfig.MIN) / (ItemTweaksConfig.MAX - ItemTweaksConfig.MIN));
}

@Override
protected void updateMessage() {
@Override protected void updateMessage() {
setMessage(Component.literal(this.label + ": " + fmt(ItemTweaksConfig.clamp(toFloat()))));
}

@Override protected void applyValue() { this.onChange.accept(toFloat()); }

@Override
protected void applyValue() {
this.onChange.accept(toFloat());
}
public void extractWidgetRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float pt) {
int x = getX(), y = getY(), w = getWidth(), h = getHeight();
int handleX = x + (int) Mth.clamp(this.value * (w - HANDLE_W), 0, w - HANDLE_W);

private float toFloat() {
return ItemTweaksConfig.MIN + (float) (this.value * (ItemTweaksConfig.MAX - ItemTweaksConfig.MIN));
graphics.fill(x, y, x + w, y + h, COL_TRACK_BG);
if (handleX > x) graphics.fill(x, y, handleX, y + h, COL_TRACK_FILL);
// Handle is 1px taller than the track so it looks like it's lifted above the surface.
graphics.fill(handleX, y - 1, handleX + HANDLE_W, y + h + 1,
isHovered() ? COL_HANDLE_HOV : COL_HANDLE);

String text = this.label + ": " + fmt(ItemTweaksConfig.clamp(toFloat()));
graphics.text(this.font, text,
x + (w - this.font.width(text)) / 2,
y + (h - 8) / 2, // vertically center the 8px-tall MC font
COL_SLIDER_TEXT, true);
}
}
}
Loading