XGUI/Elements/TreeView.cs
using Sandbox.UI;
using Sandbox.UI.Construct; // Make sure Add is available
using System;
using System.Collections.Generic;
using System.Linq;

namespace XGUI;

public partial class TreeView : Panel
{
	public class TreeViewNode : Panel
	{
		public TreeView ParentTreeView { get; }
		public TreeViewNode ParentNode { get; }
		public object Data { get; set; }
		public string Text { get; set; }
		public string IconName { get; set; }

		public List<TreeViewNode> ChildNodes { get; } = new();
		public bool IsExpanded { get; private set; }
		public bool IsSelected { get; private set; }

		public Panel HeaderRow { get; private set; }
		public XGUIIconPanel IconPanel { get; private set; }
		public Label NodeLabel { get; private set; }
		public Panel Expander { get; private set; }
		public Panel ChildrenContainer { get; private set; }

		private const float IndentSize = 16f; // Indentation per level

		public TreeViewNode( TreeView parentTreeView, TreeViewNode parentNode, string text, string iconName = null, object data = null )
		{
			ParentTreeView = parentTreeView;
			ParentNode = parentNode;
			Text = text;
			IconName = iconName;
			Data = data;

			AddClass( "treeview-node" );
			Render();
		}

		private void Render()
		{
			HeaderRow = Add.Panel( "treeview-node-header" );
			HeaderRow.Style.PaddingLeft = (GetDepth() * IndentSize);

			Expander = HeaderRow.Add.Panel( "treeview-expander" );
			Expander.AddEventListener( "onclick", ToggleExpand );

			IconPanel = HeaderRow.AddChild<XGUIIconPanel>();
			IconPanel.AddClass( "treeview-icon" );
			if ( !string.IsNullOrEmpty( IconName ) )
			{
				IconPanel.SetIcon( IconName, XGUIIconSystem.IconType.UI, 16 );
			}
			else
			{
				IconPanel.Style.Display = DisplayMode.None; // Hide if no icon
			}

			NodeLabel = HeaderRow.Add.Label( Text, "treeview-label" );

			ChildrenContainer = Add.Panel( "treeview-children-container" );
			ChildrenContainer.Style.Display = DisplayMode.None; // Initially hidden

			UpdateExpanderIcon();
		}

		public void SetText( string text )
		{
			Text = text;
			NodeLabel.Text = text;
		}

		public void SetIcon( string iconName )
		{
			IconName = iconName;
			if ( !string.IsNullOrEmpty( IconName ) )
			{
				IconPanel.SetIcon( IconName, XGUIIconSystem.IconType.UI, 16 );
				IconPanel.Style.Display = DisplayMode.Flex;
			}
			else
			{
				IconPanel.Style.Display = DisplayMode.None;
			}
		}

		public int GetDepth()
		{
			int depth = 0;
			var current = ParentNode;
			while ( current != null )
			{
				depth++;
				current = current.ParentNode;
			}
			return depth;
		}

		public TreeViewNode AddChild( string text, string iconName = null, object data = null )
		{
			var childNode = new TreeViewNode( ParentTreeView, this, text, iconName, data );
			ChildNodes.Add( childNode );
			ChildrenContainer.AddChild( childNode );
			UpdateExpanderIcon();
			return childNode;
		}

		public void RemoveChild( TreeViewNode childNode )
		{
			if ( ChildNodes.Remove( childNode ) )
			{
				childNode.Delete();
				UpdateExpanderIcon();
			}
		}

		public void ClearChildren()
		{
			foreach ( var child in ChildNodes.ToList() )
			{
				child.Delete();
			}
			ChildNodes.Clear();
			UpdateExpanderIcon();
		}


		public void ToggleExpand( PanelEvent e )
		{
			if ( !ChildNodes.Any() ) return;

			IsExpanded = !IsExpanded;
			ChildrenContainer.Style.Display = IsExpanded ? DisplayMode.Flex : DisplayMode.None;
			UpdateExpanderIcon();

			if ( IsExpanded )
				ParentTreeView.OnNodeExpanded?.Invoke( this );
			else
				ParentTreeView.OnNodeCollapsed?.Invoke( this );

			e.StopPropagation();
		}

		public void Expand()
		{
			if ( IsExpanded || !ChildNodes.Any() ) return;
			IsExpanded = true;
			ChildrenContainer.Style.Display = DisplayMode.Flex;
			UpdateExpanderIcon();
			ParentTreeView.OnNodeExpanded?.Invoke( this );
		}

		public void Collapse()
		{
			if ( !IsExpanded || !ChildNodes.Any() ) return;
			IsExpanded = false;
			ChildrenContainer.Style.Display = DisplayMode.None;
			UpdateExpanderIcon();
			ParentTreeView.OnNodeCollapsed?.Invoke( this );
		}

