aboutsummaryrefslogtreecommitdiff
path: root/common/src/main/java/dev
diff options
context:
space:
mode:
authorCrendgrim <Crendgrim@users.noreply.github.com>2023-08-31 00:06:47 +0200
committerGitHub <noreply@github.com>2023-08-30 23:06:47 +0100
commit554646dbd857e2fab1be8339ce8d0231ef2dbb4c (patch)
treeb55e6f1d282c18e743857bba3c9d5e4f226200ed /common/src/main/java/dev
parentc11566072608683034864dbd4e0d3f5afa067537 (diff)
downloadYetAnotherConfigLib-554646dbd857e2fab1be8339ce8d0231ef2dbb4c.tar.gz
YetAnotherConfigLib-554646dbd857e2fab1be8339ce8d0231ef2dbb4c.tar.bz2
YetAnotherConfigLib-554646dbd857e2fab1be8339ce8d0231ef2dbb4c.zip
Add dropdown controllers (#95)
Diffstat (limited to 'common/src/main/java/dev')
-rw-r--r--common/src/main/java/dev/isxander/yacl3/api/controller/DropdownStringControllerBuilder.java18
-rw-r--r--common/src/main/java/dev/isxander/yacl3/api/controller/ItemControllerBuilder.java11
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java18
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java43
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java17
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java19
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java17
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java2
-rw-r--r--common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java17
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownController.java80
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java238
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringController.java34
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringControllerElement.java31
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemController.java68
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java87
-rw-r--r--common/src/main/java/dev/isxander/yacl3/gui/utils/ItemRegistryHelper.java104
-rw-r--r--common/src/main/java/dev/isxander/yacl3/impl/controller/DropdownStringControllerBuilderImpl.java49
-rw-r--r--common/src/main/java/dev/isxander/yacl3/impl/controller/ItemControllerBuilderImpl.java18
18 files changed, 870 insertions, 1 deletions
diff --git a/common/src/main/java/dev/isxander/yacl3/api/controller/DropdownStringControllerBuilder.java b/common/src/main/java/dev/isxander/yacl3/api/controller/DropdownStringControllerBuilder.java
new file mode 100644
index 0000000..3f5fb33
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/api/controller/DropdownStringControllerBuilder.java
@@ -0,0 +1,18 @@
+package dev.isxander.yacl3.api.controller;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.impl.controller.DropdownStringControllerBuilderImpl;
+
+import java.util.List;
+
+public interface DropdownStringControllerBuilder extends StringControllerBuilder {
+ DropdownStringControllerBuilder values(List<String> values);
+ DropdownStringControllerBuilder values(String... values);
+ DropdownStringControllerBuilder allowEmptyValue(boolean allowEmptyValue);
+ DropdownStringControllerBuilder allowAnyValue(boolean allowAnyValue);
+
+
+ static DropdownStringControllerBuilder create(Option<String> option) {
+ return new DropdownStringControllerBuilderImpl(option);
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/api/controller/ItemControllerBuilder.java b/common/src/main/java/dev/isxander/yacl3/api/controller/ItemControllerBuilder.java
new file mode 100644
index 0000000..5a1f5fa
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/api/controller/ItemControllerBuilder.java
@@ -0,0 +1,11 @@
+package dev.isxander.yacl3.api.controller;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.impl.controller.ItemControllerBuilderImpl;
+import net.minecraft.world.item.Item;
+
+public interface ItemControllerBuilder extends ControllerBuilder<Item> {
+ static ItemControllerBuilder create(Option<Item> option) {
+ return new ItemControllerBuilderImpl(option);
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java b/common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
index acbf338..deff6d7 100644
--- a/common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
+++ b/common/src/main/java/dev/isxander/yacl3/config/GsonConfigInstance.java
@@ -1,9 +1,12 @@
package dev.isxander.yacl3.config;
import com.google.gson.*;
+import dev.isxander.yacl3.gui.utils.ItemRegistryHelper;
import dev.isxander.yacl3.impl.utils.YACLConstants;
+import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
+import net.minecraft.world.item.Item;
import java.awt.*;
import java.io.IOException;
@@ -66,6 +69,7 @@ public class GsonConfigInstance<T> extends ConfigInstance<T> {
.registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
.registerTypeHierarchyAdapter(Style.class, new Style.Serializer())
.registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter())
+ .registerTypeHierarchyAdapter(Item.class, new ItemTypeAdapter())
.serializeNulls()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
@@ -129,6 +133,17 @@ public class GsonConfigInstance<T> extends ConfigInstance<T> {
return new JsonPrimitive(color.getRGB());
}
}
+ public static class ItemTypeAdapter implements JsonSerializer<Item>, JsonDeserializer<Item> {
+ @Override
+ public Item deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+ return ItemRegistryHelper.getItemFromName(jsonElement.getAsString());
+ }
+
+ @Override
+ public JsonElement serialize(Item item, Type type, JsonSerializationContext jsonSerializationContext) {
+ return new JsonPrimitive(BuiltInRegistries.ITEM.getKey(item).toString());
+ }
+ }
/**
* Creates a builder for a GSON config instance.
@@ -148,7 +163,8 @@ public class GsonConfigInstance<T> extends ConfigInstance<T> {
.serializeNulls()
.registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
.registerTypeHierarchyAdapter(Style.class, new Style.Serializer())
- .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter());
+ .registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter())
+ .registerTypeHierarchyAdapter(Item.class, new ItemTypeAdapter());
private Builder(Class<T> configClass) {
this.configClass = configClass;
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java
new file mode 100644
index 0000000..44239d5
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/Dropdown.java
@@ -0,0 +1,43 @@
+package dev.isxander.yacl3.config.v2.api.autogen;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An option factory.
+ * <p>
+ * This creates a regular option with a
+ * {@link dev.isxander.yacl3.api.controller.DropdownStringControllerBuilder} controller.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface Dropdown {
+ /**
+ * The allowed values for the field. These will be shown in a dropdown
+ * that the user can filter and select from.
+ * <p>
+ * Only values in this list will be accepted and written to the config
+ * file, unless {@link #allow()} is set to ${@code ALLOW_ANY}.
+ * <p>
+ * Empty string is a valid value only if it appears in this list, or if
+ * {@link #allow()} is set to {@code ALLOW_EMPTY} or {@code ALLOW_ANY}.
+ */
+ String[] values();
+
+ /**
+ * Whether to accept the empty string as a valid value if it does not
+ * already appear in {@link #values()}. If it already appears there,
+ * the value of this does not apply.
+ */
+ boolean allowEmptyValue() default false;
+
+ /**
+ * Whether to accept any string as a valid value. The list of strings
+ * supplied in {@link #values()} are only used as dropdown suggestions.
+ * Empty strings are still prohibited unless the empty string appears in
+ * {@link #values()} or {@link #allowEmptyValue()}.
+ */
+ boolean allowAnyValue() default false;
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java
new file mode 100644
index 0000000..84d2c7a
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/api/autogen/ItemField.java
@@ -0,0 +1,17 @@
+package dev.isxander.yacl3.config.v2.api.autogen;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * An option factory.
+ * <p>
+ * This creates a regular option with a
+ * {@link dev.isxander.yacl3.api.controller.ItemControllerBuilder} controller.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface ItemField {
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java
new file mode 100644
index 0000000..c487aab
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/DropdownImpl.java
@@ -0,0 +1,19 @@
+package dev.isxander.yacl3.config.v2.impl.autogen;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.controller.ControllerBuilder;
+import dev.isxander.yacl3.api.controller.DropdownStringControllerBuilder;
+import dev.isxander.yacl3.config.v2.api.ConfigField;
+import dev.isxander.yacl3.config.v2.api.autogen.Dropdown;
+import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess;
+import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory;
+
+public class DropdownImpl extends SimpleOptionFactory<Dropdown, String> {
+ @Override
+ protected ControllerBuilder<String> createController(Dropdown annotation, ConfigField<String> field, OptionAccess storage, Option<String> option) {
+ return DropdownStringControllerBuilder.create(option)
+ .values(annotation.values())
+ .allowEmptyValue(annotation.allowEmptyValue())
+ .allowAnyValue(annotation.allowAnyValue());
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java
new file mode 100644
index 0000000..2802f5c
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/ItemFieldImpl.java
@@ -0,0 +1,17 @@
+package dev.isxander.yacl3.config.v2.impl.autogen;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.controller.ControllerBuilder;
+import dev.isxander.yacl3.api.controller.ItemControllerBuilder;
+import dev.isxander.yacl3.config.v2.api.ConfigField;
+import dev.isxander.yacl3.config.v2.api.autogen.ItemField;
+import dev.isxander.yacl3.config.v2.api.autogen.OptionAccess;
+import dev.isxander.yacl3.config.v2.api.autogen.SimpleOptionFactory;
+import net.minecraft.world.item.Item;
+
+public class ItemFieldImpl extends SimpleOptionFactory<ItemField, Item> {
+ @Override
+ protected ControllerBuilder<Item> createController(ItemField annotation, ConfigField<Item> field, OptionAccess storage, Option<Item> option) {
+ return ItemControllerBuilder.create(option);
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java
index 4bcf5d6..4f6e3c7 100644
--- a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/autogen/OptionFactoryRegistry.java
@@ -31,6 +31,8 @@ public class OptionFactoryRegistry {
registerOptionFactory(EnumCycler.class, new EnumCyclerImpl());
registerOptionFactory(StringField.class, new StringFieldImpl());
registerOptionFactory(ColorField.class, new ColorFieldImpl());
+ registerOptionFactory(Dropdown.class, new DropdownImpl());
+ registerOptionFactory(ItemField.class, new ItemFieldImpl());
registerOptionFactory(Label.class, new LabelImpl());
registerOptionFactory(ListGroup.class, new ListGroupImpl<>());
diff --git a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java
index 1df9cfb..d308c23 100644
--- a/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java
+++ b/common/src/main/java/dev/isxander/yacl3/config/v2/impl/serializer/GsonConfigSerializer.java
@@ -1,12 +1,16 @@
package dev.isxander.yacl3.config.v2.impl.serializer;
import com.google.gson.*;
+import dev.isxander.yacl3.config.GsonConfigInstance;
import dev.isxander.yacl3.config.v2.api.*;
import dev.isxander.yacl3.config.v2.api.serializer.GsonConfigSerializerBuilder;
+import dev.isxander.yacl3.gui.utils.ItemRegistryHelper;
import dev.isxander.yacl3.impl.utils.YACLConstants;
import dev.isxander.yacl3.platform.YACLPlatform;
+import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.Style;
+import net.minecraft.world.item.Item;
import org.quiltmc.parsers.json.JsonReader;
import org.quiltmc.parsers.json.JsonWriter;
import org.quiltmc.parsers.json.gson.GsonReader;
@@ -129,6 +133,18 @@ public class GsonConfigSerializer<T> extends ConfigSerializer<T> {
}
}
+ public static class ItemTypeAdapter implements JsonSerializer<Item>, JsonDeserializer<Item> {
+ @Override
+ public Item deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
+ return ItemRegistryHelper.getItemFromName(jsonElement.getAsString());
+ }
+
+ @Override
+ public JsonElement serialize(Item item, Type type, JsonSerializationContext jsonSerializationContext) {
+ return new JsonPrimitive(BuiltInRegistries.ITEM.getKey(item).toString());
+ }
+ }
+
public static class Builder<T> implements GsonConfigSerializerBuilder<T> {
private final ConfigClassHandler<T> config;
private Path path;
@@ -139,6 +155,7 @@ public class GsonConfigSerializer<T> extends ConfigSerializer<T> {
.registerTypeHierarchyAdapter(Component.class, new Component.Serializer())
.registerTypeHierarchyAdapter(Style.class, new Style.Serializer())
.registerTypeHierarchyAdapter(Color.class, new ColorTypeAdapter())
+ .registerTypeHierarchyAdapter(Item.class, new ItemTypeAdapter())
.setPrettyPrinting();
public Builder(ConfigClassHandler<T> config) {
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownController.java b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownController.java
new file mode 100644
index 0000000..8251f9e
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownController.java
@@ -0,0 +1,80 @@
+package dev.isxander.yacl3.gui.controllers.dropdown;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.gui.controllers.string.IStringController;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public abstract class AbstractDropdownController<T> implements IStringController<T> {
+ protected final Option<T> option;
+ private final List<String> allowedValues;
+ public final boolean allowEmptyValue;
+ public final boolean allowAnyValue;
+
+ /**
+ * Constructs a dropdown controller
+ *
+ * @param option bound option
+ * @param allowedValues possible values
+ */
+ protected AbstractDropdownController(Option<T> option, List<String> allowedValues, boolean allowEmptyValue, boolean allowAnyValue) {
+ this.option = option;
+ this.allowedValues = allowedValues;
+ this.allowEmptyValue = allowEmptyValue;
+ this.allowAnyValue = allowAnyValue;
+ }
+
+ protected AbstractDropdownController(Option<T> option) {
+ this(option, Collections.emptyList(), false, false);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Option<T> option() {
+ return option;
+ }
+
+ public List<String> getAllowedValues() {
+ return getAllowedValues("");
+ }
+ public List<String> getAllowedValues(String inputField) {
+ List<String> values = new ArrayList<>(allowedValues);
+ if (allowEmptyValue && !values.contains("")) values.add("");
+ if (allowAnyValue && !inputField.isBlank() && !allowedValues.contains(inputField)) {
+ values.add(inputField);
+ }
+ String currentValue = getString();
+ if (allowAnyValue && !allowedValues.contains(currentValue)) {
+ values.add(currentValue);
+ }
+ return values;
+ }
+
+ public boolean isValueValid(String value) {
+ if (value.isBlank()) return allowEmptyValue;
+ return allowAnyValue || getAllowedValues().contains(value);
+ }
+
+ protected String getValidValue(String value) {
+ return getValidValue(value, 0);
+ }
+ protected String getValidValue(String value, int offset) {
+ if (offset == -1) return getString();
+
+ return getAllowedValues(value).stream()
+ .filter(val -> val.toLowerCase().contains(value.toLowerCase()))
+ .sorted((s1, s2) -> {
+ if (s1.startsWith(value) && !s2.startsWith(value)) return -1;
+ if (!s1.startsWith(value) && s2.startsWith(value)) return 1;
+ return s1.compareTo(s2);
+ })
+ .skip(offset)
+ .findFirst()
+ .orElseGet(this::getString);
+ }
+
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java
new file mode 100644
index 0000000..f91fc41
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java
@@ -0,0 +1,238 @@
+package dev.isxander.yacl3.gui.controllers.dropdown;
+
+import com.mojang.blaze3d.platform.InputConstants;
+import com.mojang.blaze3d.vertex.PoseStack;
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.YACLScreen;
+import dev.isxander.yacl3.gui.controllers.string.StringControllerElement;
+import dev.isxander.yacl3.gui.utils.GuiUtils;
+import net.minecraft.ChatFormatting;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.Component;
+
+import java.awt.Color;
+import java.util.List;
+import java.util.function.Consumer;
+
+public abstract class AbstractDropdownControllerElement<T, U> extends StringControllerElement {
+ public static final int MAX_SHOWN_NUMBER_OF_ITEMS = 7;
+
+ private final AbstractDropdownController<T> dropdownController;
+ protected boolean dropdownVisible = false;
+ // Stores the current selection position. The item at this position in the dropdown list will be chosen as the
+ // accepted value when the element is closed.
+ protected int selectedIndex = 0;
+ // Stores a cached list of matching values
+ protected List<U> matchingValues = null;
+
+ public AbstractDropdownControllerElement(AbstractDropdownController<T> control, YACLScreen screen, Dimension<Integer> dim) {
+ super(control, screen, dim, false);
+ this.dropdownController = control;
+ this.dropdownController.option.addListener((opt, val) -> this.matchingValues = this.computeMatchingValues());
+ }
+
+ public void showDropdown() {
+ dropdownVisible = true;
+ selectedIndex = 0;
+ }
+
+ public void closeDropdown() {
+ dropdownVisible = false;
+ ensureValidValue();
+ }
+
+ public void ensureValidValue() {
+ inputField = dropdownController.getValidValue(inputField, selectedIndex);
+ this.matchingValues = this.computeMatchingValues();
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (super.mouseClicked(mouseX, mouseY, button)) {
+ if (!dropdownVisible) {
+ showDropdown();
+ doSelectAll();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void setFocused(boolean focused) {
+ if (focused) {
+ doSelectAll();
+ super.setFocused(true);
+ } else unfocus();
+ }
+
+ @Override
+ public void unfocus() {
+ closeDropdown();
+ super.unfocus();
+ }
+
+ @Override
+ public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
+ if (!inputFieldFocused)
+ return false;
+ if (dropdownVisible) {
+ switch (keyCode) {
+ case InputConstants.KEY_DOWN -> {
+ selectNextEntry();
+ return true;
+ }
+ case InputConstants.KEY_UP -> {
+ selectPreviousEntry();
+ return true;
+ }
+ case InputConstants.KEY_TAB -> {
+ if (Screen.hasShiftDown()) {
+ selectPreviousEntry();
+ } else {
+ selectNextEntry();
+ }
+ return true;
+ }
+ }
+ } else {
+ if (keyCode == InputConstants.KEY_RETURN || keyCode == InputConstants.KEY_NUMPADENTER) {
+ showDropdown();
+ return true;
+ }
+ }
+ return super.keyPressed(keyCode, scanCode, modifiers);
+ }
+
+ @Override
+ public boolean charTyped(char chr, int modifiers) {
+ if (!dropdownVisible) {
+ showDropdown();
+ }
+ return super.charTyped(chr, modifiers);
+ }
+
+ @Override
+ protected int getValueColor() {
+ if (inputFieldFocused) {
+ if (!dropdownController.isValueValid(inputField)) {
+ return 0xFFF06080;
+ }
+ }
+ return super.getValueColor();
+ }
+
+ public void selectNextEntry() {
+ if (selectedIndex == getDropdownLength() - 1) {
+ selectedIndex = 0;
+ } else {
+ selectedIndex++;
+ }
+ }
+
+ public void selectPreviousEntry() {
+ if (selectedIndex == 0) {
+ selectedIndex = getDropdownLength() - 1;
+ } else {
+ selectedIndex--;
+ }
+ }
+
+ public int getDropdownLength() {
+ return matchingValues.size();
+ }
+
+ @Override
+ public boolean modifyInput(Consumer<StringBuilder> builder) {
+ boolean success = super.modifyInput(builder);
+ if (success) {
+ this.matchingValues = this.computeMatchingValues();
+ }
+ return success;
+ }
+
+ public abstract List<U> computeMatchingValues();
+
+ public boolean matchingValue(String value) {
+ return value.toLowerCase().contains(inputField.toLowerCase());
+ }
+
+ @Override
+ public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
+ if (matchingValues == null) matchingValues = computeMatchingValues();
+
+ super.render(graphics, mouseX, mouseY, delta);
+
+ if (inputFieldFocused && dropdownVisible) {
+ PoseStack matrices = graphics.pose();
+ matrices.pushPose();
+ matrices.translate(0, 0, 200);
+ renderDropdown(graphics);
+ matrices.popPose();
+ }
+ }
+
+ public void renderDropdown(GuiGraphics graphics) {
+ if (matchingValues.size() == 0) return;
+ // Limit the visible options to allow scrolling through the suggestion list
+ int begin = Math.max(0, selectedIndex - MAX_SHOWN_NUMBER_OF_ITEMS / 2);
+ int end = begin + MAX_SHOWN_NUMBER_OF_ITEMS;
+ if (end >= matchingValues.size()) {
+ end = matchingValues.size();
+ begin = Math.max(0, end - MAX_SHOWN_NUMBER_OF_ITEMS);
+ }
+
+ renderDropdownBackground(graphics, end - begin);
+ if (matchingValues.size() >= 1) {
+ // Highlight the currently selected element
+ graphics.setColor(0.0f, 0.0f, 0.0f, 0.5f);
+ int x = getDimension().x();
+ int y = getDimension().yLimit() + 2 + getDimension().height() * (selectedIndex - begin);
+ graphics.fill(x, y, x + getDimension().width(), y + getDimension().height(), -1);
+ graphics.setColor(1.0f, 1.0f, 1.0f, 1.0f);
+ graphics.renderOutline(x, y, getDimension().width(), getDimension().height(), -1);
+
+ }
+
+ int n = 1;
+ for (int i = begin; i < end; ++i) {
+ renderDropdownEntry(graphics, matchingValues.get(i), n);
+ ++n;
+ }
+ }
+
+ protected int getDropdownEntryPadding() {
+ return 0;
+ }
+
+ protected void renderDropdownEntry(GuiGraphics graphics, U value, int n) {
+ String entry = getString(value);
+ int color = -1;
+ Component text;
+ if (entry.isBlank()) {
+ text = Component.translatable("yacl.control.text.blank").withStyle(ChatFormatting.GRAY);
+ } else {
+ text = shortenString(entry);
+ }
+ graphics.drawString(textRenderer, text, getDimension().xLimit() - textRenderer.width(text) - getDecorationPadding() - getDropdownEntryPadding(), getTextY() + n * getDimension().height() + 2, color, true);
+ }
+
+ public abstract String getString(U object);
+
+ public Component shortenString(String value) {
+ return Component.literal(GuiUtils.shortenString(value, textRenderer, getDimension().width() - 20, "..."));
+ }
+
+ public void renderDropdownBackground(GuiGraphics graphics, int numberOfItems) {
+ graphics.setColor(0.25f, 0.25f, 0.25f, 1.0f);
+ graphics.blit(Screen.BACKGROUND_LOCATION, getDimension().x(), getDimension().yLimit() + 2, 0, 0.0f, 0.0f, getDimension().width(), getDimension().height() * numberOfItems + 2, 32, 32);
+ graphics.setColor(1.0f, 1.0f, 1.0f, 1.0f);
+ graphics.renderOutline(getDimension().x(), getDimension().yLimit() + 2, getDimension().width(), getDimension().height() * numberOfItems, -1);
+ }
+
+ protected int getDecorationPadding() {
+ return super.getXPadding();
+ }
+
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringController.java b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringController.java
new file mode 100644
index 0000000..fafc759
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringController.java
@@ -0,0 +1,34 @@
+package dev.isxander.yacl3.gui.controllers.dropdown;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.AbstractWidget;
+import dev.isxander.yacl3.gui.YACLScreen;
+
+import java.util.List;
+
+public class DropdownStringController extends AbstractDropdownController<String> {
+
+ public DropdownStringController(Option<String> option, List<String> allowedValues, boolean allowEmptyValue, boolean allowAnyValue) {
+ super(option, allowedValues, allowEmptyValue, allowAnyValue);
+ }
+
+ @Override
+ public String getString() {
+ return option().pendingValue();
+ }
+
+ @Override
+ public void setFromString(String value) {
+ option().requestSet(getValidValue(value));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) {
+ return new DropdownStringControllerElement(this, screen, widgetDimension);
+ }
+
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringControllerElement.java b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringControllerElement.java
new file mode 100644
index 0000000..615aada
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownStringControllerElement.java
@@ -0,0 +1,31 @@
+package dev.isxander.yacl3.gui.controllers.dropdown;
+
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.YACLScreen;
+
+import java.util.List;
+
+public class DropdownStringControllerElement extends AbstractDropdownControllerElement<String, String> {
+ private final DropdownStringController controller;
+
+ public DropdownStringControllerElement(DropdownStringController control, YACLScreen screen, Dimension<Integer> dim) {
+ super(control, screen, dim);
+ this.controller = control;
+ }
+
+ @Override
+ public List<String> computeMatchingValues() {
+ return controller.getAllowedValues(inputField).stream()
+ .filter(this::matchingValue)
+ .sorted((s1, s2) -> {
+ if (s1.startsWith(inputField) && !s2.startsWith(inputField)) return -1;
+ if (!s1.startsWith(inputField) && s2.startsWith(inputField)) return 1;
+ return s1.compareTo(s2);
+ })
+ .toList();
+ }
+
+ public String getString(String object) {
+ return object;
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemController.java b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemController.java
new file mode 100644
index 0000000..ac903c7
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemController.java
@@ -0,0 +1,68 @@
+package dev.isxander.yacl3.gui.controllers.dropdown;
+
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.AbstractWidget;
+import dev.isxander.yacl3.gui.YACLScreen;
+import dev.isxander.yacl3.gui.utils.ItemRegistryHelper;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.item.Item;
+
+/**
+ * Simple controller that simply runs the button action on press
+ * and renders a {@link} Text on the right.
+ */
+public class ItemController extends AbstractDropdownController<Item> {
+
+ /**
+ * Constructs an item controller
+ *
+ * @param option bound option
+ */
+ public ItemController(Option<Item> option) {
+ super(option);
+ }
+
+ @Override
+ public String getString() {
+ return BuiltInRegistries.ITEM.getKey(option.pendingValue()).toString();
+ }
+
+ @Override
+ public void setFromString(String value) {
+ option.requestSet(ItemRegistryHelper.getItemFromName(value, option.pendingValue()));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Component formatValue() {
+ return Component.literal(getString());
+ }
+
+
+ @Override
+ public boolean isValueValid(String value) {
+ return ItemRegistryHelper.isRegisteredItem(value);
+ }
+
+ @Override
+ protected String getValidValue(String value, int offset) {
+ return ItemRegistryHelper.getMatchingItemIdentifiers(value)
+ .skip(offset)
+ .findFirst()
+ .map(ResourceLocation::toString)
+ .orElseGet(this::getString);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) {
+ return new ItemControllerElement(this, screen, widgetDimension);
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java
new file mode 100644
index 0000000..b0bf566
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java
@@ -0,0 +1,87 @@
+package dev.isxander.yacl3.gui.controllers.dropdown;
+
+import dev.isxander.yacl3.api.utils.Dimension;
+import dev.isxander.yacl3.gui.YACLScreen;
+import dev.isxander.yacl3.gui.utils.ItemRegistryHelper;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+public class ItemControllerElement extends AbstractDropdownControllerElement<Item, ResourceLocation> {
+ private final ItemController itemController;
+ protected Item currentItem = null;
+ protected Map<ResourceLocation, Item> matchingItems = new HashMap<>();
+
+
+ public ItemControllerElement(ItemController control, YACLScreen screen, Dimension<Integer> dim) {
+ super(control, screen, dim);
+ this.itemController = control;
+ }
+
+ @Override
+ protected void drawValueText(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
+ var oldDimension = getDimension();
+ setDimension(getDimension().withWidth(getDimension().width() - getDecorationPadding()));
+ super.drawValueText(graphics, mouseX, mouseY, delta);
+ setDimension(oldDimension);
+ if (currentItem != null) {
+ graphics.renderFakeItem(new ItemStack(currentItem), getDimension().xLimit() - getXPadding() - getDecorationPadding() + 2, getDimension().y() + 2);
+ }
+ }
+
+ @Override
+ public List<ResourceLocation> computeMatchingValues() {
+ List<ResourceLocation> identifiers = ItemRegistryHelper.getMatchingItemIdentifiers(inputField).toList();
+ currentItem = ItemRegistryHelper.getItemFromName(inputField, null);
+ for (ResourceLocation identifier : identifiers) {
+ matchingItems.put(identifier, BuiltInRegistries.ITEM.get(identifier));
+ }
+ return identifiers;
+ }
+
+ @Override
+ protected void renderDropdownEntry(GuiGraphics graphics, ResourceLocation identifier, int n) {
+ super.renderDropdownEntry(graphics, identifier, n);
+ graphics.renderFakeItem(new ItemStack(matchingItems.get(identifier)), getDimension().xLimit() - getDecorationPadding() - 2, getDimension().y() + n * getDimension().height() + 4);
+ }
+
+ @Override
+ public String getString(ResourceLocation identifier) {
+ return BuiltInRegistries.ITEM.get(identifier).toString();
+ }
+
+ @Override
+ protected int getDecorationPadding() {
+ return 16;
+ }
+
+ @Override
+ protected int getDropdownEntryPadding() {
+ return 4;
+ }
+
+ @Override
+ protected int getControlWidth() {
+ return super.getControlWidth() + getDecorationPadding();
+ }
+
+ @Override
+ protected Component getValueText() {
+ if (inputField.isEmpty() || itemController == null)
+ return super.getValueText();
+
+ if (inputFieldFocused)
+ return Component.literal(inputField);
+
+ return itemController.option().pendingValue().getDescription();
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/gui/utils/ItemRegistryHelper.java b/common/src/main/java/dev/isxander/yacl3/gui/utils/ItemRegistryHelper.java
new file mode 100644
index 0000000..5b52246
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/gui/utils/ItemRegistryHelper.java
@@ -0,0 +1,104 @@
+package dev.isxander.yacl3.gui.utils;
+
+
+import net.minecraft.ResourceLocationException;
+import net.minecraft.core.registries.BuiltInRegistries;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.Items;
+
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+public final class ItemRegistryHelper {
+
+ /**
+ * Checks whether the given string is an identifier referring to a known item
+ * @param identifier Item identifier, either of the format "namespace:path" or "path". If no namespace is included,
+ * the default vanilla namespace "minecraft" is used.
+ * @return true if the identifier refers to a registered item, false otherwise
+ */
+ public static boolean isRegisteredItem(String identifier) {
+ try {
+ ResourceLocation itemIdentifier = new ResourceLocation(identifier.toLowerCase());
+ return BuiltInRegistries.ITEM.containsKey(itemIdentifier);
+ } catch (ResourceLocationException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Looks up the item of the given identifier string.
+ * @param identifier Item identifier, either of the format "namespace:path" or "path". If no namespace is included,
+ * the default vanilla namespace "minecraft" is used.
+ * @param defaultItem Fallback item that gets returned if the identifier does not name a registered item.
+ * @return The item identified by the given string, or the fallback if the identifier is not known.
+ */
+ public static Item getItemFromName(String identifier, Item defaultItem) {
+ try {
+ ResourceLocation itemIdentifier = new ResourceLocation(identifier.toLowerCase());
+ if (BuiltInRegistries.ITEM.containsKey(itemIdentifier)) {
+ return BuiltInRegistries.ITEM.get(itemIdentifier);
+ }
+ } catch (ResourceLocationException ignored) { }
+ return defaultItem;
+ }
+ /**
+ * Looks up the item of the given identifier string.
+ * @param identifier Item identifier, either of the format "namespace:path" or "path". If no namespace is included,
+ * the default vanilla namespace "minecraft" is used.
+ * @return The item identified by the given string, or `Items.AIR` if the identifier is not known.
+ */
+ public static Item getItemFromName(String identifier) {
+ return getItemFromName(identifier, Items.AIR);
+ }
+
+ /**
+ * Returns a list of item identifiers matching the given string. The value matches an identifier if:
+ * <li>No namespace is provided in the value and the value is a substring of the path segment of any identifier,
+ * regardless of namespace.</li>
+ * <li>A namespace is provided, equals the identifier's namespace, and the value is the begin of the identifier's
+ * path segment.</li>
+ * @param value (partial) identifier, either of the format "namespace:path" or "path".
+ * @return list of matching item identifiers; empty if the given string does not correspond to any known identifiers
+ */
+ public static Stream<ResourceLocation> getMatchingItemIdentifiers(String value) {
+ int sep = value.indexOf(ResourceLocation.NAMESPACE_SEPARATOR);
+ Predicate<ResourceLocation> filterPredicate;
+ if (sep == -1) {
+ filterPredicate = identifier ->
+ identifier.getPath().contains(value)
+ || BuiltInRegistries.ITEM.get(identifier).getDescription().getString().toLowerCase().contains(value.toLowerCase());
+ } else {
+ String namespace = value.substring(0, sep);
+ String path = value.substring(sep + 1);
+ filterPredicate = identifier -> identifier.getNamespace().equals(namespace) && identifier.getPath().startsWith(path);
+ }
+ return BuiltInRegistries.ITEM.keySet().stream()
+ .filter(filterPredicate)
+ /*
+ Sort items as follows based on the given "value" string's path:
+ - if both items' paths begin with the entered string, sort the identifiers (including namespace)
+ - otherwise, if either of the items' path begins with the entered string, sort it to the left
+ - else neither path matches: sort by identifiers again
+
+ This allows the user to enter "diamond_ore" and match "minecraft:diamond_ore" before
+ "minecraft:deepslate_diamond_ore", even though the second is lexicographically smaller
+ */
+ .sorted((id1, id2) -> {
+ String path = (sep == -1 ? value : value.substring(sep + 1));
+ boolean id1StartsWith = id1.getPath().startsWith(path);
+ boolean id2StartsWith = id2.getPath().startsWith(path);
+ if (id1StartsWith) {
+ if (id2StartsWith) {
+ return id1.compareTo(id2);
+ }
+ return -1;
+ }
+ if (id2StartsWith) {
+ return 1;
+ }
+ return id1.compareTo(id2);
+ });
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/impl/controller/DropdownStringControllerBuilderImpl.java b/common/src/main/java/dev/isxander/yacl3/impl/controller/DropdownStringControllerBuilderImpl.java
new file mode 100644
index 0000000..b300a6a
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/impl/controller/DropdownStringControllerBuilderImpl.java
@@ -0,0 +1,49 @@
+package dev.isxander.yacl3.impl.controller;
+
+import dev.isxander.yacl3.api.Controller;
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.controller.DropdownStringControllerBuilder;
+import dev.isxander.yacl3.gui.controllers.dropdown.DropdownStringController;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class DropdownStringControllerBuilderImpl extends StringControllerBuilderImpl implements DropdownStringControllerBuilder {
+ private List<String> values;
+ private boolean allowEmptyValue = false;
+ private boolean allowAnyValue = false;
+
+ public DropdownStringControllerBuilderImpl(Option<String> option) {
+ super(option);
+ }
+
+ @Override
+ public DropdownStringControllerBuilder values(List<String> values) {
+ this.values = values;
+ return this;
+ }
+
+ @Override
+ public DropdownStringControllerBuilderImpl values(String... values) {
+ this.values = Arrays.asList(values);
+ return this;
+ }
+
+ @Override
+ public DropdownStringControllerBuilderImpl allowEmptyValue(boolean allowEmptyValue) {
+ this.allowEmptyValue = allowEmptyValue;
+ return this;
+ }
+
+ @Override
+ public DropdownStringControllerBuilderImpl allowAnyValue(boolean allowAnyValue) {
+ this.allowAnyValue = allowAnyValue;
+ return this;
+ }
+
+ @Override
+ public Controller<String> build() {
+ return new DropdownStringController(option, values, allowEmptyValue, allowAnyValue);
+ }
+
+}
diff --git a/common/src/main/java/dev/isxander/yacl3/impl/controller/ItemControllerBuilderImpl.java b/common/src/main/java/dev/isxander/yacl3/impl/controller/ItemControllerBuilderImpl.java
new file mode 100644
index 0000000..9a817fb
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl3/impl/controller/ItemControllerBuilderImpl.java
@@ -0,0 +1,18 @@
+package dev.isxander.yacl3.impl.controller;
+
+import dev.isxander.yacl3.api.Controller;
+import dev.isxander.yacl3.api.Option;
+import dev.isxander.yacl3.api.controller.ItemControllerBuilder;
+import dev.isxander.yacl3.gui.controllers.dropdown.ItemController;
+import net.minecraft.world.item.Item;
+
+public class ItemControllerBuilderImpl extends AbstractControllerBuilderImpl<Item> implements ItemControllerBuilder {
+ public ItemControllerBuilderImpl(Option<Item> option) {
+ super(option);
+ }
+
+ @Override
+ public Controller<Item> build() {
+ return new ItemController(option);
+ }
+}