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.
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};" );
}
}
}