		public void ExpandAll()
		{
			Expand();
			foreach ( var child in ChildNodes )
			{
				child.ExpandAll();
			}
		}

		public void CollapseAll()
		{
			Collapse();
			foreach ( var child in ChildNodes )
			{
				child.CollapseAll();
			}
		}

		private void UpdateExpanderIcon()
		{
			if ( !ChildNodes.Any() )
			{
				Expander.SetClass( "empty", true );
				Expander.SetClass( "expanded", false );
				Expander.SetClass( "collapsed", false );
			}
			else
			{
				Expander.SetClass( "empty", false );
				Expander.SetClass( "expanded", IsExpanded );
				Expander.SetClass( "collapsed", !IsExpanded );
			}
		}

		private bool IsEventTargetExpanderOrChild( Panel eventTarget )
		{
			if ( eventTarget == null ) return false;
			if ( eventTarget == Expander ) return true;

			var parent = eventTarget.Parent;
			while ( parent != null )
			{
				if ( parent == Expander ) return true;
				parent = parent.Parent;
			}
			return false;
		}

		protected override void OnClick( MousePanelEvent e )
		{
			base.OnClick( e );

			if ( IsEventTargetExpanderOrChild( e.Target as Panel ) )
			{
				// ToggleExpand is already called by the event listener on Expander
			}
			else
			{
				ParentTreeView.SelectItem( this );
			}
			e.StopPropagation();
		}

		protected override void OnRightClick( MousePanelEvent e )
		{
			base.OnRightClick( e );
			ParentTreeView.SelectItem( this );
			ParentTreeView.OnNodeRightClick?.Invoke( this, e );
			e.StopPropagation();
		}

		protected override void OnDoubleClick( MousePanelEvent e )
		{
			base.OnDoubleClick( e );
			if ( !IsEventTargetExpanderOrChild( e.Target as Panel ) )
			{
				ParentTreeView.OnNodeActivated?.Invoke( this );
			}
		}

		public void SetSelected( bool selected )
		{
			IsSelected = selected;
			SetClass( "selected", selected );
		}
	}

	public Panel ItemContainer { get; private set; }
	public List<TreeViewNode> RootNodes { get; } = new();
	public TreeViewNode SelectedNode { get; private set; }

	public Action<TreeViewNode> OnNodeSelected { get; set; }
	public Action<TreeViewNode> OnNodeActivated { get; set; } // Double-click
	public Action<TreeViewNode> OnNodeExpanded { get; set; }
	public Action<TreeViewNode> OnNodeCollapsed { get; set; }
	public Action<TreeViewNode, MousePanelEvent> OnNodeRightClick { get; set; }

	public TreeView()
	{
		AddClass( "treeview" );
		//ItemContainer = Add.Panel( "treeview-item-container" );
		ItemContainer = new ScrollPanel();
		ItemContainer.AddClass( "treeview-item-container" );
		AddChild( ItemContainer );
	}

	public TreeViewNode AddRootNode( string text, string iconName = null, object data = null )
	{
		var node = new TreeViewNode( this, null, text, iconName, data );
		RootNodes.Add( node );
		ItemContainer.AddChild( node );
		return node;
	}

	public void RemoveRootNode( TreeViewNode node )
	{
		if ( RootNodes.Remove( node ) )
		{
			node.Delete();
		}
	}

	public void ClearNodes()
	{
		foreach ( var node in RootNodes.ToList() )
		{
			node.Delete();
		}
		RootNodes.Clear();
		SelectedNode = null;
	}

	public void SelectItem( TreeViewNode node )
	{
		if ( SelectedNode == node && node != null )
		{
			// Optional: If you want re-selection to fire the event, do it here.
			// OnNodeSelected?.Invoke(SelectedNode); // Uncomment if re-selection should fire event
			return; // Or simply return if re-selecting the same node does nothing new.
		}

		if ( SelectedNode != null )
		{
			SelectedNode.SetSelected( false );
		}

		SelectedNode = node;

		if ( SelectedNode != null )
		{
			SelectedNode.SetSelected( true );
		}
		OnNodeSelected?.Invoke( SelectedNode );
	}

	public TreeViewNode FindNode( Func<TreeViewNode, bool> predicate )
	{
		foreach ( var rootNode in RootNodes )
		{
			var found = FindNodeRecursive( rootNode, predicate );
			if ( found != null ) return found;
		}
		return null;
	}

	private TreeViewNode FindNodeRecursive( TreeViewNode currentNode, Func<TreeViewNode, bool> predicate )
	{
		if ( predicate( currentNode ) )
		{
			return currentNode;
		}
		foreach ( var child in currentNode.ChildNodes )
		{
			var found = FindNodeRecursive( child, predicate );
			if ( found != null ) return found;
		}
		return null;
	}
}