Editor/Tilemap/TilemapLayerListWidget.cs

Editor UI widget that displays and manages a TileMap layer list. It draws header and rows, shows drag handles, visibility and collision toggles, handles mouse input for selecting, dragging/reordering layers, and toggling visibility/collisions.

Native Interop
using Editor;
using Sandbox;
using System;
using Saandy.Tilemapper;

namespace Saandy.Editor.Tilemapper;

public sealed class TilemapLayerListWidget : Widget
{
	private const float HeaderHeight = 30.0f;
	private const float RowHeight = 36.0f;
	private const float Padding = 8.0f;
	private const float ButtonSize = 22.0f;
	private const float ButtonGap = 6.0f;
	private const float DragHandleWidth = 24.0f;
	private const float DragStartDistance = 5.0f;

	private enum PressAction
	{
		None,
		SelectOrDrag,
		Visibility,
		Collision
	}

	private TileMap _tilemap;
	private int _hoveredIndex = -1;
	private int _pressedIndex = -1;
	private int _dragIndex = -1;
	private Vector2 _pressPosition;
	private bool _isDragging;
	private PressAction _pressAction;

	public TilemapLayerListWidget( Widget parent ) : base( parent )
	{
		MouseTracking = true;
		HorizontalSizeMode = SizeMode.Flexible;
		VerticalSizeMode = SizeMode.CanGrow;
		MinimumHeight = 190;
	}

	public void SetTilemap( TileMap tilemap )
	{
		_tilemap = tilemap;
		RefreshSize();
		Update();
	}

	private void RefreshSize()
	{
		int count = _tilemap?.LayerCount ?? 1;
		MinimumHeight = Math.Max( 120.0f, HeaderHeight + count * RowHeight + Padding );
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		base.OnMouseMove( e );

		_hoveredIndex = GetLayerIndexAtPosition( e.LocalPosition );

		if ( _tilemap != null && _tilemap.IsValid() && _pressedIndex >= 0 && IsLeftButtonDown( e ) )
		{
			if ( _pressAction == PressAction.SelectOrDrag )
			{
				Vector2 delta = e.LocalPosition - _pressPosition;
				bool movedFarEnough = MathF.Abs( delta.x ) >= DragStartDistance || MathF.Abs( delta.y ) >= DragStartDistance;

				if ( !_isDragging && movedFarEnough )
				{
					_isDragging = true;
					_dragIndex = _pressedIndex;
				}

				if ( _isDragging )
				{
					int targetIndex = GetLayerIndexAtPosition( e.LocalPosition );

					if ( targetIndex >= 0 && targetIndex < _tilemap.LayerCount && targetIndex != _dragIndex )
					{
						_tilemap.MoveLayer( _dragIndex, targetIndex );
						_dragIndex = targetIndex;
						_pressedIndex = targetIndex;
						RefreshSize();
					}

					e.Accepted = true;
				}
			}
		}

		Update();
	}

	protected override void OnMouseLeave()
	{
		base.OnMouseLeave();

		_hoveredIndex = -1;
		Update();
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );

		if ( _tilemap == null || !_tilemap.IsValid() || !e.LeftMouseButton )
			return;

		int index = GetLayerIndexAtPosition( e.LocalPosition );
		if ( index < 0 || index >= _tilemap.LayerCount )
			return;

		_pressedIndex = index;
		_dragIndex = -1;
		_isDragging = false;
		_pressPosition = e.LocalPosition;

		if ( GetVisibilityButtonRect( index ).IsInside( e.LocalPosition ) )
		{
			_pressAction = PressAction.Visibility;
			e.Accepted = true;
			Update();
			return;
		}

		if ( GetCollisionButtonRect( index ).IsInside( e.LocalPosition ) )
		{
			_pressAction = PressAction.Collision;
			e.Accepted = true;
			Update();
			return;
		}

