Editor/ReadOnlyTagSetControlWidget.cs
using Editor;
using ExtendedBox.General;
using Sandbox;
using Sandbox.UI;
using System;
using System.Collections.Generic;
using System.Linq;

namespace ExtendedBox;

[CustomEditor(typeof(ReadOnlyTagSet))]
public sealed class ReadOnlyTagSetControlWidget : ControlWidget
{
    private readonly Layout _tagsArea;
    private GridLayout? _tagsPopupGrid;

    public override bool SupportsMultiEdit => true;

    public ReadOnlyTagSetControlWidget(SerializedProperty property) : base(property)
    {
        Layout = Layout.Row();
        Layout.Spacing = 3;
        Layout.Margin = new Margin(3, 0);

        _tagsArea = Layout.AddRow(1);
        _tagsArea.Spacing = 2;
        _tagsArea.Margin = new Margin(0, 3);

        Layout.AddStretchCell();

        Layout.Add(new Editor.Button(null, "local_offer") { MouseLeftPress = OpenPopup, FixedWidth = Theme.RowHeight, FixedHeight = Theme.RowHeight, OnPaintOverride = PaintTagAdd, ToolTip = "Tags" });
    }

    protected override int ValueHash
    {
        get
        {
            var tags = SerializedProperty.GetValue<ReadOnlyTagSet>();
            if(tags is null)
                return 0;

            HashCode code = default;

            foreach(var tag in tags.TryGetAll())
                code.Add(tag);

            return code.ToHashCode();
        }
    }
    protected override void OnValueChanged()
    {
        _tagsArea.Clear(true);

        var tags = SerializedProperty.GetValue<ReadOnlyTagSet>();
        if(tags is null)
            return;

        foreach(var tag in tags.TryGetAll().Take(32))
            _tagsArea.Add(new TagButton(this) { TagText = tag, MouseLeftPress = () => RemoveTag(tag) });
    }

    private void ToggleTag(string tag)
    {
        var tags = SerializedProperty.GetValue<ReadOnlyTagSet>();
        if(tags is null)
            return;

        tags = TagSetBuilder.Create(tags).Toggle(tag).Build();
        SerializedProperty.SetValue(tags);
        SerializedProperty.Parent?.NoteChanged(SerializedProperty);
    }

    private void AddTag(string tag)
    {
        var tags = SerializedProperty.GetValue<ReadOnlyTagSet>();
        if(tags is null)
            return;

        tags = TagSetBuilder.Create(tags).Add(tag).Build();
        SerializedProperty.SetValue(tags);
        SerializedProperty.Parent?.NoteChanged(SerializedProperty);
    }

    private void RemoveTag(string tag)
    {
        var tags = SerializedProperty.GetValue<ReadOnlyTagSet>();
        if(tags is null)
            return;

        tags = TagSetBuilder.Create(tags).Remove(tag).Build();
        SerializedProperty.SetValue(tags);
        SerializedProperty.Parent?.NoteChanged(SerializedProperty);
    }

    bool PaintTagAdd()
    {
        var alpha = Paint.HasMouseOver ? 1.0f : 0.7f;

        Paint.SetPen(Theme.Blue.WithAlpha(0.5f * alpha));
        Paint.DrawIcon(new Rect(0, Theme.RowHeight), "local_offer", 16);

        Paint.SetPen(Theme.Blue.WithAlpha(0.8f * alpha));
        Paint.DrawIcon(new Rect(0, Theme.RowHeight), "add", 13, TextFlag.LeftBottom);
        return true;
    }

