88 lines
2.5 KiB
Dart
88 lines
2.5 KiB
Dart
|
|
// 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();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|