Editor/AutoRig/JointListPanel.cs

An editor UI widget that lists skeleton joints as selectable rows beside a preview. It builds rows from a RigResult, shows name, parent and influence count, supports click/right-click callbacks, selection highlighting, and keeps indentation by hierarchy depth.

File Access
using System;
using System.Collections.Generic;
using AutoRig.Rig;
using Editor;
using Sandbox;

namespace AutoRig.Editor;

/// <summary>
/// The joint list beside the preview: every joint as a hierarchy-indented row
/// with a red joint dot (matching the viewport), its name, and muted secondary
/// metadata (parent + influenced-vertex count). Left-click selects (raising
/// <see cref="JointSelected"/>); right-click raises <see cref="JointContext"/>
/// for the delete/rename menu. <see cref="Select"/> drives selection the other
/// way so the viewport and list stay in lock-step. Sits at the preview's height.
/// </summary>
public sealed class JointListPanel : Widget
{
    /// <summary>Left-clicked a joint (name).</summary>
    public Action<string> JointSelected { get; set; }

    /// <summary>Right-clicked a joint (name) - the dialog raises its menu.</summary>
    public Action<string> JointContext { get; set; }

    readonly Label _header;
    readonly Widget _canvas;
    readonly List<JointRow> _rows = new();
    string _selected = "";

    public JointListPanel( Widget parent, RigResult rig ) : base( parent )
    {
        MinimumWidth = 300;
        MaximumWidth = 380;

        Layout = Layout.Column();
        Layout.Margin = 0;
        Layout.Spacing = 0;

        _header = new Label( "", this );
        _header.SetStyles(
            "font-weight: 600; padding: 8px 10px 6px 10px; color: #f0f0f2;" );
        Layout.Add( _header );

        var scroll = new ScrollArea( this );
        _canvas = new Widget( scroll );
        _canvas.Layout = Layout.Column();
        _canvas.Layout.Margin = new Sandbox.UI.Margin( 4, 2, 4, 6 );
        _canvas.Layout.Spacing = 2;
        scroll.Canvas = _canvas;
        Layout.Add( scroll, 1 );

        Rebuild( rig );
    }

    /// <summary>Repopulates after a skeleton edit (delete/rename).</summary>
    public void Rebuild( RigResult rig )
    {
        _canvas.Layout.Clear( true );
        _rows.Clear();
        _header.Text = $"Joints  ({rig.Skeleton.Joints.Count})";

        var influence = InfluenceCounts( rig );
        var depth = Depths( rig );
        var joints = rig.Skeleton.Joints;
        for ( var i = 0; i < joints.Count; i++ )
        {
            var joint = joints[i];
            var parentName = joint.Parent >= 0 ? joints[joint.Parent].Name : null;
            var detail = parentName is not null
                ? $"↳ {parentName} · {influence[i]} verts"
                : $"root · {influence[i]} verts";
            var row = new JointRow( _canvas, joint.Name, detail, depth[i] );
            row.Clicked = () => Choose( row.JointName );
            row.RightClicked = () =>
            {
                Choose( row.JointName );
                JointContext?.Invoke( row.JointName );
            };
            _rows.Add( row );
            _canvas.Layout.Add( row );
        }
        _canvas.Layout.AddStretchCell();
        Select( _selected );
    }

    void Choose( string jointName )
    {
        Select( jointName );
        JointSelected?.Invoke( jointName );
    }

    /// <summary>Highlights a joint by name (empty = clear); only restyles rows.</summary>
    public void Select( string jointName )
    {
        _selected = jointName ?? "";
        foreach ( var row in _rows )
            row.SetSelected( row.JointName == _selected );
    }

    static int[] InfluenceCounts( RigResult rig )
    {
        var counts = new int[rig.Skeleton.Joints.Count];
        var indices = rig.Weights?.BoneIndices;
        var weights = rig.Weights?.Weights;
        if ( indices is not null && weights is not null )
            for ( var k = 0; k < indices.Length; k++ )
                if ( weights[k] > 0.001f && indices[k] >= 0 && indices[k] < counts.Length )
                    counts[indices[k]]++;
        return counts;
    }

    static int[] Depths( RigResult rig )
    {
        var joints = rig.Skeleton.Joints;
        var depth = new int[joints.Count];
        for ( var i = 0; i < joints.Count; i++ )
        {
            var d = 0;
            var p = joints[i].Parent;
            var guard = 0;
            while ( p >= 0 && guard++ < joints.Count )
            {
                d++;
                p = joints[p].Parent;
            }
            depth[i] = d;
        }
        return depth;
    }

    /// <summary>One selectable joint row: red dot + name + muted detail, indented
    /// by hierarchy depth, with hover and selected states.</summary>
    sealed class JointRow : Widget
    {
        public string JointName { get; }
        public Action Clicked { get; set; }
        public Action RightClicked { get; set; }

        bool _selected;
        bool _hover;

        public JointRow( Widget parent, string name, string detail, int depth ) : base( parent )
        {
            JointName = name;
            FixedHeight = 38;
            Cursor = CursorShape.Finger;

            Layout = Layout.Row();
            Layout.Margin = new Sandbox.UI.Margin( 8 + depth * 12, 0, 8, 0 );
            Layout.Spacing = 8;

            var dot = new Label( "fiber_manual_record", this );
            dot.SetStyles(
                "font-family: Material Icons; font-size: 12px; "
                + $"color: {new Color( 0.92f, 0.28f, 0.24f ).Hex};" );
            Layout.Add( dot, 0 );

            var text = Layout.AddColumn();
            text.Spacing = 0;
            var nameLabel = new Label( name, this );
            nameLabel.SetStyles( "font-size: 12px; color: #e8e8ea;" );
            text.Add( nameLabel );
            var detailLabel = new Label( detail, this );
            detailLabel.SetStyles( $"font-size: 10px; color: {Theme.TextLight.Hex};" );
            text.Add( detailLabel );
            Layout.AddStretchCell();

            Restyle();
        }

        public void SetSelected( bool value )
        {
            if ( _selected == value )
                return;
            _selected = value;
            Restyle();
        }

        protected override void OnMouseEnter()
        {
            base.OnMouseEnter();
            _hover = true;
            Restyle();
        }

        protected override void OnMouseLeave()
        {
            base.OnMouseLeave();
            _hover = false;
            Restyle();
        }

        protected override void OnMousePress( MouseEvent e )
        {
            base.OnMousePress( e );
            if ( e.RightMouseButton )
                RightClicked?.Invoke();
            else if ( e.LeftMouseButton )
                Clicked?.Invoke();
        }

        void Restyle()
        {
            var background = _selected
                ? Theme.Blue.WithAlpha( 0.22f )
                : _hover ? new Color( 1f, 1f, 1f, 0.06f ) : Color.Transparent;
            var border = _selected ? Theme.Blue.Hex : "transparent";
            SetStyles(
                $"background-color: {background.Rgba}; border-radius: 5px; "
                + $"border-left: 2px solid {border};" );
        }
    }
}