Files
BadNote/lib/editor/render/ink_picture_cache.dart

88 lines
2.5 KiB
Dart
Raw Permalink Normal View History

// 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<String, ui.Picture> _cache =
LinkedHashMap<String, ui.Picture>();
/// 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<ui.Picture>.from(_cache.values);
_cache.clear();
for (final pic in pictures) {
_disposeDeferred(pic);
}
}
static void _disposeDeferred(ui.Picture picture) {
WidgetsBinding.instance.addPostFrameCallback((_) {
picture.dispose();
});
}
}