Code/Demos/BeatPad/ChannelRackBlob.cs
using System;
using System.Collections.Generic;
using Goo;
using Goo.Input;
using Sandbox.UI;
using static Sandbox.BeatPadTokens;

namespace Sandbox;

// pure presenter for the FL-style channel rack: step ruler, one row per active track, sweeping playhead. state in BeatPadUI; fixed-px cells make the rack content-sized.
internal static class ChannelRackBlob
{
    public readonly record struct Track(
        int Pad, string Label, bool[] Occupied, bool Muted, bool Soloed, bool Silenced);

    public readonly record struct Props(
        IReadOnlyList<Track> Tracks,
        int Steps,
        float PlayheadFraction,
        HandlerTable<(int, int)> CellClick,    // (step, pad)
        HandlerTable<int> TrackDelete,          // pad
        HandlerTable<int> TrackMute,            // pad
        HandlerTable<int> TrackSolo);           // pad

    // layout constants (px)
    const float CellW = 16f, CellH = 18f, CellGap = 3f, BeatGap = 6f;
    const float LabelW = 44f, LabelGap = 4f, BtnW = 16f, CtrlGap = 3f, HeadGap = 6f;
    const float RowGap = 4f, RulerH = 12f, Pad = 8f;

    // x where the step cells start: label, then the S/M/X control buttons.
    const float LaneLeft = LabelW + LabelGap + 3f * BtnW + 2f * CtrlGap + HeadGap;

    // trailing margin after step i: bigger gap at the end of each 4-beat group, none after the last.
    static float CellMargin(int i, int steps) =>
        i == steps - 1 ? 0f : ((i % 4 == 3) ? BeatGap : CellGap);

    public static Container Build(Props p)
    {
        int steps = p.Steps;

        float laneLeft = LaneLeft;
        float laneWidth = 0f;
        for (int i = 0; i < steps; i++) laneWidth += CellW + CellMargin(i, steps);
        float totalH = RulerH + p.Tracks.Count * (CellH + RowGap);

        var rack = new Container
        {
            Key = "rack",
            Position = PositionMode.Relative,
            FlexDirection = FlexDirection.Column,
            Children = { BuildRuler(steps, laneLeft) },
        };

        for (int r = 0; r < p.Tracks.Count; r++)
            rack.Children.Add(BuildRow(p, p.Tracks[r], steps));

        // sweeping playhead bar across the cells region
        float head = Math.Clamp(p.PlayheadFraction, 0f, 1f);
        rack.Children.Add(new Container
        {
            Key = "playhead",
            Position = PositionMode.Absolute,
            Left = laneLeft + head * laneWidth,
            Top = 0,
            Width = 2f,
            Height = totalH,
            BackgroundColor = Hot,
            PointerEvents = PointerEvents.None,
        });

        // Seat the rack on an Ink LCD panel (matches the scope) so it reads as a screen,
        // not loose cells on the chassis. Padding insets the content from the border; the
        // playhead stays aligned because it is absolute within `rack` (the inner content box).
        return new Container
        {
            Key = "racklcd",
            BackgroundColor = Ink,
            BorderColor = Ink,
            BorderWidth = OutlineWidth,
            BorderRadius = ScopeRadius,
            PaddingLeft = Pad, PaddingTop = Pad, PaddingRight = Pad, PaddingBottom = Pad,
            Children = { rack },
        };
    }

    static Container BuildRuler(int steps, float laneLeft)
    {
        var ruler = new Container
        {
            Key = "ruler",
            FlexDirection = FlexDirection.Row,
            Height = RulerH,
            Children =
            {
                new Container { Key = "rspacer", Width = laneLeft, PointerEvents = PointerEvents.None },
            },
        };
        for (int i = 0; i < steps; i++)
        {
            ruler.Children.Add(new Container
            {
                Key = $"tick-{i}",
                Width = CellW,
                MarginRight = CellMargin(i, steps),
                AlignItems = Align.Center,
                PointerEvents = PointerEvents.None,
                Children =
                {
                    new Text(i % 4 == 0 ? (i + 1).ToString() : "")
                    {
                        FontSize = 8f, FontColor = Paper.WithAlpha(0.5f), FontFamily = FontMono,
                    },
                },
            });
        }
        return ruler;
    }

    // S/M/X control button: lit (Hot fill, Ink glyph) when active, else Paper outline.
    static Container HeadButton(string key, string glyph, bool active, float marginRight, Action<MousePanelEvent> onDown)
        => new Container
        {
            Key = key,
            Width = BtnW, Height = BtnW,
            MarginRight = marginRight,
            BorderColor = active ? Hot : Paper, BorderWidth = 1.5f, BorderRadius = 3f,
            BackgroundColor = active ? Hot : Color.Transparent,
            JustifyContent = Justify.Center, AlignItems = Align.Center,
            OnMouseDown = onDown,
            Children =
            {
                new Text(glyph) { FontSize = 9f, FontColor = active ? Ink : Paper, FontFamily = FontLabel },
            },
        };

    static Container BuildRow(Props p, Track t, int steps)
    {
        var row = new Container
        {
            Key = $"trk-{t.Pad}",
            Position = PositionMode.Relative,
            FlexDirection = FlexDirection.Row,
            AlignItems = Align.Center,
            Height = CellH,
            MarginBottom = RowGap,
            Children =
            {
                new Container
                {
                    Key = "lbl",
                    Width = LabelW,
                    MarginRight = LabelGap,
                    PointerEvents = PointerEvents.None,
                    Children =
                    {
                        new Text(t.Label) { FontSize = 9f, FontColor = Paper, FontFamily = FontLabel },
                    },
                },
                HeadButton("solo", "S", t.Soloed, CtrlGap, p.TrackSolo[t.Pad]),
                HeadButton("mute", "M", t.Muted, CtrlGap, p.TrackMute[t.Pad]),
                HeadButton("del", "X", false, HeadGap, p.TrackDelete[t.Pad]),
            },
        };

        // A silenced track (muted, or excluded by another track's solo) renders dim.
        Color onColor = t.Silenced ? Hot.WithAlpha(0.30f) : Hot;
        Color onBg = t.Silenced ? Hot.WithAlpha(0.18f) : Hot;
        for (int s = 0; s < steps; s++)
        {
            bool on = s < t.Occupied.Length && t.Occupied[s];
            row.Children.Add(new Container
            {
                Key = $"cell-{s}",
                Width = CellW, Height = CellH,
                MarginRight = CellMargin(s, steps),
                BorderColor = on ? onColor : Paper.WithAlpha(0.30f),
                BorderWidth = 1.5f,
                BorderRadius = 2f,
                BackgroundColor = on ? onBg : Paper.WithAlpha(0.08f),
                OnMouseDown = p.CellClick[(s, t.Pad)],
            });
        }

        return row;
    }
}