Internal/StatefulDrawPanel.cs

A UI Panel subclass used by the Goo UI system. It provides stateful styling, immediate-mode and shader-based custom drawing, layout glide transitions, and routes mouse/drag events to user-supplied callbacks with an auto-rebuild hook.

Native Interop
using System;
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;
using Goo.Animation;

namespace Goo.Internal;

internal sealed class StatefulDrawPanel : Panel, IPanelDraw, IStatefulHost, IStatefulEventHost
{
    StateController? _state;

    internal Action<MousePanelEvent>? _onClick;
    internal Action<MousePanelEvent>? _onRightClick;
    internal Action<MousePanelEvent>? _onMiddleClick;
    internal Action<MousePanelEvent>? _onMouseEnter;
    internal Action<MousePanelEvent>? _onMouseLeave;
    internal Action<MousePanelEvent>? _onMouseDown;
    internal Action<MousePanelEvent>? _onMouseUp;
    internal Action<MousePanelEvent>? _onMouseMove;
    internal Action<Vector2>?         _onMouseWheel;
    internal bool _userSetPointerEvents;
    internal Action? _requestRebuild;
    public Action? RequestRebuild { set => _requestRebuild = value; }

    internal Action<DragEvent>?  _onDragStart;
    internal Action<DragEvent>?  _onDrag;
    internal Action<DragEvent>?  _onDragEnd;
    internal Action<PanelEvent>? _onDragEnter;
    internal Action<PanelEvent>? _onDragLeave;
    internal Action<PanelEvent>? _onDrop;
    bool _wantsDrag;

    // User immediate-mode draw callback (null = no custom draw).
    internal DrawCallback? _draw;

    // Custom-shader draw (separate channel from the Canvas _draw callback).
    internal ShaderEffect? _effect;

    // Declared layout-move transition; null = snap (default). Clearing also drops any in-flight glide.
    LayoutTransition? _layoutTransition;
    GlideState _glide;
    Vector2 _lastLayoutPos;
    bool _hasLayoutPos;

    internal void SetLayoutTransition(LayoutTransition? transition)
    {
        _layoutTransition = transition;
        if (transition is null) _glide = default;
    }

    public void ApplyStateVariants(
        Color? baseBg,  Color? baseFg,
        Color? hoverBg, Color? activeBg, Color? focusBg,
        Color? hoverFg, Color? activeFg, Color? focusFg,
        int? transitionMs)
    {
        _state ??= new StateController(this);
        _state.ApplyVariants(
            baseBg, baseFg,
            hoverBg, activeBg, focusBg,
            hoverFg, activeFg, focusFg,
            transitionMs);
    }

    public void ClearStateVariants() => _state?.ClearVariants();

    public bool HasActiveStateVariants => _state?.HasActiveVariants ?? false;

    public void ApplyEvents(in BlobEvents events)
    {
        _onClick      = events.OnClick;
        _onRightClick = events.OnRightClick;
        _onMiddleClick = events.OnMiddleClick;
        _onMouseEnter = events.OnMouseEnter;
        _onMouseLeave = events.OnMouseLeave;
        _onMouseDown  = events.OnMouseDown;
        _onMouseUp    = events.OnMouseUp;
        _onMouseMove  = events.OnMouseMove;
        _onMouseWheel = events.OnMouseWheel;
        _onDragStart = events.OnDragStart;
        _onDrag      = events.OnDrag;
        _onDragEnd   = events.OnDragEnd;
        _onDragEnter = events.OnDragEnter;
        _onDragLeave = events.OnDragLeave;
        _onDrop      = events.OnDrop;
        _wantsDrag   = events.WantsDrag;
    }

    public bool HasEventHandlers =>
        _onClick != null || _onRightClick != null || _onMiddleClick != null || _onMouseEnter != null || _onMouseLeave != null ||
        _onMouseDown != null || _onMouseUp != null || _onMouseMove != null || _onMouseWheel != null ||
        _onDragStart != null || _onDrag != null || _onDragEnd != null ||
        _onDragEnter != null || _onDragLeave != null || _onDrop != null;

    public bool UserSetPointerEvents
    {
        get => _userSetPointerEvents;
        set => _userSetPointerEvents = value;
    }

    // Immediate-mode custom draw, run inside the engine's batched OnDraw pass via the Goo Canvas facade.
    public override void OnDraw()
    {
        if (_draw is null) return;
        _draw(new Canvas(Box.Rect));
    }

