aboutsummaryrefslogtreecommitdiff
path: root/common/src/main/java/dev/isxander/yacl
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 /common/src/main/java/dev/isxander/yacl
parente51af159ba3eba5ebda976bea1c1957cddeee7c6 (diff)
downloadYetAnotherConfigLib-21afea0da3956f2d8cca81a54fa9820152e0c077.tar.gz
YetAnotherConfigLib-21afea0da3956f2d8cca81a54fa9820152e0c077.tar.bz2
YetAnotherConfigLib-21afea0da3956f2d8cca81a54fa9820152e0c077.zip
Start overhauling UI
Diffstat (limited to 'common/src/main/java/dev/isxander/yacl')
-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
18 files changed, 1118 insertions, 363 deletions
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;