SlotDispatcher.cs

A UI helper that wraps an IHitShape and tracks which slot the mouse is over, firing enter/leave/click callbacks. It maintains optional geometric state (angle and normalized distance) when the shape implements IGeometricHitShape, and provides convenience handlers for MousePanelEvent with or without explicit receiver size.

NetworkingFile AccessNative InteropProcess ExecutionReflectionObfuscated CodeEncoded DataExternal DownloadHttp CallsCredential AccessSelf Modifying Code
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;
    readonly IGeometricHitShape? _geometric;
    int? _currentSlot;
    float? _lastAngleDeg;
    float? _lastDistanceNormalized;

    public SlotDispatcher(IHitShape shape)
    {
        _shape = shape ?? throw new ArgumentNullException(nameof(shape));
        _geometric = shape as IGeometricHitShape;
    }

    public IHitShape Shape => _shape;
    public int? CurrentSlot => _currentSlot;

    /// <summary>Cursor angle from the most recent hover update, in degrees,
    /// 0 = up, clockwise positive, [0, 360). Null when the cursor is outside
    /// the shape or the underlying <see cref="IHitShape"/> does not provide
    /// geometric state (only radial shapes do today).</summary>
    public float? LastAngleDeg => _lastAngleDeg;

    /// <summary>Cursor distance from center as a 0..1 fraction of the shape's
    /// outer radius, from the most recent hover update. Null when the cursor
    /// is outside the shape or the underlying <see cref="IHitShape"/> does not
    /// provide geometric state.</summary>
    public float? LastDistanceNormalized => _lastDistanceNormalized;

    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)
    {
        _lastAngleDeg = null;
        _lastDistanceNormalized = null;
        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)
    {
        int? newSlot;
        if (_geometric is not null)
        {
            newSlot = _geometric.ResolveWithGeometry(localPosition, panelSize, out _lastAngleDeg, out _lastDistanceNormalized);
        }
        else
        {
            newSlot = _shape.Resolve(localPosition, panelSize);
            _lastAngleDeg = null;
            _lastDistanceNormalized = null;
        }

        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;
        _lastAngleDeg = null;
        _lastDistanceNormalized = null;
    }

    void UpdateHoverFrom(MousePanelEvent e)
    {
        var size = e.Target?.Box.Rect.Size ?? Vector2.Zero;
        UpdateHoverAt(e.LocalPosition, size);
    }
}