    void OpenPopup()
    {
        var tags = SerializedProperty.GetValue<ReadOnlyTagSet>();

        if(tags is null)
        {
            if(SerializedProperty.PropertyType == typeof(ReadOnlyTagSet))
            {
                tags = new TagSet();
                SerializedProperty.SetValue(tags);
            }
            else
            {
                Log.Warning($"{nameof(ReadOnlyTagSet)} is null and we don't know how to create type: {SerializedProperty.PropertyType}");
                return;
            }
        }

        var popup = new PopupWidget(this)
        {
            FixedWidth = 200,
            Layout = Layout.Column()
        };
        popup.Layout.Margin = 8;
        popup.Layout.Spacing = 4;

        var entry = popup.Layout.Add(new LineEdit(popup));
        _tagsPopupGrid = popup.Layout.Add(Layout.Grid()) as GridLayout;

        entry.PlaceholderText = "New tag..";
        entry.FixedHeight = Theme.RowHeight;
        entry.ReturnPressed += () =>
        {
            AddTag(entry.Value);
            entry.Clear();
            RebuildTagGrid();
        };

        RebuildTagGrid();

        popup.OpenAt(ScreenRect.BottomRight + new Vector2(-200, 0));

        entry.Focus();
    }

    void RebuildTagGrid()
    {
        var scene = SceneEditorSession.Active?.Scene;
        var tags = SerializedProperty.GetValue<ReadOnlyTagSet>();

        if(!scene.IsValid())
            return;
        if(tags == null)
            return;
        if(!_tagsPopupGrid.IsValid())
            return;

        _tagsPopupGrid.Clear(true);

        var defaultTags = new List<string>();

        int i = 0;
        foreach(var g in defaultTags.GroupBy(x => x).OrderByDescending(x => x.Count()).Take(32))
        {
            var t = g.First();
            var c = g.Count();

            var button = new Editor.Button("", this) { MouseLeftPress = () => ToggleTag(t) };

            button.OnPaintOverride = () => PaintTagButton(t, c, button.LocalRect, tags.Has(t));
            _tagsPopupGrid.AddCell(i % 2, i / 2, button);
            ++i;
        }
    }

    private static bool PaintTagButton(string tagText, int count, Rect rect, bool has)
    {
        var alpha = Paint.HasMouseOver ? 1.0f : 0.7f;
        var tagColor = Theme.Blue;
        Color bg = Theme.TextControl.WithAlpha(0.1f);
        Color color = Theme.TextControl.WithAlpha(0.7f);

        if(Paint.HasMouseOver)
        {
            bg = Theme.TextControl.WithAlpha(0.2f);
            color = Theme.TextControl;
        }

        if(has)
        {
            bg = tagColor.Darken(Paint.HasMouseOver ? 0.5f : 0.6f);
            color = Paint.HasMouseOver ? Color.White : tagColor;
        }

        Paint.SetDefaultFont(8);

        Paint.Antialiasing = true;
        Paint.TextAntialiasing = true;

        //if ( Paint.HasMouseOver || has )
        {
            Paint.SetBrush(bg);
            Paint.ClearPen();
            Paint.DrawRect(rect.Shrink(2), 3);
        }

        Paint.SetPen(color.WithAlphaMultiplied(0.9f * alpha));
        Paint.ClearBrush();
        Paint.DrawText(rect.Shrink(10, 0), tagText.ToLower(), TextFlag.LeftCenter);

        Paint.SetDefaultFont(7);
        Paint.SetPen(color.WithAlphaMultiplied(0.5f * alpha));
        Paint.DrawText(rect.Shrink(10, 0), $"{count}", TextFlag.RightCenter);

        return true;
    }
}

file class TagButton : Widget
{
    public string TagText { get; set; } = string.Empty;

    public TagButton(Widget parent) : base(parent)
    {
        SetSizeMode(SizeMode.CanShrink, SizeMode.Default);
    }

    protected override Vector2 SizeHint()
    {
        Paint.SetDefaultFont(7);
        return Paint.MeasureText(TagText.ToLower()) + new Vector2(8, 0);
    }

    protected override void OnPaint()
    {
        var alpha = Paint.HasMouseOver ? 1.0f : 0.7f;
        var color = Theme.Blue;

        Paint.SetDefaultFont(7);

        Paint.Antialiasing = true;
        Paint.TextAntialiasing = true;
        Paint.SetBrush(color.Darken(0.3f).WithAlpha(0.6f * alpha));
        Paint.ClearPen();
        Paint.DrawRect(LocalRect, 3);

        Paint.SetPen(color.WithAlpha(0.9f * alpha));
        Paint.ClearBrush();
        Paint.DrawText(LocalRect.Shrink(4, 0), TagText.ToLower());
    }
}