RyanHub – file viewer
filename: common/src/main/java/rearth/oritech/client/ui/BasicMachineScreen.java
branch: 1.21
back to repo
package rearth.oritech.client.ui;

import com.mojang.blaze3d.systems.RenderSystem;
import dev.architectury.fluid.FluidStack;
import dev.architectury.hooks.fluid.FluidStackHooks;
import dev.architectury.platform.Platform;
import io.wispforest.owo.ui.base.BaseOwoHandledScreen;
import io.wispforest.owo.ui.component.*;
import io.wispforest.owo.ui.container.Containers;
import io.wispforest.owo.ui.container.FlowLayout;
import io.wispforest.owo.ui.core.*;
import io.wispforest.owo.ui.util.NinePatchTexture;
import io.wispforest.owo.ui.util.SpriteUtilInvoker;
import net.minecraft.block.Blocks;
import net.minecraft.block.RedstoneTorchBlock;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.*;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.fluid.Fluid;
import net.minecraft.item.ItemStack;
import net.minecraft.registry.Registries;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.NotNull;
import org.joml.Matrix4f;
import rearth.oracle.Oracle;
import rearth.oracle.OracleClient;
import rearth.oritech.Oritech;
import rearth.oritech.api.fluid.FluidApi;
import rearth.oritech.block.base.entity.MachineBlockEntity;
import rearth.oritech.block.base.entity.UpgradableGeneratorBlockEntity;
import rearth.oritech.block.entity.generators.BasicGeneratorEntity;
import rearth.oritech.block.entity.generators.SteamEngineEntity;
import rearth.oritech.client.renderers.LaserArmModel;
import rearth.oritech.network.NetworkContent;
import rearth.oritech.util.ScreenProvider;
import rearth.oritech.util.TooltipHelper;

import java.util.Optional;

public class BasicMachineScreen<S extends BasicMachineScreenHandler> extends BaseOwoHandledScreen<FlowLayout, S> {
    
    
    public static final Identifier BACKGROUND = Oritech.id("textures/gui/modular/gui_base.png");
    public static final Identifier ITEM_SLOT = Oritech.id("textures/gui/modular/itemslot.png");
    public static final Identifier GUI_COMPONENTS = Oritech.id("textures/gui/modular/machine_gui_components.png");
    public static final int GRAY_TEXT_COLOR = new Color(0.2f, 0.2f, 0.3f).rgb();
    public static Surface ORITECH_PANEL = (context, component) -> NinePatchTexture.draw(Identifier.of(Oritech.MOD_ID, "bedrock_panel"), context, component);
    public static Surface ORITECH_PANEL_DARK = (context, component) -> NinePatchTexture.draw(Identifier.of(Oritech.MOD_ID, "bedrock_panel_dark"), context, component);
    public static Surface ORITECH_PANEL_ORANGE = (context, component) -> NinePatchTexture.draw(Identifier.of(Oritech.MOD_ID, "bedrock_panel_orange"), context, component);

    public static ButtonComponent.Renderer ORITECH_BUTTON = (matrices, button, delta) -> {
        RenderSystem.enableDepthTest();
        var texture = button.active ? (button.isHovered() ? MinecraftClient.getInstance().mouse.wasLeftButtonClicked() ? Identifier.of(Oritech.MOD_ID, "bedrock_panel_pressed") : Identifier.of(Oritech.MOD_ID, "bedrock_panel_hover") : Identifier.of(Oritech.MOD_ID, "bedrock_panel")) : ButtonComponent.DISABLED_TEXTURE;
        NinePatchTexture.draw(texture, matrices, button.getX(), button.getY(), button.width(), button.height());
    };
    public static ButtonComponent.Renderer ORITECH_BUTTON_DARK = (matrices, button, delta) -> {
        RenderSystem.enableDepthTest();
        var texture = button.active ? (button.isHovered() ? MinecraftClient.getInstance().mouse.wasLeftButtonClicked() ? Identifier.of(Oritech.MOD_ID, "bedrock_panel_pressed") : Identifier.of(Oritech.MOD_ID, "bedrock_panel_dark_hover") : Identifier.of(Oritech.MOD_ID, "bedrock_panel_dark")) : ButtonComponent.DISABLED_TEXTURE;
        NinePatchTexture.draw(texture, matrices, button.getX(), button.getY(), button.width(), button.height());
    };
    
