diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/StatefulUIInventory.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/StatefulUIInventory.java
new file mode 100644
index 00000000..4dbb2fa8
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/StatefulUIInventory.java
@@ -0,0 +1,70 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui;
+
+import de.steamwar.inventory.ui.util.UpdateStream;
+import org.bukkit.entity.Player;
+
+public abstract class StatefulUIInventory extends UIInventory implements UpdateStream.Subscriber {
+
+ private T state;
+ private final UpdateStream stream;
+ private UpdateStream.Unsubscriber unsubscriber;
+
+ protected abstract UIComponent render(T state);
+ protected abstract void update(T state);
+
+ public StatefulUIInventory(UpdateStream stateObservable) {
+ this.stream = stateObservable;
+ }
+
+ public void dispose() {
+ unsubscriber.unsubscribe();
+ this.unsubscriber = null;
+ }
+
+ @Override
+ public UIComponent render() {
+ return render(state);
+ }
+
+ @Override
+ public void onUpdate(T current) {
+ state = current;
+ update(current);
+ updateUI();
+ }
+
+ @Override
+ void onClose() {
+ if (viewers.isEmpty()) {
+ dispose();
+ }
+ }
+
+ @Override
+ public void addViewer(Player... players) {
+ if (viewers.isEmpty()) {
+ unsubscriber = stream.subscribe(this);
+ }
+
+ super.addViewer(players);
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/UIComponent.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/UIComponent.java
new file mode 100644
index 00000000..d0206018
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/UIComponent.java
@@ -0,0 +1,82 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui;
+
+import de.steamwar.inventory.ui.item.Enchanted;
+import de.steamwar.inventory.ui.item.OnClick;
+import de.steamwar.inventory.ui.layout.FixedSizeContainer;
+import de.steamwar.inventory.ui.util.*;
+import org.bukkit.event.inventory.InventoryClickEvent;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public interface UIComponent {
+ ComponentRender render(RenderContext context);
+
+ default UIComponent onClick(Consumer event) {
+ return onClick(false, event);
+ }
+
+ default UIComponent onClick(boolean allowDoubleClick, Consumer event) {
+ return new OnClick(this, allowDoubleClick, inventoryClickEvent -> {
+ event.accept(inventoryClickEvent);
+ return true;
+ });
+ }
+
+ default UIComponent onClickWithUpdate(Function event) {
+ return onClickWithUpdate(false, event);
+ }
+
+ default UIComponent onClickWithUpdate(boolean allowDoubleClick, Function event) {
+ return new OnClick(this, allowDoubleClick, event);
+ }
+
+ default UIComponent onClickNoUpdate(Consumer event) {
+ return onClickNoUpdate(false, event);
+ }
+
+ default UIComponent onClickNoUpdate(boolean allowDoubleClick, Consumer event) {
+ return new OnClick(this, allowDoubleClick, inventoryClickEvent -> {
+ event.accept(inventoryClickEvent);
+ return false;
+ });
+ }
+
+ default UIComponent cancelOnClick() {
+ return new OnClick(this, true, inventoryClickEvent -> false);
+ }
+
+ default UIComponent enchanted() {
+ return new Enchanted(this);
+ }
+
+ default UIComponent enchanted(boolean enchanted) {
+ if (enchanted)
+ return enchanted();
+ else
+ return this;
+ }
+
+ default UIComponent size(Size size) {
+ return new FixedSizeContainer(size, this);
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/UIFragment.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/UIFragment.java
new file mode 100644
index 00000000..3e5a6583
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/UIFragment.java
@@ -0,0 +1,33 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui;
+
+import de.steamwar.inventory.ui.util.ComponentRender;
+import de.steamwar.inventory.ui.util.RenderContext;
+
+public abstract class UIFragment implements UIComponent {
+
+ abstract public UIComponent build(RenderContext context);
+
+ @Override
+ public ComponentRender render(RenderContext context) {
+ return build(context).render(context);
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/UIInventory.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/UIInventory.java
new file mode 100644
index 00000000..1a828cab
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/UIInventory.java
@@ -0,0 +1,166 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui;
+
+import de.steamwar.core.Core;
+import de.steamwar.inventory.ui.util.ComponentRender;
+import de.steamwar.inventory.ui.util.RenderContext;
+import de.steamwar.inventory.ui.util.RenderItem;
+import de.steamwar.inventory.ui.util.Size;
+import de.steamwar.message.Message;
+import lombok.AllArgsConstructor;
+import lombok.Setter;
+import org.bukkit.entity.Item;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.InventoryView;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public abstract class UIInventory {
+ protected final List viewers = new ArrayList<>();
+ private final Map callbacks = new HashMap<>();
+
+ private UIInventory parent;
+ private Player currentRender;
+
+ public abstract UIComponent render();
+ public abstract Inventory createInventory(Player player);
+ public abstract Message getMessages();
+
+ void onClose() { }
+
+ public void addViewer(Player... players) {
+ viewers.addAll(Arrays.asList(players));
+
+ for (Player player : players) {
+ openInventory(player);
+ }
+ }
+
+ private void openInventory(Player player) {
+ Inventory inventory = createInventory(player);
+ renderPlayer(player, inventory);
+ InventoryView view = player.openInventory(inventory);
+ Core.getInstance().getLogger().info("[UIINV] Opened " + view.getTitle() + " for " + player.getName());
+ }
+
+ private void renderPlayer(Player player, Inventory inventory) {
+ int width = Math.min(inventory.getSize(), 9);
+ int height = Math.max(inventory.getSize() / 9, 1);
+
+ currentRender = player;
+ ComponentRender render = render().render(new RenderContext(new Size(width, height), new Size(width, height), this));
+ currentRender = null;
+
+ if (render.getSize().getWidth() > width || render.getSize().getHeight() > height) {
+ throw new IllegalArgumentException("The UIComponent does not fit in the inventory");
+ }
+
+ Message message = getMessages();
+
+ ItemStack[] itemStacks = new ItemStack[inventory.getSize()];
+
+ render.getItems().forEach(item -> itemStacks[item.getX() + item.getY() * 9] = item.getItemStack().message(message).build(player));
+
+ inventory.setContents(itemStacks);
+
+ Map> clickEventMap = render.getItems().stream()
+ .filter(item -> item.getClickEvent() != null)
+ .collect(Collectors.toMap(renderItem -> renderItem.getX() + renderItem.getY() * 9, RenderItem::getClickEvent));
+
+ callbacks.computeIfAbsent(player, p -> new OpenPlayerInventory(p, clickEventMap).register()).setCallbacks(clickEventMap);
+ }
+
+ protected void updateTitle(String title, Object... params) {
+ if (currentRender != null) {
+ currentRender.getOpenInventory().setTitle(getMessages().parse(title, currentRender, params));
+ }
+ }
+
+ protected void updateUI() {
+ viewers.forEach(player -> renderPlayer(player, player.getOpenInventory().getTopInventory()));
+ }
+
+ protected void push(Player player, UIInventory inventory) {
+ inventory.parent = this;
+ inventory.addViewer(player);
+ viewers.remove(player);
+ }
+
+ protected void pop(Player player) {
+ if(parent == null)
+ return;
+
+ parent.addViewer(player);
+ }
+
+ @AllArgsConstructor
+ @Setter
+ private class OpenPlayerInventory implements Listener {
+ private final Player player;
+ private Map> callbacks;
+
+ OpenPlayerInventory register() {
+ Core.getInstance().getServer().getPluginManager().registerEvents(this, Core.getInstance());
+ return this;
+ }
+
+ @EventHandler
+ public void onInventoryClick(InventoryClickEvent e) {
+ if (!player.equals(e.getWhoClicked()))
+ return;
+
+ if (callbacks.containsKey(e.getRawSlot()) && callbacks.get(e.getRawSlot()) != null) {
+ e.setCancelled(true);
+ Core.getInstance().getLogger().info("[UIINV] " + e.getWhoClicked().getName() + " " + e.getClick().name() + " clicked " + e.getRawSlot() + " on " + (e.getCurrentItem() != null ? e.getCurrentItem().getItemMeta().getDisplayName() : "[EMPTY]") + " in " + e.getView().getTitle());
+ if (callbacks.get(e.getRawSlot()).apply(e)) {
+ updateUI();
+ }
+ }
+ }
+
+ @EventHandler
+ public void onInventoryClose(InventoryCloseEvent e){
+ if(!player.equals(e.getPlayer()))
+ return;
+
+ InventoryClickEvent.getHandlerList().unregister(this);
+ InventoryCloseEvent.getHandlerList().unregister(this);
+ Core.getInstance().getLogger().info("[UIINV] " + player.getName() + " closed " + e.getView().getTitle());
+ if(callbacks.containsKey(-1))
+ callbacks.get(-1).apply(null);
+
+ viewers.remove(player);
+ UIInventory.this.callbacks.remove(player);
+
+ onClose();
+ }
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/fragments/ActionBar.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/fragments/ActionBar.java
new file mode 100644
index 00000000..e4404c85
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/fragments/ActionBar.java
@@ -0,0 +1,62 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.fragments;
+
+import de.steamwar.inventory.ui.UIComponent;
+import de.steamwar.inventory.ui.UIFragment;
+import de.steamwar.inventory.ui.item.ItemBuilder;
+import de.steamwar.inventory.ui.item.UIItem;
+import de.steamwar.inventory.ui.layout.Row;
+import de.steamwar.inventory.ui.util.RenderContext;
+import de.steamwar.inventory.ui.util.Size;
+import lombok.Getter;
+import lombok.Setter;
+import org.bukkit.Material;
+
+import java.util.Arrays;
+import java.util.Map;
+
+@Getter
+@Setter
+public class ActionBar extends UIFragment {
+ private static final UIComponent DEFAULT_FILLER = new UIItem(ItemBuilder.of(Material.GRAY_STAINED_GLASS_PANE).displayName("§e")).cancelOnClick();
+
+ private final Map actionBar;
+ private UIComponent filler = DEFAULT_FILLER;
+
+ public ActionBar(Map actionBar) {
+ this.actionBar = actionBar;
+ }
+
+ public ActionBar filler(UIComponent filler) {
+ this.filler = filler;
+ return this;
+ }
+
+ @Override
+ public UIComponent build(RenderContext context) {
+ UIComponent[] components = new UIComponent[context.getContainerSize().getWidth()];
+ for (int i = 0; i < components.length; i++) {
+ components[i] = actionBar.getOrDefault(i, filler);
+ }
+
+ return new Row(Arrays.asList(components)).size(new Size(context.getContainerSize().getWidth(), 1));
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/fragments/HorizontalScrollLayout.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/fragments/HorizontalScrollLayout.java
new file mode 100644
index 00000000..456bf46c
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/fragments/HorizontalScrollLayout.java
@@ -0,0 +1,93 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.fragments;
+
+import de.steamwar.core.Core;
+import de.steamwar.inventory.ui.UIComponent;
+import de.steamwar.inventory.ui.UIFragment;
+import de.steamwar.inventory.ui.item.ItemBuilder;
+import de.steamwar.inventory.ui.item.UIItem;
+import de.steamwar.inventory.ui.layout.Column;
+import de.steamwar.inventory.ui.layout.FixedSizeContainer;
+import de.steamwar.inventory.ui.layout.Row;
+import de.steamwar.inventory.ui.util.RenderContext;
+import lombok.Setter;
+import org.bukkit.DyeColor;
+
+import java.util.*;
+
+@Setter
+public class HorizontalScrollLayout extends UIFragment {
+ private List data;
+ private int scroll = 0;
+ private final Map actionBar = new HashMap<>();
+
+ public HorizontalScrollLayout(List data) {
+ this.data = data;
+ }
+
+ private int maxScroll() {
+ return Math.max(0, Math.min(scroll, data.size() - 9 + 1));
+ }
+
+ public HorizontalScrollLayout actionBar(Map actionBar) {
+ this.actionBar.clear();
+ this.actionBar.putAll(actionBar);
+ return this;
+ }
+
+ public HorizontalScrollLayout data(List data) {
+ this.data = data;
+ return this;
+ }
+
+ @Override
+ public UIComponent build(RenderContext context) {
+ scroll = maxScroll();
+ boolean hasPrev = scroll > 0;
+ boolean hasNext = data.size() - scroll > 9;
+
+ Map actionBar = new HashMap<>(this.actionBar);
+
+ actionBar.put(0, new UIItem(ItemBuilder.color(hasPrev ? DyeColor.LIME : DyeColor.GRAY).message(Core.MESSAGE).translateName(hasPrev ? "SWLISINV_PREVIOUS_PAGE_ACTIVE" : "SWLISINV_PREVIOUS_PAGE_INACTIVE")).onClickWithUpdate(inventoryClickEvent -> {
+ if (scroll > 0) {
+ scroll = Math.max(0, scroll - 9);
+ return true;
+ } else {
+ return false;
+ }
+ }));
+ actionBar.put(8, new UIItem(ItemBuilder.color(hasNext ? DyeColor.LIME : DyeColor.GRAY).message(Core.MESSAGE).translateName(hasNext ? "SWLISINV_NEXT_PAGE_ACTIVE" : "SWLISINV_NEXT_PAGE_INACTIVE")).onClickWithUpdate(inventoryClickEvent -> {
+ if (hasNext) {
+ scroll = Math.min(scroll + 9, data.size() - 9);
+ return true;
+ } else {
+ return false;
+ }
+ }));
+
+ return new Column(
+ new FixedSizeContainer(context.getContainerSize().shrink(0, 1),
+ new Row(data.subList(scroll, Math.min(data.size(), scroll + 9)))
+ ),
+ new ActionBar(actionBar)
+ );
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/fragments/PageLayout.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/fragments/PageLayout.java
new file mode 100644
index 00000000..d17a83ba
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/fragments/PageLayout.java
@@ -0,0 +1,105 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.fragments;
+
+import de.steamwar.core.Core;
+import de.steamwar.inventory.ui.UIComponent;
+import de.steamwar.inventory.ui.UIFragment;
+import de.steamwar.inventory.ui.item.ItemBuilder;
+import de.steamwar.inventory.ui.item.UIItem;
+import de.steamwar.inventory.ui.layout.Column;
+import de.steamwar.inventory.ui.layout.FixedSizeContainer;
+import de.steamwar.inventory.ui.layout.Grid;
+import de.steamwar.inventory.ui.util.RenderContext;
+import lombok.Data;
+import lombok.Getter;
+import lombok.Setter;
+import org.bukkit.DyeColor;
+import org.bukkit.event.inventory.ClickType;
+
+import java.util.*;
+import java.util.function.Consumer;
+
+@Getter
+@Setter
+public class PageLayout extends UIFragment {
+
+ private final List> items;
+ private final UIListCallback callback;
+ private int page = 0;
+ private Consumer onPageChange = null;
+ private Map itemsOnBar = Collections.emptyMap();
+
+ public PageLayout(List> items, UIListCallback callback) {
+ System.out.println(items.size());
+ this.items = items;
+ this.callback = callback;
+ }
+
+ @Override
+ public UIComponent build(RenderContext context) {
+ List uiItems = new ArrayList<>();
+
+ int itemsOnPage = context.getContainerSize().getWidth() * (context.getContainerSize().getHeight() - 1);
+
+ int ipageLimit = items.size() - page * itemsOnPage;
+ if (ipageLimit > itemsOnPage) {
+ ipageLimit = itemsOnPage;
+ }
+
+ int i = page * itemsOnPage;
+ for (int ipage = 0; ipage < ipageLimit; ipage++) {
+ UIListEntry e = items.get(i);
+
+ uiItems.add(new UIItem(e.item).onClickNoUpdate(inventoryClickEvent -> callback.clicked(inventoryClickEvent.getClick(), e.object)));
+ i++;
+ }
+
+ Map uiBar = new HashMap<>(itemsOnBar);
+
+ if (page != 0) {
+ uiBar.put(0, new UIItem(ItemBuilder.color(DyeColor.LIME).message(Core.MESSAGE).translateName("SWLISINV_PREVIOUS_PAGE_ACTIVE")).onClick(inventoryClickEvent -> page--));
+ } else {
+ uiBar.put(0, new UIItem(ItemBuilder.color(DyeColor.GRAY).message(Core.MESSAGE).translateName("SWLISINV_PREVIOUS_PAGE_INACTIVE")).cancelOnClick());
+ }
+ if (page < items.size() / itemsOnPage - (items.size() % itemsOnPage == 0 ? 1 : 0)) {
+ uiBar.put(8, new UIItem(ItemBuilder.color(DyeColor.LIME).message(Core.MESSAGE).translateName("SWLISINV_NEXT_PAGE_ACTIVE")).onClick(inventoryClickEvent -> page++));
+ } else {
+ uiBar.put(8, new UIItem(ItemBuilder.color(DyeColor.GRAY).message(Core.MESSAGE).translateName("SWLISINV_NEXT_PAGE_INACTIVE")).cancelOnClick());
+ }
+
+ return new Column(
+ new FixedSizeContainer(context.getContainerSize().shrink(0, 1),
+ new Grid(uiItems)),
+ new ActionBar(uiBar)
+ );
+ }
+
+
+ public interface UIListCallback{
+ void clicked(ClickType click, T element);
+ }
+
+ @Data
+ public static class UIListEntry{
+ final ItemBuilder item;
+ final T object;
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/Enchanted.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/Enchanted.java
new file mode 100644
index 00000000..69f12695
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/Enchanted.java
@@ -0,0 +1,44 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.item;
+
+import de.steamwar.inventory.ui.UIComponent;
+import de.steamwar.inventory.ui.util.ComponentRender;
+import de.steamwar.inventory.ui.util.RenderContext;
+import de.steamwar.inventory.ui.util.RenderItem;
+
+public class Enchanted implements UIComponent {
+ private final UIComponent children;
+
+ public Enchanted(UIComponent children) {
+ this.children = children;
+ }
+
+ @Override
+ public ComponentRender render(RenderContext context) {
+ ComponentRender render = children.render(context);
+
+ for (RenderItem item : render.getItems()) {
+ item.getItemStack().enchanted();
+ }
+
+ return render;
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/ItemBuilder.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/ItemBuilder.java
new file mode 100644
index 00000000..351b4fe6
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/ItemBuilder.java
@@ -0,0 +1,186 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.item;
+
+import de.steamwar.core.FlatteningWrapper;
+import de.steamwar.core.TrickyTrialsWrapper;
+import de.steamwar.inventory.SWItem;
+import de.steamwar.message.Message;
+import de.steamwar.message.SubMessage;
+import org.bukkit.DyeColor;
+import org.bukkit.Material;
+import org.bukkit.OfflinePlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class ItemBuilder {
+ private static final Map DYE_COLOR_INTEGER_MAP = new EnumMap<>(DyeColor.class);
+
+ static {
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.BLACK, 16);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.RED, 1);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.GREEN, 2);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.BROWN, 3);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.BLUE, 4);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.PURPLE, 5);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.CYAN, 6);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.LIGHT_GRAY, 7);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.GRAY, 8);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.PINK, 9);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.LIME, 10);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.YELLOW, 11);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.LIGHT_BLUE, 12);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.MAGENTA, 13);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.ORANGE, 14);
+ DYE_COLOR_INTEGER_MAP.put(DyeColor.WHITE, 15);
+ }
+
+ private final ItemStack itemStack;
+ private final ItemMeta itemMeta;
+ private Message message = null;
+ private SubMessage translateName = null;
+ private List translateLore = null;
+
+ private ItemBuilder(Material material) {
+ this(new ItemStack(material));
+ }
+ private ItemBuilder(ItemStack item) {
+ this.itemStack = item;
+ this.itemMeta = itemStack.getItemMeta();
+ }
+
+ public static ItemBuilder of(Material material) {
+ return new ItemBuilder(material);
+ }
+
+ public static ItemBuilder of(ItemStack itemStack) {
+ return new ItemBuilder(itemStack.getType()).amount(itemStack.getAmount());
+ }
+
+ private static ItemBuilder of(Material material, Byte meta) {
+ ItemStack itemStack;
+ try {
+ itemStack = new ItemStack(material, 1, (short)0, meta);
+ } catch (IllegalArgumentException e) {
+ itemStack = new ItemStack(material, 1);
+ }
+
+ return new ItemBuilder(itemStack);
+ }
+
+ public static ItemBuilder color(DyeColor color) {
+ return ItemBuilder.of(SWItem.getDye(DYE_COLOR_INTEGER_MAP.get(color)), DYE_COLOR_INTEGER_MAP.get(color).byteValue());
+ }
+
+ public static ItemBuilder skull(OfflinePlayer player) {
+ return skull(player.getName());
+ }
+
+ public static ItemBuilder skull(String name) {
+ return ItemBuilder.of(FlatteningWrapper.impl.setSkullOwner(name));
+ }
+
+ public ItemBuilder message(Message message) {
+ if (this.message == null) {
+ this.message = message;
+ }
+ return this;
+ }
+
+ public ItemStack build(Player player) {
+ if (message != null) {
+ if (translateName != null) {
+ itemMeta.setDisplayName(message.parse(translateName.getMessage(), player, translateName.getParams()));
+ }
+ if (translateLore != null) {
+ itemMeta.setLore(translateLore.stream().map(subMessage -> message.parse(subMessage.getMessage(), player, subMessage.getParams())).collect(Collectors.toList()));
+ }
+ }
+ itemStack.setItemMeta(itemMeta);
+ return itemStack;
+ }
+
+ public ItemBuilder amount(int amount) {
+ itemStack.setAmount(amount);
+ return this;
+ }
+
+ public ItemBuilder displayName(String name) {
+ itemMeta.setDisplayName(name);
+ return this;
+ }
+
+ public ItemBuilder lore(String... lore) {
+ if (itemMeta.hasLore()) {
+ List loreList = itemMeta.getLore();
+ loreList.addAll(Arrays.asList(lore));
+ itemMeta.setLore(loreList);
+ } else {
+ itemMeta.setLore(Arrays.asList(lore));
+ }
+
+ return this;
+ }
+
+ public ItemBuilder translateName(String name, Object... params) {
+ this.translateName = new SubMessage(name, params);
+ return this;
+ }
+
+ public ItemBuilder addTranslateLore(SubMessage... lore) {
+ if (this.translateLore == null) {
+ this.translateLore = Arrays.asList(lore);
+ } else {
+ this.translateLore.addAll(Arrays.asList(lore));
+ }
+
+ return this;
+ }
+
+ public ItemBuilder enchanted() {
+ itemMeta.addEnchant(TrickyTrialsWrapper.impl.getUnbreakingEnchantment(), 10, true);
+ return this;
+ }
+
+ public ItemBuilder enchanted(boolean enchanted) {
+ if(enchanted) {
+ return enchanted();
+ } else {
+ return this;
+ }
+ }
+
+ public ItemBuilder hideFlags() {
+ for (ItemFlag flag : EnumSet.allOf(ItemFlag.class)) {
+ itemMeta.addItemFlags(flag);
+ }
+ return this;
+ }
+
+ public ItemBuilder customModel(int modelData) {
+ itemMeta.setCustomModelData(modelData);
+ return this;
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/OnClick.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/OnClick.java
new file mode 100644
index 00000000..f6d3e203
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/OnClick.java
@@ -0,0 +1,59 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.item;
+
+import de.steamwar.inventory.ui.UIComponent;
+import de.steamwar.inventory.ui.util.ComponentRender;
+import de.steamwar.inventory.ui.util.RenderContext;
+import de.steamwar.inventory.ui.util.RenderItem;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.event.inventory.InventoryClickEvent;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public class OnClick implements UIComponent {
+ private final UIComponent children;
+ private final boolean allowDoubleClick;
+ private final Function event;
+
+ public OnClick(UIComponent children, boolean allowDoubleClick, Function event) {
+ this.children = children;
+ this.allowDoubleClick = allowDoubleClick;
+ this.event = event;
+ }
+
+ @Override
+ public ComponentRender render(RenderContext context) {
+ ComponentRender render = children.render(context);
+
+ for (RenderItem item : render.getItems()) {
+ item.setClickEvent(ev -> {
+ if (ev.getClick() != ClickType.DOUBLE_CLICK || allowDoubleClick) {
+ return event.apply(ev);
+ }
+
+ return false;
+ });
+ }
+
+ return render;
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/Space.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/Space.java
new file mode 100644
index 00000000..d947966c
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/Space.java
@@ -0,0 +1,37 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.item;
+
+import de.steamwar.inventory.ui.UIComponent;
+import de.steamwar.inventory.ui.util.ComponentRender;
+import de.steamwar.inventory.ui.util.RenderContext;
+import de.steamwar.inventory.ui.util.Size;
+
+import java.util.ArrayList;
+
+public class Space implements UIComponent {
+ @Override
+ public ComponentRender render(RenderContext context) {
+ return new ComponentRender(
+ new ArrayList<>(),
+ new Size(1, 1)
+ );
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/UIItem.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/UIItem.java
new file mode 100644
index 00000000..cdc7de01
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/item/UIItem.java
@@ -0,0 +1,47 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.item;
+
+import de.steamwar.inventory.ui.UIComponent;
+import de.steamwar.inventory.ui.util.*;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.Arrays;
+
+public class UIItem implements UIComponent {
+
+ private final ItemBuilder itemStack;
+
+ public UIItem(ItemStack itemStack) {
+ this(ItemBuilder.of(itemStack));
+ }
+
+ public UIItem(ItemBuilder itemStack) {
+ this.itemStack = itemStack;
+ }
+
+ @Override
+ public ComponentRender render(RenderContext context) {
+ return new ComponentRender(
+ Arrays.asList(new RenderItem(0, 0, itemStack, null)),
+ new Size(1, 1)
+ );
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/Alignment.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/Alignment.java
new file mode 100644
index 00000000..3c3bf755
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/Alignment.java
@@ -0,0 +1,27 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.layout;
+
+public enum Alignment {
+ START,
+ CENTER,
+ END,
+ BETWEEN
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/Column.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/Column.java
new file mode 100644
index 00000000..a17abaf7
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/Column.java
@@ -0,0 +1,93 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.layout;
+
+import de.steamwar.inventory.ui.UIComponent;
+import de.steamwar.inventory.ui.util.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class Column implements UIComponent {
+
+ private final List components;
+
+ private Alignment alignment;
+ private boolean shrinkWrap = false;
+
+ public Column(UIComponent... components) {
+ this(Arrays.asList(components));
+ }
+
+ public Column(List components) {
+ this.components = components;
+ }
+
+ public Column centered() {
+ this.alignment = Alignment.CENTER;
+ return this;
+ }
+
+ public Column setShrinkWrap(boolean shrinkWrap) {
+ this.shrinkWrap = shrinkWrap;
+ return this;
+ }
+
+ @Override
+ public ComponentRender render(RenderContext context) {
+ List renderItems = new ArrayList<>();
+
+ int height = 0;
+ int maxWidth = 0;
+
+ for (UIComponent component : components) {
+ ComponentRender render = component.render(context.withContainerSize(context.getContainerSize().withHeight(context.getContainerSize().getHeight() - height)));
+ for(RenderItem item : render.getItems()){
+ renderItems.add(item.shift(0, height));
+ }
+ maxWidth = Math.max(maxWidth, render.getSize().getWidth());
+ height += render.getSize().getHeight();
+ }
+
+ if (alignment == Alignment.CENTER) {
+ int shiftAmount = (context.getContainerSize().getHeight() - height) / 2;
+ for (RenderItem item : renderItems) {
+ item.shift(0, shiftAmount);
+ }
+ } else if (alignment == Alignment.BETWEEN) {
+ int shiftAmount = (context.getContainerSize().getHeight() - height) / (components.size() - 1);
+ for (int i = 0; i < components.size() - 1; i++) {
+ RenderItem item = renderItems.get(i);
+ item.shift(0, shiftAmount * i);
+ }
+ } else if (alignment == Alignment.END) {
+ int shiftAmount = (context.getContainerSize().getHeight() - height);
+ for (RenderItem item : renderItems) {
+ item.shift(0, shiftAmount);
+ }
+ }
+
+ return new ComponentRender(
+ renderItems,
+ new Size(maxWidth, shrinkWrap ? height : context.getContainerSize().getHeight())
+ );
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/FixedSizeContainer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/FixedSizeContainer.java
new file mode 100644
index 00000000..75520a27
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/FixedSizeContainer.java
@@ -0,0 +1,40 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.layout;
+
+import de.steamwar.inventory.ui.UIComponent;
+import de.steamwar.inventory.ui.util.ComponentRender;
+import de.steamwar.inventory.ui.util.RenderContext;
+import de.steamwar.inventory.ui.util.Size;
+
+public class FixedSizeContainer implements UIComponent {
+ private final UIComponent children;
+ private final Size size;
+
+ public FixedSizeContainer(Size size, UIComponent children) {
+ this.children = children;
+ this.size = size;
+ }
+
+ @Override
+ public ComponentRender render(RenderContext context) {
+ return children.render(context.withContainerSize(size)).withSize(size);
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/Grid.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/Grid.java
new file mode 100644
index 00000000..8feac6c4
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/Grid.java
@@ -0,0 +1,68 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.layout;
+
+import de.steamwar.inventory.ui.UIComponent;
+import de.steamwar.inventory.ui.util.ComponentRender;
+import de.steamwar.inventory.ui.util.RenderContext;
+import de.steamwar.inventory.ui.util.RenderItem;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class Grid implements UIComponent {
+ private final List children;
+
+ public Grid(UIComponent... children) {
+ this(Arrays.asList(children));
+ }
+
+ public Grid(List children) {
+ this.children = children;
+ }
+
+ @Override
+ public ComponentRender render(RenderContext context) {
+ List renderItems = new ArrayList<>();
+ int x = 0;
+ int y = 0;
+ int currentRowMaxHeight = 0;
+
+ for (UIComponent child : children) {
+ ComponentRender render = child.render(context.withContainerSize(
+ context.getContainerSize()
+ .withWidth(context.getContainerSize().getWidth() - x)
+ .withHeight(context.getContainerSize().getHeight() - y)));
+ currentRowMaxHeight = Math.max(currentRowMaxHeight, render.getSize().getHeight());
+ for (RenderItem item : render.getItems()) {
+ renderItems.add(item.shift(x, y));
+ }
+ x += render.getSize().getWidth();
+ if(x >= context.getContainerSize().getWidth()){
+ x = 0;
+ y += currentRowMaxHeight;
+ currentRowMaxHeight = 0;
+ }
+ }
+
+ return new ComponentRender(renderItems, context.getContainerSize());
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/Row.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/Row.java
new file mode 100644
index 00000000..8a856cd4
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/layout/Row.java
@@ -0,0 +1,98 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.layout;
+
+import de.steamwar.inventory.ui.UIComponent;
+import de.steamwar.inventory.ui.util.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class Row implements UIComponent {
+
+ private final List components;
+
+ private Alignment alignment;
+ private boolean shrinkWrap = false;
+
+ public Row(UIComponent... components) {
+ this(Arrays.asList(components));
+ }
+
+ public Row(List components) {
+ this.components = components;
+ }
+
+ public Row centered() {
+ this.alignment = Alignment.CENTER;
+ return this;
+ }
+
+ public Row alignment(Alignment alignment) {
+ this.alignment = alignment;
+ return this;
+ }
+
+ public Row setShrinkWrap(boolean shrinkWrap) {
+ this.shrinkWrap = shrinkWrap;
+ return this;
+ }
+
+ @Override
+ public ComponentRender render(RenderContext context) {
+ List renderItems = new ArrayList<>();
+
+ int width = 0;
+ int maxHeight = 0;
+
+ for (UIComponent component : components) {
+ ComponentRender render = component.render(context.withContainerSize(context.getContainerSize().withWidth(context.getContainerSize().getWidth() - width)));
+ for(RenderItem item : render.getItems()){
+ renderItems.add(item.shift(width, 0));
+ }
+ maxHeight = Math.max(maxHeight, render.getSize().getHeight());
+ width += render.getSize().getWidth();
+ }
+
+ if (alignment == Alignment.CENTER) {
+ int shiftAmount = (context.getContainerSize().getWidth() - width) / 2;
+ for (RenderItem item : renderItems) {
+ item.shift(shiftAmount, 0);
+ }
+ } else if (alignment == Alignment.BETWEEN) {
+ int shiftAmount = (context.getContainerSize().getWidth() - width) / (components.size() - 1);
+ for (int i = 0; i < components.size() - 1; i++) {
+ RenderItem item = renderItems.get(i);
+ item.shift(shiftAmount * i, 0);
+ }
+ } else if (alignment == Alignment.END) {
+ int shiftAmount = (context.getContainerSize().getWidth() - width);
+ for (RenderItem item : renderItems) {
+ item.shift(shiftAmount, 0);
+ }
+ }
+
+ return new ComponentRender(
+ renderItems,
+ new Size(shrinkWrap ? width : context.getContainerSize().getWidth(), maxHeight)
+ );
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/test/TestInvCommand.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/test/TestInvCommand.java
new file mode 100644
index 00000000..53e6132f
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/test/TestInvCommand.java
@@ -0,0 +1,41 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.test;
+
+import de.steamwar.command.SWCommand;
+import de.steamwar.inventory.ui.util.UpdateStream;
+import org.bukkit.entity.Player;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TestInvCommand extends SWCommand {
+
+ public static final UpdateStream> STREAM = new UpdateStream<>(new ArrayList<>());
+
+ public TestInvCommand() {
+ super("testinv");
+ }
+
+ @Register
+ public void run(Player player) {
+ new TestInventory(STREAM).addViewer(player);
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/test/TestInventory.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/test/TestInventory.java
new file mode 100644
index 00000000..1896cefa
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/test/TestInventory.java
@@ -0,0 +1,79 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.test;
+
+import de.steamwar.core.Core;
+import de.steamwar.inventory.ui.StatefulUIInventory;
+import de.steamwar.inventory.ui.UIComponent;
+import de.steamwar.inventory.ui.fragments.HorizontalScrollLayout;
+import de.steamwar.inventory.ui.item.ItemBuilder;
+import de.steamwar.inventory.ui.item.UIItem;
+import de.steamwar.inventory.ui.util.UpdateStream;
+import de.steamwar.message.Message;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.Inventory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static de.steamwar.inventory.ui.test.TestInvCommand.STREAM;
+
+public class TestInventory extends StatefulUIInventory> {
+
+ private HorizontalScrollLayout scrollLayout;
+ private static int counter = 1;
+
+ public TestInventory(UpdateStream> stateObservable) {
+ super(stateObservable);
+ scrollLayout = new HorizontalScrollLayout(new ArrayList<>());
+ }
+
+ @Override
+ protected UIComponent render(List state) {
+ return scrollLayout;
+ }
+
+ @Override
+ protected void update(List state) {
+ System.out.println("Update");
+ List items = state.stream().map(integer -> new UIItem(ItemBuilder.of(Material.ACACIA_PLANKS).displayName(String.valueOf(integer))).onClickNoUpdate(inventoryClickEvent -> STREAM.update(current -> {
+ current.remove(integer);
+ return current;
+ }))).collect(Collectors.toList());
+ items.add(new UIItem(ItemBuilder.of(Material.BUCKET).displayName("Add")).onClickNoUpdate(inventoryClickEvent -> STREAM.update(current -> {
+ current.add(counter++);
+ return current;
+ })));
+ scrollLayout = scrollLayout.data(items);
+ }
+
+ @Override
+ public Inventory createInventory(Player player) {
+ return Bukkit.createInventory(null, 9 * 6, "Test Inventory");
+ }
+
+ @Override
+ public Message getMessages() {
+ return Core.MESSAGE;
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/ComponentRender.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/ComponentRender.java
new file mode 100644
index 00000000..0236e364
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/ComponentRender.java
@@ -0,0 +1,32 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.util;
+
+import lombok.Data;
+import lombok.With;
+
+import java.util.List;
+
+@Data
+@With
+public class ComponentRender {
+ private final List items;
+ private final Size size;
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/RenderContext.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/RenderContext.java
new file mode 100644
index 00000000..910415f0
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/RenderContext.java
@@ -0,0 +1,32 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.util;
+
+import de.steamwar.inventory.ui.UIInventory;
+import lombok.Data;
+import lombok.With;
+
+@Data
+@With
+public class RenderContext {
+ private final Size inventorySize;
+ private final Size containerSize;
+ private final UIInventory inventory;
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/RenderItem.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/RenderItem.java
new file mode 100644
index 00000000..e78578d3
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/RenderItem.java
@@ -0,0 +1,42 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.util;
+
+import de.steamwar.inventory.ui.item.ItemBuilder;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.With;
+import org.bukkit.event.inventory.InventoryClickEvent;
+
+import java.util.function.Function;
+
+@Data
+@With
+@AllArgsConstructor
+public class RenderItem {
+ private int x;
+ private int y;
+ private ItemBuilder itemStack;
+ private Function clickEvent;
+
+ public RenderItem shift(int deltaX, int deltaY) {
+ return withX(x + deltaX).withY(y + deltaY);
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/Size.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/Size.java
new file mode 100644
index 00000000..6bcb342f
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/Size.java
@@ -0,0 +1,34 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.util;
+
+import lombok.Data;
+import lombok.With;
+
+@Data
+@With
+public class Size {
+ private final int width;
+ private final int height;
+
+ public Size shrink(int width, int height) {
+ return new Size(this.width - width, this.height - height);
+ }
+}
diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/UpdateStream.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/UpdateStream.java
new file mode 100644
index 00000000..2d5c4dba
--- /dev/null
+++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/inventory/ui/util/UpdateStream.java
@@ -0,0 +1,66 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2025 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.inventory.ui.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class UpdateStream {
+
+ private T current;
+
+ private final List> subscribers = new ArrayList<>();
+
+ public UpdateStream(T current) {
+ this.current = current;
+ }
+
+ public Unsubscriber subscribe(Subscriber subscriber) {
+ subscribers.add(subscriber);
+ subscriber.onUpdate(current);
+ return () -> subscribers.remove(subscriber);
+ }
+
+ public T get() {
+ return current;
+ }
+
+ public void emit(T newCurrent) {
+ current = newCurrent;
+ subscribers.forEach(subscriber -> subscriber.onUpdate(newCurrent));
+ }
+
+ public void update(Updater updater) {
+ current = updater.update(current);
+ subscribers.forEach(subscriber -> subscriber.onUpdate(current));
+ }
+
+ public interface Subscriber {
+ void onUpdate(T current);
+ }
+
+ public interface Unsubscriber {
+ void unsubscribe();
+ }
+
+ public interface Updater {
+ T update(T current);
+ }
+}