Editor/Canvas/CanvasContextMenu.cs
using System;
using System.Collections.Generic;
using Editor;
using Grains.RazorDesigner.Document;
using Sandbox;

namespace Grains.RazorDesigner.Canvas;

public static class CanvasContextMenu
{
	private const string LogPrefix = "[Grains.RazorDesigner]";

	public readonly struct Callbacks
	{
		public Action<IReadOnlyList<ControlRecord>> Cut { get; init; }
		public Action<IReadOnlyList<ControlRecord>> Copy { get; init; }
		public Action<ControlRecord> Paste { get; init; }
		public Action<IReadOnlyList<ControlRecord>> Delete { get; init; }
		public Action<IReadOnlyList<ControlRecord>> SaveAsTemplate { get; init; }
	}

	// Returns false when the menu was suppressed (root + empty clipboard), true otherwise.
	public static bool Open(
		Widget parent,
		ControlRecord hit,
		IReadOnlyList<ControlRecord> targets,
		bool hasClipboard,
		bool isSlot,
		ControlRecord pasteRootTarget,
		Callbacks callbacks )
	{
		if ( parent is null ) return false;

		// Root / empty-canvas variant: Paste-only, suppressed when no clipboard.
		if ( hit is null )
		{
			if ( !hasClipboard ) return false;

			var rootMenu = new Menu( parent );
			rootMenu.AddOption( "Paste", "content_paste", () =>
			{
				Log.Info( $"{LogPrefix} CanvasContextMenu.Paste: into Canvas" );
				callbacks.Paste?.Invoke( pasteRootTarget );
			} );
			rootMenu.OpenAtCursor( false );
			return true;
		}

		// Slot variant: structural row — only Paste makes sense, gate on hasClipboard.
		if ( isSlot )
		{
			var slotMenu = new Menu( parent );
			var slotPaste = slotMenu.AddOption( "Paste", "content_paste", () =>
			{
				Log.Info( $"{LogPrefix} CanvasContextMenu.Paste: into slot {hit.ClassName}" );
				callbacks.Paste?.Invoke( hit );
			} );
			slotPaste.Enabled = hasClipboard;
			slotMenu.OpenAtCursor( false );
			return true;
		}

		if ( targets is null || targets.Count == 0 )
			targets = new[] { hit };

		var suffix = targets.Count > 1 ? $" ({targets.Count} items)" : "";

		var m = new Menu( parent );

		m.AddOption( $"Save as Template{suffix}…", "bookmark_add", () =>
		{
			Log.Info( $"{LogPrefix} CanvasContextMenu.SaveAsTemplate: {targets.Count} record(s)" );
			callbacks.SaveAsTemplate?.Invoke( targets );
		} );
		m.AddSeparator();

		m.AddOption( $"Cut{suffix}", "content_cut", () =>
		{
			Log.Info( $"{LogPrefix} CanvasContextMenu.Cut: {targets.Count} record(s)" );
			callbacks.Cut?.Invoke( targets );
		} );

		m.AddOption( $"Copy{suffix}", "content_copy", () =>
		{
			Log.Info( $"{LogPrefix} CanvasContextMenu.Copy: {targets.Count} record(s)" );
			callbacks.Copy?.Invoke( targets );
		} );

		var pasteOpt = m.AddOption( "Paste", "content_paste", () =>
		{
			Log.Info( $"{LogPrefix} CanvasContextMenu.Paste: onto {hit.ClassName}" );
			callbacks.Paste?.Invoke( hit );
		} );
		// Disabled-not-hidden: users learn Paste exists even before they copy.
		pasteOpt.Enabled = hasClipboard;

		m.AddSeparator();

		m.AddOption( $"Delete{suffix}", "delete", () =>
		{
			Log.Info( $"{LogPrefix} CanvasContextMenu.Delete: {targets.Count} record(s)" );
			callbacks.Delete?.Invoke( targets );
		} );

		m.OpenAtCursor( false );
		return true;
	}
}