Code/SlotDispatcher.cs
using System;
using Sandbox;
using Sandbox.UI;
namespace HitShapes;
/// <summary>Holds an <see cref="IHitShape"/> and tracks per-slot enter/leave/click transitions.</summary>
public sealed class SlotDispatcher
{
readonly IHitShape _shape;
int? _currentSlot;
public SlotDispatcher(IHitShape shape)
{
_shape = shape ?? throw new ArgumentNullException(nameof(shape));
}
public IHitShape Shape => _shape;
public int? CurrentSlot => _currentSlot;
public Action<int> OnSlotEnter { get; init; } = _ => { };
public Action<int> OnSlotLeave { get; init; } = _ => { };
public Action<int, MousePanelEvent> OnSlotClick { get; init; } = (_, _) => { };
public void HandleMouseEnter(MousePanelEvent e) => UpdateHoverFrom(e);
public void HandleMouseMove(MousePanelEvent e) => UpdateHoverFrom(e);
/// <summary>Hover update using explicit receiver size. Safe under event bubbling.</summary>
public void HandleMouseEnter(MousePanelEvent e, Panel receiver) => UpdateHoverAt(e.LocalPosition, receiver.Box.Rect.Size);
public void HandleMouseMove(MousePanelEvent e, Panel receiver) => UpdateHoverAt(e.LocalPosition, receiver.Box.Rect.Size);
public void HandleMouseLeave(MousePanelEvent e)
{
if (_currentSlot is int oldSlot)
{
_currentSlot = null;
OnSlotLeave?.Invoke(oldSlot);
}
}
public void HandleClick(MousePanelEvent e)
{
var size = e.Target?.Box.Rect.Size ?? Vector2.Zero;
var slot = _shape.Resolve(e.LocalPosition, size);
if (slot is int s) OnSlotClick?.Invoke(s, e);
}
/// <summary>Click using explicit receiver size. Safe under event bubbling.</summary>
public void HandleClick(MousePanelEvent e, Panel receiver)
{
var slot = _shape.Resolve(e.LocalPosition, receiver.Box.Rect.Size);
if (slot is int s) OnSlotClick?.Invoke(s, e);
}
/// <summary>Drive the hover state machine without a MousePanelEvent.</summary>
public void UpdateHoverAt(Vector2 localPosition, Vector2 panelSize)
{
var newSlot = _shape.Resolve(localPosition, panelSize);
if (newSlot == _currentSlot) return;
var oldSlot = _currentSlot;
_currentSlot = newSlot;
if (oldSlot is int leaving) OnSlotLeave?.Invoke(leaving);
if (newSlot is int entering) OnSlotEnter?.Invoke(entering);
}
/// <summary>Clear hover state. Call after panel recreation if the dispatcher survives it.</summary>
public void Reset() => _currentSlot = null;
void UpdateHoverFrom(MousePanelEvent e)
{
var size = e.Target?.Box.Rect.Size ?? Vector2.Zero;
UpdateHoverAt(e.LocalPosition, size);
}
}