    public FlowLayout root;
    protected TextureComponent progress_indicator;
    protected TextureComponent energyIndicator;
    private ButtonComponent cycleInputButton;
    
    private final FluidDisplay genericDisplay;
    private final FluidDisplay steamDisplay;
    private final FluidDisplay waterDisplay;
    protected final LabelComponent steamProductionLabel;
    
    protected static final class FluidDisplay {
        private final BoxComponent fillOverlay;
        private float lastFill;
        private Fluid lastDrawnFluid;
        private ColoredSpriteComponent background;
        private final TextureComponent foreGround;
        private final ScreenProvider.BarConfiguration config;
        private final FluidApi.SingleSlotStorage storage;
        
        private FluidDisplay(BoxComponent fillOverlay, float lastFill, Fluid lastDrawnFluid, ColoredSpriteComponent background, TextureComponent foreGround, ScreenProvider.BarConfiguration config, FluidApi.SingleSlotStorage storage) {
            this.fillOverlay = fillOverlay;
            this.lastFill = lastFill;
            this.lastDrawnFluid = lastDrawnFluid;
            this.background = background;
            this.foreGround = foreGround;
            this.config = config;
            this.storage = storage;
        }
    }
    
    public BasicMachineScreen(S handler, PlayerInventory inventory, Text title) {
        
        super(handler, inventory, title);
        
        if (handler.mainFluidContainer != null) {
            var config = handler.screenData.getFluidConfiguration();
            genericDisplay = initFluidDisplay(handler.mainFluidContainer, config);
        } else {
            genericDisplay = null;
        }
        
        if (handler.steamStorage != null) {
            var config = getBoilerInConfig();
            waterDisplay = initFluidDisplay(handler.waterStorage, config);
            
            var configSteam = getBoilerOutConfig();
            steamDisplay = initFluidDisplay(handler.steamStorage, configSteam);
            // the label is then actually added to the screen in the upgradable screen extension
            steamProductionLabel = Components.label(Text.translatable("title.oritech.steam_production", "0"));
            steamProductionLabel.tooltip(Text.translatable("tooltip.oritech.steam_production"));
        } else {
            steamDisplay = null;
            waterDisplay = null;
            steamProductionLabel = null;
        }
        
    }
    
    public ScreenProvider.BarConfiguration getBoilerInConfig() {
        return handler.screenData.getEnergyConfiguration();
    }
    
    public ScreenProvider.BarConfiguration getBoilerOutConfig() {
        var config = getBoilerInConfig();
        return new ScreenProvider.BarConfiguration(config.x() + config.width() + 8, config.y(), config.width(), config.height());
    }
    
    public Identifier getGuiComponents() {
        return GUI_COMPONENTS;
    }
    
    public Identifier getItemSlot() {
        return ITEM_SLOT;
    }
    
    public Identifier getBackground() {
        return BACKGROUND;
    }
    
    protected FluidDisplay initFluidDisplay(FluidApi.SingleSlotStorage container, ScreenProvider.BarConfiguration config) {
        var lastFill = 1 - ((float) container.getStack().getAmount() / container.getCapacity());
        var background = createFluidRenderer(container.getStack(), config);

        var fillOverlay = Components.box(Sizing.fixed(config.width()), Sizing.fixed((int) (config.height() * lastFill)));
        fillOverlay.color(new Color(77.6f / 255f, 77.6f / 255f, 77.6f / 255f));
        fillOverlay.fill(true);
        fillOverlay.positioning(Positioning.absolute(config.x(), config.y()));

        var foreGround = Components.texture(getGuiComponents(), 48, 0, 14, 50, 98, 96);
        foreGround.sizing(Sizing.fixed(config.width()), Sizing.fixed(config.height()));
        foreGround.positioning(Positioning.absolute(config.x(), config.y()));

        return new FluidDisplay(fillOverlay, lastFill, container.getStack().getFluid(), background, foreGround, config, container);
    }
    
