Editor/Inspector/StateRuleAppearanceProxy.cs
using System;
using Grains.RazorDesigner.Document;
using Sandbox;

namespace Grains.RazorDesigner.Inspector;

public sealed class StateRuleAppearanceProxy
{
	private readonly ControlRecord _record;
	private readonly Func<int> _ruleIndex;  // re-resolved each access; rules list can shift.

	public event Action Changed;

	public StateRuleAppearanceProxy( ControlRecord record, Func<int> ruleIndex )
	{
		_record = record ?? throw new ArgumentNullException( nameof(record) );
		_ruleIndex = ruleIndex ?? throw new ArgumentNullException( nameof(ruleIndex) );
	}

	private Appearance Current => _record.StateRules[_ruleIndex()].Delta;

	private void Mutate( Func<Appearance, Appearance> f )
	{
		var i = _ruleIndex();
		var rule = _record.StateRules[i];
		_record.StateRules[i] = rule with { Delta = f( rule.Delta ) };
		Changed?.Invoke();
	}


	[Group( "Typography" )] [Title( "Override" )]
	[Description( "Emit per-control typography rules. Off = inherit from theme." )]
	public bool OverrideTypography { get => Current.OverrideTypography; set => Mutate( a => a with { OverrideTypography = value } ); }

	[Group( "Typography" )] [Title( "Font" )]
	[Description( "Font family (e.g. Poppins, Roboto Mono). Empty = inherit family from theme." )]
	public string FontFamily { get => Current.FontFamily; set => Mutate( a => a with { FontFamily = value } ); }

	[Group( "Typography" )] [Title( "Size" )]
	[Description( "Font size." )]
	public Length FontSize { get => Current.FontSize; set => Mutate( a => a with { FontSize = value } ); }

	[Group( "Typography" )] [Title( "Weight" )]
	[Description( "CSS font-weight: 100-900 (common: 400 normal, 600 semibold, 700 bold)." )]
	public int FontWeight { get => Current.FontWeight; set => Mutate( a => a with { FontWeight = value } ); }

	[Group( "Typography" )] [Title( "Color" )]
	[Description( "Color of the control's content text. On TextEntry this is the entered text only — placeholder hint stays muted via the theme's .textentry .placeholder rule." )]
	public Color Color { get => Current.Color; set => Mutate( a => a with { Color = value } ); }

	[Group( "Typography" )] [Title( "Align" )]
	[Description( "Horizontal text alignment within the label." )]
	public TextAlignment TextAlign { get => Current.TextAlign; set => Mutate( a => a with { TextAlign = value } ); }

	[Group( "Typography" )] [Title( "Italic" )]
	[Description( "Render the text in italic (font-style: italic)." )]
	public bool FontStyleItalic { get => Current.FontStyleItalic; set => Mutate( a => a with { FontStyleItalic = value } ); }

	[Group( "Typography" )] [Title( "Transform" )]
	[Description( "CSS text-transform — uppercase, lowercase, capitalize, or none." )]
	public TextTransformKind TextTransform { get => Current.TextTransform; set => Mutate( a => a with { TextTransform = value } ); }

	[Group( "Typography" )] [Title( "Letter Spacing" )]
	[Description( "Extra space between characters. Auto = inherit." )]
	public Length LetterSpacing { get => Current.LetterSpacing; set => Mutate( a => a with { LetterSpacing = value } ); }

	[Group( "Typography" )] [Title( "Line Height" )]
	[Description( "Line box height. Auto = inherit. Use em or % — the engine treats a bare number as px." )]
	public Length LineHeight { get => Current.LineHeight; set => Mutate( a => a with { LineHeight = value } ); }


	[Group( "Background" )] [Title( "Override" )]
	[Description( "Emit per-control background rules. Off = inherit from theme." )]
	public bool OverrideBackground { get => Current.OverrideBackground; set => Mutate( a => a with { OverrideBackground = value } ); }

	[Group( "Background" )] [Title( "Color" )]
	[Description( "Background color." )]
	public Color BackgroundColor { get => Current.BackgroundColor; set => Mutate( a => a with { BackgroundColor = value } ); }

	[Group( "Background" )] [Title( "Image" )]
	[Description( "Background image: url('asset.png'), linear-gradient(to bottom, #fff, #000), or empty. Use the button to pick an image asset." )]
	[BackgroundImagePicker]
	public string BackgroundImage { get => Current.BackgroundImage; set => Mutate( a => a with { BackgroundImage = value } ); }

	[Group( "Background" )] [Title( "Size" )]
	[Description( "CSS background-size: 'cover', 'contain', or 1–2 lengths (e.g. '100% 100%', '200px 100px'). Empty = the texture's native pixel size (which visibly rescales as you zoom the canvas)." )]
	public string BackgroundSize { get => Current.BackgroundSize; set => Mutate( a => a with { BackgroundSize = value } ); }

	[Group( "Background" )] [Title( "Position" )]
	[Description( "CSS background-position: X then optional Y. 'center' = 50%. e.g. 'center', '50% 100%', '10px 20px'. Empty = top-left." )]
	public string BackgroundPosition { get => Current.BackgroundPosition; set => Mutate( a => a with { BackgroundPosition = value } ); }

	[Group( "Background" )] [Title( "Repeat" )]
	[Description( "CSS background-repeat: 'no-repeat', 'repeat', 'repeat-x', 'repeat-y'. Empty = 'repeat' (a bare url() tiles by default — usually you want 'no-repeat')." )]
	public string BackgroundRepeat { get => Current.BackgroundRepeat; set => Mutate( a => a with { BackgroundRepeat = value } ); }


