Demos/BeatPad/KnobBlob.cs
using System;
using Goo;
using Sandbox.UI;
using Sandbox.BeatPad;
using static Sandbox.BeatPadTokens;

namespace Sandbox;

// Pure presenter for one knob: circle body + ink drop-shadow + a rotated indicator
// stem + an uppercase label + a value readout that fades (Flash 1..0 drives readout alpha).
// Angle comes straight from the value (no spring; pointer-immediate per spec).
internal static class KnobBlob
{
    public readonly record struct Props(
        int Index,
        string Label,
        float Value,        // 0..1 (for OUT this is the meter level)
        float Flash,        // 1 just after change -> 0
        bool ReadOnly,
        Action<MousePanelEvent> OnDown,
        Action<MousePanelEvent> OnUp,
        Action<MousePanelEvent> OnMove);

    public static Container Build(Props p)
    {
        float angle = KnobMath.ValueToAngle(p.Value);

        return new Container
        {
            Key = $"knob-{p.Index}",
            Width = KnobSize,
            Height = KnobSize + 34f, // room for label/readout below the circle (label gap + larger type)
            Position = PositionMode.Relative,
            FlexDirection = FlexDirection.Column,
            AlignItems = Align.Center,
            Children =
            {
                // circle cluster (fixed-size box holding shadow + body + stem)
                new Container
                {
                    Key = "body",
                    Width = KnobSize, Height = KnobSize,
                    Position = PositionMode.Relative,
                    OnMouseDown = p.ReadOnly ? null : p.OnDown,
                    OnMouseUp = p.ReadOnly ? null : p.OnUp,
                    OnMouseMove = p.ReadOnly ? null : p.OnMove,
                    Children =
                    {
                        new Container
                        {
                            Key = "shadow",
                            Position = PositionMode.Absolute,
                            Left = KnobShadow, Top = KnobShadow,
                            Width = Length.Percent(100), Height = Length.Percent(100),
                            BackgroundColor = Ink,
                            BorderRadius = Length.Percent(50),
                            PointerEvents = PointerEvents.None,
                        },
                        new Container
                        {
                            Key = "disc",
                            Position = PositionMode.Absolute,
                            Left = 0, Top = 0,
                            Width = Length.Percent(100), Height = Length.Percent(100),
                            BackgroundColor = Paper,
                            BorderColor = Ink,
                            BorderWidth = OutlineWidth,
                            BorderRadius = Length.Percent(50),
                            PointerEvents = PointerEvents.None,
                        },
                        // rotated stem wrapper (full-size, rotates about center)
                        new Container
                        {
                            Key = "stem-rot",
                            Position = PositionMode.Absolute,
                            Left = 0, Top = 0,
                            Width = Length.Percent(100), Height = Length.Percent(100),
                            Transform = Goo.PanelTransform.Rotate(angle),
                            PointerEvents = PointerEvents.None,
                            Children =
                            {
                                new Container
                                {
                                    Key = "stem",
                                    Position = PositionMode.Absolute,
                                    Left = Length.Percent(50),
                                    Top = 4f,
                                    MarginLeft = -StemWidth * 0.5f,
                                    Width = StemWidth,
                                    Height = StemHeight,
                                    BackgroundColor = Ink,
                                    BorderRadius = 1f,
                                    PointerEvents = PointerEvents.None,
                                },
                            },
                        },
                    },
                },
                new Text(p.Label)
                {
                    Key = "label",
                    MarginTop = 6f, // sit the label slightly off the knob body
                    FontSize = 13f,
                    FontColor = Ink,
                    FontFamily = FontLabel,
                    TextTransform = TextTransform.Uppercase,
                    LetterSpacing = 1.5f,
                    TextAlign = TextAlign.Center,
                },
                new Text($"{(int)MathF.Round(p.Value * 100f)}")
                {
                    Key = "readout",
                    FontSize = 9f,
                    FontColor = Hot.WithAlpha(p.Flash),
                    FontFamily = FontMono,
                    TextAlign = TextAlign.Center,
                },
            },
        };
    }
}