    public static Component getItemFrame(int x, int y) {
        return Components.texture(ITEM_SLOT, 0, 0, 18, 18, 18, 18).sizing(Sizing.fixed(18)).positioning(Positioning.absolute(x - 1, y - 1));
    }
    
    @Override
    protected @NotNull OwoUIAdapter<FlowLayout> createAdapter() {
        return OwoUIAdapter.create(this, Containers::verticalFlow);
    }
    
    @Override
    protected void build(FlowLayout rootComponent) {
        this.root = rootComponent;
        
        rootComponent
          .surface(Surface.VANILLA_TRANSLUCENT)
          .horizontalAlignment(HorizontalAlignment.CENTER)
          .verticalAlignment(VerticalAlignment.CENTER);
        
        if (showExtensionPanel()) {
            rootComponent.child(
              Containers.horizontalFlow(Sizing.fixed(176 + 250), Sizing.fixed(166 + 40))
                .child(Containers.horizontalFlow(Sizing.content(), Sizing.content())
                         .child(buildExtensionPanel())
                         .surface(ORITECH_PANEL)
                         .positioning(Positioning.absolute(176 + 117, 30)))
                .positioning(Positioning.relative(50, 50))
                .zIndex(-1)
            );
        }
        
        // equipment panel
        if (handler.armorSlots != null) {
            rootComponent.child(
              Containers.horizontalFlow(Sizing.fixed(176 + 250), Sizing.fixed(166 + 40))
                .child(Containers.horizontalFlow(Sizing.content(), Sizing.content())
                         .child(buildEquipmentPanel())
                         .surface(ORITECH_PANEL)
                         .positioning(Positioning.absolute(176 - 80, 30)))
                .positioning(Positioning.relative(50, 50))
                .zIndex(-1)
            );
        }
        
        // show oracle lib help button
        if (Oritech.CONFIG.enableHelpButton()) {
            var hasOracleLib = Platform.isModLoaded("oracle_index");
            Optional<Identifier> linkTarget = hasOracleLib ? getHelpBookLink() : Optional.empty();
            var oracleButton = Components.button(Text.literal("?"), elem -> onOracleButtonClick(hasOracleLib, linkTarget));
            oracleButton.renderer(ORITECH_BUTTON_DARK);
            if (hasOracleLib) {
                oracleButton.tooltip(Text.translatable("tooltip.oritech.oracle_available"));
            } else {
                oracleButton.tooltip(Text.translatable("tooltip.oritech.oracle_missing"));
            }
            
            // calculate help button position
            oracleButton.positioning(Positioning.relative(0, 96));
            oracleButton.zIndex(10);
            if (linkTarget.isPresent() || !hasOracleLib) {  // only show button if either lib is not installed, or a link is present
                rootComponent.child(
                  Containers.horizontalFlow(Sizing.fixed(176 + 25), Sizing.fixed(166 + 20))
                    .child(oracleButton)
                    .positioning(Positioning.relative(50, 50)));
            }
        }
        
        rootComponent.child(
          Components.texture(BACKGROUND, 0, 0, 176, 166, 176, 166)
        ).child(
          buildOverlay().positioning(Positioning.relative(50, 50))
        );
    }
    
    public boolean showExtensionPanel() {
        return handler.screenData.showExpansionPanel();
    }
    
    @Override
    protected void handledScreenTick() {
        super.handledScreenTick();
        
        if (handler.screenData.showEnergy()) {
            if (handler.steamStorage != null) {
                updateFluidDisplay(waterDisplay);
                updateFluidDisplay(steamDisplay);
            } else {
                updateEnergyBar();
            }
        }
        
        if (handler.screenData.showProgress())
            updateProgressBar();
        
        if (showExtensionPanel())
            updateSettingsButtons();
        
        if (handler.mainFluidContainer != null)
            updateFluidDisplay(genericDisplay);
        
        if (steamProductionLabel != null) {
            var productionRate = handler.screenData.getDisplayedEnergyUsage() * Oritech.CONFIG.generators.steamEngineData.rfToSteamRatio();
            productionRate = Math.min(this.waterDisplay.storage.getStack().getAmount(), productionRate);
            steamProductionLabel.text(Text.translatable("title.oritech.steam_production", String.format("%.0f", productionRate)));
        }
    }
    
