Give inspector drawers the record they sit in, read-only
A drawer saw only its value; Aetheria's schematic-shape drawer needs sibling members of the owning document (texture path, aspect, hull tints). The model owns the seam: CultInspectorEdit.Record is a second copy of the stored document as the edit began, cloned on first read and kept for the edit. The Studio lowers it as CultInspector.Record, so a runtime CultUI panel gets the same seam from the same edit. The Draw signature is unchanged, so existing drawers keep compiling. Why not hand drawers the edit copy the Studio already draws: that copy is what Commit upserts, so a write through it would be saved without passing through the returned value, GUI.changed, or read-only metadata. The record copy is read by nothing, so writing to it saves nothing and changes neither the edit nor the cache. It does not show edits other drawers made earlier in the same frame. The Studio's separate document-type field collapses into the held edit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
452f928bb7
commit
90e7dbee27
4 changed files with 35 additions and 7 deletions
|
|
@ -238,14 +238,24 @@ namespace GameCult.Caching
|
|||
// A private copy of one stored document. A lowering draws into Document; the cached object is never touched.
|
||||
public sealed class CultInspectorEdit
|
||||
{
|
||||
internal CultInspectorEdit(CultStoredDocument source, object document)
|
||||
private readonly Func<object> _snapshot;
|
||||
private object? _record;
|
||||
|
||||
internal CultInspectorEdit(CultStoredDocument source, object document, Func<object> snapshot)
|
||||
{
|
||||
Source = source;
|
||||
Document = document;
|
||||
_snapshot = snapshot;
|
||||
}
|
||||
|
||||
public CultStoredDocument Source { get; }
|
||||
public object Document { get; }
|
||||
|
||||
// The record being drawn, as stored when this edit began, for drawers that read the document they sit in. It is a
|
||||
// second copy, made on first read: not the cached object, and not Document, which a lowering commits. Nothing
|
||||
// reads it back, so writing to it saves nothing and changes neither the edit nor the cache.
|
||||
public object Record => _record ??= _snapshot();
|
||||
|
||||
public bool IsSpent { get; private set; }
|
||||
|
||||
public bool IsFor(CultStoredDocument record) => !IsSpent && ReferenceEquals(Source, record);
|
||||
|
|
@ -342,7 +352,8 @@ namespace GameCult.Caching
|
|||
public CultInspectorEdit BeginEdit(CultStoredDocument record)
|
||||
{
|
||||
if (record == null) throw new ArgumentNullException(nameof(record));
|
||||
return new CultInspectorEdit(record, Clone(record.Document, record.Descriptor.DocumentType));
|
||||
var type = record.Descriptor.DocumentType;
|
||||
return new CultInspectorEdit(record, Clone(record.Document, type), () => Clone(record.Document, type));
|
||||
}
|
||||
|
||||
// Whether CreateDefault makes a value: a string, struct, list or dictionary, or a concrete class with a parameterless constructor.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ namespace GameCult.Unity.Caching.Editor
|
|||
// frame's edit copy, so nothing it or any other drawer did that frame is saved. type is the value's declared type.
|
||||
// member is the member the value belongs to, also for list elements and dictionary keys and values, and null for a
|
||||
// bare value. inspector.DrawDefault hands the value back to built-in drawing; inspector.DrawValue draws a sub-value
|
||||
// with claims.
|
||||
// with claims. inspector.Record is the record being drawn, read-only: a copy nothing saves.
|
||||
public interface ICultInspectorDrawer
|
||||
{
|
||||
object Draw(CultInspector inspector, string label, Type type, object value, MemberInfo member);
|
||||
|
|
@ -33,7 +33,7 @@ namespace GameCult.Unity.Caching.Editor
|
|||
private readonly Dictionary<string, bool> _foldouts = new Dictionary<string, bool>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, string> _notices = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
private string _path = string.Empty;
|
||||
private Type _document;
|
||||
private CultInspectorEdit _edit;
|
||||
private bool _drawerFailed;
|
||||
|
||||
internal CultInspector(CultInspectorModel model)
|
||||
|
|
@ -45,6 +45,9 @@ namespace GameCult.Unity.Caching.Editor
|
|||
|
||||
public IReadOnlyList<CultStoredDocument> Records { get; internal set; } = Array.Empty<CultStoredDocument>();
|
||||
|
||||
// The document being drawn, as the model's CultInspectorEdit.Record: a copy for reading, never saved.
|
||||
public object Record => _edit?.Record;
|
||||
|
||||
private static GUIStyle ErrorStyle => _errorStyle ??= new GUIStyle(EditorStyles.label) { normal = { textColor = new Color(1f, .35f, .3f) } };
|
||||
|
||||
private static CultInspectorDrawerClaims Claims
|
||||
|
|
@ -62,7 +65,7 @@ namespace GameCult.Unity.Caching.Editor
|
|||
internal bool DrawDocument(CultInspectorEdit edit)
|
||||
{
|
||||
_drawerFailed = false;
|
||||
_document = edit.Document.GetType();
|
||||
_edit = edit;
|
||||
DrawMembers(edit.Document, edit.Source.Descriptor.DocumentType, edit.Source.Key.Value);
|
||||
return !_drawerFailed;
|
||||
}
|
||||
|
|
@ -242,7 +245,7 @@ namespace GameCult.Unity.Caching.Editor
|
|||
var item = DrawValue("Value", shape.ValueType, entries[i].Value, member, path + "{" + i + "}.value");
|
||||
if (EditorGUI.EndChangeCheck())
|
||||
{
|
||||
key = Model.ReplaceKey(_document, shape.Type, entries.Select(e => e.Key).ToArray(), i, key, out var notice);
|
||||
key = Model.ReplaceKey(_edit.Document.GetType(),shape.Type, entries.Select(e => e.Key).ToArray(), i, key, out var notice);
|
||||
Notice(path, notice);
|
||||
entries[i] = new KeyValuePair<object, object>(key, item);
|
||||
changed = true;
|
||||
|
|
@ -261,7 +264,7 @@ namespace GameCult.Unity.Caching.Editor
|
|||
|
||||
if (Add(shape.ValueType, path, out var created))
|
||||
{
|
||||
var fresh = Model.FreshKey(_document, shape.Type, entries.Select(e => e.Key).ToArray(), Records, out var notice);
|
||||
var fresh = Model.FreshKey(_edit.Document.GetType(),shape.Type, entries.Select(e => e.Key).ToArray(), Records, out var notice);
|
||||
Notice(path, notice);
|
||||
if (fresh != null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -114,6 +114,15 @@ for list elements and dictionary keys and values, so an attribute drawer
|
|||
checks the type it is handed. `inspector.DrawDefault` hands a value back to
|
||||
built-in drawing; `inspector.DrawValue` draws a sub-value with claims applied.
|
||||
|
||||
`inspector.Record` is the whole document the value sits in, for drawers that
|
||||
read sibling members (a texture path, a hull's tints). It is read-only by
|
||||
contract: the model's `CultInspectorEdit.Record`, a second copy of the stored
|
||||
document as the edit began, made on first read. It is neither the cached
|
||||
object nor the edit copy the Studio saves, so writing to it saves nothing; an
|
||||
edit reaches the record only through the value `Draw` returns. Within one frame
|
||||
it does not show edits other drawers made earlier in that frame; after a save
|
||||
the next frame's edit starts from the saved record.
|
||||
|
||||
## Inspector Annotations
|
||||
|
||||
The annotations live in `GameCult.Caching`, so headless shared models carry
|
||||
|
|
|
|||
|
|
@ -216,6 +216,11 @@ namespace GameCult.Caching.Tests
|
|||
var record = readOnly.AllStoredDocuments.Single();
|
||||
var edit = model.BeginEdit(record);
|
||||
Assert.That(edit.Document, Is.Not.SameAs(record.Document));
|
||||
var drawn = (InspectItem)edit.Record;
|
||||
Assert.That(drawn, Is.Not.SameAs(record.Document).And.Not.SameAs(edit.Document));
|
||||
Assert.That(edit.Record, Is.SameAs(drawn), "one record copy per edit");
|
||||
drawn.Name = "a drawer wrote here";
|
||||
Assert.That(((InspectItem)edit.Document).Name, Is.EqualTo("original"), "the record copy is not the committed copy");
|
||||
((InspectItem)edit.Document).Name = "changed";
|
||||
((InspectItem)edit.Document).Numbers.Add(3);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue