Skip to content

UI & Display

Learn how to build user interfaces for your inventory system using Stack Inventory.

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.

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;
}

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);
}

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);
}
}
}

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);
}

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);
}

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();
}
}

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();
}
}
  • 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 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
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);
}