aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorisXander <xandersmith2008@gmail.com>2023-05-21 12:41:45 +0100
committerisXander <xandersmith2008@gmail.com>2023-05-21 12:41:45 +0100
commit21afea0da3956f2d8cca81a54fa9820152e0c077 (patch)
treee5944f94a5f85d3fcbe048da633e62f5357fe835
parente51af159ba3eba5ebda976bea1c1957cddeee7c6 (diff)
downloadYetAnotherConfigLib-21afea0da3956f2d8cca81a54fa9820152e0c077.tar.gz
YetAnotherConfigLib-21afea0da3956f2d8cca81a54fa9820152e0c077.tar.bz2
YetAnotherConfigLib-21afea0da3956f2d8cca81a54fa9820152e0c077.zip
Start overhauling UI
-rw-r--r--common/build.gradle.kts5
-rw-r--r--common/src/main/java/dev/isxander/yacl/api/Option.java9
-rw-r--r--common/src/main/java/dev/isxander/yacl/api/OptionDescription.java39
-rw-r--r--common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java100
-rw-r--r--common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java38
-rw-r--r--common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java63
-rw-r--r--common/src/main/java/dev/isxander/yacl/gui/ImageRenderer.java258
-rw-r--r--common/src/main/java/dev/isxander/yacl/gui/OptionDescriptionWidget.java156
-rw-r--r--common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java116
-rw-r--r--common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java23
-rw-r--r--common/src/main/java/dev/isxander/yacl/gui/TabListWidget.java105
-rw-r--r--common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java315
-rw-r--r--common/src/main/java/dev/isxander/yacl/gui/YACLTooltipPositioner.java48
-rw-r--r--common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java17
-rw-r--r--common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java10
-rw-r--r--common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java5
-rw-r--r--common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java15
-rw-r--r--common/src/main/java/dev/isxander/yacl/impl/OptionDescriptionImpl.java124
-rw-r--r--common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java40
-rw-r--r--fabric/build.gradle.kts3
-rw-r--r--gradle/libs.versions.toml9
-rw-r--r--test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java133
-rw-r--r--test-fabric/build.gradle.kts3
23 files changed, 1153 insertions, 481 deletions
diff --git a/common/build.gradle.kts b/common/build.gradle.kts
index f725a17..e84ba2c 100644
--- a/common/build.gradle.kts
+++ b/common/build.gradle.kts
@@ -26,6 +26,11 @@ dependencies {
officialMojangMappings()
})
modImplementation(libs.fabric.loader)
+
+ implementation(libs.twelvemonkeys.imageio.core)
+ implementation(libs.twelvemonkeys.imageio.webp)
+ include(libs.twelvemonkeys.imageio.core)
+ include(libs.twelvemonkeys.imageio.webp)
}
java {
diff --git a/common/src/main/java/dev/isxander/yacl/api/Option.java b/common/src/main/java/dev/isxander/yacl/api/Option.java
index a6c0311..5f66c19 100644
--- a/common/src/main/java/dev/isxander/yacl/api/Option.java
+++ b/common/src/main/java/dev/isxander/yacl/api/Option.java
@@ -17,10 +17,13 @@ public interface Option<T> {
*/
@NotNull Component name();
+ @NotNull OptionDescription description();
+
/**
* Tooltip (or description) of the option.
* Rendered on hover.
*/
+ @Deprecated
@NotNull Component tooltip();
/**
@@ -126,12 +129,17 @@ public interface Option<T> {
*/
Builder<T> name(@NotNull Component name);
+ Builder<T> description(@NotNull OptionDescription description);
+
+ Builder<T> description(@NotNull Function<T, OptionDescription> descriptionFunction);
+
/**
* Sets the tooltip to be used by the option.
* No need to wrap the text yourself, the gui does this itself.
*
* @param tooltipGetter function to get tooltip depending on value {@link Builder#build()}.
*/
+ @Deprecated
Builder<T> tooltip(@NotNull Function<T, Component> tooltipGetter);
/**
@@ -150,6 +158,7 @@ public interface Option<T> {
*
* @param tooltips text lines - merged with a new-line on {@link Builder#build()}.
*/
+ @Deprecated
Builder<T> tooltip(@NotNull Component... tooltips);
/**
diff --git a/common/src/main/java/dev/isxander/yacl/api/OptionDescription.java b/common/src/main/java/dev/isxander/yacl/api/OptionDescription.java
new file mode 100644
index 0000000..3b28a65
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl/api/OptionDescription.java
@@ -0,0 +1,39 @@
+package dev.isxander.yacl.api;
+
+import dev.isxander.yacl.gui.ImageRenderer;
+import dev.isxander.yacl.impl.OptionDescriptionImpl;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+public interface OptionDescription {
+ Component descriptiveName();
+
+ Component description();
+
+ CompletableFuture<Optional<ImageRenderer>> image();
+
+ static Builder createBuilder() {
+ return new OptionDescriptionImpl.BuilderImpl();
+ }
+
+ interface Builder {
+ Builder name(Component name);
+
+ Builder description(Component description);
+
+ Builder image(ResourceLocation image, int width, int height);
+ Builder image(Path path, ResourceLocation uniqueLocation);
+
+ Builder gifImage(ResourceLocation image);
+ Builder gifImage(Path path, ResourceLocation uniqueLocation);
+
+ Builder webpImage(ResourceLocation image, int frameDelayMS);
+ Builder webpImage(Path path, ResourceLocation uniqueLocation, int frameDelayMS);
+
+ OptionDescription build();
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java b/common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java
deleted file mode 100644
index 6668584..0000000
--- a/common/src/main/java/dev/isxander/yacl/gui/CategoryListWidget.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package dev.isxander.yacl.gui;
-
-import com.google.common.collect.ImmutableList;
-import com.mojang.blaze3d.systems.RenderSystem;
-import com.mojang.blaze3d.vertex.PoseStack;
-import dev.isxander.yacl.api.ConfigCategory;
-import dev.isxander.yacl.gui.utils.GuiUtils;
-import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.GuiGraphics;
-import net.minecraft.client.gui.components.events.GuiEventListener;
-import net.minecraft.client.gui.narration.NarratableEntry;
-
-import java.util.List;
-
-public class CategoryListWidget extends ElementListWidgetExt<CategoryListWidget.CategoryEntry> {
- private final YACLScreen yaclScreen;
-
- public CategoryListWidget(Minecraft client, YACLScreen yaclScreen, int screenWidth, int screenHeight) {
- super(client, 0, 0, screenWidth / 3, yaclScreen.searchFieldWidget.getY() - 5, true);
- this.yaclScreen = yaclScreen;
- setRenderBackground(false);
- setRenderTopAndBottom(false);
-
- for (ConfigCategory category : yaclScreen.config.categories()) {
- addEntry(new CategoryEntry(category));
- }
- }
-
- @Override
- public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
- GuiUtils.enableScissor(0, 0, width, height);
- super.render(graphics, mouseX, mouseY, delta);
- RenderSystem.disableScissor();
- }
-
- @Override
- public int getRowWidth() {
- return Math.min(width - width / 10, 396);
- }
-
- @Override
- public int getRowLeft() {
- return super.getRowLeft() - 2;
- }
-
- @Override
- protected int getScrollbarPosition() {
- return width - 2;
- }
-
- @Override
- protected void renderBackground(GuiGraphics graphics) {
-
- }
-
- public class CategoryEntry extends Entry<CategoryEntry> {
- private final CategoryWidget categoryButton;
- public final int categoryIndex;
-
- public CategoryEntry(ConfigCategory category) {
- this.categoryIndex = yaclScreen.config.categories().indexOf(category);
- categoryButton = new CategoryWidget(
- yaclScreen,
- category,
- categoryIndex,
- getRowLeft(), 0,
- getRowWidth(), 20
- );
- }
-
- @Override
- public void render(GuiGraphics graphics, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) {
- if (mouseY > y1) {
- mouseY = -20;
- }
-
- categoryButton.setY(y);
- categoryButton.render(graphics, mouseX, mouseY, tickDelta);
- }
-
- public void postRender(GuiGraphics graphics, int mouseX, int mouseY, float tickDelta) {
- categoryButton.renderHoveredTooltip(graphics);
- }
-
- @Override
- public int getItemHeight() {
- return 21;
- }
-
- @Override
- public List<? extends GuiEventListener> children() {
- return ImmutableList.of(categoryButton);
- }
-
- @Override
- public List<? extends NarratableEntry> narratables() {
- return ImmutableList.of(categoryButton);
- }
- }
-}
diff --git a/common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java b/common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java
deleted file mode 100644
index 60817a2..0000000
--- a/common/src/main/java/dev/isxander/yacl/gui/CategoryWidget.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package dev.isxander.yacl.gui;
-
-import dev.isxander.yacl.api.ConfigCategory;
-import net.minecraft.client.sounds.SoundManager;
-
-public class CategoryWidget extends TooltipButtonWidget {
- private final int categoryIndex;
-
- public CategoryWidget(YACLScreen screen, ConfigCategory category, int categoryIndex, int x, int y, int width, int height) {
- super(screen, x, y, width, height, category.name(), category.tooltip(), btn -> {
- screen.searchFieldWidget.setValue("");
- screen.changeCategory(categoryIndex);
- });
- this.categoryIndex = categoryIndex;
- }
-
- private boolean isCurrentCategory() {
- return ((YACLScreen) screen).getCurrentCategoryIdx() == categoryIndex;
- }
-
- @Override
- protected int getTextureY() {
- int i = 1;
- if (!this.active) {
- i = 0;
- } else if (this.isHoveredOrFocused() || isCurrentCategory()) {
- i = 2;
- }
-
- return 46 + i * 20;
- }
-
- @Override
- public void playDownSound(SoundManager soundManager) {
- if (!isCurrentCategory())
- super.playDownSound(soundManager);
- }
-}
diff --git a/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java b/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java
index cb9fc87..ffeffbf 100644
--- a/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java
+++ b/common/src/main/java/dev/isxander/yacl/gui/ElementListWidgetExt.java
@@ -3,13 +3,18 @@ package dev.isxander.yacl.gui;
import com.mojang.blaze3d.platform.InputConstants;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.components.ContainerObjectSelectionList;
import net.minecraft.client.gui.components.events.GuiEventListener;
+import net.minecraft.client.gui.layouts.LayoutElement;
+import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.util.Mth;
import org.jetbrains.annotations.Nullable;
-public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> extends ContainerObjectSelectionList<E> {
- protected final int x, y;
+import java.util.function.Consumer;
+
+public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> extends ContainerObjectSelectionList<E> implements LayoutElement {
+ protected int x, y;
private double smoothScrollAmount = getScrollAmount();
private boolean returnSmoothAmount = false;
@@ -33,9 +38,8 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten
@Override
protected void renderBackground(GuiGraphics graphics) {
// render transparent background if in-game.
- setRenderBackground(minecraft.level == null);
- if (minecraft.level != null)
- graphics.fill(x0, y0, x1, y1, 0x6B000000);
+ setRenderBackground(true);
+ setRenderTopAndBottom(false);
}
@Override
@@ -48,10 +52,23 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten
public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
smoothScrollAmount = Mth.lerp(Minecraft.getInstance().getDeltaFrameTime() * 0.5, smoothScrollAmount, getScrollAmount());
returnSmoothAmount = true;
+
+ graphics.enableScissor(x0, y0, x1, y1);
+
super.render(graphics, mouseX, mouseY, delta);
+
+ graphics.disableScissor();
+
returnSmoothAmount = false;
}
+ public void updateDimensions(ScreenRectangle rectangle) {
+ this.x0 = rectangle.left();
+ this.y0 = rectangle.top();
+ this.x1 = rectangle.right();
+ this.y1 = rectangle.bottom();
+ }
+
/**
* awful code to only use smooth scroll state when rendering,
* not other code that needs target scroll amount
@@ -141,6 +158,42 @@ public class ElementListWidgetExt<E extends ElementListWidgetExt.Entry<E>> exten
/* END cloth config code */
+ @Override
+ public void setX(int i) {
+ this.x = x0 = i;
+ this.x1 = x0 + width;
+ }
+
+ @Override
+ public void setY(int i) {
+ this.y = y0 = i;
+ this.y1 = y0 + height;
+ }
+
+ @Override
+ public int getX() {
+ return x;
+ }
+
+ @Override
+ public int getY() {
+ return y;
+ }
+
+ @Override
+ public int getWidth() {
+ return width;
+ }
+
+ @Override
+ public int getHeight() {
+ return height;
+ }
+
+ @Override
+ public void visitWidgets(Consumer<AbstractWidget> consumer) {
+ }
+
public abstract static class Entry<E extends Entry<E>> extends ContainerObjectSelectionList.Entry<E> {
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
diff --git a/common/src/main/java/dev/isxander/yacl/gui/ImageRenderer.java b/common/src/main/java/dev/isxander/yacl/gui/ImageRenderer.java
new file mode 100644
index 0000000..8ea8ba3
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl/gui/ImageRenderer.java
@@ -0,0 +1,258 @@
+package dev.isxander.yacl.gui;
+
+import com.mojang.blaze3d.Blaze3D;
+import com.mojang.blaze3d.platform.NativeImage;
+import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
+import net.fabricmc.loader.api.FabricLoader;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.renderer.texture.DynamicTexture;
+import net.minecraft.client.renderer.texture.TextureManager;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.packs.resources.Resource;
+import net.minecraft.server.packs.resources.ResourceManager;
+import net.minecraft.util.FastColor;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.metadata.IIOMetadataNode;
+import java.awt.image.BufferedImage;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.IntStream;
+
+public interface ImageRenderer extends AutoCloseable {
+ int render(GuiGraphics graphics, int x, int y, int width);
+
+ class TextureBacked implements ImageRenderer {
+ private final ResourceLocation location;
+ private final int width, height;
+
+ public TextureBacked(ResourceLocation location, int width, int height) {
+ this.location = location;
+ this.width = width;
+ this.height = height;
+ }
+
+ @Override
+ public int render(GuiGraphics graphics, int x, int y, int renderWidth) {
+ float ratio = renderWidth / (float)this.width;
+ int targetHeight = (int) (this.height * ratio);
+
+ graphics.pose().pushPose();
+ graphics.pose().translate(x, y, 0);
+ graphics.pose().scale(ratio, ratio, 1);
+ graphics.blit(location, 0, 0, 0, 0, this.width, this.height, this.width, this.height);
+ graphics.pose().popPose();
+
+ return targetHeight;
+ }
+
+ @Override
+ public void close() {
+
+ }
+ }
+
+ class NativeImageBacked implements ImageRenderer {
+ protected static final TextureManager textureManager = Minecraft.getInstance().getTextureManager();
+
+ protected NativeImage image;
+ protected DynamicTexture texture;
+ protected final ResourceLocation uniqueLocation;
+ protected int width, height;
+
+ public NativeImageBacked(NativeImage image, ResourceLocation uniqueLocation) {
+ this.image = image;
+ this.texture = new DynamicTexture(image);
+ this.uniqueLocation = uniqueLocation;
+ textureManager.register(this.uniqueLocation, this.texture);
+ this.width = image.getWidth();
+ this.height = image.getHeight();
+ }
+
+ private NativeImageBacked(Path imagePath, ResourceLocation uniqueLocation) throws IOException {
+ this.uniqueLocation = uniqueLocation;
+ this.image = NativeImage.read(new FileInputStream(imagePath.toFile()));
+ this.width = image.getWidth();
+ this.height = image.getHeight();
+ this.texture = new DynamicTexture(image);
+ textureManager.register(this.uniqueLocation, this.texture);
+ }
+
+ public static Optional<ImageRenderer> createFromPath(Path path, ResourceLocation uniqueLocation) {
+ try {
+ return Optional.of(new NativeImageBacked(path, uniqueLocation));
+ } catch (IOException e) {
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public int render(GuiGraphics graphics, int x, int y, int renderWidth) {
+ if (image == null) return 0;
+
+ float ratio = renderWidth / (float)this.width;
+ int targetHeight = (int) (this.height * ratio);
+
+ graphics.pose().pushPose();
+ graphics.pose().translate(x, y, 0);
+ graphics.pose().scale(ratio, ratio, 1);
+ graphics.blit(uniqueLocation, 0, 0, 0, 0, this.width, this.height, this.width, this.height);
+ graphics.pose().popPose();
+
+ return targetHeight;
+ }
+
+ @Override
+ public void close() {
+ image.close();
+ image = null;
+ texture = null;
+ textureManager.release(uniqueLocation);
+ }
+ }
+
+ class AnimatedNativeImageBacked extends NativeImageBacked {
+ private int currentFrame;
+ private double lastFrameTime;
+
+ private double frameDelay;
+ private int frameCount;
+
+ private int packCols, packRows;
+ private int frameWidth, frameHeight;
+
+ public AnimatedNativeImageBacked(NativeImage image, int frameWidth, int frameHeight, int frameCount, double frameDelayMS, int packCols, int packRows, ResourceLocation uniqueLocation) {
+ super(image, uniqueLocation);
+ this.frameWidth = frameWidth;
+ this.frameHeight = frameHeight;
+ this.frameCount = frameCount;
+ this.frameDelay = frameDelayMS;
+ this.packCols = packCols;
+ this.packRows = packRows;
+ }
+
+ public static AnimatedNativeImageBacked createGIFFromTexture(ResourceLocation textureLocation) throws IOException {
+ ResourceManager resourceManager = Minecraft.getInstance().getResourceManager();
+ Resource resource = resourceManager.getResource(textureLocation).orElseThrow();
+
+ return createGIF(resource.open(), textureLocation);
+ }
+
+ public static AnimatedNativeImageBacked createWEBPFromTexture(ResourceLocation textureLocation, int frameDelayMS) throws IOException {
+ ResourceManager resourceManager = Minecraft.getInstance().getResourceManager();
+ Resource resource = resourceManager.getResource(textureLocation).orElseThrow();
+
+ return createWEBP(resource.open(), textureLocation, frameDelayMS);
+ }
+
+ public static AnimatedNativeImageBacked createGIF(InputStream is, ResourceLocation uniqueLocation) {
+ try (is) {
+ ImageReader reader = ImageIO.getImageReadersBySuffix("gif").next();
+ reader.setInput(ImageIO.createImageInputStream(is));
+
+ IIOMetadata metadata = reader.getImageMetadata(0);
+ String metaFormatName = metadata.getNativeMetadataFormatName();
+ IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metaFormatName);
+ IIOMetadataNode graphicsControlExtensionNode = (IIOMetadataNode) root.getElementsByTagName("GraphicControlExtension").item(0);
+ int delay = Integer.parseInt(graphicsControlExtensionNode.getAttribute("delayTime")) * 10;
+
+ return createFromImageReader(reader, delay, uniqueLocation);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static AnimatedNativeImageBacked createWEBP(InputStream is, ResourceLocation uniqueLocation, int frameDelayMS) {
+ try (is) {
+ ImageReader reader = ImageIO.getImageReadersBySuffix("webp").next();
+ reader.setInput(ImageIO.createImageInputStream(is));
+ return createFromImageReader(reader, frameDelayMS, uniqueLocation);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static AnimatedNativeImageBacked createFromImageReader(ImageReader reader, int frameDelayMS, ResourceLocation uniqueLocation) throws IOException {
+ int frameCount = reader.getNumImages(true);
+
+ int frameWidth = reader.getWidth(0);
+ int frameHeight = reader.getHeight(0);
+
+ // Packs the frames into an optimal 1:1 texture.
+ // OpenGL can only have texture axis with a max of 32768 pixels,
+ // and packing them to that length is not efficient, apparently.
+ double ratio = frameWidth / (double)frameHeight;
+ int cols = (int)Math.ceil(Math.sqrt(frameCount) / Math.sqrt(ratio));
+ int rows = (int)Math.ceil(frameCount / (double)cols);
+
+ NativeImage image = new NativeImage(frameWidth * cols, frameHeight * rows, true);
+ for (int i = reader.getMinIndex(); i < frameCount - 1; i++) {
+ BufferedImage bi = reader.read(i);
+ for (int w = 0; w < bi.getWidth(); w++) {
+ for (int h = 0; h < bi.getHeight(); h++) {
+ int rgb = bi.getRGB(w, h);
+ int r = FastColor.ARGB32.red(rgb);
+ int g = FastColor.ARGB32.green(rgb);
+ int b = FastColor.ARGB32.blue(rgb);
+
+ int col = i % cols;
+ int row = (int) Math.floor(i / (double)cols);
+
+ image.setPixelRGBA(
+ bi.getWidth() * col + w,
+ bi.getHeight() * row + h,
+ FastColor.ABGR32.color(255, b, g, r) // NativeImage uses ABGR for some reason
+ );
+ }
+ }
+ }
+ image.upload(0, 0, 0, false);
+
+ return new AnimatedNativeImageBacked(image, frameWidth, frameHeight, frameCount, frameDelayMS, cols, rows, uniqueLocation);
+ }
+
+ @Override
+ public int render(GuiGraphics graphics, int x, int y, int renderWidth) {
+ if (image == null) return 0;
+
+ float ratio = renderWidth / (float)frameWidth;
+ int targetHeight = (int) (frameHeight * ratio);
+
+ int currentCol = currentFrame % packCols;
+ int currentRow = (int) Math.floor(currentFrame / (double)packCols);
+
+ graphics.pose().pushPose();
+ graphics.pose().translate(x, y, 0);
+ graphics.pose().scale(ratio, ratio, 1);
+ graphics.blit(
+ uniqueLocation,
+ 0, 0,
+ frameWidth * currentCol, frameHeight * currentRow,
+ frameWidth, frameHeight,
+ this.width, this.height
+ );
+ graphics.pose().popPose();
+
+ double timeMS = Blaze3D.getTime() * 1000;
+ if (lastFrameTime == 0) lastFrameTime = timeMS;
+ if (timeMS - lastFrameTime >= frameDelay) {
+ currentFrame++;
+ lastFrameTime = timeMS;
+ }
+ if (currentFrame >= frameCount) currentFrame = 0;
+
+ return targetHeight;
+ }
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl/gui/OptionDescriptionWidget.java b/common/src/main/java/dev/isxander/yacl/gui/OptionDescriptionWidget.java
new file mode 100644
index 0000000..d9ad045
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl/gui/OptionDescriptionWidget.java
@@ -0,0 +1,156 @@
+package dev.isxander.yacl.gui;
+
+import dev.isxander.yacl.api.OptionDescription;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.Font;
+import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.components.AbstractWidget;
+import net.minecraft.client.gui.components.MultiLineLabel;
+import net.minecraft.client.gui.narration.NarrationElementOutput;
+import net.minecraft.client.gui.navigation.ScreenRectangle;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.Style;
+import net.minecraft.util.FormattedCharSequence;
+import net.minecraft.util.Mth;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+public class OptionDescriptionWidget extends AbstractWidget implements AutoCloseable {
+ private @Nullable OptionDescription description;
+ private List<FormattedCharSequence> wrappedText;
+
+ private static final Minecraft minecraft = Minecraft.getInstance();
+ private static final Font font = minecraft.font;
+
+ private Supplier<ScreenRectangle> dimensions;
+
+ private int scrollAmount;
+ private int maxScrollAmount;
+ private int descriptionY;
+
+ public OptionDescriptionWidget(Supplier<ScreenRectangle> dimensions, @Nullable OptionDescription description) {
+ super(0, 0, 0, 0, description == null ? Component.empty() : description.descriptiveName());
+ this.dimensions = dimensions;
+ this.setOptionDescription(description);
+ }
+
+ @Override
+ public void renderWidget(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
+ if (description == null) return;
+
+ ScreenRectangle dimensions = this.dimensions.get();
+ this.setX(dimensions.left());
+ this.setY(dimensions.top());
+ this.width = dimensions.width();
+ this.height = dimensions.height();
+
+ int y = getY();
+
+ int nameWidth = font.width(description.descriptiveName());
+ if (nameWidth > getWidth()) {
+ renderScrollingString(graphics, font, description.descriptiveName(), getX(), y, getX() + getWidth(), y + font.lineHeight, -1);
+ } else {
+ graphics.drawString(font, description.descriptiveName(), getX(), y, 0xFFFFFF);
+ }
+
+ y += 5 + font.lineHeight;
+
+ graphics.enableScissor(getX(), y, getX() + getWidth(), getY() + getHeight());
+
+ y -= scrollAmount;
+
+ if (description.image().isDone()) {
+ var image = description.image().join();
+ if (image.isPresent()) {
+ image.get().render(graphics, getX(), y, getWidth());
+ y += image.get().render(graphics, getX(), y, getWidth()) + 5;
+ }
+ }
+
+ if (wrappedText == null && description.description() != null)
+ wrappedText = font.split(description.description(), getWidth());
+
+ descriptionY = y;
+ for (var line : wrappedText) {
+ graphics.drawString(font, line, getX(), y, 0xFFFFFF);
+ y += font.lineHeight;
+ }
+
+ graphics.disableScissor();
+
+ maxScrollAmount = Math.max(0, y + scrollAmount - getY() - getHeight());
+
+ Style hoveredStyle = getDescStyle(mouseX, mouseY);
+ if (hoveredStyle != null && hoveredStyle.getHoverEvent() != null) {
+ graphics.renderComponentHoverEffect(font, hoveredStyle, mouseX, mouseY);
+ }
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ Style clickedStyle = getDescStyle((int) mouseX, (int) mouseY);
+ if (clickedStyle != null && clickedStyle.getClickEvent() != null) {
+ if (minecraft.screen.handleComponentClicked(clickedStyle)) {
+ playDownSound(minecraft.getSoundManager());
+ return true;
+ }
+ return false;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
+ if (isMouseOver(mouseX, mouseY)) {
+ scrollAmount = Mth.clamp(scrollAmount - (int) amount * 10, 0, maxScrollAmount);
+ return true;
+ }
+ return false;
+ }
+
+ private Style getDescStyle(int mouseX, int mouseY) {
+ if (!clicked(mouseX, mouseY))
+ return null;
+
+ int x = mouseX - getX();
+ int y = mouseY - descriptionY;
+
+ if (x < 0 || x > getX() + getWidth()) return null;
+ if (y < 0 || y > getY() + getHeight()) return null;
+
+ int line = y / font.lineHeight;
+
+ if (line >= wrappedText.size()) return null;
+
+ return font.getSplitter().componentStyleAtWidth(wrappedText.get(line), x);
+ }
+
+ @Override
+ protected void updateWidgetNarration(NarrationElementOutput builder) {
+
+ }
+
+ public void setOptionDescription(OptionDescription description) {
+ //this.close();
+ this.description = description;
+ this.wrappedText = null;
+ }
+
+ @Override
+ public void close() {
+ if (description != null) {
+ description.image().thenAccept(image -> {
+ if (image.isPresent()) {
+ try {
+ image.get().close();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ }
+ }
+}
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 fc7c317..390e6c0 100644
--- a/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java
+++ b/common/src/main/java/dev/isxander/yacl/gui/OptionListWidget.java
@@ -1,7 +1,6 @@
package dev.isxander.yacl.gui;
import com.google.common.collect.ImmutableList;
-import com.mojang.blaze3d.vertex.PoseStack;
import dev.isxander.yacl.api.*;
import dev.isxander.yacl.api.utils.Dimension;
import dev.isxander.yacl.impl.utils.YACLConstants;
@@ -21,24 +20,27 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.function.Consumer;
public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entry> {
private final YACLScreen yaclScreen;
- private boolean singleCategory = false;
-
+ private final ConfigCategory category;
private ImmutableList<Entry> viewableChildren;
+ private String searchQuery = "";
+ private final Consumer<Option<?>> hoverEvent;
+ private Option<?> lastHoveredOption;
- public OptionListWidget(YACLScreen screen, Minecraft client, int width, int height) {
- super(client, width / 3, 0, width / 3 * 2 + 1, height, true);
+ public OptionListWidget(YACLScreen screen, ConfigCategory category, Minecraft client, int x, int y, int width, int height, Consumer<Option<?>> hoverEvent) {
+ super(client, x, y, width, height, true);
this.yaclScreen = screen;
+ this.category = category;
+ this.hoverEvent = hoverEvent;
refreshOptions();
- for (ConfigCategory category : screen.config.categories()) {
- for (OptionGroup group : category.groups()) {
- if (group instanceof ListOption<?> listOption) {
- listOption.addRefreshListener(() -> refreshListEntries(listOption, category));
- }
+ for (OptionGroup group : category.groups()) {
+ if (group instanceof ListOption<?> listOption) {
+ listOption.addRefreshListener(() -> refreshListEntries(listOption, category));
}
}
}
@@ -46,47 +48,36 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
public void refreshOptions() {
clearEntries();
- List<ConfigCategory> categories = new ArrayList<>();
- if (yaclScreen.getCurrentCategoryIdx() == -1) {
- // -1 = no category, search in progress, so use all categories for search
- categories.addAll(yaclScreen.config.categories());
- } else {
- categories.add(yaclScreen.config.categories().get(yaclScreen.getCurrentCategoryIdx()));
- }
- singleCategory = categories.size() == 1;
-
- for (ConfigCategory category : categories) {
- for (OptionGroup group : category.groups()) {
- GroupSeparatorEntry groupSeparatorEntry;
- if (!group.isRoot()) {
- groupSeparatorEntry = group instanceof ListOption<?> listOption
- ? new ListGroupSeparatorEntry(listOption, yaclScreen)
- : new GroupSeparatorEntry(group, yaclScreen);
- addEntry(groupSeparatorEntry);
- } else {
- groupSeparatorEntry = null;
- }
+ for (OptionGroup group : category.groups()) {
+ GroupSeparatorEntry groupSeparatorEntry;
+ if (!group.isRoot()) {
+ groupSeparatorEntry = group instanceof ListOption<?> listOption
+ ? new ListGroupSeparatorEntry(listOption, yaclScreen)
+ : new GroupSeparatorEntry(group, yaclScreen);
+ addEntry(groupSeparatorEntry);
+ } else {
+ groupSeparatorEntry = null;
+ }
- List<Entry> optionEntries = new ArrayList<>();
+ List<Entry> optionEntries = new ArrayList<>();
- // add empty entry to make sure users know it's empty not just bugging out
- if (groupSeparatorEntry instanceof ListGroupSeparatorEntry listGroupSeparatorEntry) {
- if (listGroupSeparatorEntry.listOption.options().isEmpty()) {
- EmptyListLabel emptyListLabel = new EmptyListLabel(listGroupSeparatorEntry, category);
- addEntry(emptyListLabel);
- optionEntries.add(emptyListLabel);
- }
+ // add empty entry to make sure users know it's empty not just bugging out
+ if (groupSeparatorEntry instanceof ListGroupSeparatorEntry listGroupSeparatorEntry) {
+ if (listGroupSeparatorEntry.listOption.options().isEmpty()) {
+ EmptyListLabel emptyListLabel = new EmptyListLabel(listGroupSeparatorEntry, category);
+ addEntry(emptyListLabel);
+ optionEntries.add(emptyListLabel);
}
+ }
- for (Option<?> option : group.options()) {
- OptionEntry entry = new OptionEntry(option, category, group, groupSeparatorEntry, option.controller().provideWidget(yaclScreen, getDefaultEntryDimension()));
- addEntry(entry);
- optionEntries.add(entry);
- }
+ for (Option<?> option : group.options()) {
+ OptionEntry entry = new OptionEntry(option, category, group, groupSeparatorEntry, option.controller().provideWidget(yaclScreen, getDefaultEntryDimension()));
+ addEntry(entry);
+ optionEntries.add(entry);
+ }
- if (groupSeparatorEntry != null) {
- groupSeparatorEntry.setChildEntries(optionEntries);
- }
+ if (groupSeparatorEntry != null) {
+ groupSeparatorEntry.setChildEntries(optionEntries);
}
}
@@ -137,6 +128,12 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
}
}
+ public void updateSearchQuery(String query) {
+ this.searchQuery = query;
+ expandAllGroups();
+ recacheViewableChildren();
+ }
+
@Override
public int getRowWidth() {
return Math.min(396, (int)(width / 1.3f));
@@ -294,6 +291,13 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
resetButton.setY(y);
resetButton.render(graphics, mouseX, mouseY, tickDelta);
}
+
+ if (isHovered()) {
+ if (lastHoveredOption != option) {
+ lastHoveredOption = option;
+ hoverEvent.accept(option);
+ }
+ }
}
@Override
@@ -318,12 +322,10 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
@Override
public boolean isViewable() {
- String query = yaclScreen.searchFieldWidget.getQuery();
return (groupSeparatorEntry == null || groupSeparatorEntry.isExpanded())
- && (yaclScreen.searchFieldWidget.isEmpty()
- || (!singleCategory && categoryName.contains(query))
- || groupName.contains(query)
- || widget.matchesSearch(query));
+ && (searchQuery.isEmpty()
+ || groupName.contains(searchQuery)
+ || widget.matchesSearch(searchQuery));
}
@Override
@@ -427,7 +429,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
@Override
public boolean isViewable() {
- return yaclScreen.searchFieldWidget.isEmpty() || childEntries.stream().anyMatch(Entry::isViewable);
+ return searchQuery.isEmpty() || childEntries.stream().anyMatch(Entry::isViewable);
}
@Override
@@ -524,6 +526,11 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
}
@Override
+ public void setExpanded(boolean expanded) {
+ super.setExpanded(listOption.available() && expanded);
+ }
+
+ @Override
public List<? extends GuiEventListener> children() {
return ImmutableList.of(expandMinimizeButton, addListButton, resetListButton);
}
@@ -547,10 +554,7 @@ public class OptionListWidget extends ElementListWidgetExt<OptionListWidget.Entr
@Override
public boolean isViewable() {
- String query = yaclScreen.searchFieldWidget.getQuery();
- return parent.isExpanded() && (yaclScreen.searchFieldWidget.isEmpty()
- || (!singleCategory && categoryName.contains(query))
- || groupName.contains(query));
+ return parent.isExpanded() && (searchQuery.isEmpty() || groupName.contains(searchQuery));
}
@Override
diff --git a/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java b/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java
index fb098a9..24db7ab 100644
--- a/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java
+++ b/common/src/main/java/dev/isxander/yacl/gui/SearchFieldWidget.java
@@ -6,20 +6,24 @@ import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.network.chat.Component;
+import java.util.function.Consumer;
+
public class SearchFieldWidget extends EditBox {
private Component emptyText;
private final YACLScreen yaclScreen;
private final Font font;
+ private final Consumer<String> updateConsumer;
private boolean isEmpty = true;
- public SearchFieldWidget(YACLScreen yaclScreen, Font font, int x, int y, int width, int height, Component text, Component emptyText) {
+ public SearchFieldWidget(YACLScreen yaclScreen, Font font, int x, int y, int width, int height, Component text, Component emptyText, Consumer<String> updateConsumer) {
super(font, x, y, width, height, text);
- setResponder(string -> update());
+ setResponder(this::update);
setFilter(string -> !string.endsWith(" ") && !string.startsWith(" "));
this.yaclScreen = yaclScreen;
this.font = font;
this.emptyText = emptyText;
+ this.updateConsumer = updateConsumer;
}
@Override
@@ -30,23 +34,14 @@ public class SearchFieldWidget extends EditBox {
}
}
- private void update() {
+ private void update(String query) {
boolean wasEmpty = isEmpty;
- isEmpty = getValue().isEmpty();
+ isEmpty = query.isEmpty();
if (isEmpty && wasEmpty)
return;
- if (!isEmpty && yaclScreen.getCurrentCategoryIdx() != -1)
- yaclScreen.changeCategory(-1);
- if (isEmpty && yaclScreen.getCurrentCategoryIdx() == -1)
- yaclScreen.changeCategory(0);
-
- yaclScreen.optionList.expandAllGroups();
- yaclScreen.optionList.recacheViewableChildren();
-
- yaclScreen.optionList.setScrollAmount(0);
- yaclScreen.categoryList.setScrollAmount(0);
+ updateConsumer.accept(query);
}
public String getQuery() {
diff --git a/common/src/main/java/dev/isxander/yacl/gui/TabListWidget.java b/common/src/main/java/dev/isxander/yacl/gui/TabListWidget.java
new file mode 100644
index 0000000..b9e756c
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl/gui/TabListWidget.java
@@ -0,0 +1,105 @@
+package dev.isxander.yacl.gui;
+
+import com.google.common.collect.ImmutableList;
+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.ScreenRectangle;
+import net.minecraft.network.chat.CommonComponents;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * Author: MrCrayfish
+ */
+public class TabListWidget<T extends ElementListWidgetExt<?>> extends AbstractWidget implements ContainerEventHandler
+{
+ private final Supplier<ScreenRectangle> dimensions;
+ private final T list;
+
+ public TabListWidget(Supplier<ScreenRectangle> dimensions, T list) {
+ super(0, 0, 100, 0, CommonComponents.EMPTY);
+ this.dimensions = dimensions;
+ this.list = list;
+ }
+
+ @Override
+ public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float deltaTick) {
+ ScreenRectangle dimensions = this.dimensions.get();
+ this.setX(dimensions.left());
+ this.setY(dimensions.top());
+ this.width = dimensions.width();
+ this.height = dimensions.height();
+ this.list.updateDimensions(dimensions);
+ this.list.render(guiGraphics, mouseX, mouseY, deltaTick);
+ }
+
+ @Override
+ protected void updateWidgetNarration(NarrationElementOutput output) {
+ this.list.updateNarration(output);
+ }
+
+ @Override
+ public List<? extends GuiEventListener> children() {
+ return ImmutableList.of(this.list);
+ }
+
+ public T getList() {
+ return list;
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ return this.list.mouseClicked(mouseX, mouseY, button);
+ }
+
+ @Override
+ public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+ return this.list.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
+ }
+
+ @Override
+ public boolean mouseReleased(double mouseX, double mouseY, int button) {
+ return this.list.mouseReleased(mouseX, mouseY, button);
+ }
+
+ @Override
+ public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
+ return this.list.mouseScrolled(mouseX, mouseY, amount);
+ }
+
+ @Override
+ public boolean keyPressed(int i, int j, int k) {
+ return this.list.keyPressed(i, j, k);
+ }
+
+ @Override
+ public boolean charTyped(char c, int i) {
+ return this.list.charTyped(c, i);
+ }
+
+ @Override
+ public boolean isDragging() {
+ return this.list.isDragging();
+ }
+
+ @Override
+ public void setDragging(boolean dragging) {
+ this.list.setDragging(dragging);
+ }
+
+ @Nullable
+ @Override
+ public GuiEventListener getFocused() {
+ return this.list.getFocused();
+ }
+
+ @Override
+ public void setFocused(@Nullable GuiEventListener listener) {
+ this.list.setFocused(listener);
+ }
+}
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 b3e614f..77090c8 100644
--- a/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java
+++ b/common/src/main/java/dev/isxander/yacl/gui/YACLScreen.java
@@ -2,10 +2,7 @@ package dev.isxander.yacl.gui;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.*;
-import dev.isxander.yacl.api.Option;
-import dev.isxander.yacl.api.OptionFlag;
-import dev.isxander.yacl.api.PlaceholderCategory;
-import dev.isxander.yacl.api.YetAnotherConfigLib;
+import dev.isxander.yacl.api.*;
import dev.isxander.yacl.api.utils.Dimension;
import dev.isxander.yacl.api.utils.MutableDimension;
import dev.isxander.yacl.api.utils.OptionUtils;
@@ -14,117 +11,81 @@ import dev.isxander.yacl.impl.utils.YACLConstants;
import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
+import net.minecraft.client.gui.components.AbstractWidget;
+import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.MultiLineLabel;
+import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.gui.components.events.GuiEventListener;
+import net.minecraft.client.gui.components.tabs.Tab;
+import net.minecraft.client.gui.components.tabs.TabManager;
+import net.minecraft.client.gui.components.tabs.TabNavigationBar;
+import net.minecraft.client.gui.navigation.ScreenRectangle;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.gui.screens.inventory.tooltip.TooltipRenderUtil;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import java.io.*;
import java.util.HashSet;
+import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
public class YACLScreen extends Screen {
public final YetAnotherConfigLib config;
- private int currentCategoryIdx;
private final Screen parent;
- public OptionListWidget optionList;
- public CategoryListWidget categoryList;
- public TooltipButtonWidget finishedSaveButton, cancelResetButton, undoButton;
- public SearchFieldWidget searchFieldWidget;
+ public final TabManager tabManager = new TabManager(this::addRenderableWidget, this::removeWidget);
+ public TabNavigationBar tabNavigationBar;
+ public Tab[] tabs;
+ public ScreenRectangle tabArea;
public Component saveButtonMessage, saveButtonTooltipMessage;
private int saveButtonMessageTime;
-
public YACLScreen(YetAnotherConfigLib config, Screen parent) {
super(config.title());
this.config = config;
this.parent = parent;
- this.currentCategoryIdx = 0;
}
@Override
protected void init() {
- int columnWidth = width / 3;
- int padding = columnWidth / 20;
- columnWidth = Math.min(columnWidth, 400);
- int paddedWidth = columnWidth - padding * 2;
-
- MutableDimension<Integer> actionDim = Dimension.ofInt(width / 3 / 2, height - padding - 20, paddedWidth, 20);
- finishedSaveButton = new TooltipButtonWidget(
- this,
- actionDim.x() - actionDim.width() / 2,
- actionDim.y(),
- actionDim.width(),
- actionDim.height(),
- Component.empty(),
- Component.empty(),
- btn -> finishOrSave()
- );
- actionDim.expand(-actionDim.width() / 2 - 2, 0).move(-actionDim.width() / 2 - 2, -22);
- cancelResetButton = new TooltipButtonWidget(
- this,
- actionDim.x() - actionDim.width() / 2,
- actionDim.y(),
- actionDim.width(),
- actionDim.height(),
- Component.empty(),
- Component.empty(),
- btn -> cancelOrReset()
- );
- actionDim.move(actionDim.width() + 4, 0);
- undoButton = new TooltipButtonWidget(
- this,
- actionDim.x() - actionDim.width() / 2,
- actionDim.y(),
- actionDim.width(),
- actionDim.height(),
- Component.translatable("yacl.gui.undo"),
- Component.translatable("yacl.gui.undo.tooltip"),
- btn -> undo()
- );
-
- searchFieldWidget = new SearchFieldWidget(
- this,
- font,
- width / 3 / 2 - paddedWidth / 2 + 1,
- undoButton.getY() - 22,
- paddedWidth - 2, 18,
- Component.translatable("gui.recipebook.search_hint"),
- Component.translatable("gui.recipebook.search_hint")
- );
-
- categoryList = new CategoryListWidget(minecraft, this, width, height);
- addWidget(categoryList);
-
- updateActionAvailability();
- addRenderableWidget(searchFieldWidget);
- addRenderableWidget(cancelResetButton);
- addRenderableWidget(undoButton);
- addRenderableWidget(finishedSaveButton);
-
- optionList = new OptionListWidget(this, minecraft, width, height);
- addWidget(optionList);
+ if (tabs != null) {
+ closeTabs();
+ }
+
+ 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.selectTab(0, false);
+ tabNavigationBar.arrangeElements();
+ ScreenRectangle navBarArea = tabNavigationBar.getRectangle();
+ tabArea = new ScreenRectangle(0, navBarArea.height() - 1, this.width, this.height - navBarArea.height() + 1);
+ tabManager.setTabArea(tabArea);
+ addRenderableWidget(tabNavigationBar);
config.initConsumer().accept(this);
}
@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float delta) {
- renderBackground(graphics);
+ renderDirtBackground(graphics);
super.render(graphics, mouseX, mouseY, delta);
- categoryList.render(graphics, mouseX, mouseY, delta);
- searchFieldWidget.render(graphics, mouseX, mouseY, delta);
- optionList.render(graphics, mouseX, mouseY, delta);
-
- categoryList.postRender(graphics, mouseX, mouseY, delta);
- optionList.postRender(graphics, mouseX, mouseY, delta);
for (GuiEventListener child : children()) {
if (child instanceof TooltipButtonWidget tooltipButtonWidget) {
@@ -171,54 +132,8 @@ 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) {
- if (idx == currentCategoryIdx)
- return;
-
- if (idx != -1 && config.categories().get(idx) instanceof PlaceholderCategory placeholderCategory) {
- minecraft.setScreen(placeholderCategory.screen().apply(minecraft, this));
- } else {
- currentCategoryIdx = idx;
- optionList.refreshOptions();
- }
- }
-
- public int getCurrentCategoryIdx() {
- return currentCategoryIdx;
- }
-
- private void updateActionAvailability() {
- boolean pendingChanges = pendingChanges();
-
- undoButton.active = pendingChanges;
- finishedSaveButton.setMessage(pendingChanges ? Component.translatable("yacl.gui.save") : GuiUtils.translatableFallback("yacl.gui.done", CommonComponents.GUI_DONE));
- finishedSaveButton.setTooltip(pendingChanges ? Component.translatable("yacl.gui.save.tooltip") : Component.translatable("yacl.gui.finished.tooltip"));
- cancelResetButton.setMessage(pendingChanges ? GuiUtils.translatableFallback("yacl.gui.cancel", CommonComponents.GUI_CANCEL) : Component.translatable("controls.reset"));
- cancelResetButton.setTooltip(pendingChanges ? Component.translatable("yacl.gui.cancel.tooltip") : Component.translatable("yacl.gui.reset.tooltip"));
- }
-
- @Override
public void tick() {
- searchFieldWidget.tick();
-
- updateActionAvailability();
+ tabManager.tickCurrent();
if (saveButtonMessage != null) {
if (saveButtonMessageTime > 140) {
@@ -227,9 +142,9 @@ public class YACLScreen extends Screen {
saveButtonMessageTime = 0;
} else {
saveButtonMessageTime++;
- finishedSaveButton.setMessage(saveButtonMessage);
+ //finishedSaveButton.setMessage(saveButtonMessage);
if (saveButtonTooltipMessage != null) {
- finishedSaveButton.setTooltip(saveButtonTooltipMessage);
+ //finishedSaveButton.setTooltip(saveButtonTooltipMessage);
}
}
}
@@ -266,6 +181,19 @@ public class YACLScreen extends Screen {
@Override
public void onClose() {
minecraft.setScreen(parent);
+ closeTabs();
+ }
+
+ private void closeTabs() {
+ for (Tab tab : tabs) {
+ if (tab instanceof Closeable closeable) {
+ try {
+ closeable.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
}
public static void renderMultilineTooltip(GuiGraphics graphics, Font font, MultiLineLabel text, int centerX, int yAbove, int yBelow, int screenWidth, int screenHeight) {
@@ -312,4 +240,137 @@ public class YACLScreen extends Screen {
graphics.pose().popPose();
}
}
+
+ private class CategoryTab implements Tab, Closeable {
+ private final ConfigCategory category;
+
+ private final TabListWidget<OptionListWidget> optionList;
+ private final Button saveFinishedButton;
+ private final Button cancelResetButton;
+ private final Button undoButton;
+ private final SearchFieldWidget searchField;
+ private OptionDescriptionWidget descriptionWidget;
+
+ public CategoryTab(ConfigCategory category) {
+ this.category = category;
+
+ 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());
+ })
+ );
+
+ int columnWidth = width / 3;
+ int padding = columnWidth / 20;
+ columnWidth = Math.min(columnWidth, 400);
+ int paddedWidth = columnWidth - padding * 2;
+ MutableDimension<Integer> actionDim = Dimension.ofInt(width / 3 * 2 + width / 6, height - padding - 20, paddedWidth, 20);
+
+ saveFinishedButton = Button.builder(Component.literal("Done"), btn -> finishOrSave())
+ .pos(actionDim.x() - actionDim.width() / 2, actionDim.y())
+ .size(actionDim.width(), actionDim.height())
+ .build();
+
+ actionDim.expand(-actionDim.width() / 2 - 2, 0).move(-actionDim.width() / 2 - 2, -22);
+ cancelResetButton = Button.builder(Component.literal("Cancel"), btn -> cancelOrReset())
+ .pos(actionDim.x() - actionDim.width() / 2, actionDim.y())
+ .size(actionDim.width(), actionDim.height())
+ .build();
+
+ actionDim.move(actionDim.width() + 4, 0);
+ undoButton = Button.builder(Component.translatable("yacl.gui.undo"), btn -> undo())
+ .pos(actionDim.x() - actionDim.width() / 2, actionDim.y())
+ .size(actionDim.width(), actionDim.height())
+ .tooltip(Tooltip.create(Component.translatable("yacl.gui.undo.tooltip")))
+ .build();
+
+ searchField = new SearchFieldWidget(
+ YACLScreen.this,
+ font,
+ width / 3 * 2 + width / 6 - paddedWidth / 2 + 1,
+ undoButton.getY() - 22,
+ paddedWidth - 2, 18,
+ Component.translatable("gui.recipebook.search_hint"),
+ Component.translatable("gui.recipebook.search_hint"),
+ searchQuery -> optionList.getList().updateSearchQuery(searchQuery)
+ );
+
+ descriptionWidget = new OptionDescriptionWidget(
+ () -> new ScreenRectangle(
+ width / 3 * 2 + padding,
+ tabArea.top() + padding,
+ paddedWidth,
+ searchField.getY() - 1 - tabArea.top() - padding * 2
+ ),
+ null
+ );
+
+ updateButtons();
+ }
+
+ @Override
+ public Component getTabTitle() {
+ return category.name();
+ }
+
+ @Override
+ public void visitChildren(Consumer<AbstractWidget> consumer) {
+ consumer.accept(optionList);
+ consumer.accept(saveFinishedButton);
+ consumer.accept(cancelResetButton);
+ consumer.accept(undoButton);
+ consumer.accept(searchField);
+ consumer.accept(descriptionWidget);
+ }
+
+ @Override
+ public void doLayout(ScreenRectangle screenRectangle) {
+
+ }
+
+ @Override
+ public void tick() {
+ updateButtons();
+ searchField.tick();
+ }
+
+ private void updateButtons() {
+ boolean pendingChanges = pendingChanges();
+
+ undoButton.active = pendingChanges;
+ saveFinishedButton.setMessage(pendingChanges ? Component.translatable("yacl.gui.save") : GuiUtils.translatableFallback("yacl.gui.done", CommonComponents.GUI_DONE));
+ saveFinishedButton.setTooltip(Tooltip.create(pendingChanges ? Component.translatable("yacl.gui.save.tooltip") : Component.translatable("yacl.gui.finished.tooltip")));
+ cancelResetButton.setMessage(pendingChanges ? GuiUtils.translatableFallback("yacl.gui.cancel", CommonComponents.GUI_CANCEL) : Component.translatable("controls.reset"));
+ cancelResetButton.setTooltip(Tooltip.create(pendingChanges ? Component.translatable("yacl.gui.cancel.tooltip") : Component.translatable("yacl.gui.reset.tooltip")));
+ }
+
+ @Override
+ public void close() {
+ descriptionWidget.close();
+ }
+ }
+
+ private class PlaceholderTab implements Tab {
+ private final PlaceholderCategory category;
+
+ public PlaceholderTab(PlaceholderCategory category) {
+ this.category = category;
+ }
+
+ @Override
+ public Component getTabTitle() {
+ return category.name();
+ }
+
+ @Override
+ public void visitChildren(Consumer<AbstractWidget> consumer) {
+
+ }
+
+ @Override
+ public void doLayout(ScreenRectangle screenRectangle) {
+ minecraft.setScreen(category.screen().apply(minecraft, YACLScreen.this));
+ }
+ }
}
diff --git a/common/src/main/java/dev/isxander/yacl/gui/YACLTooltipPositioner.java b/common/src/main/java/dev/isxander/yacl/gui/YACLTooltipPositioner.java
new file mode 100644
index 0000000..d6e6220
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl/gui/YACLTooltipPositioner.java
@@ -0,0 +1,48 @@
+package dev.isxander.yacl.gui;
+
+import net.minecraft.client.gui.navigation.ScreenRectangle;
+import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipPositioner;
+import net.minecraft.util.Mth;
+import org.joml.Vector2i;
+import org.joml.Vector2ic;
+
+import java.util.function.Supplier;
+
+public class YACLTooltipPositioner implements ClientTooltipPositioner {
+ private final Supplier<ScreenRectangle> buttonDimensions;
+
+ public YACLTooltipPositioner(net.minecraft.client.gui.components.AbstractWidget widget) {
+ this.buttonDimensions = widget::getRectangle;
+ }
+
+ public YACLTooltipPositioner(dev.isxander.yacl.gui.AbstractWidget widget) {
+ this.buttonDimensions = () -> {
+ var dim = widget.getDimension();
+ return new ScreenRectangle(dim.x(), dim.y(), dim.width(), dim.height());
+ };
+ }
+
+ public YACLTooltipPositioner(Supplier<ScreenRectangle> buttonDimensions) {
+ this.buttonDimensions = buttonDimensions;
+ }
+
+ @Override
+ public Vector2ic positionTooltip(int guiWidth, int guiHeight, int x, int y, int width, int height) {
+ ScreenRectangle buttonDimensions = this.buttonDimensions.get();
+
+ int centerX = buttonDimensions.left() + buttonDimensions.width() / 2;
+ int aboveY = buttonDimensions.top() - height - 4;
+ int belowY = buttonDimensions.top() + buttonDimensions.height() + 4;
+
+ int maxBelow = guiHeight - (belowY + height);
+ int minAbove = aboveY - height;
+
+ int yResult = aboveY;
+ if (minAbove < 8)
+ yResult = maxBelow > minAbove ? belowY : aboveY;
+
+ int xResult = Mth.clamp(centerX - width / 2, -4, guiWidth - width - 4);
+
+ return new Vector2i(xResult, yResult);
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java
index 11da99e..f0e0bdd 100644
--- a/common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java
+++ b/common/src/main/java/dev/isxander/yacl/impl/ButtonOptionImpl.java
@@ -19,7 +19,7 @@ import java.util.function.Function;
@ApiStatus.Internal
public final class ButtonOptionImpl implements ButtonOption {
private final Component name;
- private final Component tooltip;
+ private final OptionDescription description;
private final BiConsumer<YACLScreen, ButtonOption> action;
private boolean available;
private final Controller<BiConsumer<YACLScreen, ButtonOption>> controller;
@@ -27,13 +27,13 @@ public final class ButtonOptionImpl implements ButtonOption {
public ButtonOptionImpl(
@NotNull Component name,
- @Nullable Component tooltip,
+ @Nullable OptionDescription description,
@NotNull BiConsumer<YACLScreen, ButtonOption> action,
boolean available,
@NotNull Function<ButtonOption, Controller<BiConsumer<YACLScreen, ButtonOption>>> controlGetter
) {
this.name = name;
- this.tooltip = tooltip;
+ this.description = description;
this.action = action;
this.available = available;
this.controller = controlGetter.apply(this);
@@ -46,8 +46,13 @@ public final class ButtonOptionImpl implements ButtonOption {
}
@Override
+ public @NotNull OptionDescription description() {
+ return description;
+ }
+
+ @Override
public @NotNull Component tooltip() {
- return tooltip;
+ return description().description();
}
@Override
@@ -162,7 +167,7 @@ public final class ButtonOptionImpl implements ButtonOption {
public Builder tooltip(@NotNull Component... tooltips) {
Validate.notNull(tooltips, "`tooltips` cannot be empty");
- tooltipLines.addAll(List.of(tooltips));
+ //tooltipLines.addAll(List.of(tooltips));
return this;
}
@@ -212,7 +217,7 @@ public final class ButtonOptionImpl implements ButtonOption {
concatenatedTooltip.append(line);
}
- return new ButtonOptionImpl(name, concatenatedTooltip, action, available, controlGetter);
+ return new ButtonOptionImpl(name, OptionDescription.createBuilder().name(name).description(concatenatedTooltip).build(), action, available, controlGetter);
}
}
}
diff --git a/common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java
index 732a373..2a7759c 100644
--- a/common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java
+++ b/common/src/main/java/dev/isxander/yacl/impl/LabelOptionImpl.java
@@ -19,6 +19,7 @@ import java.util.function.BiConsumer;
public final class LabelOptionImpl implements LabelOption {
private final Component label;
private final Component name = Component.literal("Label Option");
+ private final OptionDescription description;
private final Component tooltip = Component.empty();
private final LabelController labelController;
private final Binding<Component> binding;
@@ -27,6 +28,10 @@ public final class LabelOptionImpl implements LabelOption {
this.label = label;
this.labelController = new LabelController(this);
this.binding = Binding.immutable(label);
+ this.description = OptionDescription.createBuilder()
+ .name(this.name)
+ .description(this.label)
+ .build();
}
@Override
@@ -40,6 +45,11 @@ public final class LabelOptionImpl implements LabelOption {
}
@Override
+ public @NotNull OptionDescription description() {
+ return description;
+ }
+
+ @Override
public @NotNull Component tooltip() {
return tooltip;
}
diff --git a/common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java b/common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java
index c15efe6..d02259e 100644
--- a/common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java
+++ b/common/src/main/java/dev/isxander/yacl/impl/ListOptionEntryImpl.java
@@ -34,6 +34,11 @@ public final class ListOptionEntryImpl<T> implements ListOptionEntry<T> {
}
@Override
+ public @NotNull OptionDescription description() {
+ return group.description();
+ }
+
+ @Override
public @NotNull Component tooltip() {
return Component.empty();
}
diff --git a/common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java
index f47493c..24fe2b1 100644
--- a/common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java
+++ b/common/src/main/java/dev/isxander/yacl/impl/ListOptionImpl.java
@@ -19,7 +19,7 @@ import java.util.stream.Collectors;
@ApiStatus.Internal
public final class ListOptionImpl<T> implements ListOption<T> {
private final Component name;
- private final Component tooltip;
+ private final OptionDescription description;
private final Binding<List<T>> binding;
private final T initialValue;
private final List<ListOptionEntry<T>> entries;
@@ -31,9 +31,9 @@ public final class ListOptionImpl<T> implements ListOption<T> {
private final List<BiConsumer<Option<List<T>>, List<T>>> listeners;
private final List<Runnable> refreshListeners;
- public ListOptionImpl(@NotNull Component name, @NotNull Component tooltip, @NotNull Binding<List<T>> binding, @NotNull T initialValue, @NotNull Class<T> typeClass, @NotNull Function<ListOptionEntry<T>, Controller<T>> controllerFunction, ImmutableSet<OptionFlag> flags, boolean collapsed, boolean available, Collection<BiConsumer<Option<List<T>>, List<T>>> listeners) {
+ public ListOptionImpl(@NotNull Component name, @NotNull OptionDescription description, @NotNull Binding<List<T>> binding, @NotNull T initialValue, @NotNull Class<T> typeClass, @NotNull Function<ListOptionEntry<T>, Controller<T>> controllerFunction, ImmutableSet<OptionFlag> flags, boolean collapsed, boolean available, Collection<BiConsumer<Option<List<T>>, List<T>>> listeners) {
this.name = name;
- this.tooltip = tooltip;
+ this.description = description;
this.binding = binding;
this.initialValue = initialValue;
this.entryFactory = new EntryFactory(controllerFunction);
@@ -54,8 +54,13 @@ public final class ListOptionImpl<T> implements ListOption<T> {
}
@Override
+ public @NotNull OptionDescription description() {
+ return this.description;
+ }
+
+ @Override
public @NotNull Component tooltip() {
- return this.tooltip;
+ return description().description();
}
@Override
@@ -332,7 +337,7 @@ public final class ListOptionImpl<T> implements ListOption<T> {
concatenatedTooltip.append(line);
}
- return new ListOptionImpl<>(name, concatenatedTooltip, binding, initialValue, typeClass, controllerFunction, ImmutableSet.copyOf(flags), collapsed, available, listeners);
+ return new ListOptionImpl<>(name, OptionDescription.createBuilder().name(name).description(concatenatedTooltip).build(), binding, initialValue, typeClass, controllerFunction, ImmutableSet.copyOf(flags), collapsed, available, listeners);
}
}
}
diff --git a/common/src/main/java/dev/isxander/yacl/impl/OptionDescriptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/OptionDescriptionImpl.java
new file mode 100644
index 0000000..f57a410
--- /dev/null
+++ b/common/src/main/java/dev/isxander/yacl/impl/OptionDescriptionImpl.java
@@ -0,0 +1,124 @@
+package dev.isxander.yacl.impl;
+
+import dev.isxander.yacl.api.OptionDescription;
+import dev.isxander.yacl.gui.ImageRenderer;
+import net.minecraft.ChatFormatting;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import org.apache.commons.lang3.Validate;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+
+public record OptionDescriptionImpl(Component descriptiveName, Component description, CompletableFuture<Optional<ImageRenderer>> image) implements OptionDescription {
+ public static class BuilderImpl implements Builder {
+ private Component name;
+ private Component description;
+ private CompletableFuture<Optional<ImageRenderer>> image = CompletableFuture.completedFuture(Optional.empty());
+ private boolean imageUnset = true;
+
+ @Override
+ public Builder name(Component name) {
+ this.name = name;
+ return this;
+ }
+
+ @Override
+ public Builder description(Component description) {
+ this.description = description;
+ return this;
+ }
+
+ @Override
+ public Builder image(ResourceLocation image, int width, int height) {
+ Validate.isTrue(imageUnset, "Image already set!");
+ Validate.isTrue(width > 0, "Width must be greater than 0!");
+ Validate.isTrue(height > 0, "Height must be greater than 0!");
+
+ this.image = CompletableFuture.completedFuture(Optional.of(new ImageRenderer.TextureBacked(image, width, height)));
+ imageUnset = false;
+ return this;
+ }
+
+ @Override
+ public Builder image(Path path, ResourceLocation uniqueLocation) {
+ Validate.isTrue(imageUnset, "Image already set!");
+ this.image = CompletableFuture.supplyAsync(() -> ImageRenderer.NativeImageBacked.createFromPath(path, uniqueLocation));
+ imageUnset = false;
+ return this;
+ }
+
+ @Override
+ public Builder gifImage(ResourceLocation image) {
+ Validate.isTrue(imageUnset, "Image already set!");
+ this.image = CompletableFuture.supplyAsync(() -> {
+ try {
+ return Optional.of(ImageRenderer.AnimatedNativeImageBacked.createGIFFromTexture(image));
+ } catch (IOException e) {
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ });
+ imageUnset = false;
+ return this;
+ }
+
+ @Override
+ public Builder gifImage(Path path, ResourceLocation uniqueLocation) {
+ Validate.isTrue(imageUnset, "Image already set!");
+ this.image = CompletableFuture.supplyAsync(() -> {
+ try {
+ return Optional.of(ImageRenderer.AnimatedNativeImageBacked.createGIF(new FileInputStream(path.toFile()), uniqueLocation));
+ } catch (IOException e) {
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ });
+ imageUnset = false;
+ return this;
+ }
+
+ @Override
+ public Builder webpImage(ResourceLocation image, int frameDelayMS) {
+ Validate.isTrue(imageUnset, "Image already set!");
+ this.image = CompletableFuture.supplyAsync(() -> {
+ try {
+ return Optional.of(ImageRenderer.AnimatedNativeImageBacked.createWEBPFromTexture(image, frameDelayMS));
+ } catch (IOException e) {
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ });
+ imageUnset = false;
+ return this;
+ }
+
+ @Override
+ public Builder webpImage(Path path, ResourceLocation uniqueLocation, int frameDelayMS) {
+ Validate.isTrue(imageUnset, "Image already set!");
+ this.image = CompletableFuture.supplyAsync(() -> {
+ try {
+ return Optional.of(ImageRenderer.AnimatedNativeImageBacked.createWEBP(new FileInputStream(path.toFile()), uniqueLocation, frameDelayMS));
+ } catch (IOException e) {
+ e.printStackTrace();
+ return Optional.empty();
+ }
+ });
+ imageUnset = false;
+ return this;
+ }
+
+ @Override
+ public OptionDescription build() {
+ Validate.notNull(name, "Name must be set!");
+
+ if (description == null)
+ description = Component.empty();
+
+ return new OptionDescriptionImpl(name.copy().withStyle(ChatFormatting.BOLD), description, image);
+ }
+ }
+}
diff --git a/common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java b/common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java
index 4b65d56..ef4d13b 100644
--- a/common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java
+++ b/common/src/main/java/dev/isxander/yacl/impl/OptionImpl.java
@@ -1,11 +1,9 @@
package dev.isxander.yacl.impl;
import com.google.common.collect.ImmutableSet;
-import dev.isxander.yacl.api.Binding;
-import dev.isxander.yacl.api.Controller;
-import dev.isxander.yacl.api.Option;
-import dev.isxander.yacl.api.OptionFlag;
+import dev.isxander.yacl.api.*;
import net.minecraft.ChatFormatting;
+import net.minecraft.Util;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.ComponentContents;
import net.minecraft.network.chat.MutableComponent;
@@ -23,7 +21,7 @@ import java.util.function.Supplier;
@ApiStatus.Internal
public final class OptionImpl<T> implements Option<T> {
private final Component name;
- private Component tooltip;
+ private OptionDescription description;
private final Controller<T> controller;
private final Binding<T> binding;
private boolean available;
@@ -38,7 +36,7 @@ public final class OptionImpl<T> implements Option<T> {
public OptionImpl(
@NotNull Component name,
- @Nullable Function<T, Component> tooltipGetter,
+ @NotNull Function<T, OptionDescription> descriptionFunction,
@NotNull Function<Option<T>, Controller<T>> controlGetter,
@NotNull Binding<T> binding,
boolean available,
@@ -54,7 +52,9 @@ public final class OptionImpl<T> implements Option<T> {
this.listeners = new ArrayList<>(listeners);
this.controller = controlGetter.apply(this);
- addListener((opt, pending) -> tooltip = tooltipGetter.apply(pending));
+ var memoizedDescriptionFunction = Util.memoize(descriptionFunction);
+ addListener((opt, pending) -> description = memoizedDescriptionFunction.apply(pending));
+
requestSet(binding().getValue());
}
@@ -64,8 +64,13 @@ public final class OptionImpl<T> implements Option<T> {
}
@Override
+ public @NotNull OptionDescription description() {
+ return this.description;
+ }
+
+ @Override
public @NotNull Component tooltip() {
- return tooltip;
+ return description.description();
}
@Override
@@ -147,6 +152,7 @@ public final class OptionImpl<T> implements Option<T> {
public static class BuilderImpl<T> implements Builder<T> {
private Component name = Component.literal("Name not specified!").withStyle(ChatFormatting.RED);
+ private Function<T, OptionDescription> descriptionFunction = null;
private final List<Function<T, Component>> tooltipGetters = new ArrayList<>();
private Function<Option<T>, Controller<T>> controlGetter;
@@ -176,6 +182,17 @@ public final class OptionImpl<T> implements Option<T> {
}
@Override
+ public Builder<T> description(@NotNull OptionDescription description) {
+ return description(opt -> description);
+ }
+
+ @Override
+ public Builder<T> description(@NotNull Function<T, OptionDescription> descriptionFunction) {
+ this.descriptionFunction = descriptionFunction;
+ return this;
+ }
+
+ @Override
public Builder<T> tooltip(@NotNull Function<T, Component> tooltipGetter) {
Validate.notNull(tooltipGetter, "`tooltipGetter` cannot be null");
@@ -292,12 +309,11 @@ public final class OptionImpl<T> implements Option<T> {
return concatenatedTooltip;
};
-
- if (instant) {
- listeners.add((opt, pendingValue) -> opt.applyValue());
+ if (descriptionFunction == null) {
+ descriptionFunction = opt -> OptionDescription.createBuilder().name(name).description(concatenatedTooltipGetter.apply(opt)).build();
}
- return new OptionImpl<>(name, concatenatedTooltipGetter, controlGetter, binding, available, ImmutableSet.copyOf(flags), typeClass, listeners);
+ return new OptionImpl<>(name, descriptionFunction, controlGetter, binding, available, ImmutableSet.copyOf(flags), typeClass, listeners);
}
}
}
diff --git a/fabric/build.gradle.kts b/fabric/build.gradle.kts
index 8e6b4fa..6d527d6 100644
--- a/fabric/build.gradle.kts
+++ b/fabric/build.gradle.kts
@@ -40,6 +40,9 @@ dependencies {
).forEach { modApi(fabricApi.module(it, libs.versions.fabric.api.get())) }
modApi(libs.mod.menu)
+ implementation(libs.twelvemonkeys.imageio.core)
+ implementation(libs.twelvemonkeys.imageio.webp)
+
"common"(project(path = ":common", configuration = "namedElements")) { isTransitive = false }
"shadowCommon"(project(path = ":common", configuration = "transformProductionFabric")) { isTransitive = false }
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8cbe798..8aaf92f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -10,12 +10,13 @@ github_release = "2.+"
machete = "2.+"
grgit = "5.0.+"
-minecraft = "1.20-pre2"
-quilt_mappings = "0"
+minecraft = "1.20-pre4"
+quilt_mappings = "1"
fabric_loader = "0.14.19"
# Common Dependencies
-mixin_extras = "0.2.0-beta.7"
+mixin_extras = "0.2.0-beta.8"
+twelvemonkeys_imageio = "3.9.4"
# Fabric-like Dependencies
fabric_api = "0.81.2+1.20"
@@ -30,6 +31,8 @@ fabric_loader = { module = "net.fabricmc:fabric-loader", version.ref = "fabric_l
# Common Dependencies
mixin_extras_common = { module = "com.github.llamalad7.mixinextras:mixinextras-common", version.ref = "mixin_extras" }
+twelvemonkeys_imageio_core = { module = "com.twelvemonkeys.imageio:imageio-core", version.ref = "twelvemonkeys_imageio" }
+twelvemonkeys_imageio_webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys_imageio" }
# Fabric-like Dependencies
fabric_api = { module = "net.fabricmc.fabric-api:fabric-api", version.ref = "fabric_api" }
diff --git a/test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java b/test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java
index 9065e76..9717206 100644
--- a/test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java
+++ b/test-common/src/main/java/dev/isxander/yacl/test/GuiTest.java
@@ -20,8 +20,10 @@ import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.ClickEvent;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.HoverEvent;
+import net.minecraft.resources.ResourceLocation;
import java.awt.Color;
+import java.nio.file.Path;
import java.util.List;
public class GuiTest {
@@ -59,7 +61,17 @@ public class GuiTest {
.tooltip(Component.literal("Test!"))
.option(Option.createBuilder(boolean.class)
.name(Component.literal("Boolean Toggle"))
- .tooltip(value -> Component.literal("A simple toggle button that contains the value '" + value + "'"))
+ .description(OptionDescription.createBuilder()
+ .name(Component.literal("Boolean Toggle"))
+ .description(Component.empty()
+ .append(Component.literal("a").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("a")))))
+ .append(Component.literal("b").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("b")))))
+ .append(Component.literal("c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c c").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("c")))))
+ .append(Component.literal("e").withStyle(style -> style.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.literal("e")))))
+ .withStyle(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "https://isxander.dev")))
+ )
+ .webpImage(Path.of("D:\\Xander\\Downloads\\e.webp"), new ResourceLocation("yacl", "e.webp"), 33)
+ .build())
.binding(
defaults.booleanToggle,
() -> config.booleanToggle,
@@ -70,6 +82,11 @@ public class GuiTest {
.build())
.option(Option.createBuilder(boolean.class)
.name(Component.literal("Custom Boolean Toggle"))
+ .description(OptionDescription.createBuilder()
+ .name(Component.literal("Custom Boolean Toggle"))
+ .description(Component.literal("You can customize controllers like so! YACL is truly infinitely customizable!"))
+ .image(Path.of("D:\\Xander\\Downloads\\_MG_0860-Enhanced-NR.png"), new ResourceLocation("yacl", "f.webp"))
+ .build())
.tooltip(Component.literal("You can customize these controllers like this!"))
.tooltip(Component.empty())
.tooltip(opt -> Component.empty())
@@ -315,120 +332,6 @@ public class GuiTest {
.build())
.build())
.build())
- .category(ConfigCategory.createBuilder()
- .name(Component.literal("Scroll Test"))
- .option(Option.createBuilder(int.class)
- .name(Component.literal("Int Slider that is cut off because the slider"))
- .binding(
- defaults.scrollingSlider,
- () -> config.scrollingSlider,
- (value) -> config.scrollingSlider = value
- )
- .controller(opt -> new IntegerSliderController(opt, 0, 10, 1))
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .option(ButtonOption.createBuilder()
- .name(Component.literal("Option"))
- .action((screen, opt) -> {
- })
- .controller(ActionController::new)
- .build())
- .build())
- .category(ConfigCategory.createBuilder()
- .name(Component.literal("Category Test"))
- .build())
- .category(ConfigCategory.createBuilder()
- .name(Component.literal("Category Test"))
- .build())
- .category(ConfigCategory.createBuilder()
- .name(Component.literal("Category Test"))
- .build())
- .category(ConfigCategory.createBuilder()
- .name(Component.literal("Category Test"))
- .build())
- .category(ConfigCategory.createBuilder()
- .name(Component.literal("Category Test"))
- .build())
- .category(ConfigCategory.createBuilder()
- .name(Component.literal("Category Test"))
- .build())
- .category(ConfigCategory.createBuilder()
- .name(Component.literal("Category Test"))
- .build())
- .category(ConfigCategory.createBuilder()
- .name(Component.literal("Category Test"))
- .build())
.save(() -> {
Minecraft.getInstance().options.save();
ConfigTest.GSON.save();
diff --git a/test-fabric/build.gradle.kts b/test-fabric/build.gradle.kts
index b6ff7e5..5786b4a 100644
--- a/test-fabric/build.gradle.kts
+++ b/test-fabric/build.gradle.kts
@@ -31,6 +31,9 @@ dependencies {
})
modImplementation(libs.fabric.loader)
+ implementation(libs.twelvemonkeys.imageio.core)
+ implementation(libs.twelvemonkeys.imageio.webp)
+
"common"(project(path = ":test-common", configuration = "namedElements")) { isTransitive = false }
implementation(project(path = ":fabric", configuration = "namedElements"))