    private void updateProgressBar() {
        var config = handler.screenData.getIndicatorConfiguration();
        var progress = handler.screenData.getProgress();
        
        
        if (handler.blockEntity instanceof MachineBlockEntity machineEntity && (machineEntity.getCurrentRecipe().getTime() > 0 || machineEntity.progress > 0)) {
            
            var progressTicks = machineEntity.progress;
            var recipeDurationTicks = machineEntity.getCurrentRecipe().getTime();
            var effectiveDurationTicks = (int) (recipeDurationTicks * machineEntity.getSpeedMultiplier());
            
            if (machineEntity instanceof UpgradableGeneratorBlockEntity generatorBlock) {
                if (recipeDurationTicks <= 0)
                    recipeDurationTicks = (int) (generatorBlock.currentMaxBurnTime / generatorBlock.getSpeedMultiplier() * generatorBlock.getEfficiencyMultiplier());
                effectiveDurationTicks = generatorBlock.currentMaxBurnTime;
            }
            
            if (machineEntity instanceof BasicGeneratorEntity generatorEntity)
                recipeDurationTicks = generatorEntity.currentMaxBurnTime;
            
            
            progress_indicator.tooltip(Text.translatable("tooltip.oritech.progress_indicator", progressTicks, effectiveDurationTicks, recipeDurationTicks));
        }
        
        
        if (config.horizontal()) {
            progress_indicator.visibleArea(PositionedRectangle.of(0, 0, (int) (config.width() * progress), config.height()));
        } else {
            progress_indicator.visibleArea(PositionedRectangle.of(0, 0, config.width(), (int) (config.height() * progress)));
        }
    }
    
    protected void updateEnergyBar() {
        
        var capacity = handler.energyStorage.getCapacity();
        var amount = handler.energyStorage.getAmount();
        
        var fillAmount = (float) amount / capacity;
        var tooltipText = getEnergyTooltip(amount, capacity, (long) handler.screenData.getDisplayedEnergyUsage(), (long) handler.screenData.getDisplayedEnergyTransfer());
        
        energyIndicator.tooltip(tooltipText);
        energyIndicator.visibleArea(PositionedRectangle.of(0, 96 - ((int) (96 * (fillAmount))), 24, (int) (96 * fillAmount)));
    }
    
    public static Text getEnergyTooltip(long amount, long max, long showedUsage, long showedTransfer) {
        var percentage = (float) amount / max;
        var energyFill = String.format("%.1f", percentage * 100);
        var storedAmount = TooltipHelper.getEnergyText(amount);
        var maxAmount = TooltipHelper.getEnergyText(max);
        var transfer = TooltipHelper.getEnergyText(showedTransfer);
        return Text.translatable("tooltip.oritech.energy_usage", storedAmount, maxAmount, energyFill, showedUsage, transfer);
    }
    
    public void updateSettingsButtons() {
        
        var activeMode = handler.screenData.getInventoryInputMode();
        var modeName = activeMode.name().toLowerCase();
        
        cycleInputButton.setMessage(Text.translatable("button.%s.input_mode_%s".formatted(Oritech.MOD_ID, modeName)).withColor(GRAY_TEXT_COLOR));
        cycleInputButton.tooltip(Text.translatable("tooltip.%s.input_mode_%s".formatted(Oritech.MOD_ID, modeName)));
    }
    
    private Component buildExtensionPanel() {
        
        var container = Containers.verticalFlow(Sizing.content(), Sizing.content());
        container.surface(Surface.PANEL_INSET);
        container.horizontalAlignment(HorizontalAlignment.CENTER);
        
        container.padding(Insets.of(1, 4, 1, 1));
        container.margins(Insets.of(7));
        
        addExtensionComponents(container);
        updateSettingsButtons();
        
        return container;
    }
    