    // IPanelDraw custom-shader paint, interleaved into the engine batched UI render (post-#4692).
    void IPanelDraw.Draw(CommandList cl)
    {
        if (_effect is null) return;
        // BoxSize (set in Apply from Box.Rect) is PHYSICAL px; this is the physical-per-CSS DPI scale
        // so a shader can convert any CSS-px length L to physical via L*DpiScale and stay crisp at any DPI.
        cl.Attributes.Set("DpiScale", ScaleToScreen);
        _effect.Apply(cl, Box.Rect);
        cl.DrawQuad(Box.Rect, _effect.Material, Color.White);
    }

    // Runs inside FinalLayout after yoga assigns the rect; banking here sees real positions the same frame.
    public override void OnLayout(ref Rect rect)
    {
        base.OnLayout(ref rect);
        if (_layoutTransition is { } t && _hasLayoutPos)
        {
            var jump = _lastLayoutPos - rect.Position;
            if (jump.LengthSquared > 0.01f)
            {
                if (!_glide.IsActive) _glide = new GlideState(t.Ms, t.Easing);
                _glide.Bank(jump);
            }
        }
        _lastLayoutPos = rect.Position;
        _hasLayoutPos = true;
        rect.Position += _glide.Offset;
    }

    // Re-dirty each frame so either draw channel updates continuously; a live glide also re-runs layout.
    public override void Tick()
    {
        base.Tick();
        if (_glide.IsActive)
        {
            _glide.Advance(Time.Delta);
            Style.Dirty();
            MarkRenderDirty();
        }
        if (_draw is not null || _effect is not null)
            MarkRenderDirty();
    }

    protected override void OnClick(MousePanelEvent e)
    {
        base.OnClick(e);
        EventDispatch.Fire(_onClick, e, _requestRebuild);
    }

    protected override void OnRightClick(MousePanelEvent e)
    {
        base.OnRightClick(e);
        EventDispatch.Fire(_onRightClick, e, _requestRebuild);
    }

    protected override void OnMiddleClick(MousePanelEvent e)
    {
        base.OnMiddleClick(e);
        EventDispatch.Fire(_onMiddleClick, e, _requestRebuild);
    }

    protected override void OnMouseOver(MousePanelEvent e)
    {
        base.OnMouseOver(e);
        EventDispatch.Fire(_onMouseEnter, e, _requestRebuild);
    }

    protected override void OnMouseOut(MousePanelEvent e)
    {
        base.OnMouseOut(e);
        EventDispatch.Fire(_onMouseLeave, e, _requestRebuild);
    }

    protected override void OnMouseDown(MousePanelEvent e)
    {
        base.OnMouseDown(e);
        EventDispatch.Fire(_onMouseDown, e, _requestRebuild);
    }

    protected override void OnMouseUp(MousePanelEvent e)
    {
        base.OnMouseUp(e);
        EventDispatch.Fire(_onMouseUp, e, _requestRebuild);
    }

    protected override void OnMouseMove(MousePanelEvent e)
    {
        base.OnMouseMove(e);
        EventDispatch.Fire(_onMouseMove, e, _requestRebuild);
    }

    // The engine calls this on the deepest hovered panel and, by default, bubbles to the parent
    // when this panel cannot scroll (Panel.OnMouseWheel -> TryScroll -> Parent). A user handler
    // consumes the wheel (no base call) so it does not leak into an ancestor scroll container;
    // with no handler we defer to base to preserve default scrolling/bubbling.
    public override void OnMouseWheel(Vector2 value)
    {
        if (_onMouseWheel is not null)
        {
            EventDispatch.Fire(_onMouseWheel, value, _requestRebuild);
            return;
        }
        base.OnMouseWheel(value);
    }

    public override bool WantsDrag => _wantsDrag;

    // Route every drag event through EventDispatch.Fire so the AutoRebuildOnEvents contract holds:
    // a handler that mutates state to reposition/resize gets an automatic rebuild, same as OnClick et al.
    protected override void OnDragStart( DragEvent e ) => EventDispatch.Fire( _onDragStart, e, _requestRebuild );
    protected override void OnDrag( DragEvent e ) => EventDispatch.Fire( _onDrag, e, _requestRebuild );
    protected override void OnDragEnd( DragEvent e ) => EventDispatch.Fire( _onDragEnd, e, _requestRebuild );

    protected override void OnDragEnter( PanelEvent e )
    {
        base.OnDragEnter( e );
        EventDispatch.Fire( _onDragEnter, e, _requestRebuild );
    }

    protected override void OnDragLeave( PanelEvent e )
    {
        base.OnDragLeave( e );
        EventDispatch.Fire( _onDragLeave, e, _requestRebuild );
    }

    protected override void OnDrop( PanelEvent e )
    {
        base.OnDrop( e );
        EventDispatch.Fire( _onDrop, e, _requestRebuild );
    }
}