UI & Display
Learn how to build user interfaces for your inventory system using Stack Inventory.
Overview
Section titled “Overview”Stack Inventory provides the data layer (items, containers) but leaves UI implementation to you. This guide shows common patterns for creating inventory interfaces in Godot.
Basic Slot UI
Section titled “Basic Slot UI”A single inventory slot display:
using Godot;using StackInventory.Core.Items;
public partial class InventorySlotUI : Control{ [Export] public TextureRect IconDisplay { get; set; } [Export] public Label AmountLabel { get; set; } [Export] public Panel Background { get; set; }
private IItem _item; private int _amount;
public void SetItem(IItem item, int amount) { _item = item; _amount = amount;
if (item == null || amount <= 0) { Clear(); return; }
// Load and display icon if (!string.IsNullOrEmpty(item.IconPath)) { IconDisplay.Texture = GD.Load<Texture2D>(item.IconPath); }
// Show amount if > 1 AmountLabel.Text = amount > 1 ? amount.ToString() : ""; AmountLabel.Visible = amount > 1;
// Update tooltip TooltipText = $"{item.Name}\n{item.Tooltip}"; }
public void Clear() { _item = null; _amount = 0; IconDisplay.Texture = null; AmountLabel.Text = ""; AmountLabel.Visible = false; TooltipText = ""; }
public IItem GetItem() => _item; public int GetAmount() => _amount;}Grid Inventory UI
Section titled “Grid Inventory UI”Display items in a grid:
public partial class InventoryGridUI : Control{ [Export] public PackedScene SlotScene { get; set; } [Export] public GridContainer Grid { get; set; } [Export] public int Columns { get; set; } = 5; [Export] public int Rows { get; set; } = 4;
private List<InventorySlotUI> _slotUIs = new(); private ListInventory _inventory;
public override void _Ready() { Grid.Columns = Columns; CreateSlots(); }
public void SetInventory(ListInventory inventory) { if (_inventory != null) { // Disconnect old signals _inventory.ItemAdded -= OnInventoryChanged; _inventory.ItemRemoved -= OnInventoryChanged; }
_inventory = inventory;
if (_inventory != null) { // Connect new signals _inventory.ItemAdded += OnInventoryChanged; _inventory.ItemRemoved += OnInventoryChanged; Refresh(); } }
private void CreateSlots() { int totalSlots = Columns * Rows;
for (int i = 0; i < totalSlots; i++) { var slotUI = SlotScene.Instantiate<InventorySlotUI>(); Grid.AddChild(slotUI); _slotUIs.Add(slotUI);
int index = i; // Capture for lambda slotUI.GuiInput += (e) => OnSlotClicked(index, e); } }
private void Refresh() { if (_inventory == null) return;
// Clear all slots foreach (var slot in _slotUIs) { slot.Clear(); }
// Fill with items var items = _inventory.GetAllItems().ToList(); for (int i = 0; i < items.Count && i < _slotUIs.Count; i++) { _slotUIs[i].SetItem(items[i].item, items[i].amount); } }
private void OnInventoryChanged(IItem item, int amount) { Refresh(); }
private void OnSlotClicked(int slotIndex, InputEvent e) { if (e is InputEventMouseButton mb && mb.Pressed) { if (mb.ButtonIndex == MouseButton.Left) { // Handle left click EmitSignal(SignalName.SlotLeftClicked, slotIndex); } else if (mb.ButtonIndex == MouseButton.Right) { // Handle right click EmitSignal(SignalName.SlotRightClicked, slotIndex); } } }
[Signal] public delegate void SlotLeftClickedEventHandler(int slotIndex);
[Signal] public delegate void SlotRightClickedEventHandler(int slotIndex);}Item Tooltip
Section titled “Item Tooltip”Rich tooltip display:
public partial class ItemTooltip : PanelContainer{ [Export] public Label NameLabel { get; set; } [Export] public Label DescriptionLabel { get; set; } [Export] public Label ValueLabel { get; set; } [Export] public VBoxContainer TagsContainer { get; set; }
public void ShowItem(IItem item) { if (item == null) { Hide(); return; }
NameLabel.Text = item.Name; DescriptionLabel.Text = item.Tooltip; ValueLabel.Text = $"Value: {item.Value}g";
// Clear old tags foreach (var child in TagsContainer.GetChildren()) { child.QueueFree(); }
// Add tag labels foreach (var tag in item.Tags) { var tagLabel = new Label { Text = $"[{tag.Id}]", AddThemeColorOverride("font_color", new Color(0.8f, 0.8f, 0.6f)) }; TagsContainer.AddChild(tagLabel); }
Show(); }
public override void _Process(double delta) { // Follow mouse cursor if (Visible) { GlobalPosition = GetViewport().GetMousePosition() + new Vector2(10, 10); } }}Drag and Drop
Section titled “Drag and Drop”Implement drag-and-drop for item management:
public partial class DraggableSlotUI : InventorySlotUI{ private bool _isDragging = false; private Control _dragPreview;
public override void _GuiInput(InputEvent e) { if (e is InputEventMouseButton mb) { if (mb.ButtonIndex == MouseButton.Left) { if (mb.Pressed && GetItem() != null) { StartDrag(); } else if (!mb.Pressed && _isDragging) { EndDrag(); } } } }
private void StartDrag() { _isDragging = true;
// Create drag preview _dragPreview = new TextureRect { Texture = IconDisplay.Texture, Size = IconDisplay.Size, Modulate = new Color(1, 1, 1, 0.7f) };
GetTree().Root.AddChild(_dragPreview); EmitSignal(SignalName.DragStarted, this); }
private void EndDrag() { _isDragging = false;
if (_dragPreview != null) { _dragPreview.QueueFree(); _dragPreview = null; }
EmitSignal(SignalName.DragEnded, this); }
public override void _Process(double delta) { if (_isDragging && _dragPreview != null) { _dragPreview.GlobalPosition = GetViewport().GetMousePosition(); } }
public bool CanAcceptDrop(DraggableSlotUI source) { // Can drop if empty or same item type return GetItem() == null || GetItem().Id == source.GetItem()?.Id; }
[Signal] public delegate void DragStartedEventHandler(DraggableSlotUI slot);
[Signal] public delegate void DragEndedEventHandler(DraggableSlotUI slot);}Hotbar UI
Section titled “Hotbar UI”Quick-access bar display:
public partial class HotbarUI : HBoxContainer{ [Export] public PackedScene SlotScene { get; set; } [Export] public int HotbarSize { get; set; } = 10;
private List<InventorySlotUI> _slots = new(); private int _selectedIndex = 0;
public override void _Ready() { for (int i = 0; i < HotbarSize; i++) { var slot = SlotScene.Instantiate<InventorySlotUI>(); AddChild(slot); _slots.Add(slot);
int index = i; slot.GuiInput += (e) => OnSlotInput(index, e); }
SelectSlot(0); }
public override void _Input(InputEvent e) { // Number key selection if (e is InputEventKey key && key.Pressed) { if (key.Keycode >= Key.Key1 && key.Keycode <= Key.Key9) { int slot = (int)(key.Keycode - Key.Key1); if (slot < HotbarSize) { SelectSlot(slot); } } else if (key.Keycode == Key.Key0) { SelectSlot(9); // Key 0 = slot 10 } }
// Mouse wheel scrolling if (e is InputEventMouseButton mb) { if (mb.ButtonIndex == MouseButton.WheelUp && mb.Pressed) { SelectSlot((_selectedIndex - 1 + HotbarSize) % HotbarSize); } else if (mb.ButtonIndex == MouseButton.WheelDown && mb.Pressed) { SelectSlot((_selectedIndex + 1) % HotbarSize); } } }
public void SelectSlot(int index) { if (index < 0 || index >= _slots.Count) return;
// Deselect old if (_selectedIndex >= 0 && _selectedIndex < _slots.Count) { _slots[_selectedIndex].Modulate = new Color(1, 1, 1); }
// Select new _selectedIndex = index; _slots[_selectedIndex].Modulate = new Color(1.3f, 1.3f, 1.3f);
EmitSignal(SignalName.SlotSelected, index); }
public void SetSlotItem(int index, IItem item, int amount) { if (index >= 0 && index < _slots.Count) { _slots[index].SetItem(item, amount); } }
private void OnSlotInput(int index, InputEvent e) { if (e is InputEventMouseButton mb && mb.Pressed) { if (mb.ButtonIndex == MouseButton.Left) { SelectSlot(index); } } }
[Signal] public delegate void SlotSelectedEventHandler(int index);}Equipment Panel
Section titled “Equipment Panel”Character equipment display:
public partial class EquipmentPanelUI : Control{ [Export] public InventorySlotUI HeadSlot { get; set; } [Export] public InventorySlotUI ChestSlot { get; set; } [Export] public InventorySlotUI LegsSlot { get; set; } [Export] public InventorySlotUI FeetSlot { get; set; } [Export] public InventorySlotUI MainHandSlot { get; set; } [Export] public InventorySlotUI OffHandSlot { get; set; }
private Equipment _equipment;
public void SetEquipment(Equipment equipment) { _equipment = equipment; equipment.ItemEquipped += OnItemEquipped; equipment.ItemUnequipped += OnItemUnequipped; Refresh(); }
private void Refresh() { if (_equipment == null) return;
HeadSlot.SetItem(_equipment.GetEquipped(Equipment.EquipSlot.Head), 1); ChestSlot.SetItem(_equipment.GetEquipped(Equipment.EquipSlot.Chest), 1); LegsSlot.SetItem(_equipment.GetEquipped(Equipment.EquipSlot.Legs), 1); FeetSlot.SetItem(_equipment.GetEquipped(Equipment.EquipSlot.Feet), 1); MainHandSlot.SetItem(_equipment.GetEquipped(Equipment.EquipSlot.MainHand), 1); OffHandSlot.SetItem(_equipment.GetEquipped(Equipment.EquipSlot.OffHand), 1); }
private void OnItemEquipped(int slot, IItem item) { Refresh(); }
private void OnItemUnequipped(int slot, IItem item) { Refresh(); }}Filtering and Sorting
Section titled “Filtering and Sorting”Add filter and sort controls:
public partial class FilteredInventoryUI : InventoryGridUI{ [Export] public OptionButton FilterDropdown { get; set; } [Export] public Button SortButton { get; set; }
private string _currentFilter = ""; private enum SortMode { Name, Value, Type } private SortMode _sortMode = SortMode.Name;
public override void _Ready() { base._Ready();
FilterDropdown.AddItem("All", 0); FilterDropdown.AddItem("Weapons", 1); FilterDropdown.AddItem("Armor", 2); FilterDropdown.AddItem("Consumables", 3); FilterDropdown.ItemSelected += OnFilterChanged;
SortButton.Pressed += OnSortPressed; }
private void OnFilterChanged(long index) { _currentFilter = index switch { 1 => "Weapon", 2 => "Armor", 3 => "Consumable", _ => "" }; Refresh(); }
private void OnSortPressed() { _sortMode = (_sortMode + 1) % (SortMode)3; SortButton.Text = $"Sort: {_sortMode}"; Refresh(); }
protected override List<(IItem item, int amount)> GetFilteredItems() { var items = _inventory.GetAllItems();
// Apply filter if (!string.IsNullOrEmpty(_currentFilter)) { items = items.Where(i => i.item.HasTag(_currentFilter)); }
// Apply sort items = _sortMode switch { SortMode.Name => items.OrderBy(i => i.item.Name), SortMode.Value => items.OrderByDescending(i => i.item.Value), SortMode.Type => items.OrderBy(i => i.item.Tags.FirstOrDefault()?.Id ?? ""), _ => items };
return items.ToList(); }}UI Best Practices
Section titled “UI Best Practices”- Emit signals for player actions
- Show feedback (hover effects, selection highlights)
- Handle edge cases (empty slots, full inventory)
- Use themes for consistent styling
- Cache references to avoid GetNode calls
DON’T ❌
Section titled “DON’T ❌”- Don’t update UI every frame unless necessary
- Don’t store game logic in UI code
- Don’t directly modify inventory from UI (use signals)
- Don’t forget to disconnect signals on cleanup
Example: Complete Inventory Window
Section titled “Example: Complete Inventory Window”public partial class InventoryWindow : Window{ [Export] public InventoryGridUI GridUI { get; set; } [Export] public EquipmentPanelUI EquipmentUI { get; set; } [Export] public HotbarUI HotbarUI { get; set; } [Export] public ItemTooltip Tooltip { get; set; } [Export] public Label GoldLabel { get; set; }
private ListInventory _inventory; private Equipment _equipment;
public void Initialize(ListInventory inventory, Equipment equipment) { _inventory = inventory; _equipment = equipment;
GridUI.SetInventory(inventory); EquipmentUI.SetEquipment(equipment);
GridUI.SlotLeftClicked += OnSlotLeftClicked; GridUI.SlotRightClicked += OnSlotRightClicked; }
public override void _Input(InputEvent e) { if (e.IsActionPressed("ui_cancel")) { Hide(); } }
private void OnSlotLeftClicked(int slotIndex) { // Use item or equip var slot = GridUI.GetSlot(slotIndex); var item = slot.GetItem();
if (item != null) { if (item.HasTag("Equippable")) { _equipment.Equip(GetEquipSlotForItem(item), item); _inventory.RemoveItem(item, 1); } else if (item.HasTag("Consumable")) { UseItem(item); _inventory.RemoveItem(item, 1); } } }
private void OnSlotRightClicked(int slotIndex) { // Show context menu or drop item var slot = GridUI.GetSlot(slotIndex); var item = slot.GetItem();
if (item != null) { EmitSignal(SignalName.ItemDropRequested, item); } }
[Signal] public delegate void ItemDropRequestedEventHandler(IItem item);}Next Steps
Section titled “Next Steps”- Containers Guide - Build inventory logic
- Tag System - Filter UI by tags
- Architecture - Understand the design
- API Reference - Explore IItem interface