    private Component buildEquipmentPanel() {
        
        var container = Containers.verticalFlow(Sizing.content(), Sizing.content());
        container.surface(Surface.PANEL_INSET);
        container.horizontalAlignment(HorizontalAlignment.CENTER);
        
        container.padding(Insets.of(2));
        container.margins(Insets.of(6));
        
        for (int i = handler.armorSlots.size() - 1; i >= 0; i--) {
            var slotId = handler.armorSlots.get(i);
            
            var slotContainer = Containers.horizontalFlow(Sizing.content(), Sizing.content());
            
            var slotComponent = slotAsComponent(slotId);
            var background = Components.texture(getEquipmentSlotTexture(i), 0, 0, 16, 16, 16, 16);
            
            slotContainer.child(slotComponent);
            slotContainer.child(background.positioning(Positioning.absolute(0, 0)));
            
            container.child(slotContainer.margins(Insets.of(1)));
            
            // separator box
            if (i > 0)
                container.child(Components.box(Sizing.fixed(18), Sizing.fixed(1)).color(new Color(0.8f, 0.8f, 0.8f)));
        }
        
        return container;
    }
    
    private Identifier getEquipmentSlotTexture(int armorSlot) {
        return switch (armorSlot) {
            case 0 -> Identifier.of("minecraft", "textures/item/empty_armor_slot_boots.png");
            case 1 -> Identifier.of("minecraft", "textures/item/empty_armor_slot_leggings.png");
            case 2 -> Identifier.of("minecraft", "textures/item/empty_armor_slot_chestplate.png");
            case 3 -> Identifier.of("minecraft", "textures/item/empty_armor_slot_helmet.png");
            case 4 -> Identifier.of("minecraft", "textures/item/empty_slot_axe.png");
            default -> null;
        };
        
    }
    
    public void addExtensionComponents(FlowLayout container) {
        
        cycleInputButton = Components.button(Text.translatable("button.oritech.input_mode_fill_matching_recipe").withColor(GRAY_TEXT_COLOR),
          button -> {
              NetworkContent.UI_CHANNEL.clientHandle().send(new NetworkContent.InventoryInputModeSelectorPacket(handler.blockPos));
          });
        cycleInputButton.horizontalSizing(Sizing.fixed(73));
        cycleInputButton.margins(Insets.of(3));
        cycleInputButton.renderer(ORITECH_BUTTON);
        cycleInputButton.textShadow(false);
        
        container.child(Components.label(Text.translatable("title.oritech.details")).margins(Insets.of(3, 1, 1, 1)));
        
        var inputSlots = handler.screenData.getGuiSlots().stream().filter(slot -> !slot.output()).count();
        if (handler.screenData.inputOptionsEnabled() && inputSlots > 1)
            container.child(cycleInputButton);
        
        for (var label : handler.screenData.getExtraExtensionLabels()) {
            container.child(Components.label(label.getLeft()).tooltip(label.getRight()).margins(Insets.of(3)));
        }
        
        if (handler.showRedstoneAddon()) {
            // separator
            container.child(Components.box(Sizing.fixed(73), Sizing.fixed(1))
                              .color(new Color(0.8f, 0.8f, 0.8f))
                              .margins(Insets.of(2)));
            
            // current input state
            var hasRedstone = handler.screenData.receivedRedstoneSignal() > 0;
            var statusContainer = Containers.horizontalFlow(Sizing.content(), Sizing.content());
            
            statusContainer.child(Components.block(Blocks.REDSTONE_TORCH.getDefaultState().with(RedstoneTorchBlock.LIT, hasRedstone))
                                    .sizing(Sizing.fixed(20)).margins(Insets.of(-6, -4, -8, -4)));
            statusContainer.child(Components.label(Text.translatable("text.oritech.redstone_power", handler.screenData.receivedRedstoneSignal()))
                              .margins(Insets.of(3, 1, 1, 1)));
            
            container.child(statusContainer);
            
            // current input state
            if (!handler.screenData.currentRedstoneEffect().isEmpty())
                container.child(Components.label(Text.translatable(handler.screenData.currentRedstoneEffect()))
                                  .tooltip(Text.translatable(handler.screenData.currentRedstoneEffect() + ".tooltip"))
                                  .margins(Insets.of(3, 3, 1, 1)));
        }
        
    }
    
