Whiskers UI — Unity C# (UI Toolkit)

Unity WebGL App Viewport
Build project in Unity (WebGL) and place Build/ folder here to run live player.
WhiskersPanel.cs (UnityEngine.UIElements)
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

[RequireComponent(typeof(UIDocument))]
public class WhiskersPanel : MonoBehaviour
{
    private UIDocument _doc;
    private VisualElement _panel;
    private VisualElement _body;
    private bool _collapsed = false;

    // Drag State
    private bool _isDragging = false;
    private Vector2 _dragOffset;

    // Form State
    private readonly List<string> _surfaces = new() { "Wobbler", "Opartica", "Autobahn" };
    private readonly List<string> _blendModes = new() { "normal", "lighten", "darken", "screen", "multiply", "add" };
    private int _surfaceIdx = 0;
    private int _layerIdx = 0;
    private int _blendIdx = 0;

    void OnEnable()
    {
        _doc = GetComponent<UIDocument>();
        var root = _doc.rootVisualElement;
        root.style.flexGrow = 1;

        // 1. Floating Panel Window
        _panel = new VisualElement();
        _panel.style.position = Position.Absolute;
        _panel.style.left = 30;
        _panel.style.top = 30;
        _panel.style.width = 210;
        _panel.style.backgroundColor = new Color(0.13f, 0.13f, 0.13f, 0.92f);
        _panel.style.borderTopColor = _panel.style.borderBottomColor = 
            _panel.style.borderLeftColor = _panel.style.borderRightColor = new Color(0.27f, 0.27f, 0.27f);
        _panel.style.borderTopWidth = _panel.style.borderBottomWidth = 
            _panel.style.borderLeftWidth = _panel.style.borderRightWidth = 3;
        _panel.style.borderTopLeftRadius = _panel.style.borderTopRightRadius = 
            _panel.style.borderBottomLeftRadius = _panel.style.borderBottomRightRadius = 10;
        root.Add(_panel);

        // 2. Draggable Title Bar
        var titleBar = new VisualElement();
        titleBar.style.height = 34;
        titleBar.style.backgroundColor = new Color(0.17f, 0.17f, 0.17f);
        titleBar.style.flexDirection = FlexDirection.Row;
        titleBar.style.justifyContent = Justify.SpaceBetween;
        titleBar.style.alignItems = Align.Center;
        titleBar.style.paddingLeft = titleBar.style.paddingRight = 10;

        var titleLabel = CreateLabel("Whiskers", 13, Color.white, true);
        var collapseBtn = new Button(() => {
            _collapsed = !_collapsed;
            _body.style.display = _collapsed ? DisplayStyle.None : DisplayStyle.Flex;
        }) { text = "−" };
        collapseBtn.style.backgroundColor = Color.clear;
        collapseBtn.style.color = new Color(0.7f, 0.7f, 0.7f);

        titleBar.Add(titleLabel);
        titleBar.Add(collapseBtn);
        _panel.Add(titleBar);

        // Pointer Drag Events
        titleBar.RegisterCallback<PointerDownEvent>(e => {
            _isDragging = true;
            _dragOffset = e.localPosition;
            titleBar.CapturePointer(e.pointerId);
        });
        titleBar.RegisterCallback<PointerMoveEvent>(e => {
            if (!_isDragging) return;
            _panel.style.left = _panel.resolvedStyle.left + (e.localPosition.x - _dragOffset.x);
            _panel.style.top = _panel.resolvedStyle.top + (e.localPosition.y - _dragOffset.y);
        });
        titleBar.RegisterCallback<PointerUpEvent>(e => {
            _isDragging = false;
            titleBar.ReleasePointer(e.pointerId);
        });

        // 3. Panel Body (Flex Layout Stack)
        _body = new VisualElement();
        _body.style.paddingTop = _body.style.paddingBottom = 12;
        _body.style.paddingLeft = _body.style.paddingRight = 10;
        _body.style.alignItems = Align.Center;
        _panel.Add(_body);

        // Surface Stepper
        _body.Add(CreateStepper(_surfaces, _surfaceIdx, idx => _surfaceIdx = idx));

        // Lock & Hide Row
        var lockHideRow = CreateRow();
        lockHideRow.Add(CreateToggle("LOCK"));
        lockHideRow.Add(CreateToggle("HIDE"));
        _body.Add(lockHideRow);

        // Layer Controls
        _body.Add(CreateLabel("LAYER", 11, new Color(0.85f, 0.85f, 0.85f)));
        _body.Add(CreateStepper(new List<string> { "0", "1", "2" }, _layerIdx, idx => _layerIdx = idx));

        // 2-Column Crop Matrix (Sliders Left + Vertical Slider Right)
        var cropBox = CreateRow();
        cropBox.style.alignItems = Align.FlexEnd;

        var leftCol = new VisualElement();
        leftCol.style.alignItems = Align.Center;
        leftCol.Add(CreateToggle("CROP"));
        leftCol.Add(CreateLabel("SCALE", 10, new Color(0.6f, 0.6f, 0.6f)));
        leftCol.Add(CreateSlider(0.2f, 3.0f, 1.0f, 85));
        leftCol.Add(CreateSlider(-100f, 100f, 0f, 85));
        leftCol.Add(CreateLabel("SHIFT X", 10, new Color(0.6f, 0.6f, 0.6f)));

        var rightCol = new VisualElement();
        rightCol.style.alignItems = Align.Center;
        rightCol.Add(CreateSlider(-100f, 100f, 0f, 60, SliderDirection.Vertical));
        rightCol.Add(CreateLabel("SHIFT Y", 10, new Color(0.6f, 0.6f, 0.6f)));

        cropBox.Add(leftCol);
        cropBox.Add(rightCol);
        _body.Add(cropBox);

        // Mask Row
        var maskRow = CreateRow();
        maskRow.Add(new Toggle());
        maskRow.Add(CreateButton("MASK", new Color(0.36f, 0.14f, 0.51f), 65, 22));
        _body.Add(maskRow);

        // Feather
        _body.Add(CreateLabel("FEATHER", 11, new Color(0.85f, 0.85f, 0.85f)));
        _body.Add(CreateSlider(0.0f, 1.0f, 0.2f, 105));

        // Blend
        _body.Add(CreateLabel("BLEND", 11, new Color(0.85f, 0.85f, 0.85f)));
        _body.Add(CreateStepper(_blendModes, _blendIdx, idx => _blendIdx = idx));

        // Reset Button
        _body.Add(CreateButton("RESET", new Color(0.86f, 0.2f, 0.2f), 85, 22));

        // Global Divider & Label
        var divider = new VisualElement();
        divider.style.width = 170;
        divider.style.height = 2;
        divider.style.backgroundColor = new Color(0.2f, 0.2f, 0.2f);
        divider.style.marginTop = 6;
        _body.Add(divider);

        _body.Add(CreateLabel("GLOBAL", 10, new Color(0.5f, 0.5f, 0.5f)));

        // Action Buttons
        _body.Add(CreateButton("IMPORT", new Color(0.33f, 0.33f, 0.33f), 85, 22));
        _body.Add(CreateButton("EXPORT", new Color(0.33f, 0.33f, 0.33f), 85, 22));
        _body.Add(CreateButton("PRESENT", new Color(0.11f, 0.43f, 0.2f), 95, 24));
    }