		_pressAction = PressAction.SelectOrDrag;
		_tilemap.SetActiveLayer( index );
		e.Accepted = true;
		Update();
	}

	protected override void OnMouseReleased( MouseEvent e )
	{
		base.OnMouseReleased( e );

		if ( _tilemap == null || !_tilemap.IsValid() )
		{
			ResetPressState();
			Update();
			return;
		}

		int releaseIndex = GetLayerIndexAtPosition( e.LocalPosition );

		if ( !_isDragging && _pressedIndex >= 0 && releaseIndex == _pressedIndex )
		{
			var layer = _tilemap.GetLayer( _pressedIndex );

			switch ( _pressAction )
			{
				case PressAction.Visibility:
					if ( layer != null )
						_tilemap.SetLayerVisible( _pressedIndex, !layer.IsVisible );
					break;

				case PressAction.Collision:
					if ( layer != null )
						_tilemap.SetLayerCollisionsEnabled( _pressedIndex, !layer.CollisionsEnabled );
					break;

				case PressAction.SelectOrDrag:
					_tilemap.SetActiveLayer( _pressedIndex );
					break;
			}
		}

		ResetPressState();
		e.Accepted = true;
		Update();
	}

	private void ResetPressState()
	{
		_pressedIndex = -1;
		_dragIndex = -1;
		_isDragging = false;
		_pressAction = PressAction.None;
	}

	protected override void OnPaint()
	{
		base.OnPaint();

		Paint.ClearPen();
		Paint.SetBrush( Color.Black.WithAlpha( 0.18f ) );
		Paint.DrawRect( LocalRect );

		DrawHeader();

		if ( _tilemap == null || !_tilemap.IsValid() )
		{
			Paint.SetPen( Color.White.WithAlpha( 0.7f ) );
			Paint.DrawText( new Rect( Padding, HeaderHeight + Padding, LocalRect.Width - Padding * 2.0f, 40.0f ), "No TileMap selected", TextFlag.LeftTop );
			return;
		}

		for ( int i = 0; i < _tilemap.LayerCount; i++ )
		{
			DrawLayerRow( i );
		}
	}

	private void DrawHeader()
	{
		Rect headerRect = new Rect( 0.0f, 0.0f, LocalRect.Width, HeaderHeight );

		Paint.ClearPen();
		Paint.SetBrush( Color.Black.WithAlpha( 0.30f ) );
		Paint.DrawRect( headerRect );

		Paint.SetPen( Color.White );
		Paint.DrawText( new Rect( Padding, 0.0f, LocalRect.Width - Padding * 2.0f, HeaderHeight ), "Layers  (0 = top / closest)", TextFlag.LeftCenter );
	}

	private void DrawLayerRow( int index )
	{
		var layer = _tilemap.GetLayer( index );
		if ( layer == null )
			return;

		Rect row = GetLayerRowRect( index );
		bool active = index == _tilemap.ActiveLayerIndex;
		bool hovered = index == _hoveredIndex;
		bool dragging = index == _dragIndex && _isDragging;
		bool visible = layer.IsVisible;

		float layerAlpha = visible ? 1.0f : 0.42f;

		Paint.ClearPen();
		Paint.SetBrush( active ? Color.FromBytes( 70, 125, 210 ).WithAlpha( 0.70f * layerAlpha ) : hovered ? Color.White.WithAlpha( 0.08f * layerAlpha ) : Color.White.WithAlpha( 0.035f * layerAlpha ) );
		Paint.DrawRect( row.Shrink( 2.0f ) );

		if ( dragging )
		{
			Paint.SetPen( Color.White.WithAlpha( 0.80f ) );
			Paint.DrawRect( row.Shrink( 2.0f ) );
		}

		DrawDragHandle( index, visible );
		DrawVisibilityButton( index, layer );
		DrawCollisionButton( index, layer );
		DrawLayerLabel( index, layer, active, visible );
	}

	private void DrawDragHandle( int index, bool visible )
	{
		Rect row = GetLayerRowRect( index );
		Rect handle = new Rect( Padding, row.Top, DragHandleWidth, RowHeight );

		Paint.SetPen( Color.White.WithAlpha( visible ? 0.45f : 0.20f ) );
		Paint.DrawText( handle, "☰", TextFlag.Center );
	}

	private void DrawVisibilityButton( int index, TileMap.TileLayer layer )
	{
		Rect button = GetVisibilityButtonRect( index );
		bool pressed = _pressedIndex == index && _pressAction == PressAction.Visibility;

		Paint.ClearPen();
		Paint.SetBrush( layer.IsVisible ? Color.FromBytes( 70, 130, 210 ).WithAlpha( pressed ? 0.95f : 0.75f ) : Color.Black.WithAlpha( pressed ? 0.65f : 0.40f ) );
		Paint.DrawRect( button );

		Paint.SetPen( Color.White.WithAlpha( layer.IsVisible ? 1.0f : 0.45f ) );
		Paint.DrawText( button, layer.IsVisible ? "👁" : "—", TextFlag.Center );
	}

	private void DrawCollisionButton( int index, TileMap.TileLayer layer )
	{
		Rect button = GetCollisionButtonRect( index );
		bool pressed = _pressedIndex == index && _pressAction == PressAction.Collision;
		bool collisionActuallyEnabled = layer.IsVisible && layer.CollisionsEnabled;

		Paint.ClearPen();
		Paint.SetBrush( collisionActuallyEnabled ? Color.FromBytes( 90, 190, 110 ).WithAlpha( pressed ? 1.0f : 0.88f ) : Color.Black.WithAlpha( pressed ? 0.65f : 0.35f ) );
		Paint.DrawRect( button );

		Paint.SetPen( Color.White.WithAlpha( collisionActuallyEnabled ? 1.0f : 0.35f ) );
		Paint.DrawText( button, layer.CollisionsEnabled ? "C" : "", TextFlag.Center );
	}

	private void DrawLayerLabel( int index, TileMap.TileLayer layer, bool active, bool visible )
	{
		Rect row = GetLayerRowRect( index );
		Rect visibility = GetVisibilityButtonRect( index );
		float labelLeft = Padding + DragHandleWidth + 4.0f;
		float labelRight = visibility.Left - ButtonGap;

		Paint.SetPen( Color.White.WithAlpha( visible ? active ? 1.0f : 0.85f : 0.35f ) );
		string label = $"{index}: {layer.Name}";
		Paint.DrawText( new Rect( labelLeft, row.Top, Math.Max( 0.0f, labelRight - labelLeft ), RowHeight ), label, TextFlag.LeftCenter );
	}

	private int GetLayerIndexAtPosition( Vector2 position )
	{
		if ( position.y < HeaderHeight )
			return -1;

		int index = (int)MathF.Floor( (position.y - HeaderHeight) / RowHeight );

		if ( _tilemap == null || index < 0 || index >= _tilemap.LayerCount )
			return -1;

		return index;
	}

	private Rect GetLayerRowRect( int index )
	{
		return new Rect( 0.0f, HeaderHeight + index * RowHeight, LocalRect.Width, RowHeight );
	}

	private Rect GetVisibilityButtonRect( int index )
	{
		Rect row = GetLayerRowRect( index );
		return new Rect(
			LocalRect.Width - Padding - ButtonSize,
			row.Top + (RowHeight - ButtonSize) * 0.5f,
			ButtonSize,
			ButtonSize
		);
	}

	private Rect GetCollisionButtonRect( int index )
	{
		Rect visibility = GetVisibilityButtonRect( index );
		Rect row = GetLayerRowRect( index );
		return new Rect(
			visibility.Left - ButtonGap - ButtonSize,
			row.Top + (RowHeight - ButtonSize) * 0.5f,
			ButtonSize,
			ButtonSize
		);
	}

	private static bool IsLeftButtonDown( MouseEvent e )
	{
		return e.LeftMouseButton || (e.ButtonState & MouseButtons.Left) == MouseButtons.Left;
	}
}