    private FlowLayout buildOverlay() {
        
        var overlay = Containers.verticalFlow(Sizing.fixed(176), Sizing.fixed(166));
        fillOverlay(overlay);
        
        return overlay;
    }
    
    public void fillOverlay(FlowLayout overlay) {
        
        addTitle(overlay);
        
        if (handler.mainFluidContainer != null) {
            addFluidDisplay(overlay, genericDisplay);
            updateFluidDisplay(genericDisplay);
        }
        
        for (var slot : handler.screenData.getGuiSlots()) {
            overlay.child(this.slotAsComponent(slot.index()).positioning(Positioning.absolute(slot.x(), slot.y())));
            overlay.child(getItemFrame(slot.x(), slot.y()));
        }
        
        if (handler.screenData.showEnergy()) {
            if (handler.steamStorage != null) {
                addFluidDisplay(overlay, steamDisplay);
                updateFluidDisplay(steamDisplay);
                addFluidDisplay(overlay, waterDisplay);
                updateFluidDisplay(waterDisplay);
            } else {
                addEnergyBar(overlay);
                updateEnergyBar();
            }
            
            if (handler.blockEntity instanceof SteamEngineEntity) {
                addEnergyBar(overlay);
                updateEnergyBar();
            }
        }
        
        if (handler.screenData.showProgress()) {
            addProgressArrow(overlay);
            updateProgressBar();
        }
    }
    
    @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
    private void onOracleButtonClick(boolean enabled, Optional<Identifier> target) {
        if (!enabled || target.isEmpty()) {
            Oritech.LOGGER.info("Oracle Index mod is missing. Install it here: https://www.curseforge.com/minecraft/mc-mods/oracle-index (or from modrinth)");
            return;
        }
        
        OracleClient.openScreen("oritech", target.get(), this);
    }
    
    private Optional<Identifier> getHelpBookLink() {
        
        if (this.handler.screenData.getWikiLink().isPresent()) return Optional.of(Identifier.of(Oracle.MOD_ID, "books/oritech/" + handler.screenData.getWikiLink().get() + ".mdx"));
        
        var blockItem = this.handler.machineBlock.getBlock().asItem();
        var itemId = Registries.ITEM.getId(blockItem);
        
        if (OracleClient.ITEM_LINKS.containsKey(itemId)) {
            return Optional.of(OracleClient.ITEM_LINKS.get(itemId).linkTarget());
        } else {
            return Optional.empty();
        }
        
    }
    
    public boolean useHighTitle() {
        return handler.machineBlock.getBlock().getName().toString().length() > 18;
    }
    
    private void addTitle(FlowLayout overlay) {
        var blockTitle = handler.machineBlock.getBlock().getName();
        var label = Components.label(blockTitle);
        label.color(new Color(64 / 255f, 64 / 255f, 64 / 255f));
        label.zIndex(1);
        
        var blockIcon = Components.item(getTitleIcon());
        blockIcon.sizing(Sizing.fixed(24));
        
        var iconPanel = Containers.horizontalFlow(Sizing.content(0), Sizing.content(0));
        iconPanel.padding(Insets.of(2, 5, 3, 3));
        iconPanel.child(blockIcon);
        iconPanel.surface(ORITECH_PANEL);
        iconPanel.zIndex(50);
        
        var textPanel = Containers.horizontalFlow(Sizing.content(0), Sizing.content(0));
        textPanel.padding(Insets.of(5, 6, 6, 5));
        textPanel.child(label);
        textPanel.surface(ORITECH_PANEL);
        
        var combinedPanel = Containers.horizontalFlow(Sizing.content(), Sizing.content());
        combinedPanel.child(iconPanel);
        combinedPanel.child(textPanel.margins(Insets.of(4, 0, -1, 0)));
        
        var horizontalPos = blockTitle.getString().length() > 15 ? 100 : 65;
        var verticalPos = useHighTitle()? - 25 : - 15;
        
        overlay.child(combinedPanel.positioning(Positioning.relative(horizontalPos, verticalPos)));
        overlay.allowOverflow(true);
        
    }
    
