diff --git a/src/main/java/dev/hephaestus/glowcase/block/entity/TextBlockEntity.java b/src/main/java/dev/hephaestus/glowcase/block/entity/TextBlockEntity.java index 9e19fede..069a2a35 100644 --- a/src/main/java/dev/hephaestus/glowcase/block/entity/TextBlockEntity.java +++ b/src/main/java/dev/hephaestus/glowcase/block/entity/TextBlockEntity.java @@ -6,18 +6,21 @@ import eu.pb4.placeholders.api.ParserContext; import eu.pb4.placeholders.api.parsers.NodeParser; import eu.pb4.placeholders.api.parsers.TagParser; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import io.netty.buffer.ByteBuf; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentSerialization; import net.minecraft.network.chat.Style; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; import net.minecraft.util.StringRepresentable; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueOutput; +import java.util.ArrayList; +import java.util.List; + public class TextBlockEntity extends GlowcaseBlockEntity { public static final NodeParser PARSER = TagParser.DEFAULT; @@ -25,6 +28,7 @@ public class TextBlockEntity extends GlowcaseBlockEntity { public List lines = new ArrayList<>(); public TextAlignment textAlignment = TextAlignment.CENTER; + public HorizontalAlignment horizontalAlignment = HorizontalAlignment.CENTER; public ZOffset zOffset = ZOffset.CENTER; public boolean shadow = true; public float scale = 1F; @@ -47,6 +51,7 @@ protected void saveAdditional(ValueOutput view) { view.putInt("background_color", this.backgroundColor); view.store("text_alignment", TextAlignment.CODEC, this.textAlignment); + view.store("horizontal_alignment", HorizontalAlignment.CODEC, this.horizontalAlignment); view.store("z_offset", ZOffset.CODEC, this.zOffset); view.putBoolean("shadow", this.shadow); view.putFloat("viewDistance", this.viewDistance); @@ -69,6 +74,7 @@ protected void loadAdditional(ValueInput view) { this.backgroundColor = view.getIntOr("background_color", 0); this.shadow = view.getBooleanOr("shadow", true); this.textAlignment = view.read("text_alignment", TextAlignment.CODEC).orElse(TextAlignment.CENTER); + this.horizontalAlignment = view.read("horizontal_alignment", HorizontalAlignment.CODEC).orElse(HorizontalAlignment.CENTER); this.zOffset = view.read("z_offset", ZOffset.CODEC).orElse(ZOffset.CENTER); this.viewDistance = view.getFloatOr("viewDistance", -1); this.lines = new ArrayList<>(view.read("lines", ComponentSerialization.CODEC.listOf()).orElseGet(List::of)); @@ -111,7 +117,13 @@ public void setRawLine(int i, String string) { } public enum TextAlignment implements StringRepresentable { - LEFT, CENTER, CENTER_LEFT, CENTER_RIGHT, RIGHT; + LEFT, + CENTER, + @Deprecated + CENTER_LEFT, + @Deprecated + CENTER_RIGHT, + RIGHT; public static final Codec CODEC = StringRepresentable.fromEnum(TextAlignment::values); @@ -131,4 +143,16 @@ public String getSerializedName() { return name().toLowerCase(); } } + + public enum HorizontalAlignment implements StringRepresentable { + LEFT, CENTER, RIGHT; + + public static final Codec CODEC = StringRepresentable.fromEnum(HorizontalAlignment::values); + public static final StreamCodec STREAM_CODEC = ByteBufCodecs.BYTE.map(index -> TextBlockEntity.HorizontalAlignment.values()[index], textAlignment -> (byte) textAlignment.ordinal()); + + @Override + public String getSerializedName() { + return name().toLowerCase(); + } + } } diff --git a/src/main/java/dev/hephaestus/glowcase/client/GlowcaseClient.java b/src/main/java/dev/hephaestus/glowcase/client/GlowcaseClient.java index 2eeee6e1..2c00e6fe 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/GlowcaseClient.java +++ b/src/main/java/dev/hephaestus/glowcase/client/GlowcaseClient.java @@ -37,7 +37,7 @@ public class GlowcaseClient implements ClientModInitializer { public void onInitializeClient() { Glowcase.proxy = new GlowcaseClientProxy(); - BlockEntityRenderers.register(Glowcase.TEXT_BLOCK_ENTITY.get(), (ctx) -> new TextBlockEntityRenderer()); + BlockEntityRenderers.register(Glowcase.TEXT_BLOCK_ENTITY.get(), (ctx) -> new TextBlockEntityRenderer(ctx)); BlockEntityRenderers.register(Glowcase.HYPERLINK_BLOCK_ENTITY.get(), HyperlinkBlockEntityRenderer::new); BlockEntityRenderers.register(Glowcase.CONFIG_LINK_BLOCK_ENTITY.get(), ConfigLinkBlockEntityRenderer::new); BlockEntityRenderers.register(Glowcase.ITEM_DISPLAY_BLOCK_ENTITY.get(), ItemDisplayBlockEntityRenderer::new); diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/GlowcaseScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/GlowcaseScreen.java index c00f0d8c..0f541f4b 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/GlowcaseScreen.java +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/GlowcaseScreen.java @@ -9,6 +9,10 @@ protected GlowcaseScreen() { super(Component.empty()); } + protected GlowcaseScreen(Component title) { + super(title); + } + @Override public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float deltaTicks) { this.extractTransparentBackground(graphics); diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/TextBlockEditScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/TextBlockEditScreen.java index fb5d1d6a..48e8f931 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/TextBlockEditScreen.java +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/TextBlockEditScreen.java @@ -1,6 +1,5 @@ package dev.hephaestus.glowcase.client.gui.screen.ingame; -import com.google.common.primitives.Floats; import dev.hephaestus.glowcase.block.entity.TextBlockEntity; import dev.hephaestus.glowcase.client.gui.widget.ingame.ColorPickerWidget; import dev.hephaestus.glowcase.client.util.ColorUtil; @@ -8,8 +7,8 @@ import eu.pb4.placeholders.api.parsers.tag.TagRegistry; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.AbstractSliderButton; import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.Checkbox; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.components.events.GuiEventListener; @@ -30,22 +29,14 @@ public class TextBlockEditScreen extends TextEditorScreen { private final TextBlockEntity textBlockEntity; private List textWidgets; - private List colorListeners; private TextFieldHelper selectionManager; + private EditBox colorEntryWidget; private int currentRow; private long ticksSinceOpened = 0; private ColorPickerWidget colorPickerWidget; - private Button changeAlignment; - private EditBox colorEntryWidget; - private EditBox backgroundColorEntryWidget; private Color colorEntryPreColorPicker; //used for color picker cancel button - private Button zOffsetToggle; - private Checkbox shadowToggle; - - private EditBox viewDistanceField; - private Button viewDistanceHelpButton; public TextBlockEditScreen(TextBlockEntity textBlockEntity) { this.textBlockEntity = textBlockEntity; @@ -67,42 +58,24 @@ public void init() { int middle = width / 2; - Button decreaseSize = Button.builder(Component.literal("-"), action -> { - this.textBlockEntity.scale = Math.max(0, this.textBlockEntity.scale - (minecraft.hasShiftDown() ? 1F : 0.125F)); - this.textBlockEntity.renderDirty = true; - }).bounds(middle - 130, 0, 20, 20).build(); - - Button increaseSize = Button.builder(Component.literal("+"), action -> { - this.textBlockEntity.scale += minecraft.hasShiftDown() ? 1F : 0.125F; - this.textBlockEntity.renderDirty = true; - }).bounds(middle - 110, 0, 20, 20).build(); - - this.changeAlignment = Button.builder(Component.translatableEscape("gui.glowcase.alignment", this.textBlockEntity.textAlignment), action -> { - switch (textBlockEntity.textAlignment) { - case LEFT -> textBlockEntity.textAlignment = TextBlockEntity.TextAlignment.CENTER; - case CENTER -> textBlockEntity.textAlignment = TextBlockEntity.TextAlignment.CENTER_LEFT; - case CENTER_LEFT -> textBlockEntity.textAlignment = TextBlockEntity.TextAlignment.CENTER_RIGHT; - case CENTER_RIGHT -> textBlockEntity.textAlignment = TextBlockEntity.TextAlignment.RIGHT; - case RIGHT -> textBlockEntity.textAlignment = TextBlockEntity.TextAlignment.LEFT; - } - this.textBlockEntity.renderDirty = true; - - this.changeAlignment.setMessage(Component.translatableEscape("gui.glowcase.alignment", this.textBlockEntity.textAlignment)); - }).bounds(middle - 90 + innerPadding, 0, 160, 20).build(); + var scaleSlider = new TextScaleSliderWidget(textBlockEntity, middle - 203, 0, 113, 20); + this.addRenderableWidget(scaleSlider); - this.shadowToggle = Checkbox.builder(Component.translatable("gui.glowcase.shadow"), this.font) - .selected(this.textBlockEntity.shadow) - .onValueChange((widget, checked) -> { - this.textBlockEntity.shadow = checked; - this.textBlockEntity.renderDirty = true; - }) - .pos(middle - 90 + innerPadding, 20 + innerPadding).build(); + var moreOptionsButton = Button.builder( + Component.translatable("gui.glowcase.more"), + button -> { + var optionsScreen = new TextBlockOptionsScreen(this, textBlockEntity); + Minecraft.getInstance().setScreen(optionsScreen); + }) + .bounds(middle + 124, 0, 80, 20) + .build(); + this.addRenderableWidget(moreOptionsButton); - this.colorEntryWidget = new EditBox(this.minecraft.font, middle + 70 + innerPadding * 2, 0, 64, 20, Component.empty()); + this.colorEntryWidget = new EditBox(this.minecraft.font, middle + 54, 0, 64, 20, Component.empty()); this.colorEntryWidget.setTooltip(Tooltip.create(Component.translatable("gui.glowcase.color"))); this.colorEntryWidget.setValue(ColorUtil.toAlphaHex(this.textBlockEntity.color)); this.colorEntryWidget.setResponder(string -> { - ColorUtil.parse(this.colorEntryWidget.getValue(), this.textBlockEntity.color).ifSuccess(newColor -> { + ColorUtil.parse(string, this.textBlockEntity.color).ifSuccess(newColor -> { final int color = (Math.max(newColor >>> 24, 0x1A) << 24) | (newColor & ColorUtil.COLOR_MASK); this.textBlockEntity.color = color; @@ -114,70 +87,21 @@ public void init() { }); }); - this.backgroundColorEntryWidget = new EditBox(this.minecraft.font, middle + 136 + innerPadding * 2, 0, 64, 20, Component.empty()); - this.backgroundColorEntryWidget.setTooltip(Tooltip.create(Component.translatable("gui.glowcase.background_color"))); - this.backgroundColorEntryWidget.setValue(ColorUtil.toAlphaHex(this.textBlockEntity.backgroundColor)); - this.backgroundColorEntryWidget.setResponder(string -> { - ColorUtil.parse(string, this.textBlockEntity.backgroundColor).ifSuccess(newColor -> { - this.textBlockEntity.backgroundColor = newColor; - if (this.colorEntryWidget.isFocused()) { - this.colorPickerWidget.setColor(new Color(newColor)); - } - this.textBlockEntity.renderDirty = true; - }); - }); - - this.zOffsetToggle = Button.builder(Component.literal(this.textBlockEntity.zOffset.name()), action -> { - switch (textBlockEntity.zOffset) { - case FRONT -> textBlockEntity.zOffset = TextBlockEntity.ZOffset.CENTER; - case CENTER -> textBlockEntity.zOffset = TextBlockEntity.ZOffset.BACK; - case BACK -> textBlockEntity.zOffset = TextBlockEntity.ZOffset.FRONT; - } - this.textBlockEntity.renderDirty = true; - - this.zOffsetToggle.setMessage(Component.literal(this.textBlockEntity.zOffset.name())); - }).bounds(middle + 2, 20 + innerPadding, 72, 20).build(); - this.colorPickerWidget = ColorPickerWidget.builder(this, 216, 10).size(182, 104).build(); this.colorPickerWidget.toggle(false); //start deactivated - this.viewDistanceField = new EditBox(this.minecraft.font, middle - 203, 20 + innerPadding, 83 + innerPadding, 20, Component.empty()); - this.viewDistanceField.setValue(String.valueOf(this.textBlockEntity.viewDistance)); - this.viewDistanceField.setResponder(s -> { - if (Floats.tryParse(s) instanceof Float parsed) { - this.textBlockEntity.viewDistance = parsed; - } - }); - this.viewDistanceField.setTooltip(Tooltip.create(Component.translatable("gui.glowcase.screen.text_edit.view_distance"))); - this.viewDistanceHelpButton = Button.builder(Component.literal("?"), action -> { - }) - .bounds(middle - 115 + innerPadding + 5, 20 + innerPadding, 20, 20).build(); - this.viewDistanceHelpButton.setTooltip(Tooltip.create(Component.translatable("gui.glowcase.screen.text_edit.view_distance"))); - this.addRenderableWidget(colorPickerWidget); - this.addRenderableWidget(increaseSize); - this.addRenderableWidget(decreaseSize); - this.addRenderableWidget(this.changeAlignment); - this.addRenderableWidget(this.shadowToggle); - this.addRenderableWidget(this.zOffsetToggle); this.addRenderableWidget(this.colorEntryWidget); - this.addRenderableWidget(this.backgroundColorEntryWidget); - - this.addRenderableWidget(this.viewDistanceField); - this.addRenderableWidget(this.viewDistanceHelpButton); this.textWidgets = List.of( - this.colorEntryWidget, - this.backgroundColorEntryWidget, - this.viewDistanceField + this.colorEntryWidget ); this.colorListeners = List.of( - this.colorEntryWidget, - this.backgroundColorEntryWidget + this.colorEntryWidget ); - addFormattingButtons(middle + 70, 20, innerPadding, 20, 2); + addFormattingButtons(middle - 90 + 6, 0, 0, 20, 2); } @Override @@ -204,14 +128,13 @@ public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mo super.extractRenderState(graphics, mouseX, mouseY, delta); graphics.pose().pushMatrix(); - graphics.pose().translate(0, 40 + 2 * this.width / 100F); + graphics.pose().translate(0, 20 + 2 * this.width / 100F); for (int i = 0; i < this.textBlockEntity.lines.size(); ++i) { var text = this.currentRow == i ? Component.literal(this.textBlockEntity.getRawLine(i)) : this.textBlockEntity.lines.get(i); int lineWidth = this.font.width(text); switch (this.textBlockEntity.textAlignment) { - case LEFT -> - graphics.text(minecraft.font, text, this.width / 10, i * 12, this.textBlockEntity.color); + case LEFT -> graphics.text(minecraft.font, text, this.width / 10, i * 12, this.textBlockEntity.color); case CENTER, CENTER_LEFT, CENTER_RIGHT -> graphics.text(minecraft.font, text, this.width / 2 - lineWidth / 2, i * 12, this.textBlockEntity.color); case RIGHT -> @@ -255,7 +178,7 @@ public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mo } graphics.pose().popMatrix(); - graphics.text(minecraft.font, Component.translatable("gui.glowcase.scale_value", this.textBlockEntity.scale), width / 2 - 203, 7, 0xFFFFFFFF); + colorPickerWidget.extractRenderState(graphics, mouseX, mouseY, delta); } @@ -509,4 +432,28 @@ public void toggleColorPicker(boolean active) { TextFieldHelper getSelectionManager() { return this.selectionManager; } + + public static class TextScaleSliderWidget extends AbstractSliderButton { + private static final float MIN_SCALE = 0.125F; + private static final float MAX_SCALE = 16; + + private final TextBlockEntity entity; + + public TextScaleSliderWidget(TextBlockEntity entity, int x, int y, int width, int height) { + var initialValue = (entity.scale - MIN_SCALE) / (MAX_SCALE - MIN_SCALE); + super(x, y, width, height, Component.translatable("gui.glowcase.scale_value", entity.scale), initialValue); + this.entity = entity; + } + + @Override + protected void updateMessage() { + this.setMessage(Component.translatable("gui.glowcase.scale_value", entity.scale)); + } + + @Override + protected void applyValue() { + entity.scale = (float) Math.round(Mth.lerp(this.value, MIN_SCALE, MAX_SCALE) * 8F) / 8F; + entity.renderDirty = true; + } + } } diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/TextBlockOptionsScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/TextBlockOptionsScreen.java new file mode 100644 index 00000000..28016505 --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/TextBlockOptionsScreen.java @@ -0,0 +1,281 @@ +package dev.hephaestus.glowcase.client.gui.screen.ingame; + +import dev.hephaestus.glowcase.block.entity.TextBlockEntity; +import dev.hephaestus.glowcase.client.util.ColorUtil; +import dev.hephaestus.glowcase.packet.C2SEditTextBlock; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.*; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.util.Mth; + +import java.util.List; + +public class TextBlockOptionsScreen extends Screen { + private final Screen returnScreen; + private final TextBlockEntity entity; + + public final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this); + public TextOptionList options; + + public TextBlockOptionsScreen(Screen returnScreen, TextBlockEntity entity) { + super(Component.translatable("gui.glowcase.text_options")); + this.returnScreen = returnScreen; + this.entity = entity; + } + + @Override + public void onClose() { + super.onClose(); + C2SEditTextBlock.of(this.entity).send(); + Minecraft.getInstance().setScreen(this.returnScreen); + } + + @Override + public void init() { + super.init(); + + this.layout.addTitleHeader(this.title, this.font); + this.layout.addToFooter(Button.builder(CommonComponents.GUI_DONE, _ -> this.onClose()).width(200).build()); + + this.options = this.layout.addToContents(new TextOptionList(this.minecraft, this.width, this.layout.getContentHeight(), this.layout.getHeaderHeight())); + this.options.add( + new TextBlockEditScreen.TextScaleSliderWidget(this.entity, -1, -1, Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT), + new TextRenderDistanceSliderWidget(this.entity, -1, -1) + ); + this.options.add( + CycleButton.builder( + alignment -> Component.literal(alignment.toString()), + entity.horizontalAlignment + ) + .withValues(TextBlockEntity.HorizontalAlignment.values()) + .create( + Component.translatable("gui.glowcase.x_offset_label"), + (_, alignment) -> { + entity.horizontalAlignment = alignment; + entity.renderDirty = true; + } + ), + CycleButton.builder( + offset -> Component.literal(offset.toString()), + entity.zOffset + ) + .withValues(TextBlockEntity.ZOffset.values()) + .create( + Component.translatable("gui.glowcase.z_offset_label"), + (_, offset) -> { + entity.zOffset = offset; + entity.renderDirty = true; + } + ) + ); + this.options.add( + CycleButton.builder( + alignment -> Component.literal(alignment.toString()), + entity.textAlignment + ) + .withValues( + // not `.values()` to not have CENTER_LEFT or CENTER_RIGHT, unless they get removed + TextBlockEntity.TextAlignment.CENTER, + TextBlockEntity.TextAlignment.LEFT, + TextBlockEntity.TextAlignment.RIGHT + ) + .create( + Component.translatable("gui.glowcase.text_alignment"), + (_, alignment) -> { + entity.textAlignment = alignment; + entity.renderDirty = true; + } + ), + CycleButton.onOffBuilder(entity.shadow).create( + Component.translatable("gui.glowcase.text_shadow"), + (_, shadow) -> { + entity.shadow = shadow; + entity.renderDirty = true; + } + ) + ); + + this.options.addHeader(Component.translatable("gui.glowcase.color")); + var colorEditBox = new EditBox( + this.font, + Button.DEFAULT_WIDTH, + Button.DEFAULT_HEIGHT, + Component.translatable("gui.glowcase.color") + ); + colorEditBox.setValue(ColorUtil.toAlphaHex(this.entity.color)); + colorEditBox.setResponder(string -> ColorUtil.parse(string, entity.color) + .ifSuccess(newColor -> { + entity.color = newColor; + entity.renderDirty = true; + })); + this.options.add(colorEditBox); + + this.options.addHeader(Component.translatable("gui.glowcase.background_color")); + var backgroundEditBox = new EditBox( + this.font, + Button.DEFAULT_WIDTH, + Button.DEFAULT_HEIGHT, + Component.translatable("gui.glowcase.background_color") + ); + backgroundEditBox.setValue(ColorUtil.toAlphaHex(this.entity.backgroundColor)); + backgroundEditBox.setResponder(string -> ColorUtil.parse(string, entity.backgroundColor) + .ifSuccess(newColor -> { + entity.backgroundColor = newColor; + entity.renderDirty = true; + })); + this.options.add(backgroundEditBox); + + this.layout.visitWidgets(this::addRenderableWidget); + this.layout.arrangeElements(); + } + + public static class TextOptionList extends ContainerObjectSelectionList { + public TextOptionList(Minecraft minecraft, int width, int height, int y) { + super(minecraft, width, height, y, Button.DEFAULT_HEIGHT + 5); + } + + public void addHeader(Component text) { + int lineHeight = this.minecraft.font.lineHeight; + int paddingTop = this.children().isEmpty() ? 0 : lineHeight * 2; + this.addEntry(new HeaderEntry(new StringWidget(text, this.minecraft.font), paddingTop), paddingTop + lineHeight + 4); + } + + public void add(AbstractWidget widget) { + this.addEntry(new WidgetEntry(widget)); + } + + public void add(AbstractWidget leftWidget, AbstractWidget rightWidget) { + this.addEntry(new TwoWidgetsEntry(leftWidget, rightWidget)); + } + + @Override + public int getRowWidth() { + return 310; + } + + public static abstract class Entry extends ContainerObjectSelectionList.Entry { + + } + + public static class HeaderEntry extends Entry { + protected final StringWidget widget; + protected final int paddingTop; + + public HeaderEntry(StringWidget widget, int paddingTop) { + this.widget = widget; + this.paddingTop = paddingTop; + } + + @Override + public void extractContent(GuiGraphicsExtractor graphics, int mouseX, int mouseY, boolean hovered, float a) { + this.widget.setPosition(this.getContentX(), this.getContentY() + this.paddingTop); + this.widget.extractRenderState(graphics, mouseX, mouseY, a); + } + + @Override + public List narratables() { + return List.of(this.widget); + } + + @Override + public List children() { + return List.of(this.widget); + } + } + + public static class WidgetEntry extends Entry { + protected final AbstractWidget widget; + + public WidgetEntry(AbstractWidget widget) { + this.widget = widget; + } + + @Override + public void extractContent(GuiGraphicsExtractor graphics, int mouseX, int mouseY, boolean hovered, float a) { + this.widget.setPosition(this.getContentX(), this.getContentY()); + this.widget.extractRenderState(graphics, mouseX, mouseY, a); + } + + @Override + public List narratables() { + return List.of(this.widget); + } + + @Override + public List children() { + return List.of(this.widget); + } + } + + public static class TwoWidgetsEntry extends Entry { + protected final AbstractWidget leftWidget; + protected final AbstractWidget rightWidget; + + public TwoWidgetsEntry(AbstractWidget leftWidget, AbstractWidget rightWidget) { + this.leftWidget = leftWidget; + this.rightWidget = rightWidget; + } + + @Override + public void extractContent(GuiGraphicsExtractor graphics, int mouseX, int mouseY, boolean hovered, float a) { + this.leftWidget.setPosition(this.getContentX(), this.getContentY()); + this.leftWidget.extractRenderState(graphics, mouseX, mouseY, a); + this.rightWidget.setPosition(this.getContentX() + 160, this.getContentY()); + this.rightWidget.extractRenderState(graphics, mouseX, mouseY, a); + } + + @Override + public List narratables() { + return List.of(this.leftWidget, this.rightWidget); + } + + @Override + public List children() { + return List.of(this.leftWidget, this.rightWidget); + } + } + } + + private static class TextRenderDistanceSliderWidget extends AbstractSliderButton { + private static final float INFINITE_VALUE = -1; + private static final float MIN_VALUE = 1; + private static final float MAX_VALUE = 256; + + private final TextBlockEntity entity; + + public TextRenderDistanceSliderWidget(TextBlockEntity entity, int x, int y) { + double initialValue = entity.viewDistance == INFINITE_VALUE ? 1 : (entity.viewDistance - MIN_VALUE) / (MAX_VALUE - MIN_VALUE); + super(x, y, Button.DEFAULT_WIDTH, Button.DEFAULT_HEIGHT, createMessage(entity), initialValue); + + this.entity = entity; + } + + private static MutableComponent createMessage(TextBlockEntity entity) { + return entity.viewDistance == INFINITE_VALUE + ? Component.translatable("gui.glowcase.render_distance_value.infinite") + : Component.translatable("gui.glowcase.render_distance_value", entity.viewDistance); + } + + @Override + protected void updateMessage() { + this.setMessage(createMessage(this.entity)); + } + + @Override + protected void applyValue() { + if (this.value == 1) { + this.entity.viewDistance = INFINITE_VALUE; + } else { + this.entity.viewDistance = (float) Math.round(Mth.lerp(this.value, MIN_VALUE, MAX_VALUE)); + } + this.entity.renderDirty = true; + } + } +} diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/widget/ingame/IconButtonWidget.java b/src/main/java/dev/hephaestus/glowcase/client/gui/widget/ingame/IconButtonWidget.java index 246519c0..40d8fbb5 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/gui/widget/ingame/IconButtonWidget.java +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/widget/ingame/IconButtonWidget.java @@ -31,11 +31,18 @@ public IconButtonWidget(int x, int y, int width, int height, int iconWidth, int @Override protected void extractContents(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) { + this.extractDefaultSprite(graphics); + Identifier drawnIcon = this.icon; if(this.hoverIcon != null && this.isMouseOver(mouseX, mouseY)) { drawnIcon = this.hoverIcon; } - graphics.blitSprite(RenderPipelines.GUI_TEXTURED, drawnIcon, this.getX(), this.getY(), this.iconWidth, this.iconHeight); + graphics.blitSprite(RenderPipelines.GUI_TEXTURED, + drawnIcon, + this.getX() + (this.width - this.iconWidth) / 2, + this.getY() + (this.height - this.iconHeight) / 2, + this.iconWidth, + this.iconHeight); } public void setPosition(int x, int y, int z, int size, int iconSize) { diff --git a/src/main/java/dev/hephaestus/glowcase/client/render/block/entity/TextBlockEntityRenderer.java b/src/main/java/dev/hephaestus/glowcase/client/render/block/entity/TextBlockEntityRenderer.java index ee20dc0c..4a774259 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/render/block/entity/TextBlockEntityRenderer.java +++ b/src/main/java/dev/hephaestus/glowcase/client/render/block/entity/TextBlockEntityRenderer.java @@ -7,28 +7,43 @@ import dev.hephaestus.glowcase.client.util.BlockEntityRenderUtil; import net.minecraft.client.gui.Font; import net.minecraft.client.renderer.SubmitNodeCollector; -import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; import net.minecraft.client.renderer.feature.ModelFeatureRenderer; import net.minecraft.client.renderer.state.level.CameraRenderState; import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; +import net.minecraft.util.FormattedCharSequence; import net.minecraft.util.LightCoordsUtil; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.phys.Vec3; -import org.joml.Matrix4f; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.List; + @NullMarked public class TextBlockEntityRenderer implements BakedBlockEntityRenderer { public static Identifier ITEM_TEXTURE = Glowcase.id("textures/item/text_block.png"); private boolean wasOutOfRange = false; + private final Font font; + + public TextBlockEntityRenderer(BlockEntityRendererProvider.Context ctx) { + this.font = ctx.font(); + } + public static class TextRenderState extends BlockEntityRenderState { public boolean shouldRenderPlaceholder; - public TextBlockEntity.ZOffset zOffset; public int rotation16; + public List lines = List.of(); + public TextBlockEntity.TextAlignment textAlignment; + public TextBlockEntity.HorizontalAlignment horizontalAlignment; + public TextBlockEntity.ZOffset zOffset; + public boolean shadow; + public float scale = 1; + public int color; + public int backgroundColor; } @Override @@ -36,7 +51,6 @@ public TextRenderState createRenderState() { return new TextRenderState(); } - @Override public TextRenderState createBakedRenderState() { return new TextRenderState(); @@ -46,8 +60,17 @@ public TextRenderState createBakedRenderState() { public void extractRenderState(TextBlockEntity blockEntity, TextRenderState state, float partialTicks, Vec3 cameraPosition, ModelFeatureRenderer.@Nullable CrumblingOverlay breakProgress) { BakedBlockEntityRenderer.super.extractRenderState(blockEntity, state, partialTicks, cameraPosition, breakProgress); state.shouldRenderPlaceholder = blockEntity.lines.stream().allMatch(t -> t.getString().isBlank()) || BlockEntityRenderUtil.shouldRenderPlaceholder(blockEntity.getBlockPos()); - state.zOffset = blockEntity.zOffset; + state.rotation16 = blockEntity.getBlockState().getValue(BlockStateProperties.ROTATION_16); + + state.lines = blockEntity.lines.stream().map(Component::getVisualOrderText).toList(); + state.textAlignment = blockEntity.textAlignment; + state.horizontalAlignment = blockEntity.horizontalAlignment; + state.zOffset = blockEntity.zOffset; + state.shadow = blockEntity.shadow; + state.scale = blockEntity.scale; + state.color = blockEntity.color; + state.backgroundColor = blockEntity.backgroundColor; } @Override @@ -57,6 +80,15 @@ public void extractBakingRenderState(TextBlockEntity blockEntity, TextRenderStat state.shouldRenderPlaceholder = blockEntity.lines.stream().allMatch(t -> t.getString().isBlank()) || BlockEntityRenderUtil.shouldRenderPlaceholder(blockEntity.getBlockPos()); state.zOffset = blockEntity.zOffset; state.rotation16 = blockEntity.getBlockState().getValue(BlockStateProperties.ROTATION_16); + + state.lines = blockEntity.lines.stream().map(Component::getVisualOrderText).toList(); + state.scale = blockEntity.scale; + } + + @Override + public boolean shouldBake(TextBlockEntity entity) { +// return !entity.lines.isEmpty(); + return false; } @Override @@ -64,23 +96,75 @@ public void submitForRendering(TextRenderState state, PoseStack poseStack, Submi if (state.shouldRenderPlaceholder) { BlockEntityRenderUtil.renderPlaceholderWithBlockRotation(state, state.rotation16, ITEM_TEXTURE, 1.0F, poseStack, submitNodeCollector, state.zOffset == TextBlockEntity.ZOffset.CENTER ? 0.01F : state.zOffset == TextBlockEntity.ZOffset.FRONT ? 0.4F : -0.4F); } - } - @Override - public void submitForBaking(TextRenderState state, PoseStack poseStack, SubmitNodeCollector submitNodeCollector) { - // TODO: Port to 26.1 + // TODO: move this to a baked rendering method + // currently it errors "Rendersystem called from wrong thread" on `this.font.width(text)` + int maxWidth = 0; + for (var line : state.lines) { + maxWidth = Math.max(maxWidth, this.font.width(line)); + } + poseStack.pushPose(); - poseStack.mulPose(Axis.YP.rotationDegrees(-(state.rotation16 * 360) / 16.0F)); - float a = 1 / 9f; - poseStack.translate(-.5, 1.5, -.5); - poseStack.scale(a, -a, a); - submitNodeCollector.submitText(poseStack, 0, 0, Component.literal("waff :3").getVisualOrderText(), true, Font.DisplayMode.NORMAL, LightCoordsUtil.FULL_BRIGHT, 0xFFFFFFFF, 0, 0); + poseStack.translate(0.5D, 0.5D, 0.5D); + // 2D rendering of the font has Y axis going down, not up + poseStack.scale(1, -1, 1); + + switch (state.zOffset) { + case FRONT -> poseStack.translate(0D, 0D, 0.4D); + case BACK -> poseStack.translate(0D, 0D, -0.4D); + } + + float rotation = -(state.rotation16 * 360) / 16.0F; + poseStack.mulPose(Axis.YP.rotationDegrees(rotation)); + + poseStack.scale(0.1F * state.scale, 0.1F * state.scale, 0.1F * state.scale); + poseStack.translate(0, -(state.lines.size() * this.font.lineHeight) / 2D, 0D); + + switch (state.horizontalAlignment) { + case LEFT -> poseStack.translate(-maxWidth / 2F, 0, 0); + case RIGHT -> poseStack.translate(maxWidth / 2F, 0, 0); + } + + for (int i = 0; i < state.lines.size(); ++i) { + var line = state.lines.get(i); + + int width = this.font.width(line); + if (width == 0) { + continue; + } + + float x = switch (state.textAlignment) { + case LEFT -> -maxWidth / 2F; + case CENTER -> (maxWidth - width) / 2F - maxWidth / 2F; + case CENTER_LEFT -> -(50F / state.scale) - (width / 2F); + case CENTER_RIGHT -> (50F / state.scale) - (width / 2F); + case RIGHT -> maxWidth - width - maxWidth / 2F; + }; + + submitNodeCollector.submitText(poseStack, + x, + i * this.font.lineHeight, + line, state.shadow, + Font.DisplayMode.NORMAL, + LightCoordsUtil.FULL_BRIGHT, + state.color, + state.backgroundColor, + 0); + } + poseStack.popPose(); } @Override - public boolean shouldBake(TextBlockEntity entity) { - return !entity.lines.isEmpty(); + public void submitForBaking(TextRenderState state, PoseStack poseStack, SubmitNodeCollector submitNodeCollector) { + // TODO: Port to 26.1 +// poseStack.pushPose(); +// poseStack.mulPose(Axis.YP.rotationDegrees(-(state.rotation16 * 360) / 16.0F)); +// float a = 1 / 9f; +// poseStack.translate(-.5, 1.5, -.5); +// poseStack.scale(a, -a, a); +// submitNodeCollector.submitText(poseStack, 0, 0, Component.literal("waff :3").getVisualOrderText(), true, Font.DisplayMode.NORMAL, LightCoordsUtil.FULL_BRIGHT, 0xFFFFFFFF, 0, 0); +// poseStack.popPose(); } // FIXME 26.1 diff --git a/src/main/java/dev/hephaestus/glowcase/packet/C2SEditTextBlock.java b/src/main/java/dev/hephaestus/glowcase/packet/C2SEditTextBlock.java index 79ea677b..c1c1f976 100644 --- a/src/main/java/dev/hephaestus/glowcase/packet/C2SEditTextBlock.java +++ b/src/main/java/dev/hephaestus/glowcase/packet/C2SEditTextBlock.java @@ -14,19 +14,23 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.block.entity.BlockEntity; -public record C2SEditTextBlock(BlockPos pos, TextBlockEntity.TextAlignment alignment, TextBlockEntity.ZOffset offset, +public record C2SEditTextBlock(BlockPos pos, + TextBlockEntity.TextAlignment alignment, + TextBlockEntity.HorizontalAlignment horizontalAlignment, + TextBlockEntity.ZOffset offset, TextBlockValues values) implements C2SEditBlockEntity { public static final Type ID = new Type<>(Glowcase.id("channel.text_block")); public static final StreamCodec PACKET_CODEC = StreamCodec.composite( BlockPos.STREAM_CODEC, C2SEditTextBlock::pos, ByteBufCodecs.BYTE.map(index -> TextBlockEntity.TextAlignment.values()[index], textAlignment -> (byte) textAlignment.ordinal()), C2SEditTextBlock::alignment, + TextBlockEntity.HorizontalAlignment.STREAM_CODEC, C2SEditTextBlock::horizontalAlignment, ByteBufCodecs.BYTE.map(index -> TextBlockEntity.ZOffset.values()[index], zOffset -> (byte) zOffset.ordinal()), C2SEditTextBlock::offset, TextBlockValues.PACKET_CODEC, C2SEditTextBlock::values, C2SEditTextBlock::new ); public static C2SEditTextBlock of(TextBlockEntity be) { - return new C2SEditTextBlock(be.getBlockPos(), be.textAlignment, be.zOffset, new TextBlockValues(be.shadow, be.scale, be.backgroundColor, be.color, be.lines, be.viewDistance)); + return new C2SEditTextBlock(be.getBlockPos(), be.textAlignment, be.horizontalAlignment, be.zOffset, new TextBlockValues(be.shadow, be.scale, be.backgroundColor, be.color, be.lines, be.viewDistance)); } @Override @@ -42,6 +46,7 @@ public void receive(ServerLevel world, BlockEntity blockEntity) { be.scale = this.values().scale(); be.lines = this.values().lines(); be.textAlignment = this.alignment(); + be.horizontalAlignment = this.horizontalAlignment(); be.backgroundColor = this.values().backgroundColor(); be.color = this.values().color(); be.zOffset = this.offset(); diff --git a/src/main/resources/assets/glowcase/lang/en_us.json b/src/main/resources/assets/glowcase/lang/en_us.json index eb0c4f5c..02ed87cf 100644 --- a/src/main/resources/assets/glowcase/lang/en_us.json +++ b/src/main/resources/assets/glowcase/lang/en_us.json @@ -171,5 +171,11 @@ "gui.glowcase.config_link.missing.modmenu": "Mod Menu isn't installed.", "gui.glowcase.config_link.missing.mod_screen": "No such mod screen: %s", "gui.glowcase.config_link.missing.mod": "No such mod: %s", - "gui.glowcase.config_link.missing.link": "Not implemented: %s" + "gui.glowcase.config_link.missing.link": "Not implemented: %s", + "gui.glowcase.text_options": "Text Block Properties", + "gui.glowcase.render_distance_value": "Render Distance: %d", + "gui.glowcase.render_distance_value.infinite": "Render Distance: Infinite", + "gui.glowcase.text_alignment": "Text Alignment", + "gui.glowcase.text_shadow": "Text Shadow", + "gui.glowcase.more": "More..." } diff --git a/src/main/resources/assets/glowcase/textures/gui/sprites/horizontal_alignment/center.png b/src/main/resources/assets/glowcase/textures/gui/sprites/horizontal_alignment/center.png new file mode 100644 index 00000000..fca3d3a2 Binary files /dev/null and b/src/main/resources/assets/glowcase/textures/gui/sprites/horizontal_alignment/center.png differ diff --git a/src/main/resources/assets/glowcase/textures/gui/sprites/horizontal_alignment/left.png b/src/main/resources/assets/glowcase/textures/gui/sprites/horizontal_alignment/left.png new file mode 100644 index 00000000..a8377a75 Binary files /dev/null and b/src/main/resources/assets/glowcase/textures/gui/sprites/horizontal_alignment/left.png differ diff --git a/src/main/resources/assets/glowcase/textures/gui/sprites/horizontal_alignment/right.png b/src/main/resources/assets/glowcase/textures/gui/sprites/horizontal_alignment/right.png new file mode 100644 index 00000000..c1ade239 Binary files /dev/null and b/src/main/resources/assets/glowcase/textures/gui/sprites/horizontal_alignment/right.png differ diff --git a/src/main/resources/assets/glowcase/textures/gui/sprites/text_alignment/center.png b/src/main/resources/assets/glowcase/textures/gui/sprites/text_alignment/center.png new file mode 100644 index 00000000..24eb8748 Binary files /dev/null and b/src/main/resources/assets/glowcase/textures/gui/sprites/text_alignment/center.png differ diff --git a/src/main/resources/assets/glowcase/textures/gui/sprites/text_alignment/left.png b/src/main/resources/assets/glowcase/textures/gui/sprites/text_alignment/left.png new file mode 100644 index 00000000..ae2e9c3f Binary files /dev/null and b/src/main/resources/assets/glowcase/textures/gui/sprites/text_alignment/left.png differ diff --git a/src/main/resources/assets/glowcase/textures/gui/sprites/text_alignment/right.png b/src/main/resources/assets/glowcase/textures/gui/sprites/text_alignment/right.png new file mode 100644 index 00000000..91f73123 Binary files /dev/null and b/src/main/resources/assets/glowcase/textures/gui/sprites/text_alignment/right.png differ diff --git a/src/main/resources/assets/glowcase/textures/gui/sprites/three_dots.png b/src/main/resources/assets/glowcase/textures/gui/sprites/three_dots.png new file mode 100644 index 00000000..bc8a81aa Binary files /dev/null and b/src/main/resources/assets/glowcase/textures/gui/sprites/three_dots.png differ