    // --- HELPER BUILDERS ---
    private Label CreateLabel(string text, int size, Color color, bool bold = false)
    {
        var l = new Label(text);
        l.style.fontSize = size;
        l.style.color = color;
        l.style.unityFontStyleAndWeight = bold ? FontStyle.Bold : FontStyle.Normal;
        l.style.marginBottom = l.style.marginTop = 2;
        return l;
    }

    private Button CreateButton(string text, Color bg, float width, float height)
    {
        var b = new Button { text = text };
        b.style.width = width;
        b.style.height = height;
        b.style.backgroundColor = bg;
        b.style.color = Color.white;
        b.style.fontSize = 11;
        b.style.unityFontStyleAndWeight = FontStyle.Bold;
        b.style.borderTopLeftRadius = b.style.borderTopRightRadius = 
            b.style.borderBottomLeftRadius = b.style.borderBottomRightRadius = 4;
        b.style.marginBottom = 4;
        return b;
    }

    private VisualElement CreateStepper(List<string> list, int initialIdx, System.Action<int> onChange)
    {
        int idx = initialIdx;
        var box = new VisualElement();
        box.style.flexDirection = FlexDirection.Row;
        box.style.justifyContent = Justify.SpaceBetween;
        box.style.alignItems = Align.Center;
        box.style.width = 125;
        box.style.height = 24;
        box.style.backgroundColor = new Color(0.12f, 0.12f, 0.12f);
        box.style.borderTopColor = box.style.borderBottomColor = 
            box.style.borderLeftColor = box.style.borderRightColor = new Color(0.27f, 0.27f, 0.27f);
        box.style.borderTopWidth = box.style.borderBottomWidth = 
            box.style.borderLeftWidth = box.style.borderRightWidth = 1;
        box.style.borderTopLeftRadius = box.style.borderTopRightRadius = 
            box.style.borderBottomLeftRadius = box.style.borderBottomRightRadius = 4;
        box.style.marginBottom = 6;

        var valLabel = CreateLabel(list[idx], 11, Color.white, true);
        var prev = new Button(() => { idx = (idx - 1 + list.Count) % list.Count; valLabel.text = list[idx]; onChange(idx); }) { text = "<" };
        var next = new Button(() => { idx = (idx + 1) % list.Count; valLabel.text = list[idx]; onChange(idx); }) { text = ">" };

        prev.style.backgroundColor = next.style.backgroundColor = Color.clear;
        prev.style.color = next.style.color = Color.grey;

        box.Add(prev);
        box.Add(valLabel);
        box.Add(next);
        return box;
    }

    private Slider CreateSlider(float min, float max, float val, float size, SliderDirection dir = SliderDirection.Horizontal)
    {
        var s = new Slider(min, max, dir) { value = val };
        if (dir == SliderDirection.Horizontal) s.style.width = size;
        else s.style.height = size;
        s.style.marginBottom = 4;
        return s;
    }

    private VisualElement CreateToggle(string text)
    {
        var t = new Toggle(text);
        t.style.fontSize = 11;
        t.style.color = Color.white;
        t.style.unityFontStyleAndWeight = FontStyle.Bold;
        return t;
    }

    private VisualElement CreateRow()
    {
        var r = new VisualElement();
        r.style.flexDirection = FlexDirection.Row;
        r.style.justifyContent = Justify.Center;
        r.style.alignItems = Align.Center;
        r.style.marginBottom = 6;
        return r;
    }
}