diff options
Diffstat (limited to 'src')
23 files changed, 613 insertions, 47 deletions
diff --git a/src/main/java/dev/isxander/yacl/api/Binding.java b/src/main/java/dev/isxander/yacl/api/Binding.java index 37514ca..395beb2 100644 --- a/src/main/java/dev/isxander/yacl/api/Binding.java +++ b/src/main/java/dev/isxander/yacl/api/Binding.java @@ -46,4 +46,19 @@ public interface Binding<T> { minecraftOption::setValue ); } + + /** + * Creates an immutable binding that has no default and cannot be modified. + * + * @param value the value for the binding + */ + static <T> Binding<T> immutable(T value) { + Validate.notNull(value, "`value` must not be null"); + + return new GenericBindingImpl<>( + value, + () -> value, + changed -> {} + ); + } } diff --git a/src/main/java/dev/isxander/yacl/api/ConfigCategory.java b/src/main/java/dev/isxander/yacl/api/ConfigCategory.java index 9f2f954..1b2a2bc 100644 --- a/src/main/java/dev/isxander/yacl/api/ConfigCategory.java +++ b/src/main/java/dev/isxander/yacl/api/ConfigCategory.java @@ -138,7 +138,7 @@ public interface ConfigCategory { Validate.notNull(name, "`name` must not be null to build `ConfigCategory`"); List<OptionGroup> combinedGroups = new ArrayList<>(); - combinedGroups.add(new OptionGroupImpl(Text.empty(), Text.empty(), ImmutableList.copyOf(rootOptions), true)); + combinedGroups.add(new OptionGroupImpl(Text.empty(), Text.empty(), ImmutableList.copyOf(rootOptions), false, true)); combinedGroups.addAll(groups); Validate.notEmpty(combinedGroups, "at least one option must be added to build `ConfigCategory`"); diff --git a/src/main/java/dev/isxander/yacl/api/Controller.java b/src/main/java/dev/isxander/yacl/api/Controller.java index 198e5df..1a00920 100644 --- a/src/main/java/dev/isxander/yacl/api/Controller.java +++ b/src/main/java/dev/isxander/yacl/api/Controller.java @@ -1,8 +1,8 @@ package dev.isxander.yacl.api; import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.gui.controllers.ControllerWidget; import net.minecraft.text.Text; import org.jetbrains.annotations.ApiStatus; @@ -26,5 +26,5 @@ public interface Controller<T> { * @param screen parent screen */ @ApiStatus.Internal - ControllerWidget<?> provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension); + AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension); } diff --git a/src/main/java/dev/isxander/yacl/api/Option.java b/src/main/java/dev/isxander/yacl/api/Option.java index 9d6ebe2..a353ae4 100644 --- a/src/main/java/dev/isxander/yacl/api/Option.java +++ b/src/main/java/dev/isxander/yacl/api/Option.java @@ -3,6 +3,7 @@ package dev.isxander.yacl.api; import dev.isxander.yacl.impl.OptionImpl; import net.minecraft.text.MutableText; import net.minecraft.text.Text; +import net.minecraft.util.Formatting; import org.apache.commons.lang3.Validate; import org.jetbrains.annotations.NotNull; @@ -87,7 +88,7 @@ public interface Option<T> { } class Builder<T> { - private Text name; + private Text name = Text.literal("Name not specified!").formatted(Formatting.RED); private final List<Text> tooltipLines = new ArrayList<>(); @@ -172,7 +173,6 @@ public interface Option<T> { } public Option<T> build() { - Validate.notNull(name, "`name` must not be null when building `Option`"); Validate.notNull(controlGetter, "`control` must not be null when building `Option`"); Validate.notNull(binding, "`binding` must not be null when building `Option`"); diff --git a/src/main/java/dev/isxander/yacl/api/OptionGroup.java b/src/main/java/dev/isxander/yacl/api/OptionGroup.java index 9376b8e..f8c346b 100644 --- a/src/main/java/dev/isxander/yacl/api/OptionGroup.java +++ b/src/main/java/dev/isxander/yacl/api/OptionGroup.java @@ -34,6 +34,11 @@ public interface OptionGroup { @NotNull ImmutableList<Option<?>> options(); /** + * Dictates if the group should be collapsed by default. + */ + boolean collapsed(); + + /** * Always false when using the {@link Builder} * used to not render the separator if true */ @@ -50,6 +55,7 @@ public interface OptionGroup { private Text name = Text.empty(); private final List<Text> tooltipLines = new ArrayList<>(); private final List<Option<?>> options = new ArrayList<>(); + private boolean collapsed = false; private Builder() { @@ -107,6 +113,16 @@ public interface OptionGroup { return this; } + /** + * Dictates if the group should be collapsed by default + * + * @see OptionGroup#collapsed() + */ + public Builder collapsed(boolean collapsible) { + this.collapsed = collapsible; + return this; + } + public OptionGroup build() { Validate.notEmpty(options, "`options` must not be empty to build `OptionGroup`"); @@ -119,7 +135,7 @@ public interface OptionGroup { concatenatedTooltip.append(line); } - return new OptionGroupImpl(name, concatenatedTooltip, ImmutableList.copyOf(options), false); + return new OptionGroupImpl(name, concatenatedTooltip, ImmutableList.copyOf(options), collapsed, false); } } } diff --git a/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java b/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java index 7affbd4..a6e75f9 100644 --- a/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java +++ b/src/main/java/dev/isxander/yacl/gui/AbstractWidget.java @@ -1,12 +1,14 @@ package dev.isxander.yacl.gui; import com.mojang.blaze3d.systems.RenderSystem; +import dev.isxander.yacl.api.utils.Dimension; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.Drawable; import net.minecraft.client.gui.DrawableHelper; import net.minecraft.client.gui.Element; import net.minecraft.client.gui.Selectable; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; import net.minecraft.client.gui.widget.ClickableWidget; import net.minecraft.client.render.GameRenderer; import net.minecraft.client.sound.PositionedSoundInstance; @@ -18,7 +20,27 @@ public abstract class AbstractWidget implements Element, Drawable, Selectable { protected final MinecraftClient client = MinecraftClient.getInstance(); protected final TextRenderer textRenderer = client.textRenderer; - public void tick() { + protected Dimension<Integer> dim; + + public AbstractWidget(Dimension<Integer> dim) { + this.dim = dim; + } + + public void setDimension(Dimension<Integer> dim) { + this.dim = dim; + } + + @Override + public SelectionType getType() { + return SelectionType.NONE; + } + + public void unfocus() { + + } + + @Override + public void appendNarrations(NarrationMessageBuilder builder) { } diff --git a/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java b/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java index 1f118cc..11c9fa6 100644 --- a/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java +++ b/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java @@ -5,7 +5,6 @@ import dev.isxander.yacl.api.ConfigCategory; import dev.isxander.yacl.api.Option; import dev.isxander.yacl.api.OptionGroup; import dev.isxander.yacl.api.utils.Dimension; -import dev.isxander.yacl.gui.controllers.ControllerWidget; import dev.isxander.yacl.impl.YACLConstants; import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; @@ -14,12 +13,15 @@ import net.minecraft.client.gui.Selectable; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; import net.minecraft.client.gui.screen.narration.NarrationPart; +import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.ElementListWidget; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; import java.util.Collections; import java.util.List; +import java.util.function.Supplier; public class OptionListWidget extends ElementListWidget<OptionListWidget.Entry> { @@ -29,15 +31,32 @@ public class OptionListWidget extends ElementListWidget<OptionListWidget.Entry> right = width; for (OptionGroup group : category.groups()) { - if (!group.isRoot()) - addEntry(new GroupSeparatorEntry(group, screen)); + Supplier<Boolean> viewableSupplier; + if (!group.isRoot()) { + GroupSeparatorEntry groupSeparatorEntry = new GroupSeparatorEntry(group, screen); + viewableSupplier = groupSeparatorEntry::isExpanded; + addEntry(groupSeparatorEntry); + } else { + viewableSupplier = () -> true; + } + for (Option<?> option : group.options()) { - addEntry(new OptionEntry(option.controller().provideWidget(screen, null))); + addEntry(new OptionEntry(option.controller().provideWidget(screen, null), viewableSupplier)); } } } @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + for (Entry child : children()) { + if (child != getEntryAtPosition(mouseX, mouseY) && child instanceof OptionEntry optionEntry) + optionEntry.widget.unfocus(); + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override public boolean mouseScrolled(double mouseX, double mouseY, double amount) { for (Entry child : children()) { if (child.mouseScrolled(mouseX, mouseY, amount)) @@ -48,6 +67,26 @@ public class OptionListWidget extends ElementListWidget<OptionListWidget.Entry> } @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + for (Entry child : children()) { + if (child.keyPressed(keyCode, scanCode, modifiers)) + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + for (Entry child : children()) { + if (child.charTyped(chr, modifiers)) + return true; + } + + return super.charTyped(chr, modifiers); + } + + @Override protected int getScrollbarPositionX() { return left + super.getScrollbarPositionX(); } @@ -59,15 +98,24 @@ public class OptionListWidget extends ElementListWidget<OptionListWidget.Entry> fill(matrices, left, top, right, bottom, 0x6B000000); } - public static abstract class Entry extends ElementListWidget.Entry<Entry> { + @Override + public List<Entry> children() { + return super.children().stream().filter(Entry::isViewable).toList(); + } + public static abstract class Entry extends ElementListWidget.Entry<Entry> { + public boolean isViewable() { + return true; + } } private static class OptionEntry extends Entry { - private final ControllerWidget<?> widget; + public final AbstractWidget widget; + private final Supplier<Boolean> viewableSupplier; - public OptionEntry(ControllerWidget<?> widget) { + public OptionEntry(AbstractWidget widget, Supplier<Boolean> viewableSupplier) { this.widget = widget; + this.viewableSupplier = viewableSupplier; } @Override @@ -83,6 +131,21 @@ public class OptionListWidget extends ElementListWidget<OptionListWidget.Entry> } @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + return widget.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + return widget.charTyped(chr, modifiers); + } + + @Override + public boolean isViewable() { + return viewableSupplier.get(); + } + + @Override public List<? extends Selectable> selectableChildren() { return ImmutableList.of(widget); } @@ -97,20 +160,36 @@ public class OptionListWidget extends ElementListWidget<OptionListWidget.Entry> private final OptionGroup group; private final List<OrderedText> wrappedTooltip; + private final ButtonWidget expandMinimizeButton; + private float hoveredTicks = 0; private int prevMouseX, prevMouseY; private final Screen screen; private final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + private boolean groupExpanded; + public GroupSeparatorEntry(OptionGroup group, Screen screen) { this.group = group; this.screen = screen; this.wrappedTooltip = textRenderer.wrapLines(group.tooltip(), screen.width / 2); + this.groupExpanded = !group.collapsed(); + this.expandMinimizeButton = new ButtonWidget(0, 0, 20, 20, Text.empty(), btn -> { + groupExpanded = !groupExpanded; + updateExpandMinimizeText(); + }); + updateExpandMinimizeText(); } @Override public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { + expandMinimizeButton.x = x + entryWidth - expandMinimizeButton.getWidth(); + expandMinimizeButton.y = y + entryHeight / 2 - expandMinimizeButton.getHeight() / 2; + if (hovered) + expandMinimizeButton.render(matrices, mouseX, mouseY, tickDelta); + + hovered &= !expandMinimizeButton.isMouseOver(mouseX, mouseY); if (hovered && (!YACLConstants.HOVER_MOUSE_RESET || (mouseX == prevMouseX && mouseY == prevMouseY))) hoveredTicks += tickDelta; else @@ -126,6 +205,14 @@ public class OptionListWidget extends ElementListWidget<OptionListWidget.Entry> prevMouseY = mouseY; } + public boolean isExpanded() { + return groupExpanded; + } + + private void updateExpandMinimizeText() { + expandMinimizeButton.setMessage(Text.of(isExpanded() ? "\u25BC" : "\u25C0")); + } + @Override public List<? extends Selectable> selectableChildren() { return ImmutableList.of(new Selectable() { @@ -143,9 +230,7 @@ public class OptionListWidget extends ElementListWidget<OptionListWidget.Entry> @Override public List<? extends Element> children() { - return Collections.emptyList(); + return ImmutableList.of(expandMinimizeButton); } - - } } diff --git a/src/main/java/dev/isxander/yacl/gui/YACLScreen.java b/src/main/java/dev/isxander/yacl/gui/YACLScreen.java index 235364f..ccd3929 100644 --- a/src/main/java/dev/isxander/yacl/gui/YACLScreen.java +++ b/src/main/java/dev/isxander/yacl/gui/YACLScreen.java @@ -121,6 +121,24 @@ public class YACLScreen extends Screen { } } + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (optionList.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (optionList.charTyped(chr, modifiers)) { + return true; + } + + return super.charTyped(chr, modifiers); + } + public void changeCategory(int idx) { currentCategoryIdx = idx; refreshGUI(); diff --git a/src/main/java/dev/isxander/yacl/gui/controllers/ActionController.java b/src/main/java/dev/isxander/yacl/gui/controllers/ActionController.java index e0d0230..e337ee3 100644 --- a/src/main/java/dev/isxander/yacl/gui/controllers/ActionController.java +++ b/src/main/java/dev/isxander/yacl/gui/controllers/ActionController.java @@ -3,6 +3,7 @@ package dev.isxander.yacl.gui.controllers; import dev.isxander.yacl.api.ButtonOption; import dev.isxander.yacl.api.Controller; import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; import dev.isxander.yacl.gui.YACLScreen; import net.minecraft.text.Text; import org.jetbrains.annotations.ApiStatus; @@ -62,7 +63,7 @@ public class ActionController implements Controller<Consumer<YACLScreen>> { * {@inheritDoc} */ @Override - public ControllerWidget<ActionController> provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { + public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { return new ActionControllerElement(this, screen, widgetDimension); } @@ -88,7 +89,7 @@ public class ActionController implements Controller<Consumer<YACLScreen>> { @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!focused && !hovered) { + if (!focused) { return false; } diff --git a/src/main/java/dev/isxander/yacl/gui/controllers/BooleanController.java b/src/main/java/dev/isxander/yacl/gui/controllers/BooleanController.java index a338f5f..38be6db 100644 --- a/src/main/java/dev/isxander/yacl/gui/controllers/BooleanController.java +++ b/src/main/java/dev/isxander/yacl/gui/controllers/BooleanController.java @@ -3,8 +3,8 @@ package dev.isxander.yacl.gui.controllers; import dev.isxander.yacl.api.Controller; import dev.isxander.yacl.api.Option; import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; import dev.isxander.yacl.gui.YACLScreen; -import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.Text; import net.minecraft.util.Formatting; @@ -98,7 +98,7 @@ public class BooleanController implements Controller<Boolean> { * {@inheritDoc} */ @Override - public ControllerWidget<BooleanController> provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { + public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { return new BooleanControllerElement(this, screen, widgetDimension); } @@ -143,7 +143,7 @@ public class BooleanController implements Controller<Boolean> { @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!focused && !hovered) { + if (!focused) { return false; } diff --git a/src/main/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java b/src/main/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java index d712e56..29722e1 100644 --- a/src/main/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java +++ b/src/main/java/dev/isxander/yacl/gui/controllers/ControllerWidget.java @@ -16,8 +16,6 @@ import java.util.List; public abstract class ControllerWidget<T extends Controller<?>> extends AbstractWidget { protected final T control; protected final List<OrderedText> wrappedTooltip; - - protected Dimension<Integer> dim; protected final YACLScreen screen; protected boolean focused = false; @@ -27,8 +25,8 @@ public abstract class ControllerWidget<T extends Controller<?>> extends Abstract private int prevMouseX, prevMouseY; public ControllerWidget(T control, YACLScreen screen, Dimension<Integer> dim) { + super(dim); this.control = control; - this.dim = dim; this.screen = screen; this.wrappedTooltip = textRenderer.wrapLines(control.option().tooltip(), screen.width / 2); } @@ -47,7 +45,7 @@ public abstract class ControllerWidget<T extends Controller<?>> extends Abstract boolean firstIter = true; while (textRenderer.getWidth(nameString) > dim.width() - getControlWidth() - getXPadding() - 7) { - nameString = nameString.substring(0, nameString.length() - (firstIter ? 2 : 5)).trim(); + nameString = nameString.substring(0, Math.max(nameString.length() - (firstIter ? 2 : 5), 0)).trim(); nameString += "..."; firstIter = false; @@ -55,14 +53,14 @@ public abstract class ControllerWidget<T extends Controller<?>> extends Abstract Text shortenedName = Text.literal(nameString).fillStyle(name.getStyle()); - drawButtonRect(matrices, dim.x(), dim.y(), dim.xLimit(), dim.yLimit(), hovered || focused); + drawButtonRect(matrices, dim.x(), dim.y(), dim.xLimit(), dim.yLimit(), isHovered()); matrices.push(); matrices.translate(dim.x() + getXPadding(), getTextY(), 0); textRenderer.drawWithShadow(matrices, shortenedName, 0, 0, -1); matrices.pop(); drawValueText(matrices, mouseX, mouseY, delta); - if (hovered || focused) { + if (isHovered()) { drawHoveredControl(matrices, mouseX, mouseY, delta); } @@ -88,11 +86,16 @@ public abstract class ControllerWidget<T extends Controller<?>> extends Abstract @Override public boolean isMouseOver(double mouseX, double mouseY) { + if (dim == null) return false; return this.dim.isPointInside((int) mouseX, (int) mouseY); } protected int getControlWidth() { - return hovered || focused ? getHoveredControlWidth() : getUnhoveredControlWidth(); + return isHovered() ? getHoveredControlWidth() : getUnhoveredControlWidth(); + } + + public boolean isHovered() { + return hovered || focused; } protected abstract int getHoveredControlWidth(); @@ -124,10 +127,6 @@ public abstract class ControllerWidget<T extends Controller<?>> extends Abstract return dim.y() + dim.height() / 2f - textRenderer.fontHeight / 2f; } - public void setDimension(Dimension<Integer> dim) { - this.dim = dim; - } - @Override public boolean changeFocus(boolean lookForwards) { this.focused = !this.focused; @@ -135,8 +134,13 @@ public abstract class ControllerWidget<T extends Controller<?>> extends Abstract } @Override + public void unfocus() { + this.focused = false; + } + + @Override public SelectionType getType() { - return focused ? SelectionType.FOCUSED : hovered ? SelectionType.HOVERED : SelectionType.NONE; + return focused ? SelectionType.FOCUSED : isHovered() ? SelectionType.HOVERED : SelectionType.NONE; } @Override diff --git a/src/main/java/dev/isxander/yacl/gui/controllers/EnumController.java b/src/main/java/dev/isxander/yacl/gui/controllers/EnumController.java index cb25963..9d8e59c 100644 --- a/src/main/java/dev/isxander/yacl/gui/controllers/EnumController.java +++ b/src/main/java/dev/isxander/yacl/gui/controllers/EnumController.java @@ -4,6 +4,7 @@ import dev.isxander.yacl.api.Controller; import dev.isxander.yacl.api.NameableEnum; import dev.isxander.yacl.api.Option; import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; import dev.isxander.yacl.gui.YACLScreen; import net.minecraft.client.gui.screen.Screen; import net.minecraft.text.Text; @@ -72,7 +73,7 @@ public class EnumController<T extends Enum<T>> implements Controller<T> { * {@inheritDoc} */ @Override - public ControllerWidget<EnumController<T>> provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { + public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { return new EnumControllerElement<>(this, screen, widgetDimension, option().typeClass().getEnumConstants()); } @@ -108,7 +109,7 @@ public class EnumController<T extends Enum<T>> implements Controller<T> { @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!focused && !hovered) + if (!focused) return false; switch (keyCode) { diff --git a/src/main/java/dev/isxander/yacl/gui/controllers/LabelController.java b/src/main/java/dev/isxander/yacl/gui/controllers/LabelController.java new file mode 100644 index 0000000..63b1b42 --- /dev/null +++ b/src/main/java/dev/isxander/yacl/gui/controllers/LabelController.java @@ -0,0 +1,70 @@ +package dev.isxander.yacl.gui.controllers; + +import dev.isxander.yacl.api.Controller; +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import org.jetbrains.annotations.ApiStatus; + +public class LabelController implements Controller<Text> { + private final Option<Text> option; + private final int color; + + /** + * Constructs a label controller + * + * @param option bound option + */ + public LabelController(Option<Text> option) { + this(option, -1); + } + + /** + * Constructs a label controller + * + * @param option bound option + * @param color color of the label + */ + public LabelController(Option<Text> option, int color) { + this.option = option; + this.color = color; + } + + /** + * {@inheritDoc} + */ + @Override + public Option<Text> option() { + return option; + } + + public int color() { + return color; + } + + @Override + public Text formatValue() { + return option().pendingValue(); + } + + @Override + public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { + return new LabelControllerElement(widgetDimension); + } + + @ApiStatus.Internal + public class LabelControllerElement extends AbstractWidget { + + public LabelControllerElement(Dimension<Integer> dim) { + super(dim); + } + + @Override + public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) { + textRenderer.drawWithShadow(matrices, formatValue(), dim.x(), dim.centerY() - textRenderer.fontHeight / 2f, color()); + } + } +} diff --git a/src/main/java/dev/isxander/yacl/gui/controllers/TickBoxController.java b/src/main/java/dev/isxander/yacl/gui/controllers/TickBoxController.java index 106f76a..df0ae83 100644 --- a/src/main/java/dev/isxander/yacl/gui/controllers/TickBoxController.java +++ b/src/main/java/dev/isxander/yacl/gui/controllers/TickBoxController.java @@ -3,6 +3,7 @@ package dev.isxander.yacl.gui.controllers; import dev.isxander.yacl.api.Controller; import dev.isxander.yacl.api.Option; import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; import dev.isxander.yacl.gui.YACLScreen; import net.minecraft.client.gui.DrawableHelper; import net.minecraft.client.util.math.MatrixStack; @@ -45,7 +46,7 @@ public class TickBoxController implements Controller<Boolean> { * {@inheritDoc} */ @Override - public ControllerWidget<TickBoxController> provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { + public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { return new TickBoxControllerElement(this, screen, widgetDimension); } @@ -97,7 +98,7 @@ public class TickBoxController implements Controller<Boolean> { @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!focused && !hovered) { + if (!focused) { return false; } diff --git a/src/main/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java b/src/main/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java index ede3456..aa3c18f 100644 --- a/src/main/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java +++ b/src/main/java/dev/isxander/yacl/gui/controllers/slider/ISliderController.java @@ -2,8 +2,8 @@ package dev.isxander.yacl.gui.controllers.slider; import dev.isxander.yacl.api.Controller; import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; import dev.isxander.yacl.gui.YACLScreen; -import dev.isxander.yacl.gui.controllers.ControllerWidget; /** * Simple custom slider implementation that shifts the current value across when shown. @@ -48,7 +48,7 @@ public interface ISliderController<T extends Number> extends Controller<T> { * {@inheritDoc} */ @Override - default ControllerWidget<?> provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { + default AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { return new SliderControllerElement(this, screen, widgetDimension, min(), max(), interval()); } } diff --git a/src/main/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java b/src/main/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java index b587258..7eb7310 100644 --- a/src/main/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java +++ b/src/main/java/dev/isxander/yacl/gui/controllers/slider/SliderControllerElement.java @@ -101,7 +101,7 @@ public class SliderControllerElement extends ControllerWidget<ISliderController< @Override public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!focused && !hovered) + if (!focused) return false; switch (keyCode) { diff --git a/src/main/java/dev/isxander/yacl/gui/controllers/string/BasicStringController.java b/src/main/java/dev/isxander/yacl/gui/controllers/string/BasicStringController.java new file mode 100644 index 0000000..edda506 --- /dev/null +++ b/src/main/java/dev/isxander/yacl/gui/controllers/string/BasicStringController.java @@ -0,0 +1,42 @@ +package dev.isxander.yacl.gui.controllers.string; + +import dev.isxander.yacl.api.Option; +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.AbstractWidget; +import dev.isxander.yacl.gui.YACLScreen; + +public class BasicStringController implements IStringController<String> { + private final Option<String> option; + + /** + * Constructs a tickbox controller + * + * @param option bound option + */ + public BasicStringController(Option<String> option) { + this.option = option; + } + + /** + * {@inheritDoc} + */ + @Override + public Option<String> option() { + return option; + } + + @Override + public String getString() { + return option().pendingValue(); + } + + @Override + public void setFromString(String value) { + option().requestSet(value); + } + + @Override + public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { + return new StringControllerElement(this, screen, widgetDimension); + } +} diff --git a/src/main/java/dev/isxander/yacl/gui/controllers/string/IStringController.java b/src/main/java/dev/isxander/yacl/gui/controllers/string/IStringController.java new file mode 100644 index 0000000..ae66433 --- /dev/null +++ b/src/main/java/dev/isxander/yacl/gui/controllers/string/IStringController.java @@ -0,0 +1,14 @@ +package dev.isxander.yacl.gui.controllers.string; + +import dev.isxander.yacl.api.Controller; +import net.minecraft.text.Text; + +public interface IStringController<T> extends Controller<T> { + String getString(); + void setFromString(String value); + + @Override + default Text formatValue() { + return Text.of(getString()); + } +} diff --git a/src/main/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java b/src/main/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java new file mode 100644 index 0000000..ed066f2 --- /dev/null +++ b/src/main/java/dev/isxander/yacl/gui/controllers/string/StringControllerElement.java @@ -0,0 +1,257 @@ +package dev.isxander.yacl.gui.controllers.string; + +import dev.isxander.yacl.api.utils.Dimension; +import dev.isxander.yacl.gui.YACLScreen; +import dev.isxander.yacl.gui.controllers.ControllerWidget; +import net.minecraft.client.gui.DrawableHelper; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Pair; +import org.lwjgl.glfw.GLFW; + +public class StringControllerElement extends ControllerWidget<IStringController<?>> { + protected StringBuilder inputField; + protected Dimension<Integer> inputFieldBounds; + protected boolean inputFieldFocused; + + protected int caretPos; + protected int selectionLength; + + protected float ticks; + + private final Text emptyText; + + public StringControllerElement(IStringController<?> control, YACLScreen screen, Dimension<Integer> dim) { + super(control, screen, dim); + inputField = new StringBuilder(control.getString()); + inputFieldFocused = false; + caretPos = inputField.length(); + selectionLength = 0; + emptyText = Text.literal("Click to type...").formatted(Formatting.GRAY); + } + + @Override + protected void drawHoveredControl(MatrixStack matrices, int mouseX, int mouseY, float delta) { + ticks += delta; + + DrawableHelper.fill(matrices, inputFieldBounds.x(), inputFieldBounds.yLimit(), inputFieldBounds.xLimit(), inputFieldBounds.yLimit() + 1, -1); + DrawableHelper.fill(matrices, inputFieldBounds.x() + 1, inputFieldBounds.yLimit() + 1, inputFieldBounds.xLimit() + 1, inputFieldBounds.yLimit() + 2, 0xFF404040); + + if (inputFieldFocused || focused) { + int caretX = inputFieldBounds.x() + textRenderer.getWidth(inputField.substring(0, caretPos)) - 1; + if (inputField.isEmpty()) + caretX += inputFieldBounds.width() / 2; + + if (ticks % 20 <= 10) { + DrawableHelper.fill(matrices, caretX, inputFieldBounds.y(), caretX + 1, inputFieldBounds.yLimit(), -1); + } + + if (selectionLength != 0) { + int selectionX = inputFieldBounds.x() + textRenderer.getWidth(inputField.substring(0, caretPos + selectionLength)); + DrawableHelper.fill(matrices, caretX, inputFieldBounds.y() - 1, selectionX, inputFieldBounds.yLimit(), 0x803030FF); + } + } + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (inputFieldBounds.isPointInside((int) mouseX, (int) mouseY)) { + if (!inputFieldFocused) + inputFieldFocused = true; + else { + int textWidth = (int) mouseX - inputFieldBounds.x(); + caretPos = textRenderer.trimToWidth(control.getString(), textWidth).length(); + selectionLength = 0; + } + } else { + inputFieldFocused = false; + } + + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!inputFieldFocused) + return false; + + switch (keyCode) { + case GLFW.GLFW_KEY_LEFT -> { + if (Screen.hasShiftDown()) { + if (Screen.hasControlDown()) { + int spaceChar = findSpaceIndex(true); + selectionLength += caretPos - spaceChar; + caretPos = spaceChar; + } else if (caretPos > 0) { + caretPos--; + selectionLength += 1; + } + } else { + if (caretPos > 0) + caretPos--; + selectionLength = 0; + } + + return true; + } + case GLFW.GLFW_KEY_RIGHT -> { + if (Screen.hasShiftDown()) { + if (Screen.hasControlDown()) { + int spaceChar = findSpaceIndex(false); + selectionLength -= spaceChar - caretPos; + caretPos = spaceChar; + } else if (caretPos < inputField.length()) { + caretPos++; + selectionLength -= 1; + } + } else { + if (caretPos < inputField.length()) + caretPos++; + selectionLength = 0; + } + + return true; + } + case GLFW.GLFW_KEY_BACKSPACE -> { + if (selectionLength != 0) { + write(""); + } else if (caretPos > 0) { + inputField.deleteCharAt(caretPos - 1); + updateControl(); + caretPos--; + } + return true; + } + case GLFW.GLFW_KEY_DELETE -> { + if (caretPos < inputField.length()) { + inputField.deleteCharAt(caretPos); + updateControl(); + } + return true; + } + } + + if (Screen.isPaste(keyCode)) { + this.write(client.keyboard.getClipboard()); + } else if (Screen.isCopy(keyCode) && selectionLength != 0) { + client.keyboard.setClipboard(getSelection()); + } else if (Screen.isCut(keyCode) && selectionLength != 0) { + client.keyboard.setClipboard(getSelection()); + this.write(""); + } else if (Screen.isSelectAll(keyCode)) { + caretPos = inputField.length(); + selectionLength = -caretPos; + } + + return false; + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (!inputFieldFocused) + return false; + + write(Character.toString(chr)); + + return true; + } + + public void write(String string) { + if (selectionLength == 0) { + string = textRenderer.trimToWidth(string, getMaxLength() - textRenderer.getWidth(inputField.toString())); + + inputField.insert(caretPos, string); + caretPos += string.length(); + } else { + int start = getSelectionStart(); + int end = getSelectionEnd(); + + string = textRenderer.trimToWidth(string, getMaxLength() - textRenderer.getWidth(inputField.toString()) + textRenderer.getWidth(inputField.substring(start, end))); + + inputField.replace(start, end, string); + caretPos = start + string.length(); + selectionLength = 0; + } + updateControl(); + } + + public int getMaxLength() { + return dim.width() / 8 * 7; + } + + public int getSelectionStart() { + return Math.min(caretPos, caretPos + selectionLength); + } + + public int getSelectionEnd() { + return Math.max(caretPos, caretPos + selectionLength); + } + + protected String getSelection() { + return inputField.substring(getSelectionStart(), getSelectionEnd()); + } + + protected int findSpaceIndex(boolean reverse) { + int i; + int fromIndex = caretPos; + if (reverse) { + if (caretPos > 0) + fromIndex -= 1; + i = this.inputField.lastIndexOf(" ", fromIndex); + + if (i == -1) i = 0; + } else { + if (caretPos < inputField.length()) + fromIndex += 1; + i = this.inputField.indexOf(" ", fromIndex); + + if (i == -1) i = inputField.length(); + } + + System.out.println(i); + return i; + } + + @Override + public boolean changeFocus(boolean lookForwards) { + return inputFieldFocused = super.changeFocus(lookForwards); + } + + @Override + public void unfocus() { + super.unfocus(); + inputFieldFocused = false; + } + + @Override + public void setDimension(Dimension<Integer> dim) { + super.setDimension(dim); + + int width = Math.max(6, textRenderer.getWidth(getValueText())); + inputFieldBounds = Dimension.ofInt(dim.xLimit() - getXPadding() - width, dim.centerY() - textRenderer.fontHeight / 2, width, textRenderer.fontHeight); + } + + @Override + public boolean isHovered() { + return super.isHovered() || inputFieldFocused; + } + + protected void updateControl() { + control.setFromString(inputField.toString()); + } + + @Override + protected int getHoveredControlWidth() { + return getUnhoveredControlWidth(); + } + + @Override + protected Text getValueText() { + if (!inputFieldFocused && inputField.isEmpty()) + return emptyText; + + return super.getValueText(); + } +} diff --git a/src/main/java/dev/isxander/yacl/impl/OptionGroupImpl.java b/src/main/java/dev/isxander/yacl/impl/OptionGroupImpl.java index 1f2d4e2..58bc96b 100644 --- a/src/main/java/dev/isxander/yacl/impl/OptionGroupImpl.java +++ b/src/main/java/dev/isxander/yacl/impl/OptionGroupImpl.java @@ -6,5 +6,5 @@ import dev.isxander.yacl.api.OptionGroup; import net.minecraft.text.Text; import org.jetbrains.annotations.NotNull; -public record OptionGroupImpl(@NotNull Text name, @NotNull Text tooltip, ImmutableList<Option<?>> options, boolean isRoot) implements OptionGroup { +public record OptionGroupImpl(@NotNull Text name, @NotNull Text tooltip, ImmutableList<Option<?>> options, boolean collapsed, boolean isRoot) implements OptionGroup { } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index dba6e5a..fc5a14f 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -26,6 +26,7 @@ "mixins": [ "yet-another-config-lib.mixins.json" ], + "accessWidener": "yacl.accesswidener", "custom": { "modmenu": { "badges": ["library"] diff --git a/src/main/resources/yacl.accesswidener b/src/main/resources/yacl.accesswidener new file mode 100644 index 0000000..b43aaf3 --- /dev/null +++ b/src/main/resources/yacl.accesswidener @@ -0,0 +1,3 @@ +accessWidener v1 named + +extendable method net/minecraft/client/gui/widget/EntryListWidget children ()Ljava/util/List; diff --git a/src/testmod/java/dev/isxander/yacl/test/ModMenuIntegration.java b/src/testmod/java/dev/isxander/yacl/test/ModMenuIntegration.java index ac313e2..b966ae8 100644 --- a/src/testmod/java/dev/isxander/yacl/test/ModMenuIntegration.java +++ b/src/testmod/java/dev/isxander/yacl/test/ModMenuIntegration.java @@ -3,14 +3,12 @@ package dev.isxander.yacl.test; import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; import dev.isxander.yacl.api.*; -import dev.isxander.yacl.gui.controllers.ActionController; -import dev.isxander.yacl.gui.controllers.BooleanController; -import dev.isxander.yacl.gui.controllers.EnumController; -import dev.isxander.yacl.gui.controllers.TickBoxController; +import dev.isxander.yacl.gui.controllers.*; import dev.isxander.yacl.gui.controllers.slider.DoubleSliderController; import dev.isxander.yacl.gui.controllers.slider.FloatSliderController; import dev.isxander.yacl.gui.controllers.slider.IntegerSliderController; import dev.isxander.yacl.gui.controllers.slider.LongSliderController; +import dev.isxander.yacl.gui.controllers.string.BasicStringController; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.option.GraphicsMode; @@ -52,6 +50,7 @@ public class ModMenuIntegration implements ModMenuApi { .group(OptionGroup.createBuilder() .name(Text.of("Boolean Controllers")) .tooltip(Text.of("Test!")) + .collapsed(true) .option(Option.createBuilder(boolean.class) .name(Text.of("Boolean Toggle")) .tooltip(Text.of("A simple toggle button.")) @@ -123,6 +122,18 @@ public class ModMenuIntegration implements ModMenuApi { .build()) .build()) .group(OptionGroup.createBuilder() + .name(Text.of("Input Field Controllers")) + .option(Option.createBuilder(String.class) + .name(Text.of("Text Option")) + .binding( + "Hello", + () -> TestSettings.textField, + value -> TestSettings.textField = value + ) + .controller(BasicStringController::new) + .build()) + .build()) + .group(OptionGroup.createBuilder() .name(Text.of("Enum Controllers")) .option(Option.createBuilder(TestSettings.Alphabet.class) .name(Text.of("Enum Cycler")) @@ -135,12 +146,16 @@ public class ModMenuIntegration implements ModMenuApi { .build()) .build()) .group(OptionGroup.createBuilder() - .name(Text.of("Buttons!")) + .name(Text.of("Options that aren't really options")) .option(ButtonOption.createBuilder() .name(Text.of("Button \"Option\"")) .action(screen -> SystemToast.add(MinecraftClient.getInstance().getToastManager(), SystemToast.Type.TUTORIAL_HINT, Text.of("Button Pressed"), Text.of("Button option was invoked!"))) .controller(ActionController::new) .build()) + .option(Option.createBuilder(Text.class) + .binding(Binding.immutable(Text.of("To center, or not to center?!"))) + .controller(LabelController::new) + .build()) .build()) .group(OptionGroup.createBuilder() .name(Text.of("Minecraft Bindings")) @@ -357,6 +372,7 @@ public class ModMenuIntegration implements ModMenuApi { private static double doubleSlider = 0; private static float floatSlider = 0; private static long longSlider = 0; + private static String textField = "Hello"; private static Alphabet enumOption = Alphabet.A; private static boolean groupTestRoot = false; |