From ead0b794ac57e9ab2558338f7f3da7545d2e12ff Mon Sep 17 00:00:00 2001 From: isXander Date: Mon, 22 May 2023 19:33:00 +0100 Subject: scrollable navbar, group descriptions, auto-scroll descriptions --- .../isxander/yacl/gui/OptionDescriptionWidget.java | 70 +++++++++++++++++-- .../dev/isxander/yacl/gui/OptionListWidget.java | 27 ++++---- .../isxander/yacl/gui/ScrollableNavigationBar.java | 79 ++++++++++++++++++++++ .../java/dev/isxander/yacl/gui/TabListWidget.java | 14 ++++ .../java/dev/isxander/yacl/gui/YACLScreen.java | 23 +++---- 5 files changed, 183 insertions(+), 30 deletions(-) create mode 100644 common/src/main/java/dev/isxander/yacl/gui/ScrollableNavigationBar.java (limited to 'common/src/main/java/dev/isxander/yacl/gui') diff --git a/common/src/main/java/dev/isxander/yacl/gui/OptionDescriptionWidget.java b/common/src/main/java/dev/isxander/yacl/gui/OptionDescriptionWidget.java index 882d75b..5c346d0 100644 --- a/common/src/main/java/dev/isxander/yacl/gui/OptionDescriptionWidget.java +++ b/common/src/main/java/dev/isxander/yacl/gui/OptionDescriptionWidget.java @@ -1,5 +1,7 @@ package dev.isxander.yacl.gui; +import com.mojang.blaze3d.Blaze3D; +import com.mojang.blaze3d.platform.InputConstants; import dev.isxander.yacl.api.OptionDescription; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; @@ -17,6 +19,9 @@ import java.util.List; import java.util.function.Supplier; public class OptionDescriptionWidget extends AbstractWidget { + private static final int AUTO_SCROLL_TIMER = 3000; + private static final float AUTO_SCROLL_SPEED = 1; + private @Nullable OptionDescription description; private List wrappedText; @@ -25,10 +30,13 @@ public class OptionDescriptionWidget extends AbstractWidget { private Supplier dimensions; - private int scrollAmount; + private float targetScrollAmount, currentScrollAmount; private int maxScrollAmount; private int descriptionY; + private int lastInteractionTime; + private boolean scrollingBackward; + public OptionDescriptionWidget(Supplier dimensions, @Nullable OptionDescription description) { super(0, 0, 0, 0, description == null ? Component.empty() : description.descriptiveName()); this.dimensions = dimensions; @@ -39,6 +47,8 @@ public class OptionDescriptionWidget extends AbstractWidget { public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) { if (description == null) return; + currentScrollAmount = Mth.lerp(delta * 0.5f, currentScrollAmount, targetScrollAmount); + ScreenRectangle dimensions = this.dimensions.get(); this.setX(dimensions.left()); this.setY(dimensions.top()); @@ -58,7 +68,7 @@ public class OptionDescriptionWidget extends AbstractWidget { graphics.enableScissor(getX(), y, getX() + getWidth(), getY() + getHeight()); - y -= scrollAmount; + y -= (int)currentScrollAmount; if (description.image().isDone()) { var image = description.image().join(); @@ -79,12 +89,19 @@ public class OptionDescriptionWidget extends AbstractWidget { graphics.disableScissor(); - maxScrollAmount = Math.max(0, y + scrollAmount - getY() - getHeight()); + maxScrollAmount = Math.max(0, y + (int)currentScrollAmount - getY() - getHeight()); + if (isHoveredOrFocused()) { + lastInteractionTime = currentTimeMS(); + } Style hoveredStyle = getDescStyle(mouseX, mouseY); if (hoveredStyle != null && hoveredStyle.getHoverEvent() != null) { graphics.renderComponentHoverEffect(font, hoveredStyle, mouseX, mouseY); } + + if (isFocused()) { + graphics.renderOutline(getX(), getY(), getWidth(), getHeight(), -1); + } } @Override @@ -104,12 +121,50 @@ public class OptionDescriptionWidget extends AbstractWidget { @Override public boolean mouseScrolled(double mouseX, double mouseY, double amount) { if (isMouseOver(mouseX, mouseY)) { - scrollAmount = Mth.clamp(scrollAmount - (int) amount * 10, 0, maxScrollAmount); + targetScrollAmount = Mth.clamp(targetScrollAmount - (int) amount * 10, 0, maxScrollAmount); + lastInteractionTime = currentTimeMS(); + return true; + } + return false; + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (isFocused()) { + switch (keyCode) { + case InputConstants.KEY_UP -> + targetScrollAmount = Mth.clamp(targetScrollAmount - 10, 0, maxScrollAmount); + case InputConstants.KEY_DOWN -> + targetScrollAmount = Mth.clamp(targetScrollAmount + 10, 0, maxScrollAmount); + default -> { + return false; + } + } return true; } return false; } + public void tick() { + float pxPerTick = AUTO_SCROLL_SPEED / 20f * font.lineHeight; + if (maxScrollAmount > 0 && currentTimeMS() - lastInteractionTime > AUTO_SCROLL_TIMER) { + if (scrollingBackward) { + pxPerTick *= -1; + if (targetScrollAmount + pxPerTick < 0) { + scrollingBackward = false; + lastInteractionTime = currentTimeMS(); + } + } else { + if (targetScrollAmount + pxPerTick > maxScrollAmount) { + scrollingBackward = true; + lastInteractionTime = currentTimeMS(); + } + } + + targetScrollAmount = Mth.clamp(targetScrollAmount + pxPerTick, 0, maxScrollAmount); + } + } + private Style getDescStyle(int mouseX, int mouseY) { if (!clicked(mouseX, mouseY)) return null; @@ -135,5 +190,12 @@ public class OptionDescriptionWidget extends AbstractWidget { public void setOptionDescription(OptionDescription description) { this.description = description; this.wrappedText = null; + this.targetScrollAmount = 0; + this.currentScrollAmount = 0; + this.lastInteractionTime = currentTimeMS(); + } + + private int currentTimeMS() { + return (int)(Blaze3D.getTime() * 1000); } } diff --git a/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java b/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java index 390e6c0..98272d1 100644 --- a/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java +++ b/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java @@ -27,10 +27,10 @@ public class OptionListWidget extends ElementListWidgetExt viewableChildren; private String searchQuery = ""; - private final Consumer> hoverEvent; - private Option lastHoveredOption; + private final Consumer hoverEvent; + private OptionDescription lastHoveredOption; - public OptionListWidget(YACLScreen screen, ConfigCategory category, Minecraft client, int x, int y, int width, int height, Consumer> hoverEvent) { + public OptionListWidget(YACLScreen screen, ConfigCategory category, Minecraft client, int x, int y, int width, int height, Consumer hoverEvent) { super(client, x, y, width, height, true); this.yaclScreen = screen; this.category = category; @@ -237,6 +237,13 @@ public class OptionListWidget extends ElementListWidgetExt { public boolean isViewable() { return true; @@ -292,11 +299,8 @@ public class OptionListWidget extends ElementListWidgetExt iterable) { + super(i, tabManager, iterable); + } + + @Override + public void arrangeElements() { + int noScrollWidth = this.width - NAVBAR_MARGIN*2; + int minimumSize = tabButtons.stream() + .map(AbstractWidget::getMessage) + .mapToInt(label -> font.width(label) + 3) + .min().orElse(0); + int singleTabWidth = Math.max(noScrollWidth / Math.min(this.tabButtons.size(), 3), minimumSize); + for (TabButton tabButton : this.tabButtons) { + tabButton.setWidth(singleTabWidth); + } + + this.layout.arrangeElements(); + this.layout.setY(0); + this.scrollOffset = 0; + + int allTabsWidth = singleTabWidth * this.tabButtons.size(); + this.layout.setX(Math.max((this.width - allTabsWidth) / 2, NAVBAR_MARGIN)); + this.maxScrollOffset = Math.max(0, allTabsWidth - noScrollWidth); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double amount) { + this.setScrollOffset(this.scrollOffset - (int)(amount*10)); + return true; + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return mouseY <= 24; + } + + public void setScrollOffset(int scrollOffset) { + layout.setX(layout.getX() + this.scrollOffset); + this.scrollOffset = Mth.clamp(scrollOffset, 0, maxScrollOffset); + layout.setX(layout.getX() - this.scrollOffset); + } + + @Override + public void setFocused(@Nullable GuiEventListener child) { + super.setFocused(child); + if (child instanceof TabButton tabButton) { + this.ensureVisible(tabButton); + } + } + + protected void ensureVisible(TabButton tabButton) { + if (tabButton.getX() < NAVBAR_MARGIN) { + this.setScrollOffset(this.scrollOffset - (NAVBAR_MARGIN - tabButton.getX())); + } else if (tabButton.getX() + tabButton.getWidth() > this.width - NAVBAR_MARGIN) { + this.setScrollOffset(this.scrollOffset + (tabButton.getX() + tabButton.getWidth() - (this.width - NAVBAR_MARGIN))); + } + } +} diff --git a/common/src/main/java/dev/isxander/yacl/gui/TabListWidget.java b/common/src/main/java/dev/isxander/yacl/gui/TabListWidget.java index b9e756c..041c06a 100644 --- a/common/src/main/java/dev/isxander/yacl/gui/TabListWidget.java +++ b/common/src/main/java/dev/isxander/yacl/gui/TabListWidget.java @@ -1,11 +1,13 @@ package dev.isxander.yacl.gui; import com.google.common.collect.ImmutableList; +import net.minecraft.client.gui.ComponentPath; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.events.ContainerEventHandler; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.FocusNavigationEvent; import net.minecraft.client.gui.navigation.ScreenRectangle; import net.minecraft.network.chat.CommonComponents; import org.jetbrains.annotations.Nullable; @@ -102,4 +104,16 @@ public class TabListWidget> extends AbstractWi public void setFocused(@Nullable GuiEventListener listener) { this.list.setFocused(listener); } + + @Nullable + @Override + public ComponentPath nextFocusPath(FocusNavigationEvent event) { + return this.list.nextFocusPath(event); + } + + @Nullable + @Override + public ComponentPath getCurrentFocusPath() { + return this.list.getCurrentFocusPath(); + } } diff --git a/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java b/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java index 0ce98ba..a72640e 100644 --- a/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java +++ b/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java @@ -56,17 +56,13 @@ public class YACLScreen extends Screen { @Override protected void init() { - tabNavigationBar = TabNavigationBar.builder(tabManager, this.width) - .addTabs(tabs = config.categories() - .stream() - .map(category -> { - if (category instanceof PlaceholderCategory placeholder) - return new PlaceholderTab(placeholder); - return new CategoryTab(category); - }) - .toArray(Tab[]::new) - ) - .build(); + tabNavigationBar = new ScrollableNavigationBar(this.width, tabManager, config.categories() + .stream() + .map(category -> { + if (category instanceof PlaceholderCategory placeholder) + return new PlaceholderTab(placeholder); + return new CategoryTab(category); + }).toList()); tabNavigationBar.selectTab(0, false); tabNavigationBar.arrangeElements(); ScreenRectangle navBarArea = tabNavigationBar.getRectangle(); @@ -239,8 +235,8 @@ public class YACLScreen extends Screen { this.optionList = new TabListWidget<>( () -> new ScreenRectangle(tabArea.position(), tabArea.width() / 3 * 2 + 1, tabArea.height()), - new OptionListWidget(YACLScreen.this, category, minecraft, 0, 0, width / 3 * 2 + 1, height, hoveredOption -> { - descriptionWidget.setOptionDescription(hoveredOption.description()); + new OptionListWidget(YACLScreen.this, category, minecraft, 0, 0, width / 3 * 2 + 1, height, desc -> { + descriptionWidget.setOptionDescription(desc); }) ); @@ -316,6 +312,7 @@ public class YACLScreen extends Screen { public void tick() { updateButtons(); searchField.tick(); + descriptionWidget.tick(); } private void updateButtons() { -- cgit