	[Group( "Border" )] [Title( "Override" )]
	[Description( "Emit per-control border rules. Off = inherit from theme." )]
	public bool OverrideBorder { get => Current.OverrideBorder; set => Mutate( a => a with { OverrideBorder = value } ); }

	[Group( "Border" )] [Title( "Radius" )]
	[Description( "Corner radius (applies to all four corners)." )]
	public Length BorderRadius { get => Current.BorderRadius; set => Mutate( a => a with { BorderRadius = value } ); }

	[Group( "Border" )] [Title( "Color" )]
	[Description( "Border color." )]
	public Color BorderColor { get => Current.BorderColor; set => Mutate( a => a with { BorderColor = value } ); }

	[Group( "Border" )] [Title( "Width" )]
	[Description( "Border thickness." )]
	public Length BorderWidth { get => Current.BorderWidth; set => Mutate( a => a with { BorderWidth = value } ); }


	[Group( "Effects" )] [Title( "Override" )]
	[Description( "Emit per-control box-shadow. Off = inherit from theme." )]
	public bool OverrideEffects { get => Current.OverrideEffects; set => Mutate( a => a with { OverrideEffects = value } ); }

	[Group( "Effects" )] [Title( "Shadow X" )]
	[Description( "Horizontal offset of the box-shadow." )]
	public Length BoxShadowX { get => Current.BoxShadowX; set => Mutate( a => a with { BoxShadowX = value } ); }

	[Group( "Effects" )] [Title( "Shadow Y" )]
	[Description( "Vertical offset of the box-shadow." )]
	public Length BoxShadowY { get => Current.BoxShadowY; set => Mutate( a => a with { BoxShadowY = value } ); }

	[Group( "Effects" )] [Title( "Shadow Blur" )]
	[Description( "Blur radius of the box-shadow." )]
	public Length BoxShadowBlur { get => Current.BoxShadowBlur; set => Mutate( a => a with { BoxShadowBlur = value } ); }

	[Group( "Effects" )] [Title( "Shadow Color" )]
	[Description( "Box-shadow color." )]
	public Color BoxShadowColor { get => Current.BoxShadowColor; set => Mutate( a => a with { BoxShadowColor = value } ); }

	[Group( "Effects" )] [Title( "Shadow Inset" )]
	[Description( "Render the shadow inside the element instead of outside." )]
	public bool BoxShadowInset { get => Current.BoxShadowInset; set => Mutate( a => a with { BoxShadowInset = value } ); }

	[Group( "Effects" )] [Title( "Opacity" )]
	[Description( "0 = transparent, 1 = fully opaque." )] [Range( 0, 1 )] [Step( 0.01f )]
	public float Opacity { get => Current.Opacity; set => Mutate( a => a with { Opacity = value } ); }


	[Group( "Constraints" )] [Title( "Override" )]
	[Description( "Emit per-control margin / size constraint rules." )]
	public bool OverrideConstraints { get => Current.OverrideConstraints; set => Mutate( a => a with { OverrideConstraints = value } ); }

	[Group( "Constraints" )] [Title( "Margin" )]
	[Description( "Space between this control's edge and its siblings (per-side: top, right, bottom, left)." )]
	public Edges Margin { get => Current.Margin; set => Mutate( a => a with { Margin = value } ); }

	[Group( "Constraints" )] [Title( "Min Width" )]
	[Description( "Floor on this control's width." )]
	public Length MinWidth { get => Current.MinWidth; set => Mutate( a => a with { MinWidth = value } ); }

	[Group( "Constraints" )] [Title( "Max Width" )]
	[Description( "Ceiling on this control's width." )]
	public Length MaxWidth { get => Current.MaxWidth; set => Mutate( a => a with { MaxWidth = value } ); }

	[Group( "Constraints" )] [Title( "Min Height" )]
	[Description( "Floor on this control's height." )]
	public Length MinHeight { get => Current.MinHeight; set => Mutate( a => a with { MinHeight = value } ); }

	[Group( "Constraints" )] [Title( "Max Height" )]
	[Description( "Ceiling on this control's height." )]
	public Length MaxHeight { get => Current.MaxHeight; set => Mutate( a => a with { MaxHeight = value } ); }


	[Group( "Interaction" )] [Title( "Override" )]
	[Description( "Emit per-control cursor / overflow rules." )]
	public bool OverrideInteraction { get => Current.OverrideInteraction; set => Mutate( a => a with { OverrideInteraction = value } ); }

	[Group( "Interaction" )] [Title( "Cursor" )]
	[Description( "Mouse cursor when hovering this control." )]
	public CursorKind Cursor { get => Current.Cursor; set => Mutate( a => a with { Cursor = value } ); }

	[Group( "Interaction" )] [Title( "Overflow" )]
	[Description( "How to handle children that overflow this control's box." )]
	public OverflowKind Overflow { get => Current.Overflow; set => Mutate( a => a with { Overflow = value } ); }

	[Group( "Interaction" )] [Title( "Z-Index" )]
	[Description( "Stacking order among siblings. 0 = default (no explicit stacking)." )]
	public int ZIndex { get => Current.ZIndex; set => Mutate( a => a with { ZIndex = value } ); }

	[Group( "Interaction" )] [Title( "Pointer Events" )]
	[Description( "When off, this control is transparent to the mouse (pointer-events: none)." )]
	public bool PointerEvents { get => Current.PointerEvents; set => Mutate( a => a with { PointerEvents = value } ); }
}