diff options
6 files changed, 253 insertions, 9 deletions
diff --git a/src/main/java/io/github/cottonmc/cotton/gui/client/BackgroundPainter.java b/src/main/java/io/github/cottonmc/cotton/gui/client/BackgroundPainter.java index 52bd317..251fed7 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/client/BackgroundPainter.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/client/BackgroundPainter.java @@ -129,6 +129,9 @@ public interface BackgroundPainter { /** * Creates a new nine-patch background painter with a custom configuration. * + * <p>This method cannot be used for {@linkplain Texture.Type#GUI_SPRITE GUI sprites}. Instead, you can use the + * vanilla nine-slice mechanism or use a standalone texture referring to the same file. + * * @param texture the background painter texture * @param configurator a consumer that configures the {@link NinePatch.Builder} * @return the created nine-patch background painter @@ -136,8 +139,13 @@ public interface BackgroundPainter { * @see NinePatch * @see NinePatch.Builder * @see NinePatchBackgroundPainter + * @throws IllegalArgumentException when the texture is not {@linkplain Texture.Type#STANDALONE standalone} */ public static NinePatchBackgroundPainter createNinePatch(Texture texture, Consumer<NinePatch.Builder<Identifier>> configurator) { + if (texture.type() != Texture.Type.STANDALONE) { + throw new IllegalArgumentException("Non-standalone texture " + texture + " cannot be used for nine-patch"); + } + TextureRegion<Identifier> region = new TextureRegion<>(texture.image(), texture.u1(), texture.v1(), texture.u2(), texture.v2()); var builder = NinePatch.builder(region); configurator.accept(builder); diff --git a/src/main/java/io/github/cottonmc/cotton/gui/client/ScreenDrawing.java b/src/main/java/io/github/cottonmc/cotton/gui/client/ScreenDrawing.java index 4064a04..7431597 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/client/ScreenDrawing.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/client/ScreenDrawing.java @@ -9,6 +9,7 @@ import net.minecraft.client.render.GameRenderer; import net.minecraft.client.render.Tessellator; import net.minecraft.client.render.VertexFormat; import net.minecraft.client.render.VertexFormats; +import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.OrderedText; import net.minecraft.text.Style; import net.minecraft.util.Identifier; @@ -105,7 +106,54 @@ public class ScreenDrawing { * @since 3.0.0 */ public static void texturedRect(DrawContext context, int x, int y, int width, int height, Texture texture, int color, float opacity) { - texturedRect(context, x, y, width, height, texture.image(), texture.u1(), texture.v1(), texture.u2(), texture.v2(), color, opacity); + switch (texture.type()) { + // Standalone textures: convert into ID + UVs + case STANDALONE -> texturedRect(context, x, y, width, height, texture.image(), texture.u1(), texture.v1(), texture.u2(), texture.v2(), color, opacity); + + // GUI sprites: Work more carefully as we need to support tiling/nine-slice + case GUI_SPRITE -> { + float r = (color >> 16 & 255) / 255.0F; + float g = (color >> 8 & 255) / 255.0F; + float b = (color & 255) / 255.0F; + RenderSystem.setShaderColor(r, g, b, opacity); + + outer: if (texture.u1() == 0 && texture.u2() == 1 && texture.v1() == 0 && texture.v2() == 1) { + // If we're drawing the full texture, just let vanilla do it. + context.drawGuiTexture(texture.image(), x, y, width, height); + } else { + // If we're only drawing a region, draw the full texture in a larger size and clip it + // to only show the requested region. + float fullWidth = width / Math.abs(texture.u2() - texture.u1()); + float fullHeight = height / Math.abs(texture.v2() - texture.v1()); + + // u1 == u2 or v1 == v2, we don't care about these situations. + if (Float.isInfinite(fullWidth) || Float.isInfinite(fullHeight)) break outer; + + // Calculate the offset left/top coordinates. + int xo = x - (int) (fullWidth * texture.u1()); + int yo = y - (int) (fullHeight * texture.v1()); + + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.translate(xo, yo, 0); + + // Note: scale instead of drawing a (fullWidth, fullHeight) rectangle so that edges of nine-slice + // rectangles etc. are drawn scaled too. This matches the behavior of standalone textures. + matrices.scale(fullWidth / width, fullHeight / height, 1); + + // Clip to the wanted area on the screen... + try (var frame = Scissors.push(x, y, width, height)) { + // ...and draw the texture. + context.drawGuiTexture(texture.image(), 0, 0, width, height); + } + + matrices.pop(); + } + + // Don't let the color cause tinting to other draw calls. + RenderSystem.setShaderColor(1, 1, 1, 1); + } + } } /** diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/WBar.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/WBar.java index 0a4f51b..fad1579 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/WBar.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/WBar.java @@ -162,7 +162,8 @@ public class WBar extends WWidget { int top = y + getHeight(); top -= barSize; if (bar != null) { - ScreenDrawing.texturedRect(context, left, top, getWidth(), barSize, bar.image(), bar.u1(), MathHelper.lerp(percent, bar.v2(), bar.v1()), bar.u2(), bar.v2(), 0xFFFFFFFF); + Texture clipped = bar.withUv(bar.u1(), MathHelper.lerp(percent, bar.v2(), bar.v1()), bar.u2(), bar.v2()); + ScreenDrawing.texturedRect(context, left, top, getWidth(), barSize, clipped, 0xFFFFFFFF); } else { ScreenDrawing.coloredRect(context, left, top, getWidth(), barSize, ScreenDrawing.colorAtOpacity(0xFFFFFF, 0.5f)); } @@ -170,7 +171,8 @@ public class WBar extends WWidget { case RIGHT -> { if (bar != null) { - ScreenDrawing.texturedRect(context, x, y, barSize, getHeight(), bar.image(), bar.u1(), bar.v1(), MathHelper.lerp(percent, bar.u1(), bar.u2()), bar.v2(), 0xFFFFFFFF); + Texture clipped = bar.withUv(bar.u1(), bar.v1(), MathHelper.lerp(percent, bar.u1(), bar.u2()), bar.v2()); + ScreenDrawing.texturedRect(context, x, y, barSize, getHeight(), clipped, 0xFFFFFFFF); } else { ScreenDrawing.coloredRect(context, x, y, barSize, getHeight(), ScreenDrawing.colorAtOpacity(0xFFFFFF, 0.5f)); } @@ -178,7 +180,8 @@ public class WBar extends WWidget { case DOWN -> { if (bar != null) { - ScreenDrawing.texturedRect(context, x, y, getWidth(), barSize, bar.image(), bar.u1(), bar.v1(), bar.u2(), MathHelper.lerp(percent, bar.v1(), bar.v2()), 0xFFFFFFFF); + Texture clipped = bar.withUv(bar.u1(), bar.v1(), bar.u2(), MathHelper.lerp(percent, bar.v1(), bar.v2())); + ScreenDrawing.texturedRect(context, x, y, getWidth(), barSize, clipped, 0xFFFFFFFF); } else { ScreenDrawing.coloredRect(context, x, y, getWidth(), barSize, ScreenDrawing.colorAtOpacity(0xFFFFFF, 0.5f)); } @@ -189,7 +192,8 @@ public class WBar extends WWidget { int top = y; left -= barSize; if (bar != null) { - ScreenDrawing.texturedRect(context, left, top, barSize, getHeight(), bar.image(), MathHelper.lerp(percent, bar.u2(), bar.u1()), bar.v1(), bar.u2(), bar.v2(), 0xFFFFFFFF); + Texture clipped = bar.withUv(MathHelper.lerp(percent, bar.u2(), bar.u1()), bar.v1(), bar.u2(), bar.v2()); + ScreenDrawing.texturedRect(context, left, top, barSize, getHeight(), clipped, 0xFFFFFFFF); } else { ScreenDrawing.coloredRect(context, left, top, barSize, getHeight(), ScreenDrawing.colorAtOpacity(0xFFFFFF, 0.5f)); } diff --git a/src/main/java/io/github/cottonmc/cotton/gui/widget/data/Texture.java b/src/main/java/io/github/cottonmc/cotton/gui/widget/data/Texture.java index e1265ce..67ee2a8 100644 --- a/src/main/java/io/github/cottonmc/cotton/gui/widget/data/Texture.java +++ b/src/main/java/io/github/cottonmc/cotton/gui/widget/data/Texture.java @@ -7,36 +7,100 @@ import java.util.Objects; /** * Represents a texture for a widget. * + * <h2>Types</h2> + * <p>Each texture has a type: it's either a {@linkplain Type#STANDALONE standalone texture file} or + * a {@linkplain Type#GUI_SPRITE sprite on the GUI sprite atlas}. Their properties are slightly different. + * + * <p>GUI sprites can use their full range of features such as tiling, stretching and nine-slice drawing modes, + * while standalone textures are only drawn stretched. + * + * <p>The format of the image ID depends on the type: + * <table> + * <thead> + * <tr> + * <th>Type</th> + * <th>File path</th> + * <th>Image ID</th> + * </tr> + * </thead> + * <tbody> + * <tr> + * <td>{@link Type#STANDALONE STANDALONE}</td> + * <td>{@code assets/my_mod/textures/widget/example.png}</td> + * <td>{@code my_mod:textures/widget/example.png}</td> + * </tr> + * <tr> + * <td>{@link Type#GUI_SPRITE GUI_SPRITE}</td> + * <td>{@code assets/my_mod/textures/gui/sprites/example.png}</td> + * <td>{@code my_mod:example}</td> + * </tr> + * </tbody> + * </table> + * + * <p>Note that the image ID can only be passed to non-{@code Texture} overloads of + * <code>{@link io.github.cottonmc.cotton.gui.client.ScreenDrawing ScreenDrawing}.texturedRect()</code> + * when the {@link #type() type} is {@link Type#STANDALONE}. GUI sprites need specialised code for drawing them, + * and they need to be drawn with specific {@code Texture}-accepting methods + * or {@link net.minecraft.client.gui.DrawContext}. + * * @param image the image of this texture + * @param type the type of this texture * @param u1 the start U-coordinate, between 0 and 1 * @param v1 the start V-coordinate, between 0 and 1 * @param u2 the end U-coordinate, between 0 and 1 * @param v2 the end V-coordinate, between 0 and 1 * @since 3.0.0 */ -public record Texture(Identifier image, float u1, float v1, float u2, float v2) { +public record Texture(Identifier image, Type type, float u1, float v1, float u2, float v2) { /** * Constructs a new texture that uses the full image. * * @param image the image + * @param type the type + * @throws NullPointerException if the image or the type is null + */ + public Texture(Identifier image, Type type) { + this(image, type, 0, 0, 1, 1); + } + + /** + * Constructs a new standalone texture with custom UV values. + * + * @param image the image of this texture + * @param u1 the start U-coordinate, between 0 and 1 + * @param v1 the start V-coordinate, between 0 and 1 + * @param u2 the end U-coordinate, between 0 and 1 + * @param v2 the end V-coordinate, between 0 and 1 + * @throws NullPointerException if the image is null + */ + public Texture(Identifier image, float u1, float v1, float u2, float v2) { + this(image, Type.STANDALONE, u1, v1, u2, v2); + } + + /** + * Constructs a new standalone texture that uses the full image. + * + * @param image the image * @throws NullPointerException if the image is null */ public Texture(Identifier image) { - this(image, 0, 0, 1, 1); + this(image, Type.STANDALONE, 0, 0, 1, 1); } /** * Constructs a new texture with custom UV values. * * @param image the image + * @param type the type * @param u1 the left U coordinate * @param v1 the top V coordinate * @param u2 the right U coordinate * @param v2 the bottom V coordinate - * @throws NullPointerException if the image is null + * @throws NullPointerException if the image or the type is null */ public Texture { Objects.requireNonNull(image, "image"); + Objects.requireNonNull(type, "type"); } /** @@ -49,6 +113,31 @@ public record Texture(Identifier image, float u1, float v1, float u2, float v2) * @return the created texture */ public Texture withUv(float u1, float v1, float u2, float v2) { - return new Texture(image, u1, v1, u2, v2); + return new Texture(image, type, u1, v1, u2, v2); + } + + /** + * A {@link Texture}'s type. It represents the location of the texture. + * + * @since 9.0.0 + */ + public enum Type { + /** + * A texture in a standalone texture file. + * + * <p>The image IDs of standalone textures contain the full file path to the texture inside + * the {@code assets/<namespace>} directory. For example, {@code my_mod:textures/widget/example.png} refers to + * {@code assets/my_mod/textures/widget/example.png}. + */ + STANDALONE, + + /** + * A texture in the GUI sprite atlas. + * + * <p>The image IDs of GUI sprites only contain the subpath to the texture inside the sprite directory without + * the file extension. For example, {@code my_mod:example} refers to + * {@code assets/my_mod/textures/gui/sprites/example.png}. + */ + GUI_SPRITE, } } diff --git a/src/testMod/java/io/github/cottonmc/test/client/LibGuiTestClient.java b/src/testMod/java/io/github/cottonmc/test/client/LibGuiTestClient.java index 57c2790..a9500ae 100644 --- a/src/testMod/java/io/github/cottonmc/test/client/LibGuiTestClient.java +++ b/src/testMod/java/io/github/cottonmc/test/client/LibGuiTestClient.java @@ -68,6 +68,7 @@ public class LibGuiTestClient implements ClientModInitializer { .then(literal("#196").executes(openScreen(client -> new Issue196TestGui()))) .then(literal("darkmode").executes(openScreen(client -> new DarkModeTestGui()))) .then(literal("titlealignment").executes(openScreen(Text.literal("test title"), client -> new TitleAlignmentTestGui()))) + .then(literal("texture").executes(openScreen(client -> new TextureTestGui()))) )); } diff --git a/src/testMod/java/io/github/cottonmc/test/client/TextureTestGui.java b/src/testMod/java/io/github/cottonmc/test/client/TextureTestGui.java new file mode 100644 index 0000000..6925a3b --- /dev/null +++ b/src/testMod/java/io/github/cottonmc/test/client/TextureTestGui.java @@ -0,0 +1,94 @@ +package io.github.cottonmc.test.client; + +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; + +import io.github.cottonmc.cotton.gui.client.LightweightGuiDescription; +import io.github.cottonmc.cotton.gui.widget.WGridPanel; +import io.github.cottonmc.cotton.gui.widget.WLabeledSlider; +import io.github.cottonmc.cotton.gui.widget.WPanel; +import io.github.cottonmc.cotton.gui.widget.WSlider; +import io.github.cottonmc.cotton.gui.widget.WSprite; +import io.github.cottonmc.cotton.gui.widget.WTabPanel; +import io.github.cottonmc.cotton.gui.widget.data.Axis; +import io.github.cottonmc.cotton.gui.widget.data.Insets; +import io.github.cottonmc.cotton.gui.widget.data.Texture; +import io.github.cottonmc.cotton.gui.widget.icon.TextureIcon; + +import java.util.function.IntConsumer; + +public class TextureTestGui extends LightweightGuiDescription { + public TextureTestGui() { + WTabPanel root = new WTabPanel(); + + var panelSprite = new Texture(new Identifier("libgui:widget/panel_light"), Texture.Type.GUI_SPRITE); + var panelTexture = new Texture(new Identifier("libgui:textures/gui/sprites/widget/panel_light.png"), Texture.Type.STANDALONE); + var simpleSprite = new Texture(new Identifier("minecraft:icon/video_link"), Texture.Type.GUI_SPRITE); + + root.add(createPanel(panelSprite), tab -> tab.icon(new TextureIcon(panelSprite)).tooltip(Text.literal("Nine-slice sprite"))); + root.add(createPanel(simpleSprite), tab -> tab.icon(new TextureIcon(simpleSprite)).tooltip(Text.literal("Simple sprite"))); + root.add(createPanel(panelTexture), tab -> tab.icon(new TextureIcon(panelTexture)).tooltip(Text.literal("Standalone"))); + setRootPanel(root); + root.validate(this); + } + + @Override + public void addPainters() { + // Remove tab panel background + } + + private WPanel createPanel(Texture texture) { + WSprite sprite = new WSprite(texture); + + WLabeledSlider red = new WLabeledSlider(0, 255, Axis.HORIZONTAL, Text.literal("Red")); + WLabeledSlider green = new WLabeledSlider(0, 255, Axis.HORIZONTAL, Text.literal("Green")); + WLabeledSlider blue = new WLabeledSlider(0, 255, Axis.HORIZONTAL, Text.literal("Blue")); + WLabeledSlider alpha = new WLabeledSlider(0, 255, Axis.HORIZONTAL, Text.literal("Alpha")); + + red.setValue(255); + green.setValue(255); + blue.setValue(255); + alpha.setValue(255); + + WSlider u1 = new WSlider(0, 100, Axis.HORIZONTAL); + WSlider u2 = new WSlider(0, 100, Axis.HORIZONTAL); + WSlider v1 = new WSlider(0, 100, Axis.VERTICAL); + WSlider v2 = new WSlider(0, 100, Axis.VERTICAL); + + u2.setValue(100); + v2.setValue(100); + + IntConsumer tintListener = unused -> { + sprite.setTint(blue.getValue() | (green.getValue() << 8) | (red.getValue() << 16) | (alpha.getValue() << 24)); + }; + red.setValueChangeListener(tintListener); + green.setValueChangeListener(tintListener); + blue.setValueChangeListener(tintListener); + alpha.setValueChangeListener(tintListener); + + IntConsumer uvListener = unused -> { + sprite.setUv(u1.getValue() * 0.01f, v1.getValue() * 0.01f, u2.getValue() * 0.01f, v2.getValue() * 0.01f); + }; + u1.setValueChangeListener(uvListener); + u2.setValueChangeListener(uvListener); + v1.setValueChangeListener(uvListener); + v2.setValueChangeListener(uvListener); + + WGridPanel panel = new WGridPanel(20); + panel.setInsets(Insets.ROOT_PANEL); + panel.setGaps(3, 3); + + panel.add(red, 0, 0, 3, 1); + panel.add(green, 3, 0, 3, 1); + panel.add(blue, 0, 1, 3, 1); + panel.add(alpha, 3, 1, 3, 1); + + panel.add(u1, 2, 2, 4, 1); + panel.add(u2, 2, 3, 4, 1); + panel.add(v1, 0, 4, 1, 4); + panel.add(v2, 1, 4, 1, 4); + + panel.add(sprite, 2, 4, 4, 4); + return panel; + } +} |