diff options
5 files changed, 436 insertions, 1 deletions
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/client/modmenu/ConfigGui.java b/src/main/java/io/github/cottonmc/cotton/gui/client/modmenu/ConfigGui.java index 23c5b6f..b8abd22 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/client/modmenu/ConfigGui.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/client/modmenu/ConfigGui.java @@ -30,7 +30,21 @@ public class ConfigGui extends LightweightGuiDescription { WTextField testField = new WTextField(); testField.setSuggestion("test"); root.add(testField, 0, 3, 4, 1); - + + /* + WSlider verticalSlider = new WSlider(-100, 100, Axis.VERTICAL); + verticalSlider.setDraggingFinishedListener(() -> System.out.println("Mouse released")); + verticalSlider.setValueChangeListener(System.out::println); + + WLabeledSlider horizontalSlider = new WLabeledSlider(0, 500); + horizontalSlider.setLabelUpdater(value -> new LiteralText(value + "!")); + horizontalSlider.setDraggingFinishedListener(() -> System.out.println("Mouse released")); + horizontalSlider.setValue(250); + + root.add(verticalSlider, 6, 0, 1, 3); + root.add(horizontalSlider, 1, 4, 4, 1); + */ + root.add(new WKirbSprite(), 5, 4); WButton doneButton = new WButton(new TranslatableText("gui.done")); diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WAbstractSlider.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WAbstractSlider.java new file mode 100644 index 0000000..074371f --- /dev/null +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WAbstractSlider.java @@ -0,0 +1,210 @@ +package io.github.cottonmc.cotton.gui.widget; + +import net.minecraft.util.math.MathHelper; +import org.lwjgl.glfw.GLFW; + +import javax.annotation.Nullable; +import java.util.function.IntConsumer; + +/** + * A base class for slider widgets that can be used to select int values. + * + * <p>You can set two listeners on a slider: + * <ul> + * <li> + * A value change listener that gets all value changes (including direct setValue calls). + * </li> + * <li> + * A dragging finished listener that gets called when the player stops dragging the slider + * or modifies the value with the keyboard. + * For example, this can be used for sending sync packets to the server + * when the player has selected a value. + * </li> + * </ul> + */ +public abstract class WAbstractSlider extends WWidget { + protected final int min, max; + protected final Axis axis; + + protected int value; + + /** + * True if the user is currently dragging the thumb. + * Used for visuals. + */ + protected boolean dragging = false; + + /** + * A value:coordinate ratio. Used for converting user input into values. + */ + protected float valueToCoordRatio; + + /** + * A coordinate:value ratio. Used for rendering the thumb. + */ + protected float coordToValueRatio; + + /** + * True if there is a pending dragging finished event caused by the keyboard. + */ + private boolean valueChangedWithKeys = false; + + @Nullable private IntConsumer valueChangeListener = null; + @Nullable private Runnable draggingFinishedListener = null; + + protected WAbstractSlider(int min, int max, Axis axis) { + if (max <= min) + throw new IllegalArgumentException("Minimum value must be smaller than the maximum!"); + + this.min = min; + this.max = max; + this.axis = axis; + this.value = min; + } + + /** + * @return the thumb size along the slider axis + */ + protected abstract int getThumbWidth(); + + /** + * Checks if the mouse cursor is close enough to the slider that the user can start dragging. + * + * @param x the mouse x position + * @param y the mouse y position + * @return if the cursor is inside dragging bounds + */ + protected abstract boolean isMouseInsideBounds(int x, int y); + + @Override + public void setSize(int x, int y) { + super.setSize(x, y); + int trackHeight = (axis == Axis.HORIZONTAL ? x : y) - getThumbWidth(); + valueToCoordRatio = (float) (max - min) / trackHeight; + coordToValueRatio = 1 / valueToCoordRatio; + } + + @Override + public boolean canResize() { + return true; + } + + @Override + public boolean canFocus() { + return true; + } + + @Override + public WWidget onMouseDown(int x, int y, int button) { + // Check if cursor is inside or <=2px away from track + if (isMouseInsideBounds(x, y)) { + requestFocus(); + } + return super.onMouseDown(x, y, button); + } + + @Override + public void onMouseDrag(int x, int y, int button) { + if (isFocused()) { + dragging = true; + moveSlider(x, y); + } + } + + @Override + public void onClick(int x, int y, int button) { + moveSlider(x, y); + if (draggingFinishedListener != null) draggingFinishedListener.run(); + } + + private void moveSlider(int x, int y) { + int pos = (axis == Axis.VERTICAL ? (height - y) : x) - getThumbWidth() / 2; + int rawValue = min + Math.round(valueToCoordRatio * pos); + int previousValue = value; + value = MathHelper.clamp(rawValue, min, max); + if (value != previousValue) onValueChanged(value); + } + + @Override + public WWidget onMouseUp(int x, int y, int button) { + dragging = false; + if (draggingFinishedListener != null) draggingFinishedListener.run(); + return super.onMouseUp(x, y, button); + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + onValueChanged(value); + } + + public void setValueChangeListener(@Nullable IntConsumer valueChangeListener) { + this.valueChangeListener = valueChangeListener; + } + + public void setDraggingFinishedListener(@Nullable Runnable draggingFinishedListener) { + this.draggingFinishedListener = draggingFinishedListener; + } + + public int getMinValue() { + return min; + } + + public int getMaxValue() { + return max; + } + + public Axis getAxis() { + return axis; + } + + protected void onValueChanged(int value) { + if (valueChangeListener != null) valueChangeListener.accept(value); + } + + @Override + public void onKeyPressed(int ch, int key, int modifiers) { + boolean valueChanged = false; + if (modifiers == 0) { + if (isDecreasingKey(ch) && value > min) { + value--; + valueChanged = true; + } else if (isIncreasingKey(ch) && value < max) { + value++; + valueChanged = true; + } + } else if (modifiers == GLFW.GLFW_MOD_CONTROL) { + if (isDecreasingKey(ch) && value != min) { + value = min; + valueChanged = true; + } else if (isIncreasingKey(ch) && value != max) { + value = max; + valueChanged = true; + } + } + + if (valueChanged) { + onValueChanged(value); + valueChangedWithKeys = true; + } + } + + @Override + public void onKeyReleased(int ch, int key, int modifiers) { + if (valueChangedWithKeys && (isDecreasingKey(ch) || isIncreasingKey(ch))) { + if (draggingFinishedListener != null) draggingFinishedListener.run(); + valueChangedWithKeys = false; + } + } + + private static boolean isDecreasingKey(int ch) { + return ch == GLFW.GLFW_KEY_LEFT || ch == GLFW.GLFW_KEY_DOWN; + } + + private static boolean isIncreasingKey(int ch) { + return ch == GLFW.GLFW_KEY_RIGHT || ch == GLFW.GLFW_KEY_UP; + } +} diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabeledSlider.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabeledSlider.java new file mode 100644 index 0000000..37fe464 --- /dev/null +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WLabeledSlider.java @@ -0,0 +1,116 @@ +package io.github.cottonmc.cotton.gui.widget; + +import io.github.cottonmc.cotton.gui.client.ScreenDrawing; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.gui.widget.AbstractButtonWidget; +import net.minecraft.text.Text; + +import javax.annotation.Nullable; + +/** + * A vanilla-style labeled slider widget. + * + * <p>In addition to the standard slider listeners, + * labeled sliders also support "label updaters" that can update the label + * when the value is changed. + * + * @see WAbstractSlider for more information about listeners + */ +public class WLabeledSlider extends WAbstractSlider { + @Nullable private Text label = null; + @Nullable private LabelUpdater labelUpdater = null; + + public WLabeledSlider(int min, int max) { + super(min, max, Axis.HORIZONTAL); + } + + public WLabeledSlider(int min, int max, Text label) { + this(min, max); + this.label = label; + } + + + @Override + public void setSize(int x, int y) { + super.setSize(x, 20); + } + + @Nullable + public Text getLabel() { + return label; + } + + public void setLabel(@Nullable Text label) { + this.label = label; + } + + @Override + protected void onValueChanged(int value) { + super.onValueChanged(value); + if (labelUpdater != null) { + label = labelUpdater.updateLabel(value); + } + } + + public void setLabelUpdater(@Nullable LabelUpdater labelUpdater) { + this.labelUpdater = labelUpdater; + } + + @Override + protected int getThumbWidth() { + return 8; + } + + @Override + protected boolean isMouseInsideBounds(int x, int y) { + return x >= 0 && x <= width && y >= 0 && y <= height; + } + + @Environment(EnvType.CLIENT) + @Override + public void paintBackground(int x, int y, int mouseX, int mouseY) { + drawButton(x, y, 0, width); + + // 1: regular, 2: hovered, 0: disabled/dragging + int thumbX = Math.round(coordToValueRatio * (value - min)); + int thumbY = 0; + int thumbWidth = getThumbWidth(); + int thumbHeight = height; + boolean hovering = mouseX >= thumbX && mouseX <= thumbX + thumbWidth && mouseY >= thumbY && mouseY <= thumbY + thumbHeight; + int thumbState = dragging || hovering ? 2 : 1; + + drawButton(x + thumbX, y + thumbY, thumbState, thumbWidth); + + if (thumbState == 1 && isFocused()) { + float px = 1 / 32f; + ScreenDrawing.rect(WSlider.TEXTURE, x + thumbX, y + thumbY, thumbWidth, thumbHeight, 24*px, 0*px, 32*px, 20*px, 0xFFFFFFFF); + } + + if (label != null) { + int color = isMouseInsideBounds(mouseX, mouseY) ? 0xFFFFA0 : 0xE0E0E0; + ScreenDrawing.drawCenteredWithShadow(label.asFormattedString(), x + width / 2, y + height / 2 - 4, color); + } + } + + // state = 1: regular, 2: hovered, 0: disabled/dragging + @Environment(EnvType.CLIENT) + private void drawButton(int x, int y, int state, int width) { + float px = 1 / 256f; + float buttonLeft = 0 * px; + float buttonTop = (46 + (state * 20)) * px; + int halfWidth = width / 2; + if (halfWidth > 198) halfWidth = 198; + float buttonWidth = halfWidth * px; + float buttonHeight = 20 * px; + float buttonEndLeft = (200 - halfWidth) * px; + + ScreenDrawing.rect(AbstractButtonWidget.WIDGETS_LOCATION, x, y, halfWidth, 20, buttonLeft, buttonTop, buttonLeft + buttonWidth, buttonTop + buttonHeight, 0xFFFFFFFF); + ScreenDrawing.rect(AbstractButtonWidget.WIDGETS_LOCATION, x + halfWidth, y, halfWidth, 20, buttonEndLeft, buttonTop, 200 * px, buttonTop + buttonHeight, 0xFFFFFFFF); + } + + @FunctionalInterface + public interface LabelUpdater { + Text updateLabel(int value); + } +} diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WSlider.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WSlider.java new file mode 100644 index 0000000..c57b705 --- /dev/null +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WSlider.java @@ -0,0 +1,95 @@ +package io.github.cottonmc.cotton.gui.widget; + +import io.github.cottonmc.cotton.gui.client.BackgroundPainter; +import io.github.cottonmc.cotton.gui.client.ScreenDrawing; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.util.Identifier; + +import javax.annotation.Nullable; + +/** + * A simple slider widget that can be used to select int values. + * + * @see WAbstractSlider for supported listeners + */ +public class WSlider extends WAbstractSlider { + public static final int TRACK_WIDTH = 6; + public static final int THUMB_SIZE = 8; + public static final Identifier TEXTURE = new Identifier("libgui", "textures/widget/slider.png"); + + @Environment(EnvType.CLIENT) + @Nullable + private BackgroundPainter backgroundPainter = null; + + public WSlider(int min, int max, Axis axis) { + super(min, max, axis); + } + + public WSlider(int max, Axis axis) { + this(0, max, axis); + } + + @Override + protected int getThumbWidth() { + return THUMB_SIZE; + } + + @Override + protected boolean isMouseInsideBounds(int x, int y) { + // ao = axis-opposite mouse coordinate, aoCenter = center of ao's axis + int ao = axis == Axis.HORIZONTAL ? y : x; + int aoCenter = (axis == Axis.HORIZONTAL ? height : width) / 2; + + // Check if cursor is inside or <=2px away from track + return ao >= aoCenter - TRACK_WIDTH / 2 - 2 && ao <= aoCenter + TRACK_WIDTH / 2 + 2; + } + + @Environment(EnvType.CLIENT) + @Override + public void paintBackground(int x, int y, int mouseX, int mouseY) { + if (backgroundPainter != null) { + backgroundPainter.paintBackground(x, y, this); + } else { + float px = 1 / 32f; + // thumbX/Y: thumb position in widget-space + int thumbX, thumbY; + // thumbXOffset: thumb texture x offset in pixels + int thumbXOffset; + + if (axis == Axis.VERTICAL) { + int trackX = x + width / 2 - TRACK_WIDTH / 2; + thumbX = width / 2 - THUMB_SIZE / 2; + thumbY = height - THUMB_SIZE + 1 - (int) (coordToValueRatio * (value - min)); + thumbXOffset = 0; + + ScreenDrawing.rect(TEXTURE, trackX, y + 1, TRACK_WIDTH, 1, 16*px, 0*px, 22*px, 1*px, 0xFFFFFFFF); + ScreenDrawing.rect(TEXTURE, trackX, y + 2, TRACK_WIDTH, height - 2, 16*px, 1*px, 22*px, 2*px, 0xFFFFFFFF); + ScreenDrawing.rect(TEXTURE, trackX, y + height, TRACK_WIDTH, 1, 16*px, 2*px, 22*px, 3*px, 0xFFFFFFFF); + } else { + int trackY = y + height / 2 - TRACK_WIDTH / 2; + thumbX = Math.round(coordToValueRatio * (value - min)); + thumbY = height / 2 - THUMB_SIZE / 2; + thumbXOffset = 8; + + ScreenDrawing.rect(TEXTURE, x, trackY, 1, TRACK_WIDTH, 16*px, 3*px, 17*px, 9*px, 0xFFFFFFFF); + ScreenDrawing.rect(TEXTURE, x + 1, trackY, width - 2, TRACK_WIDTH, 17*px, 3*px, 18*px, 9*px, 0xFFFFFFFF); + ScreenDrawing.rect(TEXTURE, x + width - 1, trackY, 1, TRACK_WIDTH, 18*px, 3*px, 19*px, 9*px, 0xFFFFFFFF); + } + + // thumbState values: + // 0: default, 1: dragging, 2: hovered + int thumbState = dragging ? 1 : (mouseX >= thumbX && mouseX <= thumbX + THUMB_SIZE && mouseY >= thumbY && mouseY <= thumbY + THUMB_SIZE ? 2 : 0); + ScreenDrawing.rect(TEXTURE, x + thumbX, y + thumbY, THUMB_SIZE, THUMB_SIZE, thumbXOffset*px, 0*px + thumbState * 8*px, (thumbXOffset + 8)*px, 8*px + thumbState * 8*px, 0xFFFFFFFF); + + if (thumbState == 0 && isFocused()) { + ScreenDrawing.rect(TEXTURE, x + thumbX, y + thumbY, THUMB_SIZE, THUMB_SIZE, 0*px, 24*px, 8*px, 32*px, 0xFFFFFFFF); + } + } + } + + @Environment(EnvType.CLIENT) + public void setBackgroundPainter(BackgroundPainter backgroundPainter) { + this.backgroundPainter = backgroundPainter; + } +} diff --git a/src/main/resources/assets/libgui/textures/widget/slider.png b/src/main/resources/assets/libgui/textures/widget/slider.png Binary files differnew file mode 100644 index 0000000..e41250e --- /dev/null +++ b/src/main/resources/assets/libgui/textures/widget/slider.png |