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

namespace Sandbox;

// Pure presenter for one pad. All animated values arrive as props; no state lives here.
// Lit (0=paper, 1=hot) drives fill; PressOffset (0..4px) translates the face and
// collapses the shadow; BounceScale (~1.0) is the release overshoot.
internal static class PadBlob
{
    public readonly record struct Props(
        int Index,
        string Label,
        float Lit,
        float PressOffset,
        float BounceScale,
        bool Hovered,
        Action<MousePanelEvent> OnDown,
        Action<MousePanelEvent> OnUp,
        Action<MousePanelEvent> OnEnter,
        Action<MousePanelEvent> OnLeave);

    public static Container Build(Props p)
    {
        float off = p.PressOffset;
        Color face = Color.Lerp(Paper, Hot, p.Lit);
        Color shadowColor = p.Hovered ? Hot : Ink;

        return new Container
        {
            Key = $"pad-{p.Index}",
            Width = PadSize,
            Height = PadSize,
            Position = PositionMode.Relative,
            OnMouseDown = p.OnDown,
            OnMouseUp = p.OnUp,
            OnMouseEnter = p.OnEnter,
            OnMouseLeave = p.OnLeave,
            Children =
            {
                new Container
                {
                    Key = "shadow",
                    Position = PositionMode.Absolute,
                    Left = PadShadow, Top = PadShadow,
                    Width = Length.Percent(100), Height = Length.Percent(100),
                    BackgroundColor = shadowColor,
                    BorderRadius = PadRadius,
                    PointerEvents = PointerEvents.None,
                },
                new Container
                {
                    Key = "face",
                    Position = PositionMode.Absolute,
                    Left = 0, Top = 0,
                    Width = Length.Percent(100), Height = Length.Percent(100),
                    BackgroundColor = face,
                    BorderRadius = PadRadius,
                    BorderColor = Ink,
                    BorderWidth = OutlineWidth,
                    JustifyContent = Justify.Center,
                    AlignItems = Align.Center,
                    Transform = Goo.PanelTransform.Translate(off, off).Scale(p.BounceScale),
                    PointerEvents = PointerEvents.None,
                    Children =
                    {
                        new Text(p.Label)
                        {
                            FontSize = 11f,
                            FontColor = Ink,
                            FontFamily = FontLabel,
                            TextAlign = TextAlign.Center,
                        },
                    },
                },
            },
        };
    }
}