From 8ec18b26819fe1c68e5683ed9140d6f61dcf5e6e Mon Sep 17 00:00:00 2001 From: andreakarasho Date: Wed, 20 May 2026 21:25:40 +0200 Subject: [PATCH] feat(elements): make ElementDeclaration fields nullable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Optional opt-in via field presence — declaring a field is enough. Drops `HasImage`/`HasShadow`/`HasBorder`/`HasCustomData`/`IsFloating`/ `IsScrollable` sentinel checks in ConfigureOpenElement; checks for `is not null` instead. Layout falls back to default when null. Id stays required. ClayUI widgets that previously assigned `Shadow = s.Shadow` / `Border = s.Border` unconditionally now guard with `HasShadow` / `HasBorder` so empty styles map to null. Avoids spurious Shadow render commands (the renderer emits unconditionally on config presence) and saves storage for empty Border configs. Adds OptionalElementDeclarationTests (23 cases) covering null-by- default, declaration-is-enough (empty Custom/Shadow/Scroll), and regression for factory methods + existing styled paths. --- src/Clay.Test/ConfigTests.cs | 6 +- .../OptionalElementDeclarationTests.cs | 401 ++++++++++++++++++ src/Clay/ClayUI.cs | 40 +- src/Clay/Core/ClayContext.cs | 43 +- src/Clay/Elements/ElementDeclaration.cs | 24 +- 5 files changed, 458 insertions(+), 56 deletions(-) create mode 100644 src/Clay.Test/OptionalElementDeclarationTests.cs diff --git a/src/Clay.Test/ConfigTests.cs b/src/Clay.Test/ConfigTests.cs index 30eb3e2..1663fb3 100644 --- a/src/Clay.Test/ConfigTests.cs +++ b/src/Clay.Test/ConfigTests.cs @@ -206,21 +206,21 @@ public void Container_SetsLayout() { var layout = LayoutConfig.FillRow(gap: 8); var decl = ElementDeclaration.Container(layout); - Assert.Equal(8, decl.Layout.ChildGap); + Assert.Equal(8, decl.Layout!.Value.ChildGap); } [Fact] public void Box_SetsLayoutAndColor() { var decl = ElementDeclaration.Box(LayoutConfig.FillRow(), Color.Red); - Assert.Equal(Color.Red.R, decl.BackgroundColor.R); + Assert.Equal(Color.Red.R, decl.BackgroundColor!.Value.R); } [Fact] public void RoundedBox_SetsCornerRadius() { var decl = ElementDeclaration.RoundedBox(LayoutConfig.FillRow(), Color.Red, 8f); - Assert.True(decl.CornerRadius.HasRadius); + Assert.True(decl.CornerRadius!.Value.HasRadius); } } diff --git a/src/Clay.Test/OptionalElementDeclarationTests.cs b/src/Clay.Test/OptionalElementDeclarationTests.cs new file mode 100644 index 0000000..2ea9709 --- /dev/null +++ b/src/Clay.Test/OptionalElementDeclarationTests.cs @@ -0,0 +1,401 @@ +using Clay; + +namespace Clay.Test; + +/// +/// Tests for the nullable ElementDeclaration field contract. +/// Declaring a config field (even default) opts the element into that config. +/// Leaving the field null means the config is not emitted. +/// Id remains required. +/// +public class OptionalElementDeclarationTests : IDisposable +{ + private readonly ClayFixture _fixture; + + public OptionalElementDeclarationTests() => _fixture = new ClayFixture(); + + public void Dispose() => _fixture.Dispose(); + + private static int CountCommands(ReadOnlySpan commands, RenderCommandType type) + { + int n = 0; + foreach (var c in commands) + if (c.CommandType == type) n++; + return n; + } + + // ============ Null-by-default contract ============ + + [Fact] + public void OnlyId_NoLayout_DoesNotCrash() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration { Id = ClayApi.Id("only-id") })) { } + var commands = ClayApi.EndLayout(); + Assert.Equal(0, CountCommands(commands, RenderCommandType.Rectangle)); + Assert.Equal(0, CountCommands(commands, RenderCommandType.Border)); + Assert.Equal(0, CountCommands(commands, RenderCommandType.Shadow)); + Assert.Equal(0, CountCommands(commands, RenderCommandType.Custom)); + Assert.Equal(0, CountCommands(commands, RenderCommandType.Image)); + } + + [Fact] + public void NullBackgroundColor_DoesNotEmitRectangle() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("no-bg"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) } + })) + { } + var commands = ClayApi.EndLayout(); + Assert.Equal(0, CountCommands(commands, RenderCommandType.Rectangle)); + } + + [Fact] + public void NullScroll_DoesNotRegisterScrollContainer() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("no-scroll"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(100, 100) } + })) + { } + ClayApi.EndLayout(); + Assert.Equal(0, ClayApi.GetScrollContainerCount()); + } + + // ============ Declaration-is-enough contract ============ + + [Fact] + public void EmptyCustomConfig_EmitsCustomCommand() + { + // Regression for sentinel removal: assigning a default-constructed CustomConfig + // (CustomData == null) MUST still emit a Custom render command. + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("custom-empty"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + Custom = new CustomConfig() + })) + { } + var commands = ClayApi.EndLayout(); + Assert.Equal(1, CountCommands(commands, RenderCommandType.Custom)); + } + + [Fact] + public void EmptyShadowConfig_EmitsShadowCommand() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("shadow-empty"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + Shadow = new ShadowConfig() + })) + { } + var commands = ClayApi.EndLayout(); + Assert.Equal(1, CountCommands(commands, RenderCommandType.Shadow)); + } + + [Fact] + public void EmptyBorderConfig_DoesNotCrash() + { + // BorderConfig with zero width is registered as a config (sentinel removed) + // but the visual border command is skipped downstream when Width.HasBorder is false. + // Confirms the declaration is accepted without throwing. + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("border-empty"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + Border = new BorderConfig() + })) + { } + var commands = ClayApi.EndLayout(); + Assert.Equal(0, CountCommands(commands, RenderCommandType.Border)); + } + + [Fact] + public void EmptyScrollConfig_RegistersScrollContainer() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("scroll-empty"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(100, 100) }, + Scroll = new ScrollConfig() + })) + { } + ClayApi.EndLayout(); + Assert.True(ClayApi.GetScrollContainerCount() > 0); + } + + [Fact] + public void AssignedBackgroundColor_EmitsRectangle() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("bg"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + BackgroundColor = Color.Red + })) + { } + var commands = ClayApi.EndLayout(); + Assert.Equal(1, CountCommands(commands, RenderCommandType.Rectangle)); + } + + [Fact] + public void TransparentBackgroundColor_DoesNotCrash() + { + // Declaration accepts a transparent color; downstream rectangle emission + // still skips fully-invisible fills as an optimization. + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("bg-transparent"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + BackgroundColor = new Color(0, 0, 0, 0) + })) + { } + var commands = ClayApi.EndLayout(); + Assert.Equal(0, CountCommands(commands, RenderCommandType.Rectangle)); + } + + // ============ Regression: existing behavior preserved ============ + + [Fact] + public void Regression_BackgroundColor_StillEmitsRectangle() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("reg-bg"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(100, 100) }, + BackgroundColor = Color.Blue + })) + { } + var commands = ClayApi.EndLayout(); + Assert.Equal(1, CountCommands(commands, RenderCommandType.Rectangle)); + } + + [Fact] + public void Regression_ImageConfig_WithData_EmitsImage() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("reg-image"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(64, 64) }, + Image = ImageConfig.Create(new object(), 64, 64) + })) + { } + var commands = ClayApi.EndLayout(); + Assert.Equal(1, CountCommands(commands, RenderCommandType.Image)); + } + + [Fact] + public void Regression_ShadowDrop_EmitsShadow() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("reg-shadow"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + Shadow = ShadowConfig.Drop(2, 2, 4, Color.Black) + })) + { } + var commands = ClayApi.EndLayout(); + Assert.Equal(1, CountCommands(commands, RenderCommandType.Shadow)); + } + + [Fact] + public void Regression_BorderUniform_EmitsBorder() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("reg-border"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + Border = BorderConfig.Uniform(2, Color.White) + })) + { } + var commands = ClayApi.EndLayout(); + Assert.Equal(1, CountCommands(commands, RenderCommandType.Border)); + } + + [Fact] + public void Regression_ScrollVertical_RegistersScrollContainer() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("reg-scroll"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(100, 100) }, + Scroll = ScrollConfig.VerticalScroll + })) + { + using (ClayApi.Element(new ElementDeclaration + { + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(100, 500) }, + BackgroundColor = Color.Red + })) + { } + } + ClayApi.EndLayout(); + Assert.True(ClayApi.GetScrollContainerCount() > 0); + } + + [Fact] + public void Regression_ContainerFactory_StillBuilds() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(ElementDeclaration.Container(LayoutConfig.FillRow(gap: 4)))) { } + ClayApi.EndLayout(); + Assert.True(ClayApi.GetElementCount() > 0); + } + + [Fact] + public void Regression_BoxFactory_EmitsRectangle() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(ElementDeclaration.Box( + new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + Color.Green))) { } + var commands = ClayApi.EndLayout(); + Assert.Equal(1, CountCommands(commands, RenderCommandType.Rectangle)); + } + + [Fact] + public void Regression_RoundedBoxFactory_BuildsAndRenders() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(ElementDeclaration.RoundedBox( + new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + Color.Green, + 8f))) { } + var commands = ClayApi.EndLayout(); + Assert.Equal(1, CountCommands(commands, RenderCommandType.Rectangle)); + } + + [Fact] + public void Regression_FloatingConfig_RegistersTreeRoot() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("float-parent"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(200, 200) } + })) + { + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("float-child"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + Floating = new FloatingConfig { AttachTo = FloatingAttachTo.Parent } + })) + { } + } + ClayApi.EndLayout(); + // Root container + the floating element each get a tree root entry. + Assert.True(ClayApi.GetTreeRootCount() >= 2); + } + + [Fact] + public void NullFloating_DoesNotAddTreeRoot() + { + ClayApi.BeginLayout(); + int before = ClayApi.GetTreeRootCount(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("no-float"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) } + })) + { } + ClayApi.EndLayout(); + // Only the root container's tree root should exist (added during BeginLayout). + Assert.Equal(1, ClayApi.GetTreeRootCount() - before + 1); + } + + [Fact] + public void Regression_CornerRadius_AppliesToRectangle() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("rad"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + BackgroundColor = Color.Red, + CornerRadius = CornerRadius.All(6f) + })) + { } + var commands = ClayApi.EndLayout(); + Assert.Equal(1, CountCommands(commands, RenderCommandType.Rectangle)); + } + + [Fact] + public void UserData_AssignedOrNull_DoesNotCrash() + { + // UserData is now nint? — both null and an assigned value must be accepted by the layout pipeline. + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("ud-set"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + BackgroundColor = Color.Red, + UserData = (nint)0xABCD + })) + { } + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("ud-null"), + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + BackgroundColor = Color.Red + })) + { } + var commands = ClayApi.EndLayout(); + Assert.Equal(2, CountCommands(commands, RenderCommandType.Rectangle)); + } + + // ============ ClipContent / Scroll clip stack ============ + + [Fact] + public void NullLayout_DoesNotPushClipStack() + { + // Was previously checking layout.ClipContent — must tolerate null Layout. + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration { Id = ClayApi.Id("no-layout-clip") })) { } + var commands = ClayApi.EndLayout(); + Assert.Equal(0, CountCommands(commands, RenderCommandType.ScissorStart)); + } + + [Fact] + public void Regression_ClipContent_EmitsScissor() + { + ClayApi.BeginLayout(); + using (ClayApi.Element(new ElementDeclaration + { + Id = ClayApi.Id("clip"), + Layout = new LayoutConfig + { + Sizing = Sizing.FixedSize(100, 100), + ClipContent = true + } + })) + { + using (ClayApi.Element(new ElementDeclaration + { + Layout = new LayoutConfig { Sizing = Sizing.FixedSize(50, 50) }, + BackgroundColor = Color.Red + })) + { } + } + var commands = ClayApi.EndLayout(); + Assert.True(CountCommands(commands, RenderCommandType.ScissorStart) >= 1); + Assert.True(CountCommands(commands, RenderCommandType.ScissorEnd) >= 1); + } +} diff --git a/src/Clay/ClayUI.cs b/src/Clay/ClayUI.cs index 0cbf3e7..61afcc3 100644 --- a/src/Clay/ClayUI.cs +++ b/src/Clay/ClayUI.cs @@ -1039,7 +1039,7 @@ public static void Image(object imageData, float width, float height, ImageStyle }, Image = ImageConfig.Create(imageData, width, height), CornerRadius = s.CornerRadius, - Border = s.Border + Border = s.Border.HasBorder ? s.Border : null })) { } } @@ -1711,8 +1711,8 @@ public static void BeginHorizontal(ushort gap = 8, ChildAlignment alignment = de }, BackgroundColor = hasStyle ? s.BackgroundColor : default, CornerRadius = hasStyle ? s.CornerRadius : default, - Border = hasStyle ? s.Border : default, - Shadow = hasStyle ? s.Shadow : default + Border = hasStyle && s.Border.HasBorder ? s.Border : null, + Shadow = hasStyle && s.Shadow.HasShadow ? s.Shadow : null }); _context.LayoutDepth++; @@ -1756,8 +1756,8 @@ public static void BeginHorizontal(ushort gap = 8, ChildAlignment alignment = de }, BackgroundColor = hasStyle ? s.BackgroundColor : default, CornerRadius = hasStyle ? s.CornerRadius : default, - Border = hasStyle ? s.Border : default, - Shadow = hasStyle ? s.Shadow : default + Border = hasStyle && s.Border.HasBorder ? s.Border : null, + Shadow = hasStyle && s.Shadow.HasShadow ? s.Shadow : null }); _context.LayoutDepth++; } @@ -1827,8 +1827,8 @@ public static void BeginVertical(ushort gap = 8, ChildAlignment alignment = defa }, BackgroundColor = hasStyle ? s.BackgroundColor : default, CornerRadius = hasStyle ? s.CornerRadius : default, - Border = hasStyle ? s.Border : default, - Shadow = hasStyle ? s.Shadow : default + Border = hasStyle && s.Border.HasBorder ? s.Border : null, + Shadow = hasStyle && s.Shadow.HasShadow ? s.Shadow : null }); _context.LayoutDepth++; @@ -1872,8 +1872,8 @@ public static void BeginVertical(ushort gap = 8, ChildAlignment alignment = defa }, BackgroundColor = hasStyle ? s.BackgroundColor : default, CornerRadius = hasStyle ? s.CornerRadius : default, - Border = hasStyle ? s.Border : default, - Shadow = hasStyle ? s.Shadow : default + Border = hasStyle && s.Border.HasBorder ? s.Border : null, + Shadow = hasStyle && s.Shadow.HasShadow ? s.Shadow : null }); _context.LayoutDepth++; } @@ -1948,7 +1948,7 @@ public static void BeginPanel(string title, PanelStyle? style = null, bool scrol CornerRadius = sk.Background.HasImage ? CornerRadius.Zero : s.CornerRadius, Border = sk.Background.HasImage ? default : s.Border, Image = sk.Background.HasImage ? SkinToImageConfig(sk.Background) : default, - Shadow = s.Shadow + Shadow = s.Shadow.HasShadow ? s.Shadow : null }); _context.PanelDepth++; @@ -2042,7 +2042,7 @@ public static void BeginPanel(string title, PanelStyle? style = null, bool scrol CornerRadius = sk.Background.HasImage ? CornerRadius.Zero : s.CornerRadius, Border = sk.Background.HasImage ? default : s.Border, Image = sk.Background.HasImage ? SkinToImageConfig(sk.Background) : default, - Shadow = s.Shadow + Shadow = s.Shadow.HasShadow ? s.Shadow : null }); _context.PanelDepth++; @@ -2447,7 +2447,7 @@ public static bool BeginWindow(string title, ref bool open, WindowStyle? style = CornerRadius = sk.Body.HasImage ? CornerRadius.Zero : s.CornerRadius, Border = sk.Body.HasImage ? default : s.Border, Image = sk.Body.HasImage ? SkinToImageConfig(sk.Body) : default, - Shadow = s.Shadow + Shadow = s.Shadow.HasShadow ? s.Shadow : null }); // Title bar @@ -3638,7 +3638,7 @@ public static void Tooltip(string text, TooltipStyle? style = null) }, BackgroundColor = s.BackgroundColor, CornerRadius = s.CornerRadius, - Border = s.Border, + Border = s.Border.HasBorder ? s.Border : null, Floating = new FloatingConfig { AttachTo = FloatingAttachTo.Root, @@ -3729,7 +3729,7 @@ public static bool BeginTooltip(TooltipStyle? style = null) }, BackgroundColor = s.BackgroundColor, CornerRadius = s.CornerRadius, - Border = s.Border, + Border = s.Border.HasBorder ? s.Border : null, Floating = new FloatingConfig { AttachTo = FloatingAttachTo.Root, @@ -3909,7 +3909,7 @@ public static bool BeginPopup(string id, PopupStyle? style = null) }, BackgroundColor = s.BackgroundColor, CornerRadius = s.CornerRadius, - Border = s.Border, + Border = s.Border.HasBorder ? s.Border : null, Floating = new FloatingConfig { AttachTo = FloatingAttachTo.Root, @@ -4473,7 +4473,7 @@ public static bool BeginPopupModal(string id, ModalStyle? style = null) }, BackgroundColor = s.BackgroundColor, CornerRadius = s.CornerRadius, - Border = s.Border, + Border = s.Border.HasBorder ? s.Border : null, Floating = new FloatingConfig { AttachTo = FloatingAttachTo.Root, @@ -5034,7 +5034,7 @@ public static void BeginScrollArea(string id, float? maxHeight = null, bool hori }, BackgroundColor = s.BackgroundColor, CornerRadius = s.CornerRadius, - Shadow = s.Shadow + Shadow = s.Shadow.HasShadow ? s.Shadow : null }); _context.LayoutDepth++; @@ -5083,7 +5083,7 @@ public static void BeginScrollArea(string id, float? maxHeight = null, bool hori }, BackgroundColor = s.BackgroundColor, CornerRadius = s.CornerRadius, - Shadow = s.Shadow + Shadow = s.Shadow.HasShadow ? s.Shadow : null }); _context.LayoutDepth++; @@ -5210,7 +5210,7 @@ public static void BeginListBox(string label, float maxHeight = 150, ListBoxStyl }, BackgroundColor = s.BackgroundColor, CornerRadius = s.CornerRadius, - Border = s.Border + Border = s.Border.HasBorder ? s.Border : null }); _context.LayoutDepth++; _context.LayoutScrollInfo.Push(new ClayUIContext.ScrollWrapperInfo(scrollId, IsVertical: true, HasWrapper: true)); @@ -5365,7 +5365,7 @@ public static bool Combo(string label, ref int selectedIndex, string[] options, }, BackgroundColor = isHovered ? s.HoverColor : s.BackgroundColor, CornerRadius = s.CornerRadius, - Border = s.Border + Border = s.Border.HasBorder ? s.Border : null })) { // Selected text diff --git a/src/Clay/Core/ClayContext.cs b/src/Clay/Core/ClayContext.cs index 27d1a7e..ffe1d63 100644 --- a/src/Clay/Core/ClayContext.cs +++ b/src/Clay/Core/ClayContext.cs @@ -251,13 +251,14 @@ public void ConfigureOpenElement(ElementDeclaration declaration) ref var openLayoutElement = ref GetOpenLayoutElement(); - // Store layout config - int layoutConfigIndex = LayoutConfigs.Add(declaration.Layout); + // Store layout config (default when not provided) + var layout = declaration.Layout ?? default; + int layoutConfigIndex = LayoutConfigs.Add(layout); openLayoutElement.LayoutConfigIndex = layoutConfigIndex; // Validate percent sizing - if ((declaration.Layout.Sizing.Width.Type == SizingType.Percent && declaration.Layout.Sizing.Width.Percent > 1) || - (declaration.Layout.Sizing.Height.Type == SizingType.Percent && declaration.Layout.Sizing.Height.Percent > 1)) + if ((layout.Sizing.Width.Type == SizingType.Percent && layout.Sizing.Width.Percent > 1) || + (layout.Sizing.Height.Type == SizingType.Percent && layout.Sizing.Height.Percent > 1)) { OnError?.Invoke(new ErrorData { @@ -270,30 +271,30 @@ public void ConfigureOpenElement(ElementDeclaration declaration) openLayoutElement.ElementConfigsStart = ElementConfigs.Length; // Process shared config (background color, corner radius, user data) - if (declaration.BackgroundColor.A > 0 || declaration.CornerRadius.HasRadius || declaration.UserData != 0) + if (declaration.BackgroundColor is not null || declaration.CornerRadius is not null || declaration.UserData is not null) { int sharedIndex = SharedElementConfigs.Add(new SharedElementConfig { - BackgroundColor = declaration.BackgroundColor, - CornerRadius = declaration.CornerRadius, - UserData = declaration.UserData + BackgroundColor = declaration.BackgroundColor ?? default, + CornerRadius = declaration.CornerRadius ?? default, + UserData = declaration.UserData ?? 0 }); ElementConfigs.Add(new ElementConfig { Type = ElementConfigType.Shared, ConfigIndex = sharedIndex }); openLayoutElement.ElementConfigsLength++; } // Process image config - if (declaration.Image.HasImage) + if (declaration.Image is not null) { - int imageIndex = ImageElementConfigs.Add(declaration.Image); + int imageIndex = ImageElementConfigs.Add(declaration.Image.Value); ElementConfigs.Add(new ElementConfig { Type = ElementConfigType.Image, ConfigIndex = imageIndex }); openLayoutElement.ElementConfigsLength++; } // Process floating config - if (declaration.Floating.IsFloating) + if (declaration.Floating is not null) { - var floatingConfig = declaration.Floating; + var floatingConfig = declaration.Floating.Value; if (OpenLayoutElementStack.Length >= 2) { @@ -335,9 +336,9 @@ public void ConfigureOpenElement(ElementDeclaration declaration) } // Process custom config - if (declaration.Custom.HasCustomData) + if (declaration.Custom is not null) { - int customIndex = CustomElementConfigs.Add(declaration.Custom); + int customIndex = CustomElementConfigs.Add(declaration.Custom.Value); ElementConfigs.Add(new ElementConfig { Type = ElementConfigType.Custom, ConfigIndex = customIndex }); openLayoutElement.ElementConfigsLength++; } @@ -353,15 +354,15 @@ public void ConfigureOpenElement(ElementDeclaration declaration) } // Push to clip stack if this element clips children (one entry per element) - if (declaration.Layout.ClipContent || declaration.Scroll.IsScrollable) + if (layout.ClipContent || declaration.Scroll is not null) { OpenClipElementStack.Add((int)openLayoutElement.Id); } // Process scroll config - if (declaration.Scroll.IsScrollable) + if (declaration.Scroll is not null) { - int scrollIndex = ScrollElementConfigs.Add(declaration.Scroll); + int scrollIndex = ScrollElementConfigs.Add(declaration.Scroll.Value); ElementConfigs.Add(new ElementConfig { Type = ElementConfigType.Scroll, ConfigIndex = scrollIndex }); openLayoutElement.ElementConfigsLength++; @@ -392,17 +393,17 @@ public void ConfigureOpenElement(ElementDeclaration declaration) } // Process border config - if (declaration.Border.HasBorder) + if (declaration.Border is not null) { - int borderIndex = BorderElementConfigs.Add(declaration.Border); + int borderIndex = BorderElementConfigs.Add(declaration.Border.Value); ElementConfigs.Add(new ElementConfig { Type = ElementConfigType.Border, ConfigIndex = borderIndex }); openLayoutElement.ElementConfigsLength++; } // Process shadow config - if (declaration.Shadow.HasShadow) + if (declaration.Shadow is not null) { - int shadowIndex = ShadowElementConfigs.Add(declaration.Shadow); + int shadowIndex = ShadowElementConfigs.Add(declaration.Shadow.Value); ElementConfigs.Add(new ElementConfig { Type = ElementConfigType.Shadow, ConfigIndex = shadowIndex }); openLayoutElement.ElementConfigsLength++; } diff --git a/src/Clay/Elements/ElementDeclaration.cs b/src/Clay/Elements/ElementDeclaration.cs index 33997fc..7aceb2b 100644 --- a/src/Clay/Elements/ElementDeclaration.cs +++ b/src/Clay/Elements/ElementDeclaration.cs @@ -9,59 +9,59 @@ namespace Clay; public struct ElementDeclaration { /// - /// Unique identifier for this element. + /// Unique identifier for this element. Required. /// public ElementId Id; /// /// Layout configuration (sizing, padding, direction, etc.). /// - public LayoutConfig Layout; + public LayoutConfig? Layout; /// /// Background color of the element. /// - public Color BackgroundColor; + public Color? BackgroundColor; /// /// Corner radius for rounded corners. /// - public CornerRadius CornerRadius; + public CornerRadius? CornerRadius; /// /// Image configuration (if this is an image element). /// - public ImageConfig Image; + public ImageConfig? Image; /// /// Floating configuration (for overlay/popup elements). /// - public FloatingConfig Floating; + public FloatingConfig? Floating; /// /// Custom element configuration. /// - public CustomConfig Custom; + public CustomConfig? Custom; /// /// Scroll container configuration. /// - public ScrollConfig Scroll; + public ScrollConfig? Scroll; /// /// Border configuration. /// - public BorderConfig Border; + public BorderConfig? Border; /// /// Shadow configuration. /// - public ShadowConfig Shadow; + public ShadowConfig? Shadow; /// /// User-defined data (passed through to render commands). /// - public nint UserData; + public nint? UserData; /// /// Creates a basic container element. @@ -83,7 +83,7 @@ public static ElementDeclaration RoundedBox(LayoutConfig layout, Color backgroun { Layout = layout, BackgroundColor = backgroundColor, - CornerRadius = CornerRadius.All(cornerRadius) + CornerRadius = global::Clay.CornerRadius.All(cornerRadius) }; }