Dnd/DropZone.cs
using System;
using Sandbox;
using Sandbox.UI;
namespace Goo;
// Where within a drop zone an interaction landed, in the zone's own rendered frame.
// Positional consumers map it to their model, e.g. cellX = (int)(Local.x / (ZoneSize.x / Cols)).
public readonly record struct DropLocation(Vector2 Local, Vector2 ZoneSize);
// A drop target for a typed payload; stops propagation so the innermost zone wins. See engine-fact-sbox-ui-drop-dispatch.
public sealed class DropZone<T> : Cell<Container>
{
public DragContext<T> Context = null!; // set via configure
public Func<bool, Container> Content = null!; // receives isHovered; renders the zone
public Action<T, DropLocation>? OnDropPayload; // consumer commit
public Action<DropLocation?>? OnHover; // location on enter (each cell crossing), null on leave
bool _hovered;
protected override Container Build() => new Container
{
PointerEvents = PointerEvents.All,
OnDragEnter = e =>
{
_hovered = true;
OnHover?.Invoke( Loc( e ) );
Rebuild();
e.StopPropagation();
},
OnDragLeave = e =>
{
// Co-located child targets bubble a 'leave' here; treat it as real only when the cursor is outside our rect. See engine-fact-sbox-ui-drop-dispatch.
if ( e.This is { } stay && Within( stay ) )
{
e.StopPropagation();
return;
}
_hovered = false;
OnHover?.Invoke( null );
Rebuild();
e.StopPropagation();
},
OnDrop = e =>
{
_hovered = false;
if ( Context.HasPayload )
OnDropPayload?.Invoke( Context.Payload, Loc( e ) );
OnHover?.Invoke( null );
Context.Complete();
Rebuild();
e.StopPropagation();
},
Children = { Content( _hovered ) },
};
// e.This is the panel currently handling the event during bubble propagation = this zone.
static DropLocation Loc( PanelEvent e )
{
var zone = e.This;
if ( zone is null ) return default;
return new DropLocation( zone.MousePosition, zone.Box.Rect.Size );
}
// True when the cursor still sits inside the zone's own rendered rect (a child-to-child move),
// as opposed to having actually exited the zone. MousePosition is relative to the zone's
// top-left in the same rendered frame as Box.Rect (engine-fact-sbox-ui-drop-dispatch).
static bool Within( Panel zone )
{
var mp = zone.MousePosition;
var size = zone.Box.Rect.Size;
return mp.x >= 0f && mp.y >= 0f && mp.x <= size.x && mp.y <= size.y;
}
}