using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Theriapolis.Game.CodexUI.Core; namespace Theriapolis.Game.CodexUI.Drag; /// /// Screen-level coordinator for click-drag interactions. Source widgets call /// on left-mouse-down within their bounds and supply /// (a) an arbitrary payload object — game-specific state, e.g. StatPoolPayload — /// and (b) a ghost callback that paints a small follow-the-cursor visual. /// /// Drop targets register via ; on left-mouse-up /// the controller hit-tests in registration order and fires /// with the matching target's id. Pressing mid-drag /// cancels and fires . /// /// One-controller-per-screen; concrete screens own the instance and pass it /// down to widgets that participate in drag-drop. /// public sealed class DragDropController { public bool IsDragging => _payload is not null; public object? Payload => _payload; public Point CursorPosition { get; private set; } private object? _payload; private System.Action? _ghost; private readonly System.Collections.Generic.List _targets = new(); public event System.Action? OnDrop; // (payload, targetId) public event System.Action? OnCancel; public event System.Action? OnDropAnywhere; // fired when drop lands outside any registered target public void BeginDrag(object payload, System.Action ghost) { _payload = payload; _ghost = ghost; } public void RegisterTarget(string id, Rectangle bounds) => _targets.Add(new DropTarget(id, bounds)); public void ClearTargets() => _targets.Clear(); public void Update(GameTime gt, CodexInput input) { CursorPosition = input.MousePosition; if (!IsDragging) { _targets.Clear(); return; } if (input.KeyJustPressed(Keys.Escape)) { OnCancel?.Invoke(_payload!); _payload = null; _ghost = null; _targets.Clear(); return; } if (input.LeftJustReleased) { string? hit = null; foreach (var t in _targets) if (t.Bounds.Contains(input.MousePosition)) { hit = t.Id; break; } if (hit is not null) OnDrop?.Invoke(_payload!, hit); else OnDropAnywhere?.Invoke(_payload!, input.MousePosition); _payload = null; _ghost = null; _targets.Clear(); } } public void Draw(SpriteBatch sb) { if (_payload is null || _ghost is null) return; _ghost(sb, CursorPosition); } private readonly struct DropTarget { public readonly string Id; public readonly Rectangle Bounds; public DropTarget(string id, Rectangle bounds) { Id = id; Bounds = bounds; } } } /// /// Payload type for the stat-assignment drag-drop dance. Mirrors the React /// design's {from, value, idx, ability} object so behavior ports verbatim. /// public sealed class StatPoolPayload { public required string Source { get; init; } // "pool" or "slot" public required int Value { get; init; } public int? PoolIdx { get; init; } // index in pool list when Source == "pool" public Theriapolis.Core.Rules.Stats.AbilityId? Ability { get; init; } // when Source == "slot" }