diff --git a/src/main/java/dev/hephaestus/glowcase/Glowcase.java b/src/main/java/dev/hephaestus/glowcase/Glowcase.java index 84476b4..286d3c2 100644 --- a/src/main/java/dev/hephaestus/glowcase/Glowcase.java +++ b/src/main/java/dev/hephaestus/glowcase/Glowcase.java @@ -4,9 +4,11 @@ import com.google.common.base.Suppliers; import dev.hephaestus.glowcase.block.HyperlinkBlock; import dev.hephaestus.glowcase.block.ItemDisplayBlock; +import dev.hephaestus.glowcase.block.PopupBlock; import dev.hephaestus.glowcase.block.TextBlock; import dev.hephaestus.glowcase.block.entity.HyperlinkBlockEntity; import dev.hephaestus.glowcase.block.entity.ItemDisplayBlockEntity; +import dev.hephaestus.glowcase.block.entity.PopupBlockEntity; import dev.hephaestus.glowcase.block.entity.TextBlockEntity; import dev.hephaestus.glowcase.compat.PolydexCompatibility; import net.fabricmc.api.ModInitializer; @@ -46,6 +48,10 @@ public class Glowcase implements ModInitializer { public static final Supplier TEXT_BLOCK_ITEM = registerItem("text_block", () -> new BlockItem(TEXT_BLOCK.get(), new Item.Settings())); public static final Supplier> TEXT_BLOCK_ENTITY = registerBlockEntity("text_block", () -> BlockEntityType.Builder.create(TextBlockEntity::new, TEXT_BLOCK.get()).build(null)); + public static final Supplier POPUP_BLOCK = registerBlock("popup_block", PopupBlock::new); + public static final Supplier POPUP_BLOCK_ITEM = registerItem("popup_block", () -> new BlockItem(POPUP_BLOCK.get(), new Item.Settings())); + public static final Supplier> POPUP_BLOCK_ENTITY = registerBlockEntity("popup_block", () -> BlockEntityType.Builder.create(PopupBlockEntity::new, POPUP_BLOCK.get()).build(null)); + public static final Supplier ITEM_GROUP = registerItemGroup("items", () -> FabricItemGroup.builder() .displayName(Text.translatable("itemGroup.glowcase.items")) .icon(() -> new ItemStack(Items.GLOWSTONE)) @@ -53,6 +59,7 @@ public class Glowcase implements ModInitializer { entries.add(HYPERLINK_BLOCK_ITEM.get()); entries.add(ITEM_DISPLAY_BLOCK_ITEM.get()); entries.add(TEXT_BLOCK_ITEM.get()); + entries.add(POPUP_BLOCK_ITEM.get()); }) .build() ); diff --git a/src/main/java/dev/hephaestus/glowcase/GlowcaseCommonProxy.java b/src/main/java/dev/hephaestus/glowcase/GlowcaseCommonProxy.java index 65ee6c0..bc436a6 100644 --- a/src/main/java/dev/hephaestus/glowcase/GlowcaseCommonProxy.java +++ b/src/main/java/dev/hephaestus/glowcase/GlowcaseCommonProxy.java @@ -18,4 +18,12 @@ public void openItemDisplayBlockEditScreen(BlockPos pos) { public void openTextBlockEditScreen(BlockPos pos) { //No-op } + + public void openPopupBlockEditScreen(BlockPos pos) { + //No-op + } + + public void openPopupBlockViewScreen(BlockPos pos) { + //No-op + } } diff --git a/src/main/java/dev/hephaestus/glowcase/GlowcaseNetworking.java b/src/main/java/dev/hephaestus/glowcase/GlowcaseNetworking.java index 8b932a4..6c7eab2 100644 --- a/src/main/java/dev/hephaestus/glowcase/GlowcaseNetworking.java +++ b/src/main/java/dev/hephaestus/glowcase/GlowcaseNetworking.java @@ -2,6 +2,7 @@ import dev.hephaestus.glowcase.packet.C2SEditHyperlinkBlock; import dev.hephaestus.glowcase.packet.C2SEditItemDisplayBlock; +import dev.hephaestus.glowcase.packet.C2SEditPopupBlock; import dev.hephaestus.glowcase.packet.C2SEditTextBlock; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; @@ -11,9 +12,11 @@ public static void init() { PayloadTypeRegistry.playC2S().register(C2SEditHyperlinkBlock.ID, C2SEditHyperlinkBlock.PACKET_CODEC); PayloadTypeRegistry.playC2S().register(C2SEditItemDisplayBlock.ID, C2SEditItemDisplayBlock.PACKET_CODEC); PayloadTypeRegistry.playC2S().register(C2SEditTextBlock.ID, C2SEditTextBlock.PACKET_CODEC); + PayloadTypeRegistry.playC2S().register(C2SEditPopupBlock.ID, C2SEditPopupBlock.PACKET_CODEC); ServerPlayNetworking.registerGlobalReceiver(C2SEditHyperlinkBlock.ID, C2SEditHyperlinkBlock::receive); ServerPlayNetworking.registerGlobalReceiver(C2SEditItemDisplayBlock.ID, C2SEditItemDisplayBlock::receive); ServerPlayNetworking.registerGlobalReceiver(C2SEditTextBlock.ID, C2SEditTextBlock::receive); + ServerPlayNetworking.registerGlobalReceiver(C2SEditPopupBlock.ID, C2SEditPopupBlock::receive); } } diff --git a/src/main/java/dev/hephaestus/glowcase/block/PopupBlock.java b/src/main/java/dev/hephaestus/glowcase/block/PopupBlock.java new file mode 100644 index 0000000..122084e --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/block/PopupBlock.java @@ -0,0 +1,85 @@ +package dev.hephaestus.glowcase.block; + +import dev.hephaestus.glowcase.Glowcase; +import dev.hephaestus.glowcase.block.entity.PopupBlockEntity; +import net.minecraft.block.BlockEntityProvider; +import net.minecraft.block.BlockState; +import net.minecraft.block.ShapeContext; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.NbtComponent; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Formatting; +import net.minecraft.util.Hand; +import net.minecraft.util.ItemActionResult; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.shape.VoxelShape; +import net.minecraft.util.shape.VoxelShapes; +import net.minecraft.world.BlockView; +import net.minecraft.world.World; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class PopupBlock extends GlowcaseBlock implements BlockEntityProvider { + private static final VoxelShape OUTLINE = VoxelShapes.cuboid(0.25, 0.25, 0.25, 0.75, 0.75, 0.75); + + @Override + public VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) { + return OUTLINE; + } + + @Nullable + @Override + public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { + return new PopupBlockEntity(pos, state); + } + + @Override + public void onPlaced(World world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) { + if (world.isClient && placer instanceof PlayerEntity player && canEditGlowcase(player, pos)) { + //load any ctrl-picked NBT clientside + NbtComponent blockEntityTag = stack.get(DataComponentTypes.BLOCK_ENTITY_DATA); + if (blockEntityTag != null && world.getBlockEntity(pos) instanceof PopupBlockEntity be) { + blockEntityTag.applyToBlockEntity(be, world.getRegistryManager()); + } + + Glowcase.proxy.openPopupBlockEditScreen(pos); + } + } + + @Override + protected ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, BlockHitResult hit) { + if (!(world.getBlockEntity(pos) instanceof PopupBlockEntity be)) return ActionResult.CONSUME; + if (world.isClient) { + Glowcase.proxy.openPopupBlockViewScreen(pos); + } + return ActionResult.SUCCESS; + } + + @Override + protected ItemActionResult onUseWithItem(ItemStack stack, BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { + if (!(world.getBlockEntity(pos) instanceof PopupBlockEntity)) return ItemActionResult.CONSUME; + if (player.getStackInHand(hand).isIn(Glowcase.ITEM_TAG) && canEditGlowcase(player, pos)) { + if (world.isClient) { + Glowcase.proxy.openPopupBlockEditScreen(pos); + } + return ItemActionResult.SUCCESS; + } + return ItemActionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; + } + + @Override + public void appendTooltip(ItemStack itemStack, Item.TooltipContext context, List tooltip, TooltipType options) { + tooltip.add(Text.translatable("block.glowcase.popup_block.tooltip.0").formatted(Formatting.GRAY)); + tooltip.add(Text.translatable("block.glowcase.generic.tooltip").formatted(Formatting.DARK_GRAY)); + tooltip.add(Text.translatable("block.glowcase.popup_block.tooltip.1").formatted(Formatting.DARK_GRAY)); + } +} diff --git a/src/main/java/dev/hephaestus/glowcase/block/entity/PopupBlockEntity.java b/src/main/java/dev/hephaestus/glowcase/block/entity/PopupBlockEntity.java new file mode 100644 index 0000000..c33d48c --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/block/entity/PopupBlockEntity.java @@ -0,0 +1,137 @@ +package dev.hephaestus.glowcase.block.entity; + +import dev.hephaestus.glowcase.Glowcase; +import dev.hephaestus.glowcase.client.render.block.entity.BakedBlockEntityRenderer; +import eu.pb4.placeholders.api.ParserContext; +import eu.pb4.placeholders.api.parsers.NodeParser; +import eu.pb4.placeholders.api.parsers.TagParser; +import net.minecraft.block.BlockState; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.nbt.NbtList; +import net.minecraft.nbt.NbtString; +import net.minecraft.network.listener.ClientPlayPacketListener; +import net.minecraft.network.packet.Packet; +import net.minecraft.network.packet.s2c.play.BlockEntityUpdateS2CPacket; +import net.minecraft.registry.RegistryWrapper; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.math.BlockPos; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class PopupBlockEntity extends BlockEntity { + public static final NodeParser PARSER = TagParser.DEFAULT; + public String title = ""; + public List lines = new ArrayList<>(); + public TextBlockEntity.TextAlignment textAlignment = TextBlockEntity.TextAlignment.CENTER; + public int color = 0xFFFFFF; + public boolean renderDirty = true; + + public PopupBlockEntity(BlockPos pos, BlockState state) { + super(Glowcase.POPUP_BLOCK_ENTITY.get(), pos, state); + lines.add(Text.empty()); + } + + @Override + protected void writeNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { + super.writeNbt(tag, registryLookup); + + tag.putString("title", this.title); + tag.putInt("color", this.color); + + tag.putString("text_alignment", this.textAlignment.name()); + + NbtList lines = tag.getList("lines", 8); + for (var text : this.lines) { + lines.add(NbtString.of(Text.Serialization.toJsonString(text, registryLookup))); + } + + tag.put("lines", lines); + } + + @Override + protected void readNbt(NbtCompound tag, RegistryWrapper.WrapperLookup registryLookup) { + super.readNbt(tag, registryLookup); + + this.title = tag.getString("title"); + this.lines = new ArrayList<>(); + this.color = tag.getInt("color"); + + this.textAlignment = TextBlockEntity.TextAlignment.valueOf(tag.getString("text_alignment")); + + NbtList lines = tag.getList("lines", 8); + + for (NbtElement line : lines) { + if (line.getType() == NbtElement.END_TYPE) break; + this.lines.add(Text.Serialization.fromJson(line.asString(), registryLookup)); + } + + this.renderDirty = true; + } + + public String getRawLine(int i) { + var line = this.lines.get(i); + + if (line.getStyle() == null) { + return line.getString(); + } + + var insert = line.getStyle().getInsertion(); + + if (insert == null) { + return line.getString(); + } + return insert; + } + + public void addRawLine(int i, String string) { + var parsed = PARSER.parseText(string, ParserContext.of()); + + if (parsed.getString().equals(string)) { + this.lines.add(i, Text.literal(string)); + } else { + this.lines.add(i, Text.empty().append(parsed).setStyle(Style.EMPTY.withInsertion(string))); + } + } + + public void setRawLine(int i, String string) { + var parsed = PARSER.parseText(string, ParserContext.of()); + + if (parsed.getString().equals(string)) { + this.lines.set(i, Text.literal(string)); + } else { + this.lines.set(i, Text.empty().append(parsed).setStyle(Style.EMPTY.withInsertion(string))); + } + } + + @SuppressWarnings({"MethodCallSideOnly", "VariableUseSideOnly"}) + @Override + public void markRemoved() { + if (world != null && world.isClient) { + BakedBlockEntityRenderer.Manager.markForRebuild(getPos()); + } + super.markRemoved(); + } + + // standard blockentity boilerplate + + public void dispatch() { + if (world instanceof ServerWorld sworld) sworld.getChunkManager().markForUpdate(pos); + } + + @Override + public NbtCompound toInitialChunkDataNbt(RegistryWrapper.WrapperLookup registryLookup) { + return createNbt(registryLookup); + } + + @Nullable + @Override + public Packet toUpdatePacket() { + return BlockEntityUpdateS2CPacket.create(this); + } +} diff --git a/src/main/java/dev/hephaestus/glowcase/client/GlowcaseClient.java b/src/main/java/dev/hephaestus/glowcase/client/GlowcaseClient.java index 02b4a8e..9332af1 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/GlowcaseClient.java +++ b/src/main/java/dev/hephaestus/glowcase/client/GlowcaseClient.java @@ -5,6 +5,7 @@ import dev.hephaestus.glowcase.client.render.block.entity.HyperlinkBlockEntityRenderer; import dev.hephaestus.glowcase.client.render.block.entity.ItemDisplayBlockEntityRenderer; import dev.hephaestus.glowcase.client.render.block.entity.TextBlockEntityRenderer; +import dev.hephaestus.glowcase.client.render.block.entity.PopupBlockEntityRenderer; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.rendering.v1.InvalidateRenderStateCallback; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; @@ -18,6 +19,7 @@ public void onInitializeClient() { BlockEntityRendererFactories.register(Glowcase.TEXT_BLOCK_ENTITY.get(), TextBlockEntityRenderer::new); BlockEntityRendererFactories.register(Glowcase.HYPERLINK_BLOCK_ENTITY.get(), HyperlinkBlockEntityRenderer::new); BlockEntityRendererFactories.register(Glowcase.ITEM_DISPLAY_BLOCK_ENTITY.get(), ItemDisplayBlockEntityRenderer::new); + BlockEntityRendererFactories.register(Glowcase.POPUP_BLOCK_ENTITY.get(), PopupBlockEntityRenderer::new); WorldRenderEvents.AFTER_TRANSLUCENT.register(BakedBlockEntityRenderer.Manager::render); InvalidateRenderStateCallback.EVENT.register(BakedBlockEntityRenderer.Manager::reset); diff --git a/src/main/java/dev/hephaestus/glowcase/client/GlowcaseClientProxy.java b/src/main/java/dev/hephaestus/glowcase/client/GlowcaseClientProxy.java index 2cf548d..1157df2 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/GlowcaseClientProxy.java +++ b/src/main/java/dev/hephaestus/glowcase/client/GlowcaseClientProxy.java @@ -3,10 +3,9 @@ import dev.hephaestus.glowcase.GlowcaseCommonProxy; import dev.hephaestus.glowcase.block.entity.HyperlinkBlockEntity; import dev.hephaestus.glowcase.block.entity.ItemDisplayBlockEntity; +import dev.hephaestus.glowcase.block.entity.PopupBlockEntity; import dev.hephaestus.glowcase.block.entity.TextBlockEntity; -import dev.hephaestus.glowcase.client.gui.screen.ingame.HyperlinkBlockEditScreen; -import dev.hephaestus.glowcase.client.gui.screen.ingame.ItemDisplayBlockEditScreen; -import dev.hephaestus.glowcase.client.gui.screen.ingame.TextBlockEditScreen; +import dev.hephaestus.glowcase.client.gui.screen.ingame.*; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.screen.ConfirmLinkScreen; import net.minecraft.util.math.BlockPos; @@ -40,4 +39,20 @@ public void openTextBlockEditScreen(BlockPos pos) { MinecraftClient.getInstance().setScreen(new TextBlockEditScreen(be)); } } + + @Override + public void openPopupBlockEditScreen(BlockPos pos) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.world != null && client.world.getBlockEntity(pos) instanceof PopupBlockEntity be) { + MinecraftClient.getInstance().setScreen(new PopupBlockEditScreen(be)); + } + } + + @Override + public void openPopupBlockViewScreen(BlockPos pos) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.world != null && client.world.getBlockEntity(pos) instanceof PopupBlockEntity be) { + MinecraftClient.getInstance().setScreen(new PopupBlockViewScreen(be)); + } + } } diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/PopupBlockEditScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/PopupBlockEditScreen.java new file mode 100644 index 0000000..ce8469f --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/PopupBlockEditScreen.java @@ -0,0 +1,329 @@ +package dev.hephaestus.glowcase.client.gui.screen.ingame; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import dev.hephaestus.glowcase.block.entity.HyperlinkBlockEntity; +import dev.hephaestus.glowcase.block.entity.PopupBlockEntity; +import dev.hephaestus.glowcase.block.entity.TextBlockEntity; +import dev.hephaestus.glowcase.packet.C2SEditPopupBlock; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.render.*; +import net.minecraft.client.util.SelectionManager; +import net.minecraft.text.Text; +import net.minecraft.text.TextColor; +import net.minecraft.util.math.MathHelper; +import org.lwjgl.glfw.GLFW; + +//TODO: multi-character selection at some point? it may be a bit complex but it'd be nice +public class PopupBlockEditScreen extends GlowcaseScreen { + private final PopupBlockEntity popupBlockEntity; + + private SelectionManager selectionManager; + private int currentRow; + private long ticksSinceOpened = 0; + private TextFieldWidget titleEntryWidget; + private ButtonWidget changeAlignment; + private TextFieldWidget colorEntryWidget; + + public PopupBlockEditScreen(PopupBlockEntity popupBlockEntity) { + this.popupBlockEntity = popupBlockEntity; + } + + @Override + public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) { + renderDarkening(context); + } + + @Override + public void init() { + super.init(); + + int innerPadding = width / 100; + + this.selectionManager = new SelectionManager( + () -> this.popupBlockEntity.getRawLine(this.currentRow), + (string) -> { + popupBlockEntity.setRawLine(this.currentRow, string); + this.popupBlockEntity.renderDirty = true; + }, + SelectionManager.makeClipboardGetter(this.client), + SelectionManager.makeClipboardSetter(this.client), + (string) -> true); + + this.titleEntryWidget = new TextFieldWidget(this.client.textRenderer, width / 10, 0, 8 * width / 10, 20, Text.empty()); + this.titleEntryWidget.setMaxLength(HyperlinkBlockEntity.TITLE_MAX_LENGTH); + this.titleEntryWidget.setText(this.popupBlockEntity.title); + this.titleEntryWidget.setPlaceholder(Text.translatable("gui.glowcase.title")); + this.titleEntryWidget.setChangedListener(string -> { + this.popupBlockEntity.title = this.titleEntryWidget.getText(); + this.popupBlockEntity.renderDirty = true; + }); + + this.changeAlignment = ButtonWidget.builder(Text.stringifiedTranslatable("gui.glowcase.alignment", this.popupBlockEntity.textAlignment), action -> { + switch (popupBlockEntity.textAlignment) { + case LEFT -> popupBlockEntity.textAlignment = TextBlockEntity.TextAlignment.CENTER; + case CENTER -> popupBlockEntity.textAlignment = TextBlockEntity.TextAlignment.RIGHT; + case RIGHT -> popupBlockEntity.textAlignment = TextBlockEntity.TextAlignment.LEFT; + } + this.popupBlockEntity.renderDirty = true; + + this.changeAlignment.setMessage(Text.stringifiedTranslatable("gui.glowcase.alignment", this.popupBlockEntity.textAlignment)); + }).dimensions(120 + innerPadding, 20 + innerPadding, 160, 20).build(); + + this.colorEntryWidget = new TextFieldWidget(this.client.textRenderer, 280 + innerPadding * 2, 20 + innerPadding, 50, 20, Text.empty()); + this.colorEntryWidget.setText("#" + Integer.toHexString(this.popupBlockEntity.color & 0x00FFFFFF)); + this.colorEntryWidget.setChangedListener(string -> { + TextColor.parse(this.colorEntryWidget.getText()).ifSuccess(color -> { + this.popupBlockEntity.color = color == null ? 0xFFFFFFFF : color.getRgb() | 0xFF000000; + this.popupBlockEntity.renderDirty = true; + }); + }); + + this.addDrawableChild(this.titleEntryWidget); + this.addDrawableChild(this.changeAlignment); + this.addDrawableChild(this.colorEntryWidget); + } + + @Override + public void tick() { + ++this.ticksSinceOpened; + } + + @Override + public void close() { + C2SEditPopupBlock.of(popupBlockEntity).send(); + super.close(); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + if (this.client != null) { + super.render(context, mouseX, mouseY, delta); + + context.getMatrices().push(); + context.getMatrices().translate(0, 40 + 2 * this.width / 100F, 0); + for (int i = 0; i < this.popupBlockEntity.lines.size(); ++i) { + var text = this.currentRow == i ? Text.literal(this.popupBlockEntity.getRawLine(i)) : this.popupBlockEntity.lines.get(i); + + int lineWidth = this.textRenderer.getWidth(text); + switch (this.popupBlockEntity.textAlignment) { + case LEFT -> context.drawTextWithShadow(client.textRenderer, text, this.width / 10, i * 12, this.popupBlockEntity.color); + case CENTER -> context.drawTextWithShadow(client.textRenderer, text, this.width / 2 - lineWidth / 2, i * 12, this.popupBlockEntity.color); + case RIGHT -> context.drawTextWithShadow(client.textRenderer, text, this.width - this.width / 10 - lineWidth, i * 12, this.popupBlockEntity.color); + } + } + + int caretStart = this.selectionManager.getSelectionStart(); + int caretEnd = this.selectionManager.getSelectionEnd(); + + if (caretStart >= 0) { + String line = this.popupBlockEntity.getRawLine(this.currentRow); + int selectionStart = MathHelper.clamp(Math.min(caretStart, caretEnd), 0, line.length()); + int selectionEnd = MathHelper.clamp(Math.max(caretStart, caretEnd), 0, line.length()); + + String preSelection = line.substring(0, MathHelper.clamp(line.length(), 0, selectionStart)); + int startX = this.client.textRenderer.getWidth(preSelection); + + float push = switch (this.popupBlockEntity.textAlignment) { + case LEFT -> this.width / 10F; + case CENTER -> this.width / 2F - this.textRenderer.getWidth(line) / 2F; + case RIGHT -> this.width - this.width / 10F - this.textRenderer.getWidth(line); + }; + + startX += (int) push; + + + int caretStartY = this.currentRow * 12; + int caretEndY = this.currentRow * 12 + 9; + if (this.ticksSinceOpened / 6 % 2 == 0 && !this.titleEntryWidget.isActive() && !this.colorEntryWidget.isActive()) { + if (selectionStart < line.length()) { + context.fill(startX, caretStartY, startX + 1, caretEndY, 0xCCFFFFFF); + } else { + context.drawText(client.textRenderer, "_", startX, this.currentRow * 12, 0xFFFFFFFF, false); + } + } + + if (caretStart != caretEnd) { + int endX = startX + this.client.textRenderer.getWidth(line.substring(selectionStart, selectionEnd)); + Tessellator tessellator = Tessellator.getInstance(); + BufferBuilder bufferBuilder = tessellator.begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_COLOR); + RenderSystem.enableColorLogicOp(); + RenderSystem.logicOp(GlStateManager.LogicOp.OR_REVERSE); + bufferBuilder.vertex(context.getMatrices().peek().getPositionMatrix(), startX, caretEndY, 0.0F).color(0, 0, 255, 255); + bufferBuilder.vertex(context.getMatrices().peek().getPositionMatrix(), endX, caretEndY, 0.0F).color(0, 0, 255, 255); + bufferBuilder.vertex(context.getMatrices().peek().getPositionMatrix(), endX, caretStartY, 0.0F).color(0, 0, 255, 255); + bufferBuilder.vertex(context.getMatrices().peek().getPositionMatrix(), startX, caretStartY, 0.0F).color(0, 0, 255, 255); + BufferRenderer.drawWithGlobalProgram(bufferBuilder.end()); + RenderSystem.disableColorLogicOp(); + } + } + + context.getMatrices().pop(); + } + } + + @Override + public boolean charTyped(char chr, int keyCode) { + if (this.titleEntryWidget.isActive()) { + return this.titleEntryWidget.charTyped(chr, keyCode); + } else if (this.colorEntryWidget.isActive()) { + return this.colorEntryWidget.charTyped(chr, keyCode); + } else { + this.selectionManager.insert(chr); + return true; + } + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (this.titleEntryWidget.isActive()) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + this.close(); + return true; + } else { + return this.titleEntryWidget.keyPressed(keyCode, scanCode, modifiers); + } + } else if (this.colorEntryWidget.isActive()) { + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + this.close(); + return true; + } else { + return this.colorEntryWidget.keyPressed(keyCode, scanCode, modifiers); + } + } else { + setFocused(null); + if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { + this.popupBlockEntity.addRawLine(this.currentRow + 1, + this.popupBlockEntity.getRawLine(this.currentRow).substring( + MathHelper.clamp(this.selectionManager.getSelectionStart(), 0, this.popupBlockEntity.getRawLine(this.currentRow).length()) + )); + this.popupBlockEntity.setRawLine(this.currentRow, + this.popupBlockEntity.getRawLine(this.currentRow).substring(0, MathHelper.clamp(this.selectionManager.getSelectionStart(), 0, this.popupBlockEntity.getRawLine(this.currentRow).length()) + )); + this.popupBlockEntity.renderDirty = true; + ++this.currentRow; + this.selectionManager.moveCursorToStart(); + return true; + } else if (keyCode == GLFW.GLFW_KEY_UP) { + this.currentRow = Math.max(this.currentRow - 1, 0); + this.selectionManager.putCursorAtEnd(); + return true; + } else if (keyCode == GLFW.GLFW_KEY_DOWN) { + this.currentRow = Math.min(this.currentRow + 1, (this.popupBlockEntity.lines.size() - 1)); + this.selectionManager.putCursorAtEnd(); + return true; + } else if (keyCode == GLFW.GLFW_KEY_BACKSPACE && this.currentRow > 0 && this.popupBlockEntity.lines.size() > 1 && this.selectionManager.getSelectionStart() == 0 && this.selectionManager.getSelectionEnd() == this.selectionManager.getSelectionStart()) { + --this.currentRow; + this.selectionManager.putCursorAtEnd(); + deleteLine(); + return true; + } else if (keyCode == GLFW.GLFW_KEY_DELETE && this.currentRow < this.popupBlockEntity.lines.size() - 1 && this.selectionManager.getSelectionEnd() == this.popupBlockEntity.getRawLine(this.currentRow).length()) { + deleteLine(); + return true; + } else { + try { + boolean val = this.selectionManager.handleSpecialKey(keyCode) || super.keyPressed(keyCode, scanCode, modifiers); + int selectionOffset = this.popupBlockEntity.getRawLine(this.currentRow).length() - this.selectionManager.getSelectionStart(); + + // Find line feed characters and create proper newlines + for (int i = 0; i < this.popupBlockEntity.lines.size(); ++i) { + int lineFeedIndex = this.popupBlockEntity.getRawLine(i).indexOf("\n"); + + if (lineFeedIndex >= 0) { + this.popupBlockEntity.addRawLine(i + 1, + this.popupBlockEntity.getRawLine(i).substring( + MathHelper.clamp(lineFeedIndex + 1, 0, this.popupBlockEntity.getRawLine(i).length()) + )); + this.popupBlockEntity.setRawLine(i, + this.popupBlockEntity.getRawLine(i).substring(0, MathHelper.clamp(lineFeedIndex, 0, this.popupBlockEntity.getRawLine(i).length()) + )); + this.popupBlockEntity.renderDirty = true; + ++this.currentRow; + this.selectionManager.putCursorAtEnd(); + this.selectionManager.moveCursor(-selectionOffset); + } + } + return val; + } catch (StringIndexOutOfBoundsException e) { + e.printStackTrace(); + MinecraftClient.getInstance().setScreen(null); + return false; + } + } + } + } + + private void deleteLine() { + this.popupBlockEntity.setRawLine(this.currentRow, + this.popupBlockEntity.getRawLine(this.currentRow) + this.popupBlockEntity.getRawLine(this.currentRow + 1) + ); + + this.popupBlockEntity.lines.remove(this.currentRow + 1); + this.popupBlockEntity.renderDirty = true; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + int topOffset = (int) (40 + 2 * this.width / 100F); + if (!this.titleEntryWidget.mouseClicked(mouseX, mouseY, button)) { + this.titleEntryWidget.setFocused(false); + } + if (!this.colorEntryWidget.mouseClicked(mouseX, mouseY, button)) { + this.colorEntryWidget.setFocused(false); + } + if (mouseY > topOffset) { + this.currentRow = MathHelper.clamp((int) (mouseY - topOffset) / 12, 0, this.popupBlockEntity.lines.size() - 1); + this.setFocused(null); + String baseContents = this.popupBlockEntity.getRawLine(currentRow); + int baseContentsWidth = this.textRenderer.getWidth(baseContents); + int contentsStart; + int contentsEnd; + switch (this.popupBlockEntity.textAlignment) { + case LEFT -> { + contentsStart = this.width / 10; + contentsEnd = contentsStart + baseContentsWidth; + } + case CENTER -> { + int midpoint = this.width / 2; + int textMidpoint = baseContentsWidth / 2; + contentsStart = midpoint - textMidpoint; + contentsEnd = midpoint + textMidpoint; + } + case RIGHT -> { + contentsEnd = this.width - this.width / 10; + contentsStart = contentsEnd - baseContentsWidth; + } + //even though this is exhaustive, javac won't treat contentsStart and contentsEnd as initialized + //why? who knows! just throw bc this should be impossible + default -> throw new IllegalStateException(":HOW:"); + } + + if (mouseX <= contentsStart) { + this.selectionManager.moveCursorToStart(); + } else if (mouseX >= contentsEnd) { + this.selectionManager.putCursorAtEnd(); + } else { + int lastWidth = 0; + for (int i = 1; i < baseContents.length(); i++) { + String testContents = baseContents.substring(0, i); + int width = this.textRenderer.getWidth(testContents); + int midpointWidth = (width + lastWidth) / 2; + if (mouseX < contentsStart + midpointWidth) { + this.selectionManager.moveCursorTo(i - 1, false); + break; + } else if (mouseX <= contentsStart + width) { + this.selectionManager.moveCursorTo(i, false); + break; + } + lastWidth = width; + } + } + return true; + } else { + return super.mouseClicked(mouseX, mouseY, button); + } + } +} diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/PopupBlockViewScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/PopupBlockViewScreen.java new file mode 100644 index 0000000..9ad6fe5 --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/PopupBlockViewScreen.java @@ -0,0 +1,52 @@ +package dev.hephaestus.glowcase.client.gui.screen.ingame; + +import com.mojang.blaze3d.platform.GlStateManager; +import com.mojang.blaze3d.systems.RenderSystem; +import dev.hephaestus.glowcase.block.entity.PopupBlockEntity; +import dev.hephaestus.glowcase.block.entity.TextBlockEntity; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.TextFieldWidget; +import net.minecraft.client.render.*; +import net.minecraft.client.util.SelectionManager; +import net.minecraft.text.Text; +import net.minecraft.text.TextColor; +import net.minecraft.util.math.MathHelper; +import org.lwjgl.glfw.GLFW; + +//TODO: multi-character selection at some point? it may be a bit complex but it'd be nice +public class PopupBlockViewScreen extends GlowcaseScreen { + private final PopupBlockEntity popupBlockEntity; + + public PopupBlockViewScreen(PopupBlockEntity popupBlockEntity) { + this.popupBlockEntity = popupBlockEntity; + } + + @Override + public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) { + renderDarkening(context); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + if (this.client != null) { + super.render(context, mouseX, mouseY, delta); + + context.getMatrices().push(); + context.getMatrices().translate(0, 40 + 2 * this.width / 100F, 0); + for (int i = 0; i < this.popupBlockEntity.lines.size(); ++i) { + var text = this.popupBlockEntity.lines.get(i); + + int lineWidth = this.textRenderer.getWidth(text); + switch (this.popupBlockEntity.textAlignment) { + case LEFT -> context.drawTextWithShadow(client.textRenderer, text, this.width / 10, i * 12, this.popupBlockEntity.color); + case CENTER -> context.drawTextWithShadow(client.textRenderer, text, this.width / 2 - lineWidth / 2, i * 12, this.popupBlockEntity.color); + case RIGHT -> context.drawTextWithShadow(client.textRenderer, text, this.width - this.width / 10 - lineWidth, i * 12, this.popupBlockEntity.color); + } + } + + context.getMatrices().pop(); + } + } +} diff --git a/src/main/java/dev/hephaestus/glowcase/client/render/block/entity/PopupBlockEntityRenderer.java b/src/main/java/dev/hephaestus/glowcase/client/render/block/entity/PopupBlockEntityRenderer.java new file mode 100644 index 0000000..1c0ffa3 --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/client/render/block/entity/PopupBlockEntityRenderer.java @@ -0,0 +1,47 @@ +package dev.hephaestus.glowcase.client.render.block.entity; + +import dev.hephaestus.glowcase.Glowcase; +import dev.hephaestus.glowcase.block.entity.PopupBlockEntity; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer.TextLayerType; +import net.minecraft.client.render.Camera; +import net.minecraft.client.render.LightmapTextureManager; +import net.minecraft.client.render.OverlayTexture; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.block.entity.BlockEntityRenderer; +import net.minecraft.client.render.block.entity.BlockEntityRendererFactory; +import net.minecraft.client.render.model.json.ModelTransformationMode; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.item.ItemStack; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.RotationAxis; + +public record PopupBlockEntityRenderer(BlockEntityRendererFactory.Context context) implements BlockEntityRenderer { + private static final MinecraftClient mc = MinecraftClient.getInstance(); + + public static final ItemStack STACK = new ItemStack(Glowcase.POPUP_BLOCK.get()); + + public void render(PopupBlockEntity entity, float f, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light, int overlay) { + Camera camera = context.getRenderDispatcher().camera; + matrices.push(); + matrices.translate(0.5D, 0.5D, 0.5D); + matrices.scale(0.5F, 0.5F, 0.5F); + float n = -camera.getYaw(); + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(n)); + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(camera.getPitch())); + context.getItemRenderer().renderItem(STACK, ModelTransformationMode.FIXED, light, OverlayTexture.DEFAULT_UV, matrices, vertexConsumers, entity.getWorld(), 0); + + HitResult hitResult = mc.crosshairTarget; + if (hitResult instanceof BlockHitResult && ((BlockHitResult) hitResult).getBlockPos().equals(entity.getPos())) { + float scale = 0.025F; + matrices.scale(scale, scale, scale); + matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(180)); + matrices.translate(-context.getTextRenderer().getWidth(entity.title) / 2F, -4, -scale); + // Fixes shadow being rendered in front of actual text + matrices.scale(1, 1, -1); + context.getTextRenderer().draw(entity.title, 0, 0, 0xFFFFFF, true, matrices.peek().getPositionMatrix(), vertexConsumers, TextLayerType.NORMAL, 0, LightmapTextureManager.MAX_LIGHT_COORDINATE); + } + matrices.pop(); + } +} diff --git a/src/main/java/dev/hephaestus/glowcase/packet/C2SEditPopupBlock.java b/src/main/java/dev/hephaestus/glowcase/packet/C2SEditPopupBlock.java new file mode 100644 index 0000000..7732cbc --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/packet/C2SEditPopupBlock.java @@ -0,0 +1,54 @@ +package dev.hephaestus.glowcase.packet; + +import dev.hephaestus.glowcase.Glowcase; +import dev.hephaestus.glowcase.block.entity.HyperlinkBlockEntity; +import dev.hephaestus.glowcase.block.entity.PopupBlockEntity; +import dev.hephaestus.glowcase.block.entity.TextBlockEntity; +import net.minecraft.block.entity.BlockEntity; +import net.minecraft.network.RegistryByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.text.Text; +import net.minecraft.text.TextCodecs; +import net.minecraft.util.math.BlockPos; + +import java.util.ArrayList; +import java.util.List; + +public record C2SEditPopupBlock(BlockPos pos, String title, List lines, TextBlockEntity.TextAlignment alignment, int color) implements C2SEditBlockEntity { + public static final Id ID = new Id<>(Glowcase.id("channel.popup_block")); + public static final PacketCodec PACKET_CODEC = PacketCodec.tuple( + BlockPos.PACKET_CODEC, C2SEditPopupBlock::pos, + PacketCodecs.STRING, C2SEditPopupBlock::title, + PacketCodecs.collection(ArrayList::new, TextCodecs.REGISTRY_PACKET_CODEC), C2SEditPopupBlock::lines, + PacketCodecs.BYTE.xmap(index -> TextBlockEntity.TextAlignment.values()[index], textAlignment -> (byte) textAlignment.ordinal()), C2SEditPopupBlock::alignment, + PacketCodecs.INTEGER, C2SEditPopupBlock::color, + C2SEditPopupBlock::new + ); + + public static C2SEditPopupBlock of(PopupBlockEntity be) { + return new C2SEditPopupBlock(be.getPos(), be.title, be.lines, be.textAlignment, be.color); + } + + @Override + public Id getId() { + return ID; + } + + @Override + public void receive(ServerWorld world, BlockEntity blockEntity) { + if (!(blockEntity instanceof PopupBlockEntity be)) return; + + if (this.title().length() <= HyperlinkBlockEntity.TITLE_MAX_LENGTH) { + be.title = this.title(); + } + be.lines = this.lines(); + be.textAlignment = this.alignment(); + be.color = this.color(); + + be.markDirty(); + be.dispatch(); + } +} diff --git a/src/main/resources/assets/glowcase/blockstates/popup_block.json b/src/main/resources/assets/glowcase/blockstates/popup_block.json new file mode 100644 index 0000000..0858580 --- /dev/null +++ b/src/main/resources/assets/glowcase/blockstates/popup_block.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "glowcase:block/popup_block" + } + } +} diff --git a/src/main/resources/assets/glowcase/lang/en_us.json b/src/main/resources/assets/glowcase/lang/en_us.json index 8f81d17..10fbdae 100644 --- a/src/main/resources/assets/glowcase/lang/en_us.json +++ b/src/main/resources/assets/glowcase/lang/en_us.json @@ -3,6 +3,7 @@ "block.glowcase.text_block": "Text Block", "block.glowcase.hyperlink_block": "Hyperlink Block", "block.glowcase.item_display_block": "Item Display Block", + "block.glowcase.popup_block": "Popup Block", "gui.glowcase.scale": "Scale: %d", "gui.glowcase.alignment": "Alignment: %s", "gui.glowcase.gives_item": "Gives Item: %s", @@ -21,5 +22,7 @@ "block.glowcase.hyperlink_block.tooltip.0": "Opens a URL when interacted", "block.glowcase.item_display_block.tooltip.0": "Gives an item stack when interacted", "block.glowcase.item_display_block.tooltip.1": "Interact with an item stack to set it", - "block.glowcase.item_display_block.tooltip.2": "Interact with the same item to edit" + "block.glowcase.item_display_block.tooltip.2": "Interact with the same item to edit", + "block.glowcase.popup_block.tooltip.0": "Displays formatted text when interacted", + "block.glowcase.popup_block.tooltip.1": "Supports Placeholder QuickText" } diff --git a/src/main/resources/assets/glowcase/models/block/popup_block.json b/src/main/resources/assets/glowcase/models/block/popup_block.json new file mode 100644 index 0000000..49f3a0b --- /dev/null +++ b/src/main/resources/assets/glowcase/models/block/popup_block.json @@ -0,0 +1,5 @@ +{ + "textures": { + "particle": "glowcase:item/popup_block" + } +} diff --git a/src/main/resources/assets/glowcase/models/item/popup_block.json b/src/main/resources/assets/glowcase/models/item/popup_block.json new file mode 100644 index 0000000..898e040 --- /dev/null +++ b/src/main/resources/assets/glowcase/models/item/popup_block.json @@ -0,0 +1,49 @@ +{ + "textures": { + "0": "glowcase:item/popup_block", + "particle": "glowcase:item/popup_block" + }, + "elements": [ + { + "from": [ + 2, + 2, + 8 + ], + "to": [ + 14, + 14, + 8 + ], + "rotation": { + "angle": 0, + "axis": "y", + "origin": [ + 8, + 8, + 16 + ] + }, + "faces": { + "north": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": "#0" + }, + "south": { + "uv": [ + 0, + 0, + 16, + 16 + ], + "texture": "#0" + } + } + } + ] +} diff --git a/src/main/resources/assets/glowcase/textures/item/popup_block.png b/src/main/resources/assets/glowcase/textures/item/popup_block.png new file mode 100644 index 0000000..9a17e05 Binary files /dev/null and b/src/main/resources/assets/glowcase/textures/item/popup_block.png differ diff --git a/src/main/resources/data/glowcase/tags/item/items.json b/src/main/resources/data/glowcase/tags/item/items.json index 26dc5c1..8d88222 100644 --- a/src/main/resources/data/glowcase/tags/item/items.json +++ b/src/main/resources/data/glowcase/tags/item/items.json @@ -3,6 +3,7 @@ "values": [ "glowcase:hyperlink_block", "glowcase:item_display_block", - "glowcase:text_block" + "glowcase:text_block", + "glowcase:popup_block" ] }