diff options
5 files changed, 325 insertions, 110 deletions
diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/PopupControllerScreen.java b/src/main/java/dev/isxander/yacl3/gui/controllers/PopupControllerScreen.java index f6a5db3..bb860f0 100644 --- a/src/main/java/dev/isxander/yacl3/gui/controllers/PopupControllerScreen.java +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/PopupControllerScreen.java @@ -47,11 +47,19 @@ public class PopupControllerScreen extends Screen { @Override public boolean mouseScrolled(double mouseX, double mouseY, /*? if >1.20.1 {*/ double scrollX, /*?}*/ double scrollY) { + if (controllerPopup.mouseScrolled(mouseX, mouseY, /*? if >1.20.1 {*/ scrollX, /*?}*/ scrollY)) { + return true; + } backgroundYaclScreen.mouseScrolled(mouseX, mouseY, /*? if >1.20.1 {*/ scrollX, /*?}*/ scrollY); //mouseX & mouseY are needed here return super.mouseScrolled(mouseX, mouseY, /*? if >1.20.1 {*/ scrollX, /*?}*/ scrollY); } @Override + public void mouseMoved(double mouseX, double mouseY) { + controllerPopup.mouseMoved(mouseX, mouseY); + } + + @Override public boolean charTyped(char codePoint, int modifiers) { return controllerPopup.charTyped(codePoint, modifiers); } diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java b/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java index 49e0c0e..7e73332 100644 --- a/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/AbstractDropdownControllerElement.java @@ -1,7 +1,6 @@ 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; @@ -11,18 +10,16 @@ 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 DropdownWidget<T> dropdownWidget; + 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; @@ -32,26 +29,24 @@ public abstract class AbstractDropdownControllerElement<T, U> extends StringCont 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(); + if (!dropdownController.isValueValid(inputField)) { + if (dropdownWidget == null) { + inputField = dropdownController.getValidValue(inputField); + } else { + inputField = dropdownController.getValidValue(inputField, dropdownWidget.selectedIndex()); + dropdownWidget.resetSelectedIndex(); + } + caretPos = getDefaultCaretPos(); + this.matchingValues = this.computeMatchingValues(); + } } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { if (super.mouseClicked(mouseX, mouseY, button)) { if (!dropdownVisible) { - showDropdown(); + createDropdownWidget(); doSelectAll(); } return true; @@ -69,7 +64,9 @@ public abstract class AbstractDropdownControllerElement<T, U> extends StringCont @Override public void unfocus() { - closeDropdown(); + if (dropdownVisible) { + removeDropdownWidget(); + } super.unfocus(); } @@ -80,25 +77,25 @@ public abstract class AbstractDropdownControllerElement<T, U> extends StringCont if (dropdownVisible) { switch (keyCode) { case InputConstants.KEY_DOWN -> { - selectNextEntry(); + dropdownWidget.selectNextEntry(); return true; } case InputConstants.KEY_UP -> { - selectPreviousEntry(); + dropdownWidget.selectPreviousEntry(); return true; } case InputConstants.KEY_TAB -> { if (Screen.hasShiftDown()) { - selectPreviousEntry(); + dropdownWidget.selectPreviousEntry(); } else { - selectNextEntry(); + dropdownWidget.selectNextEntry(); } return true; } } } else { if (keyCode == InputConstants.KEY_RETURN || keyCode == InputConstants.KEY_NUMPADENTER) { - showDropdown(); + createDropdownWidget(); return true; } } @@ -108,7 +105,7 @@ public abstract class AbstractDropdownControllerElement<T, U> extends StringCont @Override public boolean charTyped(char chr, int modifiers) { if (!dropdownVisible) { - showDropdown(); + createDropdownWidget(); } return super.charTyped(chr, modifiers); } @@ -123,26 +120,6 @@ public abstract class AbstractDropdownControllerElement<T, U> extends StringCont 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); @@ -161,61 +138,46 @@ public abstract class AbstractDropdownControllerElement<T, U> extends StringCont @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.isEmpty()) 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.isEmpty()) { - // 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; - } + super.render(graphics, mouseX, mouseY, delta); } - protected int getDropdownEntryPadding() { - return 0; + void renderDropdownEntry(GuiGraphics graphics, Dimension<Integer> entryDimension, int index) { + renderDropdownEntry(graphics, entryDimension, matchingValues.get(index)); } - - protected void renderDropdownEntry(GuiGraphics graphics, U value, int n) { + protected void renderDropdownEntry(GuiGraphics graphics, Dimension<Integer> entryDimension, U value) { 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); + graphics.drawString( + textRenderer, + text, + entryDimension.xLimit() - textRenderer.width(text) - getDropdownEntryPadding(), + getTextY(entryDimension), + -1, + true + ); + } + + protected int getTextY(Dimension<Integer> dim) { + return (int)(dim.y() + dim.height() / 2f - textRenderer.lineHeight / 2f); + } + + @Override + public void setDimension(Dimension<Integer> dim) { + super.setDimension(dim); + + if (dropdownWidget != null) { + dropdownWidget.setDimension(dropdownWidget.getDimension().withY(this.getDimension().y())); + // checks if the popup is being partially rendered offscreen + if (this.getDimension().y() < screen.tabArea.top() || this.getDimension().yLimit() > screen.tabArea.bottom()) { + removeDropdownWidget(); + } + } } public abstract String getString(U object); @@ -224,25 +186,32 @@ public abstract class AbstractDropdownControllerElement<T, U> extends StringCont 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( - /*? if >1.20.4 {*//* - Screen.MENU_BACKGROUND, - *//*?} else {*/ - 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(); } + protected int getDropdownEntryPadding() { + return 0; + } + + public void createDropdownWidget() { + dropdownVisible = true; + dropdownWidget = new DropdownWidget<>(dropdownController, screen, getDimension(), this); + screen.addPopupControllerWidget(dropdownWidget); + } + + public DropdownWidget<T> dropdownWidget() { + return dropdownWidget; + } + + public boolean isDropdownVisible() { + return dropdownVisible; + } + + public void removeDropdownWidget() { + ensureValidValue(); + screen.clearPopupControllerWidget(); + this.dropdownVisible = false; + this.dropdownWidget = null; + } } diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java b/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java new file mode 100644 index 0000000..f16a1dd --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/DropdownWidget.java @@ -0,0 +1,233 @@ +package dev.isxander.yacl3.gui.controllers.dropdown; + +import com.mojang.blaze3d.vertex.PoseStack; +import dev.isxander.yacl3.api.utils.Dimension; +import dev.isxander.yacl3.api.utils.MutableDimension; +import dev.isxander.yacl3.gui.YACLScreen; +import dev.isxander.yacl3.gui.controllers.ControllerPopupWidget; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +public class DropdownWidget<T> extends ControllerPopupWidget<AbstractDropdownController<T>> { + public static final int MAX_SHOWN_NUMBER_OF_ITEMS = 7; + public static final int DROPDOWN_PADDING = 2; + + private final AbstractDropdownControllerElement<T, ?> dropdownElement; + + protected Dimension<Integer> dropdownDim; + + // Scroll offset in the list of possible values + protected int firstVisibleIndex = 0; + // 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; + + public DropdownWidget(AbstractDropdownController<T> control, YACLScreen screen, Dimension<Integer> dim, AbstractDropdownControllerElement<T, ?> dropdownElement) { + super(control, screen, dim, dropdownElement); + this.dropdownElement = dropdownElement; + + setDimension(dim); + } + + @Override + public void setDimension(Dimension<Integer> dim) { + super.setDimension(dim); + + // Set up the dropdown above the controller ... + int dropdownHeight = dim.height() * numberOfVisibleItems(); + int dropdownY = dim.y() - dropdownHeight - DROPDOWN_PADDING; + + // ... unless it doesn't fit, then place it below + if (dropdownY < screen.tabArea.top()) { + dropdownY = dim.yLimit() + DROPDOWN_PADDING; + } + + dropdownDim = Dimension.ofInt(dim.x(), dropdownY, dim.width(), dropdownHeight); + } + + public int entryHeight() { + return dropdownElement.getDimension().height(); + } + + @Override + public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + if (dropdownLength() == 0) return; + + PoseStack matrices = graphics.pose(); + matrices.pushPose(); + matrices.translate(0, 0, 200); + + // Background + graphics.setColor(0.25f, 0.25f, 0.25f, 1.0f); + graphics.blit( + /*? if >1.20.4 {*//* + Screen.MENU_BACKGROUND, + *//*?} else {*/ + Screen.BACKGROUND_LOCATION, + /*?}*/ + dropdownDim.x(), dropdownDim.y(), 0, + 0.0f, 0.0f, + dropdownDim.width(), dropdownDim.height(), + 32, 32 + ); + graphics.setColor(1.0f, 1.0f, 1.0f, 1.0f); + graphics.renderOutline(dropdownDim.x(), dropdownDim.y(), dropdownDim.width(), dropdownDim.height(), -1); + + // Highlight the currently selected element + graphics.setColor(0.0f, 0.0f, 0.0f, 0.5f); + int y = dropdownDim.y() + 2 + entryHeight() * selectedVisibleIndex(); + graphics.fill(dropdownDim.x(), y, dropdownDim.xLimit(), y + entryHeight(), -1); + graphics.setColor(1.0f, 1.0f, 1.0f, 1.0f); + graphics.renderOutline(dropdownDim.x(), y, dropdownDim.width(), entryHeight(), -1); + + // Render all visible elements + MutableDimension<Integer> entryDimension = Dimension.ofInt( + dropdownDim.x() - dropdownElement.getDecorationPadding(), + dropdownDim.y() + 2, + dropdownDim.width(), + entryHeight() + ); + for (int i = firstVisibleIndex; i < lastVisibleIndex(); ++i) { + dropdownElement.renderDropdownEntry(graphics, entryDimension, i); + entryDimension.move(0, entryHeight()); + } + + matrices.popPose(); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (isMouseOver(mouseX, mouseY)) { + // Closes and cleans up the dropdown + dropdownElement.unfocus(); + return true; + } else if (dropdownElement.isMouseOver(mouseX, mouseY)) { + return dropdownElement.mouseClicked(mouseX, mouseY, button); + } else { + close(); + return false; + } + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, /*? if >1.20.1 {*/ double scrollX, /*?}*/ double scrollY) { + if (isMouseOver(mouseX, mouseY)) { + if (scrollY < 0) { + scrollDown(); + } else { + scrollUp(); + } + return true; + } + return super.mouseScrolled(mouseX, mouseY, /*? if >1.20.1 {*/ scrollX, /*?}*/ scrollY); + } + + @Override + public void mouseMoved(double mouseX, double mouseY) { + if (isMouseOver(mouseX, mouseY)) { + int index = (int) ((mouseY - dropdownDim.y()) / entryHeight()); + selectVisibleItem(index); + } + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return dropdownDim.isPointInside((int) mouseX, (int) mouseY); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + // Done to allow for typing whilst the color picker is visible + return dropdownElement.charTyped(chr, modifiers); + } + + public int dropdownLength() { + return dropdownElement.matchingValues.size(); + } + + public int numberOfVisibleItems() { + return Math.min(MAX_SHOWN_NUMBER_OF_ITEMS, dropdownLength()); + } + + public int lastVisibleIndex() { + return Math.min(firstVisibleIndex + MAX_SHOWN_NUMBER_OF_ITEMS, dropdownLength()); + } + + public int selectedIndex() { + return selectedIndex; + } + public void resetSelectedIndex() { + selectedIndex = 0; + } + /** + * @return the offset of the selected element relative to the first visible entry + */ + public int selectedVisibleIndex() { + return selectedIndex - firstVisibleIndex; + } + + public void selectVisibleItem(int visibleIndex) { + selectedIndex = Math.min(firstVisibleIndex + visibleIndex, dropdownLength() - 1); + } + + public void selectNextEntry() { + if (selectedIndex == dropdownLength() - 1) { + selectedIndex = 0; + } else { + selectedIndex++; + } + if (selectedIndex - firstVisibleIndex >= MAX_SHOWN_NUMBER_OF_ITEMS / 2) { + centerOnSelectedItem(); + } + } + + public void selectPreviousEntry() { + if (selectedIndex == 0) { + selectedIndex = dropdownLength() - 1; + } else { + selectedIndex--; + } + if (selectedIndex - firstVisibleIndex <= MAX_SHOWN_NUMBER_OF_ITEMS / 2) { + centerOnSelectedItem(); + } + } + + private void centerOnSelectedItem() { + // 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 >= dropdownLength()) { + end = dropdownLength(); + begin = Math.max(0, end - MAX_SHOWN_NUMBER_OF_ITEMS); + } + firstVisibleIndex = begin; + } + + public void scrollDown() { + if (firstVisibleIndex + 1 + MAX_SHOWN_NUMBER_OF_ITEMS <= dropdownLength()) { + firstVisibleIndex++; + } + if (selectedIndex < firstVisibleIndex) selectedIndex = firstVisibleIndex; + } + public void scrollUp() { + if (firstVisibleIndex > 0) { + firstVisibleIndex--; + } + if (selectedIndex > firstVisibleIndex + MAX_SHOWN_NUMBER_OF_ITEMS - 1) { + selectedIndex = firstVisibleIndex + MAX_SHOWN_NUMBER_OF_ITEMS - 1; + } + } + + + @Override + public void close() { + dropdownElement.removeDropdownWidget(); + } + + @Override + public Component popupTitle() { + return Component.translatable("yacl.control.dropdown.dropdown_widget_title"); + } + +} diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java b/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java index 1617c41..2c19c13 100644 --- a/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/dropdown/ItemControllerElement.java @@ -49,9 +49,13 @@ public class ItemControllerElement extends AbstractDropdownControllerElement<Ite } @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); + protected void renderDropdownEntry(GuiGraphics graphics, Dimension<Integer> entryDimension, ResourceLocation identifier) { + super.renderDropdownEntry(graphics, entryDimension, identifier); + graphics.renderFakeItem( + new ItemStack(matchingItems.get(identifier)), + entryDimension.xLimit() - 2, + entryDimension.y() + 1 + ); } @Override diff --git a/src/main/resources/assets/yet_another_config_lib_v3/lang/en_us.json b/src/main/resources/assets/yet_another_config_lib_v3/lang/en_us.json index 6556648..5e54550 100644 --- a/src/main/resources/assets/yet_another_config_lib_v3/lang/en_us.json +++ b/src/main/resources/assets/yet_another_config_lib_v3/lang/en_us.json @@ -5,6 +5,7 @@ "yacl.control.action.execute": "EXECUTE", "yacl.control.color.color_picker_title": "Color Picker", + "yacl.control.dropdown.dropdown_widget_title": "Dropdown Chooser", "yacl.control.text.blank": "<blank>", |