From ccf9bd0c0c75f9193886491fc0033a5c17f173d4 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 14 Sep 2026 14:55:03 +0200 Subject: [PATCH] Show an unkeyed struct's fields or its properties, never both Unity's Quaternion showed eulerAngles beside x/y/z/w, Rect about eleven rows over four values, and Bounds center/extents/size/min/max: settable properties alias the fields they wrap. A struct with public fields now shows only those; a struct without shows its settable properties, so record structs and CultMath matrices (properties over private rows) still get clean rows and vectors show fields only. Co-Authored-By: Claude Opus 5 --- src/GameCult.Caching/CultInspectorModel.cs | 19 +++++++++++-------- src/GameCult.Unity/Assets/Caching/README.md | 12 +++++++----- .../CultInspectorModelTests.cs | 11 +++++++++-- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/src/GameCult.Caching/CultInspectorModel.cs b/src/GameCult.Caching/CultInspectorModel.cs index 7b6eaf84..49a2da17 100644 --- a/src/GameCult.Caching/CultInspectorModel.cs +++ b/src/GameCult.Caching/CultInspectorModel.cs @@ -552,16 +552,19 @@ namespace GameCult.Caching var keyed = KeyedMembers(type); if (keyed.Length > 0) return new CultInspectorShape(type, CultInspectorValueKind.Nested) { Members = keyed }; - // An unkeyed struct is edited in place: its public fields, readonly ones read-only, then its public properties with - // a setter, init-only ones read-only. Get-only properties are computed (a vector's swizzles) or kept by the - // in-place edit, so they are not rows. + // An unkeyed struct is edited in place. With public fields it shows only them, readonly ones read-only: its + // properties alias those values (a Quaternion's eulerAngles, a Rect's min and max). Without, it shows its public + // properties with a setter, init-only ones read-only (record structs, matrices over private rows). Get-only + // properties are computed or kept by the in-place edit, so they are never rows. if (type.IsValueType) { - var members = type.GetFields(Public).OrderBy(field => field.MetadataToken) - .Select(field => (Member: (MemberInfo)field, Type: field.FieldType, Assignable: !field.IsInitOnly)) - .Concat(type.GetProperties(Public).OrderBy(property => property.MetadataToken) - .Where(property => property.GetIndexParameters().Length == 0 && property.GetGetMethod() != null && property.GetSetMethod() != null) - .Select(property => (Member: (MemberInfo)property, Type: property.PropertyType, Assignable: !IsInitOnly(property.GetSetMethod()!)))) + var fields = type.GetFields(Public); + var members = (fields.Length > 0 + ? fields.OrderBy(field => field.MetadataToken) + .Select(field => (Member: (MemberInfo)field, Type: field.FieldType, Assignable: !field.IsInitOnly)) + : type.GetProperties(Public).OrderBy(property => property.MetadataToken) + .Where(property => property.GetIndexParameters().Length == 0 && property.GetGetMethod() != null && property.GetSetMethod() != null) + .Select(property => (Member: (MemberInfo)property, Type: property.PropertyType, Assignable: !IsInitOnly(property.GetSetMethod()!)))) .Select((member, slot) => new CultInspectorMember(member.Member, member.Type, slot, member.Assignable, MetadataOf(member.Member))) .ToArray(); if (members.Length > 0) diff --git a/src/GameCult.Unity/Assets/Caching/README.md b/src/GameCult.Unity/Assets/Caching/README.md index 2d501552..a5ea3d92 100644 --- a/src/GameCult.Unity/Assets/Caching/README.md +++ b/src/GameCult.Unity/Assets/Caching/README.md @@ -45,7 +45,7 @@ catalog slots, `CultInspector*` metadata, whether a member is assignable), the shape of any value (string, integer, float, bool, enum, `CultRecordRef` with its candidate records, rank-1 list or array, dictionary, `[Union]` with only its declared subtypes, nested keyed object, struct edited in place through its -public fields and settable properties, or unsupported with a reason), drawer claim resolution +public fields, or its settable properties when it has none, or unsupported with a reason), drawer claim resolution (`CultInspectorDrawerClaims`), and the edit decisions: the key a dictionary entry keeps and the notice when a key is refused (null, an empty record reference, a duplicate: serialized the same as another key under the owning @@ -59,10 +59,12 @@ renderer and drawers to show and edit the same documents under the same rules. ## Drawers -Built in: every model shape above. A struct without `[Key]` members folds out -to its public fields and its public properties with a setter, and is edited in -place; readonly fields and `init` properties show read-only, and get-only -properties are not shown (record structs work as written). Unity object +Built in: every model shape above. A struct without `[Key]` members is edited +in place. With public fields it folds out to those fields only (readonly ones +read-only), so aliasing properties such as `Quaternion.eulerAngles` or +`Rect.min` add no rows; without public fields it folds out to its public +properties with a setter, `init` ones read-only (record structs and CultMath +matrices work as written). Get-only properties are never shown. Unity object references draw as object fields. A value no drawer claims and no shape covers shows a red error row with the model's reason, as does a multi-dimensional array. diff --git a/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs b/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs index f30b63fe..c5fd8cb1 100644 --- a/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs +++ b/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs @@ -255,8 +255,10 @@ namespace GameCult.Caching.Tests Assert.That(open.Members.Select(member => member.Name), Is.EqualTo(new[] { "x", "y" })); Assert.That(open.Members.All(member => member.IsAssignable), Is.True); (string, bool)[] Rows(Type type) => model.ShapeOf(type).Members.Select(member => (member.Name, member.IsReadOnly)).ToArray(); - Assert.That(Rows(typeof(InspectMixedStruct)), Is.EqualTo(new[] { ("x", false), ("y", true), ("Settable", false), ("Initable", true) }), - "fields, then settable properties; readonly and init are read-only; get-only is not a row"); + Assert.That(Rows(typeof(InspectMixedStruct)), Is.EqualTo(new[] { ("x", false), ("y", true) }), + "a struct with public fields shows only them; its settable properties are aliases, not rows"); + Assert.That(Rows(typeof(InspectPropertyStruct)), Is.EqualTo(new[] { ("Settable", false), ("Initable", true) }), + "without public fields, settable properties are rows; init is read-only; get-only is not a row"); Assert.That(Rows(typeof(InspectRecordStruct)), Is.EqualTo(new[] { ("A", false), ("B", false) })); Assert.That(Rows(typeof(InspectReadonlyRecordStruct)), Is.EqualTo(new[] { ("A", true), ("B", true) })); object boxed = new InspectRecordStruct(1, 2f); @@ -372,6 +374,11 @@ namespace GameCult.Caching.Tests { public float x; public readonly float y; + public float Alias { get => x; set => x = value; } + } + + public struct InspectPropertyStruct + { public int Settable { get; set; } public int Initable { get; init; } public int Computed => Settable + 1;