using System; using System.Buffers; using System.Collections.Generic; using Avalonia.Metadata; namespace Avalonia.Media.TextFormatting { /// /// Caches shaped text runs and bidi processing results to avoid redundant shaping /// when only the paragraph width constraint changes (e.g., between Measure and Arrange). /// /// /// Uses an inline single-entry store for the common case of a single paragraph, /// and only promotes to a dictionary when multiple entries are added. /// [Unstable("This API is in preview and subject to change without deprecation.")] public class TextRunCache : IDisposable { // Single-entry inline store (avoids Dictionary allocation for the common single-paragraph case). private bool _hasSingleEntry; private int _singleKey; private CachedShapingResult _singleValue; // Multi-entry store (only allocated when 2+ distinct keys are added). private Dictionary? _entries; /// /// Invalidates all cached entries and disposes their shaped buffers. /// public void Invalidate() { if (_hasSingleEntry) { DisposeCachedRuns(_singleValue); _hasSingleEntry = false; _singleValue = default; return; } if (_entries == null) { return; } foreach (var entry in _entries.Values) { DisposeCachedRuns(entry); } _entries.Clear(); } /// /// Invalidates all cached entries at or after the specified text source index. /// /// The text source index from which to invalidate. public void InvalidateFrom(int textSourceIndex) { if (_hasSingleEntry) { if (_singleKey >= textSourceIndex) { DisposeCachedRuns(_singleValue); _hasSingleEntry = false; _singleValue = default; } return; } if (_entries == null || _entries.Count == 0) { return; } var count = _entries.Count; int[]? rented = null; Span keysToRemove = count <= 16 ? stackalloc int[16] : (rented = ArrayPool.Shared.Rent(count)); var removeCount = 0; foreach (var key in _entries.Keys) { if (key >= textSourceIndex) { keysToRemove[removeCount++] = key; } } for (var i = 0; i < removeCount; i++) { if (_entries.Remove(keysToRemove[i], out var result)) { DisposeCachedRuns(result); } } if (rented != null) { ArrayPool.Shared.Return(rented); } } /// /// Tries to retrieve cached shaped runs for the given text source index. /// internal bool TryGetShapedRuns(int firstTextSourceIndex, out CachedShapingResult result) { if (_hasSingleEntry && _singleKey == firstTextSourceIndex) { result = _singleValue; return true; } if (_entries != null && _entries.TryGetValue(firstTextSourceIndex, out result)) { return true; } result = default; return false; } /// /// Adds shaped runs to the cache for the given text source index. The cache takes /// its own reference to each ; the caller retains its /// original references unchanged. /// internal void Add(int firstTextSourceIndex, CachedShapingResult result) { AddRefShapedRuns(result.ShapedRuns); if (_entries != null) { if (_entries.TryGetValue(firstTextSourceIndex, out var existing)) { DisposeCachedRuns(existing); } _entries[firstTextSourceIndex] = result; return; } if (!_hasSingleEntry) { _singleKey = firstTextSourceIndex; _singleValue = result; _hasSingleEntry = true; return; } if (_singleKey == firstTextSourceIndex) { DisposeCachedRuns(_singleValue); _singleValue = result; return; } // Second distinct key: promote to dictionary. _entries = new Dictionary { { _singleKey, _singleValue }, { firstTextSourceIndex, result } }; _hasSingleEntry = false; _singleValue = default; } private static void AddRefShapedRuns(TextRun[] runs) { for (var i = 0; i < runs.Length; i++) { if (runs[i] is ShapedTextRun shaped) { shaped.AddRef(); } } } /// public void Dispose() { Invalidate(); _entries = null; } private static void DisposeCachedRuns(CachedShapingResult result) { var runs = result.ShapedRuns; for (var i = 0; i < runs.Length; i++) { if (runs[i] is ShapedTextRun shaped) { shaped.Dispose(); } } } } /// /// Stores the result of text shaping for a paragraph segment starting at a given text source index. /// internal readonly struct CachedShapingResult { public CachedShapingResult(TextRun[] shapedRuns, FlowDirection resolvedFlowDirection, TextEndOfLine? textEndOfLine, int textSourceLength) { ShapedRuns = shapedRuns; ResolvedFlowDirection = resolvedFlowDirection; TextEndOfLine = textEndOfLine; TextSourceLength = textSourceLength; } /// /// The shaped text runs (output of ShapeTextRuns). /// public readonly TextRun[] ShapedRuns; /// /// The resolved flow direction for the paragraph. /// public readonly FlowDirection ResolvedFlowDirection; /// /// The end of line marker, if any. /// public readonly TextEndOfLine? TextEndOfLine; /// /// The total text source length consumed. /// public readonly int TextSourceLength; } }