From 9e7df68547c1dbc2af8d4736ae65c0ec050dae17 Mon Sep 17 00:00:00 2001 From: screret <68943070+screret@users.noreply.github.com> Date: Sun, 29 Dec 2024 15:57:24 +0200 Subject: [PATCH 001/286] start on modifiable armor: equipment foundry. ...the modifier slots don't work with shiftclick right rn --- .../api/item/armor/ArmorComponentItem.java | 2 + .../common/block/EquipmentFoundryBlock.java | 44 +++++++++ .../EquipmentFoundryBlockEntity.java | 93 +++++++++++++++++++ .../gtceu/common/data/GTBlockEntities.java | 5 + .../gtceu/common/data/GTBlocks.java | 6 ++ .../gtceu/common/data/GTItems.java | 42 +++------ .../widget/EquipmentFoundryBaseWidget.java | 47 ++++++++++ .../gtceu/data/recipe/CustomTags.java | 5 +- 8 files changed, 213 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/gregtechceu/gtceu/common/block/EquipmentFoundryBlock.java create mode 100644 src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java create mode 100644 src/main/java/com/gregtechceu/gtceu/common/gui/widget/EquipmentFoundryBaseWidget.java diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorComponentItem.java b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorComponentItem.java index 199ae4c9675..407a2e6fd58 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorComponentItem.java +++ b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorComponentItem.java @@ -37,6 +37,8 @@ public class ArmorComponentItem extends ArmorItem implements IComponentItem { + @Getter + private int maxModifiers = 1; @Getter private IArmorLogic armorLogic = new DummyArmorLogic(); @Getter diff --git a/src/main/java/com/gregtechceu/gtceu/common/block/EquipmentFoundryBlock.java b/src/main/java/com/gregtechceu/gtceu/common/block/EquipmentFoundryBlock.java new file mode 100644 index 00000000000..dd776a9fc31 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/common/block/EquipmentFoundryBlock.java @@ -0,0 +1,44 @@ +package com.gregtechceu.gtceu.common.block; + +import com.gregtechceu.gtceu.common.blockentity.EquipmentFoundryBlockEntity; +import com.gregtechceu.gtceu.common.data.GTBlockEntities; +import com.lowdragmc.lowdraglib.gui.factory.BlockEntityUIFactory; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.annotation.ParametersAreNonnullByDefault; + +@SuppressWarnings("deprecation") +@ParametersAreNonnullByDefault +public class EquipmentFoundryBlock extends BaseEntityBlock { + + public EquipmentFoundryBlock(Properties properties) { + super(properties); + } + + @Override + public @Nullable BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return GTBlockEntities.EQUIPMENT_FOUNDRY.get().create(pos, state); + } + + @Override + public @NotNull InteractionResult use(BlockState state, Level level, BlockPos pos, + Player player, InteractionHand hand, BlockHitResult hit) { + if (player instanceof ServerPlayer serverPlayer && + level.getBlockEntity(pos) instanceof EquipmentFoundryBlockEntity equipmentFoundry) { + BlockEntityUIFactory.INSTANCE.openUI(equipmentFoundry, serverPlayer); + } + return InteractionResult.sidedSuccess(level.isClientSide); + } + +} diff --git a/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java b/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java new file mode 100644 index 00000000000..12606845438 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java @@ -0,0 +1,93 @@ +package com.gregtechceu.gtceu.common.blockentity; + +import com.gregtechceu.gtceu.api.gui.GuiTextures; +import com.gregtechceu.gtceu.api.gui.UITemplate; +import com.gregtechceu.gtceu.api.item.armor.ArmorComponentItem; +import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler; +import com.gregtechceu.gtceu.common.gui.widget.EquipmentFoundryBaseWidget; +import com.gregtechceu.gtceu.data.recipe.CustomTags; +import com.lowdragmc.lowdraglib.gui.modular.IUIHolder; +import com.lowdragmc.lowdraglib.gui.modular.ModularUI; +import com.lowdragmc.lowdraglib.gui.widget.LabelWidget; +import com.lowdragmc.lowdraglib.gui.widget.custom.PlayerInventoryWidget; +import com.lowdragmc.lowdraglib.syncdata.IManaged; +import com.lowdragmc.lowdraglib.syncdata.IManagedStorage; +import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; +import com.lowdragmc.lowdraglib.syncdata.annotation.Persisted; +import com.lowdragmc.lowdraglib.syncdata.blockentity.IAsyncAutoSyncBlockEntity; +import com.lowdragmc.lowdraglib.syncdata.blockentity.IAutoPersistBlockEntity; +import com.lowdragmc.lowdraglib.syncdata.blockentity.IManagedBlockEntity; +import com.lowdragmc.lowdraglib.syncdata.blockentity.IRPCBlockEntity; +import com.lowdragmc.lowdraglib.syncdata.field.FieldManagedStorage; +import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; +import lombok.Getter; +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +public class EquipmentFoundryBlockEntity extends BlockEntity implements IAsyncAutoSyncBlockEntity, IRPCBlockEntity, + IAutoPersistBlockEntity, IManaged, IManagedBlockEntity, IUIHolder { + + public static final int MAX_MODIFIER_SLOTS = 10; + + protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(EquipmentFoundryBlockEntity.class); + @Getter + private final FieldManagedStorage syncStorage = new FieldManagedStorage(this); + + @Persisted @DescSynced + private final CustomItemStackHandler equipmentSlot; + @Persisted @DescSynced + private final CustomItemStackHandler modifierSlots; + + public EquipmentFoundryBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { + super(type, pos, blockState); + this.equipmentSlot = new CustomItemStackHandler(1); + // TODO remove instanceof once datagen can run + this.equipmentSlot.setFilter(stack -> stack.is(CustomTags.MODIFIABLE_EQUIPMENT) || stack.getItem() instanceof ArmorComponentItem); + this.modifierSlots = new CustomItemStackHandler(MAX_MODIFIER_SLOTS); + } + + @Override + public ModularUI createUI(Player entityPlayer) { + ModularUI modularUI = new ModularUI(176, 166, this, entityPlayer); + modularUI.background(GuiTextures.BACKGROUND); + modularUI.widget(new LabelWidget(5, 5, getBlockState().getBlock().getDescriptionId())); + modularUI.widget(new EquipmentFoundryBaseWidget(5, 10, 176, 166, + equipmentSlot, modifierSlots)); + modularUI.widget(UITemplate.bindPlayerInventory(entityPlayer.getInventory(), GuiTextures.SLOT, 7, 84, true)); + return modularUI; + } + + @Override + public boolean isInvalid() { + return isRemoved(); + } + + @Override + public boolean isRemote() { + return getLevel().isClientSide; + } + + @Override + public void markAsDirty() { + setChanged(); + } + + @Override + public IManagedStorage getRootStorage() { + return syncStorage; + } + + @Override + public ManagedFieldHolder getFieldHolder() { + return MANAGED_FIELD_HOLDER; + } + + @Override + public void onChanged() { + this.setChanged(); + } + +} diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTBlockEntities.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTBlockEntities.java index 78c2f759bfb..55913e3bfea 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTBlockEntities.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTBlockEntities.java @@ -16,6 +16,11 @@ */ public class GTBlockEntities { + public static final BlockEntityEntry EQUIPMENT_FOUNDRY = REGISTRATE + .blockEntity("equipment_foundry", EquipmentFoundryBlockEntity::new) + .validBlock(GTBlocks.EQUIPMENT_FOUNDRY) + .register(); + @SuppressWarnings("unchecked") public static final BlockEntityEntry CABLE = REGISTRATE .blockEntity("cable", CableBlockEntity::create) diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTBlocks.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTBlocks.java index b51a9b26cff..a56d8d90dce 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTBlocks.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTBlocks.java @@ -190,6 +190,12 @@ private static void registerDuctPipeBlock(int index) { DUCT_PIPES[index] = entry; } + public static final BlockEntry EQUIPMENT_FOUNDRY = REGISTRATE + .block("equipment_foundry", EquipmentFoundryBlock::new) + .initialProperties(() -> Blocks.SMITHING_TABLE) + .simpleItem() + .register(); + // Long Distance Item Pipe Blocks public static final BlockEntry LD_ITEM_PIPE = REGISTRATE .block("long_distance_item_pipeline", diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTItems.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTItems.java index 3d6a9603191..c18fe8bcbb8 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTItems.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTItems.java @@ -2331,7 +2331,7 @@ public Component getItemName(ItemStack stack) { ConfigHolder.INSTANCE.tools.voltageTierNanoSuit))) .lang("NanoMuscle™ Suite Chestplate") .properties(p -> p.rarity(Rarity.UNCOMMON)) - .tag(Tags.Items.ARMORS_CHESTPLATES) + .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.MODIFIABLE_EQUIPMENT) .register(); public static ItemEntry NANO_LEGGINGS = REGISTRATE .item("nanomuscle_leggings", @@ -2343,7 +2343,7 @@ public Component getItemName(ItemStack stack) { ConfigHolder.INSTANCE.tools.voltageTierNanoSuit))) .lang("NanoMuscle™ Suite Leggings") .properties(p -> p.rarity(Rarity.UNCOMMON)) - .tag(Tags.Items.ARMORS_LEGGINGS) + .tag(Tags.Items.ARMORS_LEGGINGS, CustomTags.MODIFIABLE_EQUIPMENT) .register(); public static ItemEntry NANO_BOOTS = REGISTRATE .item("nanomuscle_boots", (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, ArmorItem.Type.BOOTS, p) @@ -2354,8 +2354,7 @@ public Component getItemName(ItemStack stack) { ConfigHolder.INSTANCE.tools.voltageTierNanoSuit))) .lang("NanoMuscle™ Suite Boots") .properties(p -> p.rarity(Rarity.UNCOMMON)) - .tag(Tags.Items.ARMORS_BOOTS) - .tag(CustomTags.STEP_BOOTS) + .tag(Tags.Items.ARMORS_BOOTS, CustomTags.STEP_BOOTS, CustomTags.MODIFIABLE_EQUIPMENT) .register(); public static ItemEntry NANO_HELMET = REGISTRATE .item("nanomuscle_helmet", (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, ArmorItem.Type.HELMET, p) @@ -2365,7 +2364,7 @@ public Component getItemName(ItemStack stack) { Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierNanoSuit - 3)), ConfigHolder.INSTANCE.tools.voltageTierNanoSuit))) .lang("NanoMuscle™ Suite Helmet") - .tag(Tags.Items.ARMORS_HELMETS) + .tag(Tags.Items.ARMORS_HELMETS, CustomTags.MODIFIABLE_EQUIPMENT) .properties(p -> p.rarity(Rarity.UNCOMMON)) .register(); @@ -2374,8 +2373,7 @@ public Component getItemName(ItemStack stack) { (p) -> new ArmorComponentItem(GTArmorMaterials.BAD_PPE_EQUIPMENT, ArmorItem.Type.HELMET, p) .setArmorLogic(new HazmatSuit(ArmorItem.Type.HELMET, "bad_hazmat"))) .lang("Face Mask") - .tag(Tags.Items.ARMORS_HELMETS) - .tag(CustomTags.PPE_ARMOR) + .tag(Tags.Items.ARMORS_HELMETS, CustomTags.PPE_ARMOR) .onRegister(attach(new TooltipBehavior(tooltips -> { tooltips.add(Component.translatable("gtceu.hazard_trigger.protection.description")); tooltips.add(Component.translatable("gtceu.hazard_trigger.inhalation")); @@ -2386,8 +2384,7 @@ public Component getItemName(ItemStack stack) { (p) -> new ArmorComponentItem(GTArmorMaterials.BAD_PPE_EQUIPMENT, ArmorItem.Type.HELMET, p) .setArmorLogic(new HazmatSuit(ArmorItem.Type.CHESTPLATE, "bad_hazmat"))) .lang("Rubber Gloves") - .tag(Tags.Items.ARMORS_CHESTPLATES) - .tag(CustomTags.PPE_ARMOR) + .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) .onRegister(attach(new TooltipBehavior(tooltips -> { tooltips.add(Component.translatable("gtceu.hazard_trigger.protection.description")); tooltips.add(Component.translatable("gtceu.hazard_trigger.skin_contact")); @@ -2399,8 +2396,7 @@ public Component getItemName(ItemStack stack) { .setArmorLogic(new HazmatSuit(ArmorItem.Type.CHESTPLATE, "hazmat"))) .lang("Hazardous Materials Suit Chestpiece") .properties(p -> p.rarity(Rarity.UNCOMMON)) - .tag(Tags.Items.ARMORS_CHESTPLATES) - .tag(CustomTags.PPE_ARMOR) + .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.PPE_ARMOR) .register(); public static ItemEntry HAZMAT_LEGGINGS = REGISTRATE .item("hazmat_leggings", @@ -2408,8 +2404,7 @@ public Component getItemName(ItemStack stack) { .setArmorLogic(new HazmatSuit(ArmorItem.Type.LEGGINGS, "hazmat"))) .lang("Hazardous Materials Suit Leggings") .properties(p -> p.rarity(Rarity.UNCOMMON)) - .tag(Tags.Items.ARMORS_LEGGINGS) - .tag(CustomTags.PPE_ARMOR) + .tag(Tags.Items.ARMORS_LEGGINGS, CustomTags.PPE_ARMOR) .register(); public static ItemEntry HAZMAT_BOOTS = REGISTRATE .item("hazmat_boots", @@ -2417,8 +2412,7 @@ public Component getItemName(ItemStack stack) { .setArmorLogic(new HazmatSuit(ArmorItem.Type.BOOTS, "hazmat"))) .lang("Hazardous Materials Suit Boots") .properties(p -> p.rarity(Rarity.UNCOMMON)) - .tag(Tags.Items.ARMORS_BOOTS) - .tag(CustomTags.PPE_ARMOR) + .tag(Tags.Items.ARMORS_BOOTS, CustomTags.PPE_ARMOR) .register(); public static ItemEntry HAZMAT_HELMET = REGISTRATE .item("hazmat_headpiece", @@ -2440,8 +2434,7 @@ public Component getItemName(ItemStack stack) { ConfigHolder.INSTANCE.tools.voltageTierQuarkTech))) .lang("QuarkTech™ Suite Chestplate") .properties(p -> p.rarity(Rarity.RARE)) - .tag(Tags.Items.ARMORS_CHESTPLATES) - .tag(CustomTags.PPE_ARMOR) + .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) .register(); public static ItemEntry QUANTUM_LEGGINGS = REGISTRATE .item("quarktech_leggings", @@ -2453,8 +2446,7 @@ public Component getItemName(ItemStack stack) { ConfigHolder.INSTANCE.tools.voltageTierQuarkTech))) .lang("QuarkTech™ Suite Leggings") .properties(p -> p.rarity(Rarity.RARE)) - .tag(Tags.Items.ARMORS_LEGGINGS) - .tag(CustomTags.PPE_ARMOR) + .tag(Tags.Items.ARMORS_LEGGINGS, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) .register(); public static ItemEntry QUANTUM_BOOTS = REGISTRATE .item("quarktech_boots", (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, ArmorItem.Type.BOOTS, p) @@ -2465,8 +2457,7 @@ public Component getItemName(ItemStack stack) { ConfigHolder.INSTANCE.tools.voltageTierQuarkTech))) .lang("QuarkTech™ Suite Boots") .properties(p -> p.rarity(Rarity.RARE)) - .tag(Tags.Items.ARMORS_BOOTS) - .tag(CustomTags.PPE_ARMOR) + .tag(Tags.Items.ARMORS_BOOTS, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) .tag(CustomTags.STEP_BOOTS) .register(); public static ItemEntry QUANTUM_HELMET = REGISTRATE @@ -2478,8 +2469,7 @@ public Component getItemName(ItemStack stack) { ConfigHolder.INSTANCE.tools.voltageTierQuarkTech))) .lang("QuarkTech™ Suite Helmet") .properties(p -> p.rarity(Rarity.RARE)) - .tag(Tags.Items.ARMORS_HELMETS) - .tag(CustomTags.PPE_ARMOR) + .tag(Tags.Items.ARMORS_HELMETS, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) .register(); public static ItemEntry LIQUID_FUEL_JETPACK = REGISTRATE @@ -2524,8 +2514,7 @@ public Component getItemName(ItemStack stack) { ConfigHolder.INSTANCE.tools.voltageTierAdvNanoSuit))) .lang("Advanced NanoMuscle™ Suite Chestplate") .properties(p -> p.rarity(Rarity.RARE)) - .tag(Tags.Items.ARMORS_CHESTPLATES) - .tag(CustomTags.PPE_ARMOR) + .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) .register(); public static ItemEntry QUANTUM_CHESTPLATE_ADVANCED = REGISTRATE .item("advanced_quarktech_chestplate", (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, @@ -2537,8 +2526,7 @@ public Component getItemName(ItemStack stack) { ConfigHolder.INSTANCE.tools.voltageTierAdvQuarkTech))) .lang("Advanced QuarkTech™ Suite Chestplate") .properties(p -> p.rarity(Rarity.EPIC)) - .tag(Tags.Items.ARMORS_CHESTPLATES) - .tag(CustomTags.PPE_ARMOR) + .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) .register(); public static ItemEntry POWER_THRUSTER = REGISTRATE.item("power_thruster", Item::new) diff --git a/src/main/java/com/gregtechceu/gtceu/common/gui/widget/EquipmentFoundryBaseWidget.java b/src/main/java/com/gregtechceu/gtceu/common/gui/widget/EquipmentFoundryBaseWidget.java new file mode 100644 index 00000000000..37744f11bd2 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/common/gui/widget/EquipmentFoundryBaseWidget.java @@ -0,0 +1,47 @@ +package com.gregtechceu.gtceu.common.gui.widget; + +import com.gregtechceu.gtceu.api.gui.widget.BlockableSlotWidget; +import com.gregtechceu.gtceu.api.gui.widget.SlotWidget; +import com.gregtechceu.gtceu.api.item.armor.ArmorComponentItem; +import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; +import net.minecraft.world.item.ItemStack; + +public class EquipmentFoundryBaseWidget extends WidgetGroup { + + private final CustomItemStackHandler equipmentSlot; + private final CustomItemStackHandler modifierSlots; + + private WidgetGroup slotGroup; + + public EquipmentFoundryBaseWidget(int x, int y, int width, int height, + CustomItemStackHandler equipmentSlot, + CustomItemStackHandler modifierSlots) { + super(x, y, width, height); + this.equipmentSlot = equipmentSlot; + this.modifierSlots = modifierSlots; + + addWidget(new BlockableSlotWidget(equipmentSlot, 0, 20, 20) + .setIsBlocked(() -> !equipmentSlot.getStackInSlot(0).isEmpty()) + .setChangeListener(this::onEquipmentItemChanged)); + } + + + public void onEquipmentItemChanged() { + ItemStack stack = equipmentSlot.getStackInSlot(0); + if (stack.getItem() instanceof ArmorComponentItem armorComponentItem) { + this.removeWidget(slotGroup); + + // TODO implement modification + + this.slotGroup = new WidgetGroup(0, 0, 0, 0); + for (int i = 0; i < armorComponentItem.getMaxModifiers(); i++) { + SlotWidget slot = new SlotWidget(modifierSlots, i, 0, i * 18, true, true); + slotGroup.addWidget(slot); + } + slotGroup.setSelfPosition((this.getSizeWidth() + slotGroup.getSizeWidth() - 18) / 2, + 50); + this.addWidget(slotGroup); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/CustomTags.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/CustomTags.java index f262be66ba5..51dde459e12 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/CustomTags.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/CustomTags.java @@ -1,10 +1,8 @@ package com.gregtechceu.gtceu.data.recipe; -import com.gregtechceu.gtceu.api.GTValues; import com.gregtechceu.gtceu.api.data.tag.TagUtil; import net.minecraft.core.registries.Registries; -import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.BlockTags; import net.minecraft.tags.TagKey; import net.minecraft.world.entity.EntityType; @@ -102,6 +100,7 @@ public class CustomTags { public static final TagKey PPE_ARMOR = TagUtil.createModItemTag("ppe_armor"); public static final TagKey STEP_BOOTS = TagUtil.createModItemTag("step_boots"); + public static final TagKey MODIFIABLE_EQUIPMENT = TagUtil.createModItemTag("modifiable_equipment"); public static final TagKey RUBBER_LOGS = TagUtil.createModItemTag("rubber_logs"); // Platform-dependent tags public static final TagKey NEEDS_WOOD_TOOL = TagUtil.createBlockTag("needs_wood_tool"); @@ -124,8 +123,6 @@ public class CustomTags { public static final TagKey ENDSTONE_ORE_REPLACEABLES = TagUtil.createBlockTag("end_stone_ore_replaceables"); public static final TagKey CONCRETE_BLOCK = TagUtil.createBlockTag("concretes"); public static final TagKey CONCRETE_POWDER_BLOCK = TagUtil.createBlockTag("concrete_powders"); - public static final TagKey CREATE_SEATS = TagUtil.optionalTag(Registries.BLOCK, - new ResourceLocation(GTValues.MODID_CREATE, "seats")); public static final TagKey CLEANROOM_FLOORS = TagUtil.createModBlockTag("cleanroom_floors"); public static final TagKey IS_SWAMP = TagUtil.createTag(Registries.BIOME, "is_swamp", false); From e854536ec563dd981788bfcaed22e8ef03af0f89 Mon Sep 17 00:00:00 2001 From: screret <68943070+screret@users.noreply.github.com> Date: Sun, 29 Dec 2024 17:47:36 +0200 Subject: [PATCH 002/286] clean up unnecessary uses of `ItemStack#getDamageValue` --- .../gtceu/api/recipe/ingredient/SizedIngredient.java | 2 +- .../java/com/gregtechceu/gtceu/data/loot/ChestGenHooks.java | 3 --- .../gtceu/data/recipe/builder/BlastingRecipeBuilder.java | 2 +- .../gtceu/data/recipe/builder/CampfireRecipeBuilder.java | 2 +- .../data/recipe/builder/ShapedEnergyTransferRecipeBuilder.java | 2 +- .../gtceu/data/recipe/builder/ShapedRecipeBuilder.java | 2 +- .../gtceu/data/recipe/builder/ShapelessRecipeBuilder.java | 2 +- .../gtceu/data/recipe/builder/SmeltingRecipeBuilder.java | 2 +- .../gtceu/data/recipe/builder/SmokingRecipeBuilder.java | 2 +- 9 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/gregtechceu/gtceu/api/recipe/ingredient/SizedIngredient.java b/src/main/java/com/gregtechceu/gtceu/api/recipe/ingredient/SizedIngredient.java index 1a032c1bdaf..81fc4aedeba 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/recipe/ingredient/SizedIngredient.java +++ b/src/main/java/com/gregtechceu/gtceu/api/recipe/ingredient/SizedIngredient.java @@ -42,7 +42,7 @@ protected SizedIngredient(@NotNull TagKey tag, int amount) { } protected SizedIngredient(ItemStack itemStack) { - this((itemStack.hasTag() || itemStack.getDamageValue() > 0) ? NBTIngredient.createNBTIngredient(itemStack) : + this((itemStack.hasTag()) ? NBTIngredient.createNBTIngredient(itemStack) : Ingredient.of(itemStack), itemStack.getCount()); } diff --git a/src/main/java/com/gregtechceu/gtceu/data/loot/ChestGenHooks.java b/src/main/java/com/gregtechceu/gtceu/data/loot/ChestGenHooks.java index 357be21399c..0a0548e3ce8 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/loot/ChestGenHooks.java +++ b/src/main/java/com/gregtechceu/gtceu/data/loot/ChestGenHooks.java @@ -161,9 +161,6 @@ public LootItemFunctionType getType() { @Override protected ItemStack run(ItemStack itemStack, LootContext context) { - if (stack.getDamageValue() != 0) { - itemStack.setDamageValue(stack.getDamageValue()); - } CompoundTag tagCompound = stack.getTag(); if (tagCompound != null) { itemStack.setTag(tagCompound.copy()); diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/BlastingRecipeBuilder.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/BlastingRecipeBuilder.java index fc9bf7c98ed..ad6a57737ad 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/BlastingRecipeBuilder.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/BlastingRecipeBuilder.java @@ -52,7 +52,7 @@ public BlastingRecipeBuilder input(TagKey itemStack) { } public BlastingRecipeBuilder input(ItemStack itemStack) { - if (itemStack.hasTag() || itemStack.getDamageValue() > 0) { + if (itemStack.hasTag()) { input = NBTIngredient.createNBTIngredient(itemStack); } else { input = Ingredient.of(itemStack); diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/CampfireRecipeBuilder.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/CampfireRecipeBuilder.java index 1239b8d0cf8..518f06e729f 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/CampfireRecipeBuilder.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/CampfireRecipeBuilder.java @@ -52,7 +52,7 @@ public CampfireRecipeBuilder input(TagKey itemStack) { } public CampfireRecipeBuilder input(ItemStack itemStack) { - if (itemStack.hasTag() || itemStack.getDamageValue() > 0) { + if (itemStack.hasTag()) { input = NBTIngredient.createNBTIngredient(itemStack); } else { input = Ingredient.of(itemStack); diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/ShapedEnergyTransferRecipeBuilder.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/ShapedEnergyTransferRecipeBuilder.java index 55bebdd2303..588f5880c46 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/ShapedEnergyTransferRecipeBuilder.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/ShapedEnergyTransferRecipeBuilder.java @@ -55,7 +55,7 @@ public ShapedEnergyTransferRecipeBuilder define(char cha, TagKey itemStack } public ShapedEnergyTransferRecipeBuilder define(char cha, ItemStack itemStack) { - if (itemStack.hasTag() || itemStack.getDamageValue() > 0) { + if (itemStack.hasTag()) { return where(cha, NBTIngredient.createNBTIngredient(itemStack)); } else { return where(cha, Ingredient.of(itemStack)); diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/ShapedRecipeBuilder.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/ShapedRecipeBuilder.java index 2c08e5c57a7..9a6da0517c5 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/ShapedRecipeBuilder.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/ShapedRecipeBuilder.java @@ -53,7 +53,7 @@ public ShapedRecipeBuilder define(char cha, TagKey itemStack) { } public ShapedRecipeBuilder define(char cha, ItemStack itemStack) { - if (itemStack.hasTag() || itemStack.getDamageValue() > 0) { + if (itemStack.hasTag()) { return where(cha, NBTIngredient.createNBTIngredient(itemStack)); } else { return where(cha, Ingredient.of(itemStack)); diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/ShapelessRecipeBuilder.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/ShapelessRecipeBuilder.java index a5d89f33a27..18e426986ee 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/ShapelessRecipeBuilder.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/ShapelessRecipeBuilder.java @@ -55,7 +55,7 @@ public ShapelessRecipeBuilder requires(TagKey itemStack) { } public ShapelessRecipeBuilder requires(ItemStack itemStack) { - if (itemStack.hasTag() || itemStack.getDamageValue() > 0) { + if (itemStack.hasTag()) { requires(NBTIngredient.createNBTIngredient(itemStack)); } else { requires(Ingredient.of(itemStack)); diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/SmeltingRecipeBuilder.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/SmeltingRecipeBuilder.java index 1206bfa24fa..db3c9c0d3aa 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/SmeltingRecipeBuilder.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/SmeltingRecipeBuilder.java @@ -52,7 +52,7 @@ public SmeltingRecipeBuilder input(TagKey itemStack) { } public SmeltingRecipeBuilder input(ItemStack itemStack) { - if (itemStack.hasTag() || itemStack.getDamageValue() > 0) { + if (itemStack.hasTag()) { input = NBTIngredient.createNBTIngredient(itemStack); } else { input = Ingredient.of(itemStack); diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/SmokingRecipeBuilder.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/SmokingRecipeBuilder.java index 2cf526d892e..bde718935af 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/SmokingRecipeBuilder.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/SmokingRecipeBuilder.java @@ -52,7 +52,7 @@ public SmokingRecipeBuilder input(TagKey itemStack) { } public SmokingRecipeBuilder input(ItemStack itemStack) { - if (itemStack.hasTag() || itemStack.getDamageValue() > 0) { + if (itemStack.hasTag()) { input = NBTIngredient.createNBTIngredient(itemStack); } else { input = Ingredient.of(itemStack); From 64056a5423b24dba7220de90c0f35585fd38d6d6 Mon Sep 17 00:00:00 2001 From: screret <68943070+screret@users.noreply.github.com> Date: Sun, 29 Dec 2024 17:54:21 +0200 Subject: [PATCH 003/286] start on armor modifier --- .../item/armor/modifier/ArmorModifier.java | 66 ++++++++++ .../common/block/EquipmentFoundryBlock.java | 4 +- .../EquipmentFoundryBlockEntity.java | 21 +-- .../gtceu/common/data/GTRecipeTypes.java | 21 +++ .../widget/EquipmentFoundryBaseWidget.java | 3 +- .../recipe/type/EquipmentFoundryRecipe.java | 122 ++++++++++++++++++ .../EquipmentFoundryRecipeBuilder.java | 104 +++++++++++++++ 7 files changed, 331 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/gregtechceu/gtceu/api/item/armor/modifier/ArmorModifier.java create mode 100644 src/main/java/com/gregtechceu/gtceu/common/recipe/type/EquipmentFoundryRecipe.java create mode 100644 src/main/java/com/gregtechceu/gtceu/data/recipe/builder/EquipmentFoundryRecipeBuilder.java diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/armor/modifier/ArmorModifier.java b/src/main/java/com/gregtechceu/gtceu/api/item/armor/modifier/ArmorModifier.java new file mode 100644 index 00000000000..dd99a532191 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/item/armor/modifier/ArmorModifier.java @@ -0,0 +1,66 @@ +package com.gregtechceu.gtceu.api.item.armor.modifier; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.Attribute; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.item.ItemStack; + +import com.mojang.serialization.Codec; +import lombok.Getter; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +public class ArmorModifier { + + public static final Map MODIFIERS = new HashMap<>(); + public static final Codec CODEC = ResourceLocation.CODEC.xmap(ArmorModifier.MODIFIERS::get, + ArmorModifier::getId); + + @Getter + public final ResourceLocation id; + @Getter + public final ItemModifier onAddToItem; + @Getter + public final Modifier onAdd; + @Getter + public final Modifier onRemove; + + public ArmorModifier(ResourceLocation id, Modifier onAdd, Modifier onRemove) { + this.id = id; + this.onAddToItem = ItemModifier.IDENTITY; + this.onAdd = onAdd; + this.onRemove = onRemove; + } + + public ArmorModifier(ResourceLocation id, Attribute attribute, AttributeModifier modifier, EquipmentSlot slot) { + this.id = id; + this.onAddToItem = (stack) -> { + if (stack.getAttributeModifiers(slot).containsEntry(attribute, modifier)) { + return; + } + stack.addAttributeModifier(attribute, modifier, slot); + }; + this.onAdd = Modifier.IDENTITY; + this.onRemove = Modifier.IDENTITY; + } + + @FunctionalInterface + public interface Modifier { + + Modifier IDENTITY = (stack, entity) -> {}; + + void apply(ItemStack stack, @Nullable LivingEntity entity); + } + + @FunctionalInterface + public interface ItemModifier { + + ItemModifier IDENTITY = (stack) -> {}; + + void apply(ItemStack stack); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/common/block/EquipmentFoundryBlock.java b/src/main/java/com/gregtechceu/gtceu/common/block/EquipmentFoundryBlock.java index dd776a9fc31..477eb6f35e5 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/block/EquipmentFoundryBlock.java +++ b/src/main/java/com/gregtechceu/gtceu/common/block/EquipmentFoundryBlock.java @@ -2,7 +2,9 @@ import com.gregtechceu.gtceu.common.blockentity.EquipmentFoundryBlockEntity; import com.gregtechceu.gtceu.common.data.GTBlockEntities; + import com.lowdragmc.lowdraglib.gui.factory.BlockEntityUIFactory; + import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.InteractionHand; @@ -13,6 +15,7 @@ import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -40,5 +43,4 @@ public EquipmentFoundryBlock(Properties properties) { } return InteractionResult.sidedSuccess(level.isClientSide); } - } diff --git a/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java b/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java index 12606845438..da2de7af95a 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java +++ b/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java @@ -6,10 +6,10 @@ import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler; import com.gregtechceu.gtceu.common.gui.widget.EquipmentFoundryBaseWidget; import com.gregtechceu.gtceu.data.recipe.CustomTags; + import com.lowdragmc.lowdraglib.gui.modular.IUIHolder; import com.lowdragmc.lowdraglib.gui.modular.ModularUI; import com.lowdragmc.lowdraglib.gui.widget.LabelWidget; -import com.lowdragmc.lowdraglib.gui.widget.custom.PlayerInventoryWidget; import com.lowdragmc.lowdraglib.syncdata.IManaged; import com.lowdragmc.lowdraglib.syncdata.IManagedStorage; import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; @@ -20,32 +20,38 @@ import com.lowdragmc.lowdraglib.syncdata.blockentity.IRPCBlockEntity; import com.lowdragmc.lowdraglib.syncdata.field.FieldManagedStorage; import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; -import lombok.Getter; + import net.minecraft.core.BlockPos; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import lombok.Getter; + public class EquipmentFoundryBlockEntity extends BlockEntity implements IAsyncAutoSyncBlockEntity, IRPCBlockEntity, - IAutoPersistBlockEntity, IManaged, IManagedBlockEntity, IUIHolder { + IAutoPersistBlockEntity, IManaged, IManagedBlockEntity, IUIHolder { public static final int MAX_MODIFIER_SLOTS = 10; - protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder(EquipmentFoundryBlockEntity.class); + protected static final ManagedFieldHolder MANAGED_FIELD_HOLDER = new ManagedFieldHolder( + EquipmentFoundryBlockEntity.class); @Getter private final FieldManagedStorage syncStorage = new FieldManagedStorage(this); - @Persisted @DescSynced + @Persisted + @DescSynced private final CustomItemStackHandler equipmentSlot; - @Persisted @DescSynced + @Persisted + @DescSynced private final CustomItemStackHandler modifierSlots; public EquipmentFoundryBlockEntity(BlockEntityType type, BlockPos pos, BlockState blockState) { super(type, pos, blockState); this.equipmentSlot = new CustomItemStackHandler(1); // TODO remove instanceof once datagen can run - this.equipmentSlot.setFilter(stack -> stack.is(CustomTags.MODIFIABLE_EQUIPMENT) || stack.getItem() instanceof ArmorComponentItem); + this.equipmentSlot.setFilter( + stack -> stack.is(CustomTags.MODIFIABLE_EQUIPMENT) || stack.getItem() instanceof ArmorComponentItem); this.modifierSlots = new CustomItemStackHandler(MAX_MODIFIER_SLOTS); } @@ -89,5 +95,4 @@ public ManagedFieldHolder getFieldHolder() { public void onChanged() { this.setChanged(); } - } diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeTypes.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeTypes.java index 627855cf05b..1d9043723a5 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeTypes.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeTypes.java @@ -22,6 +22,7 @@ import com.gregtechceu.gtceu.common.machine.trait.customlogic.CannerLogic; import com.gregtechceu.gtceu.common.machine.trait.customlogic.FormingPressLogic; import com.gregtechceu.gtceu.common.recipe.condition.RockBreakerCondition; +import com.gregtechceu.gtceu.common.recipe.type.EquipmentFoundryRecipe; import com.gregtechceu.gtceu.data.recipe.RecipeHelper; import com.gregtechceu.gtceu.data.recipe.builder.GTRecipeBuilder; import com.gregtechceu.gtceu.integration.kjs.GTRegistryInfo; @@ -33,6 +34,7 @@ import net.minecraft.client.resources.language.I18n; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceLocation; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; @@ -42,10 +44,13 @@ import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fml.ModLoader; +import com.tterrag.registrate.util.entry.RegistryEntry; + import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static com.gregtechceu.gtceu.common.registry.GTRegistration.REGISTRATE; import static com.lowdragmc.lowdraglib.gui.texture.ProgressTexture.FillDirection.*; /** @@ -66,6 +71,22 @@ public class GTRecipeTypes { GTRegistries.RECIPE_CATEGORIES.unfreeze(); } + public static final RegistryEntry> EQUIPMENT_FOUNDRY_RECIPES = REGISTRATE + .generic("equipment_foundry", + Registries.RECIPE_TYPE, () -> new RecipeType() { + + @Override + public String toString() { + return "equipment_foundry"; + } + }) + .register(); + + public static final RegistryEntry EQUIPMENT_FOUNDRY_SERIALIZER = REGISTRATE + .generic("equipment_foundry", + Registries.RECIPE_SERIALIZER, EquipmentFoundryRecipe.Serializer::new) + .register(); + ////////////////////////////////////// // ********* Steam **********// ////////////////////////////////////// diff --git a/src/main/java/com/gregtechceu/gtceu/common/gui/widget/EquipmentFoundryBaseWidget.java b/src/main/java/com/gregtechceu/gtceu/common/gui/widget/EquipmentFoundryBaseWidget.java index 37744f11bd2..e5f86d7303e 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/gui/widget/EquipmentFoundryBaseWidget.java +++ b/src/main/java/com/gregtechceu/gtceu/common/gui/widget/EquipmentFoundryBaseWidget.java @@ -4,7 +4,9 @@ import com.gregtechceu.gtceu.api.gui.widget.SlotWidget; import com.gregtechceu.gtceu.api.item.armor.ArmorComponentItem; import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler; + import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; + import net.minecraft.world.item.ItemStack; public class EquipmentFoundryBaseWidget extends WidgetGroup { @@ -26,7 +28,6 @@ public EquipmentFoundryBaseWidget(int x, int y, int width, int height, .setChangeListener(this::onEquipmentItemChanged)); } - public void onEquipmentItemChanged() { ItemStack stack = equipmentSlot.getStackInSlot(0); if (stack.getItem() instanceof ArmorComponentItem armorComponentItem) { diff --git a/src/main/java/com/gregtechceu/gtceu/common/recipe/type/EquipmentFoundryRecipe.java b/src/main/java/com/gregtechceu/gtceu/common/recipe/type/EquipmentFoundryRecipe.java new file mode 100644 index 00000000000..ba927d01c3b --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/common/recipe/type/EquipmentFoundryRecipe.java @@ -0,0 +1,122 @@ +package com.gregtechceu.gtceu.common.recipe.type; + +import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; +import com.gregtechceu.gtceu.common.data.GTItems; +import com.gregtechceu.gtceu.common.data.GTRecipeTypes; +import com.gregtechceu.gtceu.data.recipe.CustomTags; + +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.GsonHelper; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.level.Level; +import net.minecraftforge.items.wrapper.RecipeWrapper; + +import com.google.gson.JsonObject; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import javax.annotation.ParametersAreNonnullByDefault; + +@RequiredArgsConstructor +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +public class EquipmentFoundryRecipe implements Recipe { + + @Getter + private final ResourceLocation id; + private final Ingredient ingredient; + private final ArmorModifier modifier; + + /** + * Used to check if a recipe matches current crafting inventory + */ + public boolean matches(RecipeWrapper inv, Level level) { + boolean foundItem = false, foundIngredient = false; + + for (int i = 0; i < inv.getContainerSize(); ++i) { + ItemStack stack = inv.getItem(i); + if (!stack.isEmpty()) { + if (stack.is(CustomTags.MODIFIABLE_EQUIPMENT)) { + if (foundItem) { + return false; + } + + foundItem = true; + } else if (ingredient.test(stack)) { + foundIngredient = true; + } + } + } + + return foundItem && foundIngredient; + } + + public ItemStack assemble(RecipeWrapper container, RegistryAccess registryAccess) { + ItemStack result = ItemStack.EMPTY; + + for (int i = 0; i < container.getContainerSize(); ++i) { + ItemStack stack = container.getItem(i); + if (stack.is(CustomTags.MODIFIABLE_EQUIPMENT)) { + if (!result.isEmpty()) { + return ItemStack.EMPTY; + } + + result = stack.copy(); + } else if (!ingredient.test(stack)) { + return ItemStack.EMPTY; + } + } + + if (!result.isEmpty()) { + this.modifier.onAddToItem.apply(result); + } + return result; + } + + @Override + public boolean canCraftInDimensions(int width, int height) { + return true; + } + + @Override + public ItemStack getResultItem(RegistryAccess registryAccess) { + return GTItems.QUANTUM_CHESTPLATE_ADVANCED.asStack(); + } + + @Override + public RecipeSerializer getSerializer() { + return GTRecipeTypes.EQUIPMENT_FOUNDRY_SERIALIZER.get(); + } + + @Override + public RecipeType getType() { + return GTRecipeTypes.EQUIPMENT_FOUNDRY_RECIPES.get(); + } + + public static class Serializer implements RecipeSerializer { + + public EquipmentFoundryRecipe fromJson(ResourceLocation recipeId, JsonObject json) { + Ingredient ingredient = Ingredient.fromJson(GsonHelper.getAsJsonObject(json, "ingredient"), false); + ArmorModifier modifier = ArmorModifier.MODIFIERS.get( + new ResourceLocation(GsonHelper.getAsString(json, "modifier"))); + + return new EquipmentFoundryRecipe(recipeId, ingredient, modifier); + } + + public EquipmentFoundryRecipe fromNetwork(ResourceLocation recipeId, FriendlyByteBuf buffer) { + Ingredient ingredient = Ingredient.fromNetwork(buffer); + ArmorModifier modifier = ArmorModifier.MODIFIERS.get(buffer.readResourceLocation()); + + return new EquipmentFoundryRecipe(recipeId, ingredient, modifier); + } + + public void toNetwork(FriendlyByteBuf buffer, EquipmentFoundryRecipe recipe) { + recipe.ingredient.toNetwork(buffer); + buffer.writeResourceLocation(recipe.modifier.id); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/EquipmentFoundryRecipeBuilder.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/EquipmentFoundryRecipeBuilder.java new file mode 100644 index 00000000000..283b4e1d07d --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/EquipmentFoundryRecipeBuilder.java @@ -0,0 +1,104 @@ +package com.gregtechceu.gtceu.data.recipe.builder; + +import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; +import com.gregtechceu.gtceu.api.recipe.ingredient.NBTIngredient; + +import net.minecraft.data.recipes.FinishedRecipe; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.RecipeSerializer; +import net.minecraft.world.level.ItemLike; + +import com.google.gson.JsonObject; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +@Accessors(chain = true, fluent = true) +public class EquipmentFoundryRecipeBuilder { + + @Setter + private ResourceLocation id; + @Setter + private Ingredient ingredient; + @Setter + private ArmorModifier modifier; + + public EquipmentFoundryRecipeBuilder(@Nullable ResourceLocation id) { + this.id = id; + } + + public EquipmentFoundryRecipeBuilder input(TagKey itemStack) { + return input(Ingredient.of(itemStack)); + } + + public EquipmentFoundryRecipeBuilder input(ItemStack itemStack) { + if (itemStack.hasTag()) { + ingredient = NBTIngredient.createNBTIngredient(itemStack); + } else { + ingredient = Ingredient.of(itemStack); + } + return this; + } + + public EquipmentFoundryRecipeBuilder input(ItemLike itemLike) { + return input(Ingredient.of(itemLike)); + } + + public EquipmentFoundryRecipeBuilder input(Ingredient ingredient) { + this.ingredient = ingredient; + return this; + } + + protected ResourceLocation defaultId() { + return modifier.id; + } + + public void toJson(JsonObject json) { + if (!ingredient.isEmpty()) { + json.add("ingredient", ingredient.toJson()); + } + + if (modifier != null) { + json.addProperty("modifier", modifier.id.toString()); + } + } + + public void save(Consumer consumer) { + consumer.accept(new FinishedRecipe() { + + @Override + public void serializeRecipeData(JsonObject pJson) { + toJson(pJson); + } + + @Override + public ResourceLocation getId() { + var ID = id == null ? defaultId() : id; + return new ResourceLocation(ID.getNamespace(), "equipment_foundry" + "/" + ID.getPath()); + } + + @Override + public RecipeSerializer getType() { + return RecipeSerializer.SMOKING_RECIPE; + } + + @Nullable + @Override + public JsonObject serializeAdvancement() { + return null; + } + + @Nullable + @Override + public ResourceLocation getAdvancementId() { + return null; + } + }); + } +} From 874dcab88b6c00a7aae368a588cb109eac1c4a29 Mon Sep 17 00:00:00 2001 From: screret <68943070+screret@users.noreply.github.com> Date: Mon, 30 Dec 2024 15:01:09 +0200 Subject: [PATCH 004/286] make step height modification work for all living entities --- .../gtceu/forge/ForgeCommonEventListener.java | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/gregtechceu/gtceu/forge/ForgeCommonEventListener.java b/src/main/java/com/gregtechceu/gtceu/forge/ForgeCommonEventListener.java index 9fa56ceb833..81e6224ee28 100644 --- a/src/main/java/com/gregtechceu/gtceu/forge/ForgeCommonEventListener.java +++ b/src/main/java/com/gregtechceu/gtceu/forge/ForgeCommonEventListener.java @@ -60,6 +60,7 @@ import net.minecraft.world.Difficulty; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.monster.Zombie; import net.minecraft.world.entity.player.Player; @@ -371,7 +372,7 @@ public static void onDatapackSync(OnDatapackSyncEvent event) { } @SubscribeEvent(priority = EventPriority.LOW) - public static void onEntityLivingFallEvent(LivingFallEvent event) { + public static void onLivingFall(LivingFallEvent event) { if (event.getEntity() instanceof ServerPlayer player) { if (player.fallDistance < 3.2f) return; @@ -396,15 +397,16 @@ public static void onEntityLivingFallEvent(LivingFallEvent event) { } @SubscribeEvent - public static void stepAssistHandler(LivingEvent.LivingTickEvent event) { + public static void onLivingTick(LivingEvent.LivingTickEvent event) { + LivingEntity entity = event.getEntity(); + float MAGIC_STEP_HEIGHT = 1.0023f; - if (event.getEntity() == null || !(event.getEntity() instanceof Player player)) return; - if (!player.isCrouching() && player.getItemBySlot(EquipmentSlot.FEET).is(CustomTags.STEP_BOOTS)) { - if (player.getStepHeight() < MAGIC_STEP_HEIGHT) { - player.setMaxUpStep(MAGIC_STEP_HEIGHT); + if (!entity.isCrouching() && entity.getItemBySlot(EquipmentSlot.FEET).is(CustomTags.STEP_BOOTS)) { + if (entity.getStepHeight() < MAGIC_STEP_HEIGHT) { + entity.setMaxUpStep(MAGIC_STEP_HEIGHT); } - } else if (player.getStepHeight() == MAGIC_STEP_HEIGHT) { - player.setMaxUpStep(0.6f); + } else if (entity.getStepHeight() == MAGIC_STEP_HEIGHT) { + entity.setMaxUpStep(0.6f); } } From 85fa04c7f7d57135297edde519f8400208d938f2 Mon Sep 17 00:00:00 2001 From: screret <68943070+screret@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:04:18 +0200 Subject: [PATCH 005/286] forgot to commit --- .../gtceu/blockstates/equipment_foundry.json | 7 + .../resources/assets/gtceu/lang/en_ud.json | 1 + .../resources/assets/gtceu/lang/en_us.json | 1 + .../gtceu/models/block/equipment_foundry.json | 6 + .../gtceu/models/item/equipment_foundry.json | 3 + .../loot_tables/blocks/equipment_foundry.json | 21 ++ .../tags/items/modifiable_equipment.json | 15 + .../com/gregtechceu/gtceu/api/GTValues.java | 3 +- .../gtceu/api/gui/GuiTextures.java | 4 + .../api/item/armor/ArmorComponentItem.java | 22 +- .../gtceu/api/item/armor/ArmorLogicSuite.java | 2 +- .../gtceu/api/item/armor/ArmorUtils.java | 99 ++++++ .../api/item/armor/ModifiableArmorItem.java | 295 ++++++++++++++++++ .../item/armor/modifier/ArmorModifier.java | 195 ++++++++++-- .../gregtechceu/gtceu/common/CommonProxy.java | 10 + .../common/block/EquipmentFoundryBlock.java | 8 + .../EquipmentFoundryBlockEntity.java | 148 ++++++++- .../gtceu/common/data/GTArmorModifiers.java | 113 +++++++ .../gtceu/common/data/GTItems.java | 125 +++----- .../gtceu/common/data/GTRecipes.java | 4 +- .../widget/EquipmentFoundryBaseWidget.java | 48 --- .../item/armor/AdvancedNanoMuscleSuite.java | 2 +- .../item/armor/AdvancedQuarkTechSuite.java | 2 +- .../common/item/armor/GTArmorMaterials.java | 20 +- .../common/item/armor/NanoMuscleSuite.java | 6 +- .../common/item/armor/QuarkTechSuite.java | 4 +- .../recipe/type/EquipmentFoundryRecipe.java | 38 ++- .../gtceu/core/mixins/EntityMixin.java | 10 +- .../gtceu/core/mixins/LivingEntityMixin.java | 3 +- .../core/mixins/RangedAttributeAccessor.java | 15 + .../data/recipe/VanillaRecipeHelper.java | 44 ++- .../EquipmentFoundryRecipeBuilder.java | 40 +-- .../recipe/misc/CraftingRecipeLoader.java | 10 - .../recipe/misc/EquipmentFoundryRecipes.java | 24 ++ .../gtceu/forge/ForgeCommonEventListener.java | 54 +++- .../gtceu/syncdata/GTRecipePayload.java | 4 +- .../textures/block/equipment_foundry.png | Bin 0 -> 1203 bytes .../widget/equipment_foundry_background.png | Bin 0 -> 50715 bytes .../advanced_nano_muscle_suite_layer_1.png} | Bin .../advanced_nano_muscle_suite_layer_2.png} | Bin .../advanced_quark_tech_suite_layer_1.png} | Bin .../advanced_quark_tech_suite_layer_2.png} | Bin .../armor/nano_muscle_suite_layer_1.png} | Bin .../armor/nano_muscle_suite_layer_2.png} | Bin .../armor/quark_tech_suite_layer_1.png} | Bin .../armor/quark_tech_suite_layer_2.png} | Bin src/main/resources/gtceu.mixins.json | 5 +- 47 files changed, 1167 insertions(+), 244 deletions(-) create mode 100644 src/generated/resources/assets/gtceu/blockstates/equipment_foundry.json create mode 100644 src/generated/resources/assets/gtceu/models/block/equipment_foundry.json create mode 100644 src/generated/resources/assets/gtceu/models/item/equipment_foundry.json create mode 100644 src/generated/resources/data/gtceu/loot_tables/blocks/equipment_foundry.json create mode 100644 src/generated/resources/data/gtceu/tags/items/modifiable_equipment.json create mode 100644 src/main/java/com/gregtechceu/gtceu/api/item/armor/ModifiableArmorItem.java create mode 100644 src/main/java/com/gregtechceu/gtceu/common/data/GTArmorModifiers.java delete mode 100644 src/main/java/com/gregtechceu/gtceu/common/gui/widget/EquipmentFoundryBaseWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/core/mixins/RangedAttributeAccessor.java create mode 100644 src/main/java/com/gregtechceu/gtceu/data/recipe/misc/EquipmentFoundryRecipes.java create mode 100644 src/main/resources/assets/gtceu/textures/block/equipment_foundry.png create mode 100644 src/main/resources/assets/gtceu/textures/gui/widget/equipment_foundry_background.png rename src/main/resources/assets/gtceu/textures/{armor/advanced_nano_muscle_suite_1.png => models/armor/advanced_nano_muscle_suite_layer_1.png} (100%) rename src/main/resources/assets/gtceu/textures/{armor/advanced_nano_muscle_suite_2.png => models/armor/advanced_nano_muscle_suite_layer_2.png} (100%) rename src/main/resources/assets/gtceu/textures/{armor/advanced_quark_tech_suite_1.png => models/armor/advanced_quark_tech_suite_layer_1.png} (100%) rename src/main/resources/assets/gtceu/textures/{armor/advanced_quark_tech_suite_2.png => models/armor/advanced_quark_tech_suite_layer_2.png} (100%) rename src/main/resources/assets/gtceu/textures/{armor/nano_muscule_suite_1.png => models/armor/nano_muscle_suite_layer_1.png} (100%) rename src/main/resources/assets/gtceu/textures/{armor/nano_muscule_suite_2.png => models/armor/nano_muscle_suite_layer_2.png} (100%) rename src/main/resources/assets/gtceu/textures/{armor/quark_tech_suite_1.png => models/armor/quark_tech_suite_layer_1.png} (100%) rename src/main/resources/assets/gtceu/textures/{armor/quark_tech_suite_2.png => models/armor/quark_tech_suite_layer_2.png} (100%) diff --git a/src/generated/resources/assets/gtceu/blockstates/equipment_foundry.json b/src/generated/resources/assets/gtceu/blockstates/equipment_foundry.json new file mode 100644 index 00000000000..54d31f3ed4a --- /dev/null +++ b/src/generated/resources/assets/gtceu/blockstates/equipment_foundry.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "gtceu:block/equipment_foundry" + } + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtceu/lang/en_ud.json b/src/generated/resources/assets/gtceu/lang/en_ud.json index 7c52c25c147..8d658d412b6 100644 --- a/src/generated/resources/assets/gtceu/lang/en_ud.json +++ b/src/generated/resources/assets/gtceu/lang/en_ud.json @@ -191,6 +191,7 @@ "block.gtceu.empty_tier_ii_battery": "ɹoʇıɔɐdɐƆ II ɹǝı⟘ ʎʇdɯƎ", "block.gtceu.empty_tier_iii_battery": "ɹoʇıɔɐdɐƆ III ɹǝı⟘ ʎʇdɯƎ", "block.gtceu.engine_intake_casing": "buısɐƆ ǝʞɐʇuI ǝuıbuƎ", + "block.gtceu.equipment_foundry": "ʎɹpunoℲ ʇuǝɯdınbƎ", "block.gtceu.ev_16a_energy_converter": "ɹǝʇɹǝʌuoƆ ʎbɹǝuƎ ɹ§Ɐǝ§9Ɩ ɹ§ΛƎϛ§", "block.gtceu.ev_1a_energy_converter": "ɹǝʇɹǝʌuoƆ ʎbɹǝuƎ ɹ§Ɐǝ§Ɩ ɹ§ΛƎϛ§", "block.gtceu.ev_4a_energy_converter": "ɹǝʇɹǝʌuoƆ ʎbɹǝuƎ ɹ§Ɐǝ§ㄣ ɹ§ΛƎϛ§", diff --git a/src/generated/resources/assets/gtceu/lang/en_us.json b/src/generated/resources/assets/gtceu/lang/en_us.json index 6c8d4c0ec41..2f1b351ecd5 100644 --- a/src/generated/resources/assets/gtceu/lang/en_us.json +++ b/src/generated/resources/assets/gtceu/lang/en_us.json @@ -191,6 +191,7 @@ "block.gtceu.empty_tier_ii_battery": "Empty Tier II Capacitor", "block.gtceu.empty_tier_iii_battery": "Empty Tier III Capacitor", "block.gtceu.engine_intake_casing": "Engine Intake Casing", + "block.gtceu.equipment_foundry": "Equipment Foundry", "block.gtceu.ev_16a_energy_converter": "§5EV§r 16§eA§r Energy Converter", "block.gtceu.ev_1a_energy_converter": "§5EV§r 1§eA§r Energy Converter", "block.gtceu.ev_4a_energy_converter": "§5EV§r 4§eA§r Energy Converter", diff --git a/src/generated/resources/assets/gtceu/models/block/equipment_foundry.json b/src/generated/resources/assets/gtceu/models/block/equipment_foundry.json new file mode 100644 index 00000000000..77b8bf7bbc7 --- /dev/null +++ b/src/generated/resources/assets/gtceu/models/block/equipment_foundry.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "gtceu:block/equipment_foundry" + } +} \ No newline at end of file diff --git a/src/generated/resources/assets/gtceu/models/item/equipment_foundry.json b/src/generated/resources/assets/gtceu/models/item/equipment_foundry.json new file mode 100644 index 00000000000..4de77ddab56 --- /dev/null +++ b/src/generated/resources/assets/gtceu/models/item/equipment_foundry.json @@ -0,0 +1,3 @@ +{ + "parent": "gtceu:block/equipment_foundry" +} \ No newline at end of file diff --git a/src/generated/resources/data/gtceu/loot_tables/blocks/equipment_foundry.json b/src/generated/resources/data/gtceu/loot_tables/blocks/equipment_foundry.json new file mode 100644 index 00000000000..6298ba16691 --- /dev/null +++ b/src/generated/resources/data/gtceu/loot_tables/blocks/equipment_foundry.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "gtceu:equipment_foundry" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "gtceu:blocks/equipment_foundry" +} \ No newline at end of file diff --git a/src/generated/resources/data/gtceu/tags/items/modifiable_equipment.json b/src/generated/resources/data/gtceu/tags/items/modifiable_equipment.json new file mode 100644 index 00000000000..f77d53c52e1 --- /dev/null +++ b/src/generated/resources/data/gtceu/tags/items/modifiable_equipment.json @@ -0,0 +1,15 @@ +{ + "values": [ + "gtceu:nanomuscle_chestplate", + "gtceu:nanomuscle_leggings", + "gtceu:nanomuscle_boots", + "gtceu:nanomuscle_helmet", + "gtceu:rubber_gloves", + "gtceu:quarktech_chestplate", + "gtceu:quarktech_leggings", + "gtceu:quarktech_boots", + "gtceu:quarktech_helmet", + "gtceu:avanced_nanomuscle_chestplate", + "gtceu:advanced_quarktech_chestplate" + ] +} \ No newline at end of file diff --git a/src/main/java/com/gregtechceu/gtceu/api/GTValues.java b/src/main/java/com/gregtechceu/gtceu/api/GTValues.java index b5af91ff727..eedbfb0d238 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/GTValues.java +++ b/src/main/java/com/gregtechceu/gtceu/api/GTValues.java @@ -129,7 +129,8 @@ public static int[] tiersBetween(int minInclusive, int maxInclusive) { MODID_FTB_CHUNKS = "ftbchunks", MODID_JAVD = "javd", MODID_FTB_TEAMS = "ftbteams", - MODID_ARGONAUTS = "argonauts"; + MODID_ARGONAUTS = "argonauts", + MODID_ATTRIBUTEFIX = "attributefix"; /** * Spray painting compat modids diff --git a/src/main/java/com/gregtechceu/gtceu/api/gui/GuiTextures.java b/src/main/java/com/gregtechceu/gtceu/api/gui/GuiTextures.java index da936382169..aead2a472a2 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/gui/GuiTextures.java +++ b/src/main/java/com/gregtechceu/gtceu/api/gui/GuiTextures.java @@ -1,5 +1,7 @@ package com.gregtechceu.gtceu.api.gui; +import com.gregtechceu.gtceu.GTCEu; + import com.lowdragmc.lowdraglib.gui.texture.ResourceBorderTexture; import com.lowdragmc.lowdraglib.gui.texture.ResourceTexture; @@ -193,6 +195,8 @@ public class GuiTextures { "gtceu:textures/block/overlay/machine/overlay_maintenance.png"); public static final ResourceTexture BUTTON_MINER_MODES = new ResourceTexture( "gtceu:textures/gui/widget/button_miner_modes.png"); + public static final ResourceTexture EQUIPMENT_FOUNDRY_BACKGROUND = new ResourceTexture( + GTCEu.id("textures/gui/widget/equipment_foundry_background.png")); // ORE PROCESSING public static final ResourceTexture OREBY_BASE = new ResourceTexture("gtceu:textures/gui/arrows/oreby-base.png"); diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorComponentItem.java b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorComponentItem.java index 407a2e6fd58..4edd77d3bc4 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorComponentItem.java +++ b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorComponentItem.java @@ -1,9 +1,12 @@ package com.gregtechceu.gtceu.api.item.armor; import com.gregtechceu.gtceu.api.item.IComponentItem; +import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; import com.gregtechceu.gtceu.api.item.component.*; import com.gregtechceu.gtceu.api.item.component.forge.IComponentCapability; +import lombok.Setter; +import lombok.experimental.Accessors; import net.minecraft.client.model.HumanoidModel; import net.minecraft.core.NonNullList; import net.minecraft.network.chat.Component; @@ -35,10 +38,9 @@ import java.util.List; import java.util.function.Consumer; +@Accessors(chain = true) public class ArmorComponentItem extends ArmorItem implements IComponentItem { - @Getter - private int maxModifiers = 1; @Getter private IArmorLogic armorLogic = new DummyArmorLogic(); @Getter @@ -83,9 +85,12 @@ public EquipmentSlot getEquipmentSlot() { } @Override - public void onArmorTick(ItemStack stack, Level level, Player player) { - super.onArmorTick(stack, level, player); - this.armorLogic.onArmorTick(level, player, stack); + public void onInventoryTick(ItemStack stack, Level level, Player player, int slotIndex, int selectedIndex) { + super.onInventoryTick(stack, level, player, slotIndex, selectedIndex); + // if index >= 36, the item is in an armor slot + if (slotIndex >= 36) { + //this.armorLogic.onArmorTick(level, player, stack); + } } @Override @@ -153,13 +158,16 @@ public void fillItemCategory(CreativeModeTab category, NonNullList it } @Override - public void appendHoverText(ItemStack stack, @Nullable Level level, List tooltipComponents, + public void appendHoverText(ItemStack stack, @Nullable Level level, List tooltips, TooltipFlag isAdvanced) { for (IItemComponent component : components) { if (component instanceof IAddInformation addInformation) { - addInformation.appendHoverText(stack, level, tooltipComponents, isAdvanced); + addInformation.appendHoverText(stack, level, tooltips, isAdvanced); } } + for (ArmorModifier modifier : ArmorUtils.getModifiers(stack)) { + modifier.tooltips().accept(stack, tooltips); + } } @Override diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorLogicSuite.java b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorLogicSuite.java index bba78688d91..136e7b4ed86 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorLogicSuite.java +++ b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorLogicSuite.java @@ -87,7 +87,7 @@ public InteractionResultHolder use(Item item, Level level, Player pla @Override public void appendHoverText(ItemStack stack, @Nullable Level level, List tooltipComponents, TooltipFlag isAdvanced) { - addInfo(stack, tooltipComponents); + //addInfo(stack, tooltipComponents); } }); } diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorUtils.java b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorUtils.java index 577f6ecdc56..e1be97d3e3e 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorUtils.java +++ b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorUtils.java @@ -1,17 +1,25 @@ package com.gregtechceu.gtceu.api.item.armor; +import com.gregtechceu.gtceu.GTCEu; import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper; import com.gregtechceu.gtceu.api.capability.IElectricItem; +import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; import com.gregtechceu.gtceu.common.data.GTSoundEntries; import com.gregtechceu.gtceu.config.ConfigHolder; import com.gregtechceu.gtceu.core.mixins.ServerGamePacketListenerImplAccessor; +import com.gregtechceu.gtceu.data.recipe.CustomTags; import com.gregtechceu.gtceu.utils.ItemStackHashStrategy; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.core.NonNullList; import net.minecraft.core.particles.ParticleOptions; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.nbt.Tag; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.Mth; import net.minecraft.world.InteractionResultHolder; @@ -27,6 +35,8 @@ import com.mojang.datafixers.util.Pair; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenCustomHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; import java.text.DecimalFormat; import java.util.ArrayList; @@ -34,6 +44,7 @@ import java.util.List; import javax.annotation.Nonnull; +import javax.annotation.Nullable; public class ArmorUtils { @@ -41,6 +52,94 @@ public class ArmorUtils { public static final int NIGHTVISION_DURATION = 20 * 20; // 20 seconds public static final int NIGHT_VISION_RESET = 11 * 20; // 11 seconds is before the flashing + public static final String ARMOR_KEY = "GT.Armor"; + public static final String MODIFIERS_KEY = "Modifiers"; + public static final String MAX_MODIFIERS_KEY = "MaxModifiers"; + + public static boolean isModifiable(ItemStack stack) { + return stack.is(CustomTags.MODIFIABLE_EQUIPMENT); + } + + public static boolean hasArmorTag(ItemStack stack) { + return isModifiable(stack) && stack.getTagElement(ARMOR_KEY) != null; + } + + @Nullable + public static CompoundTag getArmorTag(ItemStack stack) { + if (!isModifiable(stack)) return null; + return stack.getOrCreateTagElement(ARMOR_KEY); + } + + /** + * @param stack the stack to get maximum modifier amount for + * @return the maximum amount of modifiers for the given stack + */ + public static int getMaxModifiers(ItemStack stack) { + if (!(hasArmorTag(stack) + && getArmorTag(stack).contains(MAX_MODIFIERS_KEY, Tag.TAG_INT)) + && stack.getItem() instanceof ModifiableArmorItem armorComponentItem) { + setMaxModifiers(stack, armorComponentItem.getDefaultMaxModifiers()); + return armorComponentItem.getDefaultMaxModifiers(); + } else if (!hasArmorTag(stack)) { + return 0; + } + return getArmorTag(stack).getInt(MAX_MODIFIERS_KEY); + } + + public static void setMaxModifiers(ItemStack stack, int maxModifiers) { + if (!isModifiable(stack)) return; + getArmorTag(stack).putInt(MAX_MODIFIERS_KEY, maxModifiers); + } + + /** + * Clear all modifiers from the given piece of armor + * @param stack the armor to remove all modifiers from + */ + public static void clearModifiers(ItemStack stack) { + if (!hasArmorTag(stack)) return; + CompoundTag tag = getArmorTag(stack); + tag.remove(MODIFIERS_KEY); + } + + /** + * Add an armor modifier to the given stack + * @param stack the stack to add the modifier to, if both are valid + * @param modifier the modifier to add to the stack + */ + public static void addModifier(ItemStack stack, ArmorModifier modifier) { + CompoundTag tag = getArmorTag(stack); + ListTag modifierList = tag.getList(MODIFIERS_KEY, Tag.TAG_STRING); + if (modifierList.size() >= getMaxModifiers(stack)) return; + + modifierList.add(StringTag.valueOf(modifier.id().toString())); + modifier.onAddToItem().apply(stack); + tag.put(MODIFIERS_KEY, modifierList); + } + + /** + * An unmodifiable list of all modifiers on the given stack + * @param stack the stack to get the modifiers from + * @return the modifiers on the stack + */ + @Unmodifiable + public static @NotNull List getModifiers(ItemStack stack) { + if (!hasArmorTag(stack)) return Collections.emptyList(); + CompoundTag tag = getArmorTag(stack); + ListTag modifierList = tag.getList(MODIFIERS_KEY, Tag.TAG_STRING); + + List modifiers = new ArrayList<>(); + for (int i = 0; i < modifierList.size(); i++) { + String idString = modifierList.getString(i); + ResourceLocation id = ResourceLocation.tryParse(idString); + if (id == null) { + GTCEu.LOGGER.error("invalid armor modifier with id {}", idString); + continue; + } + modifiers.add(ArmorModifier.MODIFIERS.get(id)); + } + return modifiers; + } + /** * Check is possible to charge item */ diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ModifiableArmorItem.java b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ModifiableArmorItem.java new file mode 100644 index 00000000000..b47d2d44952 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ModifiableArmorItem.java @@ -0,0 +1,295 @@ +package com.gregtechceu.gtceu.api.item.armor; + +import com.gregtechceu.gtceu.api.item.IComponentItem; +import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; +import com.gregtechceu.gtceu.api.item.component.*; +import com.gregtechceu.gtceu.api.item.component.forge.IComponentCapability; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import net.minecraft.MethodsReturnNonnullByDefault; +import net.minecraft.client.model.HumanoidModel; +import net.minecraft.core.NonNullList; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.InteractionResultHolder; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.*; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraftforge.client.extensions.common.IClientItemExtensions; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.util.LazyOptional; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +@ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault +@Accessors(chain = true) +public class ModifiableArmorItem extends ArmorItem implements IComponentItem { + + @Getter + @Setter + private int defaultMaxModifiers; + @Getter + protected List components = new ArrayList<>(); + + + public ModifiableArmorItem(ArmorMaterial material, Type type, Properties properties) { + super(material, type, properties); + } + + @Override + public void attachComponents(IItemComponent... components) { + this.components.addAll(Arrays.asList(components)); + for (IItemComponent component : components) { + component.onAttached(this); + } + } + + + @Override + public int getMaxDamage(ItemStack stack) { + return super.getMaxDamage(stack); + } + + @Override + public boolean isValidRepairItem(ItemStack stack, ItemStack repairCandidate) { + return false; + } + + @Override + public boolean isEnchantable(ItemStack stack) { + return true; + } + + @Override + public int getEnchantmentValue() { + return 50; + } + + @Override + public void initializeClient(Consumer consumer) { + consumer.accept(new IClientItemExtensions() { + + @Override + public @NotNull HumanoidModel getHumanoidArmorModel(LivingEntity livingEntity, ItemStack itemStack, + EquipmentSlot equipmentSlot, + HumanoidModel original) { + // TODO modifiable armor model + //return armorLogic.getArmorModel(livingEntity, itemStack, equipmentSlot, original); + return original; + } + }); + } + + @Nullable + @Override + public String getArmorTexture(ItemStack stack, Entity entity, EquipmentSlot slot, String type) { + // TODO add custom texture logic (or not? do we need it?) + //return armorLogic.getArmorTexture(stack, entity, slot, type).toString(); + return null; + } + + /////////////////////////////////////////// + ///// ALL component item things /////// + /////////////////////////////////////////// + + public void fillItemCategory(CreativeModeTab category, NonNullList items) { + boolean found = false; + for (IItemComponent component : components) { + if (component instanceof ISubItemHandler subItemHandler) { + subItemHandler.fillItemCategory(this, category, items); + found = true; + } + } + if (found) return; + items.add(new ItemStack(this)); + } + + @Override + public void appendHoverText(ItemStack stack, @Nullable Level level, List tooltips, + TooltipFlag isAdvanced) { + for (IItemComponent component : components) { + if (component instanceof IAddInformation addInformation) { + addInformation.appendHoverText(stack, level, tooltips, isAdvanced); + } + } + for (ArmorModifier modifier : ArmorUtils.getModifiers(stack)) { + modifier.tooltips().accept(stack, tooltips); + } + } + + @Override + public boolean isBarVisible(ItemStack stack) { + for (IItemComponent component : components) { + if (component instanceof IDurabilityBar durabilityBar) { + return durabilityBar.isBarVisible(stack); + } + } + return super.isBarVisible(stack); + } + + @Override + public int getBarWidth(ItemStack stack) { + for (IItemComponent component : components) { + if (component instanceof IDurabilityBar durabilityBar) { + return durabilityBar.getBarWidth(stack); + } + } + return super.getBarWidth(stack); + } + + @Override + public int getBarColor(ItemStack stack) { + for (IItemComponent component : components) { + if (component instanceof IDurabilityBar durabilityBar) { + return durabilityBar.getBarColor(stack); + } + } + return super.getBarColor(stack); + } + + @Override + public InteractionResult useOn(UseOnContext context) { + for (IItemComponent component : components) { + if (component instanceof IInteractionItem interactionItem) { + var result = interactionItem.useOn(context); + if (result != InteractionResult.PASS) { + return result; + } + } + } + return super.useOn(context); + } + + @Override + public InteractionResultHolder use(Level level, Player player, InteractionHand usedHand) { + for (IItemComponent component : components) { + if (component instanceof IInteractionItem interactionItem) { + var result = interactionItem.use(this, level, player, usedHand); + if (result.getResult() != InteractionResult.PASS) { + return result; + } + } + } + return super.use(level, player, usedHand); + } + + @Override + public ItemStack finishUsingItem(ItemStack stack, Level level, LivingEntity livingEntity) { + for (IItemComponent component : components) { + if (component instanceof IInteractionItem interactionItem) { + stack = interactionItem.finishUsingItem(stack, level, livingEntity); + } + } + return super.finishUsingItem(stack, level, livingEntity); + } + + @Override + public InteractionResult onItemUseFirst(ItemStack itemStack, UseOnContext context) { + for (IItemComponent component : components) { + if (component instanceof IInteractionItem interactionItem) { + var result = interactionItem.onItemUseFirst(itemStack, context); + if (result != InteractionResult.PASS) { + return result; + } + } + } + return InteractionResult.PASS; + } + + @Override + public InteractionResult interactLivingEntity(ItemStack stack, Player player, LivingEntity interactionTarget, + InteractionHand usedHand) { + for (IItemComponent component : components) { + if (component instanceof IInteractionItem interactionItem) { + var result = interactionItem.interactLivingEntity(stack, player, interactionTarget, usedHand); + if (result != InteractionResult.PASS) { + return result; + } + } + } + return InteractionResult.PASS; + } + + @Override + public Component getName(ItemStack stack) { + for (IItemComponent component : components) { + if (component instanceof ICustomDescriptionId customDescriptionId) { + Component name = customDescriptionId.getItemName(stack); + if (name != null) { + return name; + } + } + } + return super.getName(stack); + } + + @Override + public String getDescriptionId(ItemStack stack) { + for (IItemComponent component : components) { + if (component instanceof ICustomDescriptionId customDescriptionId) { + String langId = customDescriptionId.getItemDescriptionId(stack); + if (langId != null) { + return langId; + } + } + } + return super.getDescriptionId(stack); + } + + @Override + public void inventoryTick(ItemStack stack, Level level, Entity entity, int slotId, boolean isSelected) { + for (IItemComponent component : components) { + if (component instanceof IItemLifeCycle lifeCycle) { + lifeCycle.inventoryTick(stack, level, entity, slotId, isSelected); + } + } + } + + @Override + public ItemStack getCraftingRemainingItem(ItemStack itemStack) { + for (IItemComponent component : components) { + if (component instanceof IRecipeRemainder recipeRemainder) { + return recipeRemainder.getRecipeRemained(itemStack); + } + } + return super.getCraftingRemainingItem(itemStack); + } + + @Override + public boolean hasCraftingRemainingItem(ItemStack stack) { + for (IItemComponent component : components) { + if (component instanceof IRecipeRemainder recipeRemainder) { + return recipeRemainder.getRecipeRemained(stack) != ItemStack.EMPTY; + } + } + return super.hasCraftingRemainingItem(stack); + } + + @Override + public LazyOptional getCapability(@NotNull final ItemStack itemStack, @NotNull final Capability cap) { + for (IItemComponent component : components) { + if (component instanceof IComponentCapability componentCapability) { + var value = componentCapability.getCapability(itemStack, cap); + if (value.isPresent()) { + return value; + } + } + } + return LazyOptional.empty(); + } + +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/armor/modifier/ArmorModifier.java b/src/main/java/com/gregtechceu/gtceu/api/item/armor/modifier/ArmorModifier.java index dd99a532191..93ef458cffa 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/item/armor/modifier/ArmorModifier.java +++ b/src/main/java/com/gregtechceu/gtceu/api/item/armor/modifier/ArmorModifier.java @@ -1,6 +1,11 @@ package com.gregtechceu.gtceu.api.item.armor.modifier; +import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper; +import com.gregtechceu.gtceu.api.capability.IElectricItem; + +import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.ai.attributes.Attribute; @@ -9,58 +14,206 @@ import com.mojang.serialization.Codec; import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiPredicate; +@SuppressWarnings("FieldMayBeFinal") +@Accessors(chain = true, fluent = true) public class ArmorModifier { public static final Map MODIFIERS = new HashMap<>(); public static final Codec CODEC = ResourceLocation.CODEC.xmap(ArmorModifier.MODIFIERS::get, - ArmorModifier::getId); + ArmorModifier::id); @Getter - public final ResourceLocation id; + private final ResourceLocation id; + @Getter + private ItemModifier onAddToItem; + @Getter + private Modifier onEquip; @Getter - public final ItemModifier onAddToItem; + private Modifier onTick; + @Getter + private Modifier onUnequip; + @Getter - public final Modifier onAdd; + @Setter + private DamageModifier onDamage = DamageModifier.NONE; @Getter - public final Modifier onRemove; + @Setter + private BiConsumer> tooltips = (stack, tooltips) -> {}; - public ArmorModifier(ResourceLocation id, Modifier onAdd, Modifier onRemove) { + protected ArmorModifier(ResourceLocation id, ItemModifier onAddToItem, + Modifier onEquip, Modifier onTick, Modifier onUnequip) { this.id = id; - this.onAddToItem = ItemModifier.IDENTITY; - this.onAdd = onAdd; - this.onRemove = onRemove; + this.onAddToItem = onAddToItem; + this.onEquip = onEquip; + this.onTick = onTick; + this.onUnequip = onUnequip; + MODIFIERS.put(id, this); } - public ArmorModifier(ResourceLocation id, Attribute attribute, AttributeModifier modifier, EquipmentSlot slot) { - this.id = id; - this.onAddToItem = (stack) -> { - if (stack.getAttributeModifiers(slot).containsEntry(attribute, modifier)) { - return; + public ArmorModifier energyUsagePerTick(long energyUsage) { + return this.energyUsagePerTick(energyUsage, (entity, itemStack) -> true); + } + + public ArmorModifier energyUsagePerTick(long energyUsage, BiPredicate doDrain) { + this.onTick = this.onTick.compose((entity, stack) -> { + if (!doDrain.test(entity, stack)) { + return true; + } + IElectricItem electricItem = GTCapabilityHelper.getElectricItem(stack); + if (electricItem != null) { + if (!electricItem.canUse(energyUsage)) { + return false; + } + electricItem.discharge(energyUsage, electricItem.getTier(), true, false, false); + } + return true; + }); + return this; + } + + public ArmorModifier energyUsageOnHit(long energyUsage) { + this.onDamage = this.onDamage.compose((entity, stack, source, amount) -> { + IElectricItem electricItem = GTCapabilityHelper.getElectricItem(stack); + if (electricItem != null) { + if (!electricItem.canUse(energyUsage)) { + return new DamageModifier.Result(amount, false); + } + electricItem.discharge(energyUsage, electricItem.getTier(), true, false, false); } - stack.addAttributeModifier(attribute, modifier, slot); - }; - this.onAdd = Modifier.IDENTITY; - this.onRemove = Modifier.IDENTITY; + return new DamageModifier.Result(amount); + }); + return this; + } + + public static ArmorModifier createItem(ResourceLocation id, ItemModifier modifier) { + return new ArmorModifier(id, modifier, Modifier.NONE, Modifier.NONE, Modifier.NONE); + } + + public static ArmorModifier createItemAttribute(ResourceLocation id, + Attribute attribute, AttributeModifier modifier, + @Nullable EquipmentSlot slot) { + return createItem(id, (stack) -> { + EquipmentSlot slot1 = slot != null ? slot : LivingEntity.getEquipmentSlotForItem(stack); + stack.addAttributeModifier(attribute, modifier, slot1); + }); + } + + public static ArmorModifier createEntity(ResourceLocation id, + Modifier onEquip, Modifier onTick, Modifier onUnequip) { + return new ArmorModifier(id, ItemModifier.NONE, onEquip, onTick, onUnequip); + } + + public static ArmorModifier createEntityTick(ResourceLocation id, Modifier onTick) { + return new ArmorModifier(id, ItemModifier.NONE, Modifier.NONE, onTick, Modifier.NONE); + } + + public static ArmorModifier createEntityAttribute(ResourceLocation id, + Attribute attribute, AttributeModifier modifier) { + return createEntity(id, (entity, stack) -> { + if (entity.getAttribute(attribute) == null) return true; + entity.getAttribute(attribute).addPermanentModifier(modifier); + return true; + }, Modifier.NONE, (entity, stack) -> { + if (entity.getAttribute(attribute) == null) return true; + entity.getAttribute(attribute).removeModifier(modifier); + return true; + }); + } + + public static ArmorModifier createAll(ResourceLocation id, ItemModifier onAddToItem, + Modifier onEquip, Modifier onTick, Modifier onUnequip) { + return new ArmorModifier(id, onAddToItem, onEquip, onTick, onUnequip); + } + + public static ArmorModifier createSpecial(ResourceLocation id) { + return new ArmorModifier(id, ItemModifier.NONE, Modifier.NONE, Modifier.NONE, Modifier.NONE); } @FunctionalInterface public interface Modifier { - Modifier IDENTITY = (stack, entity) -> {}; + Modifier NONE = (entity, stack) -> true; + + boolean apply(@NotNull LivingEntity entity, @NotNull ItemStack stack); + + default Modifier andThen(Modifier after) { + return (entity, stack) -> { + if (this.apply(entity, stack)) { + return after.apply(entity, stack); + } + return false; + }; + } - void apply(ItemStack stack, @Nullable LivingEntity entity); + default Modifier compose(Modifier before) { + return (entity, stack) -> { + if (before.apply(entity, stack)) { + return this.apply(entity, stack); + } + return false; + }; + } } @FunctionalInterface public interface ItemModifier { - ItemModifier IDENTITY = (stack) -> {}; + ItemModifier NONE = (stack) -> {}; void apply(ItemStack stack); + + default ItemModifier andThen(ItemModifier after) { + return (stack) -> { + this.apply(stack); + after.apply(stack); + }; + } + } + + @FunctionalInterface + public interface DamageModifier { + + DamageModifier NONE = (entity, stack, source, amount) -> new Result(amount); + + Result apply(@NotNull LivingEntity entity, @NotNull ItemStack stack, + @NotNull DamageSource source, float amount); + + default DamageModifier compose(DamageModifier before) { + return (entity, stack, source, amount) -> { + var result = before.apply(entity, stack, source, amount); + if (!result.doApplyNext) { + return result; + } + return this.apply(entity, stack, source, result.newAmount); + }; + } + + default DamageModifier andThen(DamageModifier after) { + return (entity, stack, source, amount) -> { + var result = this.apply(entity, stack, source, amount); + if (!result.doApplyNext) { + return result; + } + return after.apply(entity, stack, source, result.newAmount); + }; + } + + record Result(float newAmount, boolean doApplyNext) { + + public Result(float newAmount) { + this(newAmount, true); + } + } } } diff --git a/src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java b/src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java index 985142e0de4..775a65bf298 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java +++ b/src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java @@ -35,6 +35,7 @@ import com.gregtechceu.gtceu.common.unification.material.MaterialRegistryManager; import com.gregtechceu.gtceu.config.ConfigHolder; import com.gregtechceu.gtceu.core.mixins.AbstractRegistrateAccessor; +import com.gregtechceu.gtceu.core.mixins.RangedAttributeAccessor; import com.gregtechceu.gtceu.data.GregTechDatagen; import com.gregtechceu.gtceu.data.lang.MaterialLangGenerator; import com.gregtechceu.gtceu.data.loot.ChestGenHooks; @@ -57,6 +58,7 @@ import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.server.packs.PackType; import net.minecraft.server.packs.repository.Pack; +import net.minecraft.world.entity.ai.attributes.Attributes; import net.minecraftforge.common.capabilities.RegisterCapabilitiesEvent; import net.minecraftforge.common.crafting.CraftingHelper; import net.minecraftforge.event.AddPackFindersEvent; @@ -133,6 +135,7 @@ public static void init() { GTMachines.init(); GTFoods.init(); + GTArmorModifiers.init(); GTItems.init(); GTDimensionMarkers.init(); ChanceLogic.init(); @@ -247,6 +250,13 @@ public void loadComplete(FMLLoadCompleteEvent e) { GTCEu.LOGGER.info("TheOneProbe found. Enabling integration..."); TheOneProbePluginImpl.init(); } + + // don't modify attribute maximums if AttributeFix is loaded to not conflict with its changes. + if (!LDLib.isModLoaded(GTValues.MODID_ATTRIBUTEFIX)) { + ((RangedAttributeAccessor) Attributes.ARMOR).gtceu$setMaxValue(1024.0D); + ((RangedAttributeAccessor) Attributes.ARMOR_TOUGHNESS).gtceu$setMaxValue(1024.0D); + ((RangedAttributeAccessor) Attributes.ATTACK_KNOCKBACK).gtceu$setMaxValue(1024.0D); + } }); } diff --git a/src/main/java/com/gregtechceu/gtceu/common/block/EquipmentFoundryBlock.java b/src/main/java/com/gregtechceu/gtceu/common/block/EquipmentFoundryBlock.java index 477eb6f35e5..1d43cfc655b 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/block/EquipmentFoundryBlock.java +++ b/src/main/java/com/gregtechceu/gtceu/common/block/EquipmentFoundryBlock.java @@ -5,6 +5,7 @@ import com.lowdragmc.lowdraglib.gui.factory.BlockEntityUIFactory; +import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.core.BlockPos; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.InteractionHand; @@ -12,6 +13,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.BlockHitResult; @@ -23,12 +25,18 @@ @SuppressWarnings("deprecation") @ParametersAreNonnullByDefault +@MethodsReturnNonnullByDefault public class EquipmentFoundryBlock extends BaseEntityBlock { public EquipmentFoundryBlock(Properties properties) { super(properties); } + @Override + public RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + @Override public @Nullable BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return GTBlockEntities.EQUIPMENT_FOUNDRY.get().create(pos, state); diff --git a/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java b/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java index da2de7af95a..3a1247d229a 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java +++ b/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java @@ -2,14 +2,21 @@ import com.gregtechceu.gtceu.api.gui.GuiTextures; import com.gregtechceu.gtceu.api.gui.UITemplate; +import com.gregtechceu.gtceu.api.gui.widget.BlockableSlotWidget; +import com.gregtechceu.gtceu.api.gui.widget.SlotWidget; import com.gregtechceu.gtceu.api.item.armor.ArmorComponentItem; +import com.gregtechceu.gtceu.api.item.armor.ArmorUtils; import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler; -import com.gregtechceu.gtceu.common.gui.widget.EquipmentFoundryBaseWidget; +import com.gregtechceu.gtceu.common.data.GTRecipeTypes; +import com.gregtechceu.gtceu.common.recipe.type.EquipmentFoundryRecipe; import com.gregtechceu.gtceu.data.recipe.CustomTags; import com.lowdragmc.lowdraglib.gui.modular.IUIHolder; import com.lowdragmc.lowdraglib.gui.modular.ModularUI; -import com.lowdragmc.lowdraglib.gui.widget.LabelWidget; +import com.lowdragmc.lowdraglib.gui.texture.IGuiTexture; +import com.lowdragmc.lowdraglib.gui.texture.TextTexture; +import com.lowdragmc.lowdraglib.gui.widget.ImageWidget; +import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; import com.lowdragmc.lowdraglib.syncdata.IManaged; import com.lowdragmc.lowdraglib.syncdata.IManagedStorage; import com.lowdragmc.lowdraglib.syncdata.annotation.DescSynced; @@ -22,10 +29,14 @@ import com.lowdragmc.lowdraglib.syncdata.field.ManagedFieldHolder; import net.minecraft.core.BlockPos; +import net.minecraft.core.NonNullList; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import net.minecraftforge.items.wrapper.CombinedInvWrapper; +import net.minecraftforge.items.wrapper.RecipeWrapper; import lombok.Getter; @@ -50,22 +61,139 @@ public EquipmentFoundryBlockEntity(BlockEntityType type, BlockPos pos, BlockS super(type, pos, blockState); this.equipmentSlot = new CustomItemStackHandler(1); // TODO remove instanceof once datagen can run - this.equipmentSlot.setFilter( - stack -> stack.is(CustomTags.MODIFIABLE_EQUIPMENT) || stack.getItem() instanceof ArmorComponentItem); - this.modifierSlots = new CustomItemStackHandler(MAX_MODIFIER_SLOTS); + this.equipmentSlot.setFilter(stack -> stack.is(CustomTags.MODIFIABLE_EQUIPMENT)); + + this.modifierSlots = new CustomItemStackHandler(MAX_MODIFIER_SLOTS) { + @Override + public int getSlotLimit(int slot) { + return 1; + } + }; + + this.modifierSlots.setFilter(stack -> { + if (this.getLevel() == null) { + return false; + } + NonNullList stacks = NonNullList.create(); + int firstEmptyIndex = -1; + + stacks.add(this.equipmentSlot.getStackInSlot(0)); + for (int i = 0; i < modifierSlots.getSlots(); i++) { + ItemStack stack1 = modifierSlots.getStackInSlot(i); + stacks.add(stack1); + if (stack1.isEmpty() && firstEmptyIndex == -1) { + firstEmptyIndex = i + 1; + } + } + if (firstEmptyIndex >= 0) { + stacks.set(firstEmptyIndex, stack); + } + RecipeWrapper newWrapper = new RecipeWrapper(new CustomItemStackHandler(stacks)); + + return getLevel().getRecipeManager() + .getRecipeFor(GTRecipeTypes.EQUIPMENT_FOUNDRY_RECIPES.get(), newWrapper, this.getLevel()) + .isPresent(); + }); } @Override public ModularUI createUI(Player entityPlayer) { ModularUI modularUI = new ModularUI(176, 166, this, entityPlayer); - modularUI.background(GuiTextures.BACKGROUND); - modularUI.widget(new LabelWidget(5, 5, getBlockState().getBlock().getDescriptionId())); - modularUI.widget(new EquipmentFoundryBaseWidget(5, 10, 176, 166, - equipmentSlot, modifierSlots)); - modularUI.widget(UITemplate.bindPlayerInventory(entityPlayer.getInventory(), GuiTextures.SLOT, 7, 84, true)); + modularUI.background(GuiTextures.BACKGROUND.copy().setColor(0xff69645f)); + + IGuiTexture slotTexture = null;//GuiTextures.SLOT.copy().setColor(0xff69645f); + + TextTexture titleText = new TextTexture(getBlockState().getBlock().getDescriptionId()) + .setColor(0xffffff) + .setDropShadow(false) + .setType(TextTexture.TextType.ROLL) + .setWidth(105); + titleText.setRollSpeed(0.7f); + modularUI.widget(new WidgetGroup(9, -16, 160, 16) + .addWidget(new ImageWidget( + 16, 2, 105, 16, titleText)) + .setBackground(GuiTextures.TITLE_BAR_BACKGROUND.copy().setColor(0xff69645f))); + modularUI.widget(new ImageWidget(4, 4, 168, 75, GuiTextures.EQUIPMENT_FOUNDRY_BACKGROUND)); + + modularUI.widget(new SlotWidget(equipmentSlot, 0, 14, 32) + .setChangeListener(() -> this.onEquipmentSlotChanged(entityPlayer)) + .setBackgroundTexture(slotTexture)); + + int x = 42; + int y = 13; + for (int i = 0; i < MAX_MODIFIER_SLOTS; i++) { + final int finalI = i; + modularUI.widget(new BlockableSlotWidget(modifierSlots, i, x, y) + .setIsBlocked(() -> isModifierSlotBlocked(finalI)) + .setChangeListener(this::onModifierSlotChanged) + .setBackgroundTexture(slotTexture)); + x += 26; + if (i == 4) { + x = 42; + y = 52; + } + } + modularUI.widget(UITemplate.bindPlayerInventory(entityPlayer.getInventory(), slotTexture, 7, 84, true)); return modularUI; } + public boolean isModifierSlotBlocked(int slot) { + ItemStack equipment = equipmentSlot.getStackInSlot(0); + return ArmorUtils.getMaxModifiers(equipment) <= slot; + } + + public void onEquipmentSlotChanged(Player player) { + ItemStack stack = equipmentSlot.getStackInSlot(0); + if (stack.isEmpty()) { + for (int i = 0; i < modifierSlots.getSlots(); i++) { + ItemStack out = modifierSlots.extractItem(i, Integer.MAX_VALUE, true); + if (out.isEmpty()) { + continue; + } + out = modifierSlots.extractItem(i, Integer.MAX_VALUE, false); + out.shrink(1); + if (!player.getInventory().add(out)) { + player.drop(out, true); + } + } + } + } + + public void onModifierSlotChanged() { + if (level.isClientSide) { + return; + } + + ItemStack stack = equipmentSlot.getStackInSlot(0); + if (stack.isEmpty()) { + return; + } + + + ArmorUtils.clearModifiers(stack); + for (int i = 0; i < modifierSlots.getSlots(); i++) { + ItemStack modifier = modifierSlots.getStackInSlot(i); + if (modifier.isEmpty()) { + continue; + } + + CustomItemStackHandler handler = new CustomItemStackHandler(modifier); + RecipeWrapper recipeWrapper = new RecipeWrapper( + new CombinedInvWrapper(this.equipmentSlot, handler)); + + var maybeRecipe = getLevel().getRecipeManager() + .getRecipeFor(GTRecipeTypes.EQUIPMENT_FOUNDRY_RECIPES.get(), recipeWrapper, this.getLevel()); + if (maybeRecipe.isPresent()) { + EquipmentFoundryRecipe recipe = maybeRecipe.get(); + ItemStack newStack = recipe.assemble(recipeWrapper, level.registryAccess()); + if (newStack.isEmpty()) { + continue; + } + equipmentSlot.setStackInSlot(0, newStack); + } + } + } + @Override public boolean isInvalid() { return isRemoved(); diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTArmorModifiers.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTArmorModifiers.java new file mode 100644 index 00000000000..a5ec9b13bf5 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTArmorModifiers.java @@ -0,0 +1,113 @@ +package com.gregtechceu.gtceu.common.data; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.capability.GTCapabilityHelper; +import com.gregtechceu.gtceu.api.capability.IElectricItem; +import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; +import com.gregtechceu.gtceu.core.IFireImmuneEntity; + +import com.gregtechceu.gtceu.utils.input.KeyBind; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.network.chat.Component; +import net.minecraft.tags.DamageTypeTags; +import net.minecraft.world.damagesource.DamageTypes; +import net.minecraft.world.entity.ai.attributes.AttributeModifier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; + +import java.util.UUID; + +@Slf4j +public class GTArmorModifiers { + + private static final double SPEED_ACCEL = 0.085D; + + + private static final UUID ADD_ARMOR_UUID = UUID.fromString("95bd81ea-b3af-4cca-8866-f3e62f5f68f1"); + + public static final ArmorModifier ADD_ARMOR_1 = ArmorModifier.createItemAttribute(GTCEu.id("add_armor_1"), + Attributes.ARMOR, + new AttributeModifier(ADD_ARMOR_UUID, "Armor Modifier", 1.0D, AttributeModifier.Operation.ADDITION), + null) + .energyUsageOnHit(1024); + public static final ArmorModifier ADD_ARMOR_2 = ArmorModifier.createItemAttribute(GTCEu.id("add_armor_2"), + Attributes.ARMOR, + new AttributeModifier(ADD_ARMOR_UUID, "Armor Modifier", 2.0D, AttributeModifier.Operation.ADDITION), + null) + .energyUsageOnHit(2048); + public static final ArmorModifier ADD_ARMOR_5 = ArmorModifier.createItemAttribute(GTCEu.id("add_armor_5"), + Attributes.ARMOR, + new AttributeModifier(ADD_ARMOR_UUID, "Armor Modifier", 5.0D, AttributeModifier.Operation.ADDITION), + null) + .energyUsageOnHit(5120); + public static final ArmorModifier SPEED = ArmorModifier.createEntityTick(GTCEu.id("speed"), + (entity, stack) -> { + if (entity instanceof Player player) { + boolean sprinting = KeyBind.VANILLA_FORWARD.isKeyDown(player) && player.isSprinting(); + boolean jumping = KeyBind.VANILLA_JUMP.isKeyDown(player); + boolean sneaking = KeyBind.VANILLA_SNEAK.isKeyDown(player); + + if ((player.onGround() || player.isInWater()) && sprinting) { + float speed = 0.25F; + if (player.isInWater()) { + speed = 0.1F; + if (jumping) { + player.push(0.0, 0.1, 0.0); + } + } + player.moveRelative(speed, new Vec3(0, 0, 1)); + } else if (player.isInWater() && (sneaking || jumping)) { + if (sneaking) + player.push(0.0, -SPEED_ACCEL, 0.0); + if (jumping) + player.push(0.0, SPEED_ACCEL, 0.0); + } + } + return true; + }) + .energyUsagePerTick(819, (entity, itemStack) -> { + if (entity instanceof Player player) { + return KeyBind.VANILLA_FORWARD.isKeyDown(player) && player.isSprinting(); + } + return false; + }) + .tooltips((stack, tooltips) -> { + tooltips.add(Component.translatable("metaarmor.tooltip.speed")); + }); + public static final ArmorModifier FIRE_PROTECTION = ArmorModifier.createEntity(GTCEu.id("fire_protection"), + (entity, stack) -> { + if (!entity.level().isClientSide && !entity.fireImmune()) { + ((IFireImmuneEntity) entity).gtceu$setFireImmune(true); + if (entity.isOnFire()) entity.extinguishFire(); + } + return true; + }, + ArmorModifier.Modifier.NONE, + (entity, stack) -> { + if (!entity.level().isClientSide) { + ((IFireImmuneEntity) entity).gtceu$setFireImmune(false); + } + return true; + }); + public static final ArmorModifier DAMAGE_BLOCK = ArmorModifier.createSpecial(GTCEu.id("damage_block")) + .onDamage((entity, stack, source, amount) -> { + if (source.is(DamageTypeTags.BYPASSES_INVULNERABILITY) + || source.is(DamageTypeTags.IS_FALL) + || source.is(DamageTypeTags.IS_DROWNING) + || source.is(DamageTypes.STARVE)) { + return new ArmorModifier.DamageModifier.Result(amount); + } + + int damageLimit = Integer.MAX_VALUE; + IElectricItem electricItem = GTCapabilityHelper.getElectricItem(stack); + if (electricItem == null) { + return new ArmorModifier.DamageModifier.Result(amount); + } + damageLimit = (int) Math.min(damageLimit, 25.0D * electricItem.getCharge() / (8192 * 100.0D)); + return new ArmorModifier.DamageModifier.Result(Math.min(amount, damageLimit)); + }) + .energyUsageOnHit(8192); + + public static void init() {} +} diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTItems.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTItems.java index c18fe8bcbb8..35408f14920 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTItems.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTItems.java @@ -19,6 +19,7 @@ import com.gregtechceu.gtceu.api.item.IComponentItem; import com.gregtechceu.gtceu.api.item.TagPrefixItem; import com.gregtechceu.gtceu.api.item.armor.ArmorComponentItem; +import com.gregtechceu.gtceu.api.item.armor.ModifiableArmorItem; import com.gregtechceu.gtceu.api.item.component.*; import com.gregtechceu.gtceu.api.item.tool.MaterialToolTier; import com.gregtechceu.gtceu.common.data.materials.GTFoods; @@ -91,10 +92,12 @@ public class GTItems { ////////////////////////////////////// // ******* Misc Items ********// + ////////////////////////////////////// static { REGISTRATE.creativeModeTab(() -> ITEM); } + public static ItemEntry CREDIT_COPPER = REGISTRATE.item("copper_credit", Item::new).lang("Copper Credit") .register(); public static ItemEntry CREDIT_CUPRONICKEL = REGISTRATE.item("cupronickel_credit", Item::new) @@ -339,6 +342,7 @@ public class GTItems { .onRegister(materialInfo(new ItemMaterialInfo(new MaterialStack(GTMaterials.Steel, GTValues.M * 4)))) .register(); } + public static ItemEntry SPRAY_EMPTY = REGISTRATE.item("empty_spray_can", Item::new) .lang("Spray Can (Empty)").register(); public static ItemEntry SPRAY_SOLVENT = REGISTRATE.item("solvent_spray_can", ComponentItem::create) @@ -493,7 +497,7 @@ public Component getItemName(ItemStack stack) { .setData(ProviderType.ITEM_MODEL, NonNullBiConsumer.noop()) .onRegister(attach(new LighterBehavior(true, true, true))) .onRegister(attach(FilteredFluidContainer.create(100, true, - x -> x.getFluid().is(CustomTags.LIGHTER_FLUIDS)), + x -> x.getFluid().is(CustomTags.LIGHTER_FLUIDS)), new ItemFluidContainer())) .onRegister(modelPredicate(GTCEu.id("lighter_open"), (itemStack) -> itemStack.getOrCreateTag().getBoolean(LighterBehavior.LIGHTER_OPEN) ? 1.0f : 0.0f)) @@ -505,7 +509,7 @@ public Component getItemName(ItemStack stack) { .setData(ProviderType.ITEM_MODEL, NonNullBiConsumer.noop()) .onRegister(attach(new LighterBehavior(true, true, true))) .onRegister(attach(FilteredFluidContainer.create(1000, true, - x -> x.getFluid().is(CustomTags.LIGHTER_FLUIDS)), + x -> x.getFluid().is(CustomTags.LIGHTER_FLUIDS)), new ItemFluidContainer())) .onRegister(modelPredicate(GTCEu.id("lighter_open"), (itemStack) -> itemStack.getOrCreateTag().getBoolean(LighterBehavior.LIGHTER_OPEN) ? 1.0f : 0.0f)) @@ -1846,7 +1850,7 @@ public Component getItemName(ItemStack stack) { ///////////////////////////////////////// // *********** COVERS ***********// - ///////////////////////////////////////// + /// ////////////////////////////////////// public static ItemEntry ITEM_FILTER = REGISTRATE.item("item_filter", ComponentItem::create) .onRegister(attach(new ItemFilterBehaviour(SimpleItemFilter::loadFilter), @@ -2226,6 +2230,7 @@ public Component getItemName(ItemStack stack) { .register(); public static final ItemEntry[] DYE_ONLY_ITEMS = new ItemEntry[DyeColor.values().length]; + static { DyeColor[] colors = DyeColor.values(); for (int i = 0; i < colors.length; i++) { @@ -2238,6 +2243,7 @@ public Component getItemName(ItemStack stack) { } public static final ItemEntry[] SPRAY_CAN_DYES = new ItemEntry[DyeColor.values().length]; + static { for (int i = 0; i < DyeColor.values().length; i++) { var dyeColor = DyeColor.values()[i]; @@ -2321,48 +2327,34 @@ public Component getItemName(ItemStack stack) { .tag(Tags.Items.ARMORS_HELMETS) .register(); - public static ItemEntry NANO_CHESTPLATE = REGISTRATE + public static ItemEntry NANO_CHESTPLATE = REGISTRATE .item("nanomuscle_chestplate", - (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, ArmorItem.Type.CHESTPLATE, p) - .setArmorLogic(new NanoMuscleSuite(ArmorItem.Type.CHESTPLATE, - 512, - 6_400_000L * (long) Math.max(1, - Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierNanoSuit - 3)), - ConfigHolder.INSTANCE.tools.voltageTierNanoSuit))) + (p) -> new ModifiableArmorItem(GTArmorMaterials.NANO_MUSCLE, ArmorItem.Type.CHESTPLATE, p) + .setDefaultMaxModifiers(6)) .lang("NanoMuscle™ Suite Chestplate") .properties(p -> p.rarity(Rarity.UNCOMMON)) .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.MODIFIABLE_EQUIPMENT) .register(); - public static ItemEntry NANO_LEGGINGS = REGISTRATE + public static ItemEntry NANO_LEGGINGS = REGISTRATE .item("nanomuscle_leggings", - (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, ArmorItem.Type.LEGGINGS, p) - .setArmorLogic(new NanoMuscleSuite(ArmorItem.Type.LEGGINGS, - 512, - 6_400_000L * (long) Math.max(1, - Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierNanoSuit - 3)), - ConfigHolder.INSTANCE.tools.voltageTierNanoSuit))) + (p) -> new ModifiableArmorItem(GTArmorMaterials.NANO_MUSCLE, ArmorItem.Type.LEGGINGS, p) + .setDefaultMaxModifiers(6)) .lang("NanoMuscle™ Suite Leggings") .properties(p -> p.rarity(Rarity.UNCOMMON)) .tag(Tags.Items.ARMORS_LEGGINGS, CustomTags.MODIFIABLE_EQUIPMENT) .register(); - public static ItemEntry NANO_BOOTS = REGISTRATE - .item("nanomuscle_boots", (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, ArmorItem.Type.BOOTS, p) - .setArmorLogic(new NanoMuscleSuite(ArmorItem.Type.BOOTS, - 512, - 6_400_000L * (long) Math.max(1, - Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierNanoSuit - 3)), - ConfigHolder.INSTANCE.tools.voltageTierNanoSuit))) + public static ItemEntry NANO_BOOTS = REGISTRATE + .item("nanomuscle_boots", + (p) -> new ModifiableArmorItem(GTArmorMaterials.NANO_MUSCLE, ArmorItem.Type.BOOTS, p) + .setDefaultMaxModifiers(6)) .lang("NanoMuscle™ Suite Boots") .properties(p -> p.rarity(Rarity.UNCOMMON)) .tag(Tags.Items.ARMORS_BOOTS, CustomTags.STEP_BOOTS, CustomTags.MODIFIABLE_EQUIPMENT) .register(); - public static ItemEntry NANO_HELMET = REGISTRATE - .item("nanomuscle_helmet", (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, ArmorItem.Type.HELMET, p) - .setArmorLogic(new NanoMuscleSuite(ArmorItem.Type.HELMET, - 512, - 6_400_000L * (long) Math.max(1, - Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierNanoSuit - 3)), - ConfigHolder.INSTANCE.tools.voltageTierNanoSuit))) + public static ItemEntry NANO_HELMET = REGISTRATE + .item("nanomuscle_helmet", + (p) -> new ModifiableArmorItem(GTArmorMaterials.NANO_MUSCLE, ArmorItem.Type.HELMET, p) + .setDefaultMaxModifiers(6)) .lang("NanoMuscle™ Suite Helmet") .tag(Tags.Items.ARMORS_HELMETS, CustomTags.MODIFIABLE_EQUIPMENT) .properties(p -> p.rarity(Rarity.UNCOMMON)) @@ -2424,49 +2416,34 @@ public Component getItemName(ItemStack stack) { .tag(CustomTags.PPE_ARMOR) .register(); - public static ItemEntry QUANTUM_CHESTPLATE = REGISTRATE + public static ItemEntry QUANTUM_CHESTPLATE = REGISTRATE .item("quarktech_chestplate", - (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, ArmorItem.Type.CHESTPLATE, p) - .setArmorLogic(new QuarkTechSuite(ArmorItem.Type.CHESTPLATE, - 8192, - 100_000_000L * (long) Math.max(1, - Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierQuarkTech - 5)), - ConfigHolder.INSTANCE.tools.voltageTierQuarkTech))) + (p) -> new ModifiableArmorItem(GTArmorMaterials.QUARK_TECH, ArmorItem.Type.CHESTPLATE, p) + .setDefaultMaxModifiers(8)) .lang("QuarkTech™ Suite Chestplate") .properties(p -> p.rarity(Rarity.RARE)) .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) .register(); - public static ItemEntry QUANTUM_LEGGINGS = REGISTRATE + public static ItemEntry QUANTUM_LEGGINGS = REGISTRATE .item("quarktech_leggings", - (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, ArmorItem.Type.LEGGINGS, p) - .setArmorLogic(new QuarkTechSuite(ArmorItem.Type.LEGGINGS, - 8192, - 100_000_000L * (long) Math.max(1, - Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierQuarkTech - 5)), - ConfigHolder.INSTANCE.tools.voltageTierQuarkTech))) + (p) -> new ModifiableArmorItem(GTArmorMaterials.QUARK_TECH, ArmorItem.Type.LEGGINGS, p) + .setDefaultMaxModifiers(8)) .lang("QuarkTech™ Suite Leggings") .properties(p -> p.rarity(Rarity.RARE)) .tag(Tags.Items.ARMORS_LEGGINGS, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) .register(); - public static ItemEntry QUANTUM_BOOTS = REGISTRATE - .item("quarktech_boots", (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, ArmorItem.Type.BOOTS, p) - .setArmorLogic(new QuarkTechSuite(ArmorItem.Type.BOOTS, - 8192, - 100_000_000L * (long) Math.max(1, - Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierQuarkTech - 5)), - ConfigHolder.INSTANCE.tools.voltageTierQuarkTech))) + public static ItemEntry QUANTUM_BOOTS = REGISTRATE + .item("quarktech_boots", + (p) -> new ModifiableArmorItem(GTArmorMaterials.QUARK_TECH, ArmorItem.Type.BOOTS, p) + .setDefaultMaxModifiers(8)) .lang("QuarkTech™ Suite Boots") .properties(p -> p.rarity(Rarity.RARE)) - .tag(Tags.Items.ARMORS_BOOTS, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) - .tag(CustomTags.STEP_BOOTS) - .register(); - public static ItemEntry QUANTUM_HELMET = REGISTRATE - .item("quarktech_helmet", (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, ArmorItem.Type.HELMET, p) - .setArmorLogic(new QuarkTechSuite(ArmorItem.Type.HELMET, - 8192, - 100_000_000L * (long) Math.max(1, - Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierQuarkTech - 5)), - ConfigHolder.INSTANCE.tools.voltageTierQuarkTech))) + .tag(Tags.Items.ARMORS_BOOTS, CustomTags.PPE_ARMOR, CustomTags.STEP_BOOTS, CustomTags.MODIFIABLE_EQUIPMENT) + .register(); + public static ItemEntry QUANTUM_HELMET = REGISTRATE + .item("quarktech_helmet", + (p) -> new ModifiableArmorItem(GTArmorMaterials.QUARK_TECH, ArmorItem.Type.HELMET, p) + .setDefaultMaxModifiers(8)) .lang("QuarkTech™ Suite Helmet") .properties(p -> p.rarity(Rarity.RARE)) .tag(Tags.Items.ARMORS_HELMETS, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) @@ -2496,7 +2473,7 @@ public Component getItemName(ItemStack stack) { public static ItemEntry ELECTRIC_JETPACK_ADVANCED = REGISTRATE .item("advanced_electric_jetpack", - (p) -> new ArmorComponentItem(GTArmorMaterials.JETPACK, ArmorItem.Type.CHESTPLATE, p) + (p) ->new ArmorComponentItem(GTArmorMaterials.JETPACK, ArmorItem.Type.CHESTPLATE, p) .setArmorLogic(new AdvancedJetpack(512, 6_400_000L * (long) Math.max(1, Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierAdvImpeller - 4)), @@ -2505,25 +2482,18 @@ public Component getItemName(ItemStack stack) { .properties(p -> p.rarity(Rarity.RARE)) .tag(Tags.Items.ARMORS_CHESTPLATES) .register(); - public static ItemEntry NANO_CHESTPLATE_ADVANCED = REGISTRATE + public static ItemEntry NANO_CHESTPLATE_ADVANCED = REGISTRATE .item("avanced_nanomuscle_chestplate", - (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, ArmorItem.Type.CHESTPLATE, p) - .setArmorLogic(new AdvancedNanoMuscleSuite(512, - 12_800_000L * (long) Math.max(1, - Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierAdvNanoSuit - 3)), - ConfigHolder.INSTANCE.tools.voltageTierAdvNanoSuit))) + (p) -> new ModifiableArmorItem(GTArmorMaterials.ADVANCED_NANO_MUSCLE, ArmorItem.Type.CHESTPLATE, p) + .setDefaultMaxModifiers(6)) .lang("Advanced NanoMuscle™ Suite Chestplate") .properties(p -> p.rarity(Rarity.RARE)) .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) .register(); - public static ItemEntry QUANTUM_CHESTPLATE_ADVANCED = REGISTRATE - .item("advanced_quarktech_chestplate", (p) -> new ArmorComponentItem(GTArmorMaterials.ARMOR, - ArmorItem.Type.CHESTPLATE, p) - .setArmorLogic(new AdvancedQuarkTechSuite(8192, - 1_000_000_000L * - (long) Math.max(1, - Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierAdvQuarkTech - 6)), - ConfigHolder.INSTANCE.tools.voltageTierAdvQuarkTech))) + public static ItemEntry QUANTUM_CHESTPLATE_ADVANCED = REGISTRATE + .item("advanced_quarktech_chestplate", + (p) -> new ModifiableArmorItem(GTArmorMaterials.ADVANCED_QUARK_TECH, ArmorItem.Type.CHESTPLATE, p) + .setDefaultMaxModifiers(8)) .lang("Advanced QuarkTech™ Suite Chestplate") .properties(p -> p.rarity(Rarity.EPIC)) .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) @@ -2678,4 +2648,5 @@ T extends Item> NonNullBiConsumer, RegistrateLangProvide prov.add(ctx.get(), names.stream().map(StringUtils::capitalize).collect(Collectors.joining(" "))); }; } + } diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipes.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipes.java index f914450666c..84c0730cdf9 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipes.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipes.java @@ -42,6 +42,7 @@ public static void recipeAddition(Consumer originalConsumer) { // Decomposition info loading MaterialInfoLoader.init(); + // com.gregtechceu.gtceu.data.recipe.generated.* DecompositionRecipeHandler.init(consumer); MaterialRecipeHandler.init(consumer); @@ -77,7 +78,8 @@ public static void recipeAddition(Consumer originalConsumer) { // GCYM GCYMRecipes.init(consumer); - // Config-dependent recipes + // non-GTRecipe things + EquipmentFoundryRecipes.init(consumer); RecipeAddition.init(consumer); // Must run recycling recipes very last RecyclingRecipes.init(consumer); diff --git a/src/main/java/com/gregtechceu/gtceu/common/gui/widget/EquipmentFoundryBaseWidget.java b/src/main/java/com/gregtechceu/gtceu/common/gui/widget/EquipmentFoundryBaseWidget.java deleted file mode 100644 index e5f86d7303e..00000000000 --- a/src/main/java/com/gregtechceu/gtceu/common/gui/widget/EquipmentFoundryBaseWidget.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.gregtechceu.gtceu.common.gui.widget; - -import com.gregtechceu.gtceu.api.gui.widget.BlockableSlotWidget; -import com.gregtechceu.gtceu.api.gui.widget.SlotWidget; -import com.gregtechceu.gtceu.api.item.armor.ArmorComponentItem; -import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler; - -import com.lowdragmc.lowdraglib.gui.widget.WidgetGroup; - -import net.minecraft.world.item.ItemStack; - -public class EquipmentFoundryBaseWidget extends WidgetGroup { - - private final CustomItemStackHandler equipmentSlot; - private final CustomItemStackHandler modifierSlots; - - private WidgetGroup slotGroup; - - public EquipmentFoundryBaseWidget(int x, int y, int width, int height, - CustomItemStackHandler equipmentSlot, - CustomItemStackHandler modifierSlots) { - super(x, y, width, height); - this.equipmentSlot = equipmentSlot; - this.modifierSlots = modifierSlots; - - addWidget(new BlockableSlotWidget(equipmentSlot, 0, 20, 20) - .setIsBlocked(() -> !equipmentSlot.getStackInSlot(0).isEmpty()) - .setChangeListener(this::onEquipmentItemChanged)); - } - - public void onEquipmentItemChanged() { - ItemStack stack = equipmentSlot.getStackInSlot(0); - if (stack.getItem() instanceof ArmorComponentItem armorComponentItem) { - this.removeWidget(slotGroup); - - // TODO implement modification - - this.slotGroup = new WidgetGroup(0, 0, 0, 0); - for (int i = 0; i < armorComponentItem.getMaxModifiers(); i++) { - SlotWidget slot = new SlotWidget(modifierSlots, i, 0, i * 18, true, true); - slotGroup.addWidget(slot); - } - slotGroup.setSelfPosition((this.getSizeWidth() + slotGroup.getSizeWidth() - 18) / 2, - 50); - this.addWidget(slotGroup); - } - } -} diff --git a/src/main/java/com/gregtechceu/gtceu/common/item/armor/AdvancedNanoMuscleSuite.java b/src/main/java/com/gregtechceu/gtceu/common/item/armor/AdvancedNanoMuscleSuite.java index 241955a76c8..7747141cc1b 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/item/armor/AdvancedNanoMuscleSuite.java +++ b/src/main/java/com/gregtechceu/gtceu/common/item/armor/AdvancedNanoMuscleSuite.java @@ -229,7 +229,7 @@ public void drawHUD(ItemStack item, GuiGraphics guiGraphics) { @Override public ResourceLocation getArmorTexture(ItemStack stack, Entity entity, EquipmentSlot slot, String type) { - return GTCEu.id("textures/armor/advanced_nano_muscle_suite_1.png"); + return GTCEu.id("textures/models/armor/advanced_nano_muscle_suite_layer_1.png"); } @Override diff --git a/src/main/java/com/gregtechceu/gtceu/common/item/armor/AdvancedQuarkTechSuite.java b/src/main/java/com/gregtechceu/gtceu/common/item/armor/AdvancedQuarkTechSuite.java index 4b52258fa40..cdea37b2fba 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/item/armor/AdvancedQuarkTechSuite.java +++ b/src/main/java/com/gregtechceu/gtceu/common/item/armor/AdvancedQuarkTechSuite.java @@ -255,7 +255,7 @@ public void drawHUD(ItemStack item, GuiGraphics guiGraphics) { @Override public ResourceLocation getArmorTexture(ItemStack stack, Entity entity, EquipmentSlot slot, String type) { - return GTCEu.id("textures/armor/advanced_quark_tech_suite_1.png"); + return GTCEu.id("textures/models/armor/advanced_quark_tech_suite_layer_1.png"); } @Override diff --git a/src/main/java/com/gregtechceu/gtceu/common/item/armor/GTArmorMaterials.java b/src/main/java/com/gregtechceu/gtceu/common/item/armor/GTArmorMaterials.java index f8717cd8d24..4ba38417df2 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/item/armor/GTArmorMaterials.java +++ b/src/main/java/com/gregtechceu/gtceu/common/item/armor/GTArmorMaterials.java @@ -30,7 +30,25 @@ public enum GTArmorMaterials implements ArmorMaterial, StringRepresentable { map.put(ArmorItem.Type.CHESTPLATE, 0); map.put(ArmorItem.Type.HELMET, 0); }), 50, SoundEvents.ARMOR_EQUIP_GENERIC, 0.0F, 0.0F, () -> Ingredient.EMPTY), - ARMOR("armor", 0, Util.make(new EnumMap<>(ArmorItem.Type.class), map -> { + NANO_MUSCLE("gtceu:nano_muscle_suite", 0, Util.make(new EnumMap<>(ArmorItem.Type.class), map -> { + map.put(ArmorItem.Type.BOOTS, 0); + map.put(ArmorItem.Type.LEGGINGS, 0); + map.put(ArmorItem.Type.CHESTPLATE, 0); + map.put(ArmorItem.Type.HELMET, 0); + }), 50, SoundEvents.ARMOR_EQUIP_GENERIC, 5.0F, 0.0F, () -> Ingredient.EMPTY), + ADVANCED_NANO_MUSCLE("gtceu:advanced_nano_muscle_suite", 0, Util.make(new EnumMap<>(ArmorItem.Type.class), map -> { + map.put(ArmorItem.Type.BOOTS, 0); + map.put(ArmorItem.Type.LEGGINGS, 0); + map.put(ArmorItem.Type.CHESTPLATE, 0); + map.put(ArmorItem.Type.HELMET, 0); + }), 50, SoundEvents.ARMOR_EQUIP_GENERIC, 5.0F, 0.0F, () -> Ingredient.EMPTY), + QUARK_TECH("gtceu:quark_tech_suite", 0, Util.make(new EnumMap<>(ArmorItem.Type.class), map -> { + map.put(ArmorItem.Type.BOOTS, 0); + map.put(ArmorItem.Type.LEGGINGS, 0); + map.put(ArmorItem.Type.CHESTPLATE, 0); + map.put(ArmorItem.Type.HELMET, 0); + }), 50, SoundEvents.ARMOR_EQUIP_GENERIC, 5.0F, 0.0F, () -> Ingredient.EMPTY), + ADVANCED_QUARK_TECH("gtceu:advanced_quark_tech_suite", 0, Util.make(new EnumMap<>(ArmorItem.Type.class), map -> { map.put(ArmorItem.Type.BOOTS, 0); map.put(ArmorItem.Type.LEGGINGS, 0); map.put(ArmorItem.Type.CHESTPLATE, 0); diff --git a/src/main/java/com/gregtechceu/gtceu/common/item/armor/NanoMuscleSuite.java b/src/main/java/com/gregtechceu/gtceu/common/item/armor/NanoMuscleSuite.java index de83d205831..b759ff3a41d 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/item/armor/NanoMuscleSuite.java +++ b/src/main/java/com/gregtechceu/gtceu/common/item/armor/NanoMuscleSuite.java @@ -136,11 +136,11 @@ public ResourceLocation getArmorTexture(ItemStack stack, Entity entity, Equipmen ItemStack currentChest = Minecraft.getInstance().player.getInventory() .getArmor(ArmorItem.Type.CHESTPLATE.getSlot().getIndex()); ItemStack advancedChest = GTItems.NANO_CHESTPLATE_ADVANCED.asStack(); - String armorTexture = "nano_muscule_suite"; + String armorTexture = "nano_muscle_suite"; if (advancedChest.is(currentChest.getItem())) armorTexture = "advanced_nano_muscle_suite"; return slot != EquipmentSlot.LEGS ? - GTCEu.id(String.format("textures/armor/%s_1.png", armorTexture)) : - GTCEu.id(String.format("textures/armor/%s_2.png", armorTexture)); + GTCEu.id(String.format("textures/models/armor/%s_1.png", armorTexture)) : + GTCEu.id(String.format("textures/models/armor/%s_2.png", armorTexture)); } @Override diff --git a/src/main/java/com/gregtechceu/gtceu/common/item/armor/QuarkTechSuite.java b/src/main/java/com/gregtechceu/gtceu/common/item/armor/QuarkTechSuite.java index c2dcc0cb7bb..5d50c52c553 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/item/armor/QuarkTechSuite.java +++ b/src/main/java/com/gregtechceu/gtceu/common/item/armor/QuarkTechSuite.java @@ -315,8 +315,8 @@ public ResourceLocation getArmorTexture(ItemStack stack, Entity entity, Equipmen String armorTexture = "quark_tech_suite"; if (currentChest.is(GTItems.QUANTUM_CHESTPLATE_ADVANCED.get())) armorTexture = "advanced_quark_tech_suite"; return slot != EquipmentSlot.LEGS ? - GTCEu.id(String.format("textures/armor/%s_1.png", armorTexture)) : - GTCEu.id(String.format("textures/armor/%s_2.png", armorTexture)); + GTCEu.id(String.format("textures/models/armor/%s_1.png", armorTexture)) : + GTCEu.id(String.format("textures/models/armor/%s_2.png", armorTexture)); } @Override diff --git a/src/main/java/com/gregtechceu/gtceu/common/recipe/type/EquipmentFoundryRecipe.java b/src/main/java/com/gregtechceu/gtceu/common/recipe/type/EquipmentFoundryRecipe.java index ba927d01c3b..9cc13adb0ba 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/recipe/type/EquipmentFoundryRecipe.java +++ b/src/main/java/com/gregtechceu/gtceu/common/recipe/type/EquipmentFoundryRecipe.java @@ -1,9 +1,9 @@ package com.gregtechceu.gtceu.common.recipe.type; +import com.gregtechceu.gtceu.api.item.armor.ArmorUtils; import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; import com.gregtechceu.gtceu.common.data.GTItems; import com.gregtechceu.gtceu.common.data.GTRecipeTypes; -import com.gregtechceu.gtceu.data.recipe.CustomTags; import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.core.RegistryAccess; @@ -28,23 +28,20 @@ public class EquipmentFoundryRecipe implements Recipe { @Getter private final ResourceLocation id; + private final Ingredient equipment; private final Ingredient ingredient; private final ArmorModifier modifier; - /** - * Used to check if a recipe matches current crafting inventory - */ - public boolean matches(RecipeWrapper inv, Level level) { + public boolean matches(RecipeWrapper container, Level level) { boolean foundItem = false, foundIngredient = false; - for (int i = 0; i < inv.getContainerSize(); ++i) { - ItemStack stack = inv.getItem(i); + for (int i = 0; i < container.getContainerSize(); ++i) { + ItemStack stack = container.getItem(i); if (!stack.isEmpty()) { - if (stack.is(CustomTags.MODIFIABLE_EQUIPMENT)) { + if (equipment.test(stack)) { if (foundItem) { return false; } - foundItem = true; } else if (ingredient.test(stack)) { foundIngredient = true; @@ -57,23 +54,27 @@ public boolean matches(RecipeWrapper inv, Level level) { public ItemStack assemble(RecipeWrapper container, RegistryAccess registryAccess) { ItemStack result = ItemStack.EMPTY; + boolean foundIngredient = false; for (int i = 0; i < container.getContainerSize(); ++i) { ItemStack stack = container.getItem(i); - if (stack.is(CustomTags.MODIFIABLE_EQUIPMENT)) { + if (equipment.test(stack)) { if (!result.isEmpty()) { return ItemStack.EMPTY; } result = stack.copy(); - } else if (!ingredient.test(stack)) { - return ItemStack.EMPTY; + } else if (ingredient.test(stack)) { + foundIngredient = true; + break; } } - if (!result.isEmpty()) { - this.modifier.onAddToItem.apply(result); + if (!foundIngredient || result.isEmpty()) { + return ItemStack.EMPTY; } + + ArmorUtils.addModifier(result, modifier); return result; } @@ -100,23 +101,26 @@ public RecipeType getType() { public static class Serializer implements RecipeSerializer { public EquipmentFoundryRecipe fromJson(ResourceLocation recipeId, JsonObject json) { + Ingredient equipment = Ingredient.fromJson(GsonHelper.getAsJsonObject(json, "equipment"), false); Ingredient ingredient = Ingredient.fromJson(GsonHelper.getAsJsonObject(json, "ingredient"), false); ArmorModifier modifier = ArmorModifier.MODIFIERS.get( new ResourceLocation(GsonHelper.getAsString(json, "modifier"))); - return new EquipmentFoundryRecipe(recipeId, ingredient, modifier); + return new EquipmentFoundryRecipe(recipeId, equipment, ingredient, modifier); } public EquipmentFoundryRecipe fromNetwork(ResourceLocation recipeId, FriendlyByteBuf buffer) { + Ingredient equipment = Ingredient.fromNetwork(buffer); Ingredient ingredient = Ingredient.fromNetwork(buffer); ArmorModifier modifier = ArmorModifier.MODIFIERS.get(buffer.readResourceLocation()); - return new EquipmentFoundryRecipe(recipeId, ingredient, modifier); + return new EquipmentFoundryRecipe(recipeId, equipment, ingredient, modifier); } public void toNetwork(FriendlyByteBuf buffer, EquipmentFoundryRecipe recipe) { + recipe.equipment.toNetwork(buffer); recipe.ingredient.toNetwork(buffer); - buffer.writeResourceLocation(recipe.modifier.id); + buffer.writeResourceLocation(recipe.modifier.id()); } } } diff --git a/src/main/java/com/gregtechceu/gtceu/core/mixins/EntityMixin.java b/src/main/java/com/gregtechceu/gtceu/core/mixins/EntityMixin.java index 02b8b8615d3..42942f4586a 100644 --- a/src/main/java/com/gregtechceu/gtceu/core/mixins/EntityMixin.java +++ b/src/main/java/com/gregtechceu/gtceu/core/mixins/EntityMixin.java @@ -16,7 +16,6 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @Mixin(Entity.class) public abstract class EntityMixin implements IFireImmuneEntity { @@ -29,9 +28,9 @@ public abstract class EntityMixin implements IFireImmuneEntity { @Unique private boolean gtceu$isEntityInit = false; - @Inject(method = "fireImmune", at = @At("RETURN"), cancellable = true) - private void gtceu$changeFireImmune(CallbackInfoReturnable cir) { - cir.setReturnValue(gtceu$fireImmune || cir.getReturnValueZ()); + @ModifyReturnValue(method = "fireImmune", at = @At("RETURN")) + private boolean gtceu$changeFireImmune(boolean original) { + return gtceu$fireImmune || original; } @Inject(method = "", at = @At("TAIL")) @@ -50,8 +49,9 @@ public abstract class EntityMixin implements IFireImmuneEntity { return original; } - if (!ConfigHolder.INSTANCE.gameplay.hazardsEnabled) + if (!ConfigHolder.INSTANCE.gameplay.hazardsEnabled) { return original; + } IMedicalConditionTracker tracker = GTCapabilityHelper.getMedicalConditionTracker((Entity) (Object) this); if (tracker != null && tracker.getMaxAirSupply() != -1) { diff --git a/src/main/java/com/gregtechceu/gtceu/core/mixins/LivingEntityMixin.java b/src/main/java/com/gregtechceu/gtceu/core/mixins/LivingEntityMixin.java index f78855b4331..03c44e9bda6 100644 --- a/src/main/java/com/gregtechceu/gtceu/core/mixins/LivingEntityMixin.java +++ b/src/main/java/com/gregtechceu/gtceu/core/mixins/LivingEntityMixin.java @@ -2,6 +2,7 @@ import com.gregtechceu.gtceu.api.item.armor.ArmorComponentItem; +import com.gregtechceu.gtceu.api.item.armor.ModifiableArmorItem; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; @@ -24,7 +25,7 @@ public abstract class LivingEntityMixin { @Inject(method = "getDamageAfterArmorAbsorb", at = @At(value = "INVOKE", - target = "Lnet/minecraft/world/damagesource/CombatRules;getDamageAfterAbsorb(FFF)F")) + target = "Lnet/minecraft/world/entity/LivingEntity;hurtArmor(Lnet/minecraft/world/damagesource/DamageSource;F)V")) private void gtceu$adjustArmorAbsorption(DamageSource damageSource, float damageAmount, CallbackInfoReturnable cir) { float armorDamage = Math.max(1.0F, damageAmount / 4.0F); diff --git a/src/main/java/com/gregtechceu/gtceu/core/mixins/RangedAttributeAccessor.java b/src/main/java/com/gregtechceu/gtceu/core/mixins/RangedAttributeAccessor.java new file mode 100644 index 00000000000..f4df091ee85 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/core/mixins/RangedAttributeAccessor.java @@ -0,0 +1,15 @@ +package com.gregtechceu.gtceu.core.mixins; + +import net.minecraft.world.entity.ai.attributes.RangedAttribute; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(RangedAttribute.class) +public interface RangedAttributeAccessor { + + @Accessor("maxValue") + @Mutable + void gtceu$setMaxValue(double maxValue); +} diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/VanillaRecipeHelper.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/VanillaRecipeHelper.java index 74a6996fa2b..83907af3ed0 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/VanillaRecipeHelper.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/VanillaRecipeHelper.java @@ -9,6 +9,7 @@ import com.gregtechceu.gtceu.api.data.chemical.material.stack.MaterialStack; import com.gregtechceu.gtceu.api.data.chemical.material.stack.UnificationEntry; import com.gregtechceu.gtceu.api.data.tag.TagPrefix; +import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; import com.gregtechceu.gtceu.api.item.tool.ToolHelper; import com.gregtechceu.gtceu.data.recipe.builder.*; @@ -284,8 +285,6 @@ public static void addShapedRecipe(Consumer provider, boolean wi if (tag != null) { builder.define(sign, tag); } else builder.define(sign, ChemicalHelper.get(entry.tagPrefix, entry.material)); - } else if (content instanceof ItemProviderEntry entry) { - builder.define(sign, entry.asStack()); } } } @@ -365,8 +364,6 @@ public static void addShapedEnergyTransferRecipe(Consumer provid if (tag != null) { builder.define(sign, tag); } else builder.define(sign, ChemicalHelper.get(entry.tagPrefix, entry.material)); - } else if (content instanceof ItemProviderEntry entry) { - builder.define(sign, entry.asStack()); } } } @@ -435,8 +432,6 @@ public static void addShapedFluidContainerRecipe(Consumer provid if (tag != null) { builder.define(sign, tag); } else builder.define(sign, ChemicalHelper.get(entry.tagPrefix, entry.material)); - } else if (content instanceof ItemProviderEntry entry) { - builder.define(sign, entry.asStack()); } } } @@ -505,8 +500,6 @@ public static void addShapelessRecipe(Consumer provider, @NotNul if (tag != null) { builder.requires(tag); } else builder.requires(ChemicalHelper.get(entry.tagPrefix, entry.material)); - } else if (content instanceof ItemProviderEntry entry) { - builder.requires(entry.asStack()); } else if (content instanceof Character c) { builder.requires(ToolHelper.getToolFromSymbol(c).itemTags.get(0)); } @@ -514,6 +507,41 @@ public static void addShapelessRecipe(Consumer provider, @NotNul builder.save(provider); } + public static void addEquipmentFoundryRecipe(Consumer provider, @NotNull String regName, + @NotNull Object ingredient, @NotNull ArmorModifier modifier) { + addEquipmentFoundryRecipe(provider, GTCEu.id(regName), + Ingredient.of(CustomTags.MODIFIABLE_EQUIPMENT), ingredient, modifier); + } + + public static void addEquipmentFoundryRecipe(Consumer provider, @NotNull String regName, + @NotNull Ingredient equipment, + @NotNull Object ingredient, @NotNull ArmorModifier modifier) { + addEquipmentFoundryRecipe(provider, GTCEu.id(regName), equipment, ingredient, modifier); + } + + public static void addEquipmentFoundryRecipe(Consumer provider, @NotNull ResourceLocation regName, + @NotNull Ingredient equipment, + @NotNull Object ingredient, @NotNull ArmorModifier modifier) { + var builder = new EquipmentFoundryRecipeBuilder(regName).equipment(equipment).modifier(modifier); + if (ingredient instanceof Ingredient ing) { + builder.ingredient(ing); + } else if (ingredient instanceof ItemStack itemStack) { + builder.ingredient(itemStack); + } else if (ingredient instanceof TagKey key) { + builder.ingredient((TagKey) key); + } else if (ingredient instanceof ItemLike itemLike) { + builder.ingredient(itemLike); + } else if (ingredient instanceof UnificationEntry entry) { + TagKey tag = ChemicalHelper.getTag(entry.tagPrefix, entry.material); + if (tag != null) { + builder.ingredient(tag); + } else builder.ingredient(ChemicalHelper.get(entry.tagPrefix, entry.material)); + } else if (ingredient instanceof Character c) { + builder.ingredient(ToolHelper.getToolFromSymbol(c).itemTags.get(0)); + } + builder.save(provider); + } + /** * @param material the material to check * @return if the material is a wood diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/EquipmentFoundryRecipeBuilder.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/EquipmentFoundryRecipeBuilder.java index 283b4e1d07d..6442e03be76 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/EquipmentFoundryRecipeBuilder.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/EquipmentFoundryRecipeBuilder.java @@ -3,6 +3,8 @@ import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; import com.gregtechceu.gtceu.api.recipe.ingredient.NBTIngredient; +import com.gregtechceu.gtceu.common.data.GTRecipeTypes; +import com.gregtechceu.gtceu.data.recipe.CustomTags; import net.minecraft.data.recipes.FinishedRecipe; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; @@ -15,6 +17,7 @@ import com.google.gson.JsonObject; import lombok.Setter; import lombok.experimental.Accessors; +import lombok.experimental.Tolerate; import org.jetbrains.annotations.Nullable; import java.util.function.Consumer; @@ -25,6 +28,8 @@ public class EquipmentFoundryRecipeBuilder { @Setter private ResourceLocation id; @Setter + private Ingredient equipment = Ingredient.of(CustomTags.MODIFIABLE_EQUIPMENT); + @Setter private Ingredient ingredient; @Setter private ArmorModifier modifier; @@ -33,11 +38,13 @@ public EquipmentFoundryRecipeBuilder(@Nullable ResourceLocation id) { this.id = id; } - public EquipmentFoundryRecipeBuilder input(TagKey itemStack) { - return input(Ingredient.of(itemStack)); + @Tolerate + public EquipmentFoundryRecipeBuilder ingredient(TagKey itemStack) { + return ingredient(Ingredient.of(itemStack)); } - public EquipmentFoundryRecipeBuilder input(ItemStack itemStack) { + @Tolerate + public EquipmentFoundryRecipeBuilder ingredient(ItemStack itemStack) { if (itemStack.hasTag()) { ingredient = NBTIngredient.createNBTIngredient(itemStack); } else { @@ -46,27 +53,20 @@ public EquipmentFoundryRecipeBuilder input(ItemStack itemStack) { return this; } - public EquipmentFoundryRecipeBuilder input(ItemLike itemLike) { - return input(Ingredient.of(itemLike)); - } - - public EquipmentFoundryRecipeBuilder input(Ingredient ingredient) { - this.ingredient = ingredient; - return this; + @Tolerate + public EquipmentFoundryRecipeBuilder ingredient(ItemLike itemLike) { + return ingredient(Ingredient.of(itemLike)); } protected ResourceLocation defaultId() { - return modifier.id; + return modifier.id(); } public void toJson(JsonObject json) { - if (!ingredient.isEmpty()) { - json.add("ingredient", ingredient.toJson()); - } + json.add("equipment", equipment.toJson()); + json.add("ingredient", ingredient.toJson()); - if (modifier != null) { - json.addProperty("modifier", modifier.id.toString()); - } + json.addProperty("modifier", modifier.id().toString()); } public void save(Consumer consumer) { @@ -79,13 +79,13 @@ public void serializeRecipeData(JsonObject pJson) { @Override public ResourceLocation getId() { - var ID = id == null ? defaultId() : id; - return new ResourceLocation(ID.getNamespace(), "equipment_foundry" + "/" + ID.getPath()); + var _id = id == null ? defaultId() : id; + return _id.withPrefix("equipment_foundry/"); } @Override public RecipeSerializer getType() { - return RecipeSerializer.SMOKING_RECIPE; + return GTRecipeTypes.EQUIPMENT_FOUNDRY_SERIALIZER.get(); } @Nullable diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/misc/CraftingRecipeLoader.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/misc/CraftingRecipeLoader.java index 72fdb6ba405..d5d6d4f1e44 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/misc/CraftingRecipeLoader.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/misc/CraftingRecipeLoader.java @@ -26,9 +26,6 @@ public class CraftingRecipeLoader { public static void init(Consumer provider) { - // todo facades - // registerFacadeRecipe(provider, Iron, 4); - VanillaRecipeHelper.addShapedRecipe(provider, "small_wooden_pipe", ChemicalHelper.get(pipeSmallFluid, Wood), "sWr", 'W', ItemTags.PLANKS); VanillaRecipeHelper.addShapedRecipe(provider, "normal_wooden_pipe", ChemicalHelper.get(pipeNormalFluid, Wood), @@ -331,11 +328,4 @@ public static void init(Consumer provider) { /////////////////////////////////////////////////// SpecialRecipeBuilder.special(FacadeCoverRecipe.SERIALIZER).save(provider, "gtceu:crafting/facade_cover"); } - - // private static void registerFacadeRecipe(Consumer provider, Material material, int facadeAmount) - // { - // OreIngredient ingredient = new OreIngredient(new UnificationEntry(plate, material).toString()); - // ForgeRegistries.RECIPES.register(new FacadeRecipe(null, ingredient, facadeAmount).setRegistryName("facade_" + - // material)); - // } } diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/misc/EquipmentFoundryRecipes.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/misc/EquipmentFoundryRecipes.java new file mode 100644 index 00000000000..ed2281e8cc0 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/misc/EquipmentFoundryRecipes.java @@ -0,0 +1,24 @@ +package com.gregtechceu.gtceu.data.recipe.misc; + +import com.gregtechceu.gtceu.common.data.GTArmorModifiers; +import com.gregtechceu.gtceu.common.data.GTItems; +import com.gregtechceu.gtceu.data.recipe.CustomTags; +import com.gregtechceu.gtceu.data.recipe.VanillaRecipeHelper; + +import net.minecraft.data.recipes.FinishedRecipe; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraftforge.common.Tags; +import net.minecraftforge.common.crafting.IntersectionIngredient; + +import java.util.function.Consumer; + +public class EquipmentFoundryRecipes { + + public static void init(Consumer provider) { + VanillaRecipeHelper.addEquipmentFoundryRecipe(provider, "speed", + IntersectionIngredient.of( + Ingredient.of(CustomTags.MODIFIABLE_EQUIPMENT), + Ingredient.of(Tags.Items.ARMORS_LEGGINGS)), + GTItems.ELECTRIC_PISTON_EV, GTArmorModifiers.SPEED); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/forge/ForgeCommonEventListener.java b/src/main/java/com/gregtechceu/gtceu/forge/ForgeCommonEventListener.java index 81e6224ee28..9ed29471e3e 100644 --- a/src/main/java/com/gregtechceu/gtceu/forge/ForgeCommonEventListener.java +++ b/src/main/java/com/gregtechceu/gtceu/forge/ForgeCommonEventListener.java @@ -18,6 +18,8 @@ import com.gregtechceu.gtceu.api.item.IComponentItem; import com.gregtechceu.gtceu.api.item.TagPrefixItem; import com.gregtechceu.gtceu.api.item.armor.ArmorComponentItem; +import com.gregtechceu.gtceu.api.item.armor.ArmorUtils; +import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; import com.gregtechceu.gtceu.api.machine.MetaMachine; import com.gregtechceu.gtceu.api.machine.feature.IInteractedMachine; import com.gregtechceu.gtceu.api.misc.forge.FilteredFluidHandlerItemStack; @@ -58,6 +60,7 @@ import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.Difficulty; +import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; @@ -76,10 +79,7 @@ import net.minecraftforge.common.capabilities.ICapabilitySerializable; import net.minecraftforge.common.util.LazyOptional; import net.minecraftforge.event.*; -import net.minecraftforge.event.entity.living.LivingDeathEvent; -import net.minecraftforge.event.entity.living.LivingEvent; -import net.minecraftforge.event.entity.living.LivingFallEvent; -import net.minecraftforge.event.entity.living.MobSpawnEvent; +import net.minecraftforge.event.entity.living.*; import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.event.entity.player.PlayerInteractEvent; import net.minecraftforge.event.level.BlockEvent; @@ -104,6 +104,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -400,6 +401,14 @@ public static void onLivingFall(LivingFallEvent event) { public static void onLivingTick(LivingEvent.LivingTickEvent event) { LivingEntity entity = event.getEntity(); + for (ItemStack stack : entity.getArmorSlots()) { + List modifiers = ArmorUtils.getModifiers(stack); + if (modifiers.isEmpty()) continue; + for (ArmorModifier modifier : modifiers) { + modifier.onTick().apply(entity, stack); + } + } + float MAGIC_STEP_HEIGHT = 1.0023f; if (!entity.isCrouching() && entity.getItemBySlot(EquipmentSlot.FEET).is(CustomTags.STEP_BOOTS)) { if (entity.getStepHeight() < MAGIC_STEP_HEIGHT) { @@ -411,7 +420,42 @@ public static void onLivingTick(LivingEvent.LivingTickEvent event) { } @SubscribeEvent - public static void onEntityDie(LivingDeathEvent event) { + public static void onLivingEquipmentChange(LivingEquipmentChangeEvent event) { + final LivingEntity entity = event.getEntity(); + final ItemStack old = event.getFrom(); + final ItemStack current = event.getTo(); + + if (ItemStack.matches(old, current)) { + return; + } + + if (!old.isEmpty() && ArmorUtils.hasArmorTag(old)) { + ArmorUtils.getModifiers(old).forEach(modifier -> modifier.onUnequip().apply(entity, old)); + } + + if (!current.isEmpty() && ArmorUtils.hasArmorTag(current)) { + ArmorUtils.getModifiers(current).forEach(modifier -> modifier.onEquip().apply(entity, current)); + } + } + + @SubscribeEvent + public static void onLivingHurt(LivingHurtEvent event) { + final LivingEntity entity = event.getEntity(); + final DamageSource source = event.getSource(); + + for (final ItemStack stack : entity.getArmorSlots()) { + if (!ArmorUtils.isModifiable(stack)) continue; + + float amount = event.getAmount(); + for (ArmorModifier modifier : ArmorUtils.getModifiers(stack)) { + amount = modifier.onDamage().apply(entity, stack, source, amount).newAmount(); + } + event.setAmount(amount); + } + } + + @SubscribeEvent + public static void onLivingDie(LivingDeathEvent event) { if (event.getEntity() instanceof Player player) { IMedicalConditionTracker tracker = GTCapabilityHelper.getMedicalConditionTracker(player); if (tracker == null) { diff --git a/src/main/java/com/gregtechceu/gtceu/syncdata/GTRecipePayload.java b/src/main/java/com/gregtechceu/gtceu/syncdata/GTRecipePayload.java index e7b282872da..94496870ea2 100644 --- a/src/main/java/com/gregtechceu/gtceu/syncdata/GTRecipePayload.java +++ b/src/main/java/com/gregtechceu/gtceu/syncdata/GTRecipePayload.java @@ -88,9 +88,9 @@ public void readPayload(FriendlyByteBuf buf) { } } - static class Client { + private static class Client { - static RecipeManager getRecipeManager() { + private static RecipeManager getRecipeManager() { return Minecraft.getInstance().getConnection().getRecipeManager(); } } diff --git a/src/main/resources/assets/gtceu/textures/block/equipment_foundry.png b/src/main/resources/assets/gtceu/textures/block/equipment_foundry.png new file mode 100644 index 0000000000000000000000000000000000000000..7f57a8703a8fc99e62a77854e1f3072bf9da381f GIT binary patch literal 1203 zcmbVK&ubG=5dL1WCK}o%6~wBj1-(YEJqRUjesqzBAVT$|l>{wPEp@3q3F=7&=|!|w zORH8QVp{`6LH`W@06hrxVudu&CYwBGCc9bJss$gsH*da~?|U<|dncF8v`6+w0JNI} z$x~!wymyA^JY3epWI`8CW|HVd9Py>egWn0+H8ybe8gT3gZ#B8lJ3(OBG6z%P)wV;? z<0@Wydz7GkR_ct^e|2=UaK%FZcwyKoIJ%AlX6N^PVD4_u~nk{&llI9!@N^sUjbyIFN+a{}F*XzXBC@w9c zyuOas3|s=+<+~%uO+54lux&G zv(qc`I#;!|1s)Jr?7YkaQv|S^GSm3D@{T>pVX*ZX-!CZ4-KSLdWhROkrE?+9U?t99 zfIv+B^5w&u_jzec#!&K1zNXVDMKC8>lfl$NN9OuYR0NATVW OmNZk@+#93- literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/gtceu/textures/gui/widget/equipment_foundry_background.png b/src/main/resources/assets/gtceu/textures/gui/widget/equipment_foundry_background.png new file mode 100644 index 0000000000000000000000000000000000000000..abaff1824f75022064381b004d11d6131e0019c4 GIT binary patch literal 50715 zcmeHQU8r5f72Y?*Ts1~SN=iafLIq7CMvA2LCZ)l8L%d2vM3c0>q+)1&NGt6{?Tf9x z75Zi=6)6-Ukla9@O8tFNG!dc?Ay)d*2Tj_P(w5LF)I?49w@$w-+{Y}}e zdgR_cvt|E>;_}+HE522Zu6g|K@BE}Fw%vq(Q%@b)^+YM0o}b&hd;0I!eP#7mzkbWn zH(xD9>*jYKn7`{shYuh6;e2t|j}Lus{?MbFo_uWn2b=cH?Y)2hGuMB)C^i*yvv)rD z)Kqc$=i7cZwQcLom&=I*ZRd`esR2Pg$QekGfwxbb9IC~E0^Pswq2k!W@dU19ikty^ z2Fw~qLPocUg~>47J(c zgZ8aPc<|oo51igTnY^kHI&Bj+oESo)bZFxA4IH5UK+5neY zhM*bnZJTfEkA3{&yfc6POJ#oX(@%>#Zr^I1+Jy&zzFb{+ns7*;khdnS2@h>UoE!M) zIzP~jWPu_9=RfKdn5&2LW;1QKY}ry=Fpb2q#sf3ri7EXdb?h}RjfeC5z+ylh`#KIx zr7e5Hna7md=G5^J%hgM4j!j#QgLBZ2WP#E_1kt6<%*+%oz5YfmL{}aV_;7XQ@!^0k zDR@$FAeNEbgEqZ9ArM#7ur!?uPm{awTyhP|rE_6H?$WuiG`S1QCD*WAIv<~tuAomp z{r+y27%f>*~j3qamC;<hrMk|!3287q!QszBjT=v4 zkfV)Uf%2Ui>RtNR7gt`xX93LBr_xeO_r<()>EghgPd@%nvHH5}eW!cdnLL;ngA1542eCNde+r({ zXZ{4Ls!f+wwRy{}JnYnaUE(`2^`Vo&{e1fC0YJkHa^dYKD!Vo&{e1fC0YJkHa^dZ~V*=Pjp@+2ON*U2i#s^eJPF zZU)o>q=Hs3U<1&V-Td@uixjyLS# zm+j^=3U*_J8~G`?QgEcon=2OZH*K{A8smHUJvGmU+JZpPDcW-4Sq`z(7AT5>&xVF5 z5MVWZHk7=deKFcZ26L;B=8Fa1OrgC;z*4Z#67v<1bhKq~;Odic-$%N`FOV;_0O2+}-+4Q)BM zt~h~#1n@x>_QW=iDM!zzuV{mxtKcarSJQYfhd8$RO}I3kP#k*?zWfZI1z-UYn?2(& zk10n#YH>;5roKCSQ}2!j$C*yxgYAFl?w}Umu?Wj@5qJalKY_!TKV|g zY6TsnDXKqfD`mDn7%uhNSZ?4nJ6|0(r?!o0Xdd`u;|k4_f+J-t;If?`$kBonXb)=f z_0*|T#rEyn7X=MQTzR#PQv*2XsuG$9KG{~6&^#Ike2AI16tN%%*;*AS{t&p<)~#DN zu&Vm5sG(OI_-OlhAA`rn6@$l&Ck99ASiom{<3Zl4KryjN^IcK-4h43Kl^`S!u*@W< zh(~gSjAtogf&XUfRG{b2^Dc!6n~!q_!epRRfx?Bhd(ANJXO!ihfkp-D+Iza>X7YUZ zqUOAOJh>U@bRV>7+OQYREoY!f2BH)w_NIA%+T=83TUQ1eUE*CJHaYgDzL$Q^k8#Vy zye$K+7(Bo=xnl67;P8m$ABtn&6rzU3q{xHvXXq|6ztV&zTp#Nt?ta&gqCKrzj=O4fa{uhY||&?^w$rM9ot zLi50X8&_za6dWmI0hjIkK#tb9Kzq}AujaQ$QrC9OcWUf+X=onAYp!mPh33&Xz!x)b zDPlnkvPJp8BnjG#<=&*td^tzM(sUBTckIG*$$fF9oeK+cmrl<29lNkxa$j6&=i_s$ z71Z2wntM{;J*RZm9OK!;5Whb$an0k*V`ilOI91NzNvi_IM7gK5eR7w2*f(j|lgPij zf_EA&ZR~AW)X5%pz;ZQ>2XkzfQ^+L;{es-ZrSXL1fDbZnI7Xwty`#Zb>SPa#rHn;- z*t$R^NQf0v;&HD-nJZb;%bvEb6EAJM@Bqh`s|!zC4jHG1_6ztPZPAuVG7_hNI~uhR;~T(sVx9w9#-h9p}6HsclZ<}}aY}aE2Op`=>4nCbmDTT+ Date: Mon, 30 Dec 2024 21:49:33 +0200 Subject: [PATCH 006/286] fix slot textures --- .../gtceu/api/item/armor/ModifiableArmorItem.java | 1 - .../common/blockentity/EquipmentFoundryBlockEntity.java | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ModifiableArmorItem.java b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ModifiableArmorItem.java index b47d2d44952..b73e267ec4c 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ModifiableArmorItem.java +++ b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ModifiableArmorItem.java @@ -14,7 +14,6 @@ import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; import net.minecraft.world.InteractionResultHolder; -import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; diff --git a/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java b/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java index 3a1247d229a..5d8eeae81fe 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java +++ b/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java @@ -101,7 +101,7 @@ public ModularUI createUI(Player entityPlayer) { ModularUI modularUI = new ModularUI(176, 166, this, entityPlayer); modularUI.background(GuiTextures.BACKGROUND.copy().setColor(0xff69645f)); - IGuiTexture slotTexture = null;//GuiTextures.SLOT.copy().setColor(0xff69645f); + IGuiTexture slotTexture = GuiTextures.SLOT.copy().setColor(0xff69645f); TextTexture titleText = new TextTexture(getBlockState().getBlock().getDescriptionId()) .setColor(0xffffff) @@ -117,7 +117,7 @@ public ModularUI createUI(Player entityPlayer) { modularUI.widget(new SlotWidget(equipmentSlot, 0, 14, 32) .setChangeListener(() -> this.onEquipmentSlotChanged(entityPlayer)) - .setBackgroundTexture(slotTexture)); + .setBackgroundTexture(null)); int x = 42; int y = 13; @@ -126,7 +126,7 @@ public ModularUI createUI(Player entityPlayer) { modularUI.widget(new BlockableSlotWidget(modifierSlots, i, x, y) .setIsBlocked(() -> isModifierSlotBlocked(finalI)) .setChangeListener(this::onModifierSlotChanged) - .setBackgroundTexture(slotTexture)); + .setBackgroundTexture(null)); x += 26; if (i == 4) { x = 42; From e6458f8246ac61bd50706f0038dfe4288733ef47 Mon Sep 17 00:00:00 2001 From: Ghostipedia Date: Tue, 8 Apr 2025 13:03:05 -0400 Subject: [PATCH 007/286] The Game Runs Again! --- src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java | 1 + .../com/gregtechceu/gtceu/common/data/GTRecipeTypes.java | 2 +- .../gregtechceu/gtceu/data/recipe/VanillaRecipeHelper.java | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java b/src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java index 057a86741d8..93b38a95dd3 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java +++ b/src/main/java/com/gregtechceu/gtceu/common/CommonProxy.java @@ -53,6 +53,7 @@ import com.gregtechceu.gtceu.integration.top.forge.TheOneProbePluginImpl; import com.gregtechceu.gtceu.utils.input.KeyBind; +import com.lowdragmc.lowdraglib.LDLib; import com.lowdragmc.lowdraglib.gui.factory.UIFactory; import net.minecraft.core.registries.BuiltInRegistries; diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeTypes.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeTypes.java index 6cc42f04b44..55bec3027b2 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeTypes.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeTypes.java @@ -21,7 +21,7 @@ import com.gregtechceu.gtceu.common.machine.trait.customlogic.*; import com.gregtechceu.gtceu.common.recipe.condition.RockBreakerCondition; import com.gregtechceu.gtceu.common.recipe.type.EquipmentFoundryRecipe; -import com.gregtechceu.gtceu.data.recipe.RecipeHelper; + import com.gregtechceu.gtceu.data.recipe.RecipeUtil; import com.gregtechceu.gtceu.data.recipe.builder.GTRecipeBuilder; import com.gregtechceu.gtceu.integration.kjs.GTRegistryInfo; diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/VanillaRecipeHelper.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/VanillaRecipeHelper.java index 991333ef19e..0d443bd8a67 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/VanillaRecipeHelper.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/VanillaRecipeHelper.java @@ -558,11 +558,11 @@ public static void addEquipmentFoundryRecipe(Consumer provider, builder.ingredient((TagKey) key); } else if (ingredient instanceof ItemLike itemLike) { builder.ingredient(itemLike); - } else if (ingredient instanceof UnificationEntry entry) { - TagKey tag = ChemicalHelper.getTag(entry.tagPrefix, entry.material); + } else if (ingredient instanceof MaterialEntry entry) { + TagKey tag = ChemicalHelper.getTag(entry.tagPrefix(), entry.material()); if (tag != null) { builder.ingredient(tag); - } else builder.ingredient(ChemicalHelper.get(entry.tagPrefix, entry.material)); + } else builder.ingredient(ChemicalHelper.get(entry.tagPrefix(), entry.material())); } else if (ingredient instanceof Character c) { builder.ingredient(ToolHelper.getToolFromSymbol(c).itemTags.get(0)); } From 97afb14aa30c7d119d91bc524f685905aa9041e5 Mon Sep 17 00:00:00 2001 From: Ghostipedia Date: Fri, 11 Apr 2025 12:27:57 -0400 Subject: [PATCH 008/286] Spotless --- .../gtceu/tags/items/modifiable_equipment.json | 2 +- .../api/item/armor/ArmorComponentItem.java | 5 ++--- .../gtceu/api/item/armor/ArmorLogicSuite.java | 2 +- .../gtceu/api/item/armor/ArmorUtils.java | 10 ++++++---- .../api/item/armor/ModifiableArmorItem.java | 18 +++++++++--------- .../EquipmentFoundryBlockEntity.java | 3 +-- .../gtceu/common/data/GTArmorModifiers.java | 16 +++++++--------- .../gregtechceu/gtceu/common/data/GTItems.java | 9 +++++---- .../gtceu/common/data/GTRecipeTypes.java | 1 - .../gtceu/common/data/GTRecipes.java | 1 - .../common/item/armor/GTArmorMaterials.java | 10 +++++----- .../gtceu/core/mixins/LivingEntityMixin.java | 1 - .../builder/EquipmentFoundryRecipeBuilder.java | 2 +- .../recipe/misc/EquipmentFoundryRecipes.java | 13 +++++++++++++ 14 files changed, 51 insertions(+), 42 deletions(-) diff --git a/src/generated/resources/data/gtceu/tags/items/modifiable_equipment.json b/src/generated/resources/data/gtceu/tags/items/modifiable_equipment.json index f77d53c52e1..d9f95d710f4 100644 --- a/src/generated/resources/data/gtceu/tags/items/modifiable_equipment.json +++ b/src/generated/resources/data/gtceu/tags/items/modifiable_equipment.json @@ -9,7 +9,7 @@ "gtceu:quarktech_leggings", "gtceu:quarktech_boots", "gtceu:quarktech_helmet", - "gtceu:avanced_nanomuscle_chestplate", + "gtceu:advanced_nanomuscle_chestplate", "gtceu:advanced_quarktech_chestplate" ] } \ No newline at end of file diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorComponentItem.java b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorComponentItem.java index 9cb04918fbe..973d744c098 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorComponentItem.java +++ b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorComponentItem.java @@ -6,8 +6,6 @@ import com.gregtechceu.gtceu.api.item.component.forge.IComponentCapability; import com.gregtechceu.gtceu.common.data.GTItems; -import lombok.Setter; -import lombok.experimental.Accessors; import net.minecraft.client.model.HumanoidModel; import net.minecraft.core.NonNullList; import net.minecraft.network.chat.Component; @@ -31,6 +29,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.*; import lombok.Getter; +import lombok.experimental.Accessors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -90,7 +89,7 @@ public void onInventoryTick(ItemStack stack, Level level, Player player, int slo super.onInventoryTick(stack, level, player, slotIndex, selectedIndex); // if index >= 36, the item is in an armor slot if (slotIndex >= 36) { - //this.armorLogic.onArmorTick(level, player, stack); + // this.armorLogic.onArmorTick(level, player, stack); } } diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorLogicSuite.java b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorLogicSuite.java index 136e7b4ed86..894847cee46 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorLogicSuite.java +++ b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorLogicSuite.java @@ -87,7 +87,7 @@ public InteractionResultHolder use(Item item, Level level, Player pla @Override public void appendHoverText(ItemStack stack, @Nullable Level level, List tooltipComponents, TooltipFlag isAdvanced) { - //addInfo(stack, tooltipComponents); + // addInfo(stack, tooltipComponents); } }); } diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorUtils.java b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorUtils.java index e1be97d3e3e..11d9f71673d 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorUtils.java +++ b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ArmorUtils.java @@ -75,9 +75,8 @@ public static CompoundTag getArmorTag(ItemStack stack) { * @return the maximum amount of modifiers for the given stack */ public static int getMaxModifiers(ItemStack stack) { - if (!(hasArmorTag(stack) - && getArmorTag(stack).contains(MAX_MODIFIERS_KEY, Tag.TAG_INT)) - && stack.getItem() instanceof ModifiableArmorItem armorComponentItem) { + if (!(hasArmorTag(stack) && getArmorTag(stack).contains(MAX_MODIFIERS_KEY, Tag.TAG_INT)) && + stack.getItem() instanceof ModifiableArmorItem armorComponentItem) { setMaxModifiers(stack, armorComponentItem.getDefaultMaxModifiers()); return armorComponentItem.getDefaultMaxModifiers(); } else if (!hasArmorTag(stack)) { @@ -93,6 +92,7 @@ public static void setMaxModifiers(ItemStack stack, int maxModifiers) { /** * Clear all modifiers from the given piece of armor + * * @param stack the armor to remove all modifiers from */ public static void clearModifiers(ItemStack stack) { @@ -103,7 +103,8 @@ public static void clearModifiers(ItemStack stack) { /** * Add an armor modifier to the given stack - * @param stack the stack to add the modifier to, if both are valid + * + * @param stack the stack to add the modifier to, if both are valid * @param modifier the modifier to add to the stack */ public static void addModifier(ItemStack stack, ArmorModifier modifier) { @@ -118,6 +119,7 @@ public static void addModifier(ItemStack stack, ArmorModifier modifier) { /** * An unmodifiable list of all modifiers on the given stack + * * @param stack the stack to get the modifiers from * @return the modifiers on the stack */ diff --git a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ModifiableArmorItem.java b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ModifiableArmorItem.java index b73e267ec4c..f6b234e7a14 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/item/armor/ModifiableArmorItem.java +++ b/src/main/java/com/gregtechceu/gtceu/api/item/armor/ModifiableArmorItem.java @@ -4,9 +4,7 @@ import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; import com.gregtechceu.gtceu.api.item.component.*; import com.gregtechceu.gtceu.api.item.component.forge.IComponentCapability; -import lombok.Getter; -import lombok.Setter; -import lombok.experimental.Accessors; + import net.minecraft.MethodsReturnNonnullByDefault; import net.minecraft.client.model.HumanoidModel; import net.minecraft.core.NonNullList; @@ -24,15 +22,20 @@ import net.minecraftforge.client.extensions.common.IClientItemExtensions; import net.minecraftforge.common.capabilities.Capability; import net.minecraftforge.common.util.LazyOptional; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import javax.annotation.ParametersAreNonnullByDefault; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.Consumer; +import javax.annotation.ParametersAreNonnullByDefault; + @ParametersAreNonnullByDefault @MethodsReturnNonnullByDefault @Accessors(chain = true) @@ -44,7 +47,6 @@ public class ModifiableArmorItem extends ArmorItem implements IComponentItem { @Getter protected List components = new ArrayList<>(); - public ModifiableArmorItem(ArmorMaterial material, Type type, Properties properties) { super(material, type, properties); } @@ -57,7 +59,6 @@ public void attachComponents(IItemComponent... components) { } } - @Override public int getMaxDamage(ItemStack stack) { return super.getMaxDamage(stack); @@ -87,7 +88,7 @@ public void initializeClient(Consumer consumer) { EquipmentSlot equipmentSlot, HumanoidModel original) { // TODO modifiable armor model - //return armorLogic.getArmorModel(livingEntity, itemStack, equipmentSlot, original); + // return armorLogic.getArmorModel(livingEntity, itemStack, equipmentSlot, original); return original; } }); @@ -97,7 +98,7 @@ public void initializeClient(Consumer consumer) { @Override public String getArmorTexture(ItemStack stack, Entity entity, EquipmentSlot slot, String type) { // TODO add custom texture logic (or not? do we need it?) - //return armorLogic.getArmorTexture(stack, entity, slot, type).toString(); + // return armorLogic.getArmorTexture(stack, entity, slot, type).toString(); return null; } @@ -290,5 +291,4 @@ public LazyOptional getCapability(@NotNull final ItemStack itemStack, @No } return LazyOptional.empty(); } - } diff --git a/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java b/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java index 5d8eeae81fe..b07301cba90 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java +++ b/src/main/java/com/gregtechceu/gtceu/common/blockentity/EquipmentFoundryBlockEntity.java @@ -4,7 +4,6 @@ import com.gregtechceu.gtceu.api.gui.UITemplate; import com.gregtechceu.gtceu.api.gui.widget.BlockableSlotWidget; import com.gregtechceu.gtceu.api.gui.widget.SlotWidget; -import com.gregtechceu.gtceu.api.item.armor.ArmorComponentItem; import com.gregtechceu.gtceu.api.item.armor.ArmorUtils; import com.gregtechceu.gtceu.api.transfer.item.CustomItemStackHandler; import com.gregtechceu.gtceu.common.data.GTRecipeTypes; @@ -64,6 +63,7 @@ public EquipmentFoundryBlockEntity(BlockEntityType type, BlockPos pos, BlockS this.equipmentSlot.setFilter(stack -> stack.is(CustomTags.MODIFIABLE_EQUIPMENT)); this.modifierSlots = new CustomItemStackHandler(MAX_MODIFIER_SLOTS) { + @Override public int getSlotLimit(int slot) { return 1; @@ -169,7 +169,6 @@ public void onModifierSlotChanged() { return; } - ArmorUtils.clearModifiers(stack); for (int i = 0; i < modifierSlots.getSlots(); i++) { ItemStack modifier = modifierSlots.getStackInSlot(i); diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTArmorModifiers.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTArmorModifiers.java index a5ec9b13bf5..83884f256cb 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTArmorModifiers.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTArmorModifiers.java @@ -5,9 +5,8 @@ import com.gregtechceu.gtceu.api.capability.IElectricItem; import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; import com.gregtechceu.gtceu.core.IFireImmuneEntity; - import com.gregtechceu.gtceu.utils.input.KeyBind; -import lombok.extern.slf4j.Slf4j; + import net.minecraft.network.chat.Component; import net.minecraft.tags.DamageTypeTags; import net.minecraft.world.damagesource.DamageTypes; @@ -16,6 +15,8 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.phys.Vec3; +import lombok.extern.slf4j.Slf4j; + import java.util.UUID; @Slf4j @@ -23,7 +24,6 @@ public class GTArmorModifiers { private static final double SPEED_ACCEL = 0.085D; - private static final UUID ADD_ARMOR_UUID = UUID.fromString("95bd81ea-b3af-4cca-8866-f3e62f5f68f1"); public static final ArmorModifier ADD_ARMOR_1 = ArmorModifier.createItemAttribute(GTCEu.id("add_armor_1"), @@ -36,9 +36,9 @@ public class GTArmorModifiers { new AttributeModifier(ADD_ARMOR_UUID, "Armor Modifier", 2.0D, AttributeModifier.Operation.ADDITION), null) .energyUsageOnHit(2048); - public static final ArmorModifier ADD_ARMOR_5 = ArmorModifier.createItemAttribute(GTCEu.id("add_armor_5"), + public static final ArmorModifier ARMOR_PLATE_TUNGSTENSTEEL = ArmorModifier.createItemAttribute(GTCEu.id("add_armor_5"), Attributes.ARMOR, - new AttributeModifier(ADD_ARMOR_UUID, "Armor Modifier", 5.0D, AttributeModifier.Operation.ADDITION), + new AttributeModifier(ADD_ARMOR_UUID, "Armor Modifier", 10.0D, AttributeModifier.Operation.ADDITION), null) .energyUsageOnHit(5120); public static final ArmorModifier SPEED = ArmorModifier.createEntityTick(GTCEu.id("speed"), @@ -92,10 +92,8 @@ public class GTArmorModifiers { }); public static final ArmorModifier DAMAGE_BLOCK = ArmorModifier.createSpecial(GTCEu.id("damage_block")) .onDamage((entity, stack, source, amount) -> { - if (source.is(DamageTypeTags.BYPASSES_INVULNERABILITY) - || source.is(DamageTypeTags.IS_FALL) - || source.is(DamageTypeTags.IS_DROWNING) - || source.is(DamageTypes.STARVE)) { + if (source.is(DamageTypeTags.BYPASSES_INVULNERABILITY) || source.is(DamageTypeTags.IS_FALL) || + source.is(DamageTypeTags.IS_DROWNING) || source.is(DamageTypes.STARVE)) { return new ArmorModifier.DamageModifier.Result(amount); } diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTItems.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTItems.java index 467b345a6b7..9a089a01290 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTItems.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTItems.java @@ -2345,7 +2345,8 @@ public static ItemEntry createFluidCell(Material mat, int capacit .setDefaultMaxModifiers(8)) .lang("QuarkTech™ Suite Chestplate") .properties(p -> p.rarity(Rarity.RARE)) - .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT, ItemTags.FREEZE_IMMUNE_WEARABLES) + .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT, + ItemTags.FREEZE_IMMUNE_WEARABLES) .tag(CustomTags.PPE_ARMOR) .register(); public static ItemEntry QUANTUM_LEGGINGS = REGISTRATE @@ -2397,7 +2398,7 @@ public static ItemEntry createFluidCell(Material mat, int capacit public static ItemEntry ELECTRIC_JETPACK_ADVANCED = REGISTRATE .item("advanced_electric_jetpack", - (p) ->new ArmorComponentItem(GTArmorMaterials.JETPACK, ArmorItem.Type.CHESTPLATE, p) + (p) -> new ArmorComponentItem(GTArmorMaterials.JETPACK, ArmorItem.Type.CHESTPLATE, p) .setArmorLogic(new AdvancedJetpack(512, 6_400_000L * (long) Math.max(1, Math.pow(4, ConfigHolder.INSTANCE.tools.voltageTierAdvImpeller - 4)), @@ -2407,7 +2408,7 @@ public static ItemEntry createFluidCell(Material mat, int capacit .tag(Tags.Items.ARMORS_CHESTPLATES) .register(); public static ItemEntry NANO_CHESTPLATE_ADVANCED = REGISTRATE - .item("avanced_nanomuscle_chestplate", + .item("advanced_nanomuscle_chestplate", (p) -> new ModifiableArmorItem(GTArmorMaterials.ADVANCED_NANO_MUSCLE, ArmorItem.Type.CHESTPLATE, p) .setDefaultMaxModifiers(6)) .lang("Advanced NanoMuscle™ Suite Chestplate") @@ -2420,8 +2421,8 @@ public static ItemEntry createFluidCell(Material mat, int capacit .setDefaultMaxModifiers(8)) .lang("Advanced QuarkTech™ Suite Chestplate") .properties(p -> p.rarity(Rarity.EPIC)) - .tag(Tags.Items.ARMORS_CHESTPLATES, CustomTags.PPE_ARMOR, CustomTags.MODIFIABLE_EQUIPMENT) .tag(Tags.Items.ARMORS_CHESTPLATES) + .tag(CustomTags.MODIFIABLE_EQUIPMENT) .tag(ItemTags.FREEZE_IMMUNE_WEARABLES) .tag(CustomTags.PPE_ARMOR) .register(); diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeTypes.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeTypes.java index 55bec3027b2..0965c2449cc 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeTypes.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipeTypes.java @@ -21,7 +21,6 @@ import com.gregtechceu.gtceu.common.machine.trait.customlogic.*; import com.gregtechceu.gtceu.common.recipe.condition.RockBreakerCondition; import com.gregtechceu.gtceu.common.recipe.type.EquipmentFoundryRecipe; - import com.gregtechceu.gtceu.data.recipe.RecipeUtil; import com.gregtechceu.gtceu.data.recipe.builder.GTRecipeBuilder; import com.gregtechceu.gtceu.integration.kjs.GTRegistryInfo; diff --git a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipes.java b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipes.java index d4e1ef19f78..d4a20b95a00 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipes.java +++ b/src/main/java/com/gregtechceu/gtceu/common/data/GTRecipes.java @@ -47,7 +47,6 @@ public static void recipeAddition(Consumer originalConsumer) { // Decomposition info loading MaterialInfoLoader.init(); - // com.gregtechceu.gtceu.data.recipe.generated.* for (Material material : GTCEuAPI.materialManager.getRegisteredMaterials()) { if (material.hasFlag(MaterialFlags.NO_UNIFICATION)) { diff --git a/src/main/java/com/gregtechceu/gtceu/common/item/armor/GTArmorMaterials.java b/src/main/java/com/gregtechceu/gtceu/common/item/armor/GTArmorMaterials.java index 4ba38417df2..80ffaf25b56 100644 --- a/src/main/java/com/gregtechceu/gtceu/common/item/armor/GTArmorMaterials.java +++ b/src/main/java/com/gregtechceu/gtceu/common/item/armor/GTArmorMaterials.java @@ -31,11 +31,11 @@ public enum GTArmorMaterials implements ArmorMaterial, StringRepresentable { map.put(ArmorItem.Type.HELMET, 0); }), 50, SoundEvents.ARMOR_EQUIP_GENERIC, 0.0F, 0.0F, () -> Ingredient.EMPTY), NANO_MUSCLE("gtceu:nano_muscle_suite", 0, Util.make(new EnumMap<>(ArmorItem.Type.class), map -> { - map.put(ArmorItem.Type.BOOTS, 0); - map.put(ArmorItem.Type.LEGGINGS, 0); - map.put(ArmorItem.Type.CHESTPLATE, 0); - map.put(ArmorItem.Type.HELMET, 0); - }), 50, SoundEvents.ARMOR_EQUIP_GENERIC, 5.0F, 0.0F, () -> Ingredient.EMPTY), + map.put(ArmorItem.Type.BOOTS, 3); + map.put(ArmorItem.Type.LEGGINGS, 4); + map.put(ArmorItem.Type.CHESTPLATE,4); + map.put(ArmorItem.Type.HELMET, 2); + }), 50, SoundEvents.ARMOR_EQUIP_GENERIC, 1.0F, 0.0F, () -> Ingredient.EMPTY), ADVANCED_NANO_MUSCLE("gtceu:advanced_nano_muscle_suite", 0, Util.make(new EnumMap<>(ArmorItem.Type.class), map -> { map.put(ArmorItem.Type.BOOTS, 0); map.put(ArmorItem.Type.LEGGINGS, 0); diff --git a/src/main/java/com/gregtechceu/gtceu/core/mixins/LivingEntityMixin.java b/src/main/java/com/gregtechceu/gtceu/core/mixins/LivingEntityMixin.java index 03c44e9bda6..e31697b171c 100644 --- a/src/main/java/com/gregtechceu/gtceu/core/mixins/LivingEntityMixin.java +++ b/src/main/java/com/gregtechceu/gtceu/core/mixins/LivingEntityMixin.java @@ -2,7 +2,6 @@ import com.gregtechceu.gtceu.api.item.armor.ArmorComponentItem; -import com.gregtechceu.gtceu.api.item.armor.ModifiableArmorItem; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.EquipmentSlot; import net.minecraft.world.entity.LivingEntity; diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/EquipmentFoundryRecipeBuilder.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/EquipmentFoundryRecipeBuilder.java index 6442e03be76..e0d3e7e2da5 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/EquipmentFoundryRecipeBuilder.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/builder/EquipmentFoundryRecipeBuilder.java @@ -2,9 +2,9 @@ import com.gregtechceu.gtceu.api.item.armor.modifier.ArmorModifier; import com.gregtechceu.gtceu.api.recipe.ingredient.NBTIngredient; - import com.gregtechceu.gtceu.common.data.GTRecipeTypes; import com.gregtechceu.gtceu.data.recipe.CustomTags; + import net.minecraft.data.recipes.FinishedRecipe; import net.minecraft.resources.ResourceLocation; import net.minecraft.tags.TagKey; diff --git a/src/main/java/com/gregtechceu/gtceu/data/recipe/misc/EquipmentFoundryRecipes.java b/src/main/java/com/gregtechceu/gtceu/data/recipe/misc/EquipmentFoundryRecipes.java index ed2281e8cc0..2bf9971ad30 100644 --- a/src/main/java/com/gregtechceu/gtceu/data/recipe/misc/EquipmentFoundryRecipes.java +++ b/src/main/java/com/gregtechceu/gtceu/data/recipe/misc/EquipmentFoundryRecipes.java @@ -1,7 +1,9 @@ package com.gregtechceu.gtceu.data.recipe.misc; +import com.gregtechceu.gtceu.api.data.chemical.ChemicalHelper; import com.gregtechceu.gtceu.common.data.GTArmorModifiers; import com.gregtechceu.gtceu.common.data.GTItems; +import com.gregtechceu.gtceu.common.data.GTMaterialItems; import com.gregtechceu.gtceu.data.recipe.CustomTags; import com.gregtechceu.gtceu.data.recipe.VanillaRecipeHelper; @@ -10,8 +12,12 @@ import net.minecraftforge.common.Tags; import net.minecraftforge.common.crafting.IntersectionIngredient; +import java.util.Objects; import java.util.function.Consumer; +import static com.gregtechceu.gtceu.api.data.tag.TagPrefix.plateDense; +import static com.gregtechceu.gtceu.common.data.GTMaterials.*; + public class EquipmentFoundryRecipes { public static void init(Consumer provider) { @@ -20,5 +26,12 @@ public static void init(Consumer provider) { Ingredient.of(CustomTags.MODIFIABLE_EQUIPMENT), Ingredient.of(Tags.Items.ARMORS_LEGGINGS)), GTItems.ELECTRIC_PISTON_EV, GTArmorModifiers.SPEED); + + + VanillaRecipeHelper.addEquipmentFoundryRecipe(provider, "defense_5", + IntersectionIngredient.of( + Ingredient.of(CustomTags.MODIFIABLE_EQUIPMENT), + Ingredient.of(Tags.Items.ARMORS_CHESTPLATES)), + ChemicalHelper.get(plateDense,TungstenSteel), GTArmorModifiers.ARMOR_PLATE_TUNGSTENSTEEL); } } From 1aa4346480dbfb362e4c75984570aee6fcf7c794 Mon Sep 17 00:00:00 2001 From: screret <68943070+screret@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:48:02 +0300 Subject: [PATCH 009/286] commit so I can PR this From d235841d9ca2551a53678a67510fdf35408860c2 Mon Sep 17 00:00:00 2001 From: YoungOnion <39562198+YoungOnionMC@users.noreply.github.com> Date: Sat, 2 Aug 2025 01:50:59 -0600 Subject: [PATCH 010/286] base package for mui (#3603) --- .../gtceu/api/mui/base/GuiAxis.java | 19 + .../gtceu/api/mui/base/IJsonSerializable.java | 48 ++ .../gtceu/api/mui/base/IMathValue.java | 36 ++ .../gtceu/api/mui/base/IMuiScreen.java | 113 +++++ .../gtceu/api/mui/base/IPacketWriter.java | 16 + .../gtceu/api/mui/base/IPanelHandler.java | 86 ++++ .../gtceu/api/mui/base/ITheme.java | 68 +++ .../gtceu/api/mui/base/IThemeApi.java | 142 ++++++ .../gtceu/api/mui/base/IUIHolder.java | 42 ++ .../gtceu/api/mui/base/MCHelper.java | 47 ++ .../gtceu/api/mui/base/UIFactory.java | 112 +++++ .../gtceu/api/mui/base/XeiSettings.java | 121 +++++ .../api/mui/base/drawable/IDrawable.java | 137 ++++++ .../api/mui/base/drawable/IHoverable.java | 23 + .../gtceu/api/mui/base/drawable/IIcon.java | 36 ++ .../api/mui/base/drawable/IInterpolation.java | 17 + .../gtceu/api/mui/base/drawable/IKey.java | 270 ++++++++++ .../mui/base/drawable/IRichTextBuilder.java | 108 ++++ .../api/mui/base/drawable/ITextLine.java | 15 + .../gregtechceu/gtceu/api/mui/base/gui.json | 9 + .../api/mui/base/layout/ILayoutWidget.java | 33 ++ .../api/mui/base/layout/IResizeable.java | 168 +++++++ .../gtceu/api/mui/base/layout/IViewport.java | 124 +++++ .../api/mui/base/layout/IViewportStack.java | 209 ++++++++ .../gtceu/api/mui/base/value/IBoolValue.java | 18 + .../gtceu/api/mui/base/value/IByteValue.java | 28 ++ .../api/mui/base/value/IDoubleValue.java | 8 + .../gtceu/api/mui/base/value/IEnumValue.java | 6 + .../gtceu/api/mui/base/value/IIntValue.java | 8 + .../gtceu/api/mui/base/value/ILongValue.java | 8 + .../api/mui/base/value/IStringValue.java | 8 + .../gtceu/api/mui/base/value/IValue.java | 23 + .../mui/base/value/sync/IBoolSyncValue.java | 37 ++ .../mui/base/value/sync/IByteSyncValue.java | 17 + .../mui/base/value/sync/IDoubleSyncValue.java | 22 + .../mui/base/value/sync/IIntSyncValue.java | 22 + .../mui/base/value/sync/ILongSyncValue.java | 22 + .../value/sync/IServerKeyboardAction.java | 8 + .../base/value/sync/IServerMouseAction.java | 8 + .../mui/base/value/sync/IStringSyncValue.java | 22 + .../base/value/sync/IValueSyncHandler.java | 63 +++ .../gtceu/api/mui/base/widget/IDraggable.java | 70 +++ .../api/mui/base/widget/IFocusedWidget.java | 29 ++ .../gtceu/api/mui/base/widget/IGuiAction.java | 51 ++ .../api/mui/base/widget/IGuiElement.java | 106 ++++ .../api/mui/base/widget/INotifyEnabled.java | 7 + .../api/mui/base/widget/IParentWidget.java | 45 ++ .../api/mui/base/widget/IPositioned.java | 463 ++++++++++++++++++ .../gtceu/api/mui/base/widget/ISynced.java | 90 ++++ .../gtceu/api/mui/base/widget/ITooltip.java | 277 +++++++++++ .../api/mui/base/widget/IValueWidget.java | 14 + .../api/mui/base/widget/IVanillaSlot.java | 16 + .../gtceu/api/mui/base/widget/IWidget.java | 282 +++++++++++ .../api/mui/base/widget/Interactable.java | 213 ++++++++ 54 files changed, 3990 insertions(+) create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/GuiAxis.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/IJsonSerializable.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/IMathValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/IMuiScreen.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/IPacketWriter.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/IPanelHandler.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/ITheme.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/IThemeApi.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/IUIHolder.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/MCHelper.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/UIFactory.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/XeiSettings.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IDrawable.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IHoverable.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IIcon.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IInterpolation.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IKey.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IRichTextBuilder.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/ITextLine.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/gui.json create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/ILayoutWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IResizeable.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewport.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewportStack.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IBoolValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IByteValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IDoubleValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IEnumValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IIntValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/ILongValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IStringValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IBoolSyncValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IByteSyncValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IDoubleSyncValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IIntSyncValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/ILongSyncValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerKeyboardAction.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerMouseAction.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IStringSyncValue.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IValueSyncHandler.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDraggable.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IFocusedWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiAction.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiElement.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/INotifyEnabled.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IParentWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IPositioned.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ISynced.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ITooltip.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IValueWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IVanillaSlot.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/Interactable.java diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/GuiAxis.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/GuiAxis.java new file mode 100644 index 00000000000..df57956d26e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/GuiAxis.java @@ -0,0 +1,19 @@ +package com.gregtechceu.gtceu.api.mui.base; + +public enum GuiAxis { + + X, + Y; + + public boolean isHorizontal() { + return this == X; + } + + public boolean isVertical() { + return this == Y; + } + + public GuiAxis getOther() { + return this == X ? Y : X; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IJsonSerializable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IJsonSerializable.java new file mode 100644 index 00000000000..2782f238e76 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IJsonSerializable.java @@ -0,0 +1,48 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.google.gson.JsonObject; +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.Dynamic; +import com.mojang.serialization.JsonOps; +import org.jetbrains.annotations.ApiStatus; + +public interface IJsonSerializable> { + + /** + * Override this + * + * @return the codec to serialize this object with + */ + // TODO actually implement on subclasses + @ApiStatus.OverrideOnly + default Codec getCodec() { + return Codec.PASSTHROUGH.flatComapMap(dynamic -> { + loadFromJson(dynamic.cast(JsonOps.INSTANCE).getAsJsonObject()); + return (T) this; + }, object -> { + JsonObject jsonObject = new JsonObject(); + if (saveToJson(jsonObject)) { + return DataResult.success(new Dynamic<>(JsonOps.INSTANCE, jsonObject)); + } + return DataResult.error(() -> "Failed to serialize drawable %s".formatted(object)); + }); + } + + /** + * Reads extra json data after this drawable is created. + * + * @param json json to read from + */ + default void loadFromJson(JsonObject json) {} + + /** + * Writes all json data necessary so that deserializing it results in the same drawable. + * + * @param json json to write to + * @return if the drawable was serialized + */ + default boolean saveToJson(JsonObject json) { + return false; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMathValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMathValue.java new file mode 100644 index 00000000000..600f53a5dc1 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMathValue.java @@ -0,0 +1,36 @@ +package com.gregtechceu.gtceu.api.mui.base; + +/** + * Math value interface + *

+ * This interface provides only one method which is used by all + * mathematical related classes. The point of this interface is to + * provide generalized abstract method for computing/fetching some value + * from different mathematical classes. + */ +public interface IMathValue { + + /** + * Get computed or stored value + */ + IMathValue get(); + + boolean isNumber(); + + void set(double value); + + void set(String value); + + double doubleValue(); + + boolean booleanValue(); + + String stringValue(); + + class EvaluateException extends RuntimeException { + + public EvaluateException(String message) { + super(message); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMuiScreen.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMuiScreen.java new file mode 100644 index 00000000000..6980c0fecea --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMuiScreen.java @@ -0,0 +1,113 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.utils.Rectangle; +import com.gregtechceu.gtceu.client.mui.screen.ClientScreenHandler; +import com.gregtechceu.gtceu.client.mui.screen.ContainerScreenWrapper; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.client.mui.screen.ScreenWrapper; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import com.gregtechceu.gtceu.core.mixins.client.AbstractContainerScreenAccessor; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.inventory.Slot; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; + +/** + * Implement this interface on a {@link Screen} to be able to use it as a custom wrapper. + * The Screen should have final {@link ModularScreen} field, which is set from the constructor. + * Additionally, the Screen MUST call {@link ModularScreen#construct(IMuiScreen)} in its constructor. + * See {@link ScreenWrapper ScreenWrapper} and {@link ContainerScreenWrapper GuiContainerWrapper} + * for default implementations. + */ +@OnlyIn(Dist.CLIENT) +public interface IMuiScreen { + + /** + * Returns the {@link ModularScreen} that is being wrapped. This should return a final instance field. + * + * @return the wrapped modular screen + */ + @NotNull + ModularScreen getScreen(); + + /** + * {@link Screen GuiScreens} need to be focused when a text field is focused, to prevent key input from + * behaving unexpectedly. + * + * @param focused if the screen should be focused + */ + default void setFocused(boolean focused) { + getScreen().setFocused(focused); + } + + /** + * This method decides how the gui background is drawn. + * The intended usage is to override {@link Screen#renderBackground(GuiGraphics)} and call this method + * with the super method reference as the second parameter. + * + * @param guiGraphics this screen's {@link GuiGraphics} instance + * @param drawFunction a method reference to draw the world background normally with the + * {@code guiGraphics} as the parameter + */ + @ApiStatus.NonExtendable + default void handleDrawBackground(GuiGraphics guiGraphics, Consumer drawFunction) { + if (ClientScreenHandler.shouldDrawWorldBackground()) { + drawFunction.accept(guiGraphics); + } + ClientScreenHandler.drawDarkBackground(getWrappedScreen(), guiGraphics); + } + + /** + * This method is called every time the {@link ModularScreen} resizes. + * This usually only affects {@link AbstractContainerScreen AbstractContainerScreens}. + * + * @param area area of the main panel + */ + default void updateGuiArea(Rectangle area) { + if (getWrappedScreen() instanceof AbstractContainerScreenAccessor acc) { + acc.setLeftPos(area.x); + acc.setTopPos(area.y); + acc.setImageWidth(area.width); + acc.setImageHeight(area.height); + } + } + + /** + * @return if this wrapper is a {@link AbstractContainerScreen} + */ + @ApiStatus.NonExtendable + default boolean isGuiContainer() { + return getWrappedScreen() instanceof AbstractContainerScreen; + } + + /** + * Hovering widget is handled by {@link ModularGuiContext}. + * If it detects a slot, this method is called. Only affects {@link AbstractContainerScreen + * AbstractContainerScreens}. + * + * @param slot hovered slot + */ + @ApiStatus.NonExtendable + default void setHoveredSlot(Slot slot) { + if (getWrappedScreen() instanceof AbstractContainerScreenAccessor acc) { + acc.setHoveredSlot(slot); + } + } + + /** + * Returns the {@link Screen} that wraps the {@link ModularScreen}. + * In most cases this does not need to be overridden as this interfaces should be implemented on {@link Screen + * Screens}. + * + * @return the wrapping gui screen + */ + default Screen getWrappedScreen() { + return (Screen) this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPacketWriter.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPacketWriter.java new file mode 100644 index 00000000000..93aededd845 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPacketWriter.java @@ -0,0 +1,16 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import net.minecraft.network.FriendlyByteBuf; + +/** + * A function that can write any data to an {@link FriendlyByteBuf}. + */ +public interface IPacketWriter { + + /** + * Writes any data to a packet buffer + * + * @param buffer buffer to write to + */ + void write(FriendlyByteBuf buffer); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPanelHandler.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPanelHandler.java new file mode 100644 index 00000000000..7cd103d7ca8 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPanelHandler.java @@ -0,0 +1,86 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.value.sync.ItemSlotSH; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncHandler; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.SecondaryPanel; +import org.jetbrains.annotations.ApiStatus; + +/** + * This class can handle opening and closing of a {@link ModularPanel}. It makes sure, that the same panel is not + * created multiple + * times and instead reused. + *

+ * Using {@link #openPanel()} is the only way to open multiple panels. + *

+ *

+ * Panels can be closed with {@link #closePanel()}, but also with {@link ModularPanel#closeIfOpen(boolean)} and + * {@link ModularPanel#animateClose()}. With the difference, that the method from this interface also works on server + * side. + *

+ * Synced panels must be created with {@link PanelSyncManager#panel(String, PanelSyncHandler.IPanelBuilder, boolean)}. + * If the panel does not contain any synced widgets, a simple panel handler using + * {@link #simple(ModularPanel, SecondaryPanel.IPanelBuilder, boolean)} + * is likely what you need. + */ +public interface IPanelHandler { + + /** + * Creates a non synced panel handler. Trying to use synced values anyway will result in a crash. + * It only works on client side. Doing anything with it on server side might result in a crash. + * + * @param parent an existing parent panel of the gui + * @param provider the panel builder, that will create the new panel. It must not return null or the main panel. + * @param subPanel true if this panel should close when its parent closes (the parent is defined by the first + * parameter) + * @return a simple panel handler. + * @throws NullPointerException if the build panel of the builder is null + * @throws IllegalArgumentException if the build panel of the builder is the main panel or there are synced values + * in the panel + */ + static IPanelHandler simple(ModularPanel parent, SecondaryPanel.IPanelBuilder provider, boolean subPanel) { + return new SecondaryPanel(parent, provider, subPanel); + } + + boolean isPanelOpen(); + + /** + * Opens the panel. If there is no cached panel, one will be created. + * Can be called on both sides if this handler is synced. + */ + void openPanel(); + + /** + * Initiates the closing animation if the panel is open. + * Can be called on both sides if this handler is synced. + */ + void closePanel(); + + /** + * Initiates the closing animation of all sub panels. + * Usually for internal use. + */ + void closeSubPanels(); + + /** + * Called internally after the panel is closed. + */ + @ApiStatus.OverrideOnly + void closePanelInternal(); + + /** + * Deletes the current cached panel. Should not be used frequently. + * This only works on panels which don't have {@link ItemSlotSH} sync handlers. + * + * @throws UnsupportedOperationException if this handler has ItemSlot sync handlers + */ + void deleteCachedPanel(); + + /** + * If this is a sub panel of another panel. A sub panel will be closed when its parent is closed. + * + * @return true if this is a sub panel + */ + boolean isSubPanel(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/ITheme.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/ITheme.java new file mode 100644 index 00000000000..f53ea99f814 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/ITheme.java @@ -0,0 +1,68 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.theme.WidgetSlotTheme; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTextFieldTheme; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.theme.WidgetThemeSelectable; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; + +/** + * A theme is parsed from json and contains style information like color or background texture. + */ +public interface ITheme { + + /** + * @return the master default theme. + */ + static ITheme getDefault() { + return IThemeApi.get().getDefaultTheme(); + } + + /** + * @param id theme id + * @return theme with given id + */ + static ITheme get(String id) { + return IThemeApi.get().getTheme(id); + } + + /** + * @return theme id + */ + String getId(); + + /** + * @return parent theme + */ + ITheme getParentTheme(); + + WidgetTheme getFallback(); + + WidgetTheme getPanelTheme(); + + WidgetTheme getButtonTheme(); + + WidgetSlotTheme getItemSlotTheme(); + + WidgetSlotTheme getFluidSlotTheme(); + + WidgetTextFieldTheme getTextFieldTheme(); + + WidgetThemeSelectable getToggleButtonTheme(); + + WidgetTheme getWidgetTheme(String id); + + default T getWidgetTheme(Class clazz, String id) { + WidgetTheme theme = getWidgetTheme(id); + if (clazz.isInstance(theme)) { + return (T) theme; + } + return null; + } + + int getOpenCloseAnimationOverride(); + + boolean getSmoothProgressBarOverride(); + + RichTooltip.Pos getTooltipPosOverride(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IThemeApi.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IThemeApi.java new file mode 100644 index 00000000000..384941aaaad --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IThemeApi.java @@ -0,0 +1,142 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.theme.ThemeAPI; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.theme.WidgetThemeParser; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.utils.serialization.json.JsonBuilder; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * An API interface for Themes. + */ +public interface IThemeApi { + + // widget themes + String FALLBACK = "default"; + String PANEL = "panel"; + String BUTTON = "button"; + String ITEM_SLOT = "itemSlot"; + String FLUID_SLOT = "fluidSlot"; + String TEXT_FIELD = "textField"; + String TOGGLE_BUTTON = "toggleButton"; + + // properties + String PARENT = "parent"; + String BACKGROUND = "background"; + String HOVER_BACKGROUND = "hoverBackground"; + String COLOR = "color"; + String TEXT_COLOR = "textColor"; + String TEXT_SHADOW = "textShadow"; + String SLOT_HOVER_COLOR = "slotHoverColor"; + String MARKED_COLOR = "markedColor"; + String HINT_COLOR = "hintColor"; + String SELECTED_BACKGROUND = "selectedBackground"; + String SELECTED_HOVER_BACKGROUND = "selectedHoverBackground"; + String SELECTED_COLOR = "selectedColor"; + String SELECTED_TEXT_COLOR = "selectedTextColor"; + String SELECTED_TEXT_SHADOW = "selectedTextShadow"; + + /** + * @return the default api implementation + */ + @Contract(pure = true) + static IThemeApi get() { + return ThemeAPI.INSTANCE; + } + + /** + * @return the absolute fallback theme + */ + ITheme getDefaultTheme(); + + /** + * Finds a theme for an id + * + * @param id id of the theme + * @return the found theme or {@link #getDefaultTheme()} if no theme was found + */ + @NotNull + ITheme getTheme(String id); + + /** + * @param id id of the theme + * @return if a theme with the id is registered + */ + boolean hasTheme(String id); + + /** + * @param id id of the widget theme + * @return if a widget theme with the id is registered + */ + boolean hasWidgetTheme(String id); + + /** + * Registers a theme json object. Themes from resource packs always have greater priority. + * + * @param id id of the theme + * @param json theme data + */ + void registerTheme(String id, JsonBuilder json); + + /** + * Gets all currently from java side registered theme json's for a theme. + * + * @param id id of the theme + * @return all theme json's for a theme. + */ + List getJavaDefaultThemes(String id); + + /** + * Gets the appropriate theme for a screen. + * + * @param owner owner of the screen + * @param name name of the screen + * @param defaultTheme default theme if no theme was found + * @return the registered theme for the given screen or the given default theme or {@link #getDefaultTheme()} + */ + ITheme getThemeForScreen(String owner, String name, @Nullable String defaultTheme); + + /** + * Gets the appropriate theme for a screen. + * + * @param screen screen + * @param defaultTheme default theme if no theme was found + * @return the registered theme for the given screen or the given default theme or {@link #getDefaultTheme()} + */ + default ITheme getThemeForScreen(ModularScreen screen, @Nullable String defaultTheme) { + return getThemeForScreen(screen.getOwner(), screen.getName(), defaultTheme); + } + + /** + * Registers a theme for a screen. Themes from resource packs always have greater priority. + * + * @param owner owner of the screen + * @param name name of the screen + * @param theme theme to register + */ + default void registerThemeForScreen(String owner, String name, String theme) { + registerThemeForScreen(owner + ":" + name, theme); + } + + /** + * Registers a theme for a screen. Themes from resource packs always have greater priority. + * + * @param screen full screen id + * @param theme theme to register + */ + void registerThemeForScreen(String screen, String theme); + + /** + * Register a widget theme. + * + * @param id id of the widget theme + * @param defaultTheme the fallback widget theme + * @param parser the widget theme json parser function + */ + void registerWidgetTheme(String id, WidgetTheme defaultTheme, WidgetThemeParser parser); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IUIHolder.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IUIHolder.java new file mode 100644 index 00000000000..0cee239b6fa --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IUIHolder.java @@ -0,0 +1,42 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.factory.GuiData; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.client.mui.screen.UISettings; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +/** + * An interface to implement on {@link net.minecraft.world.level.block.entity.BlockEntity} or + * {@link net.minecraft.world.item.Item}. + */ +@FunctionalInterface +public interface IUIHolder { + + /** + * Only called on client side. + * + * @param data information about the creation context + * @param mainPanel the panel created in {@link #buildUI(GuiData, PanelSyncManager, UISettings)} + * @return a modular screen instance with the given panel + */ + @OnlyIn(Dist.CLIENT) + default ModularScreen createScreen(T data, ModularPanel mainPanel) { + return new ModularScreen(mainPanel); + } + + /** + * Called on server and client. Create only the main panel here. Only here you can add sync handlers to widgets + * directly. + * If the widget to be synced is not in this panel yet (f.e. in another panel) the sync handler must be registered + * here + * with {@link PanelSyncManager}. + * + * @param data information about the creation context + * @param syncManager sync handler where widget sync handlers should be registered + * @param settings settings which apply to the whole ui and not just this panel + */ + ModularPanel buildUI(T data, PanelSyncManager syncManager, UISettings settings); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/MCHelper.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/MCHelper.java new file mode 100644 index 00000000000..243cc022e2f --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/MCHelper.java @@ -0,0 +1,47 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; + +import java.util.List; + +public class MCHelper { + + public static Minecraft getMc() { + return Minecraft.getInstance(); + } + + public static LocalPlayer getPlayer() { + return getMc().player; + } + + public static boolean closeScreen() { + LocalPlayer player = getMc().player; + if (player != null) { + player.closeContainer(); + return true; + } + getMc().setScreen(null); + return false; + } + + public static void setScreen(Screen screen) { + getMc().setScreen(screen); + } + + public static Screen getCurrentScreen() { + return getMc().screen; + } + + public static Font getFont() { + return getMc().font; + } + + public static List getItemToolTip(ItemStack item) { + return Screen.getTooltipFromItem(getMc(), item); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/UIFactory.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/UIFactory.java new file mode 100644 index 00000000000..00ca24c6479 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/UIFactory.java @@ -0,0 +1,112 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.factory.GuiData; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.client.mui.screen.*; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * An interface for UI factories. They are responsible for opening synced GUIs and syncing necessary data. + * + * @param gui data type + */ +public interface UIFactory { + + /** + * The name of this factory. Must be constant. + * + * @return the factory name + */ + @NotNull + ResourceLocation getFactoryName(); + + /** + * Creates the main panel for the GUI. Is called on client and server side. + * + * @param guiData gui data + * @param syncManager sync manager + * @param settings ui settings + * @return new main panel + */ + @ApiStatus.OverrideOnly + ModularPanel createPanel(D guiData, PanelSyncManager syncManager, UISettings settings); + + /** + * Creates the screen for the GUI. Is only called on client side. + * + * @param guiData gui data + * @param mainPanel main panel created in {@link #createPanel(GuiData, PanelSyncManager, UISettings)} + * @return new main panel + */ + @OnlyIn(Dist.CLIENT) + @ApiStatus.OverrideOnly + ModularScreen createScreen(D guiData, ModularPanel mainPanel); + + /** + * Creates the screen wrapper for the GUI. Is only called on client side. + * + * @param container container for the gui + * @param screen the screen which was created in {@link #createScreen(GuiData, ModularPanel)} + * @return new screen wrapper + * @throws IllegalStateException if the wrapping screen is not a + * {@link AbstractContainerMenu AbstractContainerMenu} + * or if the container inside is not the same as the one passed to this method. + * This method is not the thrower, but the caller of this method. + */ + @OnlyIn(Dist.CLIENT) + @ApiStatus.OverrideOnly + default IMuiScreen createScreenWrapper(ModularContainerMenu container, ModularScreen screen) { + return new ContainerScreenWrapper(container, screen); + } + + /** + * The default container supplier. This is called when no custom container in {@link UISettings} is set. + * + * @return new container instance + */ + default ModularContainerMenu createContainer(int containerId) { + return new ModularContainerMenu(containerId); + } + + /** + * A default function to check if the current interacting player can interact with the ui. If not overridden on + * {@link UISettings}, + * then this is called every tick while a UI opened by this factory is open. Once this function returns false, the + * UI is immediately + * closed. + * + * @param player current interacting player + * @param guiData gui data of the current ui + * @return if the player can interact with the player. + */ + default boolean canInteractWith(Player player, D guiData) { + return player == guiData.getPlayer(); + } + + /** + * Writes the gui data to a buffer. + * + * @param guiData gui data + * @param buffer buffer + */ + @ApiStatus.OverrideOnly + void writeGuiData(D guiData, FriendlyByteBuf buffer); + + /** + * Reads and creates the gui data from the buffer. + * + * @param player player + * @param buffer buffer + * @return new gui data + */ + @NotNull + @ApiStatus.OverrideOnly + D readGuiData(Player player, FriendlyByteBuf buffer); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/XeiSettings.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/XeiSettings.java new file mode 100644 index 00000000000..acd44cc6d52 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/XeiSettings.java @@ -0,0 +1,121 @@ +package com.gregtechceu.gtceu.api.mui.base; + +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.utils.Rectangle; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.integration.xei.handlers.GhostIngredientSlot; +import org.jetbrains.annotations.ApiStatus; + +/** + * Keeps track of everything related to JEI in a Modular GUI. + * By default, JEI is disabled in client only GUIs. + * This class can be safely interacted with even when JEI/HEI is not installed. + */ +@ApiStatus.NonExtendable +public interface XeiSettings { + + /** + * Force XEI to be enabled + */ + void forceEnabled(); + + /** + * Force XEI to be disabled + */ + void forceDisabled(); + + /** + * Only enabled XEI in synced GUIs + */ + void defaultXei(); + + /** + * Checks if XEI is enabled for a given screen + * + * @param screen modular screen + * @return true if xei is enabled + */ + boolean isEnabled(ModularScreen screen); + + /** + * Adds an exclusion zone. XEI will always try to avoid exclusion zones.
+ * If a widgets wishes to have an exclusion zone it should use {@link #addExclusionArea(IWidget)}! + * + * @param area exclusion area + */ + void addExclusionArea(Rectangle area); + + /** + * Removes an exclusion zone. + * + * @param area exclusion area to remove (must be the same instance) + */ + void removeExclusionArea(Rectangle area); + + /** + * Adds an exclusion zone of a widget. XEI will always try to avoid exclusion zones.
+ * Useful when a widget is outside its panel. + * + * @param area widget + */ + void addExclusionArea(IWidget area); + + /** + * Removes a widget exclusion area. + * + * @param area widget + */ + void removeExclusionArea(IWidget area); + + /** + * Adds a XEI ghost slot. Ghost slots can display an ingredient, but the ingredient does not really exist. + * By calling this method users will be able to drag ingredients from JEI into the slot. + * + * @param slot slot widget + * @param slot widget type + */ + > void addGhostIngredientSlot(W slot); + + /** + * Removes a XEI ghost slot. + * + * @param slot slot widget + * @param slot widget type + */ + > void removeGhostIngredientSlot(W slot); + + XeiSettings DUMMY = new XeiSettings() { + + @Override + public void forceEnabled() {} + + @Override + public void forceDisabled() {} + + @Override + public void defaultXei() {} + + @Override + public boolean isEnabled(ModularScreen screen) { + return false; + } + + @Override + public void addExclusionArea(Rectangle area) {} + + @Override + public void removeExclusionArea(Rectangle area) {} + + @Override + public void addExclusionArea(IWidget area) {} + + @Override + public void removeExclusionArea(IWidget area) {} + + @Override + public > void addGhostIngredientSlot(W slot) {} + + @Override + public > void removeGhostIngredientSlot(W slot) {} + }; +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IDrawable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IDrawable.java new file mode 100644 index 00000000000..16887867e57 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IDrawable.java @@ -0,0 +1,137 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +import com.gregtechceu.gtceu.api.mui.drawable.DrawableStack; +import com.gregtechceu.gtceu.api.mui.drawable.Icon; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.jetbrains.annotations.Nullable; + +/** + * An object which can be drawn. This is mainly used for backgrounds and overlays in + * {@link com.gregtechceu.gtceu.api.mui.base.widget.IWidget}. + */ +public interface IDrawable { + + static IDrawable of(IDrawable... drawables) { + if (drawables == null || drawables.length == 0) { + return null; + } else if (drawables.length == 1) { + return drawables[0]; + } else { + return new DrawableStack(drawables); + } + } + + /** + * Draws this drawable at the given position with the given size. + * + * @param context current context to draw with + * @param x x position + * @param y y position + * @param width draw width + * @param height draw height + * @param widgetTheme current theme + */ + @OnlyIn(Dist.CLIENT) + void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme); + + /** + * Draws this drawable at the current (0|0) with the given size. + * + * @param context gui context + * @param width draw width + * @param height draw height + * @param widgetTheme current theme + */ + @OnlyIn(Dist.CLIENT) + default void drawAtZero(GuiContext context, int width, int height, WidgetTheme widgetTheme) { + draw(context, 0, 0, width, height, widgetTheme); + } + + /** + * Draws this drawable in a given area. + * + * @param context current context to draw with + * @param area draw area + * @param widgetTheme current theme + */ + @OnlyIn(Dist.CLIENT) + default void draw(GuiContext context, Area area, WidgetTheme widgetTheme) { + draw(context, area.x + area.getPadding().left, area.y + area.getPadding().top, area.paddedWidth(), + area.paddedHeight(), widgetTheme); + } + + /** + * Draws this drawable at the current (0|0) with the given area's size. + * + * @param context gui context + * @param area draw area + * @param widgetTheme current theme + */ + @OnlyIn(Dist.CLIENT) + default void drawAtZero(GuiContext context, Area area, WidgetTheme widgetTheme) { + draw(context, 0, 0, area.paddedWidth(), area.paddedHeight(), widgetTheme); + } + + /** + * @return if theme color can be applied on this drawable + */ + default boolean canApplyTheme() { + return false; + } + + /** + * @return a widget with this drawable as a background + */ + default Widget asWidget() { + return new DrawableWidget(this); + } + + /** + * @return this drawable as an icon + */ + default Icon asIcon() { + return new Icon(this); + } + + /** + * An empty drawable. Does nothing. + */ + IDrawable EMPTY = (context, x, y, width, height, widgetTheme) -> {}; + + /** + * An empty drawable used to mark hover textures as "should not be used"! + */ + IDrawable NONE = (context, x, y, width, height, widgetTheme) -> {}; + + static boolean isVisible(@Nullable IDrawable drawable) { + if (drawable == null || drawable == EMPTY || drawable == NONE) return false; + if (drawable instanceof DrawableStack array) { + return array.getDrawables().length > 0; + } + return true; + } + + /** + * A widget wrapping a drawable. The drawable is drawn between the background and the overlay. + */ + class DrawableWidget extends Widget { + + private final IDrawable drawable; + + public DrawableWidget(IDrawable drawable) { + this.drawable = drawable; + } + + @OnlyIn(Dist.CLIENT) + @Override + public void draw(ModularGuiContext context, WidgetTheme widgetTheme) { + this.drawable.drawAtZero(context, getArea(), widgetTheme); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IHoverable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IHoverable.java new file mode 100644 index 00000000000..4f397651bb9 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IHoverable.java @@ -0,0 +1,23 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; +import org.jetbrains.annotations.Nullable; + +public interface IHoverable extends IIcon { + + /** + * Called every frame this hoverable is hovered inside a + * {@link com.gregtechceu.gtceu.api.mui.drawable.text.RichText}. + */ + default void onHover() {} + + @Nullable + default RichTooltip getTooltip() { + return null; + } + + void setRenderedAt(int x, int y); + + Area getRenderedArea(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IIcon.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IIcon.java new file mode 100644 index 00000000000..2c40c4199d3 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IIcon.java @@ -0,0 +1,36 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +import com.gregtechceu.gtceu.api.mui.drawable.HoverableIcon; +import com.gregtechceu.gtceu.api.mui.drawable.InteractableIcon; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Box; + +/** + * A {@link IDrawable} with a fixed size. + */ +public interface IIcon extends IDrawable { + + /** + * @return width of this icon or 0 if the width should be dynamic + */ + int getWidth(); + + /** + * @return height of this icon or 0 of the height should be dynamic + */ + int getHeight(); + + /** + * @return the margin of this icon. Only used if width or height is 0 + */ + Box getMargin(); + + default HoverableIcon asHoverable() { + return new HoverableIcon(this); + } + + default InteractableIcon asInteractable() { + return new InteractableIcon(this); + } + + IIcon EMPTY_2PX = EMPTY.asIcon().height(2); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IInterpolation.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IInterpolation.java new file mode 100644 index 00000000000..d008d1ccc81 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IInterpolation.java @@ -0,0 +1,17 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +/** + * A function which interpolates between two values. + */ +public interface IInterpolation { + + /** + * Calculates a new value between a and b based on a curve. + * + * @param a start value + * @param b end value + * @param x progress (between 0.0 and 1.0) + * @return new value + */ + float interpolate(float a, float b, float x); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IKey.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IKey.java new file mode 100644 index 00000000000..f8b26602183 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IKey.java @@ -0,0 +1,270 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +import com.google.gson.JsonObject; +import com.gregtechceu.gtceu.api.mui.base.IJsonSerializable; +import com.gregtechceu.gtceu.api.mui.drawable.text.*; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.widgets.TextWidget; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.utils.serialization.json.JsonHelper; +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * This represents a piece of text in a GUI. + */ +public interface IKey extends IDrawable, IJsonSerializable { + + int TEXT_COLOR = 0xFF404040; + + TextRenderer renderer = new TextRenderer(); + + IKey EMPTY = str(""); + IKey LINE_FEED = str("\n"); + IKey SPACE = str(" "); + + // Formatting for convenience + ChatFormatting BLACK = ChatFormatting.BLACK; + ChatFormatting DARK_BLUE = ChatFormatting.DARK_BLUE; + ChatFormatting DARK_GREEN = ChatFormatting.DARK_GREEN; + ChatFormatting DARK_AQUA = ChatFormatting.DARK_AQUA; + ChatFormatting DARK_RED = ChatFormatting.DARK_RED; + ChatFormatting DARK_PURPLE = ChatFormatting.DARK_PURPLE; + ChatFormatting GOLD = ChatFormatting.GOLD; + ChatFormatting GRAY = ChatFormatting.GRAY; + ChatFormatting DARK_GRAY = ChatFormatting.DARK_GRAY; + ChatFormatting BLUE = ChatFormatting.BLUE; + ChatFormatting GREEN = ChatFormatting.GREEN; + ChatFormatting AQUA = ChatFormatting.AQUA; + ChatFormatting RED = ChatFormatting.RED; + ChatFormatting LIGHT_PURPLE = ChatFormatting.LIGHT_PURPLE; + ChatFormatting YELLOW = ChatFormatting.YELLOW; + ChatFormatting WHITE = ChatFormatting.WHITE; + ChatFormatting OBFUSCATED = ChatFormatting.OBFUSCATED; + ChatFormatting BOLD = ChatFormatting.BOLD; + ChatFormatting STRIKETHROUGH = ChatFormatting.STRIKETHROUGH; + ChatFormatting UNDERLINE = ChatFormatting.UNDERLINE; + ChatFormatting ITALIC = ChatFormatting.ITALIC; + ChatFormatting RESET = ChatFormatting.RESET; + + /** + * Creates a translated text. + * + * @param key translation key + * @return text key + */ + static IKey lang(@NotNull String key) { + return new LangKey(key); + } + + /** + * Creates a translated text. + * + * @param component translation component + * @return text key + */ + static IKey lang(@NotNull Component component) { + return new LangKey(component); + } + + /** + * Creates a translated text with arguments. The arguments can change. + * + * @param key translation key + * @param args translation arguments + * @return text key + */ + static IKey lang(@NotNull String key, @Nullable Object... args) { + return new LangKey(key, args); + } + + /** + * Creates a translated text with arguments supplier. + * + * @param key translation key + * @param argsSupplier translation arguments supplier + * @return text key + */ + static IKey lang(@NotNull String key, @NotNull Supplier argsSupplier) { + return new LangKey(key, argsSupplier); + } + + /** + * Creates a translated text. + * + * @param keySupplier translation key supplier + * @return text key + */ + static IKey lang(@NotNull Supplier keySupplier) { + return new LangKey(keySupplier); + } + + /** + * Creates a translated text with arguments supplier. + * + * @param keySupplier translation key supplier + * @param argsSupplier translation arguments supplier + * @return text key + */ + static IKey lang(@NotNull Supplier keySupplier, @NotNull Supplier argsSupplier) { + return new LangKey(keySupplier, argsSupplier); + } + + /** + * Creates a string literal text. + * + * @param key string + * @return text key + */ + static IKey str(@NotNull String key) { + return new StringKey(key); + } + + /** + * Creates a formatted string literal text with arguments. The arguments can be dynamic. + * The string is formatted using {@link String#format(String, Object...)}. + * + * @param key string + * @param args arguments + * @return text key + */ + static IKey str(@NotNull String key, @Nullable Object... args) { + return new StringKey(key, args); + } + + /** + * Creates a composed text key. + * + * @param keys text keys + * @return composed text key. + */ + static IKey comp(@NotNull IKey... keys) { + return new CompoundKey(keys); + } + + /** + * Creates a dynamic text key. + * + * @param getter string supplier + * @return dynamic text key + */ + static IKey dynamic(@NotNull Supplier<@NotNull Component> getter) { + return new DynamicKey(getter); + } + + /** + * @return the current unformatted string + */ + MutableComponent get(); + + /** + * @param parentFormatting formatting of the parent in case of composite keys + * @return the current formatted string + */ + default MutableComponent getFormatted(@Nullable FormattingState parentFormatting) { + return get(); + } + + /** + * @return the current formatted string + */ + default MutableComponent getFormatted() { + return getFormatted(null); + } + + @OnlyIn(Dist.CLIENT) + @Override + default void draw(GuiContext context, int x, int y, int width, int height, WidgetTheme widgetTheme) { + renderer.setColor(widgetTheme.getTextColor()); + renderer.setShadow(widgetTheme.getTextShadow()); + renderer.setAlignment(Alignment.Center, width, height); + renderer.setScale(1f); + renderer.setPos(x, y); + renderer.draw(context.getGraphics(), getFormatted()); + } + + @Override + default TextWidget asWidget() { + return new TextWidget(this); + } + + default StyledText withStyle() { + return new StyledText(this); + } + + default AnimatedText withAnimation() { + return new AnimatedText(this); + } + + /** + * @return a formatting state of this key + */ + default @Nullable FormattingState getFormatting() { + return null; + } + + /** + * Set text formatting to this key. If {@link IKey#RESET} is used, then that's applied first and then all other + * formatting of this key. + * With {@code null}, you can remove a color formatting. No matter the parents color, the default color will be + * used. + * + * @param formatting a formatting rule + * @return this + */ + IKey style(@Nullable ChatFormatting formatting); + + default IKey style(ChatFormatting... formatting) { + for (ChatFormatting cf : formatting) style(cf); + return this; + } + + default IKey removeFormatColor() { + return style((ChatFormatting) null); + } + + IKey removeStyle(); + + default StyledText alignment(Alignment alignment) { + return withStyle().alignment(alignment); + } + + default StyledText color(@Nullable Integer color) { + return withStyle().color(color); + } + + default StyledText scale(float scale) { + return withStyle().scale(scale); + } + + default StyledText shadow(@Nullable Boolean shadow) { + return withStyle().shadow(shadow); + } + + default KeyIcon asTextIcon() { + return new KeyIcon(this); + } + + @Override + default void loadFromJson(JsonObject json) { + if (json.has("color") || json.has("shadow") || json.has("align") || json.has("alignment") || + json.has("scale")) { + StyledText styledText = this instanceof StyledText styledText1 ? styledText1 : withStyle(); + if (json.has("color")) { + styledText.color(JsonHelper.getInt(json, 0, "color")); + } + styledText.shadow(JsonHelper.getBoolean(json, false, "shadow")); + styledText.alignment( + JsonHelper.deserialize(json, Alignment.class, styledText.getAlignment(), "align", "alignment")); + styledText.scale(JsonHelper.getFloat(json, 1, "scale")); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IRichTextBuilder.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IRichTextBuilder.java new file mode 100644 index 00000000000..35b1cafc62d --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IRichTextBuilder.java @@ -0,0 +1,108 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +import com.gregtechceu.gtceu.api.mui.drawable.text.Spacer; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import net.minecraft.network.chat.Component; + +public interface IRichTextBuilder> { + + T getThis(); + + IRichTextBuilder getRichText(); + + default T add(Component c) { + getRichText().add(c); + return getThis(); + } + + default T add(String s) { + getRichText().add(s); + return getThis(); + } + + default T add(IDrawable drawable) { + getRichText().add(drawable); + return getThis(); + } + + default T addLine(String s) { + getRichText().add(s).newLine(); + return getThis(); + } + + default T addLine(ITextLine line) { + getRichText().addLine(line); + return getThis(); + } + + default T addLine(IDrawable line) { + getRichText().add(line).newLine(); + return getThis(); + } + + default T newLine() { + return add(IKey.LINE_FEED); + } + + default T space() { + return add(IKey.SPACE); + } + + default T spaceLine(int pixelSpace) { + return addLine(Spacer.of(pixelSpace)); + } + + default T spaceLine() { + return addLine(Spacer.SPACER_2PX); + } + + default T emptyLine() { + return addLine(Spacer.LINE_SPACER); + } + + default T addElements(Iterable drawables) { + for (IDrawable drawable : drawables) { + getRichText().add(drawable); + } + return getThis(); + } + + default T addDrawableLines(Iterable drawables) { + for (IDrawable drawable : drawables) { + getRichText().add(drawable).newLine(); + } + return getThis(); + } + + default T addStringLines(Iterable drawables) { + for (String drawable : drawables) { + getRichText().add(drawable).newLine(); + } + return getThis(); + } + + default T clearText() { + getRichText().clearText(); + return getThis(); + } + + default T alignment(Alignment alignment) { + getRichText().alignment(alignment); + return getThis(); + } + + default T textColor(int color) { + getRichText().textColor(color); + return getThis(); + } + + default T scale(float scale) { + getRichText().scale(scale); + return getThis(); + } + + default T textShadow(boolean shadow) { + getRichText().textShadow(shadow); + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/ITextLine.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/ITextLine.java new file mode 100644 index 00000000000..407d374a81c --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/ITextLine.java @@ -0,0 +1,15 @@ +package com.gregtechceu.gtceu.api.mui.base.drawable; + +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import net.minecraft.client.gui.Font; + +public interface ITextLine { + + int getWidth(); + + int getHeight(Font font); + + void draw(GuiContext context, Font font, float x, float y, int color, boolean shadow); + + Object getHoveringElement(Font font, int x, int y); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/gui.json b/src/main/java/com/gregtechceu/gtceu/api/mui/base/gui.json new file mode 100644 index 00000000000..1e2c6b1ae07 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/gui.json @@ -0,0 +1,9 @@ +{ + "name": "test", + "gui": [ + { + "widget": "image", + "min": 50 + } + ] +} \ No newline at end of file diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/ILayoutWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/ILayoutWidget.java new file mode 100644 index 00000000000..6d43de0cfae --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/ILayoutWidget.java @@ -0,0 +1,33 @@ +package com.gregtechceu.gtceu.api.mui.base.layout; + +import com.gregtechceu.gtceu.api.mui.base.widget.INotifyEnabled; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; + +/** + * This is responsible for laying out widgets. + */ +public interface ILayoutWidget extends INotifyEnabled { + + /** + * Called after the children tried to calculate their size. + * Might be called multiple times. + */ + void layoutWidgets(); + + /** + * Called after post calculation of this widget. + * Might be called multiple times. + * The last call guarantees, that this widget is fully calculated. + */ + default void postLayoutWidgets() {} + + /** + * Called when determining wrapping size of this widget. + * If this method returns true, size and margin of the queried child will be ignored for calculation. + * Typically return true when the child is disabled and you want to collapse it for layout. + * This method should also be used for layouting children with {@link #layoutWidgets} if it might return true. + */ + default boolean shouldIgnoreChildSize(IWidget child) { + return false; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IResizeable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IResizeable.java new file mode 100644 index 00000000000..8599e9df504 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IResizeable.java @@ -0,0 +1,168 @@ +package com.gregtechceu.gtceu.api.mui.base.layout; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; + +/** + * An interface that handles resizing of widgets. + * Usually this interface is not implemented by the users of this library or will even interact with it. + */ +public interface IResizeable { + + /** + * Called once before resizing + */ + void initResizing(); + + /** + * Resizes the given element + * + * @param guiElement element to resize + * @return true if element is fully resized + */ + boolean resize(IGuiElement guiElement); + + /** + * Called if {@link #resize(IGuiElement)} returned false after children have been resized. + * + * @param guiElement element to resize + * @return if element is fully resized + */ + boolean postResize(IGuiElement guiElement); + + /** + * Called after all elements in the tree are resized and the absolute positions needs to be calculated from the + * relative position. + * + * @param guiElement element that was resized + */ + default void applyPos(IGuiElement guiElement) {} + + /** + * @return area of the element + */ + // TODO doesnt fit with the other api methods in this interface + Area getArea(); + + /** + * @return true if the relative x position is calculated + */ + boolean isXCalculated(); + + /** + * @return true if the relative y position is calculated + */ + boolean isYCalculated(); + + /** + * @return true if the width is calculated + */ + boolean isWidthCalculated(); + + /** + * @return true if the height is calculated + */ + boolean isHeightCalculated(); + + default boolean isSizeCalculated(GuiAxis axis) { + return axis.isHorizontal() ? isWidthCalculated() : isHeightCalculated(); + } + + default boolean isPosCalculated(GuiAxis axis) { + return axis.isHorizontal() ? isXCalculated() : isYCalculated(); + } + + /** + * @return true if the relative position and size are fully calculated + */ + default boolean isFullyCalculated() { + return isXCalculated() && isYCalculated() && isWidthCalculated() && isHeightCalculated(); + } + + /** + * Marks position and size as calculated. + */ + void setResized(boolean x, boolean y, boolean w, boolean h); + + default void setPosResized(boolean x, boolean y) { + setResized(x, y, isWidthCalculated(), isHeightCalculated()); + } + + default void setSizeResized(boolean w, boolean h) { + setResized(isXCalculated(), isYCalculated(), w, h); + } + + default void setXResized(boolean v) { + setResized(v, isYCalculated(), isWidthCalculated(), isHeightCalculated()); + } + + default void setYResized(boolean v) { + setResized(isXCalculated(), v, isWidthCalculated(), isHeightCalculated()); + } + + default void setPosResized(GuiAxis axis, boolean v) { + if (axis.isHorizontal()) { + setXResized(v); + } else { + setYResized(v); + } + } + + default void setWidthResized(boolean v) { + setResized(isXCalculated(), isYCalculated(), v, isHeightCalculated()); + } + + default void setHeightResized(boolean v) { + setResized(isXCalculated(), isYCalculated(), isWidthCalculated(), v); + } + + default void setSizeResized(GuiAxis axis, boolean v) { + if (axis.isHorizontal()) { + setWidthResized(v); + } else { + setHeightResized(v); + } + } + + default void setResized(boolean b) { + setResized(b, b, b, b); + } + + /** + * Sets if margin and padding on the x-axis is applied + * + * @param b true if margin and padding are applied + */ + void setXMarginPaddingApplied(boolean b); + + /** + * Sets if margin and padding on the y-axis is applied + * + * @param b true if margin and padding are applied + */ + void setYMarginPaddingApplied(boolean b); + + default void setMarginPaddingApplied(boolean b) { + setXMarginPaddingApplied(b); + setYMarginPaddingApplied(b); + } + + default void setMarginPaddingApplied(GuiAxis axis, boolean b) { + if (axis.isHorizontal()) { + setXMarginPaddingApplied(b); + } else { + setYMarginPaddingApplied(b); + } + } + + /** + * @return true if margin and padding are applied on the x-axis + */ + boolean isXMarginPaddingApplied(); + + /** + * @return true if margin and padding are applied on the y-axis + */ + boolean isYMarginPaddingApplied(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewport.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewport.java new file mode 100644 index 00000000000..0f5bca6e666 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewport.java @@ -0,0 +1,124 @@ +package com.gregtechceu.gtceu.api.mui.base.layout; + +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.utils.HoveredWidgetList; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import java.util.function.Predicate; + +/** + * A gui element which can transform its children f.e. a scrollable list. + */ +public interface IViewport { + + /** + * Apply shifts of this viewport. + * + * @param stack viewport stack + */ + default void transformChildren(IViewportStack stack) {} + + /** + * Gathers all children at a position. Transformations from this viewport are already applied. + * + * @param stack current viewport stack. Should not be modified. + * @param widgets widget list of already gathered widgets. Add children here. + * @param x x position + * @param y y position + */ + void getWidgetsAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y); + + /** + * Gathers all children at a position. Transformations from this viewport are not applied. + * Called before {@link #getWidgetsAt(IViewportStack, HoveredWidgetList, int, int)} + * + * @param stack current viewport stack. Should not be modified. + * @param widgets widget list of already gathered widgets. Add children here. + * @param x x position + * @param y y position + */ + default void getSelfAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y) {} + + /** + * Called during drawing twice (before children are drawn). Once with transformation of this viewport and once + * without + * + * @param context gui context + * @param transformed if transformation from this viewport is active + */ + default void preDraw(ModularGuiContext context, boolean transformed) {} + + /** + * Called during drawing twice (after children are drawn). Once with transformation of this viewport and once + * without + * + * @param context gui context + * @param transformed if transformation from this viewport is active + */ + default void postDraw(ModularGuiContext context, boolean transformed) {} + + static void getChildrenAt(IWidget parent, IViewportStack stack, HoveredWidgetList widgetList, int x, int y) { + for (IWidget child : parent.getChildren()) { + if (!child.isEnabled()) { + continue; + } + if (child instanceof IViewport viewport) { + stack.pushViewport(viewport, parent.getArea()); + child.transform(stack); + viewport.getSelfAt(stack, widgetList, x, y); + viewport.transformChildren(stack); + viewport.getWidgetsAt(stack, widgetList, x, y); + stack.popViewport(viewport); + } else { + stack.pushMatrix(); + child.transform(stack); + if (child.isInside(stack, x, y)) { + widgetList.add(child, stack.peek()); + } + if (child.hasChildren()) { + getChildrenAt(child, stack, widgetList, x, y); + } + stack.popMatrix(); + } + } + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + static boolean forEachChild(IViewportStack stack, IWidget parent, Predicate predicate, int context) { + for (IWidget child : parent.getChildren()) { + if (!child.isEnabled()) { + continue; + } + stack.popMatrix(); + if (child instanceof IViewport viewport) { + stack.pushViewport(viewport, parent.getArea()); + parent.transform(stack); + if (!predicate.test(child)) { + stack.popViewport(viewport); + return false; + } + viewport.transformChildren(parent.getContext()); + if (child.hasChildren() && !forEachChild(stack, child, predicate, context)) { + stack.popViewport(viewport); + return false; + } + stack.popViewport(viewport); + } else { + stack.pushMatrix(); + parent.transform(stack); + if (!predicate.test(child)) { + stack.popMatrix(); + return false; + } + if (child.hasChildren() && !forEachChild(stack, child, predicate, context)) { + stack.popMatrix(); + return false; + } + stack.popMatrix(); + } + } + return true; + } + + IViewport EMPTY = (viewports, widgets, x, y) -> {}; +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewportStack.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewportStack.java new file mode 100644 index 00000000000..61d97789bf1 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewportStack.java @@ -0,0 +1,209 @@ +package com.gregtechceu.gtceu.api.mui.base.layout; + +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.viewport.TransformationMatrix; +import com.mojang.blaze3d.vertex.PoseStack; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +/** + * This handles all viewports in a GUI. Also keeps track of a matrix stack used for rendering and + * user interaction. + */ +public interface IViewportStack { + + /** + * Reset all viewports and the matrix stack. + */ + void reset(); + + /** + * @return current viewport + */ + Area getViewport(); + + /** + * Pushes a viewport to the top. Also pushes a new matrix. + * + * @param viewport viewport to push + * @param area area of the viewport + */ + void pushViewport(IViewport viewport, Area area); + + /** + * Only pushes a matrix without a viewport. + */ + void pushMatrix(); + + /** + * Removes the top viewport and its matrix from the stack. + * + * @param viewport viewport to remove from the top. + * @throws IllegalStateException if the given viewport doesn't match the viewport at the top. + */ + void popViewport(IViewport viewport); + + /** + * Removes the top matrix from the stack. + * + * @throws IllegalStateException if the top matrix is a viewport. + */ + void popMatrix(); + + /** + * @return the matrix stack size + */ + int getStackSize(); + + /** + * Removes all matrices ABOVE the given index. + * + * @param index matrices are removed above this index. + */ + void popUntilIndex(int index); + + /** + * Removes all matrices ABOVE the given viewport. + * + * @param viewport matrices are removed above this viewport. + */ + void popUntilViewport(IViewport viewport); + + /** + * Applies translation transformation to the current top matrix. + * + * @param x translation in x + * @param y translation in y + */ + void translate(float x, float y); + + /** + * Applies translation transformation to the current top matrix. + * + * @param x translation in x + * @param y translation in y + * @param z translation in z + */ + void translate(float x, float y, float z); + + /** + * Applies rotation transformation to the current top matrix. + * + * @param angle clockwise rotation angle in radians + * @param x x-axis rotation. 1 for yes, 0 for no + * @param y y-axis rotation. 1 for yes, 0 for no + * @param z z-axis rotation. 1 for yes, 0 for no + */ + void rotate(float angle, float x, float y, float z); + + /** + * Applies rotation transformation to the current top matrix around z. + * + * @param angle clockwise rotation angle in radians + */ + void rotateZ(float angle); + + /** + * Applies scaling transformation to the current top matrix around. + * + * @param x x scale factor + * @param y y scale factor + */ + void scale(float x, float y); + + void multiply(Matrix4f matrix); + + /** + * Resets the top matrix to the matrix below. + */ + void resetCurrent(); + + /** + * Transforms the x component of a position with the current matrix transformations. + * + * @param x x component of position + * @param y y component of position + * @return transformed x component + */ + int transformX(float x, float y); + + /** + * Transforms the y component of a position with the current matrix transformations. + * + * @param x x component of position + * @param y y component of position + * @return transformed y component + */ + int transformY(float x, float y); + + /** + * Transforms the x component of a position with the current inverted matrix transformations. + * + * @param x x component of position + * @param y y component of position + * @return un-transformed x component + */ + int unTransformX(float x, float y); + + /** + * Transforms the y component of a position with the current inverted matrix transformations. + * + * @param x x component of position + * @param y y component of position + * @return un-transformed y component + */ + int unTransformY(float x, float y); + + /** + * Transforms a vector with the current matrix transformations. + * This modifies the given vector. + * + * @param vec vector to transform + * @return transformed vector + */ + default Vector3f transform(Vector3f vec) { + return transform(vec, vec); + } + + /** + * Transforms a vector with the current matrix transformations. + * + * @param vec vector to transform + * @param dest vector to write the result to + * @return transformed vector + */ + Vector3f transform(Vector3f vec, Vector3f dest); + + /** + * Transforms a vector with the current inverted matrix transformations. + * This modifies the given vector. + * + * @param vec vector to un-transform + * @return un-transformed vector + */ + default Vector3f unTransform(Vector3f vec) { + return unTransform(vec, vec); + } + + /** + * Transforms a vector with the current inverted matrix transformations. + * This modifies the given vector. + * + * @param vec vector to un-transform + * @param dest vector to write the result to + * @return un-transformed vector + */ + Vector3f unTransform(Vector3f vec, Vector3f dest); + + /** + * Applies the current matrix transformations the current OpenGL matrix. + */ + void applyTo(PoseStack poseStack); + + /** + * @return the top matrix or null if stack is empty + */ + @Nullable + TransformationMatrix peek(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IBoolValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IBoolValue.java new file mode 100644 index 00000000000..df57a91e30d --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IBoolValue.java @@ -0,0 +1,18 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IBoolValue extends IValue, IIntValue { + + boolean getBoolValue(); + + void setBoolValue(boolean val); + + @Override + default int getIntValue() { + return getBoolValue() ? 1 : 0; + } + + @Override + default void setIntValue(int val) { + setBoolValue(val == 1); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IByteValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IByteValue.java new file mode 100644 index 00000000000..89be4e97256 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IByteValue.java @@ -0,0 +1,28 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IByteValue extends IIntValue, IStringValue { + + @Override + default void setIntValue(int val) { + setByteValue((byte) val); + } + + @Override + default void setStringValue(String val) { + setByteValue(Byte.parseByte(val)); + } + + @Override + default int getIntValue() { + return getByteValue(); + } + + @Override + default String getStringValue() { + return String.valueOf(getByteValue()); + } + + void setByteValue(byte b); + + byte getByteValue(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IDoubleValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IDoubleValue.java new file mode 100644 index 00000000000..0e50cbb29e8 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IDoubleValue.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IDoubleValue extends IValue { + + double getDoubleValue(); + + void setDoubleValue(double val); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IEnumValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IEnumValue.java new file mode 100644 index 00000000000..82c60f85aa6 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IEnumValue.java @@ -0,0 +1,6 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IEnumValue> { + + Class getEnumClass(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IIntValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IIntValue.java new file mode 100644 index 00000000000..c61b4fe51a4 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IIntValue.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IIntValue extends IValue { + + int getIntValue(); + + void setIntValue(int val); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/ILongValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/ILongValue.java new file mode 100644 index 00000000000..fe271ae8e4c --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/ILongValue.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface ILongValue extends IValue { + + long getLongValue(); + + void setLongValue(long val); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IStringValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IStringValue.java new file mode 100644 index 00000000000..c6983326c70 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IStringValue.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +public interface IStringValue extends IValue { + + String getStringValue(); + + void setStringValue(String val); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IValue.java new file mode 100644 index 00000000000..e607f6e7a97 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/IValue.java @@ -0,0 +1,23 @@ +package com.gregtechceu.gtceu.api.mui.base.value; + +/** + * A value wrapper for widgets. + * + * @param value type + */ +public interface IValue { + + /** + * Gets the current value. + * + * @return the current value + */ + T getValue(); + + /** + * Updates the current value. + * + * @param value new value + */ + void setValue(T value); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IBoolSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IBoolSyncValue.java new file mode 100644 index 00000000000..a043de7df29 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IBoolSyncValue.java @@ -0,0 +1,37 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IBoolValue; + +/** + * A helper interface for sync values which can be turned into an integer. + * + * @param value type + */ +public interface IBoolSyncValue extends IValueSyncHandler, IBoolValue, IIntSyncValue { + + @Override + default void setBoolValue(boolean val) { + setBoolValue(val, true, true); + } + + default void setBoolValue(boolean val, boolean setSource) { + setBoolValue(val, setSource, true); + } + + void setBoolValue(boolean value, boolean setSource, boolean sync); + + @Override + default void setIntValue(int value, boolean setSource, boolean sync) { + setBoolValue(value == 1, setSource, sync); + } + + @Override + default int getIntValue() { + return IBoolValue.super.getIntValue(); + } + + @Override + default void setIntValue(int val) { + IBoolValue.super.setIntValue(val); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IByteSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IByteSyncValue.java new file mode 100644 index 00000000000..c47b3e2da37 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IByteSyncValue.java @@ -0,0 +1,17 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IByteValue; + +public interface IByteSyncValue extends IByteValue, IValueSyncHandler { + + @Override + default void setByteValue(byte val) { + setByteValue(val, true); + } + + default void setByteValue(byte val, boolean setSource) { + setByteValue(val, setSource, true); + } + + void setByteValue(byte value, boolean setSource, boolean sync); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IDoubleSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IDoubleSyncValue.java new file mode 100644 index 00000000000..b1c9aa10f0e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IDoubleSyncValue.java @@ -0,0 +1,22 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IDoubleValue; + +/** + * A helper interface for sync values which can be turned into an integer. + * + * @param value type + */ +public interface IDoubleSyncValue extends IValueSyncHandler, IDoubleValue { + + @Override + default void setDoubleValue(double val) { + setDoubleValue(val, true, true); + } + + default void setDoubleValue(double val, boolean setSource) { + setDoubleValue(val, setSource, true); + } + + void setDoubleValue(double value, boolean setSource, boolean sync); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IIntSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IIntSyncValue.java new file mode 100644 index 00000000000..630bd244375 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IIntSyncValue.java @@ -0,0 +1,22 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IIntValue; + +/** + * A helper interface for sync values which can be turned into an integer. + * + * @param value type + */ +public interface IIntSyncValue extends IValueSyncHandler, IIntValue { + + @Override + default void setIntValue(int val) { + setIntValue(val, true, true); + } + + default void setIntValue(int val, boolean setSource) { + setIntValue(val, setSource, true); + } + + void setIntValue(int value, boolean setSource, boolean sync); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/ILongSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/ILongSyncValue.java new file mode 100644 index 00000000000..5259e61ba0a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/ILongSyncValue.java @@ -0,0 +1,22 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.ILongValue; + +/** + * A helper interface for sync values which can be turned into an integer. + * + * @param value type + */ +public interface ILongSyncValue extends IValueSyncHandler, ILongValue { + + @Override + default void setLongValue(long val) { + setLongValue(val, true, true); + } + + default void setLongValue(long val, boolean setSource) { + setLongValue(val, setSource, true); + } + + void setLongValue(long value, boolean setSource, boolean sync); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerKeyboardAction.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerKeyboardAction.java new file mode 100644 index 00000000000..412770b744a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerKeyboardAction.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.utils.KeyboardData; + +public interface IServerKeyboardAction { + + void onServerKeyboardAction(KeyboardData data); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerMouseAction.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerMouseAction.java new file mode 100644 index 00000000000..bc9e0bcca2e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IServerMouseAction.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.utils.MouseData; + +public interface IServerMouseAction { + + void onServerMouseAction(MouseData mouseData); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IStringSyncValue.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IStringSyncValue.java new file mode 100644 index 00000000000..1a73864b374 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IStringSyncValue.java @@ -0,0 +1,22 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IStringValue; + +/** + * A helper interface for sync values which can be turned into a string. + * + * @param value type + */ +public interface IStringSyncValue extends IValueSyncHandler, IStringValue { + + @Override + default void setStringValue(String val) { + setStringValue(val, true, true); + } + + default void setStringValue(String val, boolean setSource) { + setStringValue(val, setSource, true); + } + + void setStringValue(String value, boolean setSource, boolean sync); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IValueSyncHandler.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IValueSyncHandler.java new file mode 100644 index 00000000000..c106e58eb6f --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IValueSyncHandler.java @@ -0,0 +1,63 @@ +package com.gregtechceu.gtceu.api.mui.base.value.sync; + +import com.gregtechceu.gtceu.api.mui.base.value.IValue; +import net.minecraft.network.FriendlyByteBuf; + +/** + * A helper interface for syncing an object value. + * + * @param object value type + */ +public interface IValueSyncHandler extends IValue { + + /** + * Updates the current value and the source and syncs it to client/server. + * + * @param value new value + */ + @Override + default void setValue(T value) { + setValue(value, true, true); + } + + /** + * Updates the current value and syncs it to client/server. + * + * @param value new value + * @param setSource whether the source should be updated with the new value + */ + default void setValue(T value, boolean setSource) { + setValue(value, setSource, true); + } + + /** + * Updates the current value. + * + * @param value new value + * @param setSource whether the source should be updated with the new value + * @param sync whether the new value should be synced to client/server + */ + void setValue(T value, boolean setSource, boolean sync); + + /** + * Determines if the current value is different from source and updates the current value if it is. + * + * @param isFirstSync true if it's the first tick in the ui + * @return true if the current value was different from source + */ + boolean updateCacheFromSource(boolean isFirstSync); + + /** + * Writes the current value to the buffer + * + * @param buffer buffer to write to + */ + void write(FriendlyByteBuf buffer); + + /** + * Reads a value from the buffer and sets the current value + * + * @param buffer buffer to read from + */ + void read(FriendlyByteBuf buffer); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDraggable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDraggable.java new file mode 100644 index 00000000000..9293968d28b --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDraggable.java @@ -0,0 +1,70 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.api.mui.base.layout.IViewport; +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.utils.HoveredWidgetList; +import com.gregtechceu.gtceu.api.mui.widget.DraggableWidget; +import com.gregtechceu.gtceu.api.mui.widget.WidgetTree; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import net.minecraft.client.gui.GuiGraphics; +import org.jetbrains.annotations.Nullable; + +/** + * Marks a widget as draggable. + * The dragging is handled by ModularUI. + * + * @see DraggableWidget + */ +public interface IDraggable extends IViewport { + + /** + * Gets called every frame after everything else is rendered. + * Is only called when {@link #isMoving()} is true. + * Translate to the mouse pos and draw with {@link WidgetTree#drawTree(IWidget, ModularGuiContext)}. + * + * @param graphics + * @param partialTicks difference from last from + */ + void drawMovingState(GuiGraphics graphics, ModularGuiContext context, float partialTicks); + + /** + * @param button the mouse button that's holding down + * @return false if the action should be canceled + */ + boolean onDragStart(int button); + + /** + * The dragging has ended and getState == IDLE + * + * @param successful is false if this returned to its old position + */ + void onDragEnd(boolean successful); + + void onDrag(int mouseButton, double timeSinceLastClick); + + /** + * Gets called when the mouse is released + * + * @param widget current top most widget below the mouse + * @return if the location is valid + */ + default boolean canDropHere(int x, int y, @Nullable IGuiElement widget) { + return true; + } + + /** + * @return the size and pos during move + */ + @Nullable + Area getMovingArea(); + + boolean isMoving(); + + void setMoving(boolean moving); + + void transform(IViewportStack viewportStack); + + @Override + default void getWidgetsAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y) {} +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IFocusedWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IFocusedWidget.java new file mode 100644 index 00000000000..518acdcabb4 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IFocusedWidget.java @@ -0,0 +1,29 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +/** + * An interface for {@link IWidget}'s, that makes them focusable. + * Usually used for text fields to receive keyboard and mouse input first, no matter if its hovered or not. + */ +public interface IFocusedWidget { + + /** + * @return this widget is currently focused + */ + boolean isFocused(); + + /** + * Called when this widget gets focused + * + * @param context gui context + */ + void onFocus(ModularGuiContext context); + + /** + * Called when the focus is removed from this widget + * + * @param context gui context + */ + void onRemoveFocus(ModularGuiContext context); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiAction.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiAction.java new file mode 100644 index 00000000000..4eeb2471f3e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiAction.java @@ -0,0 +1,51 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; + +/** + * Gui action listeners that can be registered in {@link ModularScreen#registerGuiActionListener(IGuiAction)} + */ +public interface IGuiAction { + + @FunctionalInterface + interface MousePressed extends IGuiAction { + + boolean press(double mouseX, double mouseY, int button); + } + + @FunctionalInterface + interface MouseReleased extends IGuiAction { + + boolean release(double mouseX, double mouseY, int button); + } + + @FunctionalInterface + interface KeyPressed extends IGuiAction { + + boolean press(int keyCode, int scanCode, int modifiers); + } + + @FunctionalInterface + interface KeyReleased extends IGuiAction { + + boolean release(int keyCode, int scanCode, int modifiers); + } + + @FunctionalInterface + interface CharTyped extends IGuiAction { + + boolean type(char codePoint, int modifiers); + } + + @FunctionalInterface + interface MouseScroll extends IGuiAction { + + boolean scroll(double mouseX, double mouseY, double delta); + } + + @FunctionalInterface + interface MouseDrag extends IGuiAction { + + boolean drag(double mouseX, double mouseY, int button, double dragX, double dragY); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiElement.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiElement.java new file mode 100644 index 00000000000..f598a769cc1 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IGuiElement.java @@ -0,0 +1,106 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.api.mui.base.layout.IResizeable; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +/** + * Base interface for gui elements. For example widgets. + */ +public interface IGuiElement { + + /** + * @return the screen this element is in + */ + ModularScreen getScreen(); + + /** + * @return the parent of this element + */ + IGuiElement getParent(); + + /** + * Returns if this element has a parent. This is the case when the widget is valid, but never if it's root widget. + */ + boolean hasParent(); + + IResizeable resizer(); + + /** + * @return the area this element occupies + */ + Area getArea(); + + /** + * Shortcut to get the area of the parent + * + * @return parent area + */ + default Area getParentArea() { + return getParent().getArea(); + } + + /** + * Draws this element + * + * @param context gui context + */ + void draw(ModularGuiContext context); + + /** + * Called when the mouse enters the area of this element + */ + default void onMouseStartHover() {} + + /** + * Called when the mouse leaves the area of this element + */ + default void onMouseEndHover() {} + + /** + * @return if this widget is currently right below the mouse + */ + default boolean isHovering() { + return getScreen().getContext().isHovered(this); + } + + /** + * + * @param ticks time in ticks + * @return if this element is right below the mouse for a certain amount of time + */ + default boolean isHoveringFor(int ticks) { + return getScreen().getContext().isHoveredFor(this, ticks); + } + + default boolean isBelowMouse() { + IGuiElement hovered = getScreen().getContext().getHovered(); + if (hovered == null) return false; + while (!(hovered instanceof ModularPanel)) { + if (hovered == this) return true; + hovered = hovered.getParent(); + } + return hovered == this; + } + + /** + * Returns if this element is enabled. Disabled elements are not drawn and can not be interacted with. + */ + boolean isEnabled(); + + /** + * @return default width if it can't be calculated + */ + default int getDefaultWidth() { + return 18; + } + + /** + * @return default height if it can't be calculated + */ + default int getDefaultHeight() { + return 18; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/INotifyEnabled.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/INotifyEnabled.java new file mode 100644 index 00000000000..49d1f020be6 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/INotifyEnabled.java @@ -0,0 +1,7 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +public interface INotifyEnabled { + + void onChildChangeEnabled(IWidget child, boolean enabled); + +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IParentWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IParentWidget.java new file mode 100644 index 00000000000..08d2abace0e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IParentWidget.java @@ -0,0 +1,45 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; + +public interface IParentWidget> { + + W getThis(); + + boolean addChild(I child, int index); + + default W child(int index, I child) { + if (!addChild(child, index)) { + throw new IllegalStateException("Failed to add child"); + } + return getThis(); + } + + default W child(I child) { + if (!addChild(child, -1)) { + throw new IllegalStateException("Failed to add child"); + } + return getThis(); + } + + default W childIf(boolean condition, I child) { + if (condition) return child(child); + return getThis(); + } + + default W childIf(BooleanSupplier condition, I child) { + if (condition.getAsBoolean()) return child(child); + return getThis(); + } + + default W childIf(boolean condition, Supplier child) { + if (condition) return child(child.get()); + return getThis(); + } + + default W childIf(BooleanSupplier condition, Supplier child) { + if (condition.getAsBoolean()) return child(child.get()); + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IPositioned.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IPositioned.java new file mode 100644 index 00000000000..21bc6f4e32b --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IPositioned.java @@ -0,0 +1,463 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Flex; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Unit; + +import java.util.function.Consumer; +import java.util.function.DoubleSupplier; + +/** + * Helper interface for position and size builder methods for widgets. + * + * @param widget type + */ +@SuppressWarnings({ "unused", "UnusedReturnValue" }) +public interface IPositioned> { + + Flex flex(); + + Area getArea(); + + @SuppressWarnings("unchecked") + default W getThis() { + return (W) this; + } + + default W coverChildrenWidth() { + flex().coverChildrenWidth(); + return getThis(); + } + + default W coverChildrenHeight() { + flex().coverChildrenHeight(); + return getThis(); + } + + default W coverChildren() { + return coverChildrenWidth().coverChildrenHeight(); + } + + default W expanded() { + flex().expanded(); + return getThis(); + } + + default W relative(IGuiElement guiElement) { + return relative(guiElement.getArea()); + } + + default W relative(Area guiElement) { + flex().relative(guiElement); + return getThis(); + } + + default W relativeToScreen() { + flex().relativeToScreen(); + return getThis(); + } + + default W relativeToParent() { + flex().relativeToParent(); + return getThis(); + } + + default W left(int val) { + flex().left(val, 0, 0, Unit.Measure.PIXEL, true); + return getThis(); + } + + default W leftRel(float val) { + flex().left(val, 0, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W leftRelOffset(float val, int offset) { + flex().left(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W leftRelAnchor(float val, float anchor) { + flex().left(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W leftRel(float val, int offset, float anchor) { + flex().left(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W left(float val, int offset, float anchor, Unit.Measure measure) { + flex().left(val, offset, anchor, measure, false); + return getThis(); + } + + default W left(DoubleSupplier val, Unit.Measure measure) { + flex().left(val, 0, 0, measure, true); + return getThis(); + } + + default W leftRelOffset(DoubleSupplier val, int offset) { + flex().left(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W leftRelAnchor(DoubleSupplier val, float anchor) { + flex().left(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W leftRel(DoubleSupplier val, int offset, float anchor) { + flex().left(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W right(int val) { + flex().right(val, 0, 0, Unit.Measure.PIXEL, true); + return getThis(); + } + + default W rightRel(float val) { + flex().right(val, 0, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W rightRelOffset(float val, int offset) { + flex().right(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W rightRelAnchor(float val, float anchor) { + flex().right(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W rightRel(float val, int offset, float anchor) { + flex().right(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W right(float val, int offset, float anchor, Unit.Measure measure) { + flex().right(val, offset, anchor, measure, false); + return getThis(); + } + + default W right(DoubleSupplier val, Unit.Measure measure) { + flex().right(val, 0, 0, measure, true); + return getThis(); + } + + default W rightRelOffset(DoubleSupplier val, int offset) { + flex().right(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W rightRelAnchor(DoubleSupplier val, float anchor) { + flex().right(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W rightRel(DoubleSupplier val, int offset, float anchor) { + flex().right(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W top(int val) { + flex().top(val, 0, 0, Unit.Measure.PIXEL, true); + return getThis(); + } + + default W topRel(float val) { + flex().top(val, 0, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W topRelOffset(float val, int offset) { + flex().top(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W topRelAnchor(float val, float anchor) { + flex().top(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W topRel(float val, int offset, float anchor) { + flex().top(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W top(float val, int offset, float anchor, Unit.Measure measure) { + flex().top(val, offset, anchor, measure, false); + return getThis(); + } + + default W top(DoubleSupplier val, Unit.Measure measure) { + flex().top(val, 0, 0, measure, true); + return getThis(); + } + + default W topRelOffset(DoubleSupplier val, int offset) { + flex().top(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W topRelAnchor(DoubleSupplier val, float anchor) { + flex().top(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W topRel(DoubleSupplier val, int offset, float anchor) { + flex().top(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W bottom(int val) { + flex().bottom(val, 0, 0, Unit.Measure.PIXEL, true); + return getThis(); + } + + default W bottomRel(float val) { + flex().bottom(val, 0, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W bottomRelOffset(float val, int offset) { + flex().bottom(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W bottomRelAnchor(float val, float anchor) { + flex().bottom(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W bottomRel(float val, int offset, float anchor) { + flex().bottom(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W bottom(float val, int offset, float anchor, Unit.Measure measure) { + flex().bottom(val, offset, anchor, measure, false); + return getThis(); + } + + default W bottom(DoubleSupplier val, Unit.Measure measure) { + flex().bottom(val, 0, 0, measure, true); + return getThis(); + } + + default W bottomRelOffset(DoubleSupplier val, int offset) { + flex().bottom(val, offset, 0, Unit.Measure.RELATIVE, true); + return getThis(); + } + + default W bottomRelAnchor(DoubleSupplier val, float anchor) { + flex().bottom(val, 0, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W bottomRel(DoubleSupplier val, int offset, float anchor) { + flex().bottom(val, offset, anchor, Unit.Measure.RELATIVE, false); + return getThis(); + } + + default W width(int val) { + flex().width(val, Unit.Measure.PIXEL); + return getThis(); + } + + default W widthRel(float val) { + flex().width(val, Unit.Measure.RELATIVE); + return getThis(); + } + + default W width(float val, Unit.Measure measure) { + flex().width(val, measure); + return getThis(); + } + + default W width(DoubleSupplier val, Unit.Measure measure) { + flex().width(val, measure); + return getThis(); + } + + default W height(int val) { + flex().height(val, Unit.Measure.PIXEL); + return getThis(); + } + + default W heightRel(float val) { + flex().height(val, Unit.Measure.RELATIVE); + return getThis(); + } + + default W height(float val, Unit.Measure measure) { + flex().height(val, measure); + return getThis(); + } + + default W height(DoubleSupplier val, Unit.Measure measure) { + flex().height(val, measure); + return getThis(); + } + + default W pos(int x, int y) { + left(x).top(y); + return getThis(); + } + + default W posRel(float x, float y) { + leftRel(x).topRel(y); + return getThis(); + } + + default W size(int w, int h) { + width(w).height(h); + return getThis(); + } + + default W sizeRel(float w, float h) { + widthRel(w).heightRel(h); + return getThis(); + } + + default W size(int val) { + return width(val).height(val); + } + + default W sizeRel(float val) { + return widthRel(val).heightRel(val); + } + + default W full() { + return widthRel(1f).heightRel(1f); + } + + default W anchorLeft(float val) { + flex().anchorLeft(val); + return getThis(); + } + + default W anchorRight(float val) { + flex().anchorRight(val); + return getThis(); + } + + default W anchorTop(float val) { + flex().anchorTop(val); + return getThis(); + } + + default W anchorBottom(float val) { + flex().anchorBottom(val); + return getThis(); + } + + default W anchor(Alignment alignment) { + flex().anchor(alignment); + return getThis(); + } + + default W alignX(float val) { + leftRel(val).anchorLeft(val); + return getThis(); + } + + default W alignX(Alignment alignment) { + return alignX(alignment.x); + } + + default W alignY(float val) { + topRel(val).anchorTop(val); + return getThis(); + } + + default W alignY(Alignment alignment) { + return alignY(alignment.y); + } + + default W align(Alignment alignment) { + return alignX(alignment).alignY(alignment); + } + + default W center() { + return align(Alignment.Center); + } + + default W flex(Consumer flexConsumer) { + flexConsumer.accept(flex()); + return getThis(); + } + + default W padding(int left, int right, int top, int bottom) { + getArea().getPadding().all(left, right, top, bottom); + return getThis(); + } + + default W padding(int horizontal, int vertical) { + getArea().getPadding().all(horizontal, vertical); + return getThis(); + } + + default W padding(int all) { + getArea().getPadding().all(all); + return getThis(); + } + + default W paddingLeft(int val) { + getArea().getPadding().left(val); + return getThis(); + } + + default W paddingRight(int val) { + getArea().getPadding().right(val); + return getThis(); + } + + default W paddingTop(int val) { + getArea().getPadding().top(val); + return getThis(); + } + + default W paddingBottom(int val) { + getArea().getPadding().bottom(val); + return getThis(); + } + + default W margin(int left, int right, int top, int bottom) { + getArea().getMargin().all(left, right, top, bottom); + return getThis(); + } + + default W margin(int horizontal, int vertical) { + getArea().getMargin().all(horizontal, vertical); + return getThis(); + } + + default W margin(int all) { + getArea().getMargin().all(all); + return getThis(); + } + + default W marginLeft(int val) { + getArea().getMargin().left(val); + return getThis(); + } + + default W marginRight(int val) { + getArea().getMargin().right(val); + return getThis(); + } + + default W marginTop(int val) { + getArea().getMargin().top(val); + return getThis(); + } + + default W marginBottom(int val) { + getArea().getMargin().bottom(val); + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ISynced.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ISynced.java new file mode 100644 index 00000000000..faab9f120a6 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ISynced.java @@ -0,0 +1,90 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.api.mui.value.sync.ModularSyncManager; +import com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler; +import org.jetbrains.annotations.NotNull; + +/** + * Marks a widget as synced + * + * @param widget type + */ +public interface ISynced { + + /** + * @return this cast to the true widget type + */ + @SuppressWarnings("unchecked") + default W getThis() { + return (W) this; + } + + /** + * Called when this widget gets initialised or when this widget is added to the gui + * + * @param syncManager sync manager + */ + void initialiseSyncHandler(ModularSyncManager syncManager); + + /** + * Checks if the received sync handler is valid for this widget. + * Synced widgets must override this! + * + * @param syncHandler received sync handler + * @return true if sync handler is valid + */ + default boolean isValidSyncHandler(SyncHandler syncHandler) { + return false; + } + + default T castIfTypeElseNull(SyncHandler syncHandler, Class clazz) { + if (syncHandler != null && clazz.isAssignableFrom(syncHandler.getClass())) { + return (T) syncHandler; + } + return null; + } + + /** + * @return true if this widget has a valid sync handler + */ + boolean isSynced(); + + /** + * @return the sync handler of this widget + * @throws IllegalStateException if this widget has no valid sync handler + */ + @NotNull + SyncHandler getSyncHandler(); + + /** + * Sets the sync handler key. The sync handler will be obtained in + * {@link #initialiseSyncHandler(ModularSyncManager)} + * + * @param name sync handler key name + * @param id sync handler key id + * @return this + */ + W syncHandler(String name, int id); + + /** + * Sets the sync handler key. The sync handler will be obtained in + * {@link #initialiseSyncHandler(ModularSyncManager)} + * + * @param key sync handler name + * @return this + */ + default W syncHandler(String key) { + return syncHandler(key, 0); + } + + /** + * Sets the sync handler key. The sync handler will be obtained in + * {@link #initialiseSyncHandler(ModularSyncManager)} + * + * @param id sync handler id + * @return this + */ + default W syncHandler(int id) { + return syncHandler("_", id); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ITooltip.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ITooltip.java new file mode 100644 index 00000000000..82962407f1a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ITooltip.java @@ -0,0 +1,277 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.base.drawable.ITextLine; +import com.gregtechceu.gtceu.api.mui.drawable.text.StyledText; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +/** + * Helper interface with tooltip builder methods for widgets. + * + * @param widget type + */ +public interface ITooltip> { + + /** + * @return the current tooltip of this widget. Null if there is none + */ + @Nullable + RichTooltip getTooltip(); + + /** + * @return the current tooltip of this widget. Creates a new one if there is none + */ + @NotNull + RichTooltip tooltip(); + + /** + * Overwrites the current tooltip with the given one + * + * @param tooltip new tooltip + * @return this + */ + W tooltip(RichTooltip tooltip); + + /** + * @return true if this widget has a tooltip + */ + default boolean hasTooltip() { + return getTooltip() != null && !getTooltip().isEmpty(); + } + + /** + * @return this cast to the true widget type + */ + @SuppressWarnings("unchecked") + default W getThis() { + return (W) this; + } + + /** + * Helper method to call tooltip setters within a widget tree initialisation. + * Only called once. + * + * @param tooltipConsumer tooltip function + * @return this + */ + default W tooltip(Consumer tooltipConsumer) { + return tooltipStatic(tooltipConsumer); + } + + /** + * Helper method to call tooltip setters within a widget tree initialisation. + * Only called once. + * + * @param tooltipConsumer tooltip function + * @return this + */ + default W tooltipStatic(Consumer tooltipConsumer) { + tooltipConsumer.accept(tooltip()); + return getThis(); + } + + /** + * Sets a tooltip builder. The builder will be called every time the tooltip is marked dirty. + * Should be used for dynamic tooltips. + * + * @param tooltipBuilder tooltip function + * @return this + */ + default W tooltipBuilder(Consumer tooltipBuilder) { + return tooltipDynamic(tooltipBuilder); + } + + /** + * Sets a tooltip builder. The builder will be called every time the tooltip is marked dirty. + * Should be used for dynamic tooltips. + * + * @param tooltipBuilder tooltip function + * @return this + */ + default W tooltipDynamic(Consumer tooltipBuilder) { + tooltip().tooltipBuilder(tooltipBuilder); + return getThis(); + } + + /** + * Sets a general tooltip position. The true position is calculated every frame. + * + * @param pos tooltip pos + * @return this + */ + default W tooltipPos(RichTooltip.Pos pos) { + tooltip().pos(pos); + return getThis(); + } + + /** + * Sets a fixed tooltip position. + * + * @param x x pos + * @param y y pos + * @return this + */ + default W tooltipPos(int x, int y) { + tooltip().pos(x, y); + return getThis(); + } + + /** + * Sets an alignment. The alignment determines how the content is aligned in the tooltip. + * + * @param alignment alignment + * @return this + */ + default W tooltipAlignment(Alignment alignment) { + tooltip().alignment(alignment); + return getThis(); + } + + /** + * Sets if the tooltip text should have shadow enabled by default. + * Can be overridden with {@link StyledText} lines. + * + * @param textShadow true if text should have a shadow + * @return this + */ + default W tooltipTextShadow(boolean textShadow) { + tooltip().textShadow(textShadow); + return getThis(); + } + + /** + * Sets a default tooltip text color. Can be overridden with text formatting. + * + * @param textColor text color + * @return this + */ + default W tooltipTextColor(int textColor) { + tooltip().textColor(textColor); + return getThis(); + } + + /** + * Sets a tooltip scale. The whole tooltip with content will be drawn with this scale. + * + * @param scale scale + * @return this + */ + default W tooltipScale(float scale) { + tooltip().scale(scale); + return getThis(); + } + + /** + * Sets a show up timer. This is the time in ticks needed for the cursor to hover this widget for the tooltip to + * appear. + * + * @param showUpTimer show up timer in ticks + * @return this + */ + default W tooltipShowUpTimer(int showUpTimer) { + tooltip().showUpTimer(showUpTimer); + return getThis(); + } + + /** + * Sets whether the tooltip should automatically update on every render tick. In most of the cases you don't need + * this, + * as ValueSyncHandler handles tooltip update for you when value is updated. However, if you don't handle + * differently, + * you either need to manually set change listener for the sync value, or set auto update to true. + * + * @param update true if the tooltip should automatically update + * @return this + */ + default W tooltipAutoUpdate(boolean update) { + tooltip().setAutoUpdate(update); + return getThis(); + } + + /** + * Sets whether the tooltip has a title margin, which is 2px space between first and second line inserted by + * default. + * + * @param hasTitleMargin true if the tooltip should have a title margin + * @return this + */ + default W tooltipHasTitleMargin(boolean hasTitleMargin) { + // tooltip().setHasTitleMargin(hasTitleMargin); + return getThis(); + } + + /** + * Sets the line padding for the tooltip. 1px by default, and you can disable it by passing 0. + * + * @param linePadding line padding in px + * @return this + */ + default W tooltipLinePadding(int linePadding) { + // tooltip().setLinePadding(linePadding); + return getThis(); + } + + default W addTooltipElement(String s) { + tooltip().add(s); + return getThis(); + } + + default W addTooltipElement(IDrawable drawable) { + tooltip().add(drawable); + return getThis(); + } + + default W addTooltipLine(ITextLine line) { + tooltip().addLine(line); + return getThis(); + } + + /** + * Adds any drawable as a new line. Inlining elements is currently not possible. + * + * @param drawable drawable element. + * @return this + */ + default W addTooltipLine(IDrawable drawable) { + tooltip().add(drawable).newLine(); + return getThis(); + } + + /** + * Helper method to add a simple string as a line. Adds multiple lines if string contains \n. + * + * @param line text line + * @return this + */ + default W addTooltipLine(String line) { + return addTooltipLine(IKey.str(line)); + } + + /** + * Helper method to add multiple drawable lines. + * + * @param lines collection of drawable elements + * @return this + */ + default W addTooltipDrawableLines(Iterable lines) { + tooltip().addDrawableLines(lines); + return getThis(); + } + + /** + * Helper method to add multiple text lines. + * + * @param lines lines of text + * @return this + */ + default W addTooltipStringLines(Iterable lines) { + tooltip().addStringLines(lines); + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IValueWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IValueWidget.java new file mode 100644 index 00000000000..e3f7cf72915 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IValueWidget.java @@ -0,0 +1,14 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +/** + * Marks a widget as containing a value + * + * @param + */ +public interface IValueWidget extends IWidget { + + /** + * @return stored value + */ + T getWidgetValue(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IVanillaSlot.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IVanillaSlot.java new file mode 100644 index 00000000000..950b6aa86d9 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IVanillaSlot.java @@ -0,0 +1,16 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import net.minecraft.world.inventory.Slot; + +/** + * Marks a {@link IWidget} as containing a vanilla item slot. + */ +public interface IVanillaSlot { + + /** + * @return the item slot of this widget + */ + Slot getVanillaSlot(); + + boolean handleAsVanillaSlot(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IWidget.java new file mode 100644 index 00000000000..84f62f60fae --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IWidget.java @@ -0,0 +1,282 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.google.common.base.CharMatcher; +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.layout.IResizeable; +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.drawable.Stencil; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Point; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Flex; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import com.gregtechceu.gtceu.utils.FormattingUtil; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +/** + * A widget in a GUI + */ +public interface IWidget extends IGuiElement { + + String WIDGET_TRANSLATION_KEY_FORMAT = "widget.%s.name"; + /** + * This char matcher is used to remove any non-{@code [a-z0-9_.-]} characters in translation keys. + * In essence, it + */ + CharMatcher DISALLOWED_TRANSLATION_KEY_CHARS = CharMatcher.inRange('a', 'z') + .or(CharMatcher.inRange('0', '9')) + .or(CharMatcher.anyOf("-_.")) + .negate(); + + /** + * Validates and initialises this element. + * This element now becomes valid + * + * @param parent the parent this element belongs to + */ + void initialise(@NotNull IWidget parent); + + /** + * Invalidates this element. + */ + void dispose(); + + /** + * Determines if this element exist in an active gui. + * + * @return if this is in a valid gui + */ + boolean isValid(); + + /** + * Draws the background of this widget. + * + * @param context gui context + * @param widgetTheme widget theme of this widget + */ + void drawBackground(ModularGuiContext context, WidgetTheme widgetTheme); + + /** + * Draws additional stuff in this widget. + * x = 0 and y = 0 is now in the top left corner of this widget. + * Do NOT override this method, it is never called. Use {@link #draw(ModularGuiContext, WidgetTheme)} instead. + * + * @param context gui context + */ + @ApiStatus.NonExtendable + @Deprecated + @Override + default void draw(ModularGuiContext context) { + draw(context, getWidgetTheme(context.getTheme())); + } + + /** + * Draws extra elements of this widget. Called after {@link #drawBackground(ModularGuiContext, WidgetTheme)} and + * before + * {@link #drawOverlay(ModularGuiContext, WidgetTheme)} + * + * @param context gui context + * @param widgetTheme widget theme + */ + void draw(ModularGuiContext context, WidgetTheme widgetTheme); + + /** + * Draws the overlay of this widget. + * + * @param context gui context + * @param widgetTheme widget theme + */ + void drawOverlay(ModularGuiContext context, WidgetTheme widgetTheme); + + /** + * Draws foreground elements of this widget. For example tooltips. + * No transformations are applied here. + * + * @param context gui context + */ + void drawForeground(ModularGuiContext context); + + default void transform(IViewportStack stack) { + stack.translate(getArea().rx, getArea().ry, getArea().getPanelLayer() * 20); + } + + default WidgetTheme getWidgetTheme(ITheme theme) { + return theme.getFallback(); + } + + /** + * Called 20 times per second. + */ + void onUpdate(); + + /** + * @return the area this widget occupies + */ + @Override + Area getArea(); + + default String getTranslationId() { + String className = FormattingUtil.toLowerCaseUnderscore(this.getClass().getSimpleName()); + className = DISALLOWED_TRANSLATION_KEY_CHARS.removeFrom(className); + return WIDGET_TRANSLATION_KEY_FORMAT.formatted(className); + } + + /** + * Calculates if a given pos is inside this widgets area. + * This should be used over {@link Area#isInside(int, int)}, since this accounts for transformations. + * + * @param stack viewport stack + * @param mx x pos + * @param my y pos + * @return if pos is inside this widgets area + */ + default boolean isInside(IViewportStack stack, int mx, int my) { + int x = stack.unTransformX(mx, my); + int y = stack.unTransformY(mx, my); + return x >= 0 && x < getArea().w() && y >= 0 && y < getArea().h(); + } + + /** + * Calculates if a given pos is inside this widgets area. + * This should be used over {@link Area#isInside(int, int)}, since this accounts for transformations. + * + * @param stack viewport stack + * @param point position + * @return if pos is inside this widgets area + */ + default boolean isInside(IViewportStack stack, Point point) { + return isInside(stack, point.x, point.y); + } + + /** + * @return all children of this widget + */ + @NotNull + default List getChildren() { + return Collections.emptyList(); + } + + /** + * @return if this widget has any children + */ + default boolean hasChildren() { + return !getChildren().isEmpty(); + } + + /** + * @return the panel this widget is in + */ + @NotNull + ModularPanel getPanel(); + + /** + * Returns if this element is enabled. Disabled elements are not drawn and can not be interacted with. + */ + @Override + boolean isEnabled(); + + void setEnabled(boolean enabled); + + default boolean areAncestorsEnabled() { + IWidget parent = this; + do { + if (!parent.isEnabled()) return false; + parent = parent.getParent(); + } while (parent.hasParent()); + return true; + } + + default boolean canBeSeen(IViewportStack stack) { + return Stencil.isInsideScissorArea(getArea(), stack); + } + + default boolean canHover() { + return true; + } + + default boolean canClickThrough() { + return true; + } + + /** + * Marks tooltip for this widget as dirty. + */ + void markTooltipDirty(); + + /** + * @return the parent of this widget + */ + @NotNull + IWidget getParent(); + + @Override + default boolean hasParent() { + return isValid(); + } + + /** + * @return the context of the current screen + */ + ModularGuiContext getContext(); + + /** + * @return flex of this widget. Creates a new one if it doesn't already have one. + */ + Flex flex(); + + /** + * Does the same as {@link IPositioned#flex(Consumer)} + * + * @param builder function to build flex + * @return this + */ + default IWidget flexBuilder(Consumer builder) { + builder.accept(flex()); + return this; + } + + /** + * @return resizer of this widget + */ + @NotNull + @Override + IResizeable resizer(); + + /** + * Sets the resizer of this widget. + * + * @param resizer resizer + */ + void resizer(IResizeable resizer); + + /** + * Called before a widget is resized. + */ + default void beforeResize() {} + + /** + * Called after a widget is fully resized. + */ + default void onResized() {} + + /** + * Called after the full widget tree is resized and the absolute positions are calculated. + */ + default void postResize() {} + + /** + * @return flex of this widget + */ + Flex getFlex(); + + default boolean isExpanded() { + Flex flex = getFlex(); + return flex != null && flex.isExpanded(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/Interactable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/Interactable.java new file mode 100644 index 00000000000..1a2aa92be35 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/Interactable.java @@ -0,0 +1,213 @@ +package com.gregtechceu.gtceu.api.mui.base.widget; + +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.sounds.SoundEvents; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; + +/** + * An interface that handles user interactions on {@link IWidget} objects. + * These methods get called on the client + */ +public interface Interactable { + + /** + * Called when this widget is pressed. + * + * @param mouseX the X coordinate of the mouse. + * @param mouseY the Y coordinate of the mouse. + * @param button mouse button that was pressed. + * @return result that determines what happens to other widgets + * {@link #onMouseTapped(double, double, int)} is only called if this returns {@link Result#ACCEPT} or + * {@link Result#SUCCESS} + */ + @NotNull + default Result onMousePressed(double mouseX, double mouseY, int button) { + return Result.ACCEPT; + } + + /** + * Called when a mouse button was released over this widget. + * + * @param mouseX the X coordinate of the mouse. + * @param mouseY the Y coordinate of the mouse. + * @param button mouse button that was released. + * @return whether other widgets should get called to. If this returns false, + * {@link #onMouseTapped(double, double, int)} will NOT be called. + */ + default boolean onMouseReleased(double mouseX, double mouseY, int button) { + return false; + } + + /** + * Called when this widget was pressed and then released within a certain time frame. + * + * @param mouseX the X coordinate of the mouse. + * @param mouseY the Y coordinate of the mouse. + * @param button mouse button that was pressed. + * @return result that determines if other widgets should get tapped to + * {@link Result#IGNORE IGNORE} and {@link Result#ACCEPT ACCEPT} will both "ignore" the result and + * {@link Result#STOP STOP} and {@link Result#SUCCESS SUCCESS} will both stop other widgets + * from getting tapped. + */ + @NotNull + default Result onMouseTapped(double mouseX, double mouseY, int button) { + return Result.IGNORE; + } + + /** + * Called when a key over this widget is pressed. + * + * @param keyCode key that was pressed. + * @param scanCode character that was pressed. + * @param modifiers any modifiers that were used. + * @return result that determines what happens to other widgets + * {@link #onKeyTapped(int, int, int)} is only called if this returns {@link Result#ACCEPT} or + * {@link Result#SUCCESS} + */ + @NotNull + default Result onKeyPressed(int keyCode, int scanCode, int modifiers) { + return Result.IGNORE; + } + + /** + * Called when a key was released over this widget. + * + * @param keyCode key that was pressed. + * @param scanCode character that was pressed. + * @param modifiers any modifiers that were used. + * @return whether other widgets should get called too. If this returns false, {@link #onKeyTapped(int, int, int)} + * will NOT be called. + */ + default boolean onKeyReleased(int keyCode, int scanCode, int modifiers) { + return false; + } + + /** + * Called when this widget was pressed and then released within a certain time frame. + * + * @param keyCode key that was pressed. + * @param scanCode character that was pressed. + * @param modifiers any modifiers that were used. + * @return result that determines if other widgets should get tapped to + * {@link Result#IGNORE} and {@link Result#ACCEPT} will both "ignore" the result and {@link Result#STOP} and + * {@link Result#SUCCESS} will both stop other widgets from getting tapped. + */ + @NotNull + default Result onKeyTapped(int keyCode, int scanCode, int modifiers) { + return Result.IGNORE; + } + + /** + * Called when a key over this widget is pressed. + * + * @param codePoint character that was typed + * @param modifiers any modifiers that were used. + * @return result that determines what happens to other widgets + * {@link #onKeyTapped(int, int, int)} is only called if this returns {@link Result#ACCEPT} or + * {@link Result#SUCCESS} + */ + @NotNull + default Result onCharTyped(char codePoint, int modifiers) { + return Result.IGNORE; + } + + /** + * Called when this widget is focused or when the mouse is above this widget. + * This method should return true if it can scroll at all and not if it scrolled right now. + * If this scroll view scrolled to the end and this returns false, the scroll will get passed through another scroll + * view below this. + * + * @param mouseX the X coordinate of the mouse. + * @param mouseY the Y coordinate of the mouse. + * @param delta amount scrolled by (usually irrelevant) + * @return true if this widget can be scrolled at all + */ + default boolean onMouseScrolled(double mouseX, double mouseY, double delta) { + return false; + } + + /** + * Called when this widget was clicked and mouse is now dragging. + * + * @param mouseX the X coordinate of the mouse. + * @param mouseY the Y coordinate of the mouse. + * @param button mouse button that drags + * @param dragX + * @param dragY + */ + default void onMouseDrag(double mouseX, double mouseY, int button, double dragX, double dragY) {} + + /** + * @return if left or right ctrl/cmd is pressed + */ + @OnlyIn(Dist.CLIENT) + static boolean hasControlDown() { + return Screen.hasControlDown(); + } + + /** + * @return if left or right shift is pressed + */ + @OnlyIn(Dist.CLIENT) + static boolean hasShiftDown() { + return Screen.hasShiftDown(); + } + + /** + * @return if alt or alt gr is pressed + */ + @OnlyIn(Dist.CLIENT) + static boolean hasAltDown() { + return Screen.hasAltDown(); + } + + /** + * @param key key id, see {@link InputConstants} + * @return if the key is pressed + */ + @OnlyIn(Dist.CLIENT) + static boolean isKeyPressed(int key) { + return InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), key); + } + + /** + * Plays the default button click sound + */ + @OnlyIn(Dist.CLIENT) + static void playButtonClickSound() { + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + } + + enum Result { + + /** + * Nothing happens. + */ + IGNORE(false, false), + /** + * Interaction is accepted, but other widgets will get checked. + */ + ACCEPT(true, false), + /** + * Interaction is rejected and no other widgets will get checked. + */ + STOP(false, true), + /** + * Interaction is accepted and no other widgets will get checked. + */ + SUCCESS(true, true); + + public final boolean accepts; + public final boolean stops; + + Result(boolean accepts, boolean stops) { + this.accepts = accepts; + this.stops = stops; + } + } +} From 087aa05be7f8ab932dd2d71591e364fcc39eeb95 Mon Sep 17 00:00:00 2001 From: YoungOnion <39562198+YoungOnionMC@users.noreply.github.com> Date: Thu, 7 Aug 2025 02:25:53 -0600 Subject: [PATCH 011/286] Client/screen package (#3664) --- .../gtceu/api/mui/base/IMuiScreen.java | 12 +- .../gtceu/api/mui/base/IPanelHandler.java | 1 + .../gtceu/api/mui/base/IThemeApi.java | 1 + .../gtceu/api/mui/base/IUIHolder.java | 1 + .../gtceu/api/mui/base/UIFactory.java | 2 + .../gtceu/api/mui/base/XeiSettings.java | 1 + .../api/mui/base/drawable/IDrawable.java | 2 + .../api/mui/base/drawable/IHoverable.java | 1 + .../gtceu/api/mui/base/drawable/IKey.java | 4 +- .../mui/base/drawable/IRichTextBuilder.java | 1 + .../api/mui/base/drawable/ITextLine.java | 1 + .../api/mui/base/layout/IViewportStack.java | 1 + .../base/value/sync/IValueSyncHandler.java | 1 + .../gtceu/api/mui/base/widget/IDraggable.java | 2 + .../api/mui/base/widget/INotifyEnabled.java | 1 - .../gtceu/api/mui/base/widget/ISynced.java | 1 + .../gtceu/api/mui/base/widget/ITooltip.java | 1 + .../gtceu/api/mui/base/widget/IWidget.java | 3 +- .../api/mui/base/widget/Interactable.java | 3 +- .../component/DrawableTooltipComponent.java | 62 ++ .../mui/screen/ClientScreenHandler.java | 651 ++++++++++++++ .../mui/screen/ContainerScreenWrapper.java | 67 ++ .../mui/screen/CustomModularScreen.java | 43 + .../mui/screen/DraggablePanelWrapper.java | 83 ++ .../mui/screen/IClickableContainerScreen.java | 10 + .../mui/screen/ModularContainerMenu.java | 494 +++++++++++ .../gtceu/client/mui/screen/ModularPanel.java | 822 ++++++++++++++++++ .../client/mui/screen/ModularScreen.java | 813 +++++++++++++++++ .../gtceu/client/mui/screen/PanelManager.java | 348 ++++++++ .../gtceu/client/mui/screen/RichTooltip.java | 385 ++++++++ .../client/mui/screen/ScreenWrapper.java | 35 + .../client/mui/screen/SecondaryPanel.java | 104 +++ .../gtceu/client/mui/screen/UISettings.java | 97 +++ .../client/mui/screen/XeiSettingsImpl.java | 170 ++++ .../mui/screen/viewport/GuiContext.java | 169 ++++ .../mui/screen/viewport/GuiViewportStack.java | 239 +++++ .../mui/screen/viewport/LocatedElement.java | 24 + .../mui/screen/viewport/LocatedWidget.java | 46 + .../screen/viewport/ModularGuiContext.java | 409 +++++++++ .../screen/viewport/TransformationMatrix.java | 113 +++ 40 files changed, 5210 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/component/DrawableTooltipComponent.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/ClientScreenHandler.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/ContainerScreenWrapper.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/CustomModularScreen.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/DraggablePanelWrapper.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/IClickableContainerScreen.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/ModularContainerMenu.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/ModularPanel.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/ModularScreen.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/PanelManager.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/RichTooltip.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/ScreenWrapper.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/SecondaryPanel.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/UISettings.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/XeiSettingsImpl.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/GuiContext.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/GuiViewportStack.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/LocatedElement.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/LocatedWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/ModularGuiContext.java create mode 100644 src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/TransformationMatrix.java diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMuiScreen.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMuiScreen.java index 6980c0fecea..b9b7f8eb015 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMuiScreen.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IMuiScreen.java @@ -7,12 +7,14 @@ import com.gregtechceu.gtceu.client.mui.screen.ScreenWrapper; import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; import com.gregtechceu.gtceu.core.mixins.client.AbstractContainerScreenAccessor; + import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.world.inventory.Slot; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; + import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -36,16 +38,6 @@ public interface IMuiScreen { @NotNull ModularScreen getScreen(); - /** - * {@link Screen GuiScreens} need to be focused when a text field is focused, to prevent key input from - * behaving unexpectedly. - * - * @param focused if the screen should be focused - */ - default void setFocused(boolean focused) { - getScreen().setFocused(focused); - } - /** * This method decides how the gui background is drawn. * The intended usage is to override {@link Screen#renderBackground(GuiGraphics)} and call this method diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPanelHandler.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPanelHandler.java index 7cd103d7ca8..ffe21c2d4af 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPanelHandler.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IPanelHandler.java @@ -5,6 +5,7 @@ import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; import com.gregtechceu.gtceu.client.mui.screen.SecondaryPanel; + import org.jetbrains.annotations.ApiStatus; /** diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IThemeApi.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IThemeApi.java index 384941aaaad..8436e30fa69 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IThemeApi.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IThemeApi.java @@ -5,6 +5,7 @@ import com.gregtechceu.gtceu.api.mui.theme.WidgetThemeParser; import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; import com.gregtechceu.gtceu.utils.serialization.json.JsonBuilder; + import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IUIHolder.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IUIHolder.java index 0cee239b6fa..58a9ca3923e 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/IUIHolder.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/IUIHolder.java @@ -5,6 +5,7 @@ import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; import com.gregtechceu.gtceu.client.mui.screen.UISettings; + import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/UIFactory.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/UIFactory.java index 00ca24c6479..0d58fc06be3 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/UIFactory.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/UIFactory.java @@ -3,12 +3,14 @@ import com.gregtechceu.gtceu.api.mui.factory.GuiData; import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; import com.gregtechceu.gtceu.client.mui.screen.*; + import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; + import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/XeiSettings.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/XeiSettings.java index acd44cc6d52..cc9c4d19c12 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/XeiSettings.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/XeiSettings.java @@ -4,6 +4,7 @@ import com.gregtechceu.gtceu.api.mui.utils.Rectangle; import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; import com.gregtechceu.gtceu.integration.xei.handlers.GhostIngredientSlot; + import org.jetbrains.annotations.ApiStatus; /** diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IDrawable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IDrawable.java index 16887867e57..88382221b57 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IDrawable.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IDrawable.java @@ -7,8 +7,10 @@ import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; + import org.jetbrains.annotations.Nullable; /** diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IHoverable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IHoverable.java index 4f397651bb9..5e4c4745f90 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IHoverable.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IHoverable.java @@ -2,6 +2,7 @@ import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; + import org.jetbrains.annotations.Nullable; public interface IHoverable extends IIcon { diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IKey.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IKey.java index f8b26602183..db46e9d77f3 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IKey.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IKey.java @@ -1,6 +1,5 @@ package com.gregtechceu.gtceu.api.mui.base.drawable; -import com.google.gson.JsonObject; import com.gregtechceu.gtceu.api.mui.base.IJsonSerializable; import com.gregtechceu.gtceu.api.mui.drawable.text.*; import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; @@ -8,11 +7,14 @@ import com.gregtechceu.gtceu.api.mui.widgets.TextWidget; import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; import com.gregtechceu.gtceu.utils.serialization.json.JsonHelper; + import net.minecraft.ChatFormatting; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.MutableComponent; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; + +import com.google.gson.JsonObject; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IRichTextBuilder.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IRichTextBuilder.java index 35b1cafc62d..adc64fc3370 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IRichTextBuilder.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/IRichTextBuilder.java @@ -2,6 +2,7 @@ import com.gregtechceu.gtceu.api.mui.drawable.text.Spacer; import com.gregtechceu.gtceu.api.mui.utils.Alignment; + import net.minecraft.network.chat.Component; public interface IRichTextBuilder> { diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/ITextLine.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/ITextLine.java index 407d374a81c..eb44bd8902d 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/ITextLine.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/drawable/ITextLine.java @@ -1,6 +1,7 @@ package com.gregtechceu.gtceu.api.mui.base.drawable; import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + import net.minecraft.client.gui.Font; public interface ITextLine { diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewportStack.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewportStack.java index 61d97789bf1..dabd94977d7 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewportStack.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/layout/IViewportStack.java @@ -2,6 +2,7 @@ import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; import com.gregtechceu.gtceu.client.mui.screen.viewport.TransformationMatrix; + import com.mojang.blaze3d.vertex.PoseStack; import org.jetbrains.annotations.Nullable; import org.joml.Matrix4f; diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IValueSyncHandler.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IValueSyncHandler.java index c106e58eb6f..bda9f5194f5 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IValueSyncHandler.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/value/sync/IValueSyncHandler.java @@ -1,6 +1,7 @@ package com.gregtechceu.gtceu.api.mui.base.value.sync; import com.gregtechceu.gtceu.api.mui.base.value.IValue; + import net.minecraft.network.FriendlyByteBuf; /** diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDraggable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDraggable.java index 9293968d28b..42df66f763e 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDraggable.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IDraggable.java @@ -7,7 +7,9 @@ import com.gregtechceu.gtceu.api.mui.widget.WidgetTree; import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + import net.minecraft.client.gui.GuiGraphics; + import org.jetbrains.annotations.Nullable; /** diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/INotifyEnabled.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/INotifyEnabled.java index 49d1f020be6..5f65fb976d1 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/INotifyEnabled.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/INotifyEnabled.java @@ -3,5 +3,4 @@ public interface INotifyEnabled { void onChildChangeEnabled(IWidget child, boolean enabled); - } diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ISynced.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ISynced.java index faab9f120a6..066a34c927b 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ISynced.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ISynced.java @@ -2,6 +2,7 @@ import com.gregtechceu.gtceu.api.mui.value.sync.ModularSyncManager; import com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler; + import org.jetbrains.annotations.NotNull; /** diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ITooltip.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ITooltip.java index 82962407f1a..e76210f29d4 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ITooltip.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/ITooltip.java @@ -6,6 +6,7 @@ import com.gregtechceu.gtceu.api.mui.drawable.text.StyledText; import com.gregtechceu.gtceu.api.mui.utils.Alignment; import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; + import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IWidget.java index 84f62f60fae..7749eb57ca5 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IWidget.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/IWidget.java @@ -1,6 +1,5 @@ package com.gregtechceu.gtceu.api.mui.base.widget; -import com.google.common.base.CharMatcher; import com.gregtechceu.gtceu.api.mui.base.ITheme; import com.gregtechceu.gtceu.api.mui.base.layout.IResizeable; import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; @@ -12,6 +11,8 @@ import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; import com.gregtechceu.gtceu.utils.FormattingUtil; + +import com.google.common.base.CharMatcher; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/Interactable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/Interactable.java index 1a2aa92be35..a6622b55256 100644 --- a/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/Interactable.java +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/base/widget/Interactable.java @@ -1,12 +1,13 @@ package com.gregtechceu.gtceu.api.mui.base.widget; -import com.mojang.blaze3d.platform.InputConstants; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.resources.sounds.SimpleSoundInstance; import net.minecraft.sounds.SoundEvents; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; + +import com.mojang.blaze3d.platform.InputConstants; import org.jetbrains.annotations.NotNull; /** diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/component/DrawableTooltipComponent.java b/src/main/java/com/gregtechceu/gtceu/client/mui/component/DrawableTooltipComponent.java new file mode 100644 index 00000000000..70eff7ff720 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/component/DrawableTooltipComponent.java @@ -0,0 +1,62 @@ +package com.gregtechceu.gtceu.client.mui.component; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IIcon; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.drawable.text.TextIcon; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.world.inventory.tooltip.TooltipComponent; + +import org.jetbrains.annotations.NotNull; + +public class DrawableTooltipComponent implements ClientTooltipComponent, TooltipComponent { + + private final IDrawable drawable; + + public DrawableTooltipComponent(IDrawable drawable) { + this.drawable = drawable; + } + + @Override + public int getHeight() { + if (drawable instanceof TextIcon text) { + return text.getHeight(); + } + if (drawable instanceof IIcon icon) { + return icon.getHeight(); + } else if (!(drawable instanceof IKey key)) { + return 18; + } else { + return key.asTextIcon().getHeight(); + } + } + + @Override + public int getWidth(@NotNull Font font) { + if (drawable instanceof TextIcon text) { + return text.getWidth(); + } + if (drawable instanceof IIcon icon) { + return icon.getWidth(); + } else if (!(drawable instanceof IKey key)) { + return 18; + } else { + return key.asTextIcon().getWidth(); + } + } + + @Override + public void renderImage(@NotNull Font font, int x, int y, @NotNull GuiGraphics guiGraphics) { + GuiContext context = GuiContext.getDefault(); + GuiGraphics lastGraphics = context.getGraphics(); + + context.setGraphics(guiGraphics); + drawable.draw(context, x, y, getWidth(font), getHeight(), WidgetTheme.getDefault()); + context.setGraphics(lastGraphics); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ClientScreenHandler.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ClientScreenHandler.java new file mode 100644 index 00000000000..81b029d0677 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ClientScreenHandler.java @@ -0,0 +1,651 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.IMuiScreen; +import com.gregtechceu.gtceu.api.mui.base.MCHelper; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; +import com.gregtechceu.gtceu.api.mui.base.widget.IVanillaSlot; +import com.gregtechceu.gtceu.api.mui.drawable.GuiDraw; +import com.gregtechceu.gtceu.api.mui.drawable.Stencil; +import com.gregtechceu.gtceu.api.mui.overlay.OverlayStack; +import com.gregtechceu.gtceu.api.mui.utils.Animator; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.utils.FpsCounter; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.api.mui.widgets.RichTextWidget; +import com.gregtechceu.gtceu.api.mui.widgets.slot.ItemSlot; +import com.gregtechceu.gtceu.api.mui.widgets.slot.ModularSlot; +import com.gregtechceu.gtceu.api.mui.widgets.slot.SlotGroup; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.client.mui.screen.viewport.LocatedWidget; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import com.gregtechceu.gtceu.config.ConfigHolder; +import com.gregtechceu.gtceu.core.mixins.client.AbstractContainerScreenAccessor; +import com.gregtechceu.gtceu.core.mixins.client.ScreenAccessor; +import com.gregtechceu.gtceu.integration.xei.handlers.RecipeViewerHandler; + +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.util.Mth; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.ContainerScreenEvent; +import net.minecraftforge.client.event.ScreenEvent; +import net.minecraftforge.client.extensions.common.IClientItemExtensions; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TickEvent; +import net.minecraftforge.eventbus.api.EventPriority; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import lombok.Getter; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.opengl.GL11; + +import java.util.Collections; +import java.util.Objects; +import java.util.function.Predicate; + +@ApiStatus.Internal +@Mod.EventBusSubscriber(modid = GTCEu.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT) +public class ClientScreenHandler { + + @Getter + private static final GuiContext defaultContext = new GuiContext(); + private static final FpsCounter fpsCounter = new FpsCounter(); + private static ModularScreen currentScreen = null; + @Getter + private static long ticks = 0L; + private static IMuiScreen lastMui; + + @SubscribeEvent + public static void onOpenScreen(ScreenEvent.Opening event) { + Screen newGui = event.getNewScreen(); + defaultContext.reset(); + + if (lastMui != null && newGui == null) { + if (lastMui.getScreen().getPanelManager().isOpen()) { + lastMui.getScreen().getPanelManager().closeAll(); + } + lastMui.getScreen().getPanelManager().dispose(); + lastMui = null; + } else if (newGui instanceof IMuiScreen screenWrapper) { + if (lastMui == null) { + lastMui = screenWrapper; + } else if (lastMui == newGui) { + lastMui.getScreen().getPanelManager().reopen(); + } else { + if (lastMui.getScreen().getPanelManager().isOpen()) { + lastMui.getScreen().getPanelManager().closeAll(); + } + lastMui.getScreen().getPanelManager().dispose(); + lastMui = screenWrapper; + } + } + + if (newGui instanceof IMuiScreen muiScreen) { + Objects.requireNonNull(muiScreen.getScreen(), "ModularScreen must not be null!"); + if (currentScreen == muiScreen.getScreen()) { + currentScreen.getPanelManager().reopen(); + } else { + if (hasScreen()) { + currentScreen.onCloseParent(); + currentScreen.getPanelManager().dispose(); + } + currentScreen = muiScreen.getScreen(); + fpsCounter.reset(); + } + } else if (hasScreen() && event.getCurrentScreen() != null && newGui != event.getCurrentScreen()) { + currentScreen.onCloseParent(); + currentScreen.getPanelManager().dispose(); + currentScreen = null; + } + } + + @SubscribeEvent + public static void onCloseScreen(ScreenEvent.Closing event) { + if (hasScreen() && !currentScreen.getPanelManager().isReopened()) { + currentScreen.onCloseParent(); + currentScreen.getPanelManager().dispose(); + currentScreen = null; + } + } + + @SubscribeEvent + public static void onInitScreenPost(ScreenEvent.Init.Post event) { + defaultContext.updateScreenArea(event.getScreen().width, event.getScreen().height); + if (checkGui(event.getScreen())) { + currentScreen.onResize(event.getScreen().width, event.getScreen().height); + } + OverlayStack.foreach(ms -> ms.onResize(event.getScreen().width, event.getScreen().height), false); + } + + // before JEI + @SubscribeEvent(priority = EventPriority.HIGH) + public static void onScreenKeyPressedHigh(ScreenEvent.KeyPressed.Pre event) { + defaultContext.updateLatestKey(event.getKeyCode(), event.getScanCode(), event.getModifiers()); + keyPressedEvent(event, InputPhase.EARLY); + } + + // after JEI + @SubscribeEvent(priority = EventPriority.LOW) + public static void onScreenKeyPressedLow(ScreenEvent.KeyPressed.Pre event) { + keyPressedEvent(event, InputPhase.LATE); + } + + private static void keyPressedEvent(ScreenEvent.KeyPressed.Pre event, InputPhase phase) { + if (checkGui(event.getScreen())) { + currentScreen.getContext().updateLatestKey(event.getKeyCode(), event.getScanCode(), event.getModifiers()); + } + if (handleKeyboardInput(currentScreen, event.getScreen(), true, phase, + event.getKeyCode(), event.getScanCode(), event.getModifiers())) { + event.setCanceled(true); + } + } + + // before JEI + @SubscribeEvent(priority = EventPriority.HIGH) + public static void onScreenKeyReleasedHigh(ScreenEvent.KeyReleased.Pre event) { + defaultContext.updateLatestKey(event.getKeyCode(), event.getScanCode(), event.getModifiers()); + keyReleasedEvent(event, InputPhase.EARLY); + } + + // after JEI + @SubscribeEvent(priority = EventPriority.LOW) + public static void onScreenKeyReleasedLow(ScreenEvent.KeyReleased.Pre event) { + keyReleasedEvent(event, InputPhase.LATE); + } + + private static void keyReleasedEvent(ScreenEvent.KeyReleased.Pre event, InputPhase phase) { + if (checkGui(event.getScreen())) { + currentScreen.getContext().updateLatestKey(event.getKeyCode(), event.getScanCode(), event.getModifiers()); + } + if (handleKeyboardInput(currentScreen, event.getScreen(), false, phase, + event.getKeyCode(), event.getScanCode(), event.getModifiers())) { + event.setCanceled(true); + } + } + + // before JEI + @SubscribeEvent(priority = EventPriority.HIGH) + public static void onScreenMousePressed(ScreenEvent.MouseButtonPressed.Pre event) { + int button = event.getButton(); + double mouseX = event.getMouseX(); + double mouseY = event.getMouseY(); + defaultContext.updateMouseButton(button); + if (checkGui(event.getScreen())) currentScreen.getContext().updateMouseButton(button); + + if (button == -1) { + return; + } + if (currentScreen != null && currentScreen.handleDraggableInput(mouseX, mouseY, button, true) || + doAction(currentScreen, ms -> ms.onMousePressed(mouseX, mouseY, button))) { + RecipeViewerHandler.getCurrent().setSearchFocused(false); + event.setCanceled(true); + } + } + + // before JEI + @SubscribeEvent(priority = EventPriority.HIGH) + public static void onScreenMouseReleased(ScreenEvent.MouseButtonReleased.Pre event) { + int button = event.getButton(); + double mouseX = event.getMouseX(); + double mouseY = event.getMouseY(); + defaultContext.updateMouseButton(button); + if (checkGui(event.getScreen())) currentScreen.getContext().updateMouseButton(button); + + if (currentScreen != null && currentScreen.handleDraggableInput(mouseX, mouseY, button, false) || + doAction(currentScreen, ms -> ms.mouseReleased(mouseX, mouseY, button))) { + RecipeViewerHandler.getCurrent().setSearchFocused(false); + event.setCanceled(true); + } + } + + // before JEI + @SubscribeEvent(priority = EventPriority.HIGH) + public static void onScreenMouseScrolled(ScreenEvent.MouseScrolled.Pre event) { + double w = event.getScrollDelta(); + if (w == 0) return; + defaultContext.updateMouseWheel(w); + if (checkGui(event.getScreen())) currentScreen.getContext().updateMouseWheel(w); + checkGui(event.getScreen()); + if (doAction(currentScreen, ms -> ms.mouseScrolled(event.getMouseX(), event.getMouseY(), w))) { + event.setCanceled(true); + } + } + + // before JEI + @SubscribeEvent(priority = EventPriority.HIGH) + public static void onScreenMouseDragged(ScreenEvent.MouseDragged.Pre event) { + checkGui(event.getScreen()); + if (event.getMouseButton() == -1) { + return; + } + if (doAction(currentScreen, ms -> ms.mouseDragged(event.getMouseX(), event.getMouseY(), + event.getMouseButton(), event.getDragX(), event.getDragY()))) { + event.setCanceled(true); + } + } + + @SubscribeEvent(priority = EventPriority.LOW) + public static void onScreenRenderLow(ScreenEvent.Render.Pre event) { + int mx = event.getMouseX(), my = event.getMouseY(); + float pt = event.getPartialTick(); + GuiGraphics gc = event.getGuiGraphics(); + defaultContext.setGraphics(gc); + defaultContext.updateState(mx, my, pt); + defaultContext.reset(); + if (checkGui(event.getScreen())) { + currentScreen.getContext().setGraphics(gc); + currentScreen.getContext().updateState(mx, my, pt); + drawScreen(gc, currentScreen, currentScreen.getScreenWrapper().getWrappedScreen(), mx, my, pt); + event.setCanceled(true); + } + } + + @SubscribeEvent + public static void onScreenRenderNormal(ScreenEvent.Render.Post event) { + OverlayStack.draw(event.getGuiGraphics(), event.getMouseX(), event.getMouseY(), event.getPartialTick()); + } + + @SubscribeEvent + public static void onClientTick(TickEvent.ClientTickEvent event) { + if (event.phase == TickEvent.Phase.START) { + OverlayStack.onTick(); + defaultContext.tick(); + if (checkGui()) { + currentScreen.onUpdate(); + } + ticks++; + } + } + + @SubscribeEvent + public static void onRenderTick(TickEvent.RenderTickEvent event) { + if (event.phase == TickEvent.Phase.START) { + GL11.glEnable(GL11.GL_STENCIL_TEST); + } + Stencil.reset(); + } + + public static void onFrameUpdate() { + OverlayStack.foreach(ModularScreen::onFrameUpdate, true); + if (currentScreen != null) currentScreen.onFrameUpdate(); + Animator.advance(); + } + + private static boolean doAction(@Nullable ModularScreen muiScreen, Predicate action) { + return OverlayStack.interact(action, true) || (muiScreen != null && action.test(muiScreen)); + } + + /** + * This replicates vanilla behavior while also injecting custom behavior for consistency + */ + private static boolean handleKeyboardInput(@Nullable ModularScreen muiScreen, Screen mcScreen, + boolean isPress, InputPhase inputPhase, + int keyCode, int scanCode, int modifiers) { + if (isPress) { + // pressing a key + return inputPhase.isEarly() ? doAction(muiScreen, ms -> ms.keyPressed(keyCode, scanCode, modifiers)) : + keyPressed(mcScreen, keyCode, scanCode, modifiers); + } else { + // releasing a key + if (inputPhase.isEarly() && doAction(muiScreen, ms -> ms.keyReleased(keyCode, scanCode, modifiers))) { + return true; + } + if (inputPhase.isLate() && keyCode >= ' ') { + return keyPressed(mcScreen, keyCode, scanCode, modifiers); + } + } + return false; + } + + private static boolean keyPressed(Screen screen, int keyCode, int scanCode, int modifiers) { + if (currentScreen == null) return false; + // debug mode C + CTRL + SHIFT + ALT + if (keyCode == 'c' && + (modifiers & GLFW.GLFW_MOD_CONTROL) != 0 && + (modifiers & GLFW.GLFW_MOD_SHIFT) != 0 && + (modifiers & GLFW.GLFW_MOD_ALT) != 0) { + ConfigHolder.INSTANCE.dev.debugUI = !ConfigHolder.INSTANCE.dev.debugUI; + return true; + } + boolean isInventoryKey = Minecraft.getInstance().options.keyInventory + .isActiveAndMatches(InputConstants.getKey(keyCode, scanCode)); + if (keyCode == 1 || isInventoryKey) { + if (currentScreen.getContext().hasDraggable()) { + currentScreen.getContext().dropDraggable(); + } else { + currentScreen.getPanelManager().closeTopPanel(true); + } + return true; + } + return false; + } + + public static void dragSlot(double mouseX, double mouseY, int button, double dragX, double dragY) { + getMCScreen().mouseDragged(mouseX, mouseY, button, dragX, dragY); + } + + public static void clickSlot(ModularScreen ms, Slot slot) { + Screen screen = ms.getScreenWrapper().getWrappedScreen(); + if (screen instanceof ScreenAccessor acc && screen instanceof IClickableContainerScreen clickableScreen && + checkGui(screen)) { + ModularGuiContext ctx = ms.getContext(); + var buttonList = screen.children(); + try { + // remove buttons to make sure they are not clicked + acc.setChildren(Collections.emptyList()); + // set clicked slot to make sure the container clicks the desired slot + clickableScreen.gtceu$setClickedSlot(slot); + screen.mouseClicked(ctx.getMouseX(), ctx.getMouseY(), ctx.getMouseButton()); + } finally { + // undo modifications + clickableScreen.gtceu$setClickedSlot(null); + acc.setChildren(buttonList); + } + } + } + + public static void releaseSlot() { + if (hasScreen() && getMCScreen() != null) { + ModularGuiContext ctx = currentScreen.getContext(); + getMCScreen().mouseReleased(ctx.getMouseX(), ctx.getMouseY(), ctx.getMouseButton()); + } + } + + public static boolean shouldDrawWorldBackground() { + return /* ModularUI.isBlurLoaded() || */Minecraft.getInstance().level == null; + } + + public static void drawDarkBackground(Screen screen, GuiGraphics guiGraphics) { + if (hasScreen()) { + float alpha = currentScreen.getMainPanel().getAlpha(); + // vanilla color values as hex + int color = 0x101010; + int startAlpha = 0xc0; + int endAlpha = 0xd0; + GuiDraw.drawVerticalGradientRect(guiGraphics, 0, 0, screen.width, screen.height, + Color.withAlpha(color, (int) (startAlpha * alpha)), + Color.withAlpha(color, (int) (endAlpha * alpha))); + MinecraftForge.EVENT_BUS.post(new ScreenEvent.BackgroundRendered(screen, guiGraphics)); + } + } + + public static void drawScreen(GuiGraphics graphics, ModularScreen muiScreen, Screen mcScreen, int mouseX, + int mouseY, float partialTicks) { + if (mcScreen instanceof AbstractContainerScreen container) { + drawContainer(graphics, muiScreen, container, mouseX, mouseY, partialTicks); + } else { + drawScreenInternal(graphics, muiScreen, mcScreen, mouseX, mouseY, partialTicks); + } + } + + public static void drawScreenInternal(GuiGraphics graphics, ModularScreen muiScreen, Screen mcScreen, int mouseX, + int mouseY, float partialTicks) { + Stencil.reset(); + Stencil.apply(muiScreen.getScreenArea(), null); + muiScreen.render(graphics, mouseX, mouseY, partialTicks); + RenderSystem.disableDepthTest(); + drawVanillaElements(graphics, mcScreen, mouseX, mouseY, partialTicks); + RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F); + Lighting.setupForFlatItems(); + muiScreen.drawForeground(graphics, partialTicks); + RenderSystem.enableDepthTest(); + Lighting.setupFor3DItems(); + Stencil.remove(); + } + + public static void drawContainer(GuiGraphics graphics, ModularScreen muiScreen, AbstractContainerScreen mcScreen, + int mouseX, int mouseY, float partialTicks) { + AbstractContainerScreenAccessor acc = (AbstractContainerScreenAccessor) mcScreen; + + Stencil.reset(); + Stencil.apply(muiScreen.getScreenArea(), null); + mcScreen.renderBackground(graphics); + int x = mcScreen.getGuiLeft(); + int y = mcScreen.getGuiTop(); + + acc.invokeRenderBg(graphics, partialTicks, mouseX, mouseY); + muiScreen.render(graphics, mouseX, mouseY, partialTicks); + + RenderSystem.disableDepthTest(); + // mainly for invtweaks compat + drawVanillaElements(graphics, mcScreen, mouseX, mouseY, partialTicks); + graphics.pose().pushPose(); + graphics.setColor(1.0F, 1.0F, 1.0F, 1.0F); + acc.setHoveredSlot(null); + graphics.setColor(1.0F, 1.0F, 1.0F, 1.0F); + Lighting.setupForFlatItems(); + acc.invokeRenderLabels(graphics, mouseX, mouseY); + muiScreen.drawForeground(graphics, partialTicks); + Lighting.setupFor3DItems(); + + acc.setHoveredSlot(null); + IGuiElement hovered = muiScreen.getContext().getHovered(); + if (hovered instanceof IVanillaSlot vanillaSlot && vanillaSlot.handleAsVanillaSlot()) { + acc.setHoveredSlot(vanillaSlot.getVanillaSlot()); + } + + graphics.setColor(1.0F, 1.0F, 1.0F, 1.0F); + graphics.pose().pushPose(); + graphics.pose().translate(x, y, 0); + MinecraftForge.EVENT_BUS.post(new ContainerScreenEvent.Render.Foreground(mcScreen, graphics, mouseX, mouseY)); + + AbstractContainerMenu menu = mcScreen.getMenu(); + ItemStack draggingItem = acc.getDraggingItem().isEmpty() ? menu.getCarried() : acc.getDraggingItem(); + if (!draggingItem.isEmpty()) { + int xOffset = 8; + int yOffset = acc.getDraggingItem().isEmpty() ? 8 : 16; + String text = null; + + if (!acc.getDraggingItem().isEmpty() && acc.getIsSplittingStack()) { + draggingItem = draggingItem.copyWithCount(Mth.ceil(draggingItem.getCount() / 2.0F)); + } else if (acc.getIsQuickCrafting() && acc.getQuickCraftSlots().size() > 1) { + draggingItem = draggingItem.copyWithCount(acc.getQuickCraftingRemainder()); + if (draggingItem.isEmpty()) { + text = ChatFormatting.YELLOW + "0"; + } + } + + drawFloatingItemStack(mcScreen, graphics, draggingItem, mouseX - x - xOffset, mouseY - y - yOffset, text); + } + graphics.pose().popPose(); + + if (!acc.getSnapbackItem().isEmpty()) { + float delta = (float) (Util.getMillis() - acc.getSnapbackTime()) / 100.0F; + + if (delta >= 1.0F) { + delta = 1.0F; + acc.setSnapbackItem(ItemStack.EMPTY); + } + + int snapBackOffsetX = acc.getSnapbackEnd().x - acc.getSnapbackStartX(); + int snapBackOffsetY = acc.getSnapbackEnd().y - acc.getSnapbackStartY(); + int snapBackX = acc.getSnapbackStartX() + (int) ((float) snapBackOffsetX * delta); + int snapBackY = acc.getSnapbackStartY() + (int) ((float) snapBackOffsetY * delta); + drawFloatingItemStack(mcScreen, graphics, acc.getSnapbackItem(), snapBackX, snapBackY, null); + } + graphics.pose().popPose(); + RenderSystem.enableDepthTest(); + Lighting.setupFor3DItems(); + Stencil.remove(); + } + + private static void drawFloatingItemStack(AbstractContainerScreen mcScreen, GuiGraphics graphics, + ItemStack stack, int x, int y, String altText) { + graphics.pose().pushPose(); + graphics.pose().translate(0.0F, 0.0F, 232.0F); + + var font = IClientItemExtensions.of(stack).getFont(stack, IClientItemExtensions.FontContext.ITEM_COUNT); + if (font == null) font = ((ScreenAccessor) mcScreen).getFont(); + graphics.renderItem(stack, x, y); + graphics.renderItemDecorations(font, stack, + x, y - (((AbstractContainerScreenAccessor) mcScreen).getDraggingItem().isEmpty() ? 0 : 8), altText); + graphics.pose().popPose(); + } + + private static void drawVanillaElements(GuiGraphics graphics, Screen mcScreen, int mouseX, int mouseY, + float partialTicks) { + for (Renderable renderable : mcScreen.renderables) { + renderable.render(graphics, mouseX, mouseY, partialTicks); + } + } + + public static void drawDebugScreen(GuiGraphics graphics, @Nullable ModularScreen muiScreen, + @Nullable ModularScreen fallback) { + fpsCounter.onDraw(); + if (!ConfigHolder.INSTANCE.dev.debugUI) return; + if (muiScreen == null) { + if (checkGui()) { + muiScreen = currentScreen; + } else { + if (fallback == null) return; + muiScreen = fallback; + } + } + RenderSystem.disableDepthTest(); + RenderSystem.enableBlend(); + + ModularGuiContext context = muiScreen.getContext(); + Matrix4f pose = graphics.pose().last().pose(); + + int mouseX = context.getMouseX(), mouseY = context.getMouseY(); + int screenH = muiScreen.getScreenArea().height; + int color = Color.argb(180, 40, 115, 220); + int lineY = screenH - 13; + graphics.drawString(Minecraft.getInstance().font, "Mouse Pos: " + mouseX + ", " + mouseY, 5, lineY, color, + true); + lineY -= 11; + graphics.drawString(Minecraft.getInstance().font, "FPS: " + fpsCounter.getFps(), 5, screenH - 24, color); + LocatedWidget locatedHovered = muiScreen.getPanelManager().getTopWidgetLocated(true); + if (locatedHovered != null) { + drawSegmentLine(graphics, lineY -= 4, color); + lineY -= 10; + + IGuiElement hovered = locatedHovered.getElement(); + locatedHovered.applyMatrix(context); + graphics.pose().pushPose(); + context.applyTo(graphics.pose()); + + Area area = hovered.getArea(); + IGuiElement parent = hovered.getParent(); + + GuiDraw.drawBorder(graphics, 0, 0, area.width, area.height, color, 1f); + if (hovered.hasParent()) { + GuiDraw.drawBorder(graphics, -area.rx, -area.ry, parent.getArea().width, parent.getArea().height, + Color.withAlpha(color, 0.3f), 1f); + } + graphics.pose().popPose(); + locatedHovered.unapplyMatrix(context); + GuiDraw.drawText(graphics, "Pos: " + area.x + ", " + area.y + " Rel: " + area.rx + ", " + area.ry, 5, + lineY, 1, color, false); + lineY -= 11; + GuiDraw.drawText(graphics, "Size: " + area.width + ", " + area.height, 5, lineY, 1, color, false); + lineY -= 11; + GuiDraw.drawText(graphics, "Class: " + hovered, 5, lineY, 1, color, false); + if (hovered.hasParent()) { + drawSegmentLine(graphics, lineY -= 4, color); + lineY -= 10; + area = parent.getArea(); + GuiDraw.drawText(graphics, "Parent size: " + area.width + ", " + area.height, 5, lineY, 1, color, + false); + lineY -= 11; + GuiDraw.drawText(graphics, "Parent: " + parent, 5, lineY, 1, color, false); + } + if (hovered instanceof ItemSlot slotWidget) { + drawSegmentLine(graphics, lineY -= 4, color); + lineY -= 10; + ModularSlot slot = slotWidget.getSlot(); + GuiDraw.drawText(graphics, "Slot Index: " + slot.getSlotIndex(), 5, lineY, 1, color, false); + lineY -= 11; + GuiDraw.drawText(graphics, "Slot Number: " + ((Slot) slot).index, 5, lineY, 1, color, false); + lineY -= 11; + if (slotWidget.isSynced()) { + SlotGroup slotGroup = slot.getSlotGroup(); + boolean allowShiftTransfer = slotGroup != null && slotGroup.isAllowShiftTransfer(); + GuiDraw.drawText(graphics, + "Shift-Click Priority: " + + (allowShiftTransfer ? slotGroup.getShiftClickPriority() : "DISABLED"), + 5, lineY, 1, color, false); + } + } else if (hovered instanceof RichTextWidget richTextWidget) { + drawSegmentLine(graphics, lineY -= 4, color); + lineY -= 10; + Object hoveredElement = richTextWidget.getHoveredElement(); + GuiDraw.drawText(graphics, "Hovered: " + hoveredElement, 5, lineY, 1, color, false); + } + } + // dot at mouse pos + GuiDraw.drawRect(graphics, mouseX, mouseY, 1, 1, Color.withAlpha(Color.GREEN.main, 0.8f)); + graphics.setColor(1f, 1f, 1f, 1f); + } + + private static void drawSegmentLine(GuiGraphics graphics, int y, int color) { + GuiDraw.drawRect(graphics, 5, y, 140, 1, color); + } + + public static boolean hasScreen() { + return currentScreen != null; + } + + @Nullable + public static Screen getMCScreen() { + return MCHelper.getCurrentScreen(); + } + + @Nullable + public static ModularScreen getMuiScreen() { + return currentScreen; + } + + private static boolean checkGui() { + return checkGui(MCHelper.getCurrentScreen()); + } + + private static boolean checkGui(Screen screen) { + if (currentScreen == null || !(screen instanceof IMuiScreen muiScreen)) return false; + if (screen != MCHelper.getCurrentScreen() || muiScreen.getScreen() != currentScreen) { + defaultContext.reset(); + currentScreen = null; + return false; + } + return true; + } + + public static GuiContext getBestContext() { + if (checkGui()) { + return currentScreen.getContext(); + } + return defaultContext; + } + + private enum InputPhase { + + // for mui interactions + EARLY, + // for mc interactions (like E and ESC) + LATE; + + public boolean isEarly() { + return this == EARLY; + } + + public boolean isLate() { + return this == LATE; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ContainerScreenWrapper.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ContainerScreenWrapper.java new file mode 100644 index 00000000000..da22004f438 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ContainerScreenWrapper.java @@ -0,0 +1,67 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.api.mui.base.IMuiScreen; +import com.gregtechceu.gtceu.api.mui.utils.Rectangle; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import lombok.Getter; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@OnlyIn(Dist.CLIENT) +public class ContainerScreenWrapper extends AbstractContainerScreen implements IMuiScreen { + + @Getter + private final @NotNull ModularScreen screen; + + public ContainerScreenWrapper(ModularContainerMenu container, @NotNull ModularScreen screen) { + super(container, container.getPlayer().getInventory(), Component.empty()); + this.screen = screen; + this.screen.construct(this); + } + + /** + * This is only used to create the menu type with Registrate. Do not use it (even though it may work). + * + * @deprecated Internal use only. + */ + @SuppressWarnings("DataFlowIssue") + @Deprecated + @ApiStatus.Internal + public ContainerScreenWrapper(ModularContainerMenu container, Inventory playerInventory, Component title) { + super(container, playerInventory, title); + if (container.isScreenInitialized()) { + this.screen = container.getScreen(); + this.screen.construct(this); + } else { + this.screen = null; + } + } + + @Override + public void renderBackground(@NotNull GuiGraphics guiGraphics) { + handleDrawBackground(guiGraphics, super::renderBackground); + } + + @Override + protected void renderBg(@NotNull GuiGraphics guiGraphics, float partialTick, int mouseX, int mouseY) {} + + @Override + public void updateGuiArea(Rectangle area) { + this.leftPos = area.x; + this.topPos = area.y; + this.imageWidth = area.width; + this.imageHeight = area.height; + } + + @Override + public boolean isPauseScreen() { + return this.screen.isPauseScreen(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/CustomModularScreen.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/CustomModularScreen.java new file mode 100644 index 00000000000..b32ca7928d1 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/CustomModularScreen.java @@ -0,0 +1,43 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * A {@link ModularScreen} which creates its panel via an overridable function for convenience. + */ +@OnlyIn(Dist.CLIENT) +public abstract class CustomModularScreen extends ModularScreen { + + /** + * Creates a new screen with ModularUI as its owner. + */ + public CustomModularScreen() { + super(GTCEu.MOD_ID); + } + + /** + * Creates a new screen with a given owner. + * + * @param owner owner of this screen (usually a mod id) + */ + public CustomModularScreen(@NotNull String owner) { + super(owner); + } + + /** + * Creates the main panel of this screen. It's called in the super constructor and must return a new panel instance. + * + * @param context context used to build the panel + * @return the created panel + */ + @NotNull + @ApiStatus.OverrideOnly + public abstract ModularPanel buildUI(ModularGuiContext context); +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/DraggablePanelWrapper.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/DraggablePanelWrapper.java new file mode 100644 index 00000000000..967998702aa --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/DraggablePanelWrapper.java @@ -0,0 +1,83 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.base.widget.IDraggable; +import com.gregtechceu.gtceu.api.mui.widget.WidgetTree; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import net.minecraft.client.gui.GuiGraphics; + +import lombok.Getter; + +public class DraggablePanelWrapper implements IDraggable { + + private final ModularPanel panel; + @Getter + private final Area movingArea; + private int relativeClickX, relativeClickY; + @Getter + private boolean moving; + + public DraggablePanelWrapper(ModularPanel panel) { + this.panel = panel; + this.movingArea = panel.getArea().createCopy(); + } + + @Override + public void drawMovingState(GuiGraphics graphics, ModularGuiContext context, float partialTicks) { + context.pushMatrix(); + transform(context); + WidgetTree.drawTree(this.panel, context, true); + context.popMatrix(); + } + + @Override + public boolean onDragStart(int button) { + if (button == 0) { + ModularGuiContext context = this.panel.getContext(); + this.movingArea.x = context.transformX(0, 0); + this.movingArea.y = context.transformY(0, 0); + this.relativeClickX = context.getMouseX() - this.movingArea.x; + this.relativeClickY = context.getMouseY() - this.movingArea.y; + return true; + } + return false; + } + + @Override + public void onDragEnd(boolean successful) { + if (successful) { + float y = this.panel.getContext().getMouseY() - this.relativeClickY; + float x = this.panel.getContext().getMouseX() - this.relativeClickX; + y = y / (this.panel.getScreen().getScreenArea().height - this.panel.getArea().height); + x = x / (this.panel.getScreen().getScreenArea().width - this.panel.getArea().width); + this.panel.flex().resetPosition(); + this.panel.flex().relativeToScreen(); + this.panel.flex().topRelAnchor(y, y) + .leftRelAnchor(x, x); + WidgetTree.resize(this.panel); + } + } + + @Override + public void onDrag(int mouseButton, double timeSinceLastClick) { + this.movingArea.x = this.panel.getContext().getMouseX() - this.relativeClickX; + this.movingArea.y = this.panel.getContext().getMouseY() - this.relativeClickY; + } + + @Override + public void setMoving(boolean moving) { + this.moving = moving; + this.panel.setEnabled(!moving); + } + + @Override + public void transform(IViewportStack stack) { + if (isMoving()) { + Area area = this.panel.getArea(); + stack.translate(-area.x, -area.y); + stack.translate(this.movingArea.x, this.movingArea.y); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/IClickableContainerScreen.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/IClickableContainerScreen.java new file mode 100644 index 00000000000..4f9efc45132 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/IClickableContainerScreen.java @@ -0,0 +1,10 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import net.minecraft.world.inventory.Slot; + +public interface IClickableContainerScreen { + + void gtceu$setClickedSlot(Slot slot); + + Slot gtceu$getClickedSlot(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ModularContainerMenu.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ModularContainerMenu.java new file mode 100644 index 00000000000..fbc1e5e2c47 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ModularContainerMenu.java @@ -0,0 +1,494 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.UIFactory; +import com.gregtechceu.gtceu.api.mui.factory.GuiData; +import com.gregtechceu.gtceu.api.mui.factory.GuiManager; +import com.gregtechceu.gtceu.api.mui.value.sync.ModularSyncManager; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.api.mui.widget.WidgetTree; +import com.gregtechceu.gtceu.api.mui.widgets.slot.ModularSlot; +import com.gregtechceu.gtceu.api.mui.widgets.slot.SlotGroup; +import com.gregtechceu.gtceu.common.data.GTMenuTypes; +import com.gregtechceu.gtceu.core.mixins.client.AbstractContainerMenuAccessor; +import com.gregtechceu.gtceu.utils.NetworkUtils; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.items.ItemHandlerHelper; + +import lombok.Getter; +import org.jetbrains.annotations.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class ModularContainerMenu extends AbstractContainerMenu { + + public static ModularContainerMenu getCurrent(Player player) { + if (player.containerMenu instanceof ModularContainerMenu container) { + return container; + } + return null; + } + + private static final int DROP_TO_WORLD = -999; + private static final int LEFT_MOUSE = 0; + private static final int RIGHT_MOUSE = 1; + + @Getter + private Player player; + private ModularSyncManager syncManager; + private boolean init = true; + // all phantom slots (inventory doesn't contain phantom slots) + private final List phantomSlots = new ArrayList<>(); + private final List shiftClickSlots = new ArrayList<>(); + @Getter + private GuiData guiData; + private UISettings settings; + + @OnlyIn(Dist.CLIENT) + private ModularScreen optionalScreen; + + public ModularContainerMenu(int containerId) { + super(GTMenuTypes.MODULAR_CONTAINER.get(), containerId); + } + + public ModularContainerMenu(MenuType type, int containerId, + Inventory playerInv, @Nullable FriendlyByteBuf data) { + super(type, containerId); + if (data != null) { + this.player = playerInv.player; + // Copied from GuiManager to (hopefully) set up the correct state even when someone didn't use the API. + UIFactory factory = (UIFactory) GuiManager.getFactory(data.readResourceLocation()); + T guiData = factory.readGuiData(player, data); + UISettings settings = new UISettings(); + settings.defaultCanInteractWith(factory, guiData); + PanelSyncManager syncManager = new PanelSyncManager(); + ModularPanel panel = factory.createPanel(guiData, syncManager, settings); + WidgetTree.collectSyncValues(syncManager, panel); + ModularScreen screen = factory.createScreen(guiData, panel); + screen.getContext().setSettings(settings); + this.optionalScreen = screen; + this.construct(player, syncManager, settings, panel.getName(), guiData); + } + } + + @ApiStatus.Internal + public void construct(Player player, PanelSyncManager panelSyncManager, UISettings settings, String mainPanelName, + GuiData guiData) { + this.player = player; + this.syncManager = new ModularSyncManager(this); + this.syncManager.construct(mainPanelName, panelSyncManager); + this.settings = settings; + this.guiData = guiData; + sortShiftClickSlots(); + } + + @OnlyIn(Dist.CLIENT) + void initializeClient(ModularScreen screen) { + this.optionalScreen = screen; + } + + @ApiStatus.Internal + @OnlyIn(Dist.CLIENT) + public void constructClientOnly() { + this.player = Minecraft.getInstance().player; + this.syncManager = null; + } + + public boolean isInitialized() { + return this.player != null; + } + + @OnlyIn(Dist.CLIENT) + public ModularScreen getScreen() { + if (this.optionalScreen == null) throw new NullPointerException("ModularScreen is not yet initialised!"); + return this.optionalScreen; + } + + public boolean isScreenInitialized() { + return this.optionalScreen != null; + } + + public AbstractContainerMenuAccessor acc() { + return (AbstractContainerMenuAccessor) this; + } + + @MustBeInvokedByOverriders + @Override + public void removed(@NotNull Player player) { + super.removed(player); + if (this.syncManager != null) { + this.syncManager.onClose(); + } + } + + @MustBeInvokedByOverriders + @Override + public void broadcastChanges() { + super.broadcastChanges(); + if (this.syncManager != null) { + this.syncManager.detectAndSendChanges(this.init); + } + this.init = false; + } + + private void sortShiftClickSlots() { + this.shiftClickSlots.sort(ModularSlot.SHIFT_CLICK_PRIORITY); + } + + @Override + public void initializeContents(int stateId, List items, @NotNull ItemStack carried) { + if (this.slots.size() != items.size()) { + GTCEu.LOGGER.error("Here are {} slots, but expected {}", this.slots.size(), items.size()); + } + super.initializeContents(stateId, items, carried); + } + + @ApiStatus.Internal + public void registerSlot(String panelName, ModularSlot slot) { + if (slot.isPhantom()) { + if (this.phantomSlots.contains(slot)) { + throw new IllegalArgumentException("Tried to register slot which already exists!"); + } + this.phantomSlots.add(slot); + } else { + if (this.slots.contains(slot)) { + throw new IllegalArgumentException("Tried to register slot which already exists!"); + } + addSlot(slot); + } + if (slot.getSlotGroupName() != null) { + SlotGroup slotGroup = getSyncManager().getSlotGroup(panelName, slot.getSlotGroupName()); + if (slotGroup == null) { + GTCEu.LOGGER.throwing( + new IllegalArgumentException("SlotGroup '" + slot.getSlotGroupName() + "' is not registered!")); + return; + } + slot.slotGroup(slotGroup); + } + if (slot.getSlotGroup() != null) { + SlotGroup slotGroup = slot.getSlotGroup(); + if (slotGroup.isAllowShiftTransfer()) { + this.shiftClickSlots.add(slot); + if (!this.init) { + sortShiftClickSlots(); + } + } + } + } + + @Contract("_, null, null -> fail") + @NotNull + @ApiStatus.Internal + public SlotGroup validateSlotGroup(String panelName, @Nullable String slotGroupName, + @Nullable SlotGroup slotGroup) { + if (slotGroup != null) { + if (getSyncManager().getSlotGroup(panelName, slotGroup.getName()) == null) { + throw new IllegalArgumentException("Slot group is not registered in the GUI."); + } + return slotGroup; + } + if (slotGroupName != null) { + slotGroup = getSyncManager().getSlotGroup(panelName, slotGroupName); + if (slotGroup == null) { + throw new IllegalArgumentException("Can't find slot group for name " + slotGroupName); + } + return slotGroup; + } + throw new IllegalArgumentException("Either the slot group or the name must not be null!"); + } + + public ModularSyncManager getSyncManager() { + if (this.syncManager == null) { + throw new IllegalStateException("GuiSyncManager is not available for client only GUI's."); + } + return this.syncManager; + } + + public boolean isClient() { + return this.syncManager == null || NetworkUtils.isClient(this.player); + } + + public boolean isClientOnly() { + return this.syncManager == null; + } + + public ModularSlot getModularSlot(int index) { + Slot slot = this.slots.get(index); + if (slot instanceof ModularSlot modularSlot) { + return modularSlot; + } + throw new IllegalStateException( + "A non-ModularSlot was found, but all slots in a ModularContainer must extend ModularSlot."); + } + + @UnmodifiableView + public List getShiftClickSlots() { + return Collections.unmodifiableList(this.shiftClickSlots); + } + + public void onSlotChanged(ModularSlot slot, ItemStack stack, boolean onlyAmountChanged) {} + + @Override + public boolean stillValid(@NotNull Player playerIn) { + return this.settings.canPlayerInteractWithUI(playerIn); + } + + @Override + public void clicked(int slotId, int mouseButton, @NotNull ClickType clickTypeIn, @NotNull Player player) { + ItemStack returnable = ItemStack.EMPTY; + Inventory inventory = player.getInventory(); + + if (clickTypeIn == ClickType.QUICK_CRAFT || acc().getQuickcraftType() != -1) { + superClicked(slotId, mouseButton, clickTypeIn, player); + return; + } + + if ((clickTypeIn == ClickType.PICKUP || clickTypeIn == ClickType.QUICK_MOVE) && + (mouseButton == LEFT_MOUSE || mouseButton == RIGHT_MOUSE)) { + if (slotId == DROP_TO_WORLD) { + // no dif + if (!this.getCarried().isEmpty()) { + if (mouseButton == LEFT_MOUSE) { + player.drop(this.getCarried(), true); + this.setCarried(ItemStack.EMPTY); + } + + if (mouseButton == RIGHT_MOUSE) { + player.drop(this.getCarried().split(1), true); + } + } + return; + } + + // early return + if (slotId < 0) return; + + if (clickTypeIn == ClickType.QUICK_MOVE) { + Slot fromSlot = getSlot(slotId); + + if (!fromSlot.mayPickup(player)) { + return; + } + // simpler code, but effectively no difference + quickMoveStack(player, slotId); + } else { + Slot clickedSlot = getSlot(slotId); + + ItemStack slotStack = clickedSlot.getItem(); + ItemStack heldStack = this.getCarried(); + + if (slotStack.isEmpty()) { + // no dif + if (!heldStack.isEmpty() && clickedSlot.mayPlace(heldStack)) { + int stackCount = mouseButton == LEFT_MOUSE ? heldStack.getCount() : 1; + + if (stackCount > clickedSlot.getMaxStackSize(heldStack)) { + stackCount = clickedSlot.getMaxStackSize(heldStack); + } + + clickedSlot.setByPlayer(heldStack.split(stackCount)); + } + } else if (clickedSlot.mayPickup(player)) { + if (heldStack.isEmpty() && !slotStack.isEmpty()) { + // checking max stack size here, probably for oversized slots + int s = Math.min(slotStack.getCount(), slotStack.getMaxStackSize()); + int toRemove = mouseButton == LEFT_MOUSE ? s : (s + 1) / 2; + this.setCarried(slotStack.split(toRemove)); + clickedSlot.setByPlayer(slotStack); + clickedSlot.onTake(player, this.getCarried()); + } else if (clickedSlot.mayPlace(heldStack)) { + if (ItemStack.isSameItemSameTags(slotStack, heldStack)) { + int stackCount = mouseButton == LEFT_MOUSE ? heldStack.getCount() : 1; + + if (stackCount > clickedSlot.getMaxStackSize(heldStack) - slotStack.getCount()) { + stackCount = clickedSlot.getMaxStackSize(heldStack) - slotStack.getCount(); + } + + heldStack.shrink(stackCount); + slotStack.grow(stackCount); + clickedSlot.setByPlayer(slotStack); + + } else if (heldStack.getCount() <= clickedSlot.getMaxStackSize(heldStack)) { + clickedSlot.setByPlayer(heldStack); + this.setCarried(slotStack); + } + } else if (heldStack.getMaxStackSize() > 1 && + ItemStack.isSameItemSameTags(slotStack, heldStack) && !slotStack.isEmpty()) { + int stackCount = slotStack.getCount(); + + if (stackCount + heldStack.getCount() <= heldStack.getMaxStackSize()) { + heldStack.grow(stackCount); + slotStack = clickedSlot.remove(stackCount); + + if (slotStack.isEmpty()) { + clickedSlot.setByPlayer(ItemStack.EMPTY); + } + + clickedSlot.onTake(player, this.getCarried()); + } + } + } + clickedSlot.setChanged(); + } + broadcastChanges(); + } else if (clickTypeIn == ClickType.PICKUP_ALL && slotId >= 0) { + Slot slot = slots.get(slotId); + ItemStack carried = this.getCarried(); + + if (!carried.isEmpty() && (!slot.hasItem() || !slot.mayPickup(player))) { + int i = mouseButton == 0 ? 0 : slots.size() - 1; + int j = mouseButton == 0 ? 1 : -1; + + for (int k = 0; k < 2; ++k) { + for (int l = i; l >= 0 && l < slots.size() && + carried.getCount() < carried.getMaxStackSize(); l += j) { + Slot slot1 = slots.get(l); + if (slot1 instanceof ModularSlot modularSlot && modularSlot.isPhantom()) continue; + + if (slot1.hasItem() && canItemQuickReplace(slot1, carried, true) && slot1.mayPickup(player) && + canTakeItemForPickAll(carried, slot1)) { + ItemStack slotItem = slot1.getItem(); + + if (k != 0 || slotItem.getCount() != slotItem.getMaxStackSize()) { + int toRemove = Math.min(carried.getMaxStackSize() - carried.getCount(), + slotItem.getCount()); + ItemStack removed = slot1.remove(toRemove); + carried.grow(toRemove); + + if (removed.isEmpty()) { + slot1.setByPlayer(ItemStack.EMPTY); + } + + slot1.onTake(player, removed); + } + } + } + } + } + + broadcastChanges(); + return; + } else if (clickTypeIn == ClickType.SWAP && mouseButton >= 0 && mouseButton < 9) { + ModularSlot phantom = getModularSlot(slotId); + ItemStack hotbarStack = inventory.getItem(mouseButton); + if (phantom.isPhantom()) { + // insert stack from hotbar slot into phantom slot + phantom.setByPlayer(hotbarStack.isEmpty() ? ItemStack.EMPTY : hotbarStack.copy()); + broadcastChanges(); + } + } else { + superClicked(slotId, mouseButton, clickTypeIn, player); + } + } + + protected final void superClicked(int slotId, int mouseButton, @NotNull ClickType clickTypeIn, + @NotNull Player player) { + super.clicked(slotId, mouseButton, clickTypeIn, player); + } + + @Override + public @NotNull ItemStack quickMoveStack(@NotNull Player playerIn, int index) { + ModularSlot slot = getModularSlot(index); + if (!slot.isPhantom()) { + ItemStack stack = slot.getItem(); + if (!stack.isEmpty()) { + stack = stack.copy(); + int base = 0; + if (stack.getCount() > stack.getMaxStackSize()) { + base = stack.getCount() - stack.getMaxStackSize(); + stack.setCount(stack.getMaxStackSize()); + } + ItemStack remainder = transferItem(slot, stack.copy()); + if (base == 0 && remainder.isEmpty()) stack = ItemStack.EMPTY; + else stack.setCount(base + remainder.getCount()); + slot.set(stack); + return ItemStack.EMPTY; + } + } + return ItemStack.EMPTY; + } + + protected ItemStack transferItem(ModularSlot fromSlot, ItemStack fromStack) { + @Nullable + SlotGroup fromSlotGroup = fromSlot.getSlotGroup(); + // in first iteration only insert into non-empty, non-phantom slots + for (ModularSlot toSlot : getShiftClickSlots()) { + SlotGroup slotGroup = Objects.requireNonNull(toSlot.getSlotGroup()); + if (slotGroup != fromSlotGroup && toSlot.isActive() && toSlot.mayPlace(fromStack)) { + ItemStack toStack = toSlot.getItem().copy(); + if (!fromSlot.isPhantom() && ItemHandlerHelper.canItemStacksStack(fromStack, toStack)) { + int j = toStack.getCount() + fromStack.getCount(); + // Math.min(toSlot.getMaxStackSize(), fromStack.getMaxStackSize()); + int maxSize = toSlot.getMaxStackSize(fromStack); + + if (j <= maxSize) { + fromStack.setCount(0); + toStack.setCount(j); + toSlot.set(toStack); + } else if (toStack.getCount() < maxSize) { + fromStack.shrink(maxSize - toStack.getCount()); + toStack.setCount(maxSize); + toSlot.set(toStack); + } + + if (fromStack.isEmpty()) { + return fromStack; + } + } + } + } + boolean hasNonEmptyPhantom = false; + // now insert into first empty slot (phantom or not) and check if we have any non-empty phantom slots + for (ModularSlot toSlot : getShiftClickSlots()) { + ItemStack itemstack = toSlot.getItem(); + SlotGroup slotGroup = Objects.requireNonNull(toSlot.getSlotGroup()); + if (slotGroup != fromSlotGroup && toSlot.isActive() && toSlot.mayPlace(fromStack)) { + if (toSlot.isPhantom()) { + if (!itemstack.isEmpty()) { + // skip non-empty phantom for now + hasNonEmptyPhantom = true; + } else { + toSlot.set(fromStack.copy()); + return fromStack; + } + } else if (itemstack.isEmpty()) { + if (fromStack.getCount() > toSlot.getMaxStackSize(fromStack)) { + toSlot.set(fromStack.split(toSlot.getMaxStackSize(fromStack))); + } else { + toSlot.set(fromStack.split(fromStack.getCount())); + } + if (fromStack.getCount() < 1) { + break; + } + } + } + } + if (!hasNonEmptyPhantom) return fromStack; + + // now insert into the first phantom slot we can find (will be non-empty) + // unfortunately, when all phantom slots are used it will always overwrite the first one + for (ModularSlot toSlot : getShiftClickSlots()) { + SlotGroup slotGroup = Objects.requireNonNull(toSlot.getSlotGroup()); + if (slotGroup != fromSlotGroup && toSlot.isPhantom() && toSlot.isActive() && toSlot.mayPlace(fromStack)) { + // don't check for stackable, just overwrite + toSlot.set(fromStack.copy()); + return fromStack; + } + } + return fromStack; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ModularPanel.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ModularPanel.java new file mode 100644 index 00000000000..309be9549b1 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ModularPanel.java @@ -0,0 +1,822 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.api.mui.base.IPanelHandler; +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.layout.IViewport; +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.base.widget.IFocusedWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.Interactable; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Animator; +import com.gregtechceu.gtceu.api.mui.utils.HoveredWidgetList; +import com.gregtechceu.gtceu.api.mui.utils.Interpolation; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncHandler; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler; +import com.gregtechceu.gtceu.api.mui.widget.ParentWidget; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.api.mui.widgets.SlotGroupWidget; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiViewportStack; +import com.gregtechceu.gtceu.client.mui.screen.viewport.LocatedWidget; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import net.minecraft.Util; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectList; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.MustBeInvokedByOverriders; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +/** + * This class is like a window in windows. It can hold any amount of widgets. It may also be draggable. + * To open another panel on top of the main panel you must use + * {@link IPanelHandler#simple(ModularPanel, SecondaryPanel.IPanelBuilder, boolean)} + * or {@link PanelSyncManager#panel(String, PanelSyncHandler.IPanelBuilder, boolean)} if the panel should be synced. + */ +public class ModularPanel extends ParentWidget implements IViewport { + + public static ModularPanel defaultPanel(@NotNull String name) { + return defaultPanel(name, 176, 166); + } + + public static ModularPanel defaultPanel(@NotNull String name, int width, int height) { + return new ModularPanel(name).size(width, height); + } + + private static final int tapTime = 200; + + @Getter + private final @NotNull String name; + private ModularScreen screen; + @Setter + private IPanelHandler panelHandler; + @Getter + private State state = State.IDLE; + private boolean cantDisposeNow = false; + @Getter + private final @NotNull ObjectArrayList hovering = new ObjectArrayList<>(); + private final Input keyboard = new Input(); + private final Input mouse = new Input(); + + private final List clientSubPanels = new ArrayList<>(); + private boolean invisible = false; + private Animator animator; + @Getter + private float scale = 1f; + @Getter + private float alpha = 1f; + + public ModularPanel(@NotNull String name) { + this.name = Objects.requireNonNull(name, "A panels name must not be null and should be unique!"); + center(); + } + + @Override + public @NotNull ModularPanel getPanel() { + return this; + } + + @Override + public Area getParentArea() { + return getScreen().getScreenArea(); + } + + @Override + public void onInit() { + getScreen().registerFrameUpdateListener(this, this::findHoveredWidgets, false); + } + + @Override + public boolean isValidSyncHandler(SyncHandler syncHandler) { + return syncHandler instanceof IPanelHandler; + } + + /** + * @return true if this panel is currently open on a screen + */ + public boolean isOpen() { + return this.state == State.OPEN; + } + + /** + * If this panel is open it will be closed. + * If animating is enabled and an animation is already playing this method will do nothing. + * + * @param animate true if the closing animation should play first. + */ + public void closeIfOpen(boolean animate) { + if (!isOpen()) return; + closeSubPanels(); + if (!animate || !shouldAnimate()) { + this.screen.getPanelManager().closePanel(this); + return; + } + if (!isOpening() && !isClosing()) { + if (isMainPanel()) { + // if this is the main panel, start closing animation for all panels + for (ModularPanel panel : getScreen().getPanelManager().getOpenPanels()) { + if (!panel.isMainPanel()) { + panel.closeIfOpen(true); + } + } + } + getAnimator().setEndCallback(val -> this.screen.getPanelManager().closePanel(this)).backward(); + } + } + + protected void closeSubPanels() { + if (this.panelHandler != null) { + this.panelHandler.closeSubPanels(); + } + } + + public void animateClose() { + closeIfOpen(true); + } + + @Override + public boolean hasParent() { + return false; + } + + @Override + public WidgetTheme getWidgetThemeInternal(ITheme theme) { + return theme.getPanelTheme(); + } + + @Override + public void transform(IViewportStack stack) { + super.transform(stack); + // apply scaling for animation + if (getScale() != 1f) { + float x = getArea().w() / 2f; + float y = getArea().h() / 2f; + stack.translate(x, y); + stack.scale(getScale(), getScale()); + stack.translate(-x, -y); + } + } + + @Override + public void getWidgetsAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y) { + if (hasChildren()) { + IViewport.getChildrenAt(this, stack, widgets, x, y); + } + } + + @Override + public void getSelfAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y) { + if (isInside(stack, x, y)) { + widgets.add(this, stack.peek()); + } + } + + private void findHoveredWidgets() { + this.hovering.clear(); + this.hovering.trim(); + if (!isEnabled()) { + return; + } + HoveredWidgetList widgetList = new HoveredWidgetList(this.hovering); + getContext().reset(); + GuiViewportStack stack = new GuiViewportStack(); + stack.pushViewport(null, getScreen().getScreenArea()); + stack.pushViewport(this, getArea()); + transform(stack); + getSelfAt(stack, widgetList, getContext().getMouseX(), getContext().getMouseY()); + transformChildren(stack); + getWidgetsAt(stack, widgetList, getContext().getMouseX(), getContext().getMouseY()); + stack.popViewport(this); + stack.popViewport(null); + } + + @Override + public boolean canHover() { + return !this.invisible && super.canHover(); + } + + @MustBeInvokedByOverriders + public void onOpen(ModularScreen screen) { + this.screen = screen; + getArea().z(1); + this.scale = 1f; + this.alpha = 1f; + initialise(this); + if (shouldAnimate()) { + this.scale = 0.75f; + this.alpha = 0f; + getAnimator().setEndCallback(value -> { + this.scale = 1f; + this.alpha = 1f; + }).forward(); + } + this.state = State.OPEN; + } + + void reopen() { + if (this.state != State.CLOSED) throw new IllegalStateException(); + this.state = State.OPEN; + } + + @MustBeInvokedByOverriders + public void onClose() { + if (!getScreen().isOverlay()) { + getContext().getXeiSettings().removeExclusionArea(this); + } + this.state = State.CLOSED; + if (this.panelHandler != null) { + this.panelHandler.closePanelInternal(); + } + } + + @MustBeInvokedByOverriders + @Override + public void dispose() { + if (this.state == State.DISPOSED) return; + if (this.state != State.CLOSED && this.state != State.WAIT_DISPOSING) { + throw new IllegalStateException("Panel must be closed before disposing!"); + } + if (this.cantDisposeNow) { + this.state = State.WAIT_DISPOSING; + return; + } + super.dispose(); + this.screen = null; + this.state = State.DISPOSED; + } + + /** + * Wraps a function so it can be called safely. This is needed in methods where the panel can be closed and + * disposed, but doing + * so will result in unexpected errors. This wrapper stops the disposal until the function has been fully executed. + * The return value of the function is then returned. + * + * @param runnable function to be called safely + * @param return type + * @return return value of function + */ + public final T doSafe(Supplier runnable) { + if (this.state == State.DISPOSED) return null; + // make sure the screen is also not disposed + return getScreen().getPanelManager().doSafe(() -> { + this.cantDisposeNow = true; + T t = runnable.get(); + this.cantDisposeNow = false; + if (this.state == State.WAIT_DISPOSING) { + this.state = State.CLOSED; + dispose(); + } + return t; + }); + } + + public final boolean doSafeBool(BooleanSupplier runnable) { + return Objects.requireNonNull(doSafe(runnable::getAsBoolean)); + } + + public final int doSafeInt(IntSupplier runnable) { + return Objects.requireNonNull(doSafe(runnable::getAsInt)); + } + + public boolean onMousePressed(double mouseX, double mouseY, int button) { + return doSafeBool(() -> { + LocatedWidget pressed = LocatedWidget.EMPTY; + boolean result = false; + + if (this.hovering.isEmpty()) { + // no element is hovered -> try close panel + if (closeOnOutOfBoundsClick()) { + animateClose(); + result = true; + } + } else { + for (LocatedWidget widget : this.hovering) { + widget.applyMatrix(getContext()); + // click widget and see how it reacts + if (widget.getElement() instanceof Interactable interactable) { + Interactable.Result interactResult = interactable.onMousePressed(mouseX, mouseY, button); + if (interactResult.accepts) { + this.mouse.addAcceptedInteractable(interactable); + pressed = widget; + } else if (interactResult.stops) { + pressed = LocatedWidget.EMPTY; + } + if (interactResult.stops) { + result = true; + widget.unapplyMatrix(getContext()); + break; + } + } + // see if widget can be dragged + if (getContext().onHoveredClick(button, widget)) { + pressed = LocatedWidget.EMPTY; + result = true; + widget.unapplyMatrix(getContext()); + break; + } + widget.unapplyMatrix(getContext()); + // see if widgets below this can be interacted with + if (!widget.getElement().canClickThrough()) { + // act as if the widget was clicked and accepted + result = true; + pressed = widget; + break; + } + } + } + + if (result && pressed.getElement() instanceof IFocusedWidget) { + getContext().focus(pressed); + } else { + getContext().removeFocus(); + } + this.mouse.pressed(pressed, button); + return result; + }); + } + + public boolean onMouseReleased(double mouseX, double mouseY, int button) { + return isEnabled() && doSafeBool(() -> { + if (!this.mouse.doRelease) { + this.mouse.reset(); + return false; + } + if (interactFocused(widget -> widget.onMouseReleased(mouseX, mouseY, button), false)) { + return true; + } + boolean lastPressedIsHovered = false; + boolean tryTap = this.mouse.tryTap(button); + // first see if the clicked widget is still hovered and try to interact with it + for (LocatedWidget widget : this.hovering) { + if (this.mouse.isWidget(widget)) { + if (widget.getElement() instanceof Interactable interactable && + onMouseReleased(mouseX, mouseY, button, tryTap, widget, interactable)) { + return true; + } + lastPressedIsHovered = true; + break; + } + } + // now try all other hovered + for (LocatedWidget widget : this.hovering) { + if (!this.mouse.isWidget(widget) && widget.getElement() instanceof Interactable interactable && + onMouseReleased(mouseX, mouseY, button, tryTap, widget, interactable)) { + return true; + } + } + // nothing worked, but since the pressed widget is still hovered we assume success + // otherwise JEI tries to pull some weird stuff + if (lastPressedIsHovered) { + this.mouse.reset(); + return true; + } + this.mouse.reset(); + return false; + }); + } + + private boolean onMouseReleased(double mouseX, double mouseY, int button, boolean tryTap, LocatedWidget widget, + Interactable interactable) { + boolean stop = false; + widget.applyMatrix(getContext()); + if (tryTap && this.mouse.acceptedInteractions.remove(interactable)) { + Interactable.Result tabResult = interactable.onMouseTapped(mouseX, mouseY, button); + if (tabResult.stops) { + stop = true; + // we will try to trigger onMouseReleased() even after tapping tells to stop + } + } + if (interactable.onMouseReleased(mouseX, mouseY, button)) { + stop = true; + } + widget.unapplyMatrix(getContext()); + if (stop) { + this.mouse.reset(); + return true; + } + return false; + } + + public boolean onKeyPressed(int keyCode, int scanCode, int modifiers) { + return doSafeBool(() -> { + switch (interactFocused(widget -> widget.onKeyPressed(keyCode, scanCode, modifiers), + Interactable.Result.IGNORE)) { + case STOP: + this.keyboard.pressed(LocatedWidget.EMPTY, keyCode); + return true; + case SUCCESS: + this.keyboard.pressed(getContext().getFocusedWidget(), keyCode); + return true; + } + LocatedWidget pressed = null; + boolean result = false; + for (LocatedWidget widget : this.hovering) { + if (widget.getElement() instanceof Interactable interactable) { + widget.applyMatrix(getContext()); + Interactable.Result interactResult = interactable.onKeyPressed(keyCode, scanCode, modifiers); + if (interactResult.accepts) { + this.keyboard.addAcceptedInteractable(interactable); + pressed = widget; + } else if (interactResult.stops) { + pressed = null; + } + if (interactResult.stops) { + result = true; + widget.unapplyMatrix(getContext()); + break; + } + widget.unapplyMatrix(getContext()); + } + if (!widget.getElement().canClickThrough()) break; + } + this.keyboard.pressed(pressed, keyCode); + return result; + }); + } + + public boolean onKeyReleased(int keyCode, int scanCode, int modifiers) { + return doSafeBool(() -> { + if (!this.keyboard.doRelease) { + this.keyboard.reset(); + return false; + } + if (interactFocused(widget -> widget.onKeyReleased(keyCode, scanCode, modifiers), false)) { + return true; + } + boolean lastPressedIsHovered = false; + boolean tryTap = this.keyboard.tryTap(keyCode); + // first see if the clicked widget is still hovered and try to interact with it + for (LocatedWidget widget : this.hovering) { + if (this.keyboard.isWidget(widget)) { + if (widget.getElement() instanceof Interactable interactable && + onKeyReleased(keyCode, scanCode, modifiers, tryTap, widget, interactable)) { + return true; + } + lastPressedIsHovered = true; + break; + } + } + // now try all other hovered + for (LocatedWidget widget : this.hovering) { + if (!this.keyboard.isWidget(widget) && widget.getElement() instanceof Interactable interactable && + onKeyReleased(keyCode, scanCode, modifiers, tryTap, widget, interactable)) { + return true; + } + } + // nothing worked, but since the pressed widget is still hovered we assume success + // otherwise JEI tries to pull some weird stuff + if (lastPressedIsHovered) { + this.keyboard.reset(); + return true; + } + this.keyboard.reset(); + return false; + }); + } + + private boolean onKeyReleased(int keyCode, int scanCode, int modifiers, boolean tryTap, LocatedWidget widget, + Interactable interactable) { + boolean stop = false; + widget.applyMatrix(getContext()); + if (tryTap && this.keyboard.acceptedInteractions.remove(interactable)) { + Interactable.Result tabResult = interactable.onKeyTapped(keyCode, scanCode, modifiers); + if (tabResult.stops) { + stop = true; + // we will try to trigger onMouseReleased() even after tapping tells to stop + } + } + if (interactable.onKeyReleased(keyCode, scanCode, modifiers)) { + stop = true; + } + widget.unapplyMatrix(getContext()); + if (stop) { + this.keyboard.reset(); + return true; + } + return false; + } + + public boolean onCharTyped(char codePoint, int modifiers) { + return doSafeBool(() -> { + switch (interactFocused(widget -> widget.onCharTyped(codePoint, modifiers), Interactable.Result.IGNORE)) { + case STOP: + this.keyboard.pressed(LocatedWidget.EMPTY, codePoint); + return true; + case SUCCESS: + this.keyboard.pressed(getContext().getFocusedWidget(), codePoint); + return true; + } + LocatedWidget pressed = null; + boolean result = false; + for (LocatedWidget widget : this.hovering) { + if (widget.getElement() instanceof Interactable interactable) { + widget.applyMatrix(getContext()); + Interactable.Result interactResult = interactable.onCharTyped(codePoint, modifiers); + if (interactResult.accepts) { + this.keyboard.addAcceptedInteractable(interactable); + pressed = widget; + } else if (interactResult.stops) { + pressed = null; + } + if (interactResult.stops) { + result = true; + widget.unapplyMatrix(getContext()); + break; + } + widget.unapplyMatrix(getContext()); + } + if (!widget.getElement().canClickThrough()) break; + } + this.keyboard.pressed(pressed, codePoint); + return result; + }); + } + + public boolean onMouseScrolled(double mouseX, double mouseY, double delta) { + return doSafeBool(() -> { + if (interactFocused(widget -> widget.onMouseScrolled(mouseX, mouseY, delta), false)) { + return true; + } + if (this.hovering.isEmpty()) return false; + for (LocatedWidget widget : this.hovering) { + if (widget.getElement() instanceof Interactable interactable) { + widget.applyMatrix(getContext()); + boolean result = interactable.onMouseScrolled(mouseX, mouseY, delta); + widget.unapplyMatrix(getContext()); + if (result) return true; + } + } + return false; + }); + } + + public boolean onMouseDrag(double mouseX, double mouseY, int button, double dragX, double dragY) { + return doSafeBool(() -> { + if (this.mouse.held && + button == this.mouse.lastButton && + this.mouse.lastPressed != null && + this.mouse.lastPressed.getElement() instanceof Interactable interactable) { + this.mouse.lastPressed.applyMatrix(getContext()); + interactable.onMouseDrag(mouseX, mouseY, button, dragX, dragY); + this.mouse.lastPressed.unapplyMatrix(getContext()); + return true; + } + return false; + }); + } + + @SuppressWarnings("unchecked") + private T interactFocused(Function function, + T defaultValue) { + LocatedWidget focused = this.getContext().getFocusedWidget(); + T result = defaultValue; + if (focused.getElement() instanceof Interactable interactable) { + focused.applyMatrix(getContext()); + result = function.apply((W) interactable); + focused.unapplyMatrix(getContext()); + } + return result; + } + + /** + * @return if this panel can be dragged. Never works on the main panel. + */ + public boolean isDraggable() { + return getScreen().getMainPanel() != this; + } + + /** + * @return if panels below this can still be interacted with. + */ + public boolean disablePanelsBelow() { + return false; + } + + /** + * @return if this panel should be closed if outside of this panel is clicked. + */ + public boolean closeOnOutOfBoundsClick() { + return false; + } + + @Override + public ModularScreen getScreen() { + if (!isValid()) { + throw new IllegalStateException(); + } + return this.screen; + } + + @Nullable + public IWidget getTopHovering() { + LocatedWidget lw = getTopHoveringLocated(false); + return lw == null ? null : lw.getElement(); + } + + @Nullable + public LocatedWidget getTopHoveringLocated(boolean debug) { + int i = 0; + while (i < this.hovering.size()) { + LocatedWidget widget = this.hovering.get(i); + if (!widget.getElement().isValid()) { + this.hovering.remove(i); + continue; + } + if (debug || widget.getElement().canHover()) { + return widget; + } + i++; + } + return null; + } + + @Override + public int getDefaultHeight() { + return 166; + } + + @Override + public int getDefaultWidth() { + return 176; + } + + final void setPanelGuiContext(@NotNull ModularGuiContext context) { + setContext(context); + if (!context.getScreen().isOverlay()) { + context.getXeiSettings().addExclusionArea(this); + } + } + + public boolean isOpening() { + return this.animator != null && this.animator.isRunningForwards(); + } + + public boolean isClosing() { + return this.animator != null && this.animator.isRunningBackwards(); + } + + public final boolean isMainPanel() { + return getScreen().getMainPanel() == this; + } + + @ApiStatus.Internal + @Override + public void setSyncHandler(@Nullable SyncHandler syncHandler) { + if (!isValidSyncHandler(syncHandler)) + throw new IllegalStateException("Panel SyncHandler's must implement IPanelHandler!"); + + super.setSyncHandler(syncHandler); + setPanelHandler((IPanelHandler) syncHandler); + } + + @NotNull + protected Animator getAnimator() { + if (this.animator == null) { + this.animator = new Animator(getScreen().getCurrentTheme().getOpenCloseAnimationOverride(), + Interpolation.QUINT_OUT) + .setValueBounds(0.0f, 1.0f) + .setCallback(val -> { + this.alpha = (float) val; + this.scale = (float) val * 0.25f + 0.75f; + }); + } + return this.animator; + } + + public boolean shouldAnimate() { + return !getScreen().isOverlay() && getScreen().getCurrentTheme().getOpenCloseAnimationOverride() > 0; + } + + void registerSubPanel(IPanelHandler handler) { + if (!this.clientSubPanels.contains(handler)) { + this.clientSubPanels.add(handler); + } + } + + void closeClientSubPanels() { + for (IPanelHandler handler : this.clientSubPanels) { + if (handler.isSubPanel()) { + handler.closePanel(); + } + } + } + + public ModularPanel bindPlayerInventory() { + return child(SlotGroupWidget.playerInventory(true)); + } + + public ModularPanel bindPlayerInventory(int bottom) { + return child(SlotGroupWidget.playerInventory(bottom, true)); + } + + public ModularPanel invisible() { + this.invisible = true; + return background(IDrawable.EMPTY); + } + + @Override + public String toString() { + return super.toString() + "#" + getName(); + } + + public enum State { + /** + * Initial state of any panel + */ + IDLE, + /** + * State after the panel opened + */ + OPEN, + /** + * State after panel closed + */ + CLOSED, + /** + * State after panel disposed. + * Panel can still be reopened in this state. + */ + DISPOSED, + /** + * Panel is closed and is waiting to be disposed. + */ + WAIT_DISPOSING + } + + /** + * A helper class to handle input states for mouse and keyboard separatly + */ + private static class Input { + + private final ObjectList acceptedInteractions = new ObjectArrayList<>(); + @Nullable + private LocatedWidget lastPressed; + private boolean held; + private long time; + private int lastButton; + private boolean doRelease = true; + + private Input() { + reset(); + } + + private void addAcceptedInteractable(Interactable interactable) { + if (!this.held) { + this.acceptedInteractions.add(interactable); + } + } + + private void reset() { + this.acceptedInteractions.clear(); + this.held = false; + this.time = -1; + this.lastButton = -1; + this.doRelease = true; + } + + private boolean isValid() { + return this.lastPressed != null && this.time > 0; + } + + private int getTimeSinceEvent() { + return (int) Math.min(Util.getMillis() - this.time, Integer.MAX_VALUE); + } + + private boolean tryTap(int button) { + return this.lastButton == button && getTimeSinceEvent() <= tapTime; + } + + private boolean isWidget(IWidget widget) { + return this.lastPressed != null && this.lastPressed.getElement() == widget; + } + + private boolean isWidget(LocatedWidget widget) { + return isWidget(widget.getElement()); + } + + private void pressed(LocatedWidget pressed, int button) { + if (!this.held) { + this.lastPressed = pressed; + if (this.lastPressed != null) { + this.time = Util.getMillis(); + } + this.lastButton = button; + this.held = true; + } + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ModularScreen.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ModularScreen.java new file mode 100644 index 00000000000..609d1d1c16f --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ModularScreen.java @@ -0,0 +1,813 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.IMuiScreen; +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.IThemeApi; +import com.gregtechceu.gtceu.api.mui.base.MCHelper; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiAction; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.drawable.GuiDraw; +import com.gregtechceu.gtceu.api.mui.overlay.OverlayScreenWrapper; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.value.sync.ModularSyncManager; +import com.gregtechceu.gtceu.api.mui.widget.WidgetTree; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.api.mui.widget.wrapper.WidgetWrapper; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Renderable; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.gui.layouts.LayoutElement; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.navigation.ScreenRectangle; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectIterator; +import lombok.Getter; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.MustBeInvokedByOverriders; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.StreamSupport; + +/** + * This is the base class for all modular UIs. It only exists on client side. + * It handles drawing the screen, all panels and widget interactions. + */ +@OnlyIn(Dist.CLIENT) +public class ModularScreen implements GuiEventListener, Renderable, LayoutElement, NarratableEntry { + + public static boolean isScreen(@Nullable Screen guiScreen, String owner, String name) { + if (guiScreen instanceof IMuiScreen screenWrapper) { + ModularScreen screen = screenWrapper.getScreen(); + return screen.getOwner().equals(owner) && screen.getName().equals(name); + } + return false; + } + + public static boolean isActive(String owner, String name) { + return isScreen(Minecraft.getInstance().screen, owner, name); + } + + @Nullable + public static ModularScreen getCurrent() { + if (MCHelper.getCurrentScreen() instanceof IMuiScreen screenWrapper) { + return screenWrapper.getScreen(); + } + return null; + } + + /** + * The owner of this screen. Usually a modid. This is mainly used to find theme overrides. + */ + @Getter + private final String owner; + /** + * The name of this screen, which is also the name of the panel. Every UI under one owner should have a different + * name. + * Unfortunately there is no good way to verify this, so it's the UI implementors responsibility to set a proper + * name for the main panel. + * This is mainly used to find theme overrides. + */ + @Getter + private final String name; + @Getter + private final PanelManager panelManager; + @Getter + private final ModularGuiContext context = new ModularGuiContext(this); + private final Map, List> guiActionListeners = new Object2ObjectOpenHashMap<>(); + private final Object2ObjectArrayMap frameUpdates = new Object2ObjectArrayMap<>(); + @Getter + private boolean pauseScreen = false; + + @Getter + private ITheme currentTheme; + @Getter + private IMuiScreen screenWrapper; + /** + * true if this is an overlay for another screen + */ + @Getter + private boolean overlay = false; + + /** + * Creates a new screen with a ModularUI as its owner and a given {@link ModularPanel}. + * + * @param mainPanel main panel of this screen + */ + public ModularScreen(@NotNull ModularPanel mainPanel) { + this(GTCEu.MOD_ID, mainPanel); + } + + /** + * Creates a new screen with a given owner and {@link ModularPanel}. + * + * @param owner owner of this screen (usually a mod id) + * @param mainPanel main panel of this screen + */ + public ModularScreen(@NotNull String owner, @NotNull ModularPanel mainPanel) { + this(owner, context -> mainPanel); + } + + /** + * Creates a new screen with the given owner and a main panel function. The function must return a non-null value. + * + * @param owner owner of this screen (usually a mod id) + * @param mainPanelCreator function which creates the main panel of this screen + */ + public ModularScreen(@NotNull String owner, @NotNull Function mainPanelCreator) { + this(owner, Objects.requireNonNull(mainPanelCreator, "The main panel function must not be null!"), false); + } + + private ModularScreen(@NotNull String owner, @Nullable Function mainPanelCreator, + boolean ignored) { + Objects.requireNonNull(owner, "The owner must not be null!"); + this.owner = owner; + ModularPanel mainPanel = mainPanelCreator != null ? mainPanelCreator.apply(this.context) : + buildUI(this.context); + Objects.requireNonNull(mainPanel, "The main panel must not be null!"); + this.name = mainPanel.getName(); + this.currentTheme = IThemeApi.get().getThemeForScreen(this, null); + this.panelManager = new PanelManager(this, mainPanel); + } + + /** + * Intended for use in {@link CustomModularScreen} + */ + ModularScreen(@NotNull String owner) { + this(owner, null, false); + } + + /** + * Intended for use in {@link CustomModularScreen} + */ + ModularPanel buildUI(ModularGuiContext context) { + throw new UnsupportedOperationException(); + } + + /** + * Should be called in custom {@link ScreenWrapper GuiScreen} constructors which implement {@link IMuiScreen}. + * + * @param wrapper the gui screen wrapping this screen + */ + @MustBeInvokedByOverriders + public void construct(IMuiScreen wrapper) { + if (this.screenWrapper != null) throw new IllegalStateException("ModularScreen is already constructed!"); + if (wrapper == null) throw new NullPointerException("ScreenWrapper must not be null!"); + this.screenWrapper = wrapper; + if (this.screenWrapper.getWrappedScreen() instanceof AbstractContainerScreen containerScreen) { + if (containerScreen.getMenu() instanceof ModularContainerMenu modular && !modular.isScreenInitialized()) { + modular.initializeClient(this); + } + } + this.screenWrapper.updateGuiArea(this.panelManager.getMainPanel().getArea()); + this.overlay = false; + } + + @ApiStatus.Internal + @MustBeInvokedByOverriders + public void constructOverlay(Screen screen) { + if (this.screenWrapper != null) throw new IllegalStateException("ModularScreen is already constructed!"); + if (screen == null) throw new NullPointerException("ScreenWrapper must not be null!"); + this.screenWrapper = new OverlayScreenWrapper(screen, this); + this.overlay = true; + } + + /** + * Called everytime the Game window changes its size. Overriding for additional logic is allowed, but super must be + * called. + * This method resizes the entire widget tree of every panel currently open and then updates the size of the + * {@link IMuiScreen} wrapper. + *

+ * Do not call this method except in an override! + * + * @param width with of the resized game window + * @param height height of the resized game window + */ + @MustBeInvokedByOverriders + public void onResize(int width, int height) { + this.context.updateScreenArea(width, height); + if (this.panelManager.tryInit()) { + onOpen(); + } + + this.context.pushViewport(null, this.context.getScreenArea()); + for (ModularPanel panel : this.panelManager.getReverseOpenPanels()) { + WidgetTree.resize(panel); + } + + this.context.popViewport(null); + if (!isOverlay()) { + this.screenWrapper.updateGuiArea(this.panelManager.getMainPanel().getArea()); + } + } + + public final void onCloseParent() { + if (this.panelManager.closeAll()) { + onClose(); + } + } + + @ApiStatus.OverrideOnly + public void onOpen() {} + + @ApiStatus.OverrideOnly + public void onClose() {} + + public void close() { + close(false); + } + + public void close(boolean force) { + if (isActive()) { + if (force) { + MCHelper.closeScreen(); + return; + } + getMainPanel().closeIfOpen(true); + } + } + + /** + * Checks if a panel with a given name is currently open in this screen. + * + * @param name name of the panel + * @return true if a panel with the name is open + */ + public boolean isPanelOpen(String name) { + return this.panelManager.isPanelOpen(name); + } + + /** + * Checks if a panel is currently open in this screen. + * + * @param panel panel to check + * @return true if the panel is open + */ + public boolean isPanelOpen(ModularPanel panel) { + return this.panelManager.hasOpenPanel(panel); + } + + /** + * Called at the start of every client tick (20 times per second). + */ + @MustBeInvokedByOverriders + public void onUpdate() { + for (ModularPanel panel : this.panelManager.getOpenPanels()) { + WidgetTree.onUpdate(panel); + } + } + + /** + * Called 60 times per second in custom ticks. This logic is separate from rendering. + */ + @MustBeInvokedByOverriders + public void onFrameUpdate() { + this.panelManager.checkDirty(); + for (ObjectIterator> iterator = this.frameUpdates + .object2ObjectEntrySet().fastIterator(); iterator.hasNext();) { + Object2ObjectMap.Entry entry = iterator.next(); + if (!entry.getKey().isValid()) { + iterator.remove(); + continue; + } + entry.getValue().run(); + } + this.context.onFrameUpdate(); + } + + /** + * Draws this screen and all open panels with their whole widget tree. + *

+ * Do not call, only override! + */ + @Override + public void render(@NotNull GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + Lighting.setupForFlatItems(); + RenderSystem.disableDepthTest(); + + this.context.reset(); + this.context.pushViewport(null, this.context.getScreenArea()); + for (ModularPanel panel : this.panelManager.getReverseOpenPanels()) { + this.context.updateZ(panel.getArea().getPanelLayer() * 20); + if (panel.disablePanelsBelow()) { + GuiDraw.drawRect(graphics, 0, 0, this.context.getScreenArea().w(), this.context.getScreenArea().h(), + Color.argb(16, 16, 16, (int) (125 * panel.getAlpha()))); + } + WidgetTree.drawTree(panel, this.context); + } + this.context.updateZ(0); + this.context.popViewport(null); + + this.context.postRenderCallbacks.forEach(element -> element.accept(this.context)); + Lighting.setupFor3DItems(); + } + + /** + * Called after all panels with their whole widget trees and potential additional elements are drawn. + *

+ * Do not call, only override! + */ + public void drawForeground(GuiGraphics guiGraphics, float partialTicks) { + Lighting.setupForFlatItems(); + RenderSystem.disableDepthTest(); + + this.context.reset(); + this.context.pushViewport(null, this.context.getScreenArea()); + for (ModularPanel panel : this.panelManager.getReverseOpenPanels()) { + this.context.updateZ(100 + panel.getArea().getPanelLayer() * 20); + if (panel.isEnabled()) { + WidgetTree.drawTreeForeground(panel, this.context); + } + } + this.context.drawDraggable(guiGraphics); + this.context.popViewport(null); + + Lighting.setupFor3DItems(); + } + + /** + * Called when a mouse button is pressed or released. Used to handle dropping of currently dragged elements. + */ + public boolean handleDraggableInput(double mouseX, double mouseY, int button, boolean pressed) { + if (this.context.hasDraggable()) { + if (pressed) { + this.context.onMousePressed(mouseX, mouseY, button); + } else { + this.context.onMouseReleased(mouseX, mouseY, button); + } + return true; + } + return false; + } + + /** + * Called when a mouse button is pressed. Tries to invoke + * {@link com.gregtechceu.gtceu.api.mui.base.widget.Interactable#onMousePressed(double, double, int) + * Interactable#onMousePressed(double, double, int)} on every widget under + * the mouse after gui action listeners have been called. Will try to focus widgets that have been interacted with. + * Focused widgets will be interacted with first in other interaction methods (mouse scroll, release and drag, key + * press and release). + * + * @param mouseX mouse x-coordinate + * @param mouseY mouse y-coordinate + * @param button mouse button (0 = left button, 1 = right button, 2 = scroll button, 4 and 5 = side buttons) + * @return true if the action was consumed and further processing should be canceled + */ + public boolean onMousePressed(double mouseX, double mouseY, int button) { + for (IGuiAction.MousePressed action : getGuiActionListeners(IGuiAction.MousePressed.class)) { + action.press(mouseX, mouseY, button); + } + if (this.context.onMousePressed(mouseX, mouseY, button)) { + return true; + } + for (ModularPanel panel : this.panelManager.getOpenPanels()) { + if (panel.onMousePressed(mouseX, mouseY, button)) { + return true; + } + if (panel.disablePanelsBelow()) { + break; + } + } + return false; + } + + /** + * Called when a mouse button is released. Tries to invoke + * {@link com.gregtechceu.gtceu.api.mui.base.widget.Interactable#onMouseReleased(double, double, int) + * Interactable#onMouseRelease(int)} on every widget under + * the mouse after gui action listeners have been called. + * + * @param mouseX mouse x-coordinate + * @param mouseY mouse y-coordinate + * @param button mouse button (0 = left button, 1 = right button, 2 = scroll button, 4 and 5 = side buttons) + * @return true if the action was consumed and further processing should be canceled + */ + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + for (IGuiAction.MouseReleased action : getGuiActionListeners(IGuiAction.MouseReleased.class)) { + action.release(mouseX, mouseY, button); + } + if (this.context.onMouseReleased(mouseX, mouseY, button)) { + return true; + } + for (ModularPanel panel : this.panelManager.getOpenPanels()) { + if (panel.onMouseReleased(mouseX, mouseY, button)) { + return true; + } + if (panel.disablePanelsBelow()) { + break; + } + } + return false; + } + + /** + * Called when a keyboard key is pressed. Tries to invoke + * {@link com.gregtechceu.gtceu.api.mui.base.widget.Interactable#onKeyPressed(int, int, int) + * Interactable#onKeyPressed(int, int, int)} on every + * widget under the mouse after gui action listeners have been called. + * + * @param keyCode the key code of the pressed key (see constants at {@link InputConstants}) + * @param scanCode the character of the pressed key or {@link Character#MIN_VALUE} for keys without a character + * @param modifiers the key modifiers of the pressed key (see modifiers at {@link InputConstants}) + * @return true if the action was consumed and further processing should be canceled + */ + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + for (IGuiAction.KeyPressed action : getGuiActionListeners(IGuiAction.KeyPressed.class)) { + action.press(keyCode, scanCode, modifiers); + } + for (ModularPanel panel : this.panelManager.getOpenPanels()) { + if (panel.onKeyPressed(keyCode, scanCode, modifiers)) { + return true; + } + if (panel.disablePanelsBelow()) { + break; + } + } + return false; + } + + /** + * Called when a keyboard key is released. Tries to invoke + * {@link com.gregtechceu.gtceu.api.mui.base.widget.Interactable#onKeyReleased(int, int, int) + * Interactable#onKeyRelease(int, int, int)} on every + * widget under the mouse after gui action listeners have been called. + * + * @param keyCode the key code of the pressed key (see constants at {@link InputConstants}) + * @param scanCode the character of the pressed key or {@link Character#MIN_VALUE} for keys without a character + * @param modifiers the key modifiers of the pressed key (see modifiers at {@link InputConstants}) + * @return true if the action was consumed and further processing should be canceled + */ + @Override + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + for (IGuiAction.KeyReleased action : getGuiActionListeners(IGuiAction.KeyReleased.class)) { + action.release(keyCode, scanCode, modifiers); + } + for (ModularPanel panel : this.panelManager.getOpenPanels()) { + if (panel.onKeyReleased(keyCode, scanCode, modifiers)) { + return true; + } + if (panel.disablePanelsBelow()) { + break; + } + } + return false; + } + + /** + * Called when a keyboard key is released. Tries to invoke + * {@link com.gregtechceu.gtceu.api.mui.base.widget.Interactable#onCharTyped(char, int) + * Interactable#onCharTyped(char, int)} on every + * widget under the mouse after gui action listeners have been called. + * + * @param codePoint the character of the pressed key + * @param modifiers the key modifiers of the pressed key (see modifiers at {@link InputConstants}) + * @return true if the action was consumed and further processing should be canceled + */ + @Override + public boolean charTyped(char codePoint, int modifiers) { + for (IGuiAction.CharTyped action : getGuiActionListeners(IGuiAction.CharTyped.class)) { + action.type(codePoint, modifiers); + } + for (ModularPanel panel : this.panelManager.getOpenPanels()) { + if (panel.onCharTyped(codePoint, modifiers)) { + return true; + } + if (panel.disablePanelsBelow()) { + break; + } + } + return false; + } + + /** + * Called when a mouse button is released. Tries to invoke + * {@link com.gregtechceu.gtceu.api.mui.base.widget.Interactable#onMouseScrolled(double, double, double) + * Interactable#onMouseScrolled(double, double, double)} on every widget under + * the mouse after gui action listeners have been called. + * + * @param mouseX mouse x-coordinate + * @param mouseY mouse y-coordinate + * @param delta the direction and speed of the scroll + * @return true if the action was consumed and further processing should be canceled + */ + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double delta) { + for (IGuiAction.MouseScroll action : getGuiActionListeners(IGuiAction.MouseScroll.class)) { + action.scroll(mouseX, mouseY, delta); + } + for (ModularPanel panel : this.panelManager.getOpenPanels()) { + if (panel.onMouseScrolled(mouseX, mouseY, delta)) { + return true; + } + if (panel.disablePanelsBelow()) { + break; + } + } + return false; + } + + /** + * Called every time the mouse pos changes and a mouse button is held down. Invokes + * {@link com.gregtechceu.gtceu.api.mui.base.widget.Interactable#onMouseDrag(double, double, int, double, double) + * Interactable#onMouseDrag(double, double, int, double, double)} on every widget + * under the mouse after gui action listeners have been called. + * + * @param mouseX starting mouse x-coordinate + * @param mouseY starting mouse y-coordinate + * @param button mouse button that is held down (0 = left button, 1 = right button, 2 = scroll button, 4 and 5 = + * side buttons) + * @param dragX ending mouse y-coordinate + * @param dragY ending mouse y-coordinate + * @return true if the action was consumed and further processing should be canceled + */ + @Override + public boolean mouseDragged(double mouseX, double mouseY, int button, double dragX, double dragY) { + for (IGuiAction.MouseDrag action : getGuiActionListeners(IGuiAction.MouseDrag.class)) { + action.drag(mouseX, mouseY, button, dragX, dragY); + } + for (ModularPanel panel : this.panelManager.getOpenPanels()) { + if (panel.onMouseDrag(mouseX, mouseY, button, dragX, dragY)) { + return true; + } + if (panel.disablePanelsBelow()) { + break; + } + } + return false; + } + + /** + * Called with {@code true} after a widget which implements + * {@link com.gregtechceu.gtceu.api.mui.base.widget.IFocusedWidget IFocusedWidget} + * has consumed a mouse press and called with {@code false} if a widget is currently focused and anything else has + * consumed a mouse + * press. This is required for other mods like JEI/EMI to not interfere with inputs. + * + * @param focus true if the gui screen will be focused + */ + @ApiStatus.Internal + public void setFocused(boolean focus) { + this.screenWrapper.getWrappedScreen().setFocused(focus); + } + + @Override + public boolean isFocused() { + return this.screenWrapper.getWrappedScreen().isFocused(); + } + + /** + * @return true if this screen is currently open and displayed on the screen + */ + public boolean isActive() { + return getCurrent() == this; + } + + /** + * @return the owner and name as a {@link ResourceLocation} + * @see #getOwner() + * @see #getName() + */ + public ResourceLocation getResourceLocation() { + return new ResourceLocation(this.owner, this.name); + } + + public ModularSyncManager getSyncManager() { + return getContainer().getSyncManager(); + } + + public ModularPanel getMainPanel() { + return this.panelManager.getMainPanel(); + } + + public Area getScreenArea() { + return this.context.getScreenArea(); + } + + public boolean isClientOnly() { + return isOverlay() || !this.screenWrapper.isGuiContainer() || getContainer().isClientOnly(); + } + + public ModularContainerMenu getContainer() { + if (isOverlay()) { + throw new IllegalStateException("Can't get ModularContainer for overlay"); + } + if (this.screenWrapper.getWrappedScreen() instanceof AbstractContainerScreen container) { + return (ModularContainerMenu) container.getMenu(); + } + throw new IllegalStateException("Screen does not extend AbstractContainerScreen!"); + } + + @SuppressWarnings("unchecked") + private List getGuiActionListeners(Class clazz) { + return (List) this.guiActionListeners.getOrDefault(clazz, Collections.emptyList()); + } + + /** + * Registers an interaction listener. This is useful when you want to listen to any GUI interactions and not just + * for a specific widget.
+ * Do NOT register listeners which are bound to a widget here! + * Use {@link com.gregtechceu.gtceu.api.mui.widget.Widget#listenGuiAction(IGuiAction) + * Widget#listenGuiAction(IGuiAction)} for that! + * + * @param action action listener + */ + public void registerGuiActionListener(IGuiAction action) { + List list = this.guiActionListeners.computeIfAbsent(getGuiActionClass(action), + key -> new ArrayList<>()); + if (!list.contains(action)) list.add(action); + } + + /** + * Removes an interaction listener + * + * @param action action listener to remove + */ + public void removeGuiActionListener(IGuiAction action) { + this.guiActionListeners.getOrDefault(getGuiActionClass(action), Collections.emptyList()).remove(action); + } + + /** + * Registers a frame update listener which runs approximately 60 times per second. + * Listeners are automatically removed if the widget becomes invalid. + * If a listener is already registered from the given widget, the listeners get merged. + * + * @param widget widget the listener is bound to + * @param runnable listener function + */ + public void registerFrameUpdateListener(IWidget widget, Runnable runnable) { + registerFrameUpdateListener(widget, runnable, true); + } + + /** + * Registers a frame update listener which runs approximately 60 times per second. + * Listeners are automatically removed if the widget becomes invalid. + * If a listener is already registered from the given widget and merge is true, the listeners get + * merged. + * Otherwise, the current listener is overwritten (if any) + * + * @param widget widget the listener is bound to + * @param runnable listener function + * @param merge if listener should be merged with existing listener + */ + public void registerFrameUpdateListener(IWidget widget, Runnable runnable, boolean merge) { + Objects.requireNonNull(runnable); + if (merge) { + this.frameUpdates.merge(widget, runnable, (old, now) -> () -> { + old.run(); + now.run(); + }); + } else { + this.frameUpdates.put(widget, runnable); + } + } + + /** + * Removes all frame update listeners for a widget. + * + * @param widget widget to remove listeners from + */ + public void removeFrameUpdateListener(IWidget widget) { + this.frameUpdates.remove(widget); + } + + private static Class getGuiActionClass(IGuiAction action) { + Class[] classes = action.getClass().getInterfaces(); + for (Class clazz : classes) { + if (IGuiAction.class.isAssignableFrom(clazz)) { + return clazz; + } + } + throw new IllegalArgumentException(); + } + + /** + * Tries to use a specific theme for this screen. If the theme for this screen has been overriden via resource + * packs, this method does + * nothing. + * + * @param theme id of theme to use + * @return this for builder like usage + */ + public ModularScreen useTheme(String theme) { + this.currentTheme = IThemeApi.get().getThemeForScreen(this, theme); + return this; + } + + /** + * Sets if the gui should pause the game in the background. Pausing means every ticking will halt. If the client is + * connected to a + * dedicated server the UI will NEVER pause the game. + * + * @param pausesGame true if the ui should pause the game in the background. + * @return this for builder like usage + */ + public ModularScreen pausesGame(boolean pausesGame) { + this.pauseScreen = pausesGame; + return this; + } + + @Override + public void setX(int x) { + this.panelManager.getMainPanel().getArea().setX(x); + } + + @Override + public void setY(int y) { + this.panelManager.getMainPanel().getArea().setY(y); + } + + @Override + public int getX() { + return this.panelManager.getMainPanel().getArea().getX(); + } + + @Override + public int getY() { + return this.panelManager.getMainPanel().getArea().getY(); + } + + @Override + public int getWidth() { + return this.panelManager.getMainPanel().getArea().getWidth(); + } + + @Override + public int getHeight() { + return this.panelManager.getMainPanel().getArea().getHeight(); + } + + @Override + public @NotNull ScreenRectangle getRectangle() { + Area area = this.panelManager.getMainPanel().getArea(); + return new ScreenRectangle(area.x(), area.y(), area.w(), area.h()); + } + + @Override + public void visitWidgets(@NotNull Consumer consumer) { + for (WidgetWrapper wrapper : panelManager.getReverseOpenPanelsWrappers()) { + consumer.accept(wrapper); + } + } + + private static final Component USAGE_NARRATION = Component.translatable("narrator.screen.usage"); + + private NarratableEntry lastNarratable = null; + + @Override + public void updateNarration(@NotNull NarrationElementOutput output) { + output.add(NarratedElementType.USAGE, USAGE_NARRATION); + var entries = StreamSupport.stream(panelManager.getReverseOpenPanelsWrappers().spliterator(), false); + WidgetWrapper.updateNarrations(entries, output, lastNarratable, entry -> lastNarratable = entry); + } + + @Override + public @NotNull NarrationPriority narrationPriority() { + if (this.isFocused()) return NarrationPriority.FOCUSED; + else if (this.context.isHovered()) return NarrationPriority.HOVERED; + else return NarrationPriority.NONE; + } + + public enum UpOrDown { + + UP(1), + DOWN(-1); + + public final int modifier; + + UpOrDown(int modifier) { + this.modifier = modifier; + } + + public boolean isUp() { + return this == UP; + } + + public boolean isDown() { + return this == DOWN; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/PanelManager.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/PanelManager.java new file mode 100644 index 00000000000..969a2ac456d --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/PanelManager.java @@ -0,0 +1,348 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.IPanelHandler; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.widget.WidgetTree; +import com.gregtechceu.gtceu.api.mui.widget.wrapper.WidgetWrapper; +import com.gregtechceu.gtceu.client.mui.screen.viewport.LocatedWidget; +import com.gregtechceu.gtceu.utils.ReverseIterable; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectList; +import lombok.Getter; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.*; +import java.util.function.Supplier; + +public class PanelManager { + + @Getter + private final @NotNull ModularScreen screen; + /** + * At least one panel must exist always exist. + * If this panel is closed, all panels will close. + */ + private final ModularPanel mainPanel; + /** + * List of all open panels from top to bottom. + */ + private final ObjectList panels = new ObjectArrayList<>(); + // a clone of the list to avoid CMEs + private final List panelsClone = new ArrayList<>(); + private final List panelsView = Collections.unmodifiableList(this.panelsClone); + private final ReverseIterable reversePanels = new ReverseIterable<>(this.panelsView); + private final List panelWrappers = new ArrayList<>(); + private final List panelWrappersView = Collections.unmodifiableList(this.panelWrappers); + private final ReverseIterable reversePanelWrappers = new ReverseIterable<>(this.panelWrappersView); + private final ObjectList disposal = new ObjectArrayList<>(20); + private final Map panelHandlerMap = new Object2ObjectOpenHashMap<>(); + private boolean cantDisposeNow = false; + private boolean dirty = false; + private State state = State.INIT; + + public PanelManager(@NotNull ModularScreen screen, ModularPanel panel) { + this.screen = screen; + this.mainPanel = Objects.requireNonNull(panel, "Main panel must not be null!"); + } + + boolean tryInit() { + if (this.state == State.CLOSED) throw new IllegalStateException("Can't init in closed state!"); + if (this.state == State.INIT || this.state == State.DISPOSED) { + setState(State.OPEN); + openPanel(this.mainPanel, false); + checkDirty(); + return true; + } + return false; + } + + public boolean isMainPanel(ModularPanel panel) { + return this.mainPanel == panel; + } + + void checkDirty() { + if (this.dirty) { + this.panelsClone.clear(); + this.panelsClone.addAll(this.panels); + + this.panelWrappers.clear(); + this.panelsClone.stream() + .map(WidgetWrapper::new) + .forEach(this.panelWrappers::add); + + this.dirty = false; + } + } + + private void openPanel(ModularPanel panel, boolean resize) { + if (this.panels.size() == 127) { + throw new IllegalStateException("Too many panels are open!"); + } + if (this.panels.contains(panel) || isPanelOpen(panel.getName())) { + throw new IllegalStateException("Panel " + panel.getName() + " is already open."); + } + this.disposal.remove(panel); + panel.setPanelGuiContext(this.screen.getContext()); + this.panels.add(0, panel); + this.dirty = true; + panel.getArea().setPanelLayer((byte) this.panels.size()); + panel.onOpen(this.screen); + if (resize) { + WidgetTree.resize(panel); + } + } + + public boolean isPanelOpen(String name) { + for (ModularPanel panel : this.panels) { + if (panel.getName().equals(name)) { + return true; + } + } + return false; + } + + @NotNull + public ModularPanel getMainPanel() { + if (isDisposed()) { + throw new IllegalStateException("Screen has been disposed"); + } + return this.mainPanel; + } + + /** + * Returns the panel that was opened last. + * + * @return last opened panel + * @throws IndexOutOfBoundsException if the current state is {@link State#DISPOSED} + */ + @NotNull + public ModularPanel getTopMostPanel() { + return this.panels.get(0); + } + + @Nullable + public IWidget getTopWidget() { + for (ModularPanel panel : this.panels) { + IWidget widget = panel.getTopHovering(); + if (widget != null) { + return widget; + } + } + return null; + } + + @Nullable + public LocatedWidget getTopWidgetLocated(boolean debug) { + for (ModularPanel panel : this.panels) { + LocatedWidget widget = panel.getTopHoveringLocated(debug); + if (widget != null) { + return widget; + } + } + return null; + } + + @ApiStatus.Internal + public void openPanel(@NotNull ModularPanel panel, @NotNull IPanelHandler panelHandler) { + IPanelHandler existing = this.panelHandlerMap.get(panel.getName()); + if (existing == null) { + this.panelHandlerMap.put(panel.getName(), panelHandler); + } else if (existing != panelHandler) { + GTCEu.LOGGER.error( + "Tried to open a panel, but a panel handler that opens the same panel already exists. Using existing panel handler!"); + existing.openPanel(); + return; + } + openPanel(panel, true); + } + + public void closePanel(@NotNull ModularPanel panel) { + if (!hasOpenPanel(panel)) { + throw new IllegalArgumentException("Panel '" + panel.getName() + "' is open in this screen!"); + } + if (panel == getMainPanel()) { + closeAll(); + this.screen.close(true); + return; + } + if (this.panels.remove(panel)) { + finalizePanel(panel); + this.dirty = true; + } + } + + public void closeTopPanel(boolean animate) { + getTopMostPanel().closeIfOpen(animate); + } + + public boolean closeAll() { + if (this.state.isOpen) { + this.panels.forEach(this::finalizePanel); + setState(State.CLOSED); + this.screen.onClose(); + return true; + } + return false; + } + + private void finalizePanel(ModularPanel panel) { + panel.onClose(); + if (!this.disposal.contains(panel)) { + if (this.disposal.size() == 20) { + this.disposal.remove(0).dispose(); + } + this.disposal.add(panel); + } + } + + public T doSafe(Supplier runnable) { + if (isDisposed()) return null; + this.cantDisposeNow = true; + T t = runnable.get(); + this.cantDisposeNow = false; + if (this.state == State.WAIT_DISPOSAL) { + setState(State.CLOSED); + dispose(); + } + return t; + } + + @ApiStatus.Internal + public void dispose() { + if (isDisposed()) return; + if (this.cantDisposeNow) { + setState(State.WAIT_DISPOSAL); + return; + } + if (!isClosed()) throw new IllegalStateException("Must close screen first before disposing!"); + this.disposal.forEach(ModularPanel::dispose); + this.disposal.clear(); + this.panels.clear(); + this.panelsClone.clear(); + this.panelWrappers.clear(); + this.dirty = false; + setState(State.DISPOSED); + } + + @ApiStatus.Internal + public void reopen() { + if (this.panels.isEmpty()) { + throw new IllegalStateException("Screen is disposed. Can't be recovered!"); + } + this.panels.forEach(ModularPanel::reopen); + this.disposal.removeIf(this.panels::contains); + setState(State.REOPENED); + } + + public boolean hasOpenPanel(ModularPanel panel) { + return this.panels.contains(panel); + } + + public void pushUp(@NotNull ModularPanel window) { + int index = this.panels.indexOf(window); + if (index < 0) throw new IllegalStateException(); + if (index == 0) return; + this.panels.remove(index); + this.panels.add(index - 1, window); + } + + public void pushDown(@NotNull ModularPanel window) { + int index = this.panels.indexOf(window); + if (index < 0) throw new IllegalStateException(); + if (index == this.panels.size() - 1) return; + this.panels.remove(index); + this.panels.add(index + 1, window); + } + + public void pushToTop(@NotNull ModularPanel window) { + int index = this.panels.indexOf(window); + if (index < 0) throw new IllegalStateException(); + if (index == 0) return; + this.panels.remove(index); + this.panels.add(0, window); + } + + public void pushToBottom(@NotNull ModularPanel window) { + int index = this.panels.indexOf(window); + if (index < 0) throw new IllegalStateException(); + if (index == this.panels.size() - 1) return; + this.panels.remove(index); + this.panels.add(window); + } + + @NotNull + @UnmodifiableView + public List getOpenPanels() { + checkDirty(); + return this.panelsView; + } + + @NotNull + @UnmodifiableView + public Iterable getReverseOpenPanels() { + checkDirty(); + return this.reversePanels; + } + + @NotNull + @UnmodifiableView + public List getOpenPanelsWrappers() { + checkDirty(); + return this.panelWrappersView; + } + + @NotNull + @UnmodifiableView + public Iterable getReverseOpenPanelsWrappers() { + checkDirty(); + return this.reversePanelWrappers; + } + + private void setState(State state) { + this.state = state; + } + + public boolean isClosed() { + return this.state == State.CLOSED || this.state == State.DISPOSED; + } + + public boolean isDisposed() { + return this.state == State.DISPOSED; + } + + public boolean isOpen() { + return this.state.isOpen; + } + + public boolean isReopened() { + return this.state == State.REOPENED; + } + + private void checkDisposed() { + if (isDisposed()) { + throw new IllegalStateException("Screen is disposed!"); + } + } + + public enum State { + + INIT(false), + OPEN(true), + REOPENED(true), + CLOSED(false), + WAIT_DISPOSAL(true), + DISPOSED(false); + + public final boolean isOpen; + + State(boolean isOpen) { + this.isOpen = isOpen; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/RichTooltip.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/RichTooltip.java new file mode 100644 index 00000000000..4fb6211689f --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/RichTooltip.java @@ -0,0 +1,385 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.MCHelper; +import com.gregtechceu.gtceu.api.mui.base.drawable.IRichTextBuilder; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.drawable.GuiDraw; +import com.gregtechceu.gtceu.api.mui.drawable.text.RichText; +import com.gregtechceu.gtceu.api.mui.drawable.text.TextRenderer; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.utils.Rectangle; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.config.ConfigHolder; + +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.client.gui.screens.inventory.tooltip.DefaultTooltipPositioner; +import net.minecraft.locale.Language; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.FormattedText; +import net.minecraft.util.Mth; +import net.minecraft.world.inventory.tooltip.TooltipComponent; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.client.event.RenderTooltipEvent; +import net.minecraftforge.common.MinecraftForge; + +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.datafixers.util.Either; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +@Accessors(chain = true) +public class RichTooltip implements IRichTextBuilder { + + private static final Area HOLDER = new Area(); + + private final Consumer parent; + private final RichText text = new RichText(); + private Pos pos = null; + private Consumer tooltipBuilder; + @Getter + @Setter + private int showUpTimer = 0; + @Getter + @Setter + private boolean autoUpdate = false; + private int titleMargin = 0; + private boolean appliedMargin = true; + + private int x = 0, y = 0; + private int maxWidth = Integer.MAX_VALUE; + + private boolean dirty; + + public RichTooltip(IWidget parent) { + this(area -> { + area.setSize(parent.getArea()); + area.setPos(0, 0); + }); + } + + public RichTooltip(Area parent) { + this(area -> area.set(parent)); + } + + public RichTooltip(Supplier parent) { + this(area -> area.set(parent.get())); + } + + public RichTooltip(Consumer parent) { + this.parent = parent; + } + + public void buildTooltip() { + this.dirty = false; + if (this.tooltipBuilder != null) { + this.text.clearText(); + this.tooltipBuilder.accept(this); + this.appliedMargin = false; + } + } + + public void draw(GuiContext context) { + draw(context, ItemStack.EMPTY); + } + + public void draw(GuiContext context, @Nullable ItemStack stack) { + if (this.autoUpdate) markDirty(); + if (isEmpty()) return; + + if (this.maxWidth <= 0) { + this.maxWidth = Integer.MAX_VALUE; + } + if (stack == null) stack = ItemStack.EMPTY; + if (!this.appliedMargin) { + if (this.titleMargin > 0) { + this.text.insertTitleMargin(this.titleMargin); + } + this.appliedMargin = true; + } + Area screen = context.getScreenArea(); + this.maxWidth = Math.min(this.maxWidth, screen.width); + int mouseX = context.getMouseX(), mouseY = context.getMouseY(); + TextRenderer renderer = TextRenderer.SHARED; + // this only turns the text and not any drawables into strings + List> textLines = this.text.getStringRepresentation().stream() + .>map(Either::left) + .collect(Collectors.toList()); + + var gatherEvent = new RenderTooltipEvent.GatherComponents(stack, screen.width, screen.height, textLines, + this.maxWidth); + if (MinecraftForge.EVENT_BUS.post(gatherEvent)) return; // canceled + this.maxWidth = gatherEvent.getMaxWidth(); + textLines = gatherEvent.getTooltipElements(); + List components = textLines.stream() + .map(either -> either.map( + text -> ClientTooltipComponent.create(text instanceof Component ? + ((Component) text).getVisualOrderText() : Language.getInstance().getVisualOrder(text)), + ClientTooltipComponent::create)) + .toList(); + + RenderTooltipEvent.Pre event = new RenderTooltipEvent.Pre(stack, context.getGraphics(), + mouseX, mouseY, screen.width, screen.height, + TextRenderer.getFont(), components, DefaultTooltipPositioner.INSTANCE); + if (MinecraftForge.EVENT_BUS.post(event)) return; // canceled + // we are supposed to now use the strings of the event, but we can't properly determine where to put them + mouseX = event.getX(); + mouseY = event.getY(); + int screenWidth = event.getScreenWidth(), screenHeight = event.getScreenHeight(); + + // simulate to figure how big this tooltip is without any restrictions + this.text.setupRenderer(renderer, 0, 0, this.maxWidth, -1, Color.WHITE.main, false); + this.text.compileAndDraw(renderer, context, true); + + Rectangle area = determineTooltipArea(context, renderer, screenWidth, screenHeight, mouseX, mouseY); + + Lighting.setupForFlatItems(); + RenderSystem.disableDepthTest(); + RenderSystem.disableBlend(); + + GuiDraw.drawTooltipBackground(context.getGraphics(), stack, components, area.x, area.y, area.width, + area.height); + + // MinecraftForge.EVENT_BUS.post(new RenderTooltipEvent.PostBackground(stack, textLines, area.x, area.y, + // TextRenderer.getFont(), area.width, area.height)); + + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + + renderer.setPos(area.x, area.y); + this.text.compileAndDraw(renderer, context, false); + + // MinecraftForge.EVENT_BUS.post(new RenderTooltipEvent.PostText(stack, textLines, area.x, area.y, + // TextRenderer.getFont(), area.width, area.height)); + } + + public Rectangle determineTooltipArea(GuiContext context, TextRenderer renderer, int screenWidth, int screenHeight, + int mouseX, int mouseY) { + int width = (int) renderer.getLastWidth(); + int height = (int) renderer.getLastHeight(); + + Pos pos = this.pos; + if (pos == null) { + pos = context.isMuiContext() ? + context.getMuiContext().getScreen().getCurrentTheme().getTooltipPosOverride() : null; + if (pos == null) pos = ConfigHolder.INSTANCE.client.ui.tooltipPos; + } + if (pos == Pos.FIXED) { + return new Rectangle(this.x, this.y, width, height); + } + + if (pos == Pos.NEXT_TO_MOUSE) { + // vanilla style, tooltip floats next to mouse + // note that this behaves slightly different from vanilla (better imo) + final int padding = 8; + // magic number to place tooltip nicer. Look at Screen#L237 + final int mouseOffset = 12; + int x = mouseX + mouseOffset, y = mouseY - mouseOffset; + if (x < padding) { + x = padding; // this cant happen mathematically since mouse is always positive + } else if (x + width + padding > screenWidth) { + // doesn't fit on the right side of the screen + if (screenWidth - mouseX < mouseX) { // check if left side has more space + x -= mouseOffset * 2 + width; // flip side of cursor if other side has more space + if (x < padding) { + x = padding; // went off-screen + } + width = mouseX - mouseOffset - x; // max space on left side + } else { + width = screenWidth - padding - x; // max space on right side + } + // recalculate width and height + renderer.setPos(x, y); + renderer.setAlignment(this.text.getAlignment(), width, -1); + this.text.compileAndDraw(renderer, context, true); + width = (int) renderer.getLastWidth(); + height = (int) renderer.getLastHeight(); + } + y = Mth.clamp(y, padding, screenHeight - padding - height); + return new Rectangle(x, y, width, height); + } + + // the rest of the cases will put the tooltip next a given area + if (this.parent == null) { + throw new IllegalStateException("Tooltip pos is " + pos.name() + ", but no widget parent is set!"); + } + + int minWidth = this.text.getMinWidth(); + + int shiftAmount = 10; + int padding = 7; + + Area area = HOLDER; + this.parent.accept(area); + area.transformAndRectanglerize(context); + int x = 0, y = 0; + if (pos.axis.isVertical()) { // above or below + if (width < area.width) { + x = area.x + shiftAmount; + } else { + x = area.x - shiftAmount; + if (x < padding) { + x = padding; + } else if (x + width > screenWidth - padding) { + int maxWidth = Math.max(minWidth, screenWidth - x - padding); + renderer.setAlignment(this.text.getAlignment(), maxWidth); + this.text.compileAndDraw(renderer, context, true); + width = (int) renderer.getLastWidth(); + height = (int) renderer.getLastHeight(); + } + } + + if (pos == Pos.VERTICAL) { + int bottomSpace = screenHeight - area.ey(); + pos = bottomSpace < height + padding && bottomSpace < area.y ? Pos.ABOVE : Pos.BELOW; + } + + if (pos == Pos.BELOW) { + y = area.ey() + padding; + } else if (pos == Pos.ABOVE) { + y = area.y - height - padding; + } + } else if (pos.axis.isHorizontal()) { + boolean usedMoreSpaceSide = false; + Pos oPos = pos; + if (oPos == Pos.HORIZONTAL) { + if (area.x > screenWidth - area.ex()) { + pos = Pos.LEFT; + // x = 0; + } else { + pos = Pos.RIGHT; + x = screenWidth - area.ex() + padding; + } + } + + if (height < area.height) { + y = area.y + shiftAmount; + } else { + y = area.y - shiftAmount; + if (y < padding) { + y = padding; + } + } + + if (x + width > screenWidth - padding) { + int maxWidth; + if (pos == Pos.LEFT) { + maxWidth = Math.max(minWidth, area.x - padding * 2); + } else { + maxWidth = Math.max(minWidth, screenWidth - area.ex() - padding * 2); + } + usedMoreSpaceSide = true; + renderer.setAlignment(this.text.getAlignment(), maxWidth); + this.text.compileAndDraw(renderer, context, true); + width = (int) renderer.getLastWidth(); + height = (int) renderer.getLastHeight(); + } + + if (oPos == Pos.HORIZONTAL && !usedMoreSpaceSide) { + int rightSpace = screenWidth - area.ex(); + pos = rightSpace < width + padding && rightSpace < area.x ? Pos.LEFT : Pos.RIGHT; + } + + if (pos == Pos.RIGHT) { + x = area.ex() + padding; + } else if (pos == Pos.LEFT) { + x = area.x - width - padding; + } + } + return new Rectangle(x, y, width, height); + } + + public boolean isEmpty() { + if (this.dirty) buildTooltip(); + return this.text.isEmpty(); + } + + public void markDirty() { + this.dirty = true; + } + + public RichTooltip pos(Pos pos) { + this.pos = pos; + return this; + } + + public RichTooltip pos(int x, int y) { + this.pos = Pos.FIXED; + this.x = x; + this.y = y; + return this; + } + + @Override + public RichTooltip getThis() { + return this; + } + + @Override + public IRichTextBuilder getRichText() { + return text; + } + + public RichTooltip showUpTimer(int showUpTimer) { + return this.setShowUpTimer(showUpTimer); + } + + public RichTooltip tooltipBuilder(Consumer tooltipBuilder) { + Consumer existingBuilder = this.tooltipBuilder; + if (existingBuilder != null) { + this.tooltipBuilder = tooltip -> { + existingBuilder.accept(this); + tooltipBuilder.accept(this); + }; + } else { + this.tooltipBuilder = tooltipBuilder; + } + markDirty(); + return this; + } + + public RichTooltip addFromItem(ItemStack item) { + List lines = MCHelper.getItemToolTip(item); + add(lines.get(0)).spaceLine(2); + for (int i = 1, n = lines.size(); i < n; i++) { + add(lines.get(i)).newLine(); + } + return this; + } + + public RichTooltip titleMargin() { + return titleMargin(0); + } + + public RichTooltip titleMargin(int margin) { + this.titleMargin = margin; + this.appliedMargin = false; + return this; + } + + public enum Pos { + + ABOVE(GuiAxis.Y), + BELOW(GuiAxis.Y), + LEFT(GuiAxis.X), + RIGHT(GuiAxis.X), + VERTICAL(GuiAxis.Y), + HORIZONTAL(GuiAxis.X), + NEXT_TO_MOUSE(null), + FIXED(null); + + public final GuiAxis axis; + + Pos(GuiAxis axis) { + this.axis = axis; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ScreenWrapper.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ScreenWrapper.java new file mode 100644 index 00000000000..6f3dfce2bb2 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/ScreenWrapper.java @@ -0,0 +1,35 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.api.mui.base.IMuiScreen; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +@OnlyIn(Dist.CLIENT) +public class ScreenWrapper extends Screen implements IMuiScreen { + + @Getter + private final @NotNull ModularScreen screen; + + public ScreenWrapper(@NotNull ModularScreen screen) { + super(Component.empty()); + this.screen = screen; + this.screen.construct(this); + } + + @Override + public void renderBackground(@NotNull GuiGraphics guiGraphics) { + handleDrawBackground(guiGraphics, super::renderBackground); + } + + @Override + public boolean isPauseScreen() { + return this.screen.isPauseScreen(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/SecondaryPanel.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/SecondaryPanel.java new file mode 100644 index 00000000000..f52d7588a11 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/SecondaryPanel.java @@ -0,0 +1,104 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.api.mui.base.IPanelHandler; +import com.gregtechceu.gtceu.api.mui.base.MCHelper; +import com.gregtechceu.gtceu.api.mui.widget.WidgetTree; + +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import org.jetbrains.annotations.ApiStatus; + +import java.util.Objects; + +public class SecondaryPanel implements IPanelHandler { + + private final ModularPanel parent; + private final IPanelBuilder provider; + private final boolean subPanel; + private ModularScreen screen; + private ModularPanel panel; + private boolean open = false; + private boolean queueDelete = false; + + public SecondaryPanel(ModularPanel parent, IPanelBuilder provider, boolean subPanel) { + this.parent = parent; + this.provider = provider; + this.subPanel = subPanel; + parent.registerSubPanel(this); + } + + @Override + public void closePanel() { + if (!this.open) return; + this.panel.animateClose(); + } + + @Override + public void closeSubPanels() { + if (this.panel != null) { + this.panel.closeClientSubPanels(); + } + } + + @ApiStatus.Internal + @Override + public void closePanelInternal() { + this.open = false; + if (this.queueDelete) { + this.panel = null; + this.queueDelete = false; + } + } + + @Override + public void deleteCachedPanel() { + if (this.open) { + this.queueDelete = true; + } else { + this.panel = null; + } + } + + @Override + public boolean isSubPanel() { + return subPanel; + } + + @Override + public boolean isPanelOpen() { + return this.open; + } + + @Override + public void openPanel() { + if (this.open) return; + if (this.screen != this.parent.getScreen()) { + this.screen = this.parent.getScreen(); + } + if (this.panel == null) { + this.panel = buildPanel(); + if (this.panel == this.screen.getMainPanel()) { + throw new IllegalArgumentException("Must not return main panel!"); + } + if (WidgetTree.hasSyncedValues(this.panel)) { + throw new IllegalArgumentException( + "Panel has widgets with synced values, but the panel is not synced!"); + } + this.panel.setPanelHandler(this); + } + this.screen.getPanelManager().openPanel(this.panel, this); + this.open = true; + } + + @OnlyIn(Dist.CLIENT) + private ModularPanel buildPanel() { + return Objects.requireNonNull(this.provider.build(this.screen.getMainPanel(), MCHelper.getPlayer())); + } + + public interface IPanelBuilder { + + ModularPanel build(ModularPanel parentPanel, Player player); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/UISettings.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/UISettings.java new file mode 100644 index 00000000000..eca38856b46 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/UISettings.java @@ -0,0 +1,97 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.api.mui.base.UIFactory; +import com.gregtechceu.gtceu.api.mui.base.XeiSettings; +import com.gregtechceu.gtceu.api.mui.factory.GuiData; +import com.gregtechceu.gtceu.api.mui.factory.PosGuiData; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; + +import lombok.Getter; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.IntFunction; +import java.util.function.Predicate; + +public class UISettings { + + public static final double DEFAULT_INTERACT_RANGE = 8.0; + + private IntFunction containerCreator; + private Predicate canInteractWith; + @Getter + private final XeiSettings xeiSettings; + + public UISettings() { + this(new XeiSettingsImpl()); + } + + public UISettings(XeiSettings xeiSettings) { + this.xeiSettings = xeiSettings; + } + + /** + * A function for a custom {@link ModularContainerMenu} implementation. This overrides + * {@link UIFactory#createContainer(int)}. + * + * @param containerCreator container creator function. Must return a new instance. + */ + public void customContainer(IntFunction containerCreator) { + this.containerCreator = containerCreator; + } + + /** + * Overrides the default can interact check of {@link UIFactory#canInteractWith(Player, GuiData)}. + * + * @param canInteractWith function to test if a player can interact with the ui. This is called every tick while UI + * is open. Once this + * function returns false, the UI is immediately closed. + */ + public void canInteractWith(Predicate canInteractWith) { + this.canInteractWith = canInteractWith; + } + + @ApiStatus.Internal + public void defaultCanInteractWith(UIFactory factory, D guiData) { + canInteractWith(player -> factory.canInteractWith(player, guiData)); + } + + public void canInteractWithinRange(Vec3 pos, double range) { + canInteractWith(player -> player.distanceToSqr(pos) <= range * range); + } + + public void canInteractWithinRange(BlockPos pos, double range) { + canInteractWithinRange(pos.getCenter(), range); + } + + public void canInteractWithinRange(PosGuiData guiData, double range) { + canInteractWithinRange(guiData.getBlockPos(), range); + } + + public void canInteractWithinDefaultRange(Vec3 pos) { + canInteractWithinRange(pos, DEFAULT_INTERACT_RANGE); + } + + public void canInteractWithinDefaultRange(BlockPos pos) { + canInteractWithinRange(pos, DEFAULT_INTERACT_RANGE); + } + + public void canInteractWithinDefaultRange(PosGuiData guiData) { + canInteractWithinRange(guiData, DEFAULT_INTERACT_RANGE); + } + + @ApiStatus.Internal + public ModularContainerMenu createContainer(int containerId) { + return containerCreator.apply(containerId); + } + + public boolean hasContainer() { + return containerCreator != null; + } + + public boolean canPlayerInteractWithUI(Player player) { + return canInteractWith == null || canInteractWith.test(player); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/XeiSettingsImpl.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/XeiSettingsImpl.java new file mode 100644 index 00000000000..d142957c079 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/XeiSettingsImpl.java @@ -0,0 +1,170 @@ +package com.gregtechceu.gtceu.client.mui.screen; + +import com.gregtechceu.gtceu.api.mui.base.XeiSettings; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.utils.Rectangle; +import com.gregtechceu.gtceu.integration.xei.XeiState; +import com.gregtechceu.gtceu.integration.xei.handlers.GhostIngredientSlot; + +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Keeps track of everything related to JEI in a Modular GUI. + * By default, JEI is disabled in client only GUIs. + * This class can be safely interacted with even when JEI/HEI is not installed. + */ +@OnlyIn(Dist.CLIENT) +public class XeiSettingsImpl implements XeiSettings { + + private XeiState xeiState = XeiState.DEFAULT; + private final List jeiExclusionWidgets = new ArrayList<>(); + private final List jeiExclusionAreas = new ArrayList<>(); + private final List> ghostIngredientSlots = new ArrayList<>(); + + /** + * Force JEI to be enabled + */ + @Override + public void forceEnabled() { + this.xeiState = XeiState.ENABLED; + } + + /** + * Force JEI to be disabled + */ + @Override + public void forceDisabled() { + this.xeiState = XeiState.DISABLED; + } + + /** + * Only enabled JEI in synced GUIs + */ + @Override + public void defaultXei() { + this.xeiState = XeiState.DEFAULT; + } + + /** + * Checks if JEI is enabled for a given screen + * + * @param screen modular screen + * @return true if jei is enabled + */ + @Override + public boolean isEnabled(ModularScreen screen) { + return this.xeiState.test(screen); + } + + /** + * Adds an exclusion zone. JEI will always try to avoid exclusion zones.
+ * If a widgets wishes to have an exclusion zone it should use {@link #addExclusionArea(IWidget)}! + * + * @param area exclusion area + */ + @Override + public void addExclusionArea(Rectangle area) { + if (!this.jeiExclusionAreas.contains(area)) { + this.jeiExclusionAreas.add(area); + } + } + + /** + * Removes an exclusion zone. + * + * @param area exclusion area to remove (must be the same instance) + */ + @Override + public void removeExclusionArea(Rectangle area) { + this.jeiExclusionAreas.remove(area); + } + + /** + * Adds an exclusion zone of a widget. JEI will always try to avoid exclusion zones.
+ * Useful when a widget is outside its panel. + * + * @param area widget + */ + @Override + public void addExclusionArea(IWidget area) { + if (!this.jeiExclusionWidgets.contains(area)) { + this.jeiExclusionWidgets.add(area); + } + } + + /** + * Removes a widget exclusion area. + * + * @param area widget + */ + @Override + public void removeExclusionArea(IWidget area) { + this.jeiExclusionWidgets.remove(area); + } + + /** + * Adds a JEI ghost slots. Ghost slots can display an ingredient, but the ingredient does not really exist. + * By calling this method users will be able to drag ingredients from JEI into the slot. + * + * @param slot slot widget + * @param slot widget type + */ + @Override + public > void addGhostIngredientSlot(W slot) { + if (!this.ghostIngredientSlots.contains(slot)) { + this.ghostIngredientSlots.add(slot); + } + } + + /** + * Removes a JEI ghost slot. + * + * @param slot slot widget + * @param slot widget type + */ + @Override + public > void removeGhostIngredientSlot(W slot) { + this.ghostIngredientSlots.remove(slot); + } + + @UnmodifiableView + public List getExclusionAreas() { + return Collections.unmodifiableList(this.jeiExclusionAreas); + } + + @UnmodifiableView + public List getExclusionWidgets() { + return Collections.unmodifiableList(this.jeiExclusionWidgets); + } + + @UnmodifiableView + public List> getGhostIngredientSlots() { + return Collections.unmodifiableList(this.ghostIngredientSlots); + } + + @ApiStatus.Internal + public List getAllExclusionAreas() { + this.jeiExclusionWidgets.removeIf(widget -> !widget.isValid()); + List areas = new ArrayList<>(this.jeiExclusionAreas); + for (Iterator iterator = this.jeiExclusionWidgets.iterator(); iterator.hasNext();) { + IWidget widget = iterator.next(); + if (!widget.isValid()) { + iterator.remove(); + continue; + } + if (widget.isEnabled()) { + areas.add(widget.getArea()); + } + } + return areas; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/GuiContext.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/GuiContext.java new file mode 100644 index 00000000000..f3a4ba1fa2e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/GuiContext.java @@ -0,0 +1,169 @@ +package com.gregtechceu.gtceu.client.mui.screen.viewport; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.MCHelper; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.ClientScreenHandler; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.ApiStatus; +import org.joml.Matrix4f; + +/** + * A gui context contains various properties like screen size, mouse position, last clicked button etc. + * It also is a matrix/pose stack. + * A default instance can be obtained using {@link #getDefault()}, which can be used in {@link IDrawable IDrawables} for + * example. + * That instance is automatically updated at all times (except when no UI is currently open). + */ +public class GuiContext extends GuiViewportStack { + + public static GuiContext getDefault() { + return ClientScreenHandler.getBestContext(); + } + + @Getter + private final Area screenArea = new Area(); + @Getter + @Setter(onMethod_ = @ApiStatus.Internal) + private GuiGraphics graphics = null; + + /* Mouse states */ + /** + * Absolute X coordinate of the mouse without the scrolling areas applied + */ + @Getter + private int absMouseX; + /** + * Absolute Y coordinate of the mouse without the scrolling areas applied + */ + @Getter + private int absMouseY; + @Getter + private int mouseButton; + @Getter + private double mouseScrollDelta; + + /* Keyboard states */ + @Getter + private int keyCode; + @Getter + private int scanCode; + @Getter + private int modifiers; + + /* Render states */ + @Getter + private float partialTicks; + @Getter + private long tick = 0; + @Getter + private int currentDrawingZ = 0; + + public boolean isAbove(IGuiElement widget) { + return isMouseAbove(widget.getArea()); + } + + /** + * @return true the mouse is anywhere above the widget + */ + public boolean isMouseAbove(IGuiElement widget) { + return isMouseAbove(widget.getArea()); + } + + /** + * @return true the mouse is anywhere above the area + */ + public boolean isMouseAbove(Area area) { + return area.isInside(this.absMouseX, this.absMouseY); + } + + @ApiStatus.Internal + public void updateState(int mouseX, int mouseY, float partialTicks) { + this.absMouseX = mouseX; + this.absMouseY = mouseY; + this.partialTicks = partialTicks; + } + + @ApiStatus.Internal + public void updateMouseButton(int button) { + this.mouseButton = button; + } + + @ApiStatus.Internal + public void updateMouseWheel(double scrollDelta) { + this.mouseScrollDelta = scrollDelta; + } + + @ApiStatus.Internal + public void updateLatestKey(int keyCode, int scanCode, int modifiers) { + this.keyCode = keyCode; + this.scanCode = scanCode; + this.modifiers = modifiers; + } + + @ApiStatus.Internal + public void updateScreenArea(int w, int h) { + this.screenArea.set(0, 0, w, h); + this.screenArea.rx = 0; + this.screenArea.ry = 0; + } + + public void updateZ(int z) { + this.currentDrawingZ = z; + } + + @OnlyIn(Dist.CLIENT) + public Minecraft getMC() { + return Minecraft.getInstance(); + } + + @OnlyIn(Dist.CLIENT) + public Font getFont() { + return MCHelper.getFont(); + } + + public void tick() { + this.tick += 1; + } + + /* Viewport */ + + public Matrix4f getLastPose() { + if (graphics == null) return new Matrix4f(); + return graphics.pose().last().pose(); + } + + public int getMouseX() { + return unTransformX(this.absMouseX, this.absMouseY); + } + + public int getMouseY() { + return unTransformY(this.absMouseX, this.absMouseY); + } + + public int getMouse(GuiAxis axis) { + return axis.isHorizontal() ? getMouseX() : getMouseY(); + } + + public int getAbsMouse(GuiAxis axis) { + return axis.isHorizontal() ? getMouseX() : getMouseY(); + } + + public boolean isMuiContext() { + return false; + } + + public ModularGuiContext getMuiContext() { + throw new UnsupportedOperationException("This is not a MuiContext"); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/GuiViewportStack.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/GuiViewportStack.java new file mode 100644 index 00000000000..2a947091bbd --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/GuiViewportStack.java @@ -0,0 +1,239 @@ +package com.gregtechceu.gtceu.client.mui.screen.viewport; + +import com.gregtechceu.gtceu.api.mui.base.layout.IViewport; +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; + +import com.mojang.blaze3d.vertex.PoseStack; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +/** + * This class is a matrix stack aka pose stack. It keeps track of widget transformations (including position) + * and can apply these transformations to OpenGL for rendering. + * This is mainly used, but not limited to, properly displacing widgets in a scroll area. + */ +public class GuiViewportStack implements IViewportStack { + + private static final Vector3f sharedVec = new Vector3f(); + + private final ObjectArrayList viewportStack = new ObjectArrayList<>(); + private final List viewportAreas = new ArrayList<>(); + private TransformationMatrix top; + private TransformationMatrix topViewport; + + @Override + public void reset() { + this.viewportStack.clear(); + this.top = null; + this.topViewport = null; + } + + @Override + public Area getViewport() { + return this.topViewport.getArea(); + } + + @Override + public void pushViewport(IViewport viewport, Area area) { + Matrix4f parent = this.top == null ? null : this.top.getMatrix(); + Area child = getCurrentViewportArea(); + child.set(area); + if (this.topViewport != null) { + if (!this.topViewport.isViewportMatrix()) { + throw new IllegalStateException(this.topViewport.toString()); + } + if (this.topViewport.getArea() == null) { + throw new NullPointerException(this.topViewport.toString()); + } + this.topViewport.getArea().clamp(child); + } + this.viewportStack.push(new TransformationMatrix(viewport, child, parent)); + updateViewport(false); + this.topViewport = this.viewportStack.top(); + } + + @Override + public void pushMatrix() { + this.viewportStack.push(new TransformationMatrix(this.top == null ? null : this.top.getMatrix())); + updateViewport(false); + } + + private Area getCurrentViewportArea() { + while (this.viewportAreas.size() < this.viewportStack.size() + 1) { + this.viewportAreas.add(new Area()); + } + return this.viewportAreas.get(this.viewportStack.size()); + } + + @Override + public void popViewport(IViewport viewport) { + if (this.top == null || !this.top.isViewportMatrix() || this.top.getViewport() != viewport) { + String name = this.top == null ? "null" : this.top.getViewport().toString(); + throw new IllegalStateException( + "Viewports must be popped in reverse order they were pushed. Tried to pop '" + viewport + + "', but last pushed is '" + name + "'."); + } + this.viewportStack.pop(); + updateViewport(true); + } + + @Override + public void popMatrix() { + if (this.top.isViewportMatrix()) { + throw new IllegalStateException("Tried to pop viewport matrix, but at the top is a normal matrix."); + } + this.viewportStack.pop(); + updateViewport(false); + } + + public void push(TransformationMatrix transformationMatrix) { + this.viewportStack.push(new TransformationMatrix(transformationMatrix, + this.top == null ? null : this.top.getMatrix())); + updateViewport(false); + if (this.top.isViewportMatrix()) { + this.topViewport = this.top; + } + } + + public void pop(TransformationMatrix transformationMatrix) { + if (this.top.getWrapped() != transformationMatrix) { + throw new IllegalArgumentException(); + } + TransformationMatrix tm = this.viewportStack.pop(); + updateViewport(tm.isViewportMatrix()); + } + + @Override + public int getStackSize() { + return this.viewportStack.size(); + } + + @Override + public void popUntilIndex(int index) { + for (int i = this.viewportStack.size() - 1; i > index; i--) { + this.viewportStack.pop(); + } + updateViewport(true); + } + + @Override + public void popUntilViewport(IViewport viewport) { + int i = this.viewportStack.size(); + while (--i >= 0 && this.viewportStack.top().getViewport() != viewport) { + this.viewportStack.pop(); + } + updateViewport(true); + } + + public void translate(float x, float y) { + checkViewport(); + this.top.getMatrix().translate(vec(x, y, 0)); + this.top.markDirty(); + } + + public void translate(float x, float y, float z) { + checkViewport(); + this.top.getMatrix().translate(vec(x, y, z)); + this.top.markDirty(); + } + + public void rotate(float angle, float x, float y, float z) { + checkViewport(); + this.top.getMatrix().rotate(angle, vec(x, y, z)); + this.top.markDirty(); + } + + public void rotateZ(float angle) { + rotate(angle, 0f, 0f, 1f); + } + + public void scale(float x, float y) { + checkViewport(); + this.top.getMatrix().scale(vec(x, y, 1f)); + this.top.markDirty(); + } + + public void resetCurrent() { + checkViewport(); + Matrix4f belowTop = this.viewportStack.size() > 1 ? + this.viewportStack.get(this.viewportStack.size() - 2).getMatrix() : new Matrix4f(); + this.top.getMatrix().set(belowTop); + this.top.markDirty(); + } + + private void checkViewport() { + if (this.top == null) { + throw new IllegalStateException("Tried to transform viewport, but there is no viewport!"); + } + } + + private void updateViewport(boolean findTopViewport) { + this.top = this.viewportStack.isEmpty() ? null : this.viewportStack.top(); + if (!findTopViewport || this.topViewport == null || !this.topViewport.isViewportMatrix()) return; + // find new top viewport + this.topViewport = null; + if (this.viewportStack.isEmpty()) return; + ListIterator it = this.viewportStack.listIterator(this.viewportStack.size() - 1); + while (it.hasPrevious()) { + TransformationMatrix transformationMatrix1 = it.previous(); + if (transformationMatrix1.isViewportMatrix()) { + this.topViewport = transformationMatrix1; + break; + } + } + } + + @Override + public int transformX(float x, float y) { + return this.top == null ? (int) x : this.top.transformX(x, y); + } + + @Override + public int transformY(float x, float y) { + return this.top == null ? (int) y : this.top.transformY(x, y); + } + + @Override + public int unTransformX(float x, float y) { + return this.top == null ? (int) x : this.top.unTransformX(x, y); + } + + @Override + public int unTransformY(float x, float y) { + return this.top == null ? (int) y : this.top.unTransformY(x, y); + } + + @Override + public Vector3f transform(Vector3f vec, Vector3f dest) { + return this.top == null ? dest.set(vec) : this.top.transform(vec, dest); + } + + @Override + public Vector3f unTransform(Vector3f vec, Vector3f dest) { + return this.top == null ? dest.set(vec) : this.top.unTransform(vec, dest); + } + + @Override + public void applyTo(PoseStack poseStack) { + if (this.top == null) return; + poseStack.mulPoseMatrix(this.top.getMatrix()); + } + + @Nullable + @Override + public TransformationMatrix peek() { + return this.top; + } + + private static Vector3f vec(float x, float y, float z) { + sharedVec.set(x, y, z); + return sharedVec; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/LocatedElement.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/LocatedElement.java new file mode 100644 index 00000000000..f280ecf9d21 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/LocatedElement.java @@ -0,0 +1,24 @@ +package com.gregtechceu.gtceu.client.mui.screen.viewport; + +import lombok.Getter; + +public class LocatedElement { + + @Getter + private final T element; + @Getter + private final TransformationMatrix transformationMatrix; + + public LocatedElement(T element, TransformationMatrix transformationMatrix) { + this.element = element; + this.transformationMatrix = new TransformationMatrix(transformationMatrix, null); + } + + public void applyMatrix(GuiContext context) { + context.push(this.transformationMatrix); + } + + public void unapplyMatrix(GuiContext context) { + context.pop(this.transformationMatrix); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/LocatedWidget.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/LocatedWidget.java new file mode 100644 index 00000000000..b6d94556b20 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/LocatedWidget.java @@ -0,0 +1,46 @@ +package com.gregtechceu.gtceu.client.mui.screen.viewport; + +import com.gregtechceu.gtceu.api.mui.base.layout.IViewport; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; + +import java.util.ArrayList; +import java.util.List; + +public class LocatedWidget extends LocatedElement { + + public static LocatedWidget of(IWidget widget) { + if (widget == null) { + return EMPTY; + } + // first make a list of all parents + IWidget parent = widget; + List ancestors = new ArrayList<>(); + while (true) { + ancestors.add(0, parent); + if (parent instanceof ModularPanel) { + break; + } + parent = parent.getParent(); + } + // iterate through each parent starting at the root and apply each transformation + GuiViewportStack stack = new GuiViewportStack(); + for (IWidget widget1 : ancestors) { + if (widget1 instanceof IViewport viewport) { + stack.pushViewport(viewport, widget1.getArea()); + widget1.transform(stack); + viewport.transformChildren(stack); + } else { + stack.pushMatrix(); + widget1.transform(stack); + } + } + return new LocatedWidget(widget, stack.peek()); + } + + public static final LocatedWidget EMPTY = new LocatedWidget(null, TransformationMatrix.EMPTY); + + public LocatedWidget(IWidget element, TransformationMatrix transformationMatrix) { + super(element, transformationMatrix); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/ModularGuiContext.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/ModularGuiContext.java new file mode 100644 index 00000000000..0462a2192b6 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/ModularGuiContext.java @@ -0,0 +1,409 @@ +package com.gregtechceu.gtceu.client.mui.screen.viewport; + +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.MCHelper; +import com.gregtechceu.gtceu.api.mui.base.widget.*; +import com.gregtechceu.gtceu.client.mui.screen.*; + +import net.minecraft.Util; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.player.LocalPlayer; + +import lombok.Getter; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; + +/** + * This class contains all the info from {@link GuiContext} and additional MUI specific info like the current + * {@link ModularScreen}, + * current hovered widget, current dragged widget, current focused widget and XEI settings. + * An instance can only be obtained from {@link ModularScreen#getContext()}. One instance is created every time a + * {@link ModularScreen} + * is created. + */ +public class ModularGuiContext extends GuiContext { + + /* GUI elements */ + @Getter + public final ModularScreen screen; + @Getter + private LocatedWidget focusedWidget = LocatedWidget.EMPTY; + /** + * the hovered widget (widget directly below the mouse) + */ + @Getter + private @Nullable IWidget hovered; + private int timeHovered = 0; + private final HoveredIterable hoveredWidgets; + + private LocatedElement draggable; + private int lastButton = -1; + private long lastClickTime = 0; + private int lastDragX, lastDragY; + + public List> postRenderCallbacks = new ArrayList<>(); + + private UISettings settings; + + public ModularGuiContext(ModularScreen screen) { + this.screen = screen; + this.hoveredWidgets = new HoveredIterable(this.screen.getPanelManager()); + } + + /** + * @return true if any widget is being hovered + */ + public boolean isHovered() { + return this.hovered != null; + } + + /** + * @return true if the widget is directly below the mouse + */ + public boolean isHovered(IGuiElement guiElement) { + return isHovered() && this.hovered == guiElement; + } + + /** + * Checks if a widget is hovered for a certain amount of ticks + * + * @param guiElement widget + * @param ticks time hovered + * @return true if the widget is hovered for at least a certain number of ticks + */ + public boolean isHoveredFor(IGuiElement guiElement, int ticks) { + // convert from frames per second to ticks per second + return isHovered(guiElement) && this.timeHovered / 3 >= ticks; + } + + /** + * @return all widgets which are below the mouse ({@link GuiContext#isAbove(IGuiElement)} is true) + */ + public Iterable getAllBelowMouse() { + return this.hoveredWidgets; + } + + /** + * @return true if there is any focused widget + */ + public boolean isFocused() { + return this.focusedWidget.getElement() != null; + } + + /* Element focusing */ + + /** + * @return true if there is any focused widget + */ + public boolean isFocused(IFocusedWidget widget) { + return this.focusedWidget.getElement() == widget; + } + + /** + * Tries to focus the given widget + * + * @param widget widget to focus + */ + public void focus(IFocusedWidget widget) { + focus(LocatedWidget.of((IWidget) widget)); + } + + /** + * Tries to focus the given widget + * + * @param widget widget to focus + */ + public void focus(@NotNull LocatedWidget widget) { + if (this.focusedWidget.getElement() == widget.getElement()) { + return; + } + + if (widget.getElement() != null && !(widget.getElement() instanceof IFocusedWidget)) { + throw new IllegalArgumentException(); + } + + if (this.focusedWidget.getElement() != null) { + IFocusedWidget focusedWidget = (IFocusedWidget) this.focusedWidget.getElement(); + focusedWidget.onRemoveFocus(this); + this.screen.setFocused(false); + } + + this.focusedWidget = widget; + + if (this.focusedWidget.getElement() != null) { + IFocusedWidget focusedWidget = (IFocusedWidget) this.focusedWidget.getElement(); + focusedWidget.onFocus(this); + this.screen.setFocused(true); + } + } + + /** + * Removes focus from any widget + */ + public void removeFocus() { + focus(LocatedWidget.EMPTY); + } + + /** + * Tries to find the next focusable widget. + * + * @param parent focusable context + * @return true if successful + */ + public boolean focusNext(IWidget parent) { + return focus(parent, -1, 1); + } + + /** + * Tries to find the previous focusable widget. + * + * @param parent focusable context + * @return true if successful + */ + public boolean focusPrevious(IWidget parent) { + return focus(parent, -1, -1); + } + + public boolean focus(IWidget parent, int index, int factor) { + return focus(parent, index, factor, false); + } + + /** + * Focus next focusable GUI element + */ + public boolean focus(IWidget widget, int index, int factor, boolean stop) { + List children = widget.getChildren(); + + factor = factor >= 0 ? 1 : -1; + index += factor; + + for (; index >= 0 && index < children.size(); index += factor) { + IWidget child = children.get(index); + + if (!child.isEnabled()) { + continue; + } + + if (child instanceof IFocusedWidget focusedWidget1) { + focus(focusedWidget1); + + return true; + } else { + int start = factor > 0 ? -1 : child.getChildren().size(); + + if (focus(child, start, factor, true)) { + return true; + } + } + } + + IWidget grandparent = widget.getParent(); + boolean isRoot = grandparent instanceof ModularPanel; + + if (!stop && (isRoot || grandparent.canBeSeen(this))) { + List siblings = grandparent.getChildren(); + if (focus(grandparent, siblings.indexOf(widget), factor)) { + return true; + } + if (isRoot) { + return focus(grandparent, factor > 0 ? -1 : siblings.size() - 1, factor); + } + } + + return false; + } + + /* draggable */ + + public boolean hasDraggable() { + return this.draggable != null; + } + + public boolean isMouseItemEmpty() { + LocalPlayer player = MCHelper.getPlayer(); + return player == null || player.containerMenu.getCarried().isEmpty(); + } + + @ApiStatus.Internal + public boolean onMousePressed(double mouseX, double mouseY, int button) { + if ((button == 0 || button == 1) && isMouseItemEmpty() && hasDraggable()) { + dropDraggable(); + return true; + } + return false; + } + + @ApiStatus.Internal + public boolean onMouseReleased(double mouseX, double mouseY, int button) { + if (button == this.lastButton && isMouseItemEmpty() && hasDraggable()) { + long time = Util.getMillis(); + if (time - this.lastClickTime < 200) return false; + dropDraggable(); + return true; + } + return false; + } + + @ApiStatus.Internal + public void dropDraggable() { + this.draggable.applyMatrix(this); + this.draggable.getElement() + .onDragEnd(this.draggable.getElement().canDropHere(getMouseX(), getMouseY(), this.hovered)); + this.draggable.getElement().setMoving(false); + this.draggable.unapplyMatrix(this); + this.draggable = null; + this.lastButton = -1; + this.lastClickTime = 0; + } + + @ApiStatus.Internal + public boolean onHoveredClick(int button, LocatedWidget hovered) { + if ((button == 0 || button == 1) && isMouseItemEmpty() && !hasDraggable()) { + IWidget widget = hovered.getElement(); + LocatedElement draggable; + if (widget instanceof IDraggable iDraggable) { + draggable = new LocatedElement<>(iDraggable, hovered.getTransformationMatrix()); + } else if (widget instanceof ModularPanel panel) { + if (panel.isDraggable()) { + if (!panel.flex().hasFixedSize()) { + throw new IllegalStateException( + "Panel must have a fixed size. It can't specify left AND right or top AND bottom!"); + } + draggable = new LocatedElement<>(new DraggablePanelWrapper(panel), TransformationMatrix.EMPTY); + } else { + return false; + } + } else { + return false; + } + if (draggable.getElement().onDragStart(button)) { + draggable.getElement().setMoving(true); + + this.draggable = draggable; + this.lastButton = button; + this.lastClickTime = Util.getMillis(); + return true; + } + } + return false; + } + + @ApiStatus.Internal + public void drawDraggable(GuiGraphics graphics) { + if (hasDraggable()) { + this.draggable.applyMatrix(this); + this.draggable.getElement().drawMovingState(graphics, this, getPartialTicks()); + this.draggable.unapplyMatrix(this); + } + } + + @ApiStatus.Internal + public void onFrameUpdate() { + IWidget hovered = this.screen.getPanelManager().getTopWidget(); + if (hasDraggable() && (this.lastDragX != getMouseX() || this.lastDragY != getMouseY())) { + this.lastDragX = getMouseX(); + this.lastDragY = getMouseY(); + this.draggable.applyMatrix(this); + this.draggable.getElement().onDrag(this.lastButton, this.lastClickTime); + this.draggable.unapplyMatrix(this); + } + if (this.hovered != hovered) { + if (this.hovered != null) { + this.hovered.onMouseEndHover(); + } + this.hovered = hovered; + this.timeHovered = 0; + if (this.hovered != null) { + this.hovered.onMouseStartHover(); + if (this.hovered instanceof IVanillaSlot vanillaSlot && vanillaSlot.handleAsVanillaSlot()) { + this.screen.getScreenWrapper().setHoveredSlot(vanillaSlot.getVanillaSlot()); + } else { + this.screen.getScreenWrapper().setHoveredSlot(null); + } + } + } else { + this.timeHovered++; + } + } + + public ITheme getTheme() { + return this.screen.getCurrentTheme(); + } + + @Override + public boolean isMuiContext() { + return true; + } + + @Override + public ModularGuiContext getMuiContext() { + return this; + } + + public UISettings getUISettings() { + if (this.settings == null) { + throw new IllegalStateException("The screen is not yet initialised!"); + } + return this.settings; + } + + public XeiSettingsImpl getXeiSettings() { + if (this.screen.isOverlay()) { + throw new IllegalStateException("Overlays don't have JEI settings!"); + } + return (XeiSettingsImpl) getUISettings().getXeiSettings(); + } + + @ApiStatus.Internal + public void setSettings(UISettings settings) { + if (this.settings != null) { + throw new IllegalStateException("Tried to set settings twice"); + } + this.settings = settings; + } + + private static class HoveredIterable implements Iterable { + + private final PanelManager panelManager; + + private HoveredIterable(PanelManager panelManager) { + this.panelManager = panelManager; + } + + @NotNull + @Override + public Iterator iterator() { + return new Iterator<>() { + + private final Iterator panelIt = HoveredIterable.this.panelManager.getOpenPanels() + .iterator(); + private Iterator widgetIt; + + @Override + public boolean hasNext() { + if (this.widgetIt == null) { + if (!this.panelIt.hasNext()) { + return false; + } + this.widgetIt = this.panelIt.next().getHovering().iterator(); + } + return this.widgetIt.hasNext(); + } + + @Override + public IGuiElement next() { + if (this.widgetIt == null || !this.widgetIt.hasNext()) { + this.widgetIt = this.panelIt.next().getHovering().iterator(); + } + return this.widgetIt.next().getElement(); + } + }; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/TransformationMatrix.java b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/TransformationMatrix.java new file mode 100644 index 00000000000..3d60e74b3f6 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/client/mui/screen/viewport/TransformationMatrix.java @@ -0,0 +1,113 @@ +package com.gregtechceu.gtceu.client.mui.screen.viewport; + +import com.gregtechceu.gtceu.api.mui.base.layout.IViewport; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; + +import lombok.Getter; +import org.jetbrains.annotations.Nullable; +import org.joml.Matrix4f; +import org.joml.Vector3f; + +/** + * A single matrix in a matrix stack. Also has some other information. + */ +public class TransformationMatrix { + + public static final TransformationMatrix EMPTY = new TransformationMatrix(null); + + @Getter + private final TransformationMatrix wrapped; + @Getter + private final IViewport viewport; + @Getter + private final Area area; + @Getter + private final Matrix4f matrix; + private final Matrix4f invertedMatrix = new Matrix4f(); + + @Getter + private final boolean viewportMatrix; + @Getter + private boolean dirty = true; + + public TransformationMatrix(TransformationMatrix parent, @Nullable Matrix4f parentMatrix) { + this.wrapped = parent; + this.viewport = parent.viewport; + this.area = parent.area; + this.matrix = parentMatrix == null ? new Matrix4f(parent.getMatrix()) : + parentMatrix.mul(parent.getMatrix(), new Matrix4f()); + this.viewportMatrix = parent.viewportMatrix; + } + + public TransformationMatrix(@Nullable Matrix4f parent) { + this.wrapped = null; + this.viewport = null; + this.area = null; + this.matrix = new Matrix4f(); + this.viewportMatrix = false; + if (parent != null) { + this.matrix.set(parent); + } + } + + public TransformationMatrix(IViewport viewport, Area area, @Nullable Matrix4f parent) { + this.wrapped = null; + this.viewport = viewport; + this.area = area; + this.matrix = new Matrix4f(); + this.viewportMatrix = true; + if (parent != null) { + this.matrix.set(parent); + } + } + + public Matrix4f getInvertedMatrix() { + if (this.dirty) { + if (this.matrix.invert(this.invertedMatrix) == null) { + this.invertedMatrix.set(this.matrix); + } + this.dirty = false; + } + return this.invertedMatrix; + } + + public void markDirty() { + this.dirty = true; + } + + public int transformX(float x, float y) { + Matrix4f m = getMatrix(); + return (int) (x * m.m00() + y * m.m10() + m.m30()); + } + + public int transformY(float x, float y) { + Matrix4f m = getMatrix(); + return (int) (x * m.m01() + y * m.m11() + m.m31()); + } + + public int unTransformX(float x, float y) { + Matrix4f m = getInvertedMatrix(); + return (int) (x * m.m00() + y * m.m10() + m.m30()); + } + + public int unTransformY(float x, float y) { + Matrix4f m = getInvertedMatrix(); + return (int) (x * m.m01() + y * m.m11() + m.m31()); + } + + public Vector3f transform(Vector3f vec, Vector3f dest) { + return transform(getMatrix(), vec, dest); + } + + public Vector3f unTransform(Vector3f vec, Vector3f dest) { + return transform(getInvertedMatrix(), vec, dest); + } + + public static Vector3f transform(Matrix4f m, Vector3f vec, Vector3f dest) { + float x = m.m00() * vec.x + m.m10() * vec.y + m.m20() * vec.z + m.m30(); + float y = m.m01() * vec.x + m.m11() * vec.y + m.m21() * vec.z + m.m31(); + float z = m.m02() * vec.x + m.m12() * vec.y + m.m22() * vec.z + m.m32(); + dest.set(x, y, z); + return dest; + } +} From d53ae11724f1835883d0abed5b31e060bc2fe18d Mon Sep 17 00:00:00 2001 From: YoungOnion <39562198+YoungOnionMC@users.noreply.github.com> Date: Tue, 12 Aug 2025 23:19:10 -0600 Subject: [PATCH 012/286] Api/widget (#3667) --- .../api/mui/widget/AbstractParentWidget.java | 117 +++ .../api/mui/widget/AbstractScrollWidget.java | 157 ++++ .../gtceu/api/mui/widget/DragHandle.java | 108 +++ .../gtceu/api/mui/widget/DraggableWidget.java | 96 +++ .../gtceu/api/mui/widget/EmptyWidget.java | 97 +++ .../gtceu/api/mui/widget/ParentWidget.java | 27 + .../gtceu/api/mui/widget/ScrollWidget.java | 27 + .../api/mui/widget/SingleChildWidget.java | 35 + .../gtceu/api/mui/widget/Widget.java | 777 ++++++++++++++++++ .../gtceu/api/mui/widget/WidgetTree.java | 396 +++++++++ .../widget/scroll/HorizontalScrollData.java | 82 ++ .../api/mui/widget/scroll/ScrollArea.java | 204 +++++ .../api/mui/widget/scroll/ScrollData.java | 258 ++++++ .../mui/widget/scroll/VerticalScrollData.java | 81 ++ .../gtceu/api/mui/widget/sizer/Area.java | 506 ++++++++++++ .../gtceu/api/mui/widget/sizer/Box.java | 89 ++ .../api/mui/widget/sizer/DimensionSizer.java | 342 ++++++++ .../gtceu/api/mui/widget/sizer/Flex.java | 515 ++++++++++++ .../api/mui/widget/sizer/IUnResizeable.java | 71 ++ .../gtceu/api/mui/widget/sizer/Unit.java | 96 +++ .../api/mui/widget/wrapper/WidgetWrapper.java | 125 +++ 21 files changed, 4206 insertions(+) create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/AbstractParentWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/AbstractScrollWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/DragHandle.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/DraggableWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/EmptyWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/ParentWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/ScrollWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/SingleChildWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/Widget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/WidgetTree.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/HorizontalScrollData.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/ScrollArea.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/ScrollData.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/VerticalScrollData.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Area.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Box.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/DimensionSizer.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Flex.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/IUnResizeable.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Unit.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widget/wrapper/WidgetWrapper.java diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/AbstractParentWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/AbstractParentWidget.java new file mode 100644 index 00000000000..6ce1b67283d --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/AbstractParentWidget.java @@ -0,0 +1,117 @@ +package com.gregtechceu.gtceu.api.mui.widget; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.widgets.VoidWidget; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.ArrayList; +import java.util.List; + +/** + * A widget which can hold any amount of children. + * + * @param type of children (in most cases just {@link IWidget}). Use {@link VoidWidget} if no children should be + * added. + * @param type of this widget + */ +public class AbstractParentWidget> extends Widget { + + private final List children = new ArrayList<>(); + + /** + * A list of all children of this widget. The list is modifiable contrary to the annotation. + * This just means that you shouldn't carelessly modify the list. Adding to the list also requires initialising the new child. + * Removing requires disposing the old child. + * + * @return a view of all children. + */ + @SuppressWarnings("unchecked") + @UnmodifiableView + @NotNull + @Override + public List getChildren() { + return (List) this.children; + } + + /** + * A list of all children of this widget with the given children type {@link I}. The list is modifiable contrary to the annotation. + * This just means that you shouldn't carelessly modify the list. Adding to the list also requires initialising the new child. + * Removing requires disposing the old child. + * + * @return a view of all children. + */ + @UnmodifiableView + public List getTypeChildren() { + return children; + } + + @Override + public boolean canHover() { + if (IDrawable.isVisible(getBackground()) || + IDrawable.isVisible(getHoverBackground()) || + IDrawable.isVisible(getHoverOverlay()) || + getTooltip() != null) + return true; + WidgetTheme widgetTheme = getWidgetTheme(getContext().getTheme()); + if (getBackground() == null && IDrawable.isVisible(widgetTheme.getBackground())) return true; + return getHoverBackground() == null && IDrawable.isVisible(widgetTheme.getHoverBackground()); + } + + @Override + public boolean canClickThrough() { + return !canHover(); + } + + protected boolean addChild(I child, int index) { + if (child == null || child == this || getChildren().contains(child)) { + return false; + } + if (child instanceof ModularPanel) { + throw new IllegalArgumentException( + "ModularPanel should not be added as child widget; Use ModularScreen#openPanel instead"); + } + if (!isChildValid(child)) { + throw new IllegalArgumentException("Child '" + child + "' is not valid for parent '" + this + "'!"); + } + if (index < 0) { + index += getChildren().size() + 1; + } + this.children.add(index, child); + if (isValid()) { + child.initialise(this); + } + onChildAdd(child); + return true; + } + + protected boolean remove(I child) { + if (this.children.remove(child)) { + child.dispose(); + onChildRemove(child); + return true; + } + return false; + } + + protected boolean remove(int index) { + if (index < 0) { + index = getChildren().size() + index + 1; + } + I child = this.children.remove(index); + child.dispose(); + onChildRemove(child); + return true; + } + + protected boolean isChildValid(I child) { + return true; + } + + protected void onChildAdd(I child) {} + + protected void onChildRemove(I child) {} +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/AbstractScrollWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/AbstractScrollWidget.java new file mode 100644 index 00000000000..b2382c57b74 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/AbstractScrollWidget.java @@ -0,0 +1,157 @@ +package com.gregtechceu.gtceu.api.mui.widget; + +import com.gregtechceu.gtceu.api.mui.base.layout.IViewport; +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiAction; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.Interactable; +import com.gregtechceu.gtceu.api.mui.drawable.Stencil; +import com.gregtechceu.gtceu.api.mui.utils.HoveredWidgetList; +import com.gregtechceu.gtceu.api.mui.widget.scroll.HorizontalScrollData; +import com.gregtechceu.gtceu.api.mui.widget.scroll.ScrollArea; +import com.gregtechceu.gtceu.api.mui.widget.scroll.VerticalScrollData; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A scrollable parent widget. Children can be added + * + * @param type of children (in most cases just {@link IWidget}) + * @param type of this widget + */ +public abstract class AbstractScrollWidget> + extends AbstractParentWidget implements IViewport, Interactable { + + private final ScrollArea scroll = new ScrollArea(); + private boolean keepScrollBarInArea = false; + + public AbstractScrollWidget(@Nullable HorizontalScrollData x, @Nullable VerticalScrollData y) { + super(); + this.scroll.setScrollX(x); + this.scroll.setScrollY(y); + listenGuiAction((IGuiAction.MouseReleased) (mouseX, mouseY, button) -> { + this.scroll.mouseReleased(getContext()); + return false; + }); + } + + @Override + public Area getArea() { + return this.scroll; + } + + public ScrollArea getScrollArea() { + return this.scroll; + } + + @Override + public void transformChildren(IViewportStack stack) { + stack.translate(-getScrollX(), -getScrollY()); + } + + @Override + public void getSelfAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y) { + if (isInside(stack, x, y)) { + widgets.add(this, stack.peek()); + } + } + + @Override + public void getWidgetsAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y) { + if (getArea().isInside(x, y) && !getScrollArea().isInsideScrollbarArea(x, y) && hasChildren()) { + IViewport.getChildrenAt(this, stack, widgets, x, y); + } + } + + @Override + public void onResized() { + if (this.scroll.getScrollX() != null) { + this.scroll.getScrollX().clamp(this.scroll); + if (!this.keepScrollBarInArea) { + getArea().height += this.scroll.getScrollX().getThickness(); + } + } + if (this.scroll.getScrollY() != null) { + this.scroll.getScrollY().clamp(this.scroll); + if (!this.keepScrollBarInArea) { + getArea().width += this.scroll.getScrollY().getThickness(); + } + } + } + + @Override + public boolean canHover() { + return super.canHover() || + this.scroll.isInsideScrollbarArea(getContext().getMouseX(), getContext().getMouseY()); + } + + @Override + public @NotNull Result onMousePressed(double mouseX, double mouseY, int button) { + ModularGuiContext context = getContext(); + if (this.scroll.mouseClicked(context)) { + return Result.STOP; + } + return Result.IGNORE; + } + + @Override + public boolean onMouseScrolled(double mouseX, double mouseY, double delta) { + return this.scroll.mouseScroll(getContext()); + } + + @Override + public boolean onMouseReleased(double mouseX, double mouseY, int button) { + this.scroll.mouseReleased(getContext()); + return false; + } + + @Override + public void onUpdate() { + super.onUpdate(); + this.scroll.drag(getContext().getMouseX(), getContext().getMouseY()); + } + + @Override + public void preDraw(ModularGuiContext context, boolean transformed) { + if (!transformed) { + Stencil.applyAtZero(this.scroll, context); + } + } + + @Override + public void postDraw(ModularGuiContext context, boolean transformed) { + if (!transformed) { + Stencil.remove(); + this.scroll.drawScrollbar(context); + } + } + + public int getScrollX() { + return this.scroll.getScrollX() != null ? this.scroll.getScrollX().getScroll() : 0; + } + + public int getScrollY() { + return this.scroll.getScrollY() != null ? this.scroll.getScrollY().getScroll() : 0; + } + + /** + * Sets whether the scroll bar should be kept inside the area of this widget, which might cause it to overlap with + * the content of this widget. + * By setting the value to false, the size of this widget is expanded by the thickness of the scrollbars after the + * tree is resized. + * Default: false + * + * @param value if the scroll bar should be kept inside the widgets area + * @return this + */ + public W keepScrollBarInArea(boolean value) { + this.keepScrollBarInArea = value; + return getThis(); + } + + public W keepScrollBarInArea() { + return keepScrollBarInArea(true); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/DragHandle.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/DragHandle.java new file mode 100644 index 00000000000..c9cfc1c0f99 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/DragHandle.java @@ -0,0 +1,108 @@ +package com.gregtechceu.gtceu.api.mui.widget; + +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.base.widget.IDraggable; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.utils.HoveredWidgetList; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.DraggablePanelWrapper; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import net.minecraft.client.gui.GuiGraphics; +import org.jetbrains.annotations.Nullable; + +public class DragHandle extends Widget implements IDraggable { + + private IDraggable parentDraggable; + + @Override + public void onInit() { + IWidget parent = getParent(); + while (!(parent instanceof ModularPanel)) { + if (parent instanceof IDraggable draggable) { + this.parentDraggable = draggable; + return; + } + parent = parent.getParent(); + } + if (((ModularPanel) parent).isDraggable()) { + this.parentDraggable = new DraggablePanelWrapper((ModularPanel) parent); + } + } + + @Override + public void drawMovingState(GuiGraphics graphics, ModularGuiContext context, float partialTicks) { + if (this.parentDraggable != null) { + this.parentDraggable.drawMovingState(graphics, context, partialTicks); + } + } + + @Override + public boolean onDragStart(int button) { + return this.parentDraggable != null && this.parentDraggable.onDragStart(button); + } + + @Override + public void onDragEnd(boolean successful) { + if (this.parentDraggable != null) { + this.parentDraggable.onDragEnd(successful); + } + } + + @Override + public void onDrag(int mouseButton, double timeSinceLastClick) { + if (this.parentDraggable != null) { + this.parentDraggable.onDrag(mouseButton, timeSinceLastClick); + } + } + + @Override + public boolean canDropHere(int x, int y, @Nullable IGuiElement widget) { + return this.parentDraggable != null && this.parentDraggable.canDropHere(x, y, widget); + } + + @Override + public @Nullable Area getMovingArea() { + Area.SHARED.reset(); + return this.parentDraggable != null ? this.parentDraggable.getMovingArea() : Area.SHARED; + } + + @Override + public boolean isMoving() { + return this.parentDraggable != null && this.parentDraggable.isMoving(); + } + + @Override + public void setMoving(boolean moving) { + if (this.parentDraggable != null) { + this.parentDraggable.setMoving(moving); + } + } + + @Override + public void transform(IViewportStack stack) { + super.transform(stack); + } + + @Override + public void transformChildren(IViewportStack stack) { + if (this.parentDraggable != null) { + this.parentDraggable.transformChildren(stack); + } + } + + @Override + public void getWidgetsAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y) { + if (this.parentDraggable != null) { + this.parentDraggable.getWidgetsAt(stack, widgets, x, y); + } + } + + @Override + public void getSelfAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y) { + if (this.parentDraggable != null) { + this.parentDraggable.getSelfAt(stack, widgets, x, y); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/DraggableWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/DraggableWidget.java new file mode 100644 index 00000000000..b51fed096d7 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/DraggableWidget.java @@ -0,0 +1,96 @@ +package com.gregtechceu.gtceu.api.mui.widget; + +import com.gregtechceu.gtceu.api.mui.base.layout.IViewport; +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.base.widget.IDraggable; +import com.gregtechceu.gtceu.api.mui.utils.HoveredWidgetList; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import lombok.Getter; +import net.minecraft.client.gui.GuiGraphics; + +/** + * A widget that can be picked up by the cursor. + * Might not work as expected when a parent is scaling or rotating itself. + */ +public class DraggableWidget> extends Widget implements IDraggable { + + @Getter + private boolean moving = false; + private int relativeClickX, relativeClickY; + @Getter + private final Area movingArea; + private int realX, realY; + + public DraggableWidget() { + this.movingArea = getArea().createCopy(); + } + + @Override + public void drawMovingState(GuiGraphics graphics, ModularGuiContext context, float partialTicks) { + WidgetTree.drawTree(this, context, true); + } + + @Override + public boolean onDragStart(int mouseButton) { + if (mouseButton == 0) { + this.realX = getContext().transformX(0, 0) - getParentArea().x; + this.realY = getContext().transformY(0, 0) - getParentArea().y; + this.movingArea.x = this.realX; + this.movingArea.y = this.realY; + this.relativeClickX = getContext().getMouseX() - this.realX; + this.relativeClickY = getContext().getMouseY() - this.realY; + return true; + } + return false; + } + + @Override + public void onDragEnd(boolean successful) { + if (successful) { + flex().top(getContext().getMouseY() - this.relativeClickY) + .left(getContext().getMouseX() - this.relativeClickX); + this.movingArea.x = getArea().x; + this.movingArea.y = getArea().y; + WidgetTree.resize(this); + } + } + + @Override + public void onDrag(int mouseButton, double timeSinceLastClick) { + this.movingArea.x = getContext().getMouseX() - this.relativeClickX; + this.movingArea.y = getContext().getMouseY() - this.relativeClickY; + } + + @Override + public void setMoving(boolean moving) { + this.moving = moving; + setEnabled(!moving); + } + + @Override + public void getSelfAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y) { + if (!isMoving() && isInside(stack, x, y)) { + widgets.add(this, stack.peek()); + } + } + + @Override + public void getWidgetsAt(IViewportStack stack, HoveredWidgetList widgets, int x, int y) { + if (!isMoving() && hasChildren()) { + IViewport.getChildrenAt(this, stack, widgets, x, y); + } + } + + @Override + public void transform(IViewportStack stack) { + super.transform(stack); + if (isMoving()) { + // remove relative transformation + stack.translate(-getArea().rx, -getArea().ry); + // translate to current pos + stack.translate(-this.realX, -this.realY); + stack.translate(this.movingArea.x, this.movingArea.y); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/EmptyWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/EmptyWidget.java new file mode 100644 index 00000000000..fed937cd9a0 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/EmptyWidget.java @@ -0,0 +1,97 @@ +package com.gregtechceu.gtceu.api.mui.widget; + +import com.gregtechceu.gtceu.api.mui.base.layout.IResizeable; +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Flex; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +public class EmptyWidget implements IWidget { + + @Getter + private final Area area = new Area(); + @Getter + private final Flex flex = new Flex(this); + @Getter + private IWidget parent; + + @Override + public ModularScreen getScreen() { + return null; + } + + @Override + public void initialise(@NotNull IWidget parent) { + this.parent = parent; + } + + @Override + public void dispose() { + this.parent = null; + } + + @Override + public boolean isValid() { + return this.parent != null; + } + + @Override + public void drawBackground(ModularGuiContext context, WidgetTheme widgetTheme) {} + + @Override + public void draw(ModularGuiContext context, WidgetTheme widgetTheme) {} + + @Override + public void drawOverlay(ModularGuiContext context, WidgetTheme widgetTheme) {} + + @Override + public void drawForeground(ModularGuiContext context) {} + + @Override + public void onUpdate() {} + + @Override + public @NotNull ModularPanel getPanel() { + return this.parent.getPanel(); + } + + @Override + public boolean isEnabled() { + return true; + } + + @Override + public void setEnabled(boolean enabled) {} + + @Override + public boolean canBeSeen(IViewportStack stack) { + return false; + } + + @Override + public void markTooltipDirty() {} + + @Override + public ModularGuiContext getContext() { + return this.parent.getContext(); + } + + @Override + public Flex flex() { + return this.flex; + } + + @Override + public @NotNull IResizeable resizer() { + return this.flex; + } + + @Override + public void resizer(IResizeable resizer) {} +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/ParentWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/ParentWidget.java new file mode 100644 index 00000000000..08d849dfc6e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/ParentWidget.java @@ -0,0 +1,27 @@ +package com.gregtechceu.gtceu.api.mui.widget; + +import com.gregtechceu.gtceu.api.mui.base.widget.IParentWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; + +/** + * A widget which can hold any amount of children. + * + * @param type of this widget + */ +public class ParentWidget> extends AbstractParentWidget + implements IParentWidget { + + public boolean addChild(IWidget child, int index) { + return super.addChild(child, index); + } + + @Override + public boolean remove(IWidget child) { + return super.remove(child); + } + + @Override + public boolean remove(int index) { + return super.remove(index); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/ScrollWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/ScrollWidget.java new file mode 100644 index 00000000000..e30990edd10 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/ScrollWidget.java @@ -0,0 +1,27 @@ +package com.gregtechceu.gtceu.api.mui.widget; + +import com.gregtechceu.gtceu.api.mui.base.widget.IParentWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.widget.scroll.HorizontalScrollData; +import com.gregtechceu.gtceu.api.mui.widget.scroll.VerticalScrollData; + +public class ScrollWidget> extends AbstractScrollWidget + implements IParentWidget { + + public ScrollWidget() { + super(null, null); + } + + public ScrollWidget(VerticalScrollData data) { + super(null, data); + } + + public ScrollWidget(HorizontalScrollData data) { + super(data, null); + } + + @Override + public boolean addChild(IWidget child, int index) { + return super.addChild(child, index); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/SingleChildWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/SingleChildWidget.java new file mode 100644 index 00000000000..9ff5fe22a63 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/SingleChildWidget.java @@ -0,0 +1,35 @@ +package com.gregtechceu.gtceu.api.mui.widget; + +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class SingleChildWidget> extends Widget { + + private IWidget child; + private List list = Collections.emptyList(); + + @Override + public @NotNull List getChildren() { + return this.list; + } + + private void updateList() { + this.list = this.child == null ? Collections.emptyList() : Collections.singletonList(this.child); + } + + public W child(IWidget child) { + if (child == this || this.child == child) { + return getThis(); + } + + this.child = child; + if (isValid()) { + child.initialise(this); + } + updateList(); + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/Widget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/Widget.java new file mode 100644 index 00000000000..57d7be3881c --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/Widget.java @@ -0,0 +1,777 @@ +package com.gregtechceu.gtceu.api.mui.widget; + +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.IThemeApi; +import com.gregtechceu.gtceu.api.mui.base.IUIHolder; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.layout.IResizeable; +import com.gregtechceu.gtceu.api.mui.base.value.IValue; +import com.gregtechceu.gtceu.api.mui.base.widget.*; +import com.gregtechceu.gtceu.api.mui.factory.GuiData; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.value.sync.ModularSyncManager; +import com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler; +import com.gregtechceu.gtceu.api.mui.value.sync.ValueSyncHandler; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Flex; +import com.gregtechceu.gtceu.api.mui.widget.sizer.IUnResizeable; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.MustBeInvokedByOverriders; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * A very modular implementation of {@link IWidget}. This is the base class for almost all UI elements. + * This class is perfectly fine for displaying drawables (although {@link IDrawable.DrawableWidget DrawableWidget} + * is preferred) or even nothing. + *

+ * References to widgets should not be stored after the screen closed. While the screen is open its usually fine to remove and a widget + * as many times as you want. + * + * @param the type of this widget. This is used for proper return types in builder like methodsY + */ +public class Widget> implements IWidget, IPositioned, ITooltip, ISynced { + + // other + @Nullable + private String debugName; + /** + * Returns if this widget is currently enabled. Disabled widgets (and all its children) are not rendered and can't be interacted with. + */ + @Getter + @Setter + private boolean enabled = true; + // gui context + /** + * Returns if this widget is currently part of an open panel. Only if this is true information about parent, panel and gui context can + * be obtained. + */ + @Getter + private boolean valid = false; + private IWidget parent = null; + private ModularPanel panel = null; + private ModularGuiContext context = null; + // sizing + /** + * Returns the area of this widget. This contains information such as position, size, relative position to parent, padding and margin. + * Even tho this is a mutable object, you should refrain from modifying the values. + */ + @Getter + private final Area area = new Area(); + /** + * Returns the flex of this widget. This is responsible for calculating size, pos and relative pos. + * Originally this was intended to be modular for custom flex class. May come back to this in the future. + * Same as {@link #flex()}. + */ + @Getter + private final Flex flex = new Flex(this); + private IResizeable resizer = this.flex; + // syncing + /** + * Returns the value handler of this widget. Value handlers can provide and update any kind of objects like numbers and strings. + * For example text fields uses this get the current set string and updates the string after it is unfocused. + */ + @Getter + @Nullable + private IValue value; + @Nullable + private String syncKey; + /** + * This is intended to only be used when building the main panel in methods like + * {@link IUIHolder#buildUI(GuiData, com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager, com.gregtechceu.gtceu.client.mui.screen.UISettings)} + * since it's called on server and client. Otherwise, this will not work. + */ + @Setter + @Nullable + private SyncHandler syncHandler; + // rendering + /** + * The current set background. This is not an accurate representation of what is actually being displayed currently. + * Usually background is handled by the theme, which is when this is null. + * Backgrounds are drawn in {@link #drawBackground(ModularGuiContext, WidgetTheme)}. + */ + @Getter + @Nullable + private IDrawable background = null; + /** + * The current set overlay. This is used when the widget is not hovered or no hovered overlay is set. + * Overlays are drawn in {@link #drawOverlay(ModularGuiContext, WidgetTheme)}. + */ + @Getter + @Nullable + private IDrawable overlay = null; + /** + * The current set hover background. Usually this is handled by the theme. + */ + @Getter + @Nullable + private IDrawable hoverBackground = null; + /** + * The current set hover overlay. + */ + @Getter + @Nullable + private IDrawable hoverOverlay = null; + @Getter + @Nullable + private RichTooltip tooltip; + @Getter + @Nullable + private String widgetThemeOverride = null; + // listener + @Nullable + private List guiActionListeners; + @Getter + @Nullable + private Consumer onUpdateListener; + + // ----------------- + // === Lifecycle === + // ----------------- + + /** + * Called when a panel is opened. Use {@link #onInit()} and {@link #afterInit()} for custom logic. + * + * @param parent the parent this element belongs to + */ + @ApiStatus.Internal + @Override + public void initialise(@NotNull IWidget parent) { + if (!(this instanceof ModularPanel)) { + this.parent = parent; + this.panel = parent.getPanel(); + this.context = parent.getContext(); + getArea().setPanelLayer(this.panel.getArea().getPanelLayer()); + getArea().z(parent.getArea().z() + 1); + if (this.guiActionListeners != null) { + for (IGuiAction action : this.guiActionListeners) { + this.context.getScreen().registerGuiActionListener(action); + } + } + } + if (this.value != null && this.syncKey != null) { + throw new IllegalStateException( + "Widget has a value and a sync key for a synced value. This is not allowed!"); + } + this.valid = true; + if (!getScreen().isClientOnly()) { + initialiseSyncHandler(getScreen().getSyncManager()); + } + onInit(); + if (hasChildren()) { + for (IWidget child : getChildren()) { + child.initialise(this); + } + } + afterInit(); + onUpdate(); + } + + /** + * Called after this widget is initialised and before the children are initialised. + */ + @ApiStatus.OverrideOnly + public void onInit() {} + + /** + * Called after this widget is initialised and after the children are initialised. + */ + @ApiStatus.OverrideOnly + public void afterInit() {} + + /** + * Retrieves, initialises and verifies a linked sync handler. + * Custom logic should be handled in {@link #isValidSyncHandler(SyncHandler)}. + */ + @Override + public void initialiseSyncHandler(ModularSyncManager syncManager) { + if (this.syncKey != null) { + this.syncHandler = syncManager.getSyncHandler(getPanel().getName(), this.syncKey); + } + if ((this.syncKey != null || this.syncHandler != null) && !isValidSyncHandler(this.syncHandler)) { + String type = this.syncHandler == null ? null : this.syncHandler.getClass().getName(); + this.syncHandler = null; + throw new IllegalStateException("SyncHandler of type " + type + " is not valid for " + + getClass().getName() + ", with key " + this.syncKey); + } + if (this.syncHandler instanceof ValueSyncHandler valueSyncHandler && + valueSyncHandler.getChangeListener() == null) { + valueSyncHandler.setChangeListener(this::markTooltipDirty); + } + } + + /** + * Called when this widget is removed from the widget tree or after the panel is closed. + * Overriding this is fine, but super must be called. + */ + @MustBeInvokedByOverriders + @Override + public void dispose() { + if (isValid()) { + if (this.guiActionListeners != null) { + for (IGuiAction action : this.guiActionListeners) { + this.context.getScreen().removeGuiActionListener(action); + } + } + + } + if (hasChildren()) { + for (IWidget child : getChildren()) { + child.dispose(); + } + } + if (!(this instanceof ModularPanel)) { + this.panel = null; + this.parent = null; + this.context = null; + } + this.valid = false; + } + + // ----------------- + // === Rendering === + // ----------------- + + /** + * Called directly before {@link #draw(ModularGuiContext, WidgetTheme)}. Draws background textures. + * It is highly recommended to at least replicate this behaviour when overriding. + * Overriding {@link #draw(ModularGuiContext, WidgetTheme)} for custom visuals is preferred. + * If a parent of this widget is disabled, this widget will not be drawn. + * + * @param context gui context + * @param widgetTheme widget theme of this widget + */ + @Override + public void drawBackground(ModularGuiContext context, WidgetTheme widgetTheme) { + IDrawable bg = getCurrentBackground(context.getTheme(), widgetTheme); + if (bg != null) { + bg.drawAtZero(context, getArea().width, getArea().height, widgetTheme); + } + } + + /** + * Called between {@link #drawBackground(ModularGuiContext, WidgetTheme)} and {@link #drawOverlay(ModularGuiContext, WidgetTheme)}. + * Custom visuals should be drawn here. For example the {@link com.gregtechceu.gtceu.api.mui.widgets.slot.ItemSlot ItemSlot} draws its item + * here. If a parent of this widget is disabled, this widget will not be drawn. + * + * @param context gui context + * @param widgetTheme widget theme + */ + @Override + public void draw(ModularGuiContext context, WidgetTheme widgetTheme) {} + + /** + * Called directly after {@link #draw(ModularGuiContext, WidgetTheme)}. Draws overlay textures. + * It is highly recommended to at least replicate this behaviour when overriding. + * Overriding {@link #draw(ModularGuiContext, WidgetTheme)} for custom visuals is preferred. + * If a parent of this widget is disabled, this widget will not be drawn. + * + * @param context gui context + * @param widgetTheme widget theme + */ + @Override + public void drawOverlay(ModularGuiContext context, WidgetTheme widgetTheme) { + IDrawable bg = getCurrentOverlay(context.getTheme(), widgetTheme); + if (bg != null) { + bg.drawAtZero(context, getArea(), widgetTheme); + } + } + + /** + * Called after every widget of every panel and screen has been drawn. This is usually used to draw a tooltip, which is the default + * behaviour. If a parent of this widget is disabled, this widget will not be drawn. + * + * @param context gui context + */ + @Override + public void drawForeground(ModularGuiContext context) { + RichTooltip tooltip = getTooltip(); + if (tooltip != null && isHoveringFor(tooltip.getShowUpTimer())) { + tooltip.draw(context); + } + } + + /** + * Returns the actual currently displayed background. + * + * @param theme current theme + * @param widgetTheme widget theme which is used by this widget + * @return currently displayed background + */ + public IDrawable getCurrentBackground(ITheme theme, WidgetTheme widgetTheme) { + if (isHovering()) { + IDrawable hoverBackground = getHoverBackground(); + if (hoverBackground == null) hoverBackground = widgetTheme.getHoverBackground(); + if (hoverBackground != null && hoverBackground != IDrawable.NONE) return hoverBackground; + } + IDrawable background = getBackground(); + return background == null ? widgetTheme.getBackground() : background; + } + + /** + * Returns the actual currently displayed overlay. + * + * @param theme current theme + * @param widgetTheme widget theme which is used by this widget + * @return currently displayed background + */ + public IDrawable getCurrentOverlay(ITheme theme, WidgetTheme widgetTheme) { + IDrawable hoverBackground = getHoverOverlay(); + return hoverBackground != null && hoverBackground != IDrawable.NONE && isHovering() ? hoverBackground : + getOverlay(); + } + + /** + * @return the tooltip object of this widget and creates a new one if there is currently none. + */ + @Override + public @NotNull RichTooltip tooltip() { + if (this.tooltip == null) { + this.tooltip = new RichTooltip(this); + } + return this.tooltip; + } + + /** + * Sets a tooltip object. + * + * @param tooltip new tooltip + * @return this + */ + @Override + public W tooltip(RichTooltip tooltip) { + this.tooltip = tooltip; + return getThis(); + } + + /** + * Should be called when information which is displayed in the tooltip via {@link ITooltip#tooltipDynamic(Consumer)}. + * It will invalidate the current tooltip and be caused to rebuild. + */ + @Override + public void markTooltipDirty() { + if (this.tooltip != null) { + this.tooltip.markDirty(); + } + } + + /** + * Returns the widget theme this widget class would like to use. Overriding is fine. + * + * @param theme theme to get widget theme from + * @return widget theme this widget wishes to use + */ + @ApiStatus.OverrideOnly + protected WidgetTheme getWidgetThemeInternal(ITheme theme) { + return theme.getFallback(); + } + + /** + * Returns the actual used widget theme. Uses {@link #widgetTheme(String)} if it has been set, otherwise calls + * {@link #getWidgetThemeInternal(ITheme)} + * + * @param theme theme to get widget theme from + * @return widget theme this widget will use + */ + @ApiStatus.NonExtendable + @Override + public final WidgetTheme getWidgetTheme(ITheme theme) { + if (this.widgetThemeOverride != null) { + return theme.getWidgetTheme(this.widgetThemeOverride); + } + return getWidgetThemeInternal(theme); + } + + /** + * Sets a background override. Ideally this is set in the used theme. Also consider using {@link #overlay(IDrawable...)} instead. + * Using {@link IDrawable#EMPTY} will make the background invisible while still overriding the widget theme. + * Background are drawn before the widget and overlays are drawn. + * + * @param background background to use. + * @return this + */ + public W background(IDrawable... background) { + this.background = IDrawable.of(background); + return getThis(); + } + + /** + * Sets an overlay. Does not interfere with themes. Overlays are drawn after the widget and backgrounds. + * + * @param overlay overlay to use. + * @return this + */ + public W overlay(IDrawable... overlay) { + this.overlay = IDrawable.of(overlay); + return getThis(); + } + + /** + * Sets a hover background override. Ideally this is set in the used theme. Also consider using {@link #hoverOverlay(IDrawable...)} instead. + * Using {@link IDrawable#EMPTY} will make the background invisible while still overriding the widget theme. + * Background are drawn before the widget and overlays are drawn. + *

+ * Following argument special cases should be considered: + *

    + *
  • {@code null} will fallback to {@link WidgetTheme#getHoverBackground()}
  • + *
  • {@link IDrawable#EMPTY} will make the hover background invisible
  • + *
  • {@link IDrawable#NONE} will use the normal background instead (which is also achieved using {@link #disableHoverBackground()})
  • + *
  • multiple drawables, will result in them being drawn on top of each other in the order they are passed to the method
  • + *
+ * + * @param background hover background to use. + * @return this + */ + public W hoverBackground(IDrawable... background) { + this.hoverBackground = IDrawable.of(background); + return getThis(); + } + + /** + * Sets a hover overlay. + * Using {@link IDrawable#EMPTY} will make the background invisible while still overriding the widget theme. + * Background are drawn before the widget and overlays are drawn. + *

+ * Following argument special cases should be considered: + *

    + *
  • {@link IDrawable#EMPTY} will make the hover overlay invisible
  • + *
  • {@code null} and {@link IDrawable#NONE} will use the normal overlay instead (which is also achieved using {@link #disableHoverOverlay()})
  • + *
  • multiple drawables, will result in them being drawn on top of each other in the order they are passed to the method
  • + *
+ * + * @param overlay hover overlay to use. + * @return this + */ + public W hoverOverlay(IDrawable... overlay) { + this.hoverOverlay = IDrawable.of(overlay); + return getThis(); + } + + /** + * Forces the hover background to use the normal background instead. + * + * @return this + */ + public W disableHoverBackground() { + return hoverBackground(IDrawable.NONE); + } + + /** + * Forces the hover overlay to use the normal overlay instead. + * + * @return this + */ + public W disableHoverOverlay() { + return hoverOverlay(IDrawable.NONE); + } + + /** + * Sets an override widget theme. This will change of the appearance of this widget according to the widget theme. + * + * @param s id of the widget theme (see constants in {@link IThemeApi}) + * @return this + */ + public W widgetTheme(String s) { + if (!IThemeApi.get().hasWidgetTheme(s)) { + throw new IllegalArgumentException("No widget theme for id '" + s + "' exists."); + } + this.widgetThemeOverride = s; + return getThis(); + } + + // -------------- + // === Events === + // -------------- + + /** + * Called once every tick (20 times per second). Overriding is fine, but super should be called. This will be called even of the widget + * is not enabled. + * By default, this will invoke update listeners set via setters. + */ + @MustBeInvokedByOverriders + @Override + public void onUpdate() { + if (this.onUpdateListener != null) { + this.onUpdateListener.accept(getThis()); + } + } + + /** + * Registers a gui action this widget can listen to. Gui action listeners can listen to several mouse and keyboard input events. + * The listeners are called first, before any widgets are interacted with. The listeners will always be called, even if the widget + * is disabled or not hovered! + *

+ * Lambdas must be cast to the appropriate functional interface. + * These actions are automatically unregistered when the widget is removed from the widget tree. + * + * @param action gui action to register + * @return this + */ + public W listenGuiAction(IGuiAction action) { + if (this.guiActionListeners == null) { + this.guiActionListeners = new ArrayList<>(); + } + this.guiActionListeners.add(action); + if (isValid()) { + this.context.getScreen().registerGuiActionListener(action); + } + return getThis(); + } + + /** + * Sets an update listener which is called once every tick even when this widget is disabled. + * + * @param listener update listener + * @return this + */ + public W onUpdateListener(Consumer listener) { + return onUpdateListener(listener, false); + } + + /** + * Sets an update listener which is called once every tick even when this widget is disabled. + * If a listener is already set and {@code merge} is true, the listeners will be merged, so that both will be called on tick. + * + * @param listener update listener + * @return this + */ + public W onUpdateListener(Consumer listener, boolean merge) { + if (merge && this.onUpdateListener != null) { + if (listener != null) { + this.onUpdateListener = w -> { + this.onUpdateListener.accept(w); + listener.accept(w); + }; + } + } else { + this.onUpdateListener = listener; + } + return getThis(); + } + + /** + * Sets a condition for when to enable/disable this widget. This register an update listener which checks the condition every tick. + * Careful not to overwrite this when calling {@link #onUpdateListener(Consumer)} afterward! + * + * @param condition condition when to enable this widget + * @return this + */ + public W setEnabledIf(Predicate condition) { + return onUpdateListener(w -> setEnabled(condition.test(w)), true); + } + + // ---------------- + // === Resizing === + // ---------------- + + /** + * Returns the flex of this widget. This is responsible for calculating size, pos and relative pos. + * Originally this was intended to be modular for custom flex class. May come back to this in the future. + * Same as {@link #getFlex()}. + * + * @return flex of this widget + */ + @Override + public Flex flex() { + return this.flex; + } + + /** + * Returns the resizer of this widget. This is actually the field responsible for resizing this widget. + * Within MUI this is always the same as {@link #flex()}. Custom resizer have not been tested. + * The relevance of separating flex and resizer is left to be investigated in the future. + * + * @return the resizer of this widget + */ + @NotNull + @Override + public IResizeable resizer() { + return this.resizer; + } + + /** + * Sets the resizer of this widget, which is responsible for resizing this widget. + * Within MUI this setter is never used. Custom resizer have not been tested. + * The relevance of separating flex and resizer is left to be investigated in the future. + * + * @param resizer resizer + */ + @Override + public void resizer(IResizeable resizer) { + this.resizer = resizer != null ? resizer : IUnResizeable.INSTANCE; + } + + // ------------------- + // === Gui context === + // ------------------- + + /** + * Returns the screen of the panel of this widget is being opened in. + * + * @return the screen of this widget + * @throws IllegalStateException if {@link #isValid()} returns false + */ + @Override + public ModularScreen getScreen() { + return getPanel().getScreen(); + } + + /** + * Returns the panel of this widget is being opened in. + * + * @return the screen of this widget + * @throws IllegalStateException if {@link #isValid()} returns false + */ + @Override + public @NotNull ModularPanel getPanel() { + if (!isValid()) { + throw new IllegalStateException(getClass().getSimpleName() + " is not in a valid state!"); + } + return this.panel; + } + + /** + * Returns the parent of this widget. If this is a {@link ModularPanel} this will always return null contrary to the annotation. + * + * @return the screen of this widget + * @throws IllegalStateException if {@link #isValid()} returns false + */ + @Override + public @NotNull IWidget getParent() { + if (!isValid()) { + throw new IllegalStateException(getClass().getSimpleName() + " is not in a valid state!"); + } + return this.parent; + } + + /** + * Returns the gui context of the screen this widget is part of. + * + * @return the screen of this widget + * @throws IllegalStateException if {@link #isValid()} returns false + */ + @Override + public ModularGuiContext getContext() { + if (!isValid()) { + throw new IllegalStateException(getClass().getSimpleName() + " is not in a valid state!"); + } + return this.context; + } + + /** + * Used to set the gui context on panels internally. + */ + @ApiStatus.Internal + protected final void setContext(ModularGuiContext context) { + this.context = context; + } + + // --------------- + // === Syncing === + // -------------- + + /** + * Returns if this widget has a valid sync handler. + */ + @Override + public boolean isSynced() { + return this.syncHandler != null; + } + + /** + * Returns the sync handler of this widget. + * + * @throws IllegalStateException if this widget has no sync handler ({@link #isSynced()} returns false) + */ + @Override + public @NotNull SyncHandler getSyncHandler() { + if (this.syncHandler == null) { + throw new IllegalStateException("Widget is not initialised or not synced!"); + } + return this.syncHandler; + } + + /** + * Sets a sync handler id. A sync handler with the same id must have been registered to the appropriate + * {@link com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager PanelSyncManager} for this to work. + * This method is preferred over setting a sync handler directly since this does not require the widget to be defined on both sides. + * + * @param name sync handler key name + * @param id sync handler key id + * @return this + */ + @Override + public W syncHandler(String name, int id) { + this.syncKey = ModularSyncManager.makeSyncKey(name, id); + return getThis(); + } + + /** + * Used for widgets to set a value handler. Can also be a sync handler + */ + protected void setValue(IValue value) { + this.value = value; + if (value instanceof SyncHandler syncHandler1) { + setSyncHandler(syncHandler1); + } + } + + // ------------- + // === Other === + // ------------- + + /** + * Disables the widget from start. Useful inside widget tree creation, where widget references are usually not stored. + * + * @return this + */ + public W disabled() { + setEnabled(false); + return getThis(); + } + + /** + * Sets a debug name. This is only used in {@link #toString()}, which is displayed in the mui debug info. Useful for identifying widgets + * for debugging. This has no other effect. + * + * @param name debug name to use + * @return this + */ + public W debugName(String name) { + this.debugName = name; + return getThis(); + } + + /** + * Returns this widget with proper generic type. + * + * @return this + */ + @SuppressWarnings("unchecked") + @Override + public W getThis() { + return (W) this; + } + + /** + * @return the simple class plus the debug name, if set + */ + @Override + public String toString() { + if (this.debugName != null) { + return getClass().getSimpleName() + "#" + this.debugName; + } + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/WidgetTree.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/WidgetTree.java new file mode 100644 index 00000000000..6c1d1323506 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/WidgetTree.java @@ -0,0 +1,396 @@ +package com.gregtechceu.gtceu.api.mui.widget; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.layout.ILayoutWidget; +import com.gregtechceu.gtceu.api.mui.base.layout.IResizeable; +import com.gregtechceu.gtceu.api.mui.base.layout.IViewport; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; +import com.gregtechceu.gtceu.api.mui.base.widget.ISynced; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.value.sync.ModularSyncManager; +import com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.api.mui.widgets.layout.IExpander; +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import com.mojang.blaze3d.systems.RenderSystem; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectList; +import net.minecraft.client.gui.GuiGraphics; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.ApiStatus; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +/** + * Helper class to apply actions to each widget in a tree. + */ +public class WidgetTree { + + private WidgetTree() {} + + public static List getAllChildrenByLayer(IWidget parent) { + return getAllChildrenByLayer(parent, false); + } + + public static List getAllChildrenByLayer(IWidget parent, boolean includeSelf) { + List children = new ArrayList<>(); + if (includeSelf) children.add(parent); + ObjectList parents = new ObjectArrayList<>(); + parents.add(parent); + while (!parents.isEmpty()) { + for (IWidget child : parents.remove(0).getChildren()) { + if (!child.getChildren().isEmpty()) { + parents.add(child); + } + children.add(child); + } + } + return children; + } + + public static boolean foreachChildBFS(IWidget parent, Predicate consumer) { + return foreachChildBFS(parent, consumer, false); + } + + public static boolean foreachChildBFS(IWidget parent, Predicate consumer, boolean includeSelf) { + if (includeSelf && !consumer.test(parent)) return false; + ObjectList parents = new ObjectArrayList<>(); + parents.add(parent); + while (!parents.isEmpty()) { + for (IWidget child : parents.remove(0).getChildren()) { + if (child.hasChildren()) { + parents.add(child); + } + if (!consumer.test(child)) return false; + } + } + return true; + } + + public static boolean foreachChildByLayer2(IWidget parent, Predicate consumer, boolean includeSelf) { + if (includeSelf && !consumer.test(parent)) return false; + ObjectList parents = new ObjectArrayList<>(); + parents.add(parent); + while (!parents.isEmpty()) { + for (IWidget child : parents.remove(0).getChildren()) { + if (!consumer.test(child)) return false; + + if (child.hasChildren()) { + parents.add(child); + } + } + } + return true; + } + + public static boolean foreachChild(IWidget parent, Predicate consumer, boolean includeSelf) { + if (includeSelf && !consumer.test(parent)) return false; + if (parent.getChildren().isEmpty()) return true; + for (IWidget widget : parent.getChildren()) { + if (!consumer.test(widget)) return false; + if (!widget.getChildren().isEmpty() && foreachChild(widget, consumer, false)) { + return false; + } + } + return true; + } + + public static boolean foreachChildReverse(IWidget parent, Predicate consumer, boolean includeSelf) { + if (parent.getChildren().isEmpty()) { + return !includeSelf || consumer.test(parent); + } + for (IWidget widget : parent.getChildren()) { + if (!widget.getChildren().isEmpty() && foreachChildReverse(widget, consumer, false)) { + return false; + } + if (!consumer.test(widget)) return false; + } + return !includeSelf || consumer.test(parent); + } + + public static void drawTree(IWidget parent, ModularGuiContext context) { + drawTree(parent, context, false); + } + + public static void drawTree(IWidget parent, ModularGuiContext context, boolean ignoreEnabled) { + if (!parent.isEnabled() && !ignoreEnabled) return; + + GuiGraphics graphics = context.getGraphics(); + float alpha = parent.getPanel().getAlpha(); + IViewport viewport = parent instanceof IViewport ? (IViewport) parent : null; + + // transform stack according to the widget + context.pushMatrix(); + parent.transform(context); + + boolean canBeSeen = parent.canBeSeen(context); + + // apply transformations to opengl + graphics.pose().pushPose(); + context.applyTo(graphics.pose()); + + if (canBeSeen) { + // draw widget + RenderSystem.colorMask(true, true, true, true); + graphics.setColor(1f, 1f, 1f, alpha); + RenderSystem.enableBlend(); + WidgetTheme widgetTheme = parent.getWidgetTheme(context.getTheme()); + parent.drawBackground(context, widgetTheme); + parent.draw(context, widgetTheme); + parent.drawOverlay(context, widgetTheme); + } + + if (viewport != null) { + if (canBeSeen) { + // draw viewport without children transformation + graphics.setColor(1f, 1f, 1f, alpha); + RenderSystem.enableBlend(); + viewport.preDraw(context, false); + graphics.pose().popPose(); + // apply children transformation of the viewport + context.pushViewport(viewport, parent.getArea()); + viewport.transformChildren(context); + // apply to opengl and draw with transformation + graphics.pose().pushPose(); + context.applyTo(graphics.pose()); + viewport.preDraw(context, true); + } else { + // only transform stack + context.pushViewport(viewport, parent.getArea()); + viewport.transformChildren(context); + } + } + // remove all opengl transformations + graphics.pose().popPose(); + + // render all children if there are any + List children = parent.getChildren(); + if (!children.isEmpty()) { + children.forEach(widget -> drawTree(widget, context, false)); + } + + if (viewport != null) { + if (canBeSeen) { + // apply opengl transformations again and draw + graphics.setColor(1f, 1f, 1f, alpha); + RenderSystem.enableBlend(); + graphics.pose().pushPose(); + context.applyTo(graphics.pose()); + viewport.postDraw(context, true); + // remove children transformation of this viewport + context.popViewport(viewport); + graphics.pose().popPose(); + // apply transformation again to opengl and draw + graphics.pose().pushPose(); + context.applyTo(graphics.pose()); + viewport.postDraw(context, false); + graphics.pose().popPose(); + } else { + // only remove transformation + context.popViewport(viewport); + } + } + // remove all widget transformations + context.popMatrix(); + } + + public static void drawTreeForeground(IWidget parent, ModularGuiContext context) { + IViewport viewport = parent instanceof IViewport viewport1 ? viewport1 : null; + context.pushMatrix(); + parent.transform(context); + + context.getGraphics().setColor(1, 1, 1, 1); + RenderSystem.enableBlend(); + parent.drawForeground(context); + + List children = parent.getChildren(); + if (!children.isEmpty()) { + if (viewport != null) { + context.pushViewport(viewport, parent.getArea()); + viewport.transformChildren(context); + } + children.forEach(widget -> drawTreeForeground(widget, context)); + if (viewport != null) context.popViewport(viewport); + } + context.popMatrix(); + } + + @ApiStatus.Internal + public static void onUpdate(IWidget parent) { + foreachChildBFS(parent, widget -> { + widget.onUpdate(); + return true; + }, true); + } + + public static void resize(IWidget parent) { + if (!GTCEu.isClientThread()) return; + + while(!(parent instanceof ModularPanel) && (parent.getParent() instanceof ILayoutWidget || + parent.getParent().flex().dependsOnChildren())) { + parent = parent.getParent(); + } + // resize each widget and calculate their relative pos + if (!resizeWidget(parent, true) && !resizeWidget(parent, false)) { + throw new IllegalStateException("Failed to resize widgets"); + } + // now apply the calculated pos + applyPos(parent); + WidgetTree.foreachChildBFS(parent, child -> { + child.postResize(); + return true; + }, true); + } + + private static boolean resizeWidget(IWidget widget, boolean init) { + boolean alreadyCalculated = false; + // first try to resize this widget + IResizeable resizer = widget.resizer(); + if (init) { + widget.beforeResize(); + resizer.initResizing(); + } else { + // if this is not the first time check if this widget is already resized + alreadyCalculated = resizer.isFullyCalculated(); + } + boolean result = alreadyCalculated || resizer.resize(widget); + + GuiAxis expandAxis = widget instanceof IExpander expander ? expander.getExpandAxis() : null; + // now resize all children and collect children which could not be fully calculated + List anotherResize = Collections.emptyList(); + if (widget.hasChildren()) { + anotherResize = new ArrayList<>(); + for (IWidget child : widget.getChildren()) { + if (init && expandAxis != null) child.flex().checkExpanded(expandAxis); + if (!resizeWidget(child, init)) { + anotherResize.add(child); + } + } + } + + if (!alreadyCalculated) { + if (widget instanceof ILayoutWidget layoutWidget) { + layoutWidget.layoutWidgets(); + } + + // post resize this widget if possible + if (!result) { + result = resizer.postResize(widget); + } + + if (widget instanceof ILayoutWidget layoutWidget) { + layoutWidget.postLayoutWidgets(); + } + } + + // now fully resize all children which needs it + if (!anotherResize.isEmpty()) { + anotherResize.removeIf(iWidget -> resizeWidget(iWidget, false)); + } + + if (result && !alreadyCalculated) widget.onResized(); + + return result && anotherResize.isEmpty(); + } + + public static void applyPos(IWidget parent) { + WidgetTree.foreachChildBFS(parent, child -> { + child.resizer().applyPos(child); + return true; + }, true); + } + + public static IGuiElement findParent(IGuiElement parent, Predicate filter) { + if (parent == null) return null; + while (!(parent instanceof ModularPanel)) { + if (filter.test(parent)) { + return parent; + } + parent = parent.getParent(); + } + return filter.test(parent) ? parent : null; + } + + public static IWidget findParent(IWidget parent, Predicate filter) { + if (parent == null) return null; + while (!(parent instanceof ModularPanel)) { + if (filter.test(parent)) { + return parent; + } + parent = parent.getParent(); + } + return filter.test(parent) ? parent : null; + } + + public static T findParent(IWidget parent, Class type) { + if (parent == null) return null; + while (!(parent instanceof ModularPanel)) { + if (type.isAssignableFrom(parent.getClass())) { + return (T) parent; + } + parent = parent.getParent(); + } + return type.isAssignableFrom(parent.getClass()) ? (T) parent : null; + } + + @ApiStatus.Internal + public static void collectSyncValues(PanelSyncManager syncManager, ModularPanel panel) { + collectSyncValues(syncManager, panel, true); + } + + @ApiStatus.Internal + public static void collectSyncValues(PanelSyncManager syncManager, ModularPanel panel, boolean includePanel) { + AtomicInteger id = new AtomicInteger(0); + String syncKey = ModularSyncManager.AUTO_SYNC_PREFIX + panel.getName(); + foreachChildBFS(panel, widget -> { + if (widget instanceof ISynced synced) { + if (synced.isSynced() && !syncManager.hasSyncHandler(synced.getSyncHandler())) { + syncManager.syncValue(syncKey, id.getAndIncrement(), synced.getSyncHandler()); + } + } + return true; + }, includePanel); + } + + public static boolean hasSyncedValues(ModularPanel panel) { + return !foreachChildBFS(panel, widget -> !(widget instanceof ISynced synced) || !synced.isSynced(), true); + } + + public static void print(IWidget parent, Predicate test) { + StringBuilder builder = new StringBuilder("Widget tree of ") + .append(parent) + .append('\n'); + getTree(parent.getArea(), parent, test, builder, 0); + GTCEu.LOGGER.info(builder.toString()); + } + + private static void getTree(Area root, IWidget parent, Predicate test, StringBuilder builder, int indent) { + if (indent >= 2) { + builder.append(StringUtils.repeat(' ', indent - 2)) + .append("- "); + } + builder.append(parent).append(" {") + .append(parent.getArea().x - root.x) + .append(", ") + .append(parent.getArea().y - root.y) + .append(" | ") + .append(parent.getArea().width) + .append(", ") + .append(parent.getArea().height) + .append("}\n"); + if (parent.hasChildren()) { + for (IWidget child : parent.getChildren()) { + if (test.test(child)) { + getTree(root, child, test, builder, indent + 2); + } + } + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/HorizontalScrollData.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/HorizontalScrollData.java new file mode 100644 index 00000000000..be8d557d044 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/HorizontalScrollData.java @@ -0,0 +1,82 @@ +package com.gregtechceu.gtceu.api.mui.widget.scroll; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.drawable.GuiDraw; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +public class HorizontalScrollData extends ScrollData { + + /** + * Creates horizontal scroll data which handles scrolling and scroll bar. + * Scrollbar is 4 pixels high and is placed at the bottom. + */ + public HorizontalScrollData() { + this(false, DEFAULT_THICKNESS); + } + + /** + * Creates horizontal scroll data which handles scrolling and scroll bar. + * Scrollbar is 4 pixels high. + * + * @param topAlignment if the scroll bar should be placed at the top + */ + public HorizontalScrollData(boolean topAlignment) { + this(topAlignment, DEFAULT_THICKNESS); + } + + /** + * Creates horizontal scroll data which handles scrolling and scroll bar. + * + * @param topAlignment if the scroll bar should be placed at the top + * @param thickness height of the scroll bar in pixels + */ + public HorizontalScrollData(boolean topAlignment, int thickness) { + super(GuiAxis.X, topAlignment, thickness); + } + + public HorizontalScrollData cancelScrollEdge(boolean cancelScrollEdge) { + setCancelScrollEdge(cancelScrollEdge); + return this; + } + + @Override + public VerticalScrollData getOtherScrollData(ScrollArea area) { + return area.getScrollY(); + } + + @Override + public boolean isInsideScrollbarArea(ScrollArea area, int x, int y) { + if (!area.isInside(x, y) || !isScrollBarActive(area, false)) { + return false; + } + int scrollbar = getThickness(); + ScrollData data = getOtherScrollData(area); + if (data != null && isOtherScrollBarActive(area, true)) { + int thickness = data.getThickness(); + if (data.isAxisStart() ? x < area.x + thickness : x >= area.ex() - thickness) { + return false; + } + } + return isAxisStart() ? y >= area.y && y < area.y + scrollbar : y >= area.ey() - scrollbar && y < area.ey(); + } + + @Override + public void drawScrollbar(GuiContext context, ScrollArea area) { + boolean isOtherActive = isOtherScrollBarActive(area, true); + int l = getScrollBarLength(area); + int x = 0; + int y = isAxisStart() ? 0 : area.height - getThickness(); + int w = area.width; + int h = getThickness(); + GuiDraw.drawRect(context.getGraphics(), x, y, w, h, area.getScrollBarBackgroundColor()); + + x = getScrollBarStart(area, l, isOtherActive); + ScrollData data2 = getOtherScrollData(area); + if (data2 != null && isOtherActive && data2.isAxisStart()) { + x += data2.getThickness(); + } + + w = l; + drawScrollBar(context, x, y, w, h); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/ScrollArea.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/ScrollArea.java new file mode 100644 index 00000000000..82912db4daa --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/ScrollArea.java @@ -0,0 +1,204 @@ +package com.gregtechceu.gtceu.api.mui.widget.scroll; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.utils.GTMath; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import net.minecraft.client.gui.screens.Screen; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +/** + * Scrollable area + *

+ * This class is responsible for storing information for scrollable one + * directional objects. + */ +@Accessors(chain = true) +public class ScrollArea extends Area { + + @Getter + @Setter + private HorizontalScrollData scrollX; + @Getter + @Setter + private VerticalScrollData scrollY; + @Getter + @Setter + private int scrollBarBackgroundColor = Color.withAlpha(Color.BLACK.main, 0.25f); + + public ScrollArea(int x, int y, int w, int h) { + super(x, y, w, h); + } + + public ScrollArea() {} + + public void setScrollData(ScrollData data) { + if (data instanceof HorizontalScrollData scrollData) { + this.scrollX = scrollData; + } else if (data instanceof VerticalScrollData scrollData) { + this.scrollY = scrollData; + } + } + + public void removeScrollData() { + this.scrollX = null; + this.scrollY = null; + } + + public ScrollData getScrollData(GuiAxis axis) { + return axis.isVertical() ? this.scrollY : this.scrollX; + } + + /* GUI code for easier manipulations */ + + @OnlyIn(Dist.CLIENT) + public boolean mouseClicked(GuiContext context) { + return this.mouseClicked(context.getMouseX(), context.getMouseY()); + } + + /** + * This method should be invoked to register dragging + */ + public boolean mouseClicked(int x, int y) { + if (this.scrollX != null && this.scrollX.isInsideScrollbarArea(this, x, y)) { + return this.scrollX.onMouseClicked(this, x, y, 0); + } else if (this.scrollY != null && this.scrollY.isInsideScrollbarArea(this, x, y)) { + return this.scrollY.onMouseClicked(this, y, x, 0); + } else { + return false; + } + } + + @OnlyIn(Dist.CLIENT) + public boolean mouseScroll(GuiContext context) { + return this.mouseScroll(context.getMouseX(), context.getMouseY(), context.getMouseScrollDelta(), + Screen.hasShiftDown()); + } + + /** + * This method should be invoked when mouse wheel is scrolling + */ + public boolean mouseScroll(int x, int y, double scroll, boolean shift) { + if (!isInside(x, y)) { + // not hovering TODO: this shouldn't be required + return false; + } + + ScrollData data; + if (this.scrollX != null) { + data = this.scrollY == null || shift ? this.scrollX : this.scrollY; + } else if (this.scrollY != null) { + data = this.scrollY; + } else { + // no scroll data present -> cant be scrolled + return false; + } + + int scrollAmount = (int) Math.copySign(data.getScrollSpeed(), scroll); + int scrollTo; + if (data.isAnimating()) { + scrollTo = data.getAnimatingTo() - scrollAmount; + } else { + scrollTo = data.getScroll() - scrollAmount; + } + + // simulate scroll to determine whether event should be canceled + int oldScroll = data.getScroll(); + data.scrollTo(this, scrollTo); + boolean changed = data.getScroll() != oldScroll; + data.scrollTo(this, oldScroll); + if (changed) { + data.animateTo(this, scrollTo); + return true; + } + return data.isCancelScrollEdge(); + } + + @OnlyIn(Dist.CLIENT) + public void mouseReleased(GuiContext context) { + this.mouseReleased(context.getMouseX(), context.getMouseY()); + } + + /** + * When mouse button gets released + */ + public void mouseReleased(int x, int y) { + if (this.scrollX != null) { + this.scrollX.dragging = false; + this.scrollX.clickOffset = 0; + } + if (this.scrollY != null) { + this.scrollY.dragging = false; + this.scrollY.clickOffset = 0; + } + } + + @OnlyIn(Dist.CLIENT) + public void drag(GuiContext context) { + this.drag(context.getMouseX(), context.getMouseY()); + } + + /** + * This should be invoked in a drawing or and update method. It's + * responsible for scrolling through this view when dragging. + */ + public void drag(int x, int y) { + ScrollData data; + float progress; + if (this.scrollX != null && this.scrollX.dragging) { + data = this.scrollX; + progress = data.getProgress(this, x, y); + } else if (this.scrollY != null && this.scrollY.dragging) { + data = this.scrollY; + progress = data.getProgress(this, y, x); + } else { + return; + } + progress = GTMath.clamp(progress, 0f, 1f); + data.scrollTo(this, + (int) (progress * (data.getScrollSize() - data.getVisibleSize(this) + data.getThickness()))); + } + + public boolean isInsideScrollbarArea(int x, int y) { + if (!isInside(x, y)) { + return false; + } + if (this.scrollX != null && this.scrollX.isInsideScrollbarArea(this, x, y)) { + return true; + } + return this.scrollY != null && this.scrollY.isInsideScrollbarArea(this, x, y); + } + + public boolean isScrollBarXActive() { + return this.scrollX != null && this.scrollX.isScrollBarActive(this); + } + + public boolean isScrollBarYActive() { + return this.scrollY != null && this.scrollY.isScrollBarActive(this); + } + + public boolean isDragging() { + return (this.scrollX != null && this.scrollX.isDragging()) || + (this.scrollY != null && this.scrollY.isDragging()); + } + + /** + * This method is responsible for drawing a scroll bar + */ + @OnlyIn(Dist.CLIENT) + public void drawScrollbar(GuiContext context) { + boolean isXActive = false; // micro optimisation + if (this.scrollX != null && this.scrollX.isScrollBarActive(this, false)) { + isXActive = true; + this.scrollX.drawScrollbar(context, this); + } + if (this.scrollY != null && this.scrollY.isScrollBarActive(this, isXActive)) { + this.scrollY.drawScrollbar(context, this); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/ScrollData.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/ScrollData.java new file mode 100644 index 00000000000..4be2922d9b1 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/ScrollData.java @@ -0,0 +1,258 @@ +package com.gregtechceu.gtceu.api.mui.widget.scroll; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.drawable.GuiDraw; +import com.gregtechceu.gtceu.api.mui.utils.Animator; +import com.gregtechceu.gtceu.api.mui.utils.Interpolation; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import lombok.Getter; +import lombok.Setter; +import net.minecraft.util.Mth; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.jetbrains.annotations.Nullable; + +public abstract class ScrollData { + + /** + * Creates scroll data which handles scrolling and scroll bar. Scrollbar is 4 pixels thick + * and will be at the end of the cross axis (bottom/right). + * + * @param axis axis on which to scroll + * @return new scroll data + */ + public static ScrollData of(GuiAxis axis) { + return of(axis, false, DEFAULT_THICKNESS); + } + + /** + * Creates scroll data which handles scrolling and scroll bar. Scrollbar is 4 pixels thick. + * + * @param axis axis on which to scroll + * @param axisStart if the scroll bar should be at the start of the cross axis (left/top) + * @return new scroll data + */ + public static ScrollData of(GuiAxis axis, boolean axisStart) { + return of(axis, axisStart, DEFAULT_THICKNESS); + } + + /** + * Creates scroll data which handles scrolling and scroll bar. + * + * @param axis axis on which to scroll + * @param axisStart if the scroll bar should be at the start of the cross axis (left/top) + * @param thickness cross axis thickness of the scroll bar in pixel + * @return new scroll data + */ + public static ScrollData of(GuiAxis axis, boolean axisStart, int thickness) { + if (axis.isHorizontal()) return new HorizontalScrollData(axisStart, thickness); + return new VerticalScrollData(axisStart, thickness); + } + + public static final int DEFAULT_THICKNESS = 4; + + @Getter + private final GuiAxis axis; + @Getter + private final boolean axisStart; + @Getter + private final int thickness; + @Getter + @Setter + private int scrollSpeed = 30; + /** + * Determines if scrolling of widgets below should still be canceled if this scroll view + * has hit the end and is currently not scrolling. + * Most of the time this should be true + * + * @return true if scrolling should be canceled even when this view hit an edge + */ + @Getter + @Setter + private boolean cancelScrollEdge = true; + + @Getter + @Setter + private int scrollSize; + @Getter + private int scroll; + @Getter + protected boolean dragging; + protected int clickOffset; + + @Getter + private int animatingTo = 0; + private final Animator scrollAnimator = new Animator(30, Interpolation.QUAD_OUT); + + protected ScrollData(GuiAxis axis, boolean axisStart, int thickness) { + this.axis = axis; + this.axisStart = axisStart; + this.thickness = thickness <= 0 ? DEFAULT_THICKNESS : thickness; + } + + public boolean isVertical() { + return this.axis.isVertical(); + } + + public boolean isHorizontal() { + return this.axis.isHorizontal(); + } + + protected final int getRawVisibleSize(ScrollArea area) { + return Math.max(0, getRawFullVisibleSize(area) - area.getPadding().getTotal(this.axis)); + } + + protected final int getRawFullVisibleSize(ScrollArea area) { + return area.getSize(this.axis); + } + + public final int getFullVisibleSize(ScrollArea area) { + return getFullVisibleSize(area, false); + } + + public final int getFullVisibleSize(ScrollArea area, boolean isOtherActive) { + int s = getRawFullVisibleSize(area); + ScrollData data = getOtherScrollData(area); + if (data != null && (isOtherActive || data.isScrollBarActive(area, true))) { + s -= data.getThickness(); + } + return s; + } + + public final int getVisibleSize(ScrollArea area) { + return getVisibleSize(area, false); + } + + public final int getVisibleSize(ScrollArea area, int fullVisibleSize) { + return Math.max(0, fullVisibleSize - area.getPadding().getTotal(this.axis)); + } + + public final int getVisibleSize(ScrollArea area, boolean isOtherActive) { + return getVisibleSize(area, getFullVisibleSize(area, isOtherActive)); + } + + public float getProgress(ScrollArea area, int mainAxisPos, int crossAxisPos) { + float fullSize = (float) getFullVisibleSize(area); + return (mainAxisPos - area.getPoint(this.axis) - clickOffset) / (fullSize - getScrollBarLength(area)); + } + + @Nullable + public abstract ScrollData getOtherScrollData(ScrollArea area); + + /** + * Clamp scroll to the bounds of the scroll size; + */ + public boolean clamp(ScrollArea area) { + int size = getVisibleSize(area); + + int old = this.scroll; + if (this.scrollSize <= size) { + this.scroll = 0; + } else { + this.scroll = Mth.clamp(this.scroll, 0, this.scrollSize - size); + } + return old != this.scroll; // returns true if the area was clamped + } + + public boolean scrollBy(ScrollArea area, int x) { + this.scroll += x; + return clamp(area); + } + + /** + * Scroll to the position in the scroll area + */ + public boolean scrollTo(ScrollArea area, int x) { + this.scroll = x; + return clamp(area); + } + + public void animateTo(ScrollArea area, int x) { + this.scrollAnimator.setCallback(value -> { + return scrollTo(area, (int) value); // stop animation once an edge is hit + }); + this.scrollAnimator.setValueBounds(this.scroll, x); + this.scrollAnimator.forward(); + this.animatingTo = x; + } + + public final boolean isScrollBarActive(ScrollArea area) { + return isScrollBarActive(area, false); + } + + public final boolean isScrollBarActive(ScrollArea area, boolean isOtherActive) { + int s = getRawVisibleSize(area); + if (s < this.scrollSize) return true; + ScrollData data = getOtherScrollData(area); + if (data == null || s - data.getThickness() >= this.scrollSize) return false; + if (isOtherActive || data.isScrollBarActive(area, true)) { + s -= data.getThickness(); + } + return s < this.scrollSize; + } + + public final boolean isOtherScrollBarActive(ScrollArea area, boolean isSelfActive) { + ScrollData data = getOtherScrollData(area); + return data != null && data.isScrollBarActive(area, isSelfActive); + } + + public int getScrollBarLength(ScrollArea area) { + boolean isOtherActive = isOtherScrollBarActive(area, false); + int length = (int) (getVisibleSize(area, isOtherActive) * getFullVisibleSize(area, isOtherActive) / + (float) this.scrollSize); + return Math.max(length, DEFAULT_THICKNESS); // min length of 4 + } + + public abstract boolean isInsideScrollbarArea(ScrollArea area, int x, int y); + + public boolean isAnimating() { + return this.scrollAnimator.isRunning(); + } + + public int getAnimationDirection() { + if (!isAnimating()) return 0; + return this.scrollAnimator.getMax() >= this.scrollAnimator.getMin() ? 1 : -1; + } + + public int getScrollBarStart(ScrollArea area, int scrollBarLength, int fullVisibleSize) { + return ((fullVisibleSize - scrollBarLength) * getScroll()) / + (getScrollSize() - getVisibleSize(area, fullVisibleSize)); + } + + public int getScrollBarStart(ScrollArea area, int scrollBarLength, boolean isOtherActive) { + return getScrollBarStart(area, scrollBarLength, getFullVisibleSize(area, isOtherActive)); + } + + @OnlyIn(Dist.CLIENT) + public abstract void drawScrollbar(GuiContext context, ScrollArea area); + + @OnlyIn(Dist.CLIENT) + protected void drawScrollBar(GuiContext context, int x, int y, int w, int h) { + GuiDraw.drawRect(context.getGraphics(), x, y, w, h, 0xffeeeeee); + GuiDraw.drawRect(context.getGraphics(), x + 1, y + 1, w - 1, h - 1, 0xff666666); + GuiDraw.drawRect(context.getGraphics(), x + 1, y + 1, w - 2, h - 2, 0xffaaaaaa); + } + + public boolean onMouseClicked(ScrollArea area, int mainAxisPos, int crossAxisPos, int button) { + if (isAxisStart() ? crossAxisPos <= area.getPoint(this.axis.getOther()) + getThickness() : + crossAxisPos >= area.getEndPoint(this.axis.getOther()) - getThickness()) { + this.dragging = true; + this.clickOffset = mainAxisPos; + + int scrollBarSize = getScrollBarLength(area); + int start = getScrollBarStart(area, scrollBarSize, false); + int areaStart = area.getPoint(this.axis); + boolean clickInsideBar = mainAxisPos >= areaStart + start && + mainAxisPos <= areaStart + start + scrollBarSize; + + if (clickInsideBar) { + this.clickOffset = mainAxisPos - areaStart - start; // relative click position inside bar + } else { + this.clickOffset = scrollBarSize / 2; // assume click position in center of bar + } + + return true; + } + return false; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/VerticalScrollData.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/VerticalScrollData.java new file mode 100644 index 00000000000..508485cff26 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/scroll/VerticalScrollData.java @@ -0,0 +1,81 @@ +package com.gregtechceu.gtceu.api.mui.widget.scroll; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.drawable.GuiDraw; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +public class VerticalScrollData extends ScrollData { + + /** + * Creates vertical scroll data which handles scrolling and scroll bar. + * Scrollbar is 4 pixels wide and is placed on the right. + */ + public VerticalScrollData() { + this(false, DEFAULT_THICKNESS); + } + + /** + * Creates vertical scroll data which handles scrolling and scroll bar. + * Scrollbar is 4 pixels wide. + * + * @param leftAlignment if the scroll bar should be placed on the left + */ + public VerticalScrollData(boolean leftAlignment) { + this(leftAlignment, DEFAULT_THICKNESS); + } + + /** + * Creates vertical scroll data which handles scrolling and scroll bar. + * + * @param leftAlignment if the scroll bar should be placed on the left + * @param thickness width of the scroll bar in pixel + */ + public VerticalScrollData(boolean leftAlignment, int thickness) { + super(GuiAxis.Y, leftAlignment, thickness); + } + + public VerticalScrollData cancelScrollEdge(boolean cancelScrollEdge) { + setCancelScrollEdge(cancelScrollEdge); + return this; + } + + @Override + public HorizontalScrollData getOtherScrollData(ScrollArea area) { + return area.getScrollX(); + } + + @Override + public boolean isInsideScrollbarArea(ScrollArea area, int x, int y) { + if (!area.isInside(x, y) || !isScrollBarActive(area)) { + return false; + } + int scrollbar = getThickness(); + ScrollData data = getOtherScrollData(area); + if (data != null && isOtherScrollBarActive(area, true)) { + int thickness = data.getThickness(); + if (data.isAxisStart() ? y < area.y + thickness : y >= area.ey() - thickness) { + return false; + } + } + return isAxisStart() ? x >= area.x && x < area.x + scrollbar : x >= area.ex() - scrollbar && x < area.ex(); + } + + @Override + public void drawScrollbar(GuiContext context, ScrollArea area) { + boolean isOtherActive = isOtherScrollBarActive(area, true); + int l = this.getScrollBarLength(area); + int x = isAxisStart() ? 0 : area.w() - getThickness(); + int y = 0; + int w = getThickness(); + int h = area.height; + GuiDraw.drawRect(context.getGraphics(), x, y, w, h, area.getScrollBarBackgroundColor()); + + y = getScrollBarStart(area, l, isOtherActive); + ScrollData data2 = getOtherScrollData(area); + if (data2 != null && isOtherActive && data2.isAxisStart()) { + y += data2.getThickness(); + } + h = l; + drawScrollBar(context, x, y, w, h); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Area.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Area.java new file mode 100644 index 00000000000..7cbc15089c6 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Area.java @@ -0,0 +1,506 @@ +package com.gregtechceu.gtceu.api.mui.widget.sizer; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.layout.IViewportStack; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; +import com.gregtechceu.gtceu.api.mui.utils.Point; +import com.gregtechceu.gtceu.api.mui.utils.Rectangle; +import com.gregtechceu.gtceu.utils.GTMath; +import lombok.Getter; +import lombok.Setter; + +/** + * A rectangular widget area, composed of a position and a size. + * Also has fields for a relative position, a layer and margin & padding. + */ +public class Area extends Rectangle implements IUnResizeable { + + public static boolean isInside(int x, int y, int w, int h, int px, int py) { + SHARED.set(x, y, w, h); + return SHARED.isInside(px, py); + } + + public static final Area SHARED = new Area(); + + public static final Area ZERO = new Area(); + + /** + * relative position (in most cases the direct parent) + */ + public int rx, ry; + /** + * each panel has its own layer + */ + @Getter + @Setter + private byte panelLayer = 0; + /** + * the widget layer within this panel + */ + private int z; + @Getter + private final Box margin = new Box(); + @Getter + private final Box padding = new Box(); + + public Area() { + super(); + } + + public Area(int x, int y, int w, int h) { + super(x, y, w, h); + } + + public Area(Rectangle rectangle) { + super(rectangle); + } + + public int x() { + return this.x; + } + + public void x(int x) { + this.x = x; + } + + public int y() { + return this.y; + } + + public void y(int y) { + this.y = y; + } + + public int w() { + return this.width; + } + + public void w(int w) { + this.width = w; + } + + public int h() { + return this.height; + } + + public void h(int h) { + this.height = h; + } + + public int ex() { + return this.x + this.width; + } + + public void ex(int ex) { + this.x = ex - this.width; + } + + public int ey() { + return this.y + this.height; + } + + public void ey(int ey) { + this.y = ey - this.width; + } + + public int mx() { + return (int) (this.x + this.width * 0.5); + } + + public int my() { + return (int) (this.y + this.height * 0.5); + } + + public int z() { + return this.z; + } + + public void z(int z) { + this.z = z; + } + + /** + * Calculate X based on anchor value + */ + public int x(float anchor) { + return this.x + (int) (this.width * anchor); + } + + /** + * Calculate Y based on anchor value + */ + public int y(float anchor) { + return this.y + (int) (this.height * anchor); + } + + public int getPoint(GuiAxis axis) { + return axis.isHorizontal() ? this.x : this.y; + } + + public int getEndPoint(GuiAxis axis) { + return axis.isHorizontal() ? this.x + this.width : this.y + this.height; + } + + public int getSize(GuiAxis axis) { + return axis.isHorizontal() ? this.width : this.height; + } + + public int getRelativePoint(GuiAxis axis) { + return axis.isHorizontal() ? this.rx : this.ry; + } + + public void setPoint(GuiAxis axis, int v) { + if (axis.isHorizontal()) { + this.x = v; + } else { + this.y = v; + } + } + + public void setSize(GuiAxis axis, int v) { + if (axis.isHorizontal()) { + this.width = v; + } else { + this.height = v; + } + } + + public void setRelativePoint(GuiAxis axis, int v) { + if (axis.isHorizontal()) { + this.rx = v; + } else { + this.ry = v; + } + } + + void applyPos(int parentX, int parentY) { + this.x = parentX + this.rx; + this.y = parentY + this.ry; + } + + public int requestedWidth() { + return this.width + this.margin.horizontal(); + } + + public int paddedWidth() { + return this.width - this.padding.horizontal(); + } + + public int requestedHeight() { + return this.height + this.margin.vertical(); + } + + public int paddedHeight() { + return this.height - this.padding.vertical(); + } + + public int requestedSize(GuiAxis axis) { + return axis.isHorizontal() ? requestedWidth() : requestedHeight(); + } + + public int relativeEndX() { + return this.rx + this.width; + } + + public int relativeEndY() { + return this.ry + this.height; + } + + /** + * Check whether given position is inside the rect. + * Use {@link com.gregtechceu.gtceu.api.mui.base.widget.IWidget#isInside(IViewportStack, int, int)} rather than + * this! + */ + public boolean isInside(int x, int y) { + return x >= this.x && x < this.x + this.width && y >= this.y && y < this.y + this.height; + } + + /** + * Check whether given point is inside the rect. + * Use {@link com.gregtechceu.gtceu.api.mui.base.widget.IWidget#isInside(IViewportStack, Point)} rather than + * this! + */ + public boolean isInside(Point point) { + return isInside(point.x, point.y); + } + + /** + * Check whether given rect intersects this rect + */ + public boolean intersects(Rectangle area) { + return this.x < area.getX() + area.getWidth() && this.y < area.getY() + area.getHeight() && + area.getX() < this.x + this.width && area.getY() < this.y + this.height; + } + + /** + * Clamp given area inside of this one + */ + public void clamp(Area area) { + int x1 = area.x(); + int y1 = area.y(); + int x2 = area.ex(); + int y2 = area.ey(); + + x1 = GTMath.clamp(x1, this.x, this.ex()); + y1 = GTMath.clamp(y1, this.y, this.ey()); + x2 = GTMath.clamp(x2, this.x, this.ex()); + y2 = GTMath.clamp(y2, this.y, this.ey()); + + area.setPos(x1, y1, x2, y2); + } + + /** + * Increases or decreases the size of this area. The position will change so that the center of the new + * area is in the same place. + * The size will change with double of the given value. The position will change with the negative of the given + * value. + *
+ * In short, it will push or pull all four edges by the given amount. + * + * @param expand amount to expand area by (no restrictions) + */ + public void expand(int expand) { + this.expandX(expand); + this.expandY(expand); + } + + /** + * Increases or decreases the size of this area. The position will change so that the center of the new + * area is in the same place. + * The size will change with double of the given value. The position will change with the negative of the given + * value. + *
+ * In short, it will push or pull all four edges by the given amount. + * + * @param expandX amount to expand x-axis by (no restrictions) + * @param expandY amount to expand y-axis by (no restrictions) + */ + public void expand(int expandX, int expandY) { + this.expandX(expandX); + this.expandY(expandY); + } + + /** + * Increases or decreases the width of this area. The x position will change so that the center of the new + * area is in the same place. + * The width will change with double of the given value. The x position will change with the negative of the given + * value. + *
+ * In short, it will push or pull the left and right edges by the given amount. + * + * @param expand amount to expand x-axis by (no restrictions) + */ + public void expandX(int expand) { + offsetX(-expand); + growW(expand * 2); + } + + /** + * Increases or decreases the height of this area. The y position will change so that the center of the new + * area is in the same place. + * The height will change with double of the given value. The y position will change with the negative of the given + * value. + *
+ * In short, it will push or pull the top and bottom edges by the given amount. + * + * @param expand amount to expand y-axis by (no restrictions) + */ + public void expandY(int expand) { + offsetY(-expand); + growH(expand * 2); + } + + /** + * Increases or decreases the position of the area by the given amount, but doesn't change its size. + * + * @param offset amount to change position by (no restrictions) + */ + public void offset(int offset) { + offsetX(offset); + offsetY(offset); + } + + /** + * Increases or decreases the position of the area by the given amount, but doesn't change its size. + * + * @param offsetX amount to change x position by (no restrictions) + * @param offsetY amount to change y position by (no restrictions) + */ + public void offset(int offsetX, int offsetY) { + offsetX(offsetX); + offsetY(offsetY); + } + + /** + * Increases or decreases the x position of the area by the given amount, but doesn't change its size. + * + * @param offset amount to change x position by (no restrictions) + */ + public void offsetX(int offset) { + this.x += offset; + } + + /** + * Increases or decreases the y position of the area by the given amount, but doesn't change its size. + * + * @param offset amount to change y position by (no restrictions) + */ + public void offsetY(int offset) { + this.y += offset; + } + + /** + * Increases or decreases the size of the area by the given amount, but doesn't change its position. + * + * @param grow amount to change size by (no restrictions) + */ + public void grow(int grow) { + growW(grow); + growH(grow); + } + + /** + * Increases or decreases the size of the area by the given amount, but doesn't change its position. + * + * @param growW amount to change width by (no restrictions) + * @param growH amount to change height by (no restrictions) + */ + public void grow(int growW, int growH) { + growW(growW); + growH(growH); + } + + /** + * Increases or decreases the width of the area by the given amount, but doesn't change its position. + * + * @param grow amount to change width by (no restrictions) + */ + public void growW(int grow) { + this.width += grow; + } + + /** + * Increases or decreases the height of the area by the given amount, but doesn't change its position. + * + * @param grow amount to change height by (no restrictions) + */ + public void growH(int grow) { + this.height += grow; + } + + /** + * Set all values + */ + public void set(int x, int y, int w, int h) { + this.setPos(x, y); + this.setSize(w, h); + } + + /** + * Set the relative position + */ + public void setRelativePos(int rx, int ry) { + this.rx = rx; + this.ry = ry; + } + + /** + * Set the position + */ + public void setPos(int x, int y) { + this.x = x; + this.y = y; + } + + /** + * Set the size + */ + public void setSize(int w, int h) { + this.width = w; + this.height = h; + } + + public void setPos(Rectangle rectangle) { + setPos(rectangle.x, rectangle.y); + } + + public void setSize(Rectangle rectangle) { + setSize(rectangle.width, rectangle.height); + } + + /** + * Sets position and size by specifying top left and bottom right corner position. + * + * @param sx x position of top left corner + * @param sy y position of top left corner + * @param ex x position of bottom right corner + * @param ey y position of bottom right corner + */ + public void setPos(int sx, int sy, int ex, int ey) { + int x0 = Math.min(sx, ex); + int y0 = Math.min(sy, ey); + ex = Math.max(sx, ex); + ey = Math.max(sy, ey); + setPos(x0, y0); + setSize(ex - x0, ey - y0); + } + + public void reset() { + this.x = 0; + this.y = 0; + this.width = 0; + this.height = 0; + } + + public void set(Rectangle area) { + setBounds(area.x, area.y, area.width, area.height); + } + + /** + * Transforms the four corners of this rectangle with the given pose stack. The new rectangle can be rotated. + * Then a min fit rectangle, which is not rotated and aligned with the screen, is put around the corner. + * + * @param stack pose stack + */ + public void transformAndRectanglerize(IViewportStack stack) { + int xTL = stack.transformX(this.x, this.y), xTR = stack.transformX(ex(), this.y), + xBL = stack.transformX(this.x, ey()), xBR = stack.transformX(ex(), ey()); + int yTL = stack.transformY(this.x, this.y), yTR = stack.transformY(ex(), this.y), + yBL = stack.transformY(this.x, ey()), yBR = stack.transformY(ex(), ey()); + int x0 = GTMath.min(xTL, xTR, xBL, xBR); + int x1 = GTMath.max(xTL, xTR, xBL, xBR); + int y0 = GTMath.min(yTL, yTR, yBL, yBR); + int y1 = GTMath.max(yTL, yTR, yBL, yBR); + setPos(x0, y0, x1, y1); + } + + @Override + public boolean resize(IGuiElement guiElement) { + guiElement.getArea().set(this); + return true; + } + + @Override + public Area getArea() { + return this; + } + + /** + * This creates a copy, but it only copies position and size. + * + * @return copy + */ + public Area createCopy() { + return new Area(this); + } + + @Override + public String toString() { + return "Area{" + + "x=" + this.x + + ", y=" + this.y + + ", width=" + this.width + + ", height=" + this.height + + '}'; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Box.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Box.java new file mode 100644 index 00000000000..0d751c7175d --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Box.java @@ -0,0 +1,89 @@ +package com.gregtechceu.gtceu.api.mui.widget.sizer; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; + +/** + * A box with four edges. + * Used for margins and paddings. + */ +public class Box { + + public static final Box SHARED = new Box(); + + public static final Box ZERO = new Box(); + + public int left; + public int top; + public int right; + public int bottom; + + public Box all(int all) { + return this.all(all, all); + } + + public Box all(int horizontal, int vertical) { + return this.all(horizontal, horizontal, vertical, vertical); + } + + public Box all(int left, int right, int top, int bottom) { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + return this; + } + + public Box left(int left) { + this.left = left; + return this; + } + + public Box top(int top) { + this.top = top; + return this; + } + + public Box right(int right) { + this.right = right; + return this; + } + + public Box bottom(int bottom) { + this.bottom = bottom; + return this; + } + + public Box set(Box box) { + return all(box.left, box.right, box.top, box.bottom); + } + + public int vertical() { + return this.top + this.bottom; + } + + public int horizontal() { + return this.left + this.right; + } + + public int getTotal(GuiAxis axis) { + return axis.isHorizontal() ? horizontal() : vertical(); + } + + public int getStart(GuiAxis axis) { + return axis.isHorizontal() ? this.left : this.top; + } + + public int getEnd(GuiAxis axis) { + return axis.isHorizontal() ? this.right : this.bottom; + } + + @Override + public String toString() { + return "Box{" + + "left=" + left + + ", top=" + top + + ", right=" + right + + ", bottom=" + bottom + + '}'; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/DimensionSizer.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/DimensionSizer.java new file mode 100644 index 00000000000..f739ae815b6 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/DimensionSizer.java @@ -0,0 +1,342 @@ +package com.gregtechceu.gtceu.api.mui.widget.sizer; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.layout.IResizeable; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; +import com.gregtechceu.gtceu.config.ConfigHolder; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.IntSupplier; + +/** + * Handles calculating size and position in one dimension (x or y). + * Two of these can fully calculate a widget size and pos. + */ +@ApiStatus.Internal +public class DimensionSizer { + + private final GuiAxis axis; + + private final Unit p1 = new Unit(), p2 = new Unit(); + private Unit start, end, size; + private Unit next = p1; + + @Setter + private boolean coverChildren = false, expanded = false; + @Setter + private boolean cancelAutoMovement = false; + + @Getter + private boolean posCalculated = false, sizeCalculated = false; + @Getter + @Setter + private boolean marginPaddingApplied = false; + + public DimensionSizer(GuiAxis axis) { + this.axis = axis; + } + + public void reset() { + this.p1.reset(); + this.p2.reset(); + this.start = null; + this.end = null; + this.size = null; + this.next = this.p1; + } + + public void resetPosition() { + if (this.start != null) { + this.start.reset(); + this.start = null; + } + if (this.end != null) { + this.end.reset(); + this.end = null; + } + if (this.p1.isUnused()) { + this.next = this.p1; + } else if (this.p2.isUnused()) { + this.next = this.p2; + } + } + + public void resetSize() { + if (this.size != null) { + this.size.reset(); + this.size = null; + } + if (this.p1.isUnused()) { + this.next = this.p1; + } else if (this.p2.isUnused()) { + this.next = this.p2; + } + } + + public void setCoverChildren(boolean coverChildren, IGuiElement widget) { + getSize(widget); + this.coverChildren = coverChildren; + } + + public boolean hasStart() { + return this.start != null; + } + + public boolean hasEnd() { + return this.end != null; + } + + public boolean hasPos() { + return this.start != null || this.end != null; + } + + public boolean hasFixedSize() { + return this.start == null || this.end == null; + } + + public boolean hasSize() { + return this.size != null; + } + + public boolean dependsOnChildren() { + return this.coverChildren; + } + + public boolean dependsOnParent() { + return this.end != null || + (this.start != null && this.start.isRelative()) || + (this.size != null && this.size.isRelative()); + } + + public void setResized(boolean all) { + setResized(all, all); + } + + public void setResized(boolean pos, boolean size) { + this.posCalculated = pos; + this.sizeCalculated = size; + } + + private boolean needsSize(Unit unit) { + return unit.isRelative() && unit.getAnchor() != 0; + } + + public void apply(Area area, IResizeable relativeTo, IntSupplier defaultSize) { + // is already calculated + if (this.sizeCalculated && this.posCalculated) return; + int p, s; + int parentSize = relativeTo.getArea().getSize(this.axis); + boolean calcParent = relativeTo.isSizeCalculated(this.axis); + + if (this.sizeCalculated && !this.posCalculated) { + // size was calculated before + s = area.getSize(this.axis); + if (this.start != null) { + p = calcPoint(this.start, s, parentSize, calcParent); + } else if (this.end != null) { + p = calcPoint(this.end, s, parentSize, calcParent) - s; + } else { + throw new IllegalStateException(); + } + } else if (!this.sizeCalculated && this.posCalculated) { + // pos was calculated before + p = area.getRelativePoint(this.axis); + if (this.size != null) { + s = this.coverChildren ? 18 : calcSize(this.size, parentSize, calcParent); + } else { + s = defaultSize.getAsInt(); + this.sizeCalculated = s > 0; + } + } else { + // calc start, end and size + if (this.start == null && this.end == null) { + p = 0; + if (this.size == null) { + s = defaultSize.getAsInt(); + this.sizeCalculated = s > 0 && !this.expanded; + } else { + s = calcSize(this.size, parentSize, calcParent); + } + this.posCalculated = true; + } else { + if (this.size == null) { + if (this.start != null && this.end != null) { + p = calcPoint(this.start, -1, parentSize, calcParent); + boolean b = this.posCalculated; + this.posCalculated = false; + int p2 = calcPoint(this.end, -1, parentSize, calcParent); + s = Math.abs(p2 - p); + this.posCalculated &= b; + this.sizeCalculated |= this.posCalculated; + } else { + s = defaultSize.getAsInt(); + this.sizeCalculated = s > 0 && !this.expanded; + if (this.start == null) { + p = calcPoint(this.end, s, parentSize, calcParent); + p -= s; + this.posCalculated &= this.sizeCalculated; + } else { + p = calcPoint(this.start, s, parentSize, calcParent); + this.posCalculated &= (this.sizeCalculated || !needsSize(this.start)); + } + } + } else if (this.start != null) { + s = calcSize(this.size, parentSize, calcParent); + p = calcPoint(this.start, s, parentSize, calcParent); + this.posCalculated &= (this.sizeCalculated || !needsSize(this.start)); + } else { + s = calcSize(this.size, parentSize, calcParent); + p = calcPoint(this.end, s, parentSize, calcParent) - s; + this.posCalculated &= this.sizeCalculated; + } + } + } + + // apply padding and margin to size + if (this.sizeCalculated && calcParent && ((this.size != null && this.size.isRelative()) || + (this.start != null && this.end != null && (this.start.isRelative() || this.end.isRelative())))) { + Box padding = relativeTo.getArea().getPadding(); + Box margin = area.getMargin(); + s = Math.min(s, parentSize - padding.getTotal(this.axis) - margin.getTotal(this.axis)); + } + area.setRelativePoint(this.axis, p); + area.setPoint(this.axis, p + relativeTo.getArea().x); // temporary + area.setSize(this.axis, s); + } + + public int postApply(Area area, Area relativeTo, int p0, int p1) { + // only called when the widget cover its children + int moveAmount = 0; + // calculate width and recalculate x based on the new width + int s = p1 - p0, p; + area.setSize(this.axis, s); + this.sizeCalculated = true; + if (!isPosCalculated()) { + if (this.start != null) { + p = calcPoint(this.start, s, relativeTo.getSize(this.axis), true); + } else if (this.end != null) { + p = calcPoint(this.end, s, relativeTo.getSize(this.axis), true) - s; + } else { + p = area.getRelativePoint(this.axis) + p0/* + area.getMargin().getStart(this.axis) */; + if (!this.cancelAutoMovement) { + moveAmount = -p0; + } + } + area.setRelativePoint(this.axis, p); + this.posCalculated = true; + } + return moveAmount; + } + + public void coverChildrenForEmpty(Area area, Area relativeTo) { + int s = 0; + area.setSize(this.axis, s); + this.sizeCalculated = true; + if (!isPosCalculated()) { + int p; + if (this.start != null) { + p = calcPoint(this.start, s, relativeTo.getSize(this.axis), true); + } else if (this.end != null) { + p = calcPoint(this.end, s, relativeTo.getSize(this.axis), true) - s; + } else { + p = area.getRelativePoint(this.axis); + } + area.setRelativePoint(this.axis, p); + this.posCalculated = true; + } + } + + public void applyMarginAndPaddingToPos(Area area, Area relativeTo) { + // apply self margin and parent padding if not done yet + if (isMarginPaddingApplied()) return; + setMarginPaddingApplied(true); + int o = area.getMargin().getStart(this.axis) + relativeTo.getPadding().getStart(this.axis); + if (o == 0) return; + if (this.start != null && !this.start.isRelative()) return; + if (this.end != null && !this.end.isRelative() && (this.size == null || !this.size.isRelative())) return; + area.setRelativePoint(this.axis, area.getRelativePoint(this.axis) + o); + } + + private int calcSize(Unit s, int parentSize, boolean parentSizeCalculated) { + if (this.coverChildren) return 18; + float val = s.getValue(); + if (s.isRelative()) { + if (!parentSizeCalculated) return (int) val; + val *= parentSize; + } + this.sizeCalculated = true; + return (int) val; + } + + public int calcPoint(Unit p, int width, int parentSize, boolean parentSizeCalculated) { + float val = p.getValue(); + if (!parentSizeCalculated && (p == this.end || p.isRelative())) return (int) val; + if (p.isRelative()) { + val = parentSize * val; + float anchor = p.getAnchor(); + if (width > 0 && anchor != 0) { + val -= width * anchor; + } + if (p.getOffset() != 0) { + val += p.getOffset(); + } + } + if (p == this.end) { + val = parentSize - val; + } + this.posCalculated = true; + return (int) val; + } + + /** + * Tries to find a unit for start, end or size. If p1 and p2 are already used, the first one will be overwritten. + * + * @param widget widget this sizer belongs to. Used for logging + * @param newState the new unit type for the found unit + * @return a used or unused unit. + */ + private Unit getNext(IGuiElement widget, Unit.State newState) { + Unit ret = this.next; + Unit other = ret == this.p1 ? this.p2 : this.p1; + if (ret.state != Unit.State.UNUSED) { + if (ret.state == newState) return ret; + if (other.state == newState) return other; + if (ret == this.start) this.start = null; + if (ret == this.end) this.end = null; + if (ret == this.size) this.size = null; + if (ConfigHolder.INSTANCE.dev.debugUI && GTCEu.isClientThread()) { + // only log on client in debug mode since its sometimes intentional + GTCEu.LOGGER.info("unit {} of widget {} was already used and will be overwritten with unit {}", + ret.state.getText(this.axis), widget, newState.getText(this.axis)); + } + } + ret.reset(); + ret.state = newState; + this.next = other; + return ret; + } + + protected Unit getStart(IGuiElement widget) { + if (this.start == null) { + this.start = getNext(widget, Unit.State.START); + } + return this.start; + } + + protected Unit getEnd(IGuiElement widget) { + if (this.end == null) { + this.end = getNext(widget, Unit.State.END); + } + return this.end; + } + + protected Unit getSize(IGuiElement widget) { + if (this.size == null) { + this.size = getNext(widget, Unit.State.SIZE); + } + return this.size; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Flex.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Flex.java new file mode 100644 index 00000000000..291aa427c1c --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Flex.java @@ -0,0 +1,515 @@ +package com.gregtechceu.gtceu.api.mui.widget.sizer; + +import com.gregtechceu.gtceu.api.mui.GuiError; +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.layout.ILayoutWidget; +import com.gregtechceu.gtceu.api.mui.base.layout.IResizeable; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; +import com.gregtechceu.gtceu.api.mui.base.widget.IPositioned; +import com.gregtechceu.gtceu.api.mui.base.widget.IVanillaSlot; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.core.mixins.client.SlotAccessor; +import lombok.Getter; +import org.jetbrains.annotations.ApiStatus; + +import java.util.List; +import java.util.function.DoubleSupplier; + +/** + * This class handles resizing and positioning of widgets. + */ +public class Flex implements IResizeable, IPositioned { + + private final DimensionSizer x = new DimensionSizer(GuiAxis.X); + private final DimensionSizer y = new DimensionSizer(GuiAxis.Y); + @Getter + private boolean expanded = false; + private final IGuiElement parent; + private Area relativeTo; + private boolean relativeToParent = true; + + public Flex(IGuiElement parent) { + this.parent = parent; + } + + public void reset() { + this.x.reset(); + this.y.reset(); + } + + public void resetPosition() { + this.x.resetPosition(); + this.y.resetPosition(); + } + + @Override + public Flex flex() { + return this; + } + + @Override + public Area getArea() { + return this.parent.getArea(); + } + + @Override + public boolean isXCalculated() { + return this.x.isPosCalculated(); + } + + @Override + public boolean isYCalculated() { + return this.y.isPosCalculated(); + } + + @Override + public boolean isWidthCalculated() { + return this.x.isSizeCalculated(); + } + + @Override + public boolean isHeightCalculated() { + return this.y.isSizeCalculated(); + } + + public Flex coverChildrenWidth() { + this.x.setCoverChildren(true, this.parent); + return this; + } + + public Flex coverChildrenHeight() { + this.y.setCoverChildren(true, this.parent); + return this; + } + + public Flex cancelMovementX() { + this.x.setCancelAutoMovement(true); + return this; + } + + public Flex cancelMovementY() { + this.y.setCancelAutoMovement(true); + return this; + } + + public Flex expanded() { + this.expanded = true; + return this; + } + + public Flex relative(Area guiElement) { + this.relativeTo = guiElement; + this.relativeToParent = false; + return this; + } + + public Flex relativeToScreen() { + this.relativeTo = null; + this.relativeToParent = false; + return this; + } + + public Flex relativeToParent() { + this.relativeToParent = true; + return this; + } + + @ApiStatus.Internal + public Flex left(float x, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getLeft(), x, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public Flex left(DoubleSupplier x, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getLeft(), x, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public Flex right(float x, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getRight(), x, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public Flex right(DoubleSupplier x, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getRight(), x, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public Flex top(float y, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getTop(), y, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public Flex top(DoubleSupplier y, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getTop(), y, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public Flex bottom(float y, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getBottom(), y, offset, anchor, measure, autoAnchor); + } + + @ApiStatus.Internal + public Flex bottom(DoubleSupplier y, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + return unit(getBottom(), y, offset, anchor, measure, autoAnchor); + } + + private Flex unit(Unit u, float val, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + u.setValue(val); + u.setMeasure(measure); + u.setOffset(offset); + u.setAnchor(anchor); + u.setAutoAnchor(autoAnchor); + return this; + } + + private Flex unit(Unit u, DoubleSupplier val, int offset, float anchor, Unit.Measure measure, boolean autoAnchor) { + u.setValue(val); + u.setMeasure(measure); + u.setOffset(offset); + u.setAnchor(anchor); + u.setAutoAnchor(autoAnchor); + return this; + } + + public Flex width(float w, Unit.Measure measure) { + return unitSize(getWidth(), w, measure); + } + + public Flex width(DoubleSupplier w, Unit.Measure measure) { + return unitSize(getWidth(), w, measure); + } + + public Flex height(float h, Unit.Measure measure) { + return unitSize(getHeight(), h, measure); + } + + public Flex height(DoubleSupplier h, Unit.Measure measure) { + return unitSize(getHeight(), h, measure); + } + + private Flex unitSize(Unit u, float val, Unit.Measure measure) { + u.setValue(val); + u.setMeasure(measure); + return this; + } + + private Flex unitSize(Unit u, DoubleSupplier val, Unit.Measure measure) { + u.setValue(val); + u.setMeasure(measure); + return this; + } + + public Flex anchorLeft(float val) { + getLeft().setAnchor(val); + getLeft().setAutoAnchor(false); + return this; + } + + public Flex anchorRight(float val) { + getRight().setAnchor(1 - val); + getRight().setAutoAnchor(false); + return this; + } + + public Flex anchorTop(float val) { + getTop().setAnchor(val); + getTop().setAutoAnchor(false); + return this; + } + + public Flex anchorBottom(float val) { + getBottom().setAnchor(1 - val); + getBottom().setAutoAnchor(false); + return this; + } + + public Flex anchor(Alignment alignment) { + if (this.x.hasStart() || !this.x.hasEnd()) { + anchorLeft(alignment.x); + } else if (this.x.hasEnd()) { + anchorRight(alignment.x); + } + if (this.y.hasStart() || !this.y.hasEnd()) { + anchorTop(alignment.y); + } else if (this.y.hasEnd()) { + anchorBottom(alignment.y); + } + return this; + } + + private IResizeable getRelativeTo() { + IGuiElement parent = this.parent.getParent(); + IResizeable relativeTo = this.relativeToParent && parent != null ? parent.resizer() : this.relativeTo; + return relativeTo != null ? relativeTo : this.parent.getScreen().getScreenArea(); + } + + public boolean hasYPos() { + return this.y.hasPos(); + } + + public boolean hasXPos() { + return this.x.hasPos(); + } + + public boolean hasHeight() { + return this.y.hasSize(); + } + + public boolean hasWidth() { + return this.x.hasSize(); + } + + public boolean hasPos(GuiAxis axis) { + return axis.isHorizontal() ? hasXPos() : hasYPos(); + } + + public boolean hasSize(GuiAxis axis) { + return axis.isHorizontal() ? hasWidth() : hasHeight(); + } + + public boolean xAxisDependsOnChildren() { + return this.x.dependsOnChildren(); + } + + public boolean yAxisDependsOnChildren() { + return this.y.dependsOnChildren(); + } + + public boolean dependsOnChildren() { + return xAxisDependsOnChildren() || yAxisDependsOnChildren(); + } + + public boolean dependsOnChildren(GuiAxis axis) { + return axis.isHorizontal() ? xAxisDependsOnChildren() : yAxisDependsOnChildren(); + } + + public boolean hasFixedSize() { + return this.x.hasFixedSize() && this.y.hasFixedSize(); + } + + @ApiStatus.Internal + public void checkExpanded(GuiAxis axis) { + if (axis.isHorizontal()) this.x.setExpanded(this.expanded); + else this.y.setExpanded(this.expanded); + } + + @Override + public void initResizing() { + setMarginPaddingApplied(false); + setResized(false); + } + + @Override + public void setResized(boolean x, boolean y, boolean w, boolean h) { + this.x.setResized(x, w); + this.y.setResized(y, h); + } + + @Override + public void setXMarginPaddingApplied(boolean b) { + this.x.setMarginPaddingApplied(b); + } + + @Override + public void setYMarginPaddingApplied(boolean b) { + this.y.setMarginPaddingApplied(b); + } + + @Override + public boolean isXMarginPaddingApplied() { + return this.x.isMarginPaddingApplied(); + } + + @Override + public boolean isYMarginPaddingApplied() { + return this.y.isMarginPaddingApplied(); + } + + @Override + public boolean resize(IGuiElement guiElement) { + IResizeable relativeTo = getRelativeTo(); + Area relativeArea = relativeTo.getArea(); + byte panelLayer = this.parent.getArea().getPanelLayer(); + + if (relativeArea.getPanelLayer() > panelLayer || + (relativeArea.getPanelLayer() == panelLayer && relativeArea.z() >= this.parent.getArea().z())) { + GuiError.throwNew(this.parent, GuiError.Type.SIZING, + "Widget can't be relative to a widget at the same level or above"); + return true; + } + + // calculate x, y, width and height if possible + this.x.apply(guiElement.getArea(), relativeTo, guiElement::getDefaultWidth); + this.y.apply(guiElement.getArea(), relativeTo, guiElement::getDefaultHeight); + return isFullyCalculated(); + } + + @Override + public boolean postResize(IGuiElement guiElement) { + if (!this.x.dependsOnChildren() && !this.y.dependsOnChildren()) return isFullyCalculated(); + if (!(this.parent instanceof IWidget widget) || !widget.hasChildren()) { + coverChildrenForEmpty(); + return isFullyCalculated(); + } + if (this.parent instanceof ILayoutWidget) { + // layout widgets handle widget layout's themselves, so we only need to fit the right and bottom border + coverChildrenForLayout(widget); + return isFullyCalculated(); + } + // non layout widgets can have their children in any position + // we try to wrap all edges as close as possible to all widgets + // this means for each edge there is at least one widget that touches it (plus padding and margin) + + // children are now calculated and now this area can be calculated if it requires children's area + List children = widget.getChildren(); + int moveChildrenX = 0, moveChildrenY = 0; + + Box padding = this.parent.getArea().getPadding(); + // first calculate the area the children span + int x0 = Integer.MAX_VALUE, x1 = Integer.MIN_VALUE, y0 = Integer.MAX_VALUE, y1 = Integer.MIN_VALUE; + int w = 0, h = 0; + for (IWidget child : children) { + Box margin = child.getArea().getMargin(); + IResizeable resizeable = child.resizer(); + Area area = child.getArea(); + if (this.x.dependsOnChildren() && resizeable.isWidthCalculated()) { + // minimum width this widget requests + w = Math.max(w, area.requestedWidth() + padding.horizontal()); + if (resizeable.isXCalculated()) { + // if pos is calculated use that + x0 = Math.min(x0, area.rx - padding.left - margin.left); + x1 = Math.max(x1, area.rx + area.width + padding.right + margin.right); + } + } + if (this.y.dependsOnChildren() && resizeable.isHeightCalculated()) { + h = Math.max(h, area.requestedHeight() + padding.vertical()); + if (resizeable.isYCalculated()) { + y0 = Math.min(y0, area.ry - padding.top - margin.top); + y1 = Math.max(y1, area.ry + area.height + padding.bottom + margin.bottom); + } + } + } + if (x1 == Integer.MIN_VALUE) x1 = 0; + if (y1 == Integer.MIN_VALUE) y1 = 0; + if (x0 == Integer.MAX_VALUE) x0 = 0; + if (y0 == Integer.MAX_VALUE) y0 = 0; + if (w > x1 - x0) + x1 = x0 + w; // we found at least one widget which was wider than what was calculated by start and end pos + if (h > y1 - y0) y1 = y0 + h; + + // now calculate new x, y, width and height based on the children's area + Area relativeTo = getRelativeTo().getArea(); + if (this.x.dependsOnChildren()) { + // apply the size to this widget + // the return value is the amount of pixels we need to move the children + moveChildrenX = this.x.postApply(this.parent.getArea(), relativeTo, x0, x1); + } + if (this.y.dependsOnChildren()) { + moveChildrenY = this.y.postApply(this.parent.getArea(), relativeTo, y0, y1); + } + // since the edges might have been moved closer to the widgets, the widgets should move back into it's original + // (absolute) position + if (moveChildrenX != 0 || moveChildrenY != 0) { + for (IWidget child : children) { + Area area = child.getArea(); + IResizeable resizeable = child.resizer(); + if (resizeable.isXCalculated()) area.rx += moveChildrenX; + if (resizeable.isYCalculated()) area.ry += moveChildrenY; + } + } + return isFullyCalculated(); + } + + private void coverChildrenForLayout(IWidget widget) { + List children = widget.getChildren(); + Box padding = this.parent.getArea().getPadding(); + // first calculate the area the children span + int x1 = Integer.MIN_VALUE, y1 = Integer.MIN_VALUE; + int w = 0, h = 0; + for (IWidget child : children) { + final boolean shouldIgnoreChildSize = ((ILayoutWidget) this.parent).shouldIgnoreChildSize(child); + Box margin = shouldIgnoreChildSize ? Box.ZERO : child.getArea().getMargin(); + IResizeable resizeable = child.resizer(); + Area area = shouldIgnoreChildSize ? Area.ZERO : child.getArea(); + if (this.x.dependsOnChildren() && resizeable.isWidthCalculated()) { + w = Math.max(w, area.requestedWidth() + padding.horizontal()); + if (resizeable.isXCalculated()) { + x1 = Math.max(x1, area.rx + area.width + padding.right + margin.right); + } + } + if (this.y.dependsOnChildren() && resizeable.isHeightCalculated()) { + h = Math.max(h, area.requestedHeight() + padding.vertical()); + if (resizeable.isYCalculated()) { + y1 = Math.max(y1, area.ry + area.height + padding.bottom + margin.bottom); + } + } + } + if (x1 == Integer.MIN_VALUE) x1 = 0; + if (y1 == Integer.MIN_VALUE) y1 = 0; + if (w > x1) x1 = w; + if (h > y1) y1 = h; + + Area relativeTo = getRelativeTo().getArea(); + if (this.x.dependsOnChildren()) { + this.x.postApply(getArea(), relativeTo, 0, x1); + } + if (this.y.dependsOnChildren()) { + this.y.postApply(getArea(), relativeTo, 0, y1); + } + } + + private void coverChildrenForEmpty() { + if (this.x.dependsOnChildren()) { + this.x.coverChildrenForEmpty(this.parent.getArea(), getRelativeTo().getArea()); + } + if (this.y.dependsOnChildren()) { + this.y.coverChildrenForEmpty(this.parent.getArea(), getRelativeTo().getArea()); + } + } + + @Override + public void applyPos(IGuiElement parent) { + Area relativeTo = getRelativeTo().getArea(); + Area area = parent.getArea(); + // apply margin and padding if not done yet + this.x.applyMarginAndPaddingToPos(area, relativeTo); + this.y.applyMarginAndPaddingToPos(area, relativeTo); + // after all widgets x, y, width and height have been calculated we can now calculate the absolute position + area.applyPos(relativeTo.x, relativeTo.y); + Area parentArea = parent.getParentArea(); + area.rx = area.x - parentArea.x; + area.ry = area.y - parentArea.y; + if (parent instanceof IVanillaSlot vanillaSlot) { + // special treatment for minecraft slots + SlotAccessor slot = (SlotAccessor) vanillaSlot.getVanillaSlot(); + slot.gtceu$setX(parent.getArea().x); + slot.gtceu$setY(parent.getArea().y); + } + } + + private Unit getLeft() { + return this.x.getStart(this.parent); + } + + private Unit getRight() { + return this.x.getEnd(this.parent); + } + + private Unit getTop() { + return this.y.getStart(this.parent); + } + + private Unit getBottom() { + return this.y.getEnd(this.parent); + } + + private Unit getWidth() { + return this.x.getSize(this.parent); + } + + private Unit getHeight() { + return this.y.getSize(this.parent); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/IUnResizeable.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/IUnResizeable.java new file mode 100644 index 00000000000..32ef0ad379e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/IUnResizeable.java @@ -0,0 +1,71 @@ +package com.gregtechceu.gtceu.api.mui.widget.sizer; + +import com.gregtechceu.gtceu.api.mui.base.layout.IResizeable; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; + +/** + * A variation of {@link IResizeable} with default implementations which don't do anything + */ +public interface IUnResizeable extends IResizeable { + + IUnResizeable INSTANCE = new IUnResizeable() { + + @Override + public boolean resize(IGuiElement guiElement) { + return true; + } + + @Override + public Area getArea() { + Area.SHARED.set(0, 0, 0, 0); + return Area.SHARED; + } + }; + + @Override + default void initResizing() {} + + @Override + default boolean postResize(IGuiElement guiElement) { + return true; + } + + @Override + default boolean isXCalculated() { + return true; + } + + @Override + default boolean isYCalculated() { + return true; + } + + @Override + default boolean isWidthCalculated() { + return true; + } + + @Override + default boolean isHeightCalculated() { + return true; + } + + @Override + default void setResized(boolean x, boolean y, boolean w, boolean h) {} + + @Override + default void setXMarginPaddingApplied(boolean b) {} + + @Override + default void setYMarginPaddingApplied(boolean b) {} + + @Override + default boolean isXMarginPaddingApplied() { + return true; + } + + @Override + default boolean isYMarginPaddingApplied() { + return true; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Unit.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Unit.java new file mode 100644 index 00000000000..9934c8fd76a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/sizer/Unit.java @@ -0,0 +1,96 @@ +package com.gregtechceu.gtceu.api.mui.widget.sizer; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.DoubleSupplier; + +@ApiStatus.Internal +public class Unit { + + public enum State { + + UNUSED("", ""), + START("LEFT", "TOP"), + END("RIGHT", "BOTTOM"), + SIZE("WIDTH", "HEIGHT"); + + public final String xText, yText; + + State(String xText, String yText) { + this.xText = xText; + this.yText = yText; + } + + public String getText(GuiAxis axis) { + return axis.isHorizontal() ? this.xText : this.yText; + } + } + + public static final byte UNUSED = -2; + public static final byte DEFAULT = -1; + public static final byte START = 0; + public static final byte END = 1; + public static final byte SIZE = 2; + + @Getter + @Setter + private boolean autoAnchor = true; + private float value = 0f; + private DoubleSupplier valueSupplier = null; + @Getter + @Setter + private Measure measure = Measure.PIXEL; + @Setter + private float anchor = 0f; + @Getter + @Setter + private int offset = 0; + + public State state = State.UNUSED; + + public Unit() {} + + public void reset() { + this.state = State.UNUSED; + this.autoAnchor = true; + this.value = 0f; + this.valueSupplier = null; + this.measure = Measure.PIXEL; + this.anchor = 0f; + this.offset = 0; + } + + public void setValue(float value) { + this.value = value; + this.valueSupplier = null; + } + + public void setValue(DoubleSupplier valueSupplier) { + this.valueSupplier = valueSupplier; + } + + public float getValue() { + return this.valueSupplier == null ? this.value : (float) this.valueSupplier.getAsDouble(); + } + + public float getAnchor() { + float val = getValue(); + return isAutoAnchor() && isRelative() && val < 1 ? val : this.anchor; + } + + public boolean isRelative() { + return this.measure == Measure.RELATIVE; + } + + public boolean isUnused() { + return this.state == State.UNUSED; + } + + public enum Measure { + PIXEL, + RELATIVE + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widget/wrapper/WidgetWrapper.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/wrapper/WidgetWrapper.java new file mode 100644 index 00000000000..cd1912858cd --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widget/wrapper/WidgetWrapper.java @@ -0,0 +1,125 @@ +package com.gregtechceu.gtceu.api.mui.widget.wrapper; + +import com.gregtechceu.gtceu.api.mui.base.widget.IFocusedWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.widget.EmptyWidget; +import com.gregtechceu.gtceu.client.mui.screen.ModularScreen; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.TabOrderedElement; +import net.minecraft.client.gui.narration.NarratableEntry; +import net.minecraft.client.gui.narration.NarratedElementType; +import net.minecraft.client.gui.narration.NarrationElementOutput; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@OnlyIn(Dist.CLIENT) +public class WidgetWrapper extends AbstractWidget { + + private NarratableEntry lastNarratable = null; + + private final IWidget wrapped; + private final List children = new ArrayList<>(); + private final ModularScreen screen; + + public WidgetWrapper(IWidget wrapped) { + super(0, 0, 0, 0, CommonComponents.EMPTY); + this.wrapped = wrapped; + this.screen = wrapped.getScreen(); + for (IWidget widget : this.wrapped.getChildren()) { + this.children.add(new WidgetWrapper(widget)); + } + } + + public int getX() { + return this.wrapped.getArea().getX(); + } + + public void setX(int x) { + this.wrapped.getArea().setX(x); + } + + public int getY() { + return this.wrapped.getArea().getY(); + } + + public void setY(int y) { + this.wrapped.getArea().setY(y); + } + + @Override + public void visitWidgets(@NotNull Consumer consumer) { + if (this.wrapped instanceof EmptyWidget) return; + if (!this.children.isEmpty()) { + for (WidgetWrapper child : this.children) { + child.visitWidgets(consumer); + } + } else { + super.visitWidgets(consumer); + } + } + + @Override + protected void renderWidget(@NotNull GuiGraphics graphics, int mouseX, int mouseY, float partialTick) { + ModularGuiContext context = this.screen.getContext(); + GuiGraphics lastGraphics = context.getGraphics(); + context.setGraphics(graphics); + + this.wrapped.draw(context, this.wrapped.getWidgetTheme(context.getTheme())); + + context.setGraphics(lastGraphics); + } + + @Override + protected void updateWidgetNarration(@NotNull NarrationElementOutput output) { + output.add(NarratedElementType.TITLE, Component.translatable(this.wrapped.getTranslationId())); + if (!this.wrapped.isEnabled()) return; + if (this.children.isEmpty()) { + if (this.wrapped instanceof IFocusedWidget focusable && focusable.isFocused()) { + output.add(NarratedElementType.USAGE, Component.translatable("narration.button.usage.focused")); + } else if (this.wrapped.isHovering()) { + output.add(NarratedElementType.USAGE, Component.translatable("narration.button.usage.hovered")); + } + return; + } + + Stream entries = this.children.stream(); + updateNarrations(this.children.stream(), output, lastNarratable, entry -> lastNarratable = entry); + } + + public static void updateNarrations(Stream unsorted, NarrationElementOutput output, + NarratableEntry lastNarratable, + Consumer setter) { + List entries = unsorted.filter(NarratableEntry::isActive) + .sorted(Comparator.comparingInt(TabOrderedElement::getTabOrderGroup)) + .collect(Collectors.toList()); + Screen.NarratableSearchResult result = Screen.findNarratableWidget(entries, lastNarratable); + if (result != null) { + if (result.priority.isTerminal()) { + setter.accept(result.entry); + } + + if (entries.size() > 1) { + output.add(NarratedElementType.POSITION, + Component.translatable("narrator.position.screen", result.index + 1, entries.size())); + if (result.priority == NarrationPriority.FOCUSED) { + output.add(NarratedElementType.USAGE, Component.translatable("narration.component_list.usage")); + } + } + + result.entry.updateNarration(output.nest()); + } + } +} From 151245ead9968572bdf1a2aa484289d5514a7643 Mon Sep 17 00:00:00 2001 From: YoungOnion <39562198+YoungOnionMC@users.noreply.github.com> Date: Tue, 12 Aug 2025 23:19:40 -0600 Subject: [PATCH 013/286] api/widgets (#3682) --- .../widgets/AbstractCycleButtonWidget.java | 279 ++++++++++++ .../gtceu/api/mui/widgets/ButtonWidget.java | 180 ++++++++ .../gtceu/api/mui/widgets/CategoryList.java | 166 ++++++++ .../api/mui/widgets/ColorPickerDialog.java | 202 +++++++++ .../api/mui/widgets/CycleButtonWidget.java | 96 +++++ .../gtceu/api/mui/widgets/Dialog.java | 48 +++ .../api/mui/widgets/ListValueWidget.java | 31 ++ .../gtceu/api/mui/widgets/ListWidget.java | 164 ++++++++ .../gtceu/api/mui/widgets/PageButton.java | 81 ++++ .../gtceu/api/mui/widgets/PagedWidget.java | 114 +++++ .../gtceu/api/mui/widgets/PopupMenu.java | 59 +++ .../gtceu/api/mui/widgets/ProgressWidget.java | 200 +++++++++ .../gtceu/api/mui/widgets/RichTextWidget.java | 172 ++++++++ .../gtceu/api/mui/widgets/SchemaWidget.java | 183 ++++++++ .../api/mui/widgets/ScrollingTextWidget.java | 111 +++++ .../gtceu/api/mui/widgets/SliderWidget.java | 281 +++++++++++++ .../api/mui/widgets/SlotGroupWidget.java | 160 +++++++ .../gtceu/api/mui/widgets/SortButtons.java | 83 ++++ .../api/mui/widgets/SortableListWidget.java | 202 +++++++++ .../gtceu/api/mui/widgets/TextWidget.java | 110 +++++ .../gtceu/api/mui/widgets/ToggleButton.java | 94 +++++ .../gtceu/api/mui/widgets/ValueWidget.java | 16 + .../gtceu/api/mui/widgets/VoidWidget.java | 10 + .../gtceu/api/mui/widgets/layout/Column.java | 10 + .../gtceu/api/mui/widgets/layout/Flow.java | 190 +++++++++ .../gtceu/api/mui/widgets/layout/Grid.java | 326 ++++++++++++++ .../api/mui/widgets/layout/IExpander.java | 8 + .../gtceu/api/mui/widgets/layout/Row.java | 10 + .../slot/CraftingContainerWrapper.java | 157 +++++++ .../gtceu/api/mui/widgets/slot/FluidSlot.java | 306 ++++++++++++++ .../api/mui/widgets/slot/IOnSlotChanged.java | 22 + .../gtceu/api/mui/widgets/slot/ItemSlot.java | 292 +++++++++++++ .../mui/widgets/slot/ModularCraftingSlot.java | 117 +++++ .../api/mui/widgets/slot/ModularSlot.java | 221 ++++++++++ .../api/mui/widgets/slot/PhantomItemSlot.java | 104 +++++ .../gtceu/api/mui/widgets/slot/SlotGroup.java | 103 +++++ .../textfield/BaseTextFieldWidget.java | 344 +++++++++++++++ .../widgets/textfield/TextEditorWidget.java | 13 + .../widgets/textfield/TextFieldHandler.java | 398 ++++++++++++++++++ .../widgets/textfield/TextFieldRenderer.java | 206 +++++++++ .../widgets/textfield/TextFieldWidget.java | 246 +++++++++++ 41 files changed, 6115 insertions(+) create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/AbstractCycleButtonWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ButtonWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/CategoryList.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ColorPickerDialog.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/CycleButtonWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/Dialog.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ListValueWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ListWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/PageButton.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/PagedWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/PopupMenu.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ProgressWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/RichTextWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SchemaWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ScrollingTextWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SliderWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SlotGroupWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SortButtons.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SortableListWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/TextWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ToggleButton.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ValueWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/VoidWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Column.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Flow.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Grid.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/IExpander.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Row.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/CraftingContainerWrapper.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/FluidSlot.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/IOnSlotChanged.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/ItemSlot.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/ModularCraftingSlot.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/ModularSlot.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/PhantomItemSlot.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/SlotGroup.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/BaseTextFieldWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextEditorWidget.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextFieldHandler.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextFieldRenderer.java create mode 100644 src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextFieldWidget.java diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/AbstractCycleButtonWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/AbstractCycleButtonWidget.java new file mode 100644 index 00000000000..4038d648171 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/AbstractCycleButtonWidget.java @@ -0,0 +1,279 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.base.value.IBoolValue; +import com.gregtechceu.gtceu.api.mui.base.value.IEnumValue; +import com.gregtechceu.gtceu.api.mui.base.value.IIntValue; +import com.gregtechceu.gtceu.api.mui.base.widget.Interactable; +import com.gregtechceu.gtceu.api.mui.drawable.UITexture; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.value.IntValue; +import com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +public class AbstractCycleButtonWidget> extends Widget + implements Interactable { + + private int stateCount = 1; + private IIntValue intValue; + private int lastValue = -1; + protected IDrawable[] background = null; + protected IDrawable[] hoverBackground = null; + protected IDrawable[] overlay = null; + protected IDrawable[] hoverOverlay = null; + private final List stateTooltip = new ArrayList<>(); + + @Override + public void onInit() { + if (this.intValue == null) { + this.intValue = new IntValue(0); + } + } + + @Override + public boolean isValidSyncHandler(SyncHandler syncHandler) { + this.intValue = castIfTypeElseNull(syncHandler, IIntValue.class); + return this.intValue != null; + } + + protected int getState() { + int val = this.intValue.getIntValue(); + if (val != this.lastValue) { + setState(val, false); + } + return val; + } + + public void next() { + int state = (getState() + 1) % this.stateCount; + + setState(state, true); + } + + public void prev() { + int state = getState(); + if (--state == -1) { + state = this.stateCount - 1; + } + setState(state, true); + } + + public void setState(int state, boolean setSource) { + if (state < 0 || state >= this.stateCount) { + throw new IndexOutOfBoundsException("CycleButton state out of bounds"); + } + if (setSource) { + this.intValue.setIntValue(state); + } + this.lastValue = state; + markTooltipDirty(); + } + + @Override + public @NotNull Result onMousePressed(double mouseX, double mouseY, int button) { + switch (button) { + case 0: + next(); + Interactable.playButtonClickSound(); + return Result.SUCCESS; + case 1: + prev(); + Interactable.playButtonClickSound(); + return Result.SUCCESS; + } + return Result.IGNORE; + } + + @Override + public WidgetTheme getWidgetThemeInternal(ITheme theme) { + return theme.getButtonTheme(); + } + + @Override + public IDrawable getCurrentBackground(ITheme theme, WidgetTheme widgetTheme) { + // make sure texture is up-to-date + int state = getState(); + if (isHovering() && this.hoverBackground != null && this.hoverBackground[state] != null) { + return this.hoverBackground[state]; + } + return this.background != null && this.background[state] != null ? this.background[state] : + super.getCurrentBackground(theme, widgetTheme); + } + + @Override + public IDrawable getCurrentOverlay(ITheme theme, WidgetTheme widgetTheme) { + int state = getState(); + if (isHovering() && this.hoverOverlay != null && this.hoverOverlay[state] != null) { + return this.hoverOverlay[state]; + } + return this.overlay != null && this.overlay[state] != null ? this.overlay[state] : + super.getCurrentOverlay(theme, widgetTheme); + } + + @Override + public boolean hasTooltip() { + int state = getState(); + return super.hasTooltip() || (this.stateTooltip.size() > state && !this.stateTooltip.get(state).isEmpty()); + } + + @Override + public void markTooltipDirty() { + super.markTooltipDirty(); + for (RichTooltip tooltip : this.stateTooltip) { + tooltip.markDirty(); + } + getState(); + } + + @Override + public @Nullable RichTooltip getTooltip() { + RichTooltip tooltip = super.getTooltip(); + if (tooltip == null || tooltip.isEmpty()) { + return this.stateTooltip.get(getState()); + } + return tooltip; + } + + protected W value(IIntValue value) { + this.intValue = value; + setValue(value); + if (value instanceof IEnumValue enumValue) { + stateCount(enumValue.getEnumClass().getEnumConstants().length); + } else if (value instanceof IBoolValue) { + stateCount(2); + } + return getThis(); + } + + /** + * Sets the state dependent background. The images should be vertically stacked images from top to bottom + * Note: The length must be already set! + * + * @param texture background + * @return this + */ + public W stateBackground(UITexture texture) { + splitTexture(texture, this.background); + return getThis(); + } + + /** + * Sets the state dependent overlay. The images should be vertically stacked images from top to bottom + * Note: The length must be already set! + * + * @param texture background + * @return this + */ + public W stateOverlay(UITexture texture) { + splitTexture(texture, this.overlay); + return getThis(); + } + + /** + * Sets the state dependent hover background. The images should be vertically stacked images from top to bottom + * Note: The length must be already set! + * + * @param texture background + * @return this + */ + public W stateHoverBackground(UITexture texture) { + splitTexture(texture, this.hoverBackground); + return getThis(); + } + + /** + * Sets the state dependent hover overlay. The images should be vertically stacked images from top to bottom + * Note: The length must be already set! + * + * @param texture background + * @return this + */ + public W stateHoverOverlay(UITexture texture) { + splitTexture(texture, this.hoverOverlay); + return getThis(); + } + + /** + * Adds a line to the tooltip + */ + protected W addTooltip(int state, IDrawable tooltip) { + if (state >= this.stateTooltip.size() || state < 0) { + throw new IndexOutOfBoundsException(); + } + this.stateTooltip.get(state).addLine(tooltip); + return getThis(); + } + + /** + * Adds a line to the tooltip + */ + protected W addTooltip(int state, String tooltip) { + return addTooltip(state, IKey.str(tooltip)); + } + + protected W stateCount(int stateCount) { + this.stateCount = stateCount; + // adjust tooltip buffer size + while (this.stateTooltip.size() < this.stateCount) { + this.stateTooltip.add(new RichTooltip(this)); + } + while (this.stateTooltip.size() > this.stateCount) { + this.stateTooltip.remove(this.stateTooltip.size() - 1); + } + this.background = checkArray(this.background, stateCount); + this.overlay = checkArray(this.overlay, stateCount); + this.hoverBackground = checkArray(this.hoverBackground, stateCount); + this.hoverOverlay = checkArray(this.hoverOverlay, stateCount); + return getThis(); + } + + private static IDrawable[] checkArray(IDrawable[] array, int length) { + if (array == null) return new IDrawable[length]; + return array.length < length ? Arrays.copyOf(array, length) : array; + } + + protected IDrawable[] addToArray(IDrawable[] array, IDrawable[] drawable, int index) { + return addToArray(array, IDrawable.of(drawable), index); + } + + protected IDrawable[] addToArray(IDrawable[] array, IDrawable drawable, int index) { + if (index < 0) throw new IndexOutOfBoundsException(); + if (array == null || index >= array.length) { + IDrawable[] copy = new IDrawable[(int) (Math.ceil((index + 1) / 4.0) * 4)]; + if (array != null) { + System.arraycopy(array, 0, copy, 0, array.length); + } + array = copy; + } + array[index] = drawable; + return array; + } + + protected static void splitTexture(UITexture texture, IDrawable[] dest) { + for (int i = 0; i < dest.length; i++) { + float a = 1f / dest.length; + dest[i] = texture.getSubArea(0, i * a, 1, i * a + a); + } + } + + protected W tooltip(int index, Consumer builder) { + builder.accept(this.stateTooltip.get(index)); + return getThis(); + } + + protected W tooltipBuilder(int index, Consumer builder) { + this.stateTooltip.get(index).tooltipBuilder(builder); + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ButtonWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ButtonWidget.java new file mode 100644 index 00000000000..b3f24e8265a --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ButtonWidget.java @@ -0,0 +1,180 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiAction; +import com.gregtechceu.gtceu.api.mui.base.widget.Interactable; +import com.gregtechceu.gtceu.api.mui.drawable.GuiTextures; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.value.sync.InteractionSyncHandler; +import com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler; +import com.gregtechceu.gtceu.api.mui.widget.SingleChildWidget; + +import org.jetbrains.annotations.NotNull; + +public class ButtonWidget> extends SingleChildWidget implements Interactable { + + public static ButtonWidget panelCloseButton() { + ButtonWidget buttonWidget = new ButtonWidget<>(); + return buttonWidget.overlay(GuiTextures.CROSS_TINY) + .size(10).top(4).right(4) + .onMousePressed((mouseX, mouseY, button) -> { + if (button == 0 || button == 1) { + buttonWidget.getPanel().closeIfOpen(true); + return true; + } + return false; + }); + } + + private boolean playClickSound = true; + private Runnable clickSound; + private IGuiAction.MousePressed mousePressed; + private IGuiAction.MouseReleased mouseReleased; + private IGuiAction.MousePressed mouseTapped; + private IGuiAction.MouseScroll mouseScroll; + private IGuiAction.KeyPressed keyPressed; + private IGuiAction.KeyReleased keyReleased; + private IGuiAction.KeyPressed keyTapped; + + private InteractionSyncHandler syncHandler; + + @Override + public boolean isValidSyncHandler(SyncHandler syncHandler) { + this.syncHandler = castIfTypeElseNull(syncHandler, InteractionSyncHandler.class); + return this.syncHandler != null; + } + + @Override + public WidgetTheme getWidgetThemeInternal(ITheme theme) { + return theme.getButtonTheme(); + } + + public void playClickSound() { + if (this.playClickSound) { + if (this.clickSound != null) { + this.clickSound.run(); + } else { + Interactable.playButtonClickSound(); + } + } + } + + @Override + public @NotNull Result onMousePressed(double mouseX, double mouseY, int button) { + if (this.mousePressed != null && this.mousePressed.press(mouseX, mouseY, button)) { + playClickSound(); + return Result.SUCCESS; + } + if (this.syncHandler != null && this.syncHandler.onMousePressed(button)) { + playClickSound(); + return Result.SUCCESS; + } + return Result.ACCEPT; + } + + @Override + public boolean onMouseReleased(double mouseX, double mouseY, int button) { + return (this.mouseReleased != null && this.mouseReleased.release(mouseX, mouseY, button)) || + (this.syncHandler != null && this.syncHandler.onMouseReleased(button)); + } + + @NotNull + @Override + public Result onMouseTapped(double mouseX, double mouseY, int button) { + if (this.mouseTapped != null && this.mouseTapped.press(mouseX, mouseY, button)) { + playClickSound(); + return Result.SUCCESS; + } + if (this.syncHandler != null && this.syncHandler.onMouseTapped(button)) { + playClickSound(); + return Result.SUCCESS; + } + return Result.IGNORE; + } + + @Override + public @NotNull Result onKeyPressed(int keyCode, int scanCode, int modifiers) { + if (this.keyPressed != null && this.keyPressed.press(keyCode, scanCode, modifiers)) { + return Result.SUCCESS; + } + if (this.syncHandler != null && this.syncHandler.onKeyPressed(keyCode, scanCode, modifiers)) { + return Result.SUCCESS; + } + return Result.ACCEPT; + } + + @Override + public boolean onKeyReleased(int keyCode, int scanCode, int modifiers) { + return (this.keyReleased != null && this.keyReleased.release(keyCode, scanCode, modifiers)) || + (this.syncHandler != null && this.syncHandler.onKeyReleased(keyCode, scanCode, modifiers)); + } + + @NotNull + @Override + public Result onKeyTapped(int keyCode, int scanCode, int modifiers) { + if (this.keyTapped != null && this.keyTapped.press(keyCode, scanCode, modifiers)) { + return Result.SUCCESS; + } + if (this.syncHandler != null && this.syncHandler.onKeyTapped(keyCode, scanCode, modifiers)) { + return Result.SUCCESS; + } + return Result.IGNORE; + } + + @Override + public boolean onMouseScrolled(double mouseX, double mouseY, double delta) { + return (this.mouseScroll != null && this.mouseScroll.scroll(mouseX, mouseY, delta)) || + (this.syncHandler != null && this.syncHandler.onMouseScroll((int) delta)); + } + + public W onMousePressed(IGuiAction.MousePressed mousePressed) { + this.mousePressed = mousePressed; + return getThis(); + } + + public W onMouseReleased(IGuiAction.MouseReleased mouseReleased) { + this.mouseReleased = mouseReleased; + return getThis(); + } + + public W onMouseTapped(IGuiAction.MousePressed mouseTapped) { + this.mouseTapped = mouseTapped; + return getThis(); + } + + public W onMouseScrolled(IGuiAction.MouseScroll mouseScroll) { + this.mouseScroll = mouseScroll; + return getThis(); + } + + public W onKeyPressed(IGuiAction.KeyPressed keyPressed) { + this.keyPressed = keyPressed; + return getThis(); + } + + public W onKeyReleased(IGuiAction.KeyReleased keyReleased) { + this.keyReleased = keyReleased; + return getThis(); + } + + public W onKeyTapped(IGuiAction.KeyPressed keyTapped) { + this.keyTapped = keyTapped; + return getThis(); + } + + public W syncHandler(InteractionSyncHandler interactionSyncHandler) { + this.syncHandler = interactionSyncHandler; + setSyncHandler(interactionSyncHandler); + return getThis(); + } + + public W playClickSound(boolean play) { + this.playClickSound = play; + return getThis(); + } + + public W clickSound(Runnable clickSound) { + this.clickSound = clickSound; + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/CategoryList.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/CategoryList.java new file mode 100644 index 00000000000..63e61ba75d1 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/CategoryList.java @@ -0,0 +1,166 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.layout.ILayoutWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.Interactable; +import com.gregtechceu.gtceu.api.mui.drawable.GuiTextures; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.widget.AbstractParentWidget; +import com.gregtechceu.gtceu.api.mui.widget.WidgetTree; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class CategoryList extends AbstractParentWidget implements Interactable, ILayoutWidget { + + private final List subCategories = new ArrayList<>(); + private boolean expanded = false; + private int totalHeight = 0; + private IDrawable expandedOverlay; + private IDrawable collapsedOverlay; + + @Override + public void drawOverlay(ModularGuiContext context, WidgetTheme widgetTheme) { + super.drawOverlay(context, widgetTheme); + if (this.expanded) { + this.expandedOverlay.drawAtZero(context, getArea(), widgetTheme); + } else { + this.collapsedOverlay.drawAtZero(context, getArea(), widgetTheme); + } + } + + @Override + public void onInit() { + super.onInit(); + if (this.expandedOverlay == null) { + if (getParent() instanceof CategoryList categoryList) { + this.expandedOverlay = categoryList.expandedOverlay; + } else if (getParent() instanceof Root root) { + this.expandedOverlay = root.expandedOverlay; + } else { + this.expandedOverlay = IDrawable.EMPTY; + } + } + if (this.collapsedOverlay == null) { + if (getParent() instanceof CategoryList categoryList) { + this.collapsedOverlay = categoryList.collapsedOverlay; + } else if (getParent() instanceof Root root) { + this.collapsedOverlay = root.collapsedOverlay; + } else { + this.collapsedOverlay = IDrawable.EMPTY; + } + } + } + + @Override + public void onChildAdd(IWidget child) { + if (child instanceof CategoryList categoryList) { + this.subCategories.add(categoryList); + } + child.setEnabled(this.expanded); + } + + @Override + public @NotNull Result onMousePressed(double mouseX, double mouseY, int button) { + if (button == 0 || button == 1) { + expanded(!this.expanded); + return Result.SUCCESS; + } + return Result.ACCEPT; + } + + public void expanded(boolean expanded) { + if (expanded == this.expanded) return; + this.expanded = expanded; + for (IWidget widget : getChildren()) { + widget.setEnabled(expanded); + } + calculateHeightAndLayout(true); + } + + public void calculateHeightAndLayout(boolean calculateParents) { + if (this.expanded) { + int y = getArea().height; + for (IWidget widget : getChildren()) { + widget.getArea().ry = y; + widget.resizer().setYResized(true); + y += widget instanceof CategoryList categoryList && categoryList.expanded ? + categoryList.totalHeight : widget.getArea().height; + } + this.totalHeight = y; + } else { + this.totalHeight = getArea().height; + } + + if (!calculateParents) return; + if (getParent() instanceof CategoryList categoryList) { + categoryList.calculateHeightAndLayout(true); + } else if (getParent() instanceof Root root) { + root.updateHeight(); + } + } + + @Override + public void layoutWidgets() { + calculateHeightAndLayout(false); + } + + public CategoryList setCollapsedOverlay(IDrawable collapsedOverlay) { + this.collapsedOverlay = collapsedOverlay; + return this; + } + + public CategoryList setExpandedOverlay(IDrawable expandedOverlay) { + this.expandedOverlay = expandedOverlay; + return this; + } + + public static class Root extends ListWidget { + + private final List categories = new ArrayList<>(); + + private IDrawable expandedOverlay = GuiTextures.MOVE_DOWN.asIcon().size(16, 8).alignment(Alignment.CenterRight) + .marginRight(4); + private IDrawable collapsedOverlay = GuiTextures.MOVE_RIGHT.asIcon().size(8, 16) + .alignment(Alignment.CenterRight).marginRight(8); + + @Override + public void onChildAdd(IWidget child) { + if (child instanceof CategoryList categoryList) { + this.categories.add(categoryList); + } + } + + private void updateHeight() { + layoutWidgets(); + WidgetTree.applyPos(this); + } + + @Override + public void layoutWidgets() { + int y = 0; + for (IWidget widget : getChildren()) { + widget.getArea().ry = y; + widget.resizer().setYResized(true); + y += widget instanceof CategoryList categoryList && categoryList.expanded ? + categoryList.totalHeight : widget.getArea().height; + } + getScrollArea().getScrollY().setScrollSize(y); + } + + public Root setCollapsedOverlay(IDrawable collapsedOverlay) { + this.collapsedOverlay = collapsedOverlay; + return this; + } + + public Root setExpandedOverlay(IDrawable expandedOverlay) { + this.expandedOverlay = expandedOverlay; + return this; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ColorPickerDialog.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ColorPickerDialog.java new file mode 100644 index 00000000000..954b65916ac --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ColorPickerDialog.java @@ -0,0 +1,202 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.drawable.GuiTextures; +import com.gregtechceu.gtceu.api.mui.drawable.HueBar; +import com.gregtechceu.gtceu.api.mui.drawable.Rectangle; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.value.DoubleValue; +import com.gregtechceu.gtceu.api.mui.value.StringValue; +import com.gregtechceu.gtceu.api.mui.widgets.layout.Column; +import com.gregtechceu.gtceu.api.mui.widgets.layout.Row; +import com.gregtechceu.gtceu.api.mui.widgets.textfield.TextFieldWidget; + +import java.util.function.Consumer; + +public class ColorPickerDialog extends Dialog { + + private static final IDrawable handleBackground = new Rectangle().setColor(Color.WHITE.main); + + private int color; + private final int alpha; + private final boolean controlAlpha; + + private final Rectangle preview = new Rectangle(); + private final Rectangle sliderBackgroundR = new Rectangle(); + private final Rectangle sliderBackgroundG = new Rectangle(); + private final Rectangle sliderBackgroundB = new Rectangle(); + private final Rectangle sliderBackgroundA = new Rectangle(); + private final Rectangle sliderBackgroundS = new Rectangle(); + private final Rectangle sliderBackgroundV = new Rectangle(); + + public ColorPickerDialog(Consumer resultConsumer, int startColor, boolean controlAlpha) { + this("color_picker", resultConsumer, startColor, controlAlpha); + } + + public ColorPickerDialog(String name, Consumer resultConsumer, int startColor, boolean controlAlpha) { + super(name, resultConsumer); + this.alpha = Color.getAlpha(startColor); + updateColor(startColor); + this.controlAlpha = controlAlpha; + size(140, controlAlpha ? 106 : 94).background(GuiTextures.MC_BACKGROUND); + IWidget alphaSlider = controlAlpha ? new Row() + .widthRel(1f).height(12) + .child(IKey.str("A: ").asWidget().heightRel(1f)) + .child(createSlider(this.sliderBackgroundA) + .bounds(0, 255) + .value(new DoubleValue.Dynamic(() -> Color.getAlpha(this.color), + val -> updateColor(Color.withAlpha(this.color, (int) val))))) : + null; + + PagedWidget.Controller controller = new PagedWidget.Controller(); + child(new Column() + .left(5).right(5).top(5).bottom(5) + .child(new Row() + .left(5).right(5).height(14) + .child(new PageButton(0, controller) + .sizeRel(0.5f, 1f) + .invertSelected(true) + .overlay(IKey.str("RGB"))) + .child(new PageButton(1, controller) + .sizeRel(0.5f, 1f) + .invertSelected(true) + .overlay(IKey.str("HSV")))) + .child(new Row().widthRel(1f).height(12).marginTop(4) + .child(IKey.str("Hex: ").asWidget().heightRel(1f)) + .child(new TextFieldWidget() + .height(12) + .expanded() + .setValidator(this::validateRawColor) + .value(new StringValue.Dynamic(() -> { + if (controlAlpha) { + return "#" + Integer.toHexString(this.color); + } + return "#" + Integer.toHexString(Color.withAlpha(this.color, 0)); + }, val -> { + try { + updateColor(Integer.decode(val)); + } catch (NumberFormatException ignored) {} + }))) + .child(this.preview.asWidget().background(GuiTextures.CHECKBOARD).size(10, 10).margin(1))) + .child(new PagedWidget<>() + .left(5).right(5) + .expanded() + .controller(controller) + .addPage(createRGBPage(alphaSlider)) + .addPage(createHSVPage(alphaSlider))) + .child(new Row() + .left(10).right(10).height(14) + .mainAxisAlignment(Alignment.MainAxis.SPACE_BETWEEN) + .child(new ButtonWidget<>() + .heightRel(1f).width(50) + .overlay(IKey.str("Cancel")) + .onMousePressed((mouseX, mouseY, button) -> { + animateClose(); + return true; + })) + .child(new ButtonWidget<>() + .heightRel(1f).width(50) + .overlay(IKey.str("Confirm")) + .onMousePressed((mouseX, mouseY, button) -> { + closeWith(this.color); + return true; + })))); + } + + private IWidget createRGBPage(IWidget alphaSlider) { + return new Column() + .sizeRel(1f, 1f) + .child(new Row() + .widthRel(1f).height(12) + .child(IKey.str("R: ").asWidget().heightRel(1f)) + .child(createSlider(this.sliderBackgroundR) + .bounds(0, 255) + .value(new DoubleValue.Dynamic(() -> Color.getRed(this.color), + val -> updateColor(Color.withRed(this.color, (int) val)))))) + .child(new Row() + .widthRel(1f).height(12) + .child(IKey.str("G: ").asWidget().heightRel(1f)) + .child(createSlider(this.sliderBackgroundG) + .bounds(0, 255) + .value(new DoubleValue.Dynamic(() -> Color.getGreen(this.color), + val -> updateColor(Color.withGreen(this.color, (int) val)))))) + .child(new Row() + .widthRel(1f).height(12) + .child(IKey.str("B: ").asWidget().heightRel(1f)) + .child(createSlider(this.sliderBackgroundB) + .bounds(0, 255) + .value(new DoubleValue.Dynamic(() -> Color.getBlue(this.color), + val -> updateColor(Color.withBlue(this.color, (int) val)))))) + .childIf(alphaSlider != null, alphaSlider); + } + + private IWidget createHSVPage(IWidget alphaSlider) { + return new Column() + .sizeRel(1f, 1f) + .child(new Row() + .widthRel(1f).height(12) + .child(IKey.str("H: ").asWidget().heightRel(1f)) + .child(createSlider(new HueBar(GuiAxis.X)) + .bounds(0, 360) + .value(new DoubleValue.Dynamic(() -> Color.getHue(this.color), + val -> updateColor(Color.withHSVHue(this.color, (float) val)))))) + .child(new Row() + .widthRel(1f).height(12) + .child(IKey.str("S: ").asWidget().heightRel(1f)) + .child(createSlider(this.sliderBackgroundS) + .bounds(0, 1) + .value(new DoubleValue.Dynamic(() -> Color.getHSVSaturation(this.color), + val -> updateColor(Color.withHSVSaturation(this.color, (float) val)))))) + .child(new Row() + .widthRel(1f).height(12) + .child(IKey.str("V: ").asWidget().heightRel(1f)) + .child(createSlider(this.sliderBackgroundV) + .bounds(0, 1) + .value(new DoubleValue.Dynamic(() -> Color.getValue(this.color), + val -> updateColor(Color.withValue(this.color, (float) val)))))) + .childIf(alphaSlider != null, alphaSlider); + } + + private static SliderWidget createSlider(IDrawable background) { + return new SliderWidget() + .expanded() + .heightRel(1f) + .background(background.asIcon().size(0, 4)) + .sliderTexture(handleBackground) + .sliderSize(2, 8); + } + + private String validateRawColor(String raw) { + if (!raw.startsWith("#")) { + if (raw.startsWith("0x") || raw.startsWith("0X")) { + raw = raw.substring(2); + } + return "#" + raw; + } + return raw; + } + + public void updateColor(int color) { + this.color = color; + if (!this.controlAlpha) { + this.color = Color.withAlpha(this.color, this.alpha); + } + color = Color.withAlpha(color, 255); + int rs = Color.withRed(color, 0), re = Color.withRed(color, 255); + int gs = Color.withGreen(color, 0), ge = Color.withGreen(color, 255); + int bs = Color.withBlue(color, 0), be = Color.withBlue(color, 255); + int as = Color.withAlpha(color, 0), ae = Color.withAlpha(color, 255); + this.sliderBackgroundR.setHorizontalGradient(rs, re); + this.sliderBackgroundG.setHorizontalGradient(gs, ge); + this.sliderBackgroundB.setHorizontalGradient(bs, be); + this.sliderBackgroundA.setHorizontalGradient(as, ae); + this.sliderBackgroundS.setHorizontalGradient(Color.withHSVSaturation(color, 0f), + Color.withHSVSaturation(color, 1f)); + this.sliderBackgroundV.setHorizontalGradient(Color.withValue(color, 0f), Color.withValue(color, 1f)); + this.preview.setColor(this.color); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/CycleButtonWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/CycleButtonWidget.java new file mode 100644 index 00000000000..fe9090a0359 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/CycleButtonWidget.java @@ -0,0 +1,96 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.value.IIntValue; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; + +import java.util.function.Consumer; + +public class CycleButtonWidget extends AbstractCycleButtonWidget { + + @Override + public CycleButtonWidget value(IIntValue value) { + return super.value(value); + } + + public CycleButtonWidget stateBackground(int state, IDrawable drawable) { + this.background = addToArray(this.background, drawable, state); + return getThis(); + } + + public CycleButtonWidget stateHoverBackground(int state, IDrawable drawable) { + this.hoverBackground = addToArray(this.hoverBackground, drawable, state); + return getThis(); + } + + public CycleButtonWidget stateOverlay(int state, IDrawable drawable) { + this.overlay = addToArray(this.overlay, drawable, state); + return getThis(); + } + + public CycleButtonWidget stateHoverOverlay(int state, IDrawable drawable) { + this.hoverOverlay = addToArray(this.hoverOverlay, drawable, state); + return getThis(); + } + + public CycleButtonWidget stateBackground(boolean state, IDrawable drawable) { + return stateBackground(state ? 1 : 0, drawable); + } + + public CycleButtonWidget stateHoverBackground(boolean state, IDrawable drawable) { + return stateHoverBackground(state ? 1 : 0, drawable); + } + + public CycleButtonWidget stateOverlay(boolean state, IDrawable drawable) { + return stateOverlay(state ? 1 : 0, drawable); + } + + public CycleButtonWidget stateHoverOverlay(boolean state, IDrawable drawable) { + return stateHoverOverlay(state ? 1 : 0, drawable); + } + + public > CycleButtonWidget stateBackground(T state, IDrawable drawable) { + return stateBackground(state.ordinal(), drawable); + } + + public > CycleButtonWidget stateHoverBackground(T state, IDrawable drawable) { + return stateHoverBackground(state.ordinal(), drawable); + } + + public > CycleButtonWidget stateOverlay(T state, IDrawable drawable) { + return stateOverlay(state.ordinal(), drawable); + } + + public > CycleButtonWidget stateHoverOverlay(T state, IDrawable drawable) { + return stateHoverOverlay(state.ordinal(), drawable); + } + + @Override + public CycleButtonWidget addTooltip(int state, String tooltip) { + return super.addTooltip(state, tooltip); + } + + @Override + public CycleButtonWidget addTooltip(int state, IDrawable tooltip) { + return super.addTooltip(state, tooltip); + } + + public CycleButtonWidget length(int length) { + return stateCount(length); + } + + @Override + public CycleButtonWidget stateCount(int stateCount) { + return super.stateCount(stateCount); + } + + @Override + public CycleButtonWidget tooltip(int index, Consumer builder) { + return super.tooltip(index, builder); + } + + @Override + public CycleButtonWidget tooltipBuilder(int index, Consumer builder) { + return super.tooltipBuilder(index, builder); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/Dialog.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/Dialog.java new file mode 100644 index 00000000000..abb0f207ed2 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/Dialog.java @@ -0,0 +1,48 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.client.mui.screen.ModularPanel; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.function.Consumer; + +@Accessors(chain = true) +public class Dialog extends ModularPanel { + + private final Consumer resultConsumer; + @Getter + @Setter + private boolean draggable = false; + @Setter + private boolean disablePanelsBelow = true; + @Setter + private boolean closeOnOutOfBoundsClick = false; + + public Dialog(String name) { + this(name, null); + } + + public Dialog(String name, Consumer resultConsumer) { + super(name); + this.resultConsumer = resultConsumer; + } + + public void closeWith(T result) { + if (this.resultConsumer != null) { + this.resultConsumer.accept(result); + } + animateClose(); + } + + @Override + public boolean disablePanelsBelow() { + return this.disablePanelsBelow; + } + + @Override + public boolean closeOnOutOfBoundsClick() { + return this.closeOnOutOfBoundsClick; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ListValueWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ListValueWidget.java new file mode 100644 index 00000000000..4e6b91713a7 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ListValueWidget.java @@ -0,0 +1,31 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +public class ListValueWidget> extends ListWidget { + + private final Function widgetToValue; + + public ListValueWidget(Function widgetToValue) { + this.widgetToValue = widgetToValue; + } + + public List getValues() { + List list = new ArrayList<>(); + for (I widget : getTypeChildren()) { + list.add(this.widgetToValue.apply(widget)); + } + return list; + } + + public W children(Iterable values, Function widgetCreator) { + for (V value : values) { + child(widgetCreator.apply(value)); + } + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ListWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ListWidget.java new file mode 100644 index 00000000000..d4865cf1add --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ListWidget.java @@ -0,0 +1,164 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.drawable.IIcon; +import com.gregtechceu.gtceu.api.mui.base.layout.ILayoutWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.IParentWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.widget.AbstractScrollWidget; +import com.gregtechceu.gtceu.api.mui.widget.scroll.ScrollData; +import com.gregtechceu.gtceu.api.mui.widget.scroll.VerticalScrollData; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import lombok.Getter; + +import java.util.function.IntFunction; + +/** + * A widget which can hold any amount of children. + * + * @param type of children (in most cases just {@link IWidget}) + * @param type of this widget + */ +public class ListWidget> extends AbstractScrollWidget + implements ILayoutWidget, IParentWidget { + + @Getter + private ScrollData scrollData; + private IIcon childSeparator; + private final IntList separatorPositions = new IntArrayList(); + private boolean collapseDisabledChild = false; + + public ListWidget() { + super(null, null); + } + + @Override + public void onInit() { + if (this.scrollData == null) { + scrollDirection(new VerticalScrollData()); + } + } + + @Override + public void draw(ModularGuiContext context, WidgetTheme widgetTheme) { + if (this.childSeparator == null || this.separatorPositions.isEmpty()) return; + GuiAxis axis = this.scrollData.getAxis(); + int x = getArea().getPadding().left, y = getArea().getPadding().top, w, h; + if (axis.isHorizontal()) { + w = this.childSeparator.getWidth(); + h = getArea().h() - getArea().getPadding().vertical(); + } else { + w = getArea().w() - getArea().getPadding().horizontal(); + h = this.childSeparator.getHeight(); + } + for (int p : this.separatorPositions) { + if (axis.isHorizontal()) { + x = p; + } else { + y = p; + } + this.childSeparator.draw(context, x, y, w, h, widgetTheme); + } + } + + @Override + public void layoutWidgets() { + this.separatorPositions.clear(); + GuiAxis axis = this.scrollData.getAxis(); + int separatorSize = getSeparatorSize(); + int p = getArea().getPadding().getStart(axis); + for (IWidget widget : getChildren()) { + if (shouldIgnoreChildSize(widget)) continue; + if (axis.isVertical() ? + widget.getFlex().hasYPos() || !widget.resizer().isHeightCalculated() : + widget.getFlex().hasXPos() || !widget.resizer().isWidthCalculated()) { + continue; + } + p += widget.getArea().getMargin().getStart(axis); + widget.getArea().setRelativePoint(axis, p); + p += widget.getArea().getSize(axis) + widget.getArea().getMargin().getEnd(axis); + if (axis.isHorizontal()) { + widget.resizer().setXResized(true); + } else { + widget.resizer().setYResized(true); + } + this.separatorPositions.add(p); + p += separatorSize; + } + getScrollData().setScrollSize(p + getArea().getPadding().getEnd(axis)); + } + + @Override + public boolean shouldIgnoreChildSize(IWidget child) { + return this.collapseDisabledChild && !child.isEnabled(); + } + + @Override + public boolean addChild(I child, int index) { + return super.addChild(child, index); + } + + @Override + public void onChildAdd(I child) { + super.onChildAdd(child); + if (isValid()) { + this.scrollData.clamp(getScrollArea()); + } + } + + @Override + public void onChildRemove(I child) { + super.onChildRemove(child); + if (isValid()) { + this.scrollData.clamp(getScrollArea()); + } + } + + public int getSeparatorSize() { + if (this.childSeparator == null) return 0; + return this.scrollData.getAxis().isHorizontal() ? this.childSeparator.getWidth() : + this.childSeparator.getHeight(); + } + + public W scrollDirection(GuiAxis axis) { + return scrollDirection(ScrollData.of(axis)); + } + + public W scrollDirection(ScrollData data) { + this.scrollData = data; + getScrollArea().removeScrollData(); + getScrollArea().setScrollData(this.scrollData); + return getThis(); + } + + public W childSeparator(IIcon separator) { + this.childSeparator = separator; + return getThis(); + } + + public W children(Iterable widgets) { + for (I widget : widgets) { + child(widget); + } + return getThis(); + } + + public W children(int amount, IntFunction widgetCreator) { + for (int i = 0; i < amount; i++) { + child(widgetCreator.apply(i)); + } + return getThis(); + } + + /** + * Configures this widget to collapse disabled child widgets. + */ + public W collapseDisabledChild() { + this.collapseDisabledChild = true; + return getThis(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/PageButton.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/PageButton.java new file mode 100644 index 00000000000..f3ace67ed68 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/PageButton.java @@ -0,0 +1,81 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.widget.Interactable; +import com.gregtechceu.gtceu.api.mui.drawable.DrawableStack; +import com.gregtechceu.gtceu.api.mui.drawable.TabTexture; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.theme.WidgetThemeSelectable; +import com.gregtechceu.gtceu.api.mui.widget.Widget; + +import org.jetbrains.annotations.NotNull; + +public class PageButton extends Widget implements Interactable { + + private final int index; + private final PagedWidget.Controller controller; + private IDrawable inactiveTexture = null; + private boolean invert = false; + + public PageButton(int index, PagedWidget.Controller controller) { + this.index = index; + this.controller = controller; + disableHoverBackground(); + } + + @Override + public WidgetTheme getWidgetThemeInternal(ITheme theme) { + WidgetThemeSelectable widgetTheme = theme.getToggleButtonTheme(); + return isActive() ^ invertSelected() ? widgetTheme : widgetTheme.getSelected(); + } + + @Override + public @NotNull Result onMousePressed(double mouseX, double mouseY, int button) { + if (!isActive()) { + this.controller.setPage(this.index); + Interactable.playButtonClickSound(); + return Result.SUCCESS; + } + return Result.ACCEPT; + } + + @Override + public IDrawable getBackground() { + return isActive() || this.inactiveTexture == null ? super.getBackground() : this.inactiveTexture; + } + + public boolean isActive() { + return this.controller.getActivePageIndex() == this.index; + } + + public PageButton background(boolean active, IDrawable... background) { + if (active) { + return background(background); + } + if (background.length == 0) { + this.inactiveTexture = null; + } else if (background.length == 1) { + this.inactiveTexture = background[0]; + } else { + this.inactiveTexture = new DrawableStack(background); + } + return this; + } + + public PageButton tab(TabTexture texture, int location) { + return background(invertSelected(), texture.get(location, invertSelected())) + .background(!invertSelected(), texture.get(location, !invertSelected())) + .disableHoverBackground() + .size(texture.getWidth(), texture.getHeight()); + } + + public PageButton invertSelected(boolean invert) { + this.invert = invert; + return getThis(); + } + + public boolean invertSelected() { + return this.invert; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/PagedWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/PagedWidget.java new file mode 100644 index 00000000000..115ff554c1c --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/PagedWidget.java @@ -0,0 +1,114 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.widget.Widget; + +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.ArrayList; +import java.util.List; + +public class PagedWidget> extends Widget { + + @Getter + private final List pages = new ArrayList<>(); + @Getter + private IWidget currentPage; + @Getter + private int currentPageIndex = 0; + + @Override + public void afterInit() { + setPage(0); + } + + public void setPage(int page) { + if (page < 0 || page >= this.pages.size()) { + throw new IndexOutOfBoundsException(); + } + this.currentPageIndex = page; + if (this.currentPage != null) { + this.currentPage.setEnabled(false); + } + this.currentPage = this.pages.get(this.currentPageIndex); + this.currentPage.setEnabled(true); + } + + public void nextPage() { + if (++this.currentPageIndex == this.pages.size()) { + this.currentPageIndex = 0; + } + setPage(this.currentPageIndex); + } + + public void previousPage() { + if (--this.currentPageIndex == -1) { + this.currentPageIndex = this.pages.size() - 1; + } + setPage(this.currentPageIndex); + } + + @Override + public @Unmodifiable @NotNull List getChildren() { + return this.pages; + } + + public W addPage(IWidget widget) { + this.pages.add(widget); + widget.setEnabled(false); + return getThis(); + } + + public W controller(Controller controller) { + controller.setPagedWidget(this); + return getThis(); + } + + public static class Controller { + + private PagedWidget pagedWidget; + + public boolean isInitialised() { + return this.pagedWidget != null && this.pagedWidget.isValid(); + } + + private void validate() { + if (!isInitialised()) { + throw new IllegalStateException( + "PagedWidget controller does not have a valid PagedWidget! Current PagedWidget: " + + this.pagedWidget); + } + } + + private void setPagedWidget(PagedWidget pagedWidget) { + this.pagedWidget = pagedWidget; + } + + public void setPage(int page) { + validate(); + this.pagedWidget.setPage(page); + } + + public void nextPage() { + validate(); + this.pagedWidget.nextPage(); + } + + public void previousPage() { + validate(); + this.pagedWidget.previousPage(); + } + + public IWidget getActivePage() { + validate(); + return this.pagedWidget.getCurrentPage(); + } + + public int getActivePageIndex() { + validate(); + return this.pagedWidget.getCurrentPageIndex(); + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/PopupMenu.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/PopupMenu.java new file mode 100644 index 00000000000..c730d92d687 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/PopupMenu.java @@ -0,0 +1,59 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.widget.Widget; + +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; + +public class PopupMenu> extends Widget { + + private final MenuWrapper menu; + @Getter + private final @NotNull List children; + + public PopupMenu(IWidget child) { + this.menu = new MenuWrapper(child); + child.flex().relative(this.getArea()); + this.menu.setEnabled(false); + this.children = Collections.singletonList(this.menu); + } + + @Override + public void onMouseStartHover() { + this.menu.setEnabled(true); + this.menu.mightClose = false; + } + + @Override + public void onMouseEndHover() { + this.menu.mightClose = true; + } + + public static class MenuWrapper extends Widget { + + private final IWidget child; + @Getter + private final @NotNull List children; + @Setter + private boolean mightClose = false; + + private MenuWrapper(IWidget child) { + this.child = child; + this.children = Collections.singletonList(child); + flex().coverChildren().cancelMovementX().cancelMovementY(); + } + + @Override + public void onUpdate() { + super.onUpdate(); + if (this.mightClose && !isBelowMouse()) { + setEnabled(false); + } + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ProgressWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ProgressWidget.java new file mode 100644 index 00000000000..46c9227de92 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ProgressWidget.java @@ -0,0 +1,200 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.value.IDoubleValue; +import com.gregtechceu.gtceu.api.mui.drawable.UITexture; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.value.DoubleValue; +import com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import net.minecraft.util.Mth; + +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.function.DoubleSupplier; + +@Accessors(fluent = true, chain = true) +public class ProgressWidget extends Widget { + + private final UITexture[] fullTexture = new UITexture[4]; + private UITexture emptyTexture; + @Setter + private Direction direction = Direction.RIGHT; + private int imageSize = -1; + + private IDoubleValue doubleValue; + + @Override + public void onInit() { + if (this.doubleValue == null) { + this.doubleValue = new DoubleValue(0.5); + } + if (this.direction == Direction.CIRCULAR_CW && this.fullTexture[0] != null) { + UITexture base = this.fullTexture[0]; + this.fullTexture[0] = base.getSubArea(0f, 0.5f, 0.5f, 1f); + this.fullTexture[1] = base.getSubArea(0f, 0f, 0.5f, 0.5f); + this.fullTexture[2] = base.getSubArea(0.5f, 0f, 1f, 0.5f); + this.fullTexture[3] = base.getSubArea(0.5f, 0.5f, 1f, 1f); + } + } + + @Override + public boolean isValidSyncHandler(SyncHandler syncHandler) { + this.doubleValue = castIfTypeElseNull(syncHandler, IDoubleValue.class); + return this.doubleValue != null; + } + + @Override + public void onResized() { + if (this.imageSize < 0) { + this.imageSize = getArea().width; + } + } + + public float getCurrentProgress() { + return (float) this.doubleValue.getDoubleValue(); + } + + @Override + public void draw(ModularGuiContext context, WidgetTheme widgetTheme) { + if (this.emptyTexture != null) { + this.emptyTexture.draw(context, 0, 0, getArea().w(), getArea().h(), widgetTheme); + Color.setGlColorOpaque(Color.WHITE.main); + } + float progress = getCurrentProgress(); + if (this.fullTexture[0] != null && progress > 0) { + if (this.direction == Direction.CIRCULAR_CW) { + drawCircular(context, progress, widgetTheme); + return; + } + if (progress >= 1) { + this.fullTexture[0].draw(context, 0, 0, getArea().w(), getArea().h(), widgetTheme); + } else { + progress = getProgressUV(progress); + float u0 = 0, v0 = 0, u1 = 1, v1 = 1; + float x = 0, y = 0, width = getArea().width, height = getArea().height; + switch (this.direction) { + case RIGHT: + u1 = progress; + width *= progress; + break; + case LEFT: + u0 = 1 - progress; + width *= progress; + x = getArea().width - width; + break; + case DOWN: + v1 = progress; + height *= progress; + break; + case UP: + v0 = 1 - progress; + height *= progress; + y = getArea().height - height; + break; + } + this.fullTexture[0].drawSubArea(context, x, y, width, height, u0, v0, u1, v1, widgetTheme); + } + } + } + + public float getProgressUV(float uv) { + if (getScreen().getCurrentTheme().getSmoothProgressBarOverride()) { + return uv; + } + return (float) (Math.floor(uv * this.imageSize) / this.imageSize); + } + + private void drawCircular(GuiContext context, float progress, WidgetTheme widgetTheme) { + float[] subAreas = { + getProgressUV(Mth.clamp(progress / 0.25f, 0, 1)), + getProgressUV(Mth.clamp((progress - 0.25f) / 0.25f, 0, 1)), + getProgressUV(Mth.clamp((progress - 0.5f) / 0.25f, 0, 1)), + getProgressUV(Mth.clamp((progress - 0.75f) / 0.25f, 0, 1)) + }; + float halfWidth = getArea().width / 2f; + float halfHeight = getArea().height / 2f; + + float progressScaled = subAreas[0] * halfHeight; + this.fullTexture[0].drawSubArea(context, + 0, getArea().height - progressScaled, + halfWidth, progressScaled, + 0.0f, 1.0f - progressScaled / halfHeight, + 1.0f, 1.0f, widgetTheme); // BL, draw UP + + progressScaled = subAreas[1] * halfWidth; + this.fullTexture[1].drawSubArea(context, + 0, 0, + progressScaled, halfHeight, + 0.0f, 0.0f, + progressScaled / (halfWidth), 1.0f, + widgetTheme); // TL, draw RIGHT + + progressScaled = subAreas[2] * halfHeight; + this.fullTexture[2].drawSubArea(context, + halfWidth, 0, + halfWidth, progressScaled, + 0.0f, 0.0f, + 1.0f, progressScaled / halfHeight, + widgetTheme); // TR, draw DOWN + + progressScaled = subAreas[3] * halfWidth; + this.fullTexture[3].drawSubArea(context, + getArea().width - progressScaled, halfHeight, + progressScaled, halfHeight, + 1.0f - progressScaled / halfWidth, 0.0f, + 1.0f, 1.0f, widgetTheme); // BR, draw LEFT + } + + public ProgressWidget value(IDoubleValue value) { + this.doubleValue = value; + setValue(value); + return this; + } + + public ProgressWidget progress(DoubleSupplier progress) { + return value(new DoubleValue.Dynamic(progress, null)); + } + + public ProgressWidget progress(double progress) { + return value(new DoubleValue(progress)); + } + + /** + * Sets the texture to render + * + * @param emptyTexture empty bar, always rendered + * @param fullTexture full bar, partly rendered, based on progress + * @param imageSize image size in direction of progress. used for non-smooth rendering + */ + public ProgressWidget texture(UITexture emptyTexture, UITexture fullTexture, int imageSize) { + this.emptyTexture = emptyTexture; + this.fullTexture[0] = fullTexture; + this.imageSize = imageSize; + return this; + } + + /** + * @param texture a texture where the empty and full bar are stacked on top of each other + */ + public ProgressWidget texture(UITexture texture, int imageSize) { + return texture(texture.getSubArea(0, 0, 1, 0.5f), texture.getSubArea(0, 0.5f, 1, 1), imageSize); + } + + public ProgressWidget direction(Direction direction) { + this.direction = direction; + return this; + } + + public enum Direction { + LEFT, + RIGHT, + UP, + DOWN, + CIRCULAR_CW + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/RichTextWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/RichTextWidget.java new file mode 100644 index 00000000000..4ab2d1ad2df --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/RichTextWidget.java @@ -0,0 +1,172 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IHoverable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IRichTextBuilder; +import com.gregtechceu.gtceu.api.mui.base.widget.Interactable; +import com.gregtechceu.gtceu.api.mui.drawable.text.RichText; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +public class RichTextWidget extends Widget implements IRichTextBuilder, Interactable { + + private final RichText text = new RichText(); + private Consumer builder; + private boolean dirty = false; + private boolean autoUpdate = false; + + public void markDirty() { + this.dirty = true; + } + + @Override + public void draw(ModularGuiContext context, WidgetTheme widgetTheme) { + super.draw(context, widgetTheme); + if (this.autoUpdate || this.dirty) { + if (this.builder != null) { + this.text.clearText(); + this.builder.accept(this.text); + } + this.dirty = false; + } + this.text.drawAtZero(context, getArea(), widgetTheme); + } + + @Override + public void drawForeground(ModularGuiContext context) { + super.drawForeground(context); + Object o = this.text.getHoveringElement(context.getFont(), context.getMouseX(), context.getMouseY()); + // GTCEu.LOGGER.info("Mouse {}, {}", context.getMouseX(), context.getMouseY()); + if (o instanceof IHoverable hoverable) { + hoverable.onHover(); + RichTooltip tooltip = hoverable.getTooltip(); + if (tooltip != null) { + tooltip.draw(context); + } + } + } + + @Override + public @NotNull Result onMousePressed(double mouseX, double mouseY, int button) { + Object o = this.text.getHoveringElement(getContext().getFont(), getContext().getMouseX(), + getContext().getMouseY()); + if (o instanceof Interactable interactable) { + return interactable.onMousePressed(mouseX, mouseY, button); + } + return Result.ACCEPT; + } + + @Override + public boolean onMouseReleased(double mouseX, double mouseY, int button) { + Object o = this.text.getHoveringElement(getContext().getFont(), getContext().getMouseX(), + getContext().getMouseY()); + if (o instanceof Interactable interactable) { + return interactable.onMouseReleased(mouseX, mouseY, button); + } + return false; + } + + @Override + public @NotNull Result onMouseTapped(double mouseX, double mouseY, int button) { + Object o = this.text.getHoveringElement(getContext().getFont(), getContext().getMouseX(), + getContext().getMouseY()); + if (o instanceof Interactable interactable) { + return interactable.onMouseTapped(mouseX, mouseY, button); + } + return Result.IGNORE; + } + + @Override + public @NotNull Result onKeyPressed(int keyCode, int scanCode, int modifiers) { + Object o = this.text.getHoveringElement(getContext().getFont(), getContext().getMouseX(), + getContext().getMouseY()); + if (o instanceof Interactable interactable) { + return interactable.onKeyPressed(keyCode, scanCode, modifiers); + } + return Result.ACCEPT; + } + + @Override + public boolean onKeyReleased(int keyCode, int scanCode, int modifiers) { + Object o = this.text.getHoveringElement(getContext().getFont(), getContext().getMouseX(), + getContext().getMouseY()); + if (o instanceof Interactable interactable) { + return interactable.onKeyReleased(keyCode, scanCode, modifiers); + } + return false; + } + + @Override + public @NotNull Result onKeyTapped(int keyCode, int scanCode, int modifiers) { + Object o = this.text.getHoveringElement(getContext().getFont(), getContext().getMouseX(), + getContext().getMouseY()); + if (o instanceof Interactable interactable) { + return interactable.onKeyTapped(keyCode, scanCode, modifiers); + } + return Result.ACCEPT; + } + + @Override + public boolean onMouseScrolled(double mouseX, double mouseY, double delta) { + Object o = this.text.getHoveringElement(getContext().getFont(), getContext().getMouseX(), + getContext().getMouseY()); + if (o instanceof Interactable interactable) { + return interactable.onMouseScrolled(mouseX, mouseY, delta); + } + return false; + } + + @Override + public void onMouseDrag(double mouseX, double mouseY, int button, double dragX, double dragY) { + Object o = this.text.getHoveringElement(getContext().getFont(), getContext().getMouseX(), + getContext().getMouseY()); + if (o instanceof Interactable interactable) { + interactable.onMouseDrag(mouseX, mouseY, button, dragX, dragY); + } + } + + @Nullable + public Object getHoveredElement() { + if (!isHovering()) return null; + getContext().pushMatrix(); + getContext().translate(getArea().x, getArea().y); + Object o = this.text.getHoveringElement(getContext()); + getContext().popMatrix(); + return o; + } + + @Override + public IRichTextBuilder getRichText() { + return text; + } + + /** + * Sets the auto update property. If auto update is true the text will be deleted each time it is drawn. + * If {@link #builder} is not null, it will then be called. + * + * @param autoUpdate auto update + * @return this + */ + public RichTextWidget autoUpdate(boolean autoUpdate) { + this.autoUpdate = autoUpdate; + return this; + } + + /** + * A builder which is called every time before drawing when {@link #dirty} is true. + * + * @param builder text builder + * @return this + */ + public RichTextWidget textBuilder(Consumer builder) { + this.builder = builder; + markDirty(); + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SchemaWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SchemaWidget.java new file mode 100644 index 00000000000..f4938db48bd --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SchemaWidget.java @@ -0,0 +1,183 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.base.widget.Interactable; +import com.gregtechceu.gtceu.api.mui.drawable.GuiTextures; +import com.gregtechceu.gtceu.api.mui.drawable.SchemaRenderer; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.utils.VectorUtil; +import com.gregtechceu.gtceu.utils.fakelevel.ISchema; + +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3f; + +import static net.minecraft.util.Mth.PI; +import static net.minecraft.util.Mth.TWO_PI; + +public class SchemaWidget extends Widget implements Interactable { + + private final SchemaRenderer schema; + private boolean enableRotation = true; + private boolean enableTranslation = true; + private boolean enableScaling = true; + private float lastMouseX; + private float lastMouseY; + private double scale = 10; + private float pitch = (float) (Math.PI / 4f); + private float yaw = (float) (Math.PI / 4f); + private final Vector3f offset = new Vector3f(); + + public SchemaWidget(ISchema schema) { + this(new SchemaRenderer(schema)); + } + + public SchemaWidget(SchemaRenderer schema) { + this.schema = schema; + schema.cameraFunc((camera, $schema) -> { + Vector3f focus = VectorUtil.vec3fAdd(this.offset, null, $schema.getFocus()); + camera.setLookAt(focus, scale, yaw, pitch); + }); + } + + @Override + public boolean onMouseScrolled(double mouseX, double mouseY, double delta) { + if (this.enableScaling) { + scale(delta / 120.0); + return true; + } + return false; + } + + @Override + public @NotNull Result onMousePressed(double mouseX, double mouseY, int button) { + this.lastMouseX = getContext().getMouseX(); + this.lastMouseY = getContext().getMouseY(); + return Result.SUCCESS; + } + + @Override + public void onMouseDrag(double mouseX, double mouseY, int button, double dragX, double dragY) { + float dx = (float) mouseX - lastMouseX; + float dy = (float) mouseY - lastMouseY; + if (mouseX == 0 && this.enableRotation) { + float moveScale = 0.025f; + yaw = (yaw + dx * moveScale + TWO_PI) % TWO_PI; + pitch = Mth.clamp(pitch + dy * moveScale, -TWO_PI / 4 + 0.001f, TWO_PI / 4 - 0.001f); + } else if (mouseX == 2 && this.enableTranslation) { + // the idea is to construct a vector which points upwards from the camera pov (y-axis on screen) + // this vector determines the amount of z offset from mouse movement in y + float y = (float) Math.cos(pitch); + float moveScale = 0.06f; + // with this the offset can be moved by dy + offset.add(0, dy * y * moveScale, 0); + // to respect dx we need a new vector which is perpendicular on the previous vector (x-axis on screen) + // y = 0 => mouse movement in x does not move y + float phi = (yaw + PI / 2) % TWO_PI; + float x = (float) Math.cos(phi); + float z = (float) Math.sin(phi); + offset.add(dx * x * moveScale, 0, dx * z * moveScale); + } + this.lastMouseX = (float) mouseX; + this.lastMouseY = (float) mouseY; + } + + public SchemaWidget scale(double scale) { + this.scale += scale; + return this; + } + + public SchemaWidget pitch(float pitch) { + this.pitch += pitch; + return this; + } + + public SchemaWidget yaw(float yaw) { + this.yaw += yaw; + return this; + } + + public SchemaWidget offset(float x, float y, float z) { + this.offset.set(x, y, z); + return this; + } + + public SchemaWidget enableDragRotation(boolean enable) { + this.enableRotation = enable; + return this; + } + + public SchemaWidget enableDragTranslation(boolean enable) { + this.enableTranslation = enable; + return this; + } + + public SchemaWidget enableScrollScaling(boolean enable) { + this.enableScaling = enable; + return this; + } + + public SchemaWidget enableInteraction(boolean rotation, boolean translation, boolean scaling) { + return enableDragRotation(rotation) + .enableDragTranslation(translation) + .enableScrollScaling(scaling); + } + + public SchemaWidget enableAllInteraction(boolean enable) { + return enableInteraction(enable, enable, enable); + } + + @Override + public @Nullable IDrawable getOverlay() { + return schema; + } + + public static class LayerButton extends ButtonWidget { + + private final int minLayer; + private final int maxLayer; + private int currentLayer = Integer.MIN_VALUE; + + public LayerButton(ISchema schema, int minLayer, int maxLayer) { + this.minLayer = minLayer; + this.maxLayer = maxLayer; + background(GuiTextures.MC_BACKGROUND); + overlay(IKey.dynamic(() -> currentLayer > Integer.MIN_VALUE ? + Component.literal(Integer.toString(currentLayer)) : Component.literal("ALL")).scale(0.5f)); + + onMousePressed((mouseX, mouseY, button) -> { + if (button == 0 || button == 1) { + if (button == 0) { + if (currentLayer == Integer.MIN_VALUE) { + currentLayer = minLayer; + } else { + currentLayer++; + } + } else { + if (currentLayer == Integer.MIN_VALUE) { + currentLayer = maxLayer; + } else { + currentLayer--; + } + } + if (currentLayer > maxLayer || currentLayer < minLayer) { + currentLayer = Integer.MIN_VALUE; + } + return true; + } + return false; + }); + schema.setRenderFilter( + (blockPos, blockInfo) -> currentLayer == Integer.MIN_VALUE || currentLayer >= blockPos.getY()); + } + + public LayerButton startLayer(int start) { + this.currentLayer = start; + return this; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ScrollingTextWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ScrollingTextWidget.java new file mode 100644 index 00000000000..a7fc842404d --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ScrollingTextWidget.java @@ -0,0 +1,111 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.drawable.text.TextRenderer; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import net.minecraft.util.FormattedCharSequence; + +import org.jetbrains.annotations.Nullable; + +public class ScrollingTextWidget extends TextWidget { + + private static final int pauseTime = 60; + + private TextRenderer.Line line = new TextRenderer.Line(FormattedCharSequence.EMPTY, 0); + private long time = 0; + private int scroll = 0; + private boolean hovering = false; + private int pauseTimer = 0; + + public ScrollingTextWidget(IKey key) { + super(key); + tooltipBuilder(tooltip -> { + tooltip.showUpTimer(10); + if (this.line.width() > getArea().width) { + tooltip.addLine(key); + } + }); + } + + @Override + public void onMouseStartHover() { + this.hovering = true; + } + + @Override + public void onMouseEndHover() { + this.hovering = false; + this.scroll = 0; + this.time = 0; + } + + @Override + public void onUpdate() { + super.onUpdate(); + if (this.pauseTimer > 0) { + if (++this.pauseTimer == pauseTime) { + this.pauseTimer = this.scroll == 0 ? 0 : 1; + this.scroll = 0; + } + return; + } + if (this.hovering && ++this.time % 2 == 0 && ++this.scroll == this.line.upperWidth() - getArea().width - 1) { + this.pauseTimer = 1; + } + } + + @Override + public void draw(ModularGuiContext context, WidgetTheme widgetTheme) { + checkString(); + TextRenderer renderer = TextRenderer.SHARED; + renderer.setColor(getColor() != null ? getColor() : widgetTheme.getTextColor()); + renderer.setAlignment(getAlignment(), getArea().w() + 1, getArea().h()); + renderer.setShadow(isShadow() != null ? isShadow() : widgetTheme.getTextShadow()); + renderer.setPos(getArea().getPadding().left, getArea().getPadding().top); + renderer.setScale(getScale()); + renderer.setSimulate(false); + if (this.hovering) { + renderer.drawScrolling(context.getGraphics(), this.line, this.scroll, getArea(), context); + } else { + renderer.drawCut(context.getGraphics(), this.line); + } + } + + private void checkString() { + var s = getKey().get().getVisualOrderText(); + if (!s.equals(this.line.text())) { + TextRenderer.SHARED.setScale(getScale()); + this.line = TextRenderer.SHARED.line(s); + this.scroll = 0; + markTooltipDirty(); + } + } + + @Override + public ScrollingTextWidget alignment(Alignment alignment) { + return (ScrollingTextWidget) super.alignment(alignment); + } + + @Override + public ScrollingTextWidget color(@Nullable Integer color) { + return (ScrollingTextWidget) super.color(color); + } + + @Override + public ScrollingTextWidget scale(float scale) { + return (ScrollingTextWidget) super.scale(scale); + } + + @Override + public ScrollingTextWidget shadow(@Nullable Boolean shadow) { + return (ScrollingTextWidget) super.shadow(shadow); + } + + @Override + public ScrollingTextWidget widgetTheme(String widgetTheme) { + return (ScrollingTextWidget) super.widgetTheme(widgetTheme); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SliderWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SliderWidget.java new file mode 100644 index 00000000000..a2f46cc1e94 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SliderWidget.java @@ -0,0 +1,281 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.value.IDoubleValue; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiAction; +import com.gregtechceu.gtceu.api.mui.base.widget.Interactable; +import com.gregtechceu.gtceu.api.mui.drawable.GuiTextures; +import com.gregtechceu.gtceu.api.mui.drawable.Rectangle; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.value.DoubleValue; +import com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Unit; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import net.minecraft.util.Mth; + +import it.unimi.dsi.fastutil.doubles.DoubleArrayList; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.jetbrains.annotations.NotNull; + +@Accessors(chain = true) +public class SliderWidget extends Widget implements Interactable { + + private IDoubleValue doubleValue; + private IDrawable stopperDrawable = new Rectangle().setColor(Color.withAlpha(Color.WHITE.main, 0.4f)); + private IDrawable handleDrawable = GuiTextures.BUTTON_CLEAN; + @Setter + private GuiAxis axis = GuiAxis.X; + private DoubleList stopper; + private int stopperWidth = 2, stopperHeight = 4; + private final Unit sliderWidth = new Unit(), sliderHeight = new Unit(); + private final Area sliderArea = new Area(); + @Getter + private double min, max; + private double each = 0; + @Getter + private boolean dragging = false; + + private double cache = Double.MIN_VALUE; + + public SliderWidget() { + sliderHeight(1f).sliderWidth(6); + listenGuiAction((IGuiAction.MouseReleased) (mouseX, mouseY, button) -> { + boolean val = this.dragging; + this.dragging = false; + return val; + }); + bounds(0, 100); + } + + @Override + public void onInit() { + if (this.doubleValue == null) { + this.doubleValue = new DoubleValue((this.max - this.min) * 0.5 + this.min); + } + if (this.each > 0 && this.stopper == null) { + this.stopper = new DoubleArrayList(); + for (double d = this.min; d < this.max; d += this.each) { + this.stopper.add(d); + } + this.stopper.add(this.max); + } + } + + @Override + public boolean isValidSyncHandler(SyncHandler syncHandler) { + this.doubleValue = castIfTypeElseNull(syncHandler, IDoubleValue.class); + return this.doubleValue != null; + } + + @Override + public void drawBackground(ModularGuiContext context, WidgetTheme widgetTheme) { + super.drawBackground(context, widgetTheme); + if (this.stopper != null && this.stopperDrawable != null && this.stopperWidth > 0 && this.stopperHeight > 0) { + for (double stop : this.stopper) { + int pos = valueToPos(stop) + this.sliderArea.getSize(this.axis) / 2; + if (this.axis.isHorizontal()) { + pos -= this.stopperWidth / 2; + int crossAxisPos = (int) (getArea().height / 2D - this.stopperHeight / 2D); + this.stopperDrawable.draw(context, pos, crossAxisPos, this.stopperWidth, this.stopperHeight, + WidgetTheme.getDefault()); + } else { + pos -= this.stopperHeight / 2; + int crossAxisPos = (int) (getArea().width / 2D - this.stopperWidth / 2D); + this.stopperDrawable.draw(context, crossAxisPos, pos, this.stopperWidth, this.stopperHeight, + WidgetTheme.getDefault()); + } + } + } + } + + @Override + public void draw(ModularGuiContext context, WidgetTheme widgetTheme) { + if (this.handleDrawable != null) { + this.handleDrawable.draw(context, this.sliderArea, context.getTheme().getButtonTheme()); + } + } + + @Override + public void onResized() { + float sw = this.sliderWidth.getValue(); + if (this.sliderWidth.isRelative()) sw *= getArea().width; + float sh = this.sliderHeight.getValue(); + if (this.sliderHeight.isRelative()) sh *= getArea().height; + this.sliderArea.setSize((int) sw, (int) sh); + GuiAxis other = this.axis.getOther(); + this.sliderArea.setPoint(other, getArea().getSize(other) / 2 - this.sliderArea.getSize(other) / 2); + } + + @Override + public void postResize() { + setValue(getSliderValue(), false); + } + + @Override + public void onUpdate() { + super.onUpdate(); + if (this.dragging) return; + double val = getSliderValue(); + if (this.cache != val) { + setValue(val, false); + } + } + + @Override + public @NotNull Result onMousePressed(double mouseX, double mouseY, int button) { + int p = this.axis.isHorizontal() ? + getContext().unTransformX(getContext().getMouseX(), getContext().getMouseY()) : + getContext().unTransformY(getContext().getMouseX(), getContext().getMouseY()); + setValue(posToValue(p), true); + this.dragging = true; + return Result.SUCCESS; + } + + @Override + public void onMouseDrag(double mouseX, double mouseY, int button, double dragX, double dragY) { + if (this.dragging) { + onMousePressed(mouseX, mouseY, button); + } + } + + public double posToValue(int p) { + double v = (p - this.sliderArea.getSize(this.axis) / 2D) / + (double) (getArea().getSize(this.axis) - this.sliderArea.getSize(this.axis)); + return v * (this.max - this.min) + this.min; + } + + public int valueToPos(double value) { + value -= this.min; + value /= (this.max - this.min); + return (int) (value * (getArea().getSize(this.axis) - this.sliderArea.getSize(this.axis))); + } + + public double getSliderValue() { + return this.doubleValue == null ? 0.0 : this.doubleValue.getDoubleValue(); + } + + public void setValue(double value, boolean setSource) { + if (this.stopper != null && !this.stopper.isEmpty()) { + double lastDistance = Double.MAX_VALUE; + boolean found = false; + for (int i = 0; i < this.stopper.size(); i++) { + double dist = Math.abs(this.stopper.getDouble(i) - value); + if (dist < lastDistance) { + lastDistance = dist; + } else if (dist > lastDistance) { + value = this.stopper.getDouble(i - 1); + found = true; + break; + } + } + if (!found && lastDistance < Double.MAX_VALUE) { + value = this.stopper.getDouble(this.stopper.size() - 1); + } + } + value = Mth.clamp(value, this.min, this.max); + this.cache = value; + this.sliderArea.setPoint(this.axis, valueToPos(value)); + if (setSource) { + this.doubleValue.setDoubleValue(value); + } + } + + @Override + public String toString() { + return super.toString() + " # " + getSliderValue(); + } + + public SliderWidget value(IDoubleValue value) { + this.doubleValue = value; + setValue(value); + return this; + } + + public SliderWidget bounds(double min, double max) { + this.max = Math.max(min, max); + this.min = Math.min(min, max); + return this; + } + + public SliderWidget stopper(DoubleList stopper) { + if (this.stopper == null) this.stopper = new DoubleArrayList(); + this.stopper.addAll(stopper); + this.stopper.sort(Double::compare); + return this; + } + + public SliderWidget stopper(double... stopper) { + if (this.stopper == null) this.stopper = new DoubleArrayList(); + for (double stop : stopper) { + this.stopper.add(stop); + } + this.stopper.sort(Double::compare); + return this; + } + + public SliderWidget stopper(double each) { + this.each = each; + return this; + } + + public SliderWidget setAxis(GuiAxis axis) { + this.axis = axis; + return this; + } + + public SliderWidget sliderWidth(int w) { + this.sliderWidth.setValue(w); + this.sliderWidth.setMeasure(Unit.Measure.PIXEL); + return this; + } + + public SliderWidget sliderWidth(float w) { + this.sliderWidth.setValue(w); + this.sliderWidth.setMeasure(Unit.Measure.RELATIVE); + return this; + } + + public SliderWidget sliderHeight(int h) { + this.sliderHeight.setValue(h); + this.sliderHeight.setMeasure(Unit.Measure.PIXEL); + return this; + } + + public SliderWidget sliderHeight(float h) { + this.sliderHeight.setValue(h); + this.sliderHeight.setMeasure(Unit.Measure.RELATIVE); + return this; + } + + public SliderWidget sliderSize(int w, int h) { + return sliderWidth(w).sliderHeight(h); + } + + public SliderWidget sliderSize(float w, float h) { + return sliderWidth(w).sliderHeight(h); + } + + public SliderWidget sliderTexture(IDrawable sliderTexture) { + this.handleDrawable = sliderTexture; + return this; + } + + public SliderWidget stopperTexture(IDrawable sliderTexture) { + this.stopperDrawable = sliderTexture; + return this; + } + + public SliderWidget stopperSize(int w, int h) { + this.stopperWidth = w; + this.stopperHeight = h; + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SlotGroupWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SlotGroupWidget.java new file mode 100644 index 00000000000..b0591e86e7e --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SlotGroupWidget.java @@ -0,0 +1,160 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.widget.ISynced; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.widget.ParentWidget; +import com.gregtechceu.gtceu.api.mui.widgets.slot.ItemSlot; +import com.gregtechceu.gtceu.common.mui.GTGuiTextures; + +import it.unimi.dsi.fastutil.chars.Char2IntMap; +import it.unimi.dsi.fastutil.chars.Char2IntOpenHashMap; +import it.unimi.dsi.fastutil.chars.Char2ObjectMap; +import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.IntFunction; + +public class SlotGroupWidget extends ParentWidget { + + public static SlotGroupWidget playerInventory(boolean positioned) { + return positioned ? playerInventory(7, true) : playerInventory((index, slot) -> slot); + } + + public static SlotGroupWidget playerInventory(boolean positioned, SlotConsumer slotConsumer) { + return positioned ? playerInventory(7, true, slotConsumer) : playerInventory(slotConsumer); + } + + public static SlotGroupWidget playerInventory(int bottom, boolean horizontalCentered) { + return playerInventory(bottom, horizontalCentered, (index, slot) -> slot); + } + + public static SlotGroupWidget playerInventory(int bottom, boolean horizontalCentered, SlotConsumer slotConsumer) { + SlotGroupWidget widget = playerInventory(slotConsumer); + if (bottom != 0) widget.bottom(bottom); + if (horizontalCentered) widget.leftRel(0.5f); + return widget; + } + + /** + * Automatically creates and places the player inventory. + * + * @return player inventory group + */ + public static SlotGroupWidget playerInventory(SlotConsumer slotConsumer) { + SlotGroupWidget slotGroupWidget = new SlotGroupWidget(); + slotGroupWidget.coverChildren(); + slotGroupWidget.debugName("player_inventory"); + String key = "player"; + for (int i = 0; i < 9; i++) { + slotGroupWidget.child(slotConsumer.apply(i, new ItemSlot()) + .background(GTGuiTextures.SLOT) + .syncHandler(key, i) + .pos(i * 18, 3 * 18 + 4) + .debugName("slot_" + i)); + } + for (int i = 0; i < 27; i++) { + slotGroupWidget.child(slotConsumer.apply(i + 9, new ItemSlot()) + .background(GTGuiTextures.SLOT) + .syncHandler(key, i + 9) + .pos(i % 9 * 18, i / 9 * 18) + .debugName("slot_" + (i + 9))); + } + return slotGroupWidget; + } + + public interface SlotConsumer { + + ItemSlot apply(int index, ItemSlot slot); + } + + private String slotsKeyName; + + public void setSlotsSynced(String name) { + this.slotsKeyName = name; + int i = 0; + for (IWidget widget : getChildren()) { + if (widget instanceof ISynced synced) { + synced.syncHandler(name, i); + } + i++; + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private String syncKey; + private final List matrix = new ArrayList<>(); + private final Char2ObjectMap keys = new Char2ObjectOpenHashMap<>(); + + private Builder() { + this.keys.put(' ', null); + } + + public Builder synced(String name) { + this.syncKey = name; + return this; + } + + public Builder matrix(String... matrix) { + this.matrix.clear(); + Collections.addAll(this.matrix, matrix); + return this; + } + + public Builder row(String row) { + this.matrix.add(row); + return this; + } + + public Builder key(char c, IWidget widget) { + this.keys.put(c, widget); + return this; + } + + public Builder key(char c, IntFunction widget) { + this.keys.put(c, widget); + return this; + } + + public SlotGroupWidget build() { + SlotGroupWidget slotGroupWidget = new SlotGroupWidget(); + Char2IntMap charCount = new Char2IntOpenHashMap(); + int x = 0, y = 0, maxWidth = 0; + int syncId = 0; + for (String row : this.matrix) { + for (int i = 0; i < row.length(); i++) { + char c = row.charAt(i); + int count = charCount.get(c); + charCount.put(c, count + 1); + Object o = this.keys.get(c); + IWidget widget; + if (o instanceof IWidget iWidget) { + widget = iWidget; + } else if (o instanceof IntFunction function) { + widget = (IWidget) function.apply(count); + } else { + x += 18; + continue; + } + widget.flex().left(x).top(y); + slotGroupWidget.child(widget); + if (this.syncKey != null && widget instanceof ISynced synced) { + synced.syncHandler(this.syncKey, syncId++); + } + x += 18; + maxWidth = Math.max(maxWidth, x); + } + y += 18; + x = 0; + } + slotGroupWidget.flex().size(maxWidth, this.matrix.size() * 18); + return slotGroupWidget; + } + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SortButtons.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SortButtons.java new file mode 100644 index 00000000000..751e2a9393b --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SortButtons.java @@ -0,0 +1,83 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.api.mui.widgets.slot.SlotGroup; + +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.List; + +public class SortButtons extends Widget { + + @Getter + private String slotGroupName; + @Getter + private SlotGroup slotGroup; + + private boolean horizontal = true; + private final ButtonWidget sortButton = new ButtonWidget<>(); + private final ButtonWidget settingsButton = new ButtonWidget<>(); + @Getter + private final @NotNull List children = Arrays.asList(sortButton, settingsButton); + + @Override + public void onInit() { + super.onInit(); + this.slotGroup = getScreen().getContainer().validateSlotGroup(getPanel().getName(), this.slotGroupName, + this.slotGroup); + if (!this.slotGroup.isAllowSorting()) { + throw new IllegalStateException("Slot group can't be sorted!"); + } + /* + * TODO bogosort doesn't exist (yet), choose some other sorting mod to add compat for? + * this.sortButton.size(10).pos(0, 0) + * .overlay(IKey.str("z")) + * .onMousePressed(mouseButton -> { + * IBogoSortAPI.getInstance().sortSlotGroup(this.slotGroup.getSlots().get(0)); + * return true; + * }); + * this.settingsButton.size(10) + * .overlay(IKey.str("...")) + * .onMousePressed(mouseButton -> { + * IBogoSortAPI.getInstance().openConfigGui(); + * return true; + * }); + */ + if (this.horizontal) { + size(20, 10); + this.settingsButton.pos(10, 0); + } else { + size(10, 20); + this.settingsButton.pos(0, 10); + } + } + + @Override + public boolean isEnabled() { + // TODO bogosort doesn't exist (yet), pick some other sorting mod to add compat for + return false; // return super.isEnabled() && false; ModularUI.isSortModLoaded(); + } + + public SortButtons slotGroup(String slotGroupName) { + this.slotGroupName = slotGroupName; + return this; + } + + public SortButtons slotGroup(SlotGroup slotGroup) { + this.slotGroup = slotGroup; + return this; + } + + public SortButtons horizontal() { + this.horizontal = true; + return this; + } + + public SortButtons vertical() { + this.horizontal = false; + return this; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SortableListWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SortableListWidget.java new file mode 100644 index 00000000000..7e68a1ba19b --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/SortableListWidget.java @@ -0,0 +1,202 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.GTCEu; +import com.gregtechceu.gtceu.api.mui.base.widget.IGuiElement; +import com.gregtechceu.gtceu.api.mui.base.widget.IValueWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.drawable.GuiTextures; +import com.gregtechceu.gtceu.api.mui.widget.DraggableWidget; +import com.gregtechceu.gtceu.api.mui.widget.WidgetTree; + +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; + +public class SortableListWidget extends ListValueWidget, SortableListWidget> { + + private Consumer> onChange; + private Consumer> onRemove; + private int timeSinceLastMove = 0; + + public SortableListWidget() { + super(Item::getWidgetValue); + heightRel(1f); + } + + @Override + public void onInit() { + super.onInit(); + assignIndexes(); + } + + @Override + public void onUpdate() { + super.onUpdate(); + this.timeSinceLastMove++; + } + + @Override + public int getDefaultWidth() { + return 80; + } + + public void moveTo(int from, int to) { + if (this.timeSinceLastMove < 3) return; + if (from < 0 || to < 0 || from == to) { + GTCEu.LOGGER.error("Failed to move element from {} to {}", from, to); + return; + } + Item child = getTypeChildren().remove(from); + getChildren().add(to, child); + assignIndexes(); + if (isValid()) { + WidgetTree.resize(this); + } + if (this.onChange != null) { + this.onChange.accept(getValues()); + } + this.timeSinceLastMove = 0; + } + + @Override + public boolean remove(int index) { + Item widget = getTypeChildren().remove(index); + if (widget != null) { + onChildRemove(widget); + assignIndexes(); + if (isValid()) { + WidgetTree.resize(this); + } + if (this.onChange != null) { + this.onChange.accept(getValues()); + } + if (this.onRemove != null) { + this.onRemove.accept(widget); + } + return true; + } + return false; + } + + @Override + public void onChildAdd(Item child) { + if (isValid()) { + assignIndexes(); + if (this.onChange != null) this.onChange.accept(getValues()); + WidgetTree.resize(this); + } + } + + private void assignIndexes() { + List> children = getTypeChildren(); + for (int i = 0; i < children.size(); i++) { + children.get(i).index = i; + } + } + + public SortableListWidget onChange(Consumer> onChange) { + this.onChange = onChange; + return this; + } + + public SortableListWidget onRemove(Consumer> onRemove) { + this.onRemove = onRemove; + return this; + } + + public static class Item extends DraggableWidget> implements IValueWidget { + + private final T value; + private List children; + private Predicate dropPredicate; + private SortableListWidget listWidget; + @Getter + private int index = -1; + + public Item(T value) { + this.value = value; + flex().widthRel(1f).height(18); + background(GuiTextures.BUTTON_CLEAN); + } + + @Override + public void onInit() { + super.onInit(); + if (getParent() instanceof SortableListWidget sortableListWidget) { + this.listWidget = (SortableListWidget) sortableListWidget; + } + } + + @NotNull + @Override + public List getChildren() { + return this.children != null ? this.children : Collections.emptyList(); + } + + @Override + public boolean canDropHere(int x, int y, @Nullable IGuiElement widget) { + return this.dropPredicate == null || this.dropPredicate.test(widget); + } + + @Override + public void onDrag(int mouseButton, double timeSinceLastClick) { + super.onDrag(mouseButton, timeSinceLastClick); + IWidget hovered = getContext().getHovered(); + Item item = WidgetTree.findParent(hovered, Item.class); + if (item != null && item != this && item.listWidget == this.listWidget) { + this.listWidget.moveTo(this.index, item.index); + } + } + + @Override + public void onDragEnd(boolean successful) {} + + @Override + public T getWidgetValue() { + return this.value; + } + + public boolean removeSelfFromList() { + this.listWidget.remove(this.index); + return true; + } + + public Item child(IWidget widget) { + this.children = Collections.singletonList(widget); + if (isValid()) widget.initialise(this); + return this; + } + + public Item child(Function, IWidget> widgetCreator) { + return child(widgetCreator.apply(this)); + } + + public Item dropPredicate(Predicate dropPredicate) { + this.dropPredicate = dropPredicate; + return this; + } + + /* + * public Item removeable() { + * this.removeButton = new ButtonWidget<>() + * .onMousePressed(mouseButton -> this.listWidget.remove(this.index)) + * .background(GuiTextures.CLOSE.asIcon()) + * .width(10).heightRel(1f) + * .right(0); + * return this; + * } + * + * public Item removeable(Consumer>> buttonBuilder) { + * removeable(); + * buttonBuilder.accept(this.removeButton); + * return this; + * } + */ + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/TextWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/TextWidget.java new file mode 100644 index 00000000000..f370f9a62fb --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/TextWidget.java @@ -0,0 +1,110 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.drawable.text.TextRenderer; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Box; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; + +import net.minecraft.ChatFormatting; + +import lombok.Getter; +import org.jetbrains.annotations.Nullable; + +public class TextWidget extends Widget { + + @Getter + private final IKey key; + @Getter + private Alignment alignment = Alignment.CenterLeft; + @Getter + private Integer color = null; + @Getter + private Boolean shadow = null; + @Getter + private float scale = 1f; + + public TextWidget(IKey key) { + this.key = key; + } + + @Override + public void draw(ModularGuiContext context, WidgetTheme widgetTheme) { + TextRenderer renderer = TextRenderer.SHARED; + renderer.setColor(this.color != null ? this.color : widgetTheme.getTextColor()); + renderer.setAlignment(this.alignment, getArea().w() + this.scale, getArea().h()); + renderer.setShadow(this.shadow != null ? this.shadow : widgetTheme.getTextShadow()); + renderer.setPos(getArea().getPadding().left, getArea().getPadding().top); + renderer.setScale(this.scale); + renderer.setSimulate(false); + renderer.draw(context.getGraphics(), this.key.getFormatted()); + } + + private TextRenderer simulate(float maxWidth) { + Box padding = getArea().getPadding(); + TextRenderer renderer = TextRenderer.SHARED; + renderer.setAlignment(Alignment.TopLeft, maxWidth); + renderer.setPos(padding.left, padding.top); + renderer.setScale(this.scale); + renderer.setSimulate(true); + renderer.draw(null, this.key.getFormatted()); + return renderer; + } + + @Override + public int getDefaultHeight() { + float maxWidth; + if (resizer().isWidthCalculated()) { + maxWidth = getArea().width + this.scale; + } else if (getParent().resizer().isWidthCalculated()) { + maxWidth = getParent().getArea().width + this.scale; + } else { + maxWidth = getScreen().getScreenArea().width; + } + TextRenderer renderer = simulate(maxWidth); + Box padding = getArea().getPadding(); + return Math.max(1, (int) (renderer.getLastHeight() + padding.vertical() + 0.5f)); + } + + @Override + public int getDefaultWidth() { + float maxWidth = getScreen().getScreenArea().width; + if (getParent().resizer().isWidthCalculated()) { + maxWidth = getParent().getArea().width; + } + TextRenderer renderer = simulate(maxWidth); + Box padding = getArea().getPadding(); + return Math.max(1, (int) (renderer.getLastWidth() + padding.horizontal() + 0.5f)); + } + + public TextWidget alignment(Alignment alignment) { + this.alignment = alignment; + return this; + } + + public TextWidget color(@Nullable Integer color) { + this.color = color; + return this; + } + + public TextWidget scale(float scale) { + this.scale = scale; + return this; + } + + public TextWidget shadow(@Nullable Boolean shadow) { + this.shadow = shadow; + return this; + } + + public TextWidget style(ChatFormatting formatting) { + this.key.style(formatting); + return this; + } + + public Boolean isShadow() { + return this.getShadow(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ToggleButton.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ToggleButton.java new file mode 100644 index 00000000000..06aba58b337 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ToggleButton.java @@ -0,0 +1,94 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.value.IBoolValue; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.theme.WidgetThemeSelectable; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +import java.util.function.Consumer; + +@Accessors(fluent = true, chain = true) +public class ToggleButton extends AbstractCycleButtonWidget { + + @Getter + @Setter + private boolean invertSelected = false; + + public ToggleButton() { + stateCount(2); + } + + @Override + public WidgetTheme getWidgetThemeInternal(ITheme theme) { + WidgetThemeSelectable widgetTheme = theme.getToggleButtonTheme(); + return isValueSelected() ^ invertSelected() ? widgetTheme.getSelected() : widgetTheme; + } + + public boolean isValueSelected() { + return getState() == 1; + } + + public ToggleButton value(IBoolValue boolValue) { + return super.value(boolValue); + } + + public ToggleButton selectedBackground(IDrawable... selectedBackground) { + return background(true, selectedBackground); + } + + public ToggleButton selectedHoverBackground(IDrawable... selectedHoverBackground) { + return hoverBackground(true, selectedHoverBackground); + } + + @Override + public ToggleButton background(IDrawable... selectedBackground) { + return background(false, selectedBackground); + } + + @Override + public ToggleButton hoverBackground(IDrawable... selectedHoverBackground) { + return hoverBackground(false, selectedHoverBackground); + } + + public ToggleButton background(boolean selected, IDrawable... background) { + this.background = addToArray(this.background, background, selected ? 1 : 0); + return this; + } + + public ToggleButton overlay(boolean selected, IDrawable... overlay) { + this.overlay = addToArray(this.overlay, overlay, selected ? 1 : 0); + return this; + } + + public ToggleButton hoverBackground(boolean selected, IDrawable... background) { + this.hoverBackground = addToArray(this.hoverBackground, background, selected ? 1 : 0); + return this; + } + + public ToggleButton hoverOverlay(boolean selected, IDrawable... overlay) { + this.hoverOverlay = addToArray(this.hoverOverlay, overlay, selected ? 1 : 0); + return this; + } + + public ToggleButton addTooltip(boolean selected, String tooltip) { + return super.addTooltip(selected ? 1 : 0, tooltip); + } + + public ToggleButton addTooltip(boolean selected, IDrawable tooltip) { + return super.addTooltip(selected ? 1 : 0, tooltip); + } + + public ToggleButton tooltip(boolean selected, Consumer builder) { + return super.tooltip(selected ? 1 : 0, builder); + } + + public ToggleButton tooltipBuilder(boolean selected, Consumer builder) { + return super.tooltipBuilder(selected ? 1 : 0, builder); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ValueWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ValueWidget.java new file mode 100644 index 00000000000..4d04c63aa96 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/ValueWidget.java @@ -0,0 +1,16 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.base.widget.IValueWidget; +import com.gregtechceu.gtceu.api.mui.widget.Widget; + +import lombok.Getter; + +public class ValueWidget, T> extends Widget implements IValueWidget { + + @Getter + private final T widgetValue; + + public ValueWidget(T widgetValue) { + this.widgetValue = widgetValue; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/VoidWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/VoidWidget.java new file mode 100644 index 00000000000..45b485069e4 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/VoidWidget.java @@ -0,0 +1,10 @@ +package com.gregtechceu.gtceu.api.mui.widgets; + +import com.gregtechceu.gtceu.api.mui.widget.EmptyWidget; + +public class VoidWidget extends EmptyWidget { + + private VoidWidget() { + throw new UnsupportedOperationException(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Column.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Column.java new file mode 100644 index 00000000000..4a3e6e03695 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Column.java @@ -0,0 +1,10 @@ +package com.gregtechceu.gtceu.api.mui.widgets.layout; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; + +public class Column extends Flow { + + public Column() { + super(GuiAxis.Y); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Flow.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Flow.java new file mode 100644 index 00000000000..3dd23028d2d --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Flow.java @@ -0,0 +1,190 @@ +package com.gregtechceu.gtceu.api.mui.widgets.layout; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; +import com.gregtechceu.gtceu.api.mui.base.layout.ILayoutWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.widget.ParentWidget; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Box; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Accessors(fluent = true, chain = true) +public class Flow extends ParentWidget implements ILayoutWidget, IExpander { + + public static Flow row() { + return new Flow(GuiAxis.X); + } + + public static Flow column() { + return new Flow(GuiAxis.Y); + } + + /** + * The main axis on which to align children. + */ + @Getter + private final GuiAxis axis; + /** + * How the children should be laid out on the main axis. + */ + @Setter + private Alignment.MainAxis mainAxisAlignment = Alignment.MainAxis.START; + /** + * How the children should be laid out on the cross axis. + */ + @Setter + private Alignment.CrossAxis crossAxisAlignment = Alignment.CrossAxis.CENTER; + /** + * Additional space between each child on main axis. + * Does not work with {@link Alignment.MainAxis#SPACE_BETWEEN} and {@link Alignment.MainAxis#SPACE_AROUND}. + */ + @Setter + private int childPadding = 0; + /** + * Whether disabled child widgets should be collapsed for display. + */ + @Setter + private boolean collapseDisabledChild = false; + + public Flow(GuiAxis axis) { + this.axis = axis; + sizeRel(1f, 1f); + } + + @Override + public void layoutWidgets() { + if (!hasChildren()) return; + final boolean hasSize = resizer().isSizeCalculated(this.axis); + final Box padding = getArea().getPadding(); + final int size = getArea().getSize(axis) - padding.getTotal(this.axis); + Alignment.MainAxis maa = this.mainAxisAlignment; + if (!hasSize && maa != Alignment.MainAxis.START) { + maa = Alignment.MainAxis.START; + } + int space = this.childPadding; + + int childrenSize = 0; + int expandedAmount = 0; + int amount = 0; + + // calculate total size + for (IWidget widget : getChildren()) { + // ignore disabled child if configured as such + if (shouldIgnoreChildSize(widget)) continue; + // exclude children whose position of main axis is fixed + if (widget.flex().hasPos(this.axis)) continue; + amount++; + if (widget.flex().isExpanded()) { + expandedAmount++; + childrenSize += widget.getArea().getMargin().getTotal(this.axis); + continue; + } + childrenSize += widget.getArea().requestedSize(this.axis); + } + + if (amount <= 1 && maa == Alignment.MainAxis.SPACE_BETWEEN) { + maa = Alignment.MainAxis.CENTER; + } + final int spaceCount = Math.max(amount - 1, 0); + + if (maa == Alignment.MainAxis.SPACE_BETWEEN || maa == Alignment.MainAxis.SPACE_AROUND) { + if (expandedAmount > 0) { + maa = Alignment.MainAxis.START; + } else { + space = 0; + } + } + childrenSize += space * spaceCount; + + if (expandedAmount > 0 && hasSize) { + int newSize = (size - childrenSize) / expandedAmount; + for (IWidget widget : getChildren()) { + // ignore disabled child if configured as such + if (shouldIgnoreChildSize(widget)) continue; + // exclude children whose position of main axis is fixed + if (widget.flex().hasPos(this.axis)) continue; + if (widget.flex().isExpanded()) { + widget.getArea().setSize(this.axis, newSize); + widget.resizer().setSizeResized(this.axis, true); + } + } + } + + // calculate start pos + int lastP = padding.getStart(this.axis); + if (hasSize) { + if (maa == Alignment.MainAxis.CENTER) { + lastP += (int) (size / 2f - childrenSize / 2f); + } else if (maa == Alignment.MainAxis.END) { + lastP += size - childrenSize; + } + } + + for (IWidget widget : getChildren()) { + // ignore disabled child if configured as such + if (shouldIgnoreChildSize(widget)) continue; + // exclude children whose position of main axis is fixed + if (widget.flex().hasPos(this.axis)) continue; + Box margin = widget.getArea().getMargin(); + + // set calculated relative main axis pos and set end margin for next widget + widget.getArea().setRelativePoint(this.axis, lastP + margin.getStart(this.axis)); + widget.resizer().setPosResized(this.axis, true); + widget.resizer().setMarginPaddingApplied(this.axis, true); + + lastP += widget.getArea().requestedSize(this.axis) + space; + if (hasSize && maa == Alignment.MainAxis.SPACE_BETWEEN) { + lastP += (size - childrenSize) / spaceCount; + } + } + } + + @Override + public void postLayoutWidgets() { + GuiAxis other = this.axis.getOther(); + int width = getArea().getSize(other); + Box padding = getArea().getPadding(); + boolean hasWidth = resizer().isSizeCalculated(other); + for (IWidget widget : getChildren()) { + // exclude children whose position of main axis is fixed + if (widget.flex().hasPos(this.axis)) continue; + Box margin = widget.getArea().getMargin(); + // don't align auto positioned children in cross axis + if (!widget.flex().hasPos(other) && widget.resizer().isSizeCalculated(other)) { + int crossAxisPos = margin.getStart(other) + padding.getStart(other); + if (hasWidth) { + if (this.crossAxisAlignment == Alignment.CrossAxis.CENTER) { + crossAxisPos = (int) (width / 2f - widget.getArea().getSize(other) / 2f); + } else if (this.crossAxisAlignment == Alignment.CrossAxis.END) { + crossAxisPos = width - widget.getArea().getSize(other) - margin.getEnd(other) - + padding.getStart(other); + } + } + widget.getArea().setRelativePoint(other, crossAxisPos); + widget.resizer().setPosResized(other, true); + widget.resizer().setMarginPaddingApplied(other, true); + } + } + } + + @Override + public boolean shouldIgnoreChildSize(IWidget child) { + return this.collapseDisabledChild && !child.isEnabled(); + } + + /** + * Configures this widget to collapse disabled child widgets. + */ + public Flow collapseDisabledChild() { + this.collapseDisabledChild = true; + return this; + } + + @Override + public GuiAxis getExpandAxis() { + return this.axis; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Grid.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Grid.java new file mode 100644 index 00000000000..a81083f0a28 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Grid.java @@ -0,0 +1,326 @@ +package com.gregtechceu.gtceu.api.mui.widgets.layout; + +import com.gregtechceu.gtceu.api.mui.base.layout.ILayoutWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.IParentWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.widget.AbstractScrollWidget; +import com.gregtechceu.gtceu.api.mui.widget.scroll.HorizontalScrollData; +import com.gregtechceu.gtceu.api.mui.widget.scroll.ScrollData; +import com.gregtechceu.gtceu.api.mui.widget.scroll.VerticalScrollData; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Area; +import com.gregtechceu.gtceu.api.mui.widget.sizer.Box; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.IntFunction; + +public class Grid extends AbstractScrollWidget implements ILayoutWidget, IParentWidget { + + private final List> matrix = new ArrayList<>(); + private final Box minElementMargin = new Box(); + private int minRowHeight = 5, minColWidth = 5; + private Alignment alignment = Alignment.Center; + private boolean dirty = false; + private boolean collapseDisabledChild = false; + + public Grid() { + super(null, null); + } + + @Override + public void onInit() { + super.onInit(); + int maxRowSize = 0; + for (List row : this.matrix) { + maxRowSize = Math.max(maxRowSize, row.size()); + } + for (List row : this.matrix) { + while (row.size() < maxRowSize) { + row.add(null); + } + } + } + + private int getElementWidth(Area area) { + return area.width + Math.max(area.getMargin().left, this.minElementMargin.left) + + Math.max(area.getMargin().right, this.minElementMargin.right); + } + + private int getElementHeight(Area area) { + return area.height + Math.max(area.getMargin().top, this.minElementMargin.top) + + Math.max(area.getMargin().bottom, this.minElementMargin.bottom); + } + + @Override + public void layoutWidgets() { + IntList rowSizes = new IntArrayList(); + IntList colSizes = new IntArrayList(); + + int i = 0, j; + for (List row : this.matrix) { + j = 0; + rowSizes.add(this.minRowHeight); + for (IWidget child : row) { + if (i == 0) { + colSizes.add(this.minColWidth); + } + if (!shouldIgnoreChildSize(child)) { + rowSizes.set(i, Math.max(rowSizes.getInt(i), getElementHeight(child.getArea()))); + colSizes.set(j, Math.max(colSizes.getInt(j), getElementWidth(child.getArea()))); + } + j++; + } + i++; + } + + int x = 0, y = 0; + for (int r = 0; r < rowSizes.size(); r++) { + x = 0; + int height = rowSizes.getInt(r); + for (int c = 0; c < colSizes.size(); c++) { + int width = colSizes.getInt(c); + IWidget child = this.matrix.get(r).get(c); + if (child != null) { + child.getArea().rx = (int) (x + (width - child.getArea().width) * alignment.x); + child.getArea().ry = (int) (y + (height - child.getArea().height) * alignment.y); + child.resizer().setPosResized(true, true); + } + x += width; + } + y += height; + } + if (getScrollArea().getScrollX() != null) { + getScrollArea().getScrollX().setScrollSize(x); + } + if (getScrollArea().getScrollY() != null) { + getScrollArea().getScrollY().setScrollSize(y); + } + } + + @Override + public boolean shouldIgnoreChildSize(IWidget child) { + return child == null || (this.collapseDisabledChild && !child.isEnabled()); + } + + @Override + public @NotNull List getChildren() { + if (this.dirty) { + makeFlatList(); + this.dirty = false; + } + return super.getChildren(); + } + + private void makeFlatList() { + super.getChildren().clear(); + super.getChildren().addAll(this.matrix.stream().flatMap(List::stream).filter(Objects::nonNull).toList()); + } + + @Override + public int getDefaultHeight() { + int h = 0; + for (List row : this.matrix) { + int rowHeight = 0; + for (IWidget child : row) { + if (!shouldIgnoreChildSize(child)) { + rowHeight = Math.max(rowHeight, getElementHeight(child.getArea())); + } + } + h += Math.min(rowHeight, this.minRowHeight); + } + return h; + } + + @Override + public int getDefaultWidth() { + IntList colSizes = new IntArrayList(); + int i = 0, j; + for (List row : this.matrix) { + j = 0; + for (IWidget child : row) { + if (i == 0) { + colSizes.add(this.minColWidth); + } + if (!shouldIgnoreChildSize(child)) { + colSizes.set(j, Math.max(colSizes.getInt(j), getElementWidth(child.getArea()))); + } + j++; + } + i++; + } + int w = 0; + for (int colWidth : colSizes) { + w += colWidth; + } + return w; + } + + public Grid matrix(List> matrix) { + this.matrix.clear(); + for (List row : matrix) { + this.matrix.add((List) row); + } + this.dirty = true; + return this; + } + + public Grid row(List row) { + this.matrix.add(row); + this.dirty = true; + return this; + } + + public Grid row(@NotNull IWidget... row) { + Objects.requireNonNull(row); + return row(Arrays.asList(row)); + } + + @Override + public boolean addChild(IWidget child, int index) { + if (child == this || getChildren().contains(child)) { + return false; + } + if (index < 0) { + index = getChildren().size() + index + 1; + } + super.getChildren().add(index, child); + if (isValid()) { + child.initialise(this); + } + onChildAdd(child); + this.dirty = true; + return true; + } + + public Grid child(@Nullable IWidget widget) { + this.matrix.get(this.matrix.size() - 1).add(widget); + this.dirty = true; + return this; + } + + public Grid nextRow() { + this.matrix.add(new ArrayList<>()); + return this; + } + + public Grid mapTo(int rowLength, @NotNull List list, + @NotNull IndexedElementMapper widgetCreator) { + Objects.requireNonNull(widgetCreator); + Objects.requireNonNull(list); + return matrix(mapToMatrix(rowLength, list, widgetCreator)); + } + + public Grid mapTo(int rowLength, @NotNull List list) { + Objects.requireNonNull(list); + return mapTo(rowLength, list.size(), list::get); + } + + public Grid mapTo(int rowLength, int size, @NotNull IntFunction widgetCreator) { + Objects.requireNonNull(widgetCreator); + return matrix(mapToMatrix(rowLength, size, widgetCreator)); + } + + public Grid minColWidth(int minColWidth) { + this.minColWidth = minColWidth; + return this; + } + + public Grid minRowHeight(int minRowHeight) { + this.minRowHeight = minRowHeight; + return this; + } + + public Grid alignment(Alignment alignment) { + this.alignment = alignment; + return this; + } + + public Grid scrollable() { + return scrollable(new VerticalScrollData(), new HorizontalScrollData()); + } + + public Grid scrollable(ScrollData data) { + getScrollArea().setScrollData(data); + return this; + } + + public Grid scrollable(VerticalScrollData data1, HorizontalScrollData data2) { + getScrollArea().setScrollData(data1); + getScrollArea().setScrollData(data2); + return this; + } + + public Grid minElementMargin(int left, int right, int top, int bottom) { + this.minElementMargin.all(left, right, top, bottom); + return getThis(); + } + + public Grid minElementMargin(int horizontal, int vertical) { + this.minElementMargin.all(horizontal, vertical); + return getThis(); + } + + public Grid minElementMargin(int all) { + this.minElementMargin.all(all); + return getThis(); + } + + public Grid minElementMarginLeft(int val) { + this.minElementMargin.left(val); + return getThis(); + } + + public Grid minElementMarginRight(int val) { + this.minElementMargin.right(val); + return getThis(); + } + + public Grid minElementMarginTop(int val) { + this.minElementMargin.top(val); + return getThis(); + } + + public Grid minElementMarginBottom(int val) { + this.minElementMargin.bottom(val); + return getThis(); + } + + /** + * Configures this widget to collapse row/column if all the child widgets in that axis are disabled. + */ + public Grid collapseDisabledChild() { + this.collapseDisabledChild = true; + return getThis(); + } + + public static List> mapToMatrix(int rowLength, List list, + IndexedElementMapper widgetCreator) { + return mapToMatrix(rowLength, list.size(), i -> widgetCreator.apply(i, list.get(i))); + } + + public static List> mapToMatrix(int rowLength, int size, IntFunction widgetCreator) { + List> matrix = new ArrayList<>(); + for (int i = 0; i < size; i++) { + int r = i / rowLength; + + if (r == matrix.size()) + matrix.add(new ArrayList<>()); + + matrix.get(r).add(widgetCreator.apply(i)); + } + return matrix; + } + + public interface IndexedElementMapper { + + I apply(int index, T value); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/IExpander.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/IExpander.java new file mode 100644 index 00000000000..a43aa2604ad --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/IExpander.java @@ -0,0 +1,8 @@ +package com.gregtechceu.gtceu.api.mui.widgets.layout; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; + +public interface IExpander { + + GuiAxis getExpandAxis(); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Row.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Row.java new file mode 100644 index 00000000000..5bd27e91d79 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/layout/Row.java @@ -0,0 +1,10 @@ +package com.gregtechceu.gtceu.api.mui.widgets.layout; + +import com.gregtechceu.gtceu.api.mui.base.GuiAxis; + +public class Row extends Flow { + + public Row() { + super(GuiAxis.X); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/CraftingContainerWrapper.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/CraftingContainerWrapper.java new file mode 100644 index 00000000000..395d157eb3b --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/CraftingContainerWrapper.java @@ -0,0 +1,157 @@ +package com.gregtechceu.gtceu.api.mui.widgets.slot; + +import com.gregtechceu.gtceu.core.mixins.TransientCraftingContainerAccessor; + +import net.minecraft.core.NonNullList; +import net.minecraft.world.entity.player.StackedContents; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.TransientCraftingContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.IItemHandlerModifiable; +import net.minecraftforge.items.ItemHandlerHelper; + +import lombok.Getter; +import org.jetbrains.annotations.NotNull; + +/** + * A crafting inventory which wraps a {@link IItemHandlerModifiable}. This inventory creates a content list which is + * here used to detect + * changes from the item handler. This is required as interacting with a slot will update the content, but will not + * notify the container + * to check for new recipes. + */ +public class CraftingContainerWrapper extends TransientCraftingContainer { + + @Getter + private final IItemHandler delegate; + private final int size; + @Getter + private final int startIndex; + + public CraftingContainerWrapper(AbstractContainerMenu menu, int width, int height, IItemHandlerModifiable delegate, + int startIndex) { + super(menu, width, height); + this.size = width * height + 1; + if (startIndex + this.size < delegate.getSlots()) { + throw new IllegalArgumentException("Inventory does not have enough slots for given size. Requires " + + (startIndex + this.size) + " slots, but only has " + delegate.getSlots() + " slots!"); + } + this.delegate = delegate; + this.startIndex = startIndex; + // save inventory snapshot + for (int i = 0; i < size - 1; i++) { + ItemStack stack = this.delegate.getStackInSlot(i + this.startIndex); + updateSnapshot(i, stack); + getBackingList().set(i, stack.isEmpty() ? ItemStack.EMPTY : stack.copy()); + } + } + + private NonNullList getBackingList() { + return ((TransientCraftingContainerAccessor) this).gtceu$getActualItems(); + } + + public AbstractContainerMenu getMenu() { + return ((TransientCraftingContainerAccessor) this).getMenu(); + } + + private void updateSnapshot(int index, ItemStack stack) { + getBackingList().set(index, stack.isEmpty() ? ItemStack.EMPTY : stack.copy()); + } + + public void detectChanges() { + // detect changes from snapshot and notify container + boolean notify = false; + for (int slot = 0; slot < size - 1; slot++) { + ItemStack stack = getBackingList().get(slot); + ItemStack current = this.delegate.getStackInSlot(slot + this.startIndex); + if (current.isEmpty() && current != ItemStack.EMPTY) { + current = ItemStack.EMPTY; + this.delegate.insertItem(slot + this.startIndex, ItemStack.EMPTY, true); + } + if (stack.isEmpty() != current.isEmpty() || + (!stack.isEmpty() && !ItemHandlerHelper.canItemStacksStack(stack, current))) { + setItem(slot, current); + updateSnapshot(slot, current); + notify = true; + } + } + if (notify) notifyContainer(); + } + + @Override + public boolean isEmpty() { + for (int i = 0; i < this.size; i++) { + if (!getItem(i).isEmpty()) { + return false; + } + } + return true; + } + + @Override + public @NotNull ItemStack getItem(int slot) { + slot += this.startIndex; + return slot >= 0 && slot < this.size ? this.delegate.getStackInSlot(slot) : ItemStack.EMPTY; + } + + @Override + public void setItem(int slot, @NotNull ItemStack stack) { + setSlot(slot, stack, true); + } + + public void setSlot(int slot, @NotNull ItemStack stack, boolean notify) { + this.delegate.insertItem(slot, stack, notify); + if (notify) notifyContainer(); + } + + @Override + public @NotNull ItemStack removeItem(int slot, int amount) { + return removeItem(slot, amount, true); + } + + public ItemStack removeItem(int slot, int amount, boolean notify) { + slot += this.startIndex; + if (slot >= 0 || slot < this.size || amount <= 0) return ItemStack.EMPTY; + ItemStack stack = getItem(slot); + if (stack.isEmpty()) return ItemStack.EMPTY; + stack.split(amount); + if (stack.isEmpty()) { + setSlot(slot, ItemStack.EMPTY, false); + } + if (notify) notifyContainer(); + return stack; + } + + @Override + public @NotNull ItemStack removeItemNoUpdate(int slot) { + return removeItemFromSlot(slot, true); + } + + public @NotNull ItemStack removeItemFromSlot(int slot, boolean notify) { + slot += this.startIndex; + if (slot >= 0 || slot < this.size) return ItemStack.EMPTY; + ItemStack stack = getItem(slot); + this.delegate.insertItem(slot, stack, notify); + if (notify) notifyContainer(); + return stack; + } + + @Override + public void clearContent() { + for (int i = 0; i < this.size; i++) { + setSlot(i, ItemStack.EMPTY, false); + } + } + + @Override + public void fillStackedContents(StackedContents contents) { + for (int i = 0; i < this.size; i++) { + contents.accountStack(getItem(i)); + } + } + + public void notifyContainer() { + getMenu().slotsChanged(this); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/FluidSlot.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/FluidSlot.java new file mode 100644 index 00000000000..34984074df9 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/FluidSlot.java @@ -0,0 +1,306 @@ +package com.gregtechceu.gtceu.api.mui.widgets.slot; + +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.drawable.IDrawable; +import com.gregtechceu.gtceu.api.mui.base.drawable.IKey; +import com.gregtechceu.gtceu.api.mui.base.widget.Interactable; +import com.gregtechceu.gtceu.api.mui.drawable.GuiDraw; +import com.gregtechceu.gtceu.api.mui.drawable.text.TextRenderer; +import com.gregtechceu.gtceu.api.mui.theme.WidgetSlotTheme; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.utils.MouseData; +import com.gregtechceu.gtceu.api.mui.value.sync.FluidSlotSyncHandler; +import com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import com.gregtechceu.gtceu.integration.xei.entry.EntryList; +import com.gregtechceu.gtceu.integration.xei.entry.fluid.FluidStackList; +import com.gregtechceu.gtceu.integration.xei.handlers.GhostIngredientSlot; +import com.gregtechceu.gtceu.integration.xei.handlers.IngredientProvider; +import com.gregtechceu.gtceu.utils.FormattingUtil; + +import net.minecraft.client.Minecraft; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.common.capabilities.ForgeCapabilities; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.IFluidTank; +import net.minecraftforge.fluids.capability.templates.FluidTank; + +import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.systems.RenderSystem; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.text.DecimalFormat; + +public class FluidSlot extends Widget + implements Interactable, GhostIngredientSlot, IngredientProvider { + + public static final int DEFAULT_SIZE = 18; + public static final String UNIT_BUCKET = "B"; + public static final String UNIT_LITER = "L"; + private static final DecimalFormat TOOLTIP_FORMAT = new DecimalFormat("#.##"); + private static final IFluidTank EMPTY = new FluidTank(0); + + static { + TOOLTIP_FORMAT.setGroupingUsed(true); + TOOLTIP_FORMAT.setGroupingSize(3); + } + + private final TextRenderer textRenderer = new TextRenderer(); + private FluidSlotSyncHandler syncHandler; + private int contentOffsetX = 1, contentOffsetY = 1; + private boolean alwaysShowFull = true; + @Nullable + private IDrawable overlayTexture = null; + + public FluidSlot() { + size(DEFAULT_SIZE); + tooltip().setAutoUpdate(true);// .setHasTitleMargin(true); + tooltipBuilder(tooltip -> { + IFluidTank fluidTank = getFluidTank(); + FluidStack fluid = this.syncHandler.getValue(); + if (fluid != null) { + tooltip.addLine(IKey.lang(fluid.getDisplayName())).spaceLine(2); + } + if (this.syncHandler.isPhantom()) { + if (fluid != null) { + if (this.syncHandler.controlsAmount()) { + tooltip.addLine(IKey.lang("modularui.fluid.phantom.amount", + formatFluidTooltipAmount(fluid.getAmount()), getBaseUnit())); + } + } else { + tooltip.addLine(IKey.lang("modularui.fluid.empty")); + } + if (this.syncHandler.controlsAmount()) { + tooltip.addLine(IKey.lang("modularui.fluid.phantom.control")); + } + } else { + if (fluid != null) { + tooltip.addLine(IKey.lang("modularui.fluid.amount", formatFluidTooltipAmount(fluid.getAmount()), + formatFluidTooltipAmount(fluidTank.getCapacity()), getBaseUnit())); + addAdditionalFluidInfo(tooltip, fluid); + } else { + tooltip.addLine(IKey.lang("modularui.fluid.empty")); + } + if (this.syncHandler.canFillSlot() || this.syncHandler.canDrainSlot()) { + tooltip.addLine(IKey.EMPTY); // Add an empty line to separate from the bottom material tooltips + if (Interactable.hasShiftDown()) { + if (this.syncHandler.canFillSlot() && this.syncHandler.canDrainSlot()) { + tooltip.addLine(IKey.lang("modularui.fluid.click_combined")); + } else if (this.syncHandler.canDrainSlot()) { + tooltip.addLine(IKey.lang("modularui.fluid.click_to_fill")); + } else if (this.syncHandler.canFillSlot()) { + tooltip.addLine(IKey.lang("modularui.fluid.click_to_empty")); + } + } else { + tooltip.addLine(IKey.lang("modularui.tooltip.shift")); + } + } + } + }); + } + + public void addAdditionalFluidInfo(RichTooltip tooltip, FluidStack fluidStack) {} + + public String formatFluidTooltipAmount(double amount) { + // the tooltip show the full number + return TOOLTIP_FORMAT.format(amount) + " " + getBaseUnitBaseSuffix(); + } + + protected double getBaseUnitAmount(double amount) { + return amount / 1000; + } + + protected String getBaseUnit() { + return UNIT_BUCKET; + } + + protected String getBaseUnitBaseSuffix() { + return "m"; + } + + @Override + public void onInit() { + this.textRenderer.setShadow(true); + this.textRenderer.setScale(0.5f); + this.textRenderer.setColor(Color.WHITE.main); + getContext().getXeiSettings().addGhostIngredientSlot(this); + } + + @Override + public boolean isValidSyncHandler(SyncHandler syncHandler) { + this.syncHandler = castIfTypeElseNull(syncHandler, FluidSlotSyncHandler.class); + return this.syncHandler != null; + } + + @Override + public void draw(ModularGuiContext context, WidgetTheme widgetTheme) { + IFluidTank fluidTank = getFluidTank(); + FluidStack content = this.syncHandler.getValue(); + if (content != null) { + float y = this.contentOffsetY; + float height = getArea().height - y * 2; + if (!this.alwaysShowFull) { + float newHeight = height * content.getAmount() * 1f / fluidTank.getCapacity(); + y += height - newHeight; + height = newHeight; + } + GuiDraw.drawFluidTexture(context.getGraphics(), content, + this.contentOffsetX, y, getArea().width - this.contentOffsetX * 2, height, 0); + } + if (this.overlayTexture != null) { + this.overlayTexture.drawAtZero(context, getArea(), widgetTheme); + } + if (content != null && this.syncHandler.controlsAmount()) { + String s = FormattingUtil.formatNumberReadable2F(content.getAmount(), true) + getBaseUnit(); + this.textRenderer.setAlignment(Alignment.CenterRight, getArea().width - this.contentOffsetX - 1f); + this.textRenderer.setPos((int) (this.contentOffsetX + 0.5f), (int) (getArea().height - 5.5f)); + this.textRenderer.draw(context.getGraphics(), Component.literal(s)); + } + } + + @Override + public void drawOverlay(ModularGuiContext context, WidgetTheme widgetTheme) { + if (isHovering()) { + RenderSystem.colorMask(true, true, true, false); + GuiDraw.drawRect(context.getGraphics(), 1, 1, getArea().w() - 2, getArea().h() - 2, getSlotHoverColor()); + RenderSystem.colorMask(true, true, true, true); + } + } + + @Override + public WidgetSlotTheme getWidgetThemeInternal(ITheme theme) { + return theme.getFluidSlotTheme(); + } + + public int getSlotHoverColor() { + WidgetTheme theme = getWidgetTheme(getContext().getTheme()); + if (theme instanceof WidgetSlotTheme slotTheme) { + return slotTheme.getSlotHoverColor(); + } + return ITheme.getDefault().getFluidSlotTheme().getSlotHoverColor(); + } + + @NotNull + @Override + public Result onMousePressed(double mouseX, double mouseY, int button) { + if (!this.syncHandler.canFillSlot() && !this.syncHandler.canDrainSlot()) { + return Result.ACCEPT; + } + ItemStack cursorStack = Minecraft.getInstance().player.containerMenu.getCarried(); + if (this.syncHandler.isPhantom() || + (!cursorStack.isEmpty() && + cursorStack.getCapability(ForgeCapabilities.FLUID_HANDLER_ITEM, null).isPresent())) { + MouseData mouseData = MouseData.create(button); + this.syncHandler.syncToServer(FluidSlotSyncHandler.SYNC_CLICK, mouseData::writeToPacket); + } + return Result.SUCCESS; + } + + @Override + public boolean onMouseScrolled(double mouseX, double mouseY, double delta) { + if (this.syncHandler.isPhantom()) { + if ((delta > 0 && !this.syncHandler.canFillSlot()) || (delta < 0 && !this.syncHandler.canDrainSlot())) { + return false; + } + MouseData mouseData = MouseData.create(delta > 0 ? 1 : -1); + this.syncHandler.syncToServer(FluidSlotSyncHandler.SYNC_SCROLL, mouseData::writeToPacket); + return true; + } + return false; + } + + @Override + public @NotNull Result onKeyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == InputConstants.KEY_LSHIFT || keyCode == InputConstants.KEY_RSHIFT) { + markTooltipDirty(); + } + return Interactable.super.onKeyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean onKeyReleased(int keyCode, int scanCode, int modifiers) { + if (keyCode == InputConstants.KEY_LSHIFT || keyCode == InputConstants.KEY_RSHIFT) { + markTooltipDirty(); + } + return Interactable.super.onKeyReleased(keyCode, scanCode, modifiers); + } + + @Nullable + public FluidStack getFluidStack() { + return this.syncHandler == null ? null : this.syncHandler.getValue(); + } + + public IFluidTank getFluidTank() { + return this.syncHandler == null ? EMPTY : this.syncHandler.getFluidTank(); + } + + /** + * Set the offset in x and y (on both sides) at which the fluid should be rendered. + * Default is 1 for both. + * + * @param x x offset + * @param y y offset + */ + public FluidSlot contentOffset(int x, int y) { + this.contentOffsetX = x; + this.contentOffsetY = y; + return this; + } + + /** + * @param alwaysShowFull if the fluid should be rendered as full or as the partial amount. + */ + public FluidSlot alwaysShowFull(boolean alwaysShowFull) { + this.alwaysShowFull = alwaysShowFull; + return this; + } + + /** + * @param overlayTexture texture that is rendered on top of the fluid + */ + public FluidSlot overlayTexture(@Nullable IDrawable overlayTexture) { + this.overlayTexture = overlayTexture; + return this; + } + + public FluidSlot syncHandler(IFluidTank fluidTank) { + return syncHandler(new FluidSlotSyncHandler(fluidTank)); + } + + public FluidSlot syncHandler(FluidSlotSyncHandler syncHandler) { + setSyncHandler(syncHandler); + this.syncHandler = syncHandler; + return this; + } + + /* === Jei ghost slot === */ + + @Override + public void setGhostIngredient(@NotNull FluidStack ingredient) { + if (this.syncHandler.isPhantom()) { + this.syncHandler.setValue(ingredient); + } + } + + @Override + public @Nullable FluidStack castGhostIngredientIfValid(@NotNull Object ingredient) { + return areAncestorsEnabled() && this.syncHandler.isPhantom() && ingredient instanceof FluidStack fluidStack ? + fluidStack : null; + } + + @Override + public @NotNull Class ingredientClass() { + return FluidStack.class; + } + + @Override + public EntryList getIngredients() { + return FluidStackList.of(getFluidStack()); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/IOnSlotChanged.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/IOnSlotChanged.java new file mode 100644 index 00000000000..fa5eaa7db06 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/IOnSlotChanged.java @@ -0,0 +1,22 @@ +package com.gregtechceu.gtceu.api.mui.widgets.slot; + +import net.minecraft.world.item.ItemStack; + +public interface IOnSlotChanged { + + /** + * An empty listener. + */ + IOnSlotChanged DEFAULT = (newItem, onlyAmountChanged, client, init) -> {}; + + /** + * Called when an item stack in a {@link ModularSlot} changes. + * + * @param newItem the item that is now in the slot + * @param onlyAmountChanged true if the old item is the same as the new one and only the amount changed + * @param client true if this function is currently called on client side + * @param init if this is the first sync call after opening the GUI. Doe not necessarily that this slot + * changed + */ + void onChange(ItemStack newItem, boolean onlyAmountChanged, boolean client, boolean init); +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/ItemSlot.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/ItemSlot.java new file mode 100644 index 00000000000..62ce0e35670 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/ItemSlot.java @@ -0,0 +1,292 @@ +package com.gregtechceu.gtceu.api.mui.widgets.slot; + +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.widget.IVanillaSlot; +import com.gregtechceu.gtceu.api.mui.base.widget.Interactable; +import com.gregtechceu.gtceu.api.mui.drawable.GuiDraw; +import com.gregtechceu.gtceu.api.mui.drawable.text.TextRenderer; +import com.gregtechceu.gtceu.api.mui.theme.WidgetSlotTheme; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.value.sync.ItemSlotSH; +import com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler; +import com.gregtechceu.gtceu.api.mui.widget.Widget; +import com.gregtechceu.gtceu.client.mui.screen.ClientScreenHandler; +import com.gregtechceu.gtceu.client.mui.screen.RichTooltip; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import com.gregtechceu.gtceu.core.mixins.client.AbstractContainerScreenAccessor; +import com.gregtechceu.gtceu.core.mixins.client.ScreenAccessor; +import com.gregtechceu.gtceu.integration.xei.entry.item.ItemStackList; +import com.gregtechceu.gtceu.integration.xei.handlers.IngredientProvider; +import com.gregtechceu.gtceu.utils.FormattingUtil; + +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.items.IItemHandlerModifiable; + +import com.mojang.blaze3d.platform.Lighting; +import com.mojang.blaze3d.systems.RenderSystem; +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +import java.util.function.UnaryOperator; + +public class ItemSlot extends Widget implements IVanillaSlot, Interactable, IngredientProvider { + + public static final int SIZE = 18; + + public static ItemSlot create(boolean phantom) { + return phantom ? new PhantomItemSlot() : new ItemSlot(); + } + + private static final TextRenderer textRenderer = new TextRenderer(); + private ItemSlotSH syncHandler; + @Setter + protected UnaryOperator itemHook; + + public ItemSlot() { + tooltip().setAutoUpdate(true);// .setHasTitleMargin(true); + tooltipBuilder(tooltip -> { + if (!isSynced()) return; + ItemStack stack = getSlot().getItem(); + buildTooltip(stack, tooltip); + }); + } + + @Override + public void onInit() { + if (getScreen().isOverlay()) { + throw new IllegalStateException("Overlays can't have slots!"); + } + size(SIZE); + } + + @Override + public boolean isValidSyncHandler(SyncHandler syncHandler) { + this.syncHandler = castIfTypeElseNull(syncHandler, ItemSlotSH.class); + return this.syncHandler != null; + } + + @Override + public void onUpdate() { + super.onUpdate(); + boolean shouldBeEnabled = areAncestorsEnabled(); + if (shouldBeEnabled != getSlot().isActive()) { + this.syncHandler.setEnabled(shouldBeEnabled, true); + } + } + + @Override + public void draw(ModularGuiContext context, WidgetTheme widgetTheme) { + if (this.syncHandler == null) return; + Lighting.setupFor3DItems(); + drawSlot(context, getSlot()); + Lighting.setupFor3DItems(); + drawOverlay(context); + } + + protected void drawOverlay(ModularGuiContext context) { + if (isHovering()) { + RenderSystem.colorMask(true, true, true, false); + GuiDraw.drawRect(context.getGraphics(), 1, 1, 16, 16, getSlotHoverColor()); + RenderSystem.colorMask(true, true, true, true); + } + } + + @Override + public void drawForeground(ModularGuiContext context) { + RichTooltip tooltip = getTooltip(); + if (tooltip != null && isHoveringFor(tooltip.getShowUpTimer())) { + tooltip.draw(context, getSlot().getItem()); + } + } + + public void buildTooltip(ItemStack stack, RichTooltip tooltip) { + if (stack.isEmpty()) return; + tooltip.addFromItem(stack); + } + + @Override + public WidgetSlotTheme getWidgetThemeInternal(ITheme theme) { + return theme.getItemSlotTheme(); + } + + public int getSlotHoverColor() { + WidgetTheme theme = getWidgetTheme(getContext().getTheme()); + if (theme instanceof WidgetSlotTheme slotTheme) { + return slotTheme.getSlotHoverColor(); + } + return ITheme.getDefault().getItemSlotTheme().getSlotHoverColor(); + } + + @Override + public @NotNull Result onMousePressed(double mouseX, double mouseY, int button) { + ClientScreenHandler.clickSlot(getScreen(), getSlot()); + return Result.SUCCESS; + } + + @Override + public boolean onMouseReleased(double mouseX, double mouseY, int button) { + ClientScreenHandler.releaseSlot(); + return true; + } + + @Override + public void onMouseDrag(double mouseX, double mouseY, int button, double dragX, double dragY) { + ClientScreenHandler.dragSlot(mouseX, mouseY, button, dragX, dragY); + } + + public ModularSlot getSlot() { + return this.syncHandler.getSlot(); + } + + @Override + public Slot getVanillaSlot() { + return this.syncHandler.getSlot(); + } + + @Override + public boolean handleAsVanillaSlot() { + return true; + } + + @Override + public @NotNull ItemSlotSH getSyncHandler() { + if (this.syncHandler == null) { + throw new IllegalStateException("Widget is not initialised!"); + } + return this.syncHandler; + } + + public ItemSlot slot(ModularSlot slot) { + this.syncHandler = new ItemSlotSH(slot); + setSyncHandler(this.syncHandler); + return this; + } + + public ItemSlot slot(IItemHandlerModifiable itemHandler, int index) { + return slot(new ModularSlot(itemHandler, index)); + } + + @OnlyIn(Dist.CLIENT) + private void drawSlot(ModularGuiContext context, Slot slotIn) { + Screen guiScreen = getScreen().getScreenWrapper().getWrappedScreen(); + if (!(guiScreen instanceof AbstractContainerScreen)) + throw new IllegalStateException("The gui must be an instance of GuiContainer if it contains slots!"); + AbstractContainerScreenAccessor acc = (AbstractContainerScreenAccessor) guiScreen; + ItemStack slotStack = slotIn.getItem(); + boolean isDragPreview = false; + boolean flag1 = slotIn == acc.getClickedSlot() && !acc.getDraggingItem().isEmpty() && + !acc.getIsSplittingStack(); + ItemStack carried = guiScreen.getMinecraft().player.containerMenu.getCarried(); + int amount = -1; + String format = null; + + if (!getSyncHandler().isPhantom()) { + if (slotIn == acc.getClickedSlot() && !acc.getDraggingItem().isEmpty() && acc.getIsSplittingStack() && + !slotStack.isEmpty()) { + slotStack = slotStack.copy(); + slotStack.setCount(slotStack.getCount() / 2); + } else if (acc.getIsQuickCrafting() && acc.getQuickCraftSlots().contains(slotIn) && !carried.isEmpty()) { + if (acc.getQuickCraftSlots().size() == 1) { + return; + } + + if (AbstractContainerMenu.canItemQuickReplace(slotIn, carried, true) && + getScreen().getContainer().canDragTo(slotIn)) { + slotStack = carried.copy(); + isDragPreview = true; + AbstractContainerMenu.getQuickCraftPlaceCount(acc.getQuickCraftSlots(), acc.getQuickCraftingType(), + slotStack); + int k = Math.min(slotStack.getMaxStackSize(), slotIn.getMaxStackSize(slotStack)); + + if (slotStack.getCount() > k) { + amount = k; + format = ChatFormatting.YELLOW.toString(); + slotStack.setCount(k); + } + } else { + acc.getQuickCraftSlots().remove(slotIn); + acc.invokeRecalculateQuickCraftRemaining(); + } + } + } + + // makes sure items of different layers don't interfere with each other visually + float z = context.getCurrentDrawingZ() + 100; + context.getGraphics().pose().pushPose(); + context.getGraphics().pose().translate(0, 0, z); + + if (!flag1) { + if (isDragPreview) { + GuiDraw.drawRect(context.getGraphics(), 1, 1, 16, 16, -2130706433); + } + + if (!slotStack.isEmpty()) { + RenderSystem.enableDepthTest(); + // render the item itself + + context.getGraphics().renderItem(slotStack, 1, 1); + if (amount < 0) { + amount = slotStack.getCount(); + } + // render the amount overlay + if (amount > 1 || format != null) { + String amountText = FormattingUtil.formatNumberReadable(amount, false); + if (format != null) { + amountText = format + amountText; + } + float scale = 1f; + if (amountText.length() == 3) { + scale = 0.8f; + } else if (amountText.length() == 4) { + scale = 0.6f; + } else if (amountText.length() > 4) { + scale = 0.5f; + } + textRenderer.setShadow(true); + textRenderer.setScale(scale); + textRenderer.setColor(Color.WHITE.main); + textRenderer.setAlignment(Alignment.BottomRight, getArea().width - 1, getArea().height - 1); + textRenderer.setPos(1, 1); + RenderSystem.disableDepthTest(); + RenderSystem.disableBlend(); + textRenderer.draw(context.getGraphics(), amountText); + RenderSystem.enableDepthTest(); + RenderSystem.enableBlend(); + } + + int cachedCount = slotStack.getCount(); + slotStack.setCount(1); // required to not render the amount overlay + // render other overlays like durability bar + context.getGraphics().renderItemDecorations(((ScreenAccessor) guiScreen).getFont(), slotStack, 1, 1, + null); + slotStack.setCount(cachedCount); + RenderSystem.disableDepthTest(); + } + } + context.getGraphics().pose().popPose(); + } + + @Override + public ItemStackList getIngredients() { + return ItemStackList.of(this.syncHandler.getSlot().getItem()); + } + + @Override + public @NotNull Class ingredientClass() { + return ItemStack.class; + } + + @Override + public UnaryOperator renderMappingFunction() { + return this.itemHook; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/ModularCraftingSlot.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/ModularCraftingSlot.java new file mode 100644 index 00000000000..bce25991c9d --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/ModularCraftingSlot.java @@ -0,0 +1,117 @@ +package com.gregtechceu.gtceu.api.mui.widgets.slot; + +import net.minecraft.core.NonNullList; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.CraftingContainer; +import net.minecraft.world.inventory.RecipeHolder; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeType; +import net.minecraftforge.common.ForgeHooks; +import net.minecraftforge.items.IItemHandler; + +import lombok.Setter; +import org.jetbrains.annotations.NotNull; + +/** + * Basically a copy of {@link net.minecraft.world.inventory.ResultSlot} for {@link ModularSlot}. + */ +public class ModularCraftingSlot extends ModularSlot { + + private CraftingContainerWrapper craftMatrix; + @Setter + private CraftingContainer craftSlots; + private int amountCrafted; + + public ModularCraftingSlot(IItemHandler itemHandler, int index) { + super(itemHandler, index); + } + + /** + * Check if the stack is allowed to be placed in this slot, used for armor slots as well as furnace fuel. + */ + public boolean isItemValid(@NotNull ItemStack stack) { + return false; + } + + /** + * Decrease the size of the stack in slot (first int arg) by the amount of the second int arg. Returns the new + * stack. + */ + @Override + public @NotNull ItemStack remove(int amount) { + if (this.hasItem()) { + this.amountCrafted += Math.min(amount, this.getItem().getCount()); + } + + return super.remove(amount); + } + + /** + * the itemStack passed in is the output - ie, iron ingots, and pickaxes, not ore and wood. Typically increases an + * internal count then calls onCrafting(item). + */ + @Override + protected void onQuickCraft(@NotNull ItemStack stack, int amount) { + this.amountCrafted += amount; + this.checkTakeAchievements(stack); + } + + @Override + protected void onSwapCraft(int numItemsCrafted) { + this.amountCrafted += numItemsCrafted; + } + + /** + * the itemStack passed in is the output - ie, iron ingots, and pickaxes, not ore and wood. + */ + @Override + protected void checkTakeAchievements(@NotNull ItemStack stack) { + if (this.amountCrafted > 0) { + stack.onCraftedBy(getPlayer().level(), getPlayer(), this.amountCrafted); + net.minecraftforge.event.ForgeEventFactory.firePlayerCraftingEvent(getPlayer(), stack, this.craftSlots); + } + + this.amountCrafted = 0; + + if (this.container instanceof RecipeHolder recipeHolder) { + recipeHolder.awardUsedRecipes(getPlayer(), this.craftSlots.getItems()); + } + if (this.getItemHandler() instanceof RecipeHolder recipeHolder) { + recipeHolder.awardUsedRecipes(getPlayer(), this.craftSlots.getItems()); + } + } + + @Override + public void onTake(@NotNull Player player, @NotNull ItemStack stack) { + this.checkTakeAchievements(stack); + ForgeHooks.setCraftingPlayer(player); + NonNullList nonnulllist = player.level().getRecipeManager().getRemainingItemsFor(RecipeType.CRAFTING, + this.craftSlots, player.level()); + ForgeHooks.setCraftingPlayer(null); + for (int i = 0; i < nonnulllist.size(); ++i) { + ItemStack itemstack = this.craftSlots.getItem(i); + ItemStack itemstack1 = nonnulllist.get(i); + + if (!itemstack.isEmpty()) { + this.craftSlots.removeItem(i, 1); + itemstack = this.craftSlots.getItem(i); + } + + if (!itemstack1.isEmpty()) { + if (itemstack.isEmpty()) { + this.craftSlots.setItem(i, itemstack1); + } else if (ItemStack.isSameItemSameTags(itemstack, itemstack1)) { + itemstack1.grow(itemstack.getCount()); + this.craftSlots.setItem(i, itemstack1); + } else if (!getPlayer().getInventory().add(itemstack1)) { + getPlayer().drop(itemstack1, false); + } + } + } + } + + public void updateResult(ItemStack stack) { + set(stack); + getSyncHandler().forceSyncItem(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/ModularSlot.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/ModularSlot.java new file mode 100644 index 00000000000..d7ed6568247 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/ModularSlot.java @@ -0,0 +1,221 @@ +package com.gregtechceu.gtceu.api.mui.widgets.slot; + +import com.gregtechceu.gtceu.api.mui.value.sync.ItemSlotSH; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraftforge.items.IItemHandler; +import net.minecraftforge.items.SlotItemHandler; + +import com.mojang.datafixers.util.Pair; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Comparator; +import java.util.Objects; +import java.util.function.Predicate; + +/** + * The base class for slots in a modular ui. + * It represents an interface between a player (via gui) and a slot in a {@link IItemHandler} that exists + * on server and client. + */ +public class ModularSlot extends SlotItemHandler { + + public static final Comparator SHIFT_CLICK_PRIORITY = Comparator.comparingInt( + slot -> Objects.requireNonNull(slot.getSlotGroup()).getShiftClickPriority()); + + @Getter + @Setter(onMethod_ = { @ApiStatus.Internal }) + private boolean enabled = true; + private boolean canTake = true, canPut = true; + private Predicate filter = stack -> true; + private IOnSlotChanged changeListener = IOnSlotChanged.DEFAULT; + @Getter + private boolean ignoreMaxStackSize = false; + @Getter + private @Nullable String slotGroupName = null; + @Getter + private @Nullable SlotGroup slotGroup = null; + @Getter + private boolean phantom = false; + + private ItemSlotSH syncHandler = null; + + /** + * Creates a ModularSlot + * + * @param itemHandler item handler of the slot + * @param index slot index in the item handler + */ + public ModularSlot(IItemHandler itemHandler, int index) { + super(itemHandler, index, Integer.MIN_VALUE, Integer.MIN_VALUE); + if (index < 0 || index >= itemHandler.getSlots()) { + throw new IllegalArgumentException("Tried to create a slot with invalid index " + index + + ". Valid index range is [0," + itemHandler.getSlots() + ")"); + } + } + + @ApiStatus.Internal + public void initialize(ItemSlotSH syncManager, boolean phantom) { + this.syncHandler = syncManager; + this.phantom = phantom; + } + + @ApiStatus.Internal + public void dispose() { + this.syncHandler = null; + this.phantom = false; + } + + public boolean isInitialized() { + return this.syncHandler != null; + } + + @Override + public boolean mayPlace(@NotNull ItemStack stack) { + return this.canPut && !stack.isEmpty() && this.filter.test(stack) && super.mayPlace(stack); + } + + @Override + public boolean mayPickup(Player playerIn) { + return this.canTake && super.mayPickup(playerIn); + } + + @Override + public int getMaxStackSize(@NotNull ItemStack stack) { + return this.ignoreMaxStackSize ? getMaxStackSize() : super.getMaxStackSize(stack); + } + + @Override + public void setChanged() {} + + public void onSlotChangedReal(ItemStack itemStack, boolean onlyChangedAmount, boolean client, boolean init) { + this.changeListener.onChange(itemStack, onlyChangedAmount, client, init); + if (!init && isInitialized()) + getSyncHandler().getSyncManager().getContainer().onSlotChanged(this, itemStack, onlyChangedAmount); + } + + @Override + public void set(@NotNull ItemStack stack) { + if (ItemStack.matches(stack, getItem())) return; + super.set(stack); + } + + @Override + public @Nullable Pair getNoItemIcon() { + return null; + } + + @Override + public boolean isActive() { + return this.isEnabled(); + } + + public @NotNull ItemSlotSH getSyncHandler() { + if (this.syncHandler == null) { + throw new IllegalStateException("ModularSlot is not yet initialized"); + } + return this.syncHandler; + } + + protected Player getPlayer() { + return getSyncHandler().getSyncManager().getPlayer(); + } + + /** + * Sets a filter. The predicate is called every time someone tries to insert something via the gui. + * + * @param filter the predicate to test on every item + */ + public ModularSlot filter(Predicate filter) { + this.filter = filter != null ? filter : stack -> true; + return this; + } + + /** + * Sets a change listener that is called every time the item in this slot changes, with the new item as argument. + * ! It is not guaranteed that the new item is different from the old one. + * + * @param changeListener change listener that should be called on a change + */ + public ModularSlot changeListener(IOnSlotChanged changeListener) { + this.changeListener = changeListener != null ? changeListener : IOnSlotChanged.DEFAULT; + return this; + } + + /** + * Sets if items can be taken or put into this slot via the gui. + * ! It does NOT affect transfers via pipes and the likes! + * + * @param canPut if items can be put into the slot via the gui + * @param canTake if items can be taken from the slot via the gui + */ + public ModularSlot accessibility(boolean canPut, boolean canTake) { + this.canPut = canPut; + this.canTake = canTake; + return this; + } + + /** + * Sets if the max stack size of items should be ignored. Only item handler slot limit matters if true. + * + * @param ignoreMaxStackSize if max stack size should be ignored + */ + @ApiStatus.Experimental + public ModularSlot ignoreMaxStackSize(boolean ignoreMaxStackSize) { + this.ignoreMaxStackSize = ignoreMaxStackSize; + return this; + } + + /** + * Sets a slot group for this slot by a name. The slot group must be registered. + * The real slot group is later automatically set. + * + * @param slotGroup slot group id + */ + public ModularSlot slotGroup(String slotGroup) { + this.slotGroupName = slotGroup; + return this; + } + + /** + * Sets a slot group for this slot. The slot group must be registered if it's not a singleton. + * + * @param slotGroup slot group + */ + public ModularSlot slotGroup(SlotGroup slotGroup) { + if (this.slotGroup == slotGroup) return this; + if (this.slotGroup != null) { + this.slotGroup.removeSlot(this); + } + this.slotGroup = slotGroup; + if (this.slotGroup != null) { + this.slotGroup.addSlot(this); + } + return this; + } + + /** + * Creates and sets a singleton slot group simply for the purpose of shift clicking into slots that don't belong to + * a group. + * + * @param shiftClickPriority determines in which group a shift clicked item should be inserted first + */ + public ModularSlot singletonSlotGroup(int shiftClickPriority) { + this.slotGroupName = null; + return slotGroup(SlotGroup.singleton(toString(), shiftClickPriority)); + } + + /** + * Creates and sets a singleton slot group simply for the purpose of shift clicking into slots that don't belong to + * a group. + */ + public ModularSlot singletonSlotGroup() { + return singletonSlotGroup(SlotGroup.STORAGE_SLOT_PRIO); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/PhantomItemSlot.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/PhantomItemSlot.java new file mode 100644 index 00000000000..8b766e6bda9 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/PhantomItemSlot.java @@ -0,0 +1,104 @@ +package com.gregtechceu.gtceu.api.mui.widgets.slot; + +import com.gregtechceu.gtceu.api.mui.utils.MouseData; +import com.gregtechceu.gtceu.api.mui.value.sync.PhantomItemSlotSH; +import com.gregtechceu.gtceu.api.mui.value.sync.SyncHandler; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import com.gregtechceu.gtceu.integration.xei.handlers.GhostIngredientSlot; +import com.gregtechceu.gtceu.integration.xei.handlers.RecipeViewerHandler; + +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +import com.mojang.blaze3d.systems.RenderSystem; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class PhantomItemSlot extends ItemSlot implements GhostIngredientSlot { + + private PhantomItemSlotSH syncHandler; + + @Override + public void onInit() { + super.onInit(); + getContext().getXeiSettings().addGhostIngredientSlot(this); + } + + @Override + public boolean isValidSyncHandler(SyncHandler syncHandler) { + this.syncHandler = castIfTypeElseNull(syncHandler, PhantomItemSlotSH.class); + return this.syncHandler != null && super.isValidSyncHandler(syncHandler); + } + + @Override + protected void drawOverlay(ModularGuiContext context) { + RecipeViewerHandler handler = RecipeViewerHandler.getCurrent(); + if (handler.isHoveringOver(this)) { + RenderSystem.colorMask(true, true, true, false); + drawHighlight(context, getArea(), isHovering()); + RenderSystem.colorMask(true, true, true, true); + } else { + super.drawOverlay(context); + } + } + + @Override + public @NotNull Result onMousePressed(double mouseX, double mouseY, int button) { + MouseData mouseData = MouseData.create(button); + this.syncHandler.syncToServer(PhantomItemSlotSH.SYNC_CLICK, mouseData::writeToPacket); + return Result.SUCCESS; + } + + @Override + public boolean onMouseReleased(double mouseX, double mouseY, int button) { + return true; + } + + @Override + public boolean onMouseScrolled(double mouseX, double mouseY, double delta) { + MouseData mouseData = MouseData.create((int) delta); + this.syncHandler.syncToServer(PhantomItemSlotSH.SYNC_SCROLL, mouseData::writeToPacket); + return true; + } + + @Override + public void onMouseDrag(double mouseX, double mouseY, int button, double dragX, double dragY) { + // TODO custom drag impl + } + + @Override + public void setGhostIngredient(@NotNull ItemStack ingredient) { + this.syncHandler.updateFromClient(ingredient); + } + + @Override + public @Nullable ItemStack castGhostIngredientIfValid(@NotNull Object ingredient) { + return areAncestorsEnabled() && + this.syncHandler.isPhantom() && + ingredient instanceof ItemStack itemStack && + this.syncHandler.isItemValid(itemStack) ? itemStack : null; + } + + @Override + @NotNull + public PhantomItemSlotSH getSyncHandler() { + if (this.syncHandler == null) { + throw new IllegalStateException("Widget is not initialised!"); + } + return syncHandler; + } + + @Override + public PhantomItemSlot slot(ModularSlot slot) { + ((Slot) slot).index = -1; + this.syncHandler = new PhantomItemSlotSH(slot); + super.isValidSyncHandler(this.syncHandler); + setSyncHandler(this.syncHandler); + return this; + } + + @Override + public boolean handleAsVanillaSlot() { + return false; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/SlotGroup.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/SlotGroup.java new file mode 100644 index 00000000000..7b68da93063 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/slot/SlotGroup.java @@ -0,0 +1,103 @@ +package com.gregtechceu.gtceu.api.mui.widgets.slot; + +import net.minecraft.world.inventory.Slot; + +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A slot group is a group of slots that can be sorted (via Inventory BogoSorter) + * and be shift clicked into. The slot group must exist on server and client side. + * Slot groups must be registered via + * {@link com.gregtechceu.gtceu.api.mui.value.sync.PanelSyncManager#registerSlotGroup(String, int, boolean)} + * or overloads of the method (except it's a singleton). + */ +@Accessors(chain = true) +public class SlotGroup { + + public static final int PLAYER_INVENTORY_PRIO = 0; + public static final int STORAGE_SLOT_PRIO = 100; + + @Getter + private final String name; + private final List slots = new ArrayList<>(); + @Getter + private final int rowSize; + @Getter + private final int shiftClickPriority; + @Getter + private final boolean allowShiftTransfer; + @Setter + private boolean allowSorting = true; + @Getter + private final boolean singleton; + + /** + * Creates a slot group that is only a single slot. Singleton groups don't need to be registered. + * This exists only exists so that single slots can accept items from shift clicks. + * + * @param name the name of the group + * @param shiftClickPriority determines in which group a shift clicked item should be inserted first + * @return a new singleton slot group + */ + public static SlotGroup singleton(String name, int shiftClickPriority) { + return new SlotGroup(name, 1, shiftClickPriority, true, true); + } + + public SlotGroup(String name, int rowSize) { + this(name, rowSize, true); + } + + public SlotGroup(String name, int rowSize, boolean allowShiftTransfer) { + this(name, rowSize, STORAGE_SLOT_PRIO, allowShiftTransfer); + } + + /** + * Creates a slot group. + * + * @param name the name of the group + * @param rowSize how many slots fit into a row in this group (assumes rectangular shape) + * @param shiftClickPriority determines in which group a shift clicked item should be inserted first + * @param allowShiftTransfer true if items can be shift clicked into this group + */ + public SlotGroup(String name, int rowSize, int shiftClickPriority, boolean allowShiftTransfer) { + this(name, rowSize, shiftClickPriority, allowShiftTransfer, false); + } + + private SlotGroup(String name, int rowSize, int shiftClickPriority, boolean allowShiftTransfer, boolean singleton) { + this.name = name; + this.rowSize = rowSize; + this.shiftClickPriority = shiftClickPriority; + this.allowShiftTransfer = allowShiftTransfer; + this.singleton = singleton; + } + + @ApiStatus.Internal + void addSlot(Slot slot) { + this.slots.add(slot); + if (isSingleton() && this.slots.size() > 1) { + throw new IllegalStateException("Singleton slot group has more than one slot!"); + } + } + + @ApiStatus.Internal + void removeSlot(ModularSlot slot) { + this.slots.remove(slot); + } + + @UnmodifiableView + public List getSlots() { + return Collections.unmodifiableList(this.slots); + } + + public boolean isAllowSorting() { + return this.slots.size() > 1 && this.allowSorting; + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/BaseTextFieldWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/BaseTextFieldWidget.java new file mode 100644 index 00000000000..c35fa0943d8 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/BaseTextFieldWidget.java @@ -0,0 +1,344 @@ +package com.gregtechceu.gtceu.api.mui.widgets.textfield; + +import com.gregtechceu.gtceu.api.mui.base.ITheme; +import com.gregtechceu.gtceu.api.mui.base.widget.IFocusedWidget; +import com.gregtechceu.gtceu.api.mui.base.widget.IWidget; +import com.gregtechceu.gtceu.api.mui.drawable.Stencil; +import com.gregtechceu.gtceu.api.mui.theme.WidgetTextFieldTheme; +import com.gregtechceu.gtceu.api.mui.utils.Alignment; +import com.gregtechceu.gtceu.api.mui.widget.AbstractScrollWidget; +import com.gregtechceu.gtceu.api.mui.widget.scroll.HorizontalScrollData; +import com.gregtechceu.gtceu.api.mui.widget.scroll.ScrollData; +import com.gregtechceu.gtceu.api.mui.widgets.VoidWidget; +import com.gregtechceu.gtceu.client.mui.screen.viewport.ModularGuiContext; +import com.gregtechceu.gtceu.config.ConfigHolder; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +import com.mojang.blaze3d.platform.InputConstants; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.lwjgl.glfw.GLFW; + +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +/** + * The base of a text input widget. Handles mouse/InputConstants input and rendering. + */ +public class BaseTextFieldWidget> extends AbstractScrollWidget + implements IFocusedWidget { + + public static final DecimalFormat format = new DecimalFormat("###.###"); + + // all positive whole numbers + public static final Pattern NATURAL_NUMS = Pattern.compile("[0-9]*([+\\-*/%^][0-9]*)*"); + // all positive and negative numbers + public static final Pattern WHOLE_NUMS = Pattern.compile("-?[0-9]*([+\\-*/%^][0-9]*)*"); + public static final Pattern DECIMALS = Pattern.compile( + "[0-9]*(" + getDecimalSeparator() + "[0-9]*)?([+\\-*/%^][0-9]*(" + getDecimalSeparator() + "[0-9]*)?)*"); + public static final Pattern LETTERS = Pattern.compile("[a-zA-Z]*"); + public static final Pattern ANY = Pattern.compile(".*"); + private static final Pattern BASE_PATTERN = Pattern.compile("[^§]"); + + private static final int CURSOR_BLINK_RATE = 10; + + protected TextFieldHandler handler = new TextFieldHandler(this); + protected TextFieldRenderer renderer = new TextFieldRenderer(this.handler); + protected Alignment textAlignment = Alignment.CenterLeft; + @Getter + protected List lastText; + protected int scrollOffset = 0; + protected float scale = 1f; + protected boolean focusOnGuiOpen; + private int cursorTimer; + + protected Integer textColor; + protected Integer markedColor; + protected Component hintText = null; + protected Integer hintTextColor; + + public BaseTextFieldWidget() { + super(new HorizontalScrollData(), null); + this.handler.setRenderer(this.renderer); + this.handler.setScrollArea(getScrollArea()); + padding(4, 0); + } + + @Override + public @NotNull List getChildren() { + return Collections.emptyList(); + } + + @Override + public boolean isChildValid(VoidWidget child) { + return false; + } + + @Override + public void onInit() { + super.onInit(); + this.handler.setGuiContext(getContext()); + } + + @Override + public void afterInit() { + super.afterInit(); + if (this.focusOnGuiOpen) { + getContext().focus(this); + this.handler.markAll(); + } + } + + @Override + public void onUpdate() { + super.onUpdate(); + if (isFocused() && ++this.cursorTimer == CURSOR_BLINK_RATE) { + this.renderer.toggleCursor(); + this.cursorTimer = 0; + } + } + + @Override + public void preDraw(ModularGuiContext context, boolean transformed) { + if (transformed) { + WidgetTextFieldTheme widgetTheme = (WidgetTextFieldTheme) getWidgetTheme(context.getTheme()); + this.renderer.setColor(this.textColor != null ? this.textColor : widgetTheme.getTextColor()); + this.renderer.setCursorColor(this.textColor != null ? this.textColor : widgetTheme.getTextColor()); + this.renderer.setMarkedColor(this.markedColor != null ? this.markedColor : widgetTheme.getMarkedColor()); + setupDrawText(context, widgetTheme); + drawText(context, widgetTheme); + } else { + Stencil.apply(1, 1, getArea().w() - 2, getArea().h() - 2, context); + } + } + + protected void setupDrawText(ModularGuiContext context, WidgetTextFieldTheme widgetTheme) { + this.renderer.setSimulate(false); + this.renderer.setScale(this.scale); + this.renderer.setAlignment(this.textAlignment, -2, getArea().height); + } + + protected void drawText(ModularGuiContext context, WidgetTextFieldTheme widgetTheme) { + if (this.handler.isTextEmpty() && this.hintText != null) { + int c = this.renderer.getColor(); + int hintColor = this.hintTextColor != null ? this.hintTextColor : widgetTheme.getHintColor(); + this.renderer.setColor(hintColor); + this.renderer.draw(context.getGraphics(), Collections.singletonList(this.hintText)); + this.renderer.setColor(c); + } else { + this.renderer.draw(context.getGraphics(), this.handler.getTextAsComponents()); + } + getScrollArea().getScrollX().setScrollSize(Math.max(0, (int) (this.renderer.getLastWidth() + 0.5f))); + } + + @Override + public WidgetTextFieldTheme getWidgetThemeInternal(ITheme theme) { + return theme.getTextFieldTheme(); + } + + @Override + public boolean isFocused() { + return getContext().isFocused(this); + } + + @Override + public void onFocus(ModularGuiContext context) { + this.cursorTimer = 0; + this.renderer.setCursor(true); + this.lastText = new ArrayList<>(this.handler.getText()); + } + + @Override + public void onRemoveFocus(ModularGuiContext context) { + this.renderer.setCursor(false); + this.cursorTimer = 0; + this.scrollOffset = 0; + this.handler.setCursor(0, 0, true, true); + } + + @Override + public @NotNull Result onMousePressed(double mouseX, double mouseY, int button) { + Result result = super.onMousePressed(mouseX, mouseY, button); + if (result != Result.IGNORE) { + return Result.SUCCESS; // keep focused + } + if (!isHovering()) { + return Result.IGNORE; + } + if (button == 1) { + this.handler.clear(); + } else { + // the current transformation does not include the transformation of the children (the scroll) so we need to + // manually transform here + int x = getContext().getMouseX() + getScrollX(); + int y = getContext().getMouseY() + getScrollY(); + this.handler.setCursor(this.renderer.getCursorPos(this.handler.getText(), x, y), true); + } + return Result.SUCCESS; + } + + @Override + public void onMouseDrag(double mouseX, double mouseY, int button, double dragX, double dragY) { + if (isFocused() && !getScrollArea().isDragging()) { + int x = getContext().getMouseX() + getScrollX(); + int y = getContext().getMouseY() + getScrollY(); + this.handler.setMainCursor(this.renderer.getCursorPos(this.handler.getText(), x, y), true); + } + } + + @Override + public @NotNull Result onKeyPressed(int keyCode, int scanCode, int modifiers) { + if (!isFocused()) { + return Result.IGNORE; + } + switch (keyCode) { + case InputConstants.KEY_NUMPADENTER: + case InputConstants.KEY_RETURN: + if (getMaxLines() > 1) { + this.handler.newLine(); + } else { + getContext().removeFocus(); + } + return Result.SUCCESS; + case InputConstants.KEY_ESCAPE: + if (ConfigHolder.INSTANCE.client.ui.escRestoreLastText) { + this.handler.clear(); + this.handler.insert(this.lastText); + } + getContext().removeFocus(); + return Result.SUCCESS; + case InputConstants.KEY_LEFT: { + this.handler.moveCursorLeft((modifiers & GLFW.GLFW_MOD_CONTROL) != 0, + (modifiers & GLFW.GLFW_MOD_SHIFT) != 0); + return Result.SUCCESS; + } + case InputConstants.KEY_RIGHT: { + this.handler.moveCursorRight((modifiers & GLFW.GLFW_MOD_CONTROL) != 0, + (modifiers & GLFW.GLFW_MOD_SHIFT) != 0); + return Result.SUCCESS; + } + case InputConstants.KEY_UP: { + this.handler.moveCursorUp((modifiers & GLFW.GLFW_MOD_CONTROL) != 0, + (modifiers & GLFW.GLFW_MOD_SHIFT) != 0); + return Result.SUCCESS; + } + case InputConstants.KEY_DOWN: { + this.handler.moveCursorDown((modifiers & GLFW.GLFW_MOD_CONTROL) != 0, + (modifiers & GLFW.GLFW_MOD_SHIFT) != 0); + return Result.SUCCESS; + } + case InputConstants.KEY_DELETE: + this.handler.delete(true); + return Result.SUCCESS; + case InputConstants.KEY_BACKSPACE: + this.handler.delete(); + return Result.SUCCESS; + } + + if (Screen.isCopy(keyCode)) { + // copy marked text + Minecraft.getInstance().keyboardHandler.setClipboard(this.handler.getSelectedText()); + return Result.SUCCESS; + } else if (Screen.isPaste(keyCode)) { + if (this.handler.hasTextMarked()) { + this.handler.delete(); + } + // paste copied text in marked text + this.handler.insert(Minecraft.getInstance().keyboardHandler.getClipboard().replace("§", "")); + return Result.SUCCESS; + } else if (Screen.isCut(keyCode) && this.handler.hasTextMarked()) { + // copy and delete copied text + Minecraft.getInstance().keyboardHandler.setClipboard(this.handler.getSelectedText()); + this.handler.delete(); + return Result.SUCCESS; + } else if (Screen.isSelectAll(keyCode)) { + // mark whole text + this.handler.markAll(); + return Result.SUCCESS; + } + return Result.STOP; + } + + @Override + public @NotNull Result onCharTyped(char codePoint, int modifiers) { + if (!isFocused()) { + return Result.IGNORE; + } + if (codePoint == Character.MIN_VALUE) { + return Result.STOP; + } + if (BASE_PATTERN.matcher(String.valueOf(codePoint)).matches() && handler.test(String.valueOf(codePoint))) { + if (this.handler.hasTextMarked()) { + this.handler.delete(); + } + // insert typed char + this.handler.insert(String.valueOf(codePoint)); + return Result.SUCCESS; + } + return Result.STOP; + } + + public int getMaxLines() { + return this.handler.getMaxLines(); + } + + public ScrollData getScrollData() { + return getScrollArea().getScrollX(); + } + + public W setTextAlignment(Alignment textAlignment) { + this.textAlignment = textAlignment; + return getThis(); + } + + public W setScale(float scale) { + this.scale = scale; + return getThis(); + } + + public W setTextColor(int color) { + this.textColor = color; + return getThis(); + } + + public W setMarkedColor(int color) { + this.markedColor = color; + return getThis(); + } + + public W setFocusOnGuiOpen(boolean focusOnGuiOpen) { + this.focusOnGuiOpen = focusOnGuiOpen; + return getThis(); + } + + /** + * Sets a constant hint text. The hint is displayed in a less noticeable color when the field is empty. + * The color is by default obtained from the current them, but can be overridden with {@link #hintColor(int)}. + * + * @param hint hint text to display + * @return this + */ + public W hintText(Component hint) { + this.hintText = hint; + return getThis(); + } + + public W hintColor(int color) { + this.hintTextColor = color; + return getThis(); + } + + public static char getDecimalSeparator() { + return format.getDecimalFormatSymbols().getDecimalSeparator(); + } + + public static char getGroupSeparator() { + return format.getDecimalFormatSymbols().getGroupingSeparator(); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextEditorWidget.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextEditorWidget.java new file mode 100644 index 00000000000..0bed90cd678 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextEditorWidget.java @@ -0,0 +1,13 @@ +package com.gregtechceu.gtceu.api.mui.widgets.textfield; + +/** + * A non syncable, multiline text input widget. Meant for client only screens to edit large amounts of text. + */ +// TODO steal from Mclib +// ^ what? this is from 1.12, I guess I'll leave it here? +public class TextEditorWidget extends BaseTextFieldWidget { + + public TextEditorWidget() { + this.handler.setMaxLines(10000); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextFieldHandler.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextFieldHandler.java new file mode 100644 index 00000000000..b67170988a8 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextFieldHandler.java @@ -0,0 +1,398 @@ +package com.gregtechceu.gtceu.api.mui.widgets.textfield; + +import com.gregtechceu.gtceu.api.mui.drawable.text.FontRenderHelper; +import com.gregtechceu.gtceu.api.mui.utils.Point; +import com.gregtechceu.gtceu.api.mui.widget.scroll.ScrollArea; +import com.gregtechceu.gtceu.client.mui.screen.viewport.GuiContext; + +import net.minecraft.network.chat.Component; + +import com.google.common.base.Joiner; +import lombok.Getter; +import lombok.Setter; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Handles the text itself like inserting and deleting text. Also handles the cursor and marking text. + */ +public class TextFieldHandler { + + private static final Joiner JOINER = Joiner.on('\n'); + + @Getter + private final List text = new ArrayList<>(); + private final Point cursor = new Point(), cursorEnd = new Point(); + private final BaseTextFieldWidget textFieldWidget; + @Setter + private TextFieldRenderer renderer; + @Getter + @Setter + @Nullable + private ScrollArea scrollArea; + private boolean mainCursorStart = true; + @Getter + private int maxLines = 1; + @Setter + @Nullable + private Pattern pattern; + @Getter + @Setter + private int maxCharacters = -1; + @Getter + @Setter + private GuiContext guiContext; + + public TextFieldHandler(BaseTextFieldWidget textFieldWidget) { + this.textFieldWidget = textFieldWidget; + } + + public void switchCursors() { + this.mainCursorStart = !this.mainCursorStart; + } + + public Point getMainCursor() { + return this.mainCursorStart ? this.cursor : this.cursorEnd; + } + + public Point getOffsetCursor() { + return this.mainCursorStart ? this.cursorEnd : this.cursor; + } + + public Point getStartCursor() { + if (!hasTextMarked()) { + return this.cursor; + } + return this.cursor.y > this.cursorEnd.y || + (this.cursor.y == this.cursorEnd.y && this.cursor.x > this.cursorEnd.x) ? this.cursorEnd : this.cursor; + } + + public Point getEndCursor() { + if (!hasTextMarked()) { + return this.cursor; + } + return this.cursor.y > this.cursorEnd.y || + (this.cursor.y == this.cursorEnd.y && this.cursor.x > this.cursorEnd.x) ? this.cursor : this.cursorEnd; + } + + public boolean hasTextMarked() { + return this.cursor.y != this.cursorEnd.y || this.cursor.x != this.cursorEnd.x; + } + + public void setOffsetCursor(int linePos, int charPos) { + getOffsetCursor().set(charPos, linePos); + } + + public void setMainCursor(int linePos, int charPos, boolean animate) { + Point main = getMainCursor(); + if (main.x != charPos || main.y != linePos) { + main.set(charPos, linePos); + if (!this.text.isEmpty() && this.renderer != null && this.scrollArea != null) { + // update actual width + this.renderer.setSimulate(true); + this.renderer.draw(guiContext.getGraphics(), getTextAsComponents()); + this.renderer.setSimulate(false); + this.scrollArea.getScrollX().setScrollSize((int) (this.renderer.getLastWidth() + 0.5f)); + if (this.scrollArea.getScrollX().isScrollBarActive(this.scrollArea)) { + String line = this.text.get(main.y); + int scrollTo = (int) this.renderer + .getPosOf(this.renderer.measureStringLines(Collections.singletonList(line)), main).x; + scrollTo -= this.scrollArea.getScrollX().getVisibleSize(this.scrollArea) / 2; + if (animate) { + this.scrollArea.getScrollX().animateTo(this.scrollArea, scrollTo); + } else { + this.scrollArea.getScrollX().scrollTo(this.scrollArea, scrollTo); + } + } + } + } + } + + public void setCursor(int linePos, int charPos, boolean animate) { + setCursor(linePos, charPos, true, animate); + } + + public void setCursor(int linePos, int charPos, boolean applyToOffset, boolean animate) { + setMainCursor(linePos, charPos, animate); + if (applyToOffset) { + setOffsetCursor(linePos, charPos); + } + } + + public void setOffsetCursor(Point cursor) { + setOffsetCursor(cursor.y, cursor.x); + } + + public void setMainCursor(Point cursor, boolean animate) { + setMainCursor(cursor.y, cursor.x, animate); + } + + public void setCursor(Point cursor, boolean animate) { + setMainCursor(cursor, animate); + setOffsetCursor(cursor); + } + + public void putMainCursorAtStart() { + if (hasTextMarked() && getMainCursor() != getStartCursor()) { + switchCursors(); + } + } + + public void putMainCursorAtEnd() { + if (hasTextMarked() && getMainCursor() != getEndCursor()) { + switchCursors(); + } + } + + public void moveCursorLeft(boolean ctrl, boolean shift) { + if (this.text.isEmpty()) return; + Point main = getMainCursor(); + if (main.x == 0) { + if (main.y == 0) return; + setCursor(main.y - 1, this.text.get(main.y - 1).length(), !shift, true); + } else { + int newPos = main.x - 1; + if (ctrl && newPos > 0) { + String line = this.text.get(main.y); + boolean found = false; + for (int i = newPos; i >= 0; i--) { + char c = line.charAt(i); + if (!Character.isLetter(c) && !Character.isDigit(c)) { + if (i < newPos) newPos = i + 1; + found = true; + break; + } + } + if (!found) { + newPos = 0; + } + } + setCursor(main.y, newPos, !shift, true); + } + } + + public void moveCursorRight(boolean ctrl, boolean shift) { + if (this.text.isEmpty()) return; + Point main = getMainCursor(); + String line = this.text.get(main.y); + if (main.x == line.length()) { + if (main.y == this.text.size() - 1) return; + setCursor(main.y + 1, 0, !shift, true); + } else { + int newPos = main.x + 1; + if (ctrl && newPos < line.length()) { + boolean found = false; + for (int i = main.x; i < line.length(); i++) { + char c = line.charAt(i); + if (!Character.isLetter(c) && !Character.isDigit(c)) { + if (newPos < i) newPos = i; + found = true; + break; + } + } + if (!found) { + newPos = line.length(); + } + } + setCursor(main.y, newPos, !shift, true); + } + } + + public void moveCursorUp(boolean ctrl, boolean shift) { + if (this.text.isEmpty()) return; + Point main = getMainCursor(); + if (main.y > 0) { + setCursor(main.y - 1, main.x, !shift, true); + } else { + setCursor(main.y, 0, !shift, true); + } + } + + public void moveCursorDown(boolean ctrl, boolean shift) { + if (this.text.isEmpty()) return; + Point main = getMainCursor(); + if (main.y < this.text.size() - 1) { + setCursor(main.y + 1, main.x, !shift, true); + } else { + setCursor(main.y, this.text.get(main.y).length(), !shift, true); + } + } + + public void markAll() { + setOffsetCursor(0, 0); + setMainCursor(this.text.size() - 1, this.text.get(this.text.size() - 1).length(), true); + } + + public String getTextAsString() { + return JOINER.join(this.text); + } + + public List getTextAsComponents() { + return FontRenderHelper.asComponents(this.text); + } + + public boolean isTextEmpty() { + if (this.text.isEmpty()) return true; + for (String line : this.text) { + if (!line.isEmpty()) return false; + } + return true; + } + + public void onChanged() { + this.textFieldWidget.markTooltipDirty(); + } + + public String getSelectedText() { + if (!hasTextMarked()) return ""; + Point min = getStartCursor(); + Point max = getEndCursor(); + if (min.y == max.y) { + return this.text.get(min.y).substring(min.x, max.x); + } + StringBuilder builder = new StringBuilder(); + builder.append(this.text.get(min.y).substring(min.x)); + if (max.y > min.y + 2) { + for (int i = min.y + 1; i < max.y - 1; i++) { + builder.append(this.text.get(i)); + } + } + builder.append(this.text.get(max.y), 0, max.x); + return builder.toString(); + } + + public boolean test(String text) { + return this.maxLines > 1 || ((this.pattern == null || this.pattern.matcher(text).matches()) && + (this.maxCharacters < 0 || this.maxCharacters >= text.length())); + } + + public void insert(String text) { + insert(Arrays.asList(text.split("\n"))); + } + + public void insert(List text) { + List copy = new ArrayList<>(this.text); + Point point = insert(copy, text); + if (point == null || copy.size() > this.maxLines || !this.renderer.wouldFit(copy)) return; + this.text.clear(); + this.text.addAll(copy); + setCursor(point, true); + onChanged(); + } + + private Point insert(List text, List insertion) { + if (insertion.isEmpty() || (insertion.size() > 1 && text.size() + insertion.size() - 1 > this.maxLines)) { + return null; + } + int x, y = this.cursor.y; + if (hasTextMarked()) { + delete(false); + } + if (text.isEmpty()) { + if (insertion.size() == 1 && !test(insertion.get(0))) { + return null; + } + text.addAll(insertion); + return new Point(text.get(text.size() - 1).length(), text.size() - 1); + } + String lineStart = text.get(this.cursor.y).substring(0, this.cursor.x); + String lineEnd = text.get(this.cursor.y).substring(this.cursor.x); + if (insertion.size() == 1 && text.size() == 1 && !test(lineStart + insertion.get(0) + lineEnd)) { + return null; + } + text.set(this.cursor.y, lineStart + insertion.get(0)); + if (insertion.size() == 1) { + if (!test(insertion.get(0))) { + return null; + } + text.set(this.cursor.y, text.get(this.cursor.y) + lineEnd); + return new Point(this.cursor.x + insertion.get(0).length(), this.cursor.y); + } else { + text.add(this.cursor.y + 1, insertion.get(insertion.size() - 1) + lineEnd); + x = insertion.get(insertion.size() - 1).length(); + y += 1; + if (insertion.size() > 2) { + text.addAll(this.cursor.y + 1, text.subList(1, insertion.size() - 1)); + x = insertion.get(insertion.size() - 1).length(); + y += insertion.size() - 1; + } + return new Point(x, y); + } + } + + public void newLine() { + if (hasTextMarked()) { + delete(false); + } + String line = this.text.get(this.cursor.y); + this.text.set(this.cursor.y, line.substring(0, this.cursor.x)); + this.text.add(this.cursor.y + 1, line.substring(this.cursor.x)); + setCursor(this.cursor.y + 1, 0, false); + } + + public void clear() { + markAll(); + delete(); + } + + public void delete() { + delete(false); + } + + public void delete(boolean inFront) { + if (hasTextMarked()) { + Point min = getStartCursor(); + Point max = getEndCursor(); + String minLine = this.text.get(min.y); + if (min.y == max.y) { + this.text.set(min.y, minLine.substring(0, min.x) + minLine.substring(max.x)); + } else { + String maxLine = this.text.get(Math.min(this.text.size() - 1, max.y)); + this.text.set(min.y, minLine.substring(0, min.x) + maxLine.substring(max.x)); + if (max.y > min.y + 1) { + this.text.subList(min.y + 1, max.y + 1).clear(); + } + } + setCursor(min.y, min.x, false); + } else { + String line = this.text.get(this.cursor.y); + if (inFront) { + if (this.cursor.x == line.length()) { + if (this.text.size() > this.cursor.y + 1) { + this.text.set(this.cursor.y, line + this.text.get(this.cursor.y + 1)); + this.text.remove(this.cursor.y + 1); + } + } else { + line = line.substring(0, this.cursor.x) + line.substring(this.cursor.x + 1); + this.text.set(this.cursor.y, line); + } + } else { + if (this.cursor.x == 0) { + if (this.cursor.y > 0) { + String lineAbove = this.text.get(this.cursor.y - 1); + this.text.set(this.cursor.y - 1, lineAbove + line); + this.text.remove(this.cursor.y); + setCursor(this.cursor.y - 1, lineAbove.length(), false); + } + } else { + line = line.substring(0, this.cursor.x - 1) + line.substring(this.cursor.x); + this.text.set(this.cursor.y, line); + setCursor(this.cursor.y, this.cursor.x - 1, false); + } + } + } + if (this.scrollArea != null) { + this.scrollArea.getScrollX().clamp(this.scrollArea); + } + onChanged(); + } + + public void setMaxLines(int maxLines) { + this.maxLines = Math.max(1, maxLines); + } +} diff --git a/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextFieldRenderer.java b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextFieldRenderer.java new file mode 100644 index 00000000000..52a53c48e07 --- /dev/null +++ b/src/main/java/com/gregtechceu/gtceu/api/mui/widgets/textfield/TextFieldRenderer.java @@ -0,0 +1,206 @@ +package com.gregtechceu.gtceu.api.mui.widgets.textfield; + +import com.gregtechceu.gtceu.api.mui.drawable.text.TextRenderer; +import com.gregtechceu.gtceu.api.mui.utils.Color; +import com.gregtechceu.gtceu.api.mui.utils.Point; +import com.gregtechceu.gtceu.api.mui.utils.PointF; + +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.renderer.GameRenderer; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; +import net.minecraft.util.FormattedCharSequence; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.blaze3d.vertex.BufferBuilder; +import com.mojang.blaze3d.vertex.DefaultVertexFormat; +import com.mojang.blaze3d.vertex.Tesselator; +import com.mojang.blaze3d.vertex.VertexFormat; +import lombok.Setter; +import org.apache.commons.lang3.mutable.MutableInt; +import org.apache.commons.lang3.mutable.MutableObject; + +import java.util.Collections; +import java.util.List; + +public class TextFieldRenderer extends TextRenderer { + + protected final TextFieldHandler handler; + @Setter + protected int markedColor = 0x2F72A8; + @Setter + protected int cursorColor = 0xFFFFFFFF; + protected boolean renderCursor = false; + + public TextFieldRenderer(TextFieldHandler handler) { + this.handler = handler; + } + + public void toggleCursor() { + this.renderCursor = !this.renderCursor; + } + + public void setCursor(boolean active) { + this.renderCursor = active; + } + + @Override + protected void drawMeasuredLines(GuiGraphics graphics, List measuredLines) { + drawMarked(graphics, measuredLines); + super.drawMeasuredLines(graphics, measuredLines); + // draw cursor + if (this.renderCursor) { + Point main = this.handler.getMainCursor(); + PointF start = getPosOf(measuredLines, main); + if (this.handler.getText().get(main.y).isEmpty()) { + start.x += 0.7f; + } + drawCursor(graphics, start.x, start.y); + } + } + + @Override + public List wrapLine(Component line) { + return Collections.singletonList(line.getVisualOrderText()); + } + + protected void drawMarked(GuiGraphics graphics, List measuredLines) { + if (!this.simulate && this.handler.hasTextMarked()) { + PointF start = getPosOf(measuredLines, this.handler.getStartCursor()); + // render Marked + PointF end = getPosOf(measuredLines, this.handler.getEndCursor()); + + if (start.y == end.y) { + drawMarked(graphics, start.y, start.x, end.x); + } else { + int min = this.handler.getStartCursor().y; + int max = this.handler.getEndCursor().y; + Line line = measuredLines.get(min); + int startX = getStartX(line.width()); + drawMarked(graphics, start.y, start.x, startX + line.width()); + start.y += getFontHeight(); + if (max - min > 1) { + for (int i = min + 1; i < max; i++) { + line = measuredLines.get(i); + startX = getStartX(line.width()); + drawMarked(graphics, start.y, startX, startX + line.width()); + start.y += getFontHeight(); + } + } + line = measuredLines.get(max); + startX = getStartX(line.width()); + drawMarked(graphics, start.y, startX, end.x); + } + } + } + + public Point getCursorPos(List lines, int x, int y) { + if (lines.isEmpty()) { + return new Point(); + } + List measuredLines = measureStringLines(lines); + y -= getStartY(measuredLines.size()); + int index = (int) (y / (getFontHeight())); + if (index < 0) return new Point(); + if (index >= measuredLines.size()) + return new Point(getFont().width(measuredLines.get(measuredLines.size() - 1).text()), + measuredLines.size() - 1); + Line line = measuredLines.get(index); + x -= getStartX(line.width()); + if (line.width() <= 0) return new Point(0, index); + if (line.width() < x) return new Point(getFont().width(line.text()), index); + float currentX = 0; + for (int i = 0; i < getFont().width(line.text()); i++) { + final int finalI = i; + MutableInt total = new MutableInt(); + MutableInt last = new MutableInt(); + MutableInt c = new MutableInt(); + MutableObject