diff options
author | Crendgrim <Crendgrim@users.noreply.github.com> | 2023-08-31 00:06:47 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-30 23:06:47 +0100 |
commit | 554646dbd857e2fab1be8339ce8d0231ef2dbb4c (patch) | |
tree | b55e6f1d282c18e743857bba3c9d5e4f226200ed /common/src/main/java/dev/isxander/yacl3/gui/controllers | |
parent | c11566072608683034864dbd4e0d3f5afa067537 (diff) | |
download | YetAnotherConfigLib-554646dbd857e2fab1be8339ce8d0231ef2dbb4c.tar.gz YetAnotherConfigLib-554646dbd857e2fab1be8339ce8d0231ef2dbb4c.tar.bz2 YetAnotherConfigLib-554646dbd857e2fab1be8339ce8d0231ef2dbb4c.zip |
Add dropdown controllers (#95)
Diffstat (limited to 'common/src/main/java/dev/isxander/yacl3/gui/controllers')
6 files changed, 538 insertions, 0 deletions
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(); + } +} |