// lib/editor/render/ink_picture_cache.dart // // Bounded LRU cache of ui.Picture objects keyed by "hostId:revision". // // Resolution-independent: ink is vector, so a single Picture is valid at any // zoom level. There are NO DPI buckets. // // Evicted Pictures are disposed via a post-frame callback so Flutter's raster // thread is never asked to delete a Picture it may still be reading. import 'dart:collection'; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; /// Bounded LRU cache of [ui.Picture]s keyed by a string (typically /// `"$hostId:$revision"`). /// /// Usage: /// ```dart /// final picture = cache.getOrBuild(hostId, store.revision, size, () { /// final recorder = ui.PictureRecorder(); /// final canvas = ui.Canvas(recorder); /// // … draw … /// return recorder.endRecording(); /// }); /// canvas.drawPicture(picture); /// ``` class InkPictureCache { InkPictureCache({int maxSize = 12}) : _maxSize = maxSize; final int _maxSize; // LinkedHashMap preserves insertion order; we move accessed entries to the // back so the front is always the least-recently used. final LinkedHashMap _cache = LinkedHashMap(); /// Returns a cached [ui.Picture] for [key], or calls [build] to create one. /// /// The [key] should encode all inputs that affect the picture content (host /// id + revision, at minimum). [size] and [build] are only used on a cache /// miss. ui.Picture getOrBuild( String hostId, int revision, ui.Size size, ui.Picture Function() build, ) { final key = '$hostId:$revision'; if (_cache.containsKey(key)) { // Promote to most-recently-used by reinserting at the back. final pic = _cache.remove(key)!; _cache[key] = pic; return pic; } final picture = build(); _cache[key] = picture; // Evict least-recently-used entries beyond the cap. while (_cache.length > _maxSize) { final lruKey = _cache.keys.first; final evicted = _cache.remove(lruKey)!; _disposeDeferred(evicted); } return picture; } /// Disposes all cached Pictures, deferring the actual disposal to a /// post-frame callback so any in-flight raster work can complete. void dispose() { final pictures = List.from(_cache.values); _cache.clear(); for (final pic in pictures) { _disposeDeferred(pic); } } static void _disposeDeferred(ui.Picture picture) { WidgetsBinding.instance.addPostFrameCallback((_) { picture.dispose(); }); } }