    public ItemStack getTitleIcon() {
        return new ItemStack(this.handler.blockEntity.getCachedState().getBlock());
    }
    
    private void addProgressArrow(FlowLayout panel) {
        
        var config = handler.screenData.getIndicatorConfiguration();
        
        var empty = Components.texture(config.empty(), 0, 0, config.width(), config.height(), config.width(), config.height());
        progress_indicator = Components.texture(config.full(), 0, 0, config.width(), config.height(), config.width(), config.height());
        
        panel
          .child(empty.positioning(Positioning.absolute(config.x(), config.y())))
          .child(progress_indicator.positioning(Positioning.absolute(config.x(), config.y())));
    }
    
    protected void addFluidDisplay(FlowLayout panel, FluidDisplay display) {
        panel.child(display.background);
        panel.child(display.fillOverlay);
        panel.child(display.foreGround);
    }
    
    protected void updateFluidDisplay(FluidDisplay display) {
        
        var background = display.background;
        var container = display.storage;
        var config = display.config;
        
        // fluid variant inside has changed
        if (!display.lastDrawnFluid.equals(container.getStack().getFluid())) {
            var parent = background.parent();
            var targetIndex = parent.children().indexOf(background);
            var newFluid = createFluidRenderer(container.getStack(), config);
            parent.removeChild(background);
            ((FlowLayout) parent).child(targetIndex, newFluid);
            background = newFluid;
            display.background = background;
            display.lastDrawnFluid = container.getStack().getFluid();
        }
        
        var fill = 1 - ((float) container.getStack().getAmount() / container.getCapacity());
        
        var targetFill = LaserArmModel.lerp(display.lastFill, fill, 0.15f);
        display.lastFill = targetFill;
        
        display.fillOverlay.verticalSizing(Sizing.fixed((int) (config.height() * targetFill * 0.98f)));
        
        var tooltipText = container.getStack().getAmount() > 0
            ? Text.translatable("tooltip.oritech.fluid_content", container.getStack().getAmount() * 1000 / FluidStackHooks.bucketAmount(), FluidStackHooks.getName(container.getStack()).getString())
            : Text.translatable("tooltip.oritech.fluid_empty");
        background.tooltip(tooltipText);
    }
    
    public static ColoredSpriteComponent createFluidRenderer(FluidStack stack, ScreenProvider.BarConfiguration config) {
        var sprite = FluidStackHooks.getStillTexture(stack);
        var spriteColor = FluidStackHooks.getColor(stack);
        
        var parsedColor = Color.ofArgb(spriteColor);
        var opaqueColor = new Color(parsedColor.red(), parsedColor.green(), parsedColor.blue(), 1f);
        spriteColor = opaqueColor.argb();
        
        return getColoredSpriteComponent(stack, config, sprite, spriteColor);
    }
    
    @NotNull
    private static ColoredSpriteComponent getColoredSpriteComponent(FluidStack stack, ScreenProvider.BarConfiguration config, Sprite sprite, int spriteColor) {
        var tooltipText = stack.getAmount() > 0
            ? Text.translatable("tooltip.oritech.fluid_content", stack.getAmount() * 1000 / FluidStackHooks.bucketAmount(), FluidStackHooks.getName(stack).toString())
            : Text.translatable("tooltip.oritech.fluid_empty");
        
        var result = new ColoredSpriteComponent(sprite);
        result.widthMultiplier = config.width() / 60f;
        result.color = Color.ofArgb(spriteColor);
        result.sizing(Sizing.fixed(config.width()), Sizing.fixed(config.height()));
        result.positioning(Positioning.absolute(config.x(), config.y()));
        result.tooltip(tooltipText);
        return result;
    }
    
    private void addEnergyBar(FlowLayout panel) {
        
        var config = handler.screenData.getEnergyConfiguration();
        var insetSize = 1;
        var tooltipText = Text.translatable("tooltip.oritech.energy_indicator", 10, 50);
        
        var frame = Containers.horizontalFlow(Sizing.fixed(config.width() + insetSize * 2), Sizing.fixed(config.height() + insetSize * 2));
        frame.surface(Surface.PANEL_INSET);
        frame.padding(Insets.of(insetSize));
        frame.positioning(Positioning.absolute(config.x() - insetSize, config.y() - insetSize));
        panel.child(frame);
        
        var indicator_background = Components.texture(getGuiComponents(), 24, 0, 24, 96, 98, 96);
        indicator_background.sizing(Sizing.fixed(config.width()), Sizing.fixed(config.height()));
        
        energyIndicator = Components.texture(getGuiComponents(), 0, 0, 24, (96), 98, 96);
        energyIndicator.sizing(Sizing.fixed(config.width()), Sizing.fixed(config.height()));
        energyIndicator.positioning(Positioning.absolute(0, 0));
        energyIndicator.tooltip(tooltipText);
        
        frame
          .child(indicator_background)
          .child(energyIndicator);
    }
    
    public static class ColoredSpriteComponent extends SpriteComponent {
        
        public Color color;
        public float widthMultiplier = 1f;
        
        protected ColoredSpriteComponent(Sprite sprite) {
            super(sprite);
        }
        
        public Sprite getSprite() {
            return sprite;
        }
        
        @Override
        public void draw(OwoUIDrawContext context, int mouseX, int mouseY, float partialTicks, float delta) {
            if (sprite == null) return;
            SpriteUtilInvoker.markSpriteActive(this.sprite);
            drawSprite(this.x, this.y, 0, this.width, this.height, this.sprite, this.color.red(), this.color.green(), this.color.blue(), this.color.alpha(), context.getMatrices());
        }
        
        // these 2 methods are copies from drawContext, width slight modifications
        public void drawSprite(int x, int y, int z, int width, int height, Sprite sprite, float red, float green, float blue, float alpha, MatrixStack matrices) {
            
            var uvWidth = sprite.getMaxU() - sprite.getMinU();
            var newMax = sprite.getMinU() + uvWidth * widthMultiplier;
            
            this.drawTexturedQuad(sprite.getAtlasId(), matrices, x, x + width, y, y + height, z, sprite.getMinU(), newMax, sprite.getMinV(), sprite.getMaxV(), red, green, blue, alpha);
        }
        
        // direct copy of the method in drawContext, because it can't be called from here due to private access
        private void drawTexturedQuad(Identifier texture, MatrixStack matrices, int x1, int x2, int y1, int y2, int z, float u1, float u2, float v1, float v2, float red, float green, float blue, float alpha) {
            RenderSystem.setShaderTexture(0, texture);
            RenderSystem.setShader(GameRenderer::getPositionTexColorProgram);
            RenderSystem.enableBlend();
            Matrix4f matrix4f = matrices.peek().getPositionMatrix();
            BufferBuilder bufferBuilder = Tessellator.getInstance().begin(VertexFormat.DrawMode.QUADS, VertexFormats.POSITION_TEXTURE_COLOR);
            bufferBuilder.vertex(matrix4f, (float) x1, (float) y1, (float) z).texture(u1, v1).color(red, green, blue, alpha);
            bufferBuilder.vertex(matrix4f, (float) x1, (float) y2, (float) z).texture(u1, v2).color(red, green, blue, alpha);
            bufferBuilder.vertex(matrix4f, (float) x2, (float) y2, (float) z).texture(u2, v2).color(red, green, blue, alpha);
            bufferBuilder.vertex(matrix4f, (float) x2, (float) y1, (float) z).texture(u2, v1).color(red, green, blue, alpha);
            BufferRenderer.drawWithGlobalProgram(bufferBuilder.end());
            RenderSystem.disableBlend();
        }
        
    }
}