feat: working implementation of multiple widgets on macos
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
typedef ResizeCallback = void Function(Size newSize);
|
||||
typedef ResizeCallback = void Function(Size oldSize, Size newSize);
|
||||
|
||||
class ResizeObserver extends SingleChildRenderObjectWidget {
|
||||
final ResizeCallback onResized;
|
||||
@@ -34,7 +34,7 @@ class _RenderResizeObserver extends RenderProxyBox {
|
||||
void performLayout() async {
|
||||
super.performLayout();
|
||||
if (size.width != _oldSize.width || size.height != _oldSize.height) {
|
||||
onLayoutChangedCallback(size);
|
||||
onLayoutChangedCallback(_oldSize, size);
|
||||
_oldSize = Size(size.width, size.height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,6 @@ class _ThermionListenerWidgetState extends State<ThermionListenerWidget> {
|
||||
return widget.child ?? Container();
|
||||
}
|
||||
return Stack(children: [
|
||||
if (widget.child != null) Positioned.fill(child: widget.child!),
|
||||
if (isDesktop) Positioned.fill(child: _desktop(pixelRatio)),
|
||||
if (!isDesktop) Positioned.fill(child: _mobile(pixelRatio))
|
||||
]);
|
||||
|
||||
@@ -26,14 +26,21 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
ThermionFlutterTexture? _texture;
|
||||
RenderTarget? _renderTarget;
|
||||
|
||||
static final _views = <t.View>[];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_views.remove(widget.view);
|
||||
_texture?.destroy();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
if (_views.contains(widget.view)) {
|
||||
throw Exception("View already embedded in a widget");
|
||||
}
|
||||
_views.add(widget.view);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
await widget.viewer.initialized;
|
||||
|
||||
@@ -51,9 +58,10 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
|
||||
await widget.view.setRenderTarget(_renderTarget!);
|
||||
|
||||
await widget.view.updateViewport(width, height);
|
||||
await widget.view.updateViewport(_texture!.width, _texture!.height);
|
||||
var camera = await widget.view.getCamera();
|
||||
await camera.setLensProjection(aspect: width / height);
|
||||
await camera.setLensProjection(
|
||||
aspect: _texture!.width / _texture!.height);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
@@ -73,18 +81,29 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
await _renderTarget!.destroy();
|
||||
texture.destroy();
|
||||
}
|
||||
_views.clear();
|
||||
});
|
||||
});
|
||||
_callbackId = _numCallbacks;
|
||||
_numCallbacks++;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool _rendering = false;
|
||||
|
||||
static int _numCallbacks = 0;
|
||||
static int _primaryCallback = 0;
|
||||
late int _callbackId;
|
||||
int lastRender = 0;
|
||||
|
||||
void _requestFrame() {
|
||||
WidgetsBinding.instance.scheduleFrameCallback((d) async {
|
||||
if (!_rendering) {
|
||||
if (widget.viewer.rendering && !_rendering) {
|
||||
_rendering = true;
|
||||
await widget.viewer.requestFrame();
|
||||
if (_callbackId == _primaryCallback) {
|
||||
await widget.viewer.requestFrame();
|
||||
lastRender = d.inMilliseconds;
|
||||
}
|
||||
await _texture?.markFrameAvailable();
|
||||
_rendering = false;
|
||||
}
|
||||
@@ -92,29 +111,37 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
});
|
||||
}
|
||||
|
||||
bool _resizing = false;
|
||||
final _resizing = <Future>[];
|
||||
|
||||
Timer? _resizeTimer;
|
||||
|
||||
Future _resize(Size newSize) async {
|
||||
|
||||
Future _resize(Size oldSize, Size newSize) async {
|
||||
await Future.wait(_resizing);
|
||||
|
||||
_resizeTimer?.cancel();
|
||||
|
||||
_resizeTimer = Timer(const Duration(milliseconds: 10), () async {
|
||||
if (_resizing || !mounted) {
|
||||
return;
|
||||
}
|
||||
_resizeTimer!.cancel();
|
||||
_resizing = true;
|
||||
|
||||
_resizeTimer = Timer(const Duration(milliseconds: 100), () async {
|
||||
await Future.wait(_resizing);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newSize.width == _texture?.width &&
|
||||
newSize.height == _texture?.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
final completer = Completer();
|
||||
|
||||
_resizing.add(completer.future);
|
||||
|
||||
newSize *= MediaQuery.of(context).devicePixelRatio;
|
||||
|
||||
var newWidth = newSize.width.ceil();
|
||||
var newHeight = newSize.height.ceil();
|
||||
|
||||
var lastTextureId = _texture?.hardwareId;
|
||||
|
||||
await _texture?.resize(
|
||||
newWidth,
|
||||
newHeight,
|
||||
@@ -122,12 +149,23 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
0,
|
||||
);
|
||||
|
||||
await widget.view.updateViewport(newWidth, newHeight);
|
||||
var camera = await widget.view.getCamera();
|
||||
await camera.setLensProjection(aspect: newWidth / newHeight);
|
||||
if (_texture?.hardwareId != lastTextureId) {
|
||||
await _renderTarget?.destroy();
|
||||
_renderTarget = await widget.viewer.createRenderTarget(
|
||||
_texture!.width, _texture!.height, _texture!.hardwareId);
|
||||
await widget.view.setRenderTarget(_renderTarget!);
|
||||
}
|
||||
|
||||
await widget.view.updateViewport(_texture!.width, _texture!.height);
|
||||
var camera = await widget.view.getCamera();
|
||||
await camera.setLensProjection(
|
||||
aspect: _texture!.width.toDouble() / _texture!.height.toDouble());
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {});
|
||||
_resizing = false;
|
||||
completer.complete();
|
||||
_resizing.remove(completer.future);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,30 +175,17 @@ class _ThermionTextureWidgetState extends State<ThermionTextureWidget> {
|
||||
return widget.initial ?? Container(color: Colors.red);
|
||||
}
|
||||
|
||||
return Stack(children: [
|
||||
Positioned.fill(
|
||||
child: ResizeObserver(
|
||||
onResized: _resize,
|
||||
child: Stack(children: [
|
||||
Positioned.fill(
|
||||
child: Texture(
|
||||
key: ObjectKey("flutter_texture_${_texture!.flutterId}"),
|
||||
textureId: _texture!.flutterId,
|
||||
filterQuality: FilterQuality.none,
|
||||
freeze: false,
|
||||
))
|
||||
]))),
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
var img =
|
||||
await widget.viewer.capture(renderTarget: _renderTarget!);
|
||||
print(img);
|
||||
},
|
||||
child: Text("CAPTURE")),
|
||||
)
|
||||
]);
|
||||
return ResizeObserver(
|
||||
onResized: _resize,
|
||||
child: Stack(children: [
|
||||
Positioned.fill(
|
||||
child: Texture(
|
||||
key: ObjectKey("flutter_texture_${_texture!.flutterId}"),
|
||||
textureId: _texture!.flutterId,
|
||||
filterQuality: FilterQuality.none,
|
||||
freeze: false,
|
||||
))
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
|
||||
|
||||
var registrar : FlutterPluginRegistrar
|
||||
var registry: FlutterTextureRegistry
|
||||
var texture: ThermionFlutterTexture?
|
||||
|
||||
var textures: [Int64: ThermionFlutterTexture] = [:]
|
||||
|
||||
var createdAt = Date()
|
||||
|
||||
var destroying = false
|
||||
@@ -72,6 +72,13 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
|
||||
self.registry = textureRegistry;
|
||||
self.registrar = registrar
|
||||
}
|
||||
|
||||
var markTextureFrameAvailable : @convention(c) (UnsafeMutableRawPointer?) -> () = { instancePtr in
|
||||
let instance:SwiftThermionFlutterPlugin = Unmanaged<SwiftThermionFlutterPlugin>.fromOpaque(instancePtr!).takeUnretainedValue()
|
||||
for (_, texture) in instance.textures {
|
||||
instance.registry.textureFrameAvailable(texture.flutterTextureId)
|
||||
}
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
let methodName = call.method;
|
||||
@@ -86,33 +93,39 @@ public class SwiftThermionFlutterPlugin: NSObject, FlutterPlugin {
|
||||
registry.textureFrameAvailable(flutterTextureId)
|
||||
result(nil)
|
||||
case "getRenderCallback":
|
||||
result(nil)
|
||||
if(renderCallbackHolder.isEmpty) {
|
||||
renderCallbackHolder.append(unsafeBitCast(markTextureFrameAvailable, to:Int64.self))
|
||||
renderCallbackHolder.append(unsafeBitCast(Unmanaged.passUnretained(self), to:UInt64.self))
|
||||
}
|
||||
result(renderCallbackHolder)
|
||||
case "getDriverPlatform":
|
||||
result(nil)
|
||||
case "getSharedContext":
|
||||
result(nil)
|
||||
case "createTexture":
|
||||
if(destroying) {
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
let args = call.arguments as! [Any]
|
||||
let width = args[0] as! Int64
|
||||
let height = args[1] as! Int64
|
||||
|
||||
self.texture = ThermionFlutterTexture(registry: registry, width: width, height: height)
|
||||
let texture = ThermionFlutterTexture(registry: registry, width: width, height: height)
|
||||
|
||||
if(self.texture!.texture.metalTextureAddress == -1) {
|
||||
if texture.texture.metalTextureAddress == -1 {
|
||||
result(nil)
|
||||
} else {
|
||||
result([self.texture!.flutterTextureId as Any, self.texture!.texture.metalTextureAddress, nil])
|
||||
textures[texture.flutterTextureId] = texture
|
||||
result([texture.flutterTextureId, texture.texture.metalTextureAddress, nil])
|
||||
}
|
||||
case "destroyTexture":
|
||||
self.destroying = true
|
||||
self.texture?.destroy()
|
||||
self.texture = nil
|
||||
result(true)
|
||||
self.destroying = false
|
||||
let args = call.arguments as! [Any]
|
||||
let flutterTextureId = args[0] as! Int64
|
||||
|
||||
if let texture = textures[flutterTextureId] {
|
||||
texture.destroy()
|
||||
textures.removeValue(forKey: flutterTextureId)
|
||||
result(true)
|
||||
} else {
|
||||
result(false)
|
||||
}
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ public class ThermionFlutterTexture : NSObject, FlutterTexture {
|
||||
}
|
||||
|
||||
public func onTextureUnregistered(_ texture:FlutterTexture) {
|
||||
print("Texture unregistered")
|
||||
|
||||
}
|
||||
|
||||
public func destroy() {
|
||||
|
||||
@@ -13,47 +13,36 @@ import 'package:logging/logging.dart';
|
||||
///
|
||||
class ThermionFlutterMacOS extends ThermionFlutterMethodChannelInterface {
|
||||
final _channel = const MethodChannel("dev.thermion.flutter/event");
|
||||
final _logger = Logger("ThermionFlutterFFI");
|
||||
final _logger = Logger("ThermionFlutterMacOS");
|
||||
|
||||
SwapChain? _swapChain;
|
||||
static SwapChain? _swapChain;
|
||||
|
||||
ThermionFlutterMacOS._() {}
|
||||
ThermionFlutterMacOS._();
|
||||
|
||||
static ThermionFlutterMacOS? instance;
|
||||
|
||||
static void registerWith() {
|
||||
ThermionFlutterPlatform.instance = ThermionFlutterMacOS._();
|
||||
instance ??= ThermionFlutterMacOS._();
|
||||
ThermionFlutterPlatform.instance = instance!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
|
||||
var viewer = await super.createViewer(options: options);
|
||||
if (_swapChain != null) {
|
||||
throw Exception("Only a single swapchain can be created");
|
||||
}
|
||||
// this is the headless swap chain
|
||||
// since we will be using render targets, the actual dimensions don't matter
|
||||
_swapChain = await viewer.createSwapChain(1, 1);
|
||||
return viewer;
|
||||
}
|
||||
|
||||
// On desktop platforms, textures are always created
|
||||
Future<ThermionFlutterTexture?> createTexture(int width, int height) async {
|
||||
if (_swapChain == null) {
|
||||
// this is the headless swap chain
|
||||
// since we will be using render targets, the actual dimensions don't matter
|
||||
_swapChain = await viewer!.createSwapChain(width, height);
|
||||
}
|
||||
// Get screen width and height
|
||||
int screenWidth = width; //1920;
|
||||
int screenHeight = height; //1080;
|
||||
|
||||
if (width > screenWidth || height > screenHeight) {
|
||||
throw Exception("TODO - unsupported");
|
||||
}
|
||||
|
||||
var result = await _channel
|
||||
.invokeMethod("createTexture", [screenWidth, screenHeight, 0, 0]);
|
||||
|
||||
if (result == null || (result[0] == -1)) {
|
||||
throw Exception("Failed to create texture");
|
||||
}
|
||||
final flutterTextureId = result[0] as int?;
|
||||
final hardwareTextureId = result[1] as int?;
|
||||
final surfaceAddress = result[2] as int?;
|
||||
|
||||
|
||||
_logger.info(
|
||||
"Created texture with flutter texture id ${flutterTextureId}, hardwareTextureId $hardwareTextureId and surfaceAddress $surfaceAddress");
|
||||
|
||||
return MacOSMethodChannelFlutterTexture(_channel, flutterTextureId!,
|
||||
hardwareTextureId!, screenWidth, screenHeight);
|
||||
var texture = MacOSMethodChannelFlutterTexture(_channel);
|
||||
await texture.resize(width, height, 0, 0);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// On MacOS, we currently use textures/render targets, so there's no window to resize
|
||||
@@ -64,14 +53,119 @@ class ThermionFlutterMacOS extends ThermionFlutterMethodChannelInterface {
|
||||
}
|
||||
}
|
||||
|
||||
class TextureCacheEntry {
|
||||
final int flutterId;
|
||||
final int hardwareId;
|
||||
final DateTime creationTime;
|
||||
DateTime? removalTime;
|
||||
bool inUse;
|
||||
|
||||
TextureCacheEntry(this.flutterId, this.hardwareId, {this.removalTime, this.inUse = true})
|
||||
: creationTime = DateTime.now();
|
||||
}
|
||||
|
||||
|
||||
class MacOSMethodChannelFlutterTexture extends MethodChannelFlutterTexture {
|
||||
MacOSMethodChannelFlutterTexture(super.channel, super.flutterId,
|
||||
super.hardwareId, super.width, super.height);
|
||||
final _logger = Logger("MacOSMethodChannelFlutterTexture");
|
||||
|
||||
int flutterId = -1;
|
||||
int hardwareId = -1;
|
||||
int width = -1;
|
||||
int height = -1;
|
||||
|
||||
static final Map<String, List<TextureCacheEntry>> _textureCache = {};
|
||||
|
||||
MacOSMethodChannelFlutterTexture(super.channel);
|
||||
|
||||
@override
|
||||
Future resize(int width, int height, int left, int top) async {
|
||||
if (width > this.width || height > this.height || left != 0 || top != 0) {
|
||||
throw Exception();
|
||||
Future<void> resize(
|
||||
int newWidth, int newHeight, int newLeft, int newTop) async {
|
||||
if (newWidth == this.width &&
|
||||
newHeight == this.height &&
|
||||
newLeft == 0 &&
|
||||
newTop == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.width = newWidth;
|
||||
this.height = newHeight;
|
||||
|
||||
// Clean up old textures
|
||||
await _cleanupOldTextures();
|
||||
|
||||
final cacheKey = '${width}x$height';
|
||||
final availableTextures =
|
||||
_textureCache[cacheKey]?.where((entry) => !entry.inUse) ?? [];
|
||||
if (availableTextures.isNotEmpty) {
|
||||
final cachedTexture = availableTextures.first;
|
||||
flutterId = cachedTexture.flutterId;
|
||||
hardwareId = cachedTexture.hardwareId;
|
||||
cachedTexture.inUse = true;
|
||||
_logger.info(
|
||||
"Using cached texture: flutter id $flutterId, hardware id $hardwareId");
|
||||
} else {
|
||||
var result =
|
||||
await channel.invokeMethod("createTexture", [width, height, 0, 0]);
|
||||
if (result == null || (result[0] == -1)) {
|
||||
throw Exception("Failed to create texture");
|
||||
}
|
||||
flutterId = result[0] as int;
|
||||
hardwareId = result[1] as int;
|
||||
|
||||
final newEntry = TextureCacheEntry(flutterId, hardwareId, inUse: true);
|
||||
_textureCache.putIfAbsent(cacheKey, () => []).add(newEntry);
|
||||
_logger.info(
|
||||
"Created new MacOS texture: flutter id $flutterId, hardware id $hardwareId");
|
||||
}
|
||||
|
||||
// Mark old texture as not in use
|
||||
if (this.width != -1 && this.height != -1) {
|
||||
final oldCacheKey = '${this.width}x${this.height}';
|
||||
final oldEntry = _textureCache[oldCacheKey]?.firstWhere(
|
||||
(entry) => entry.flutterId == this.flutterId,
|
||||
orElse: () => TextureCacheEntry(-1, -1),
|
||||
);
|
||||
if (oldEntry != null && oldEntry.flutterId != -1) {
|
||||
oldEntry.inUse = false;
|
||||
oldEntry.removalTime = DateTime.now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future _cleanupOldTextures() async {
|
||||
final now = DateTime.now();
|
||||
final entriesToRemove = <String, List<TextureCacheEntry>>{};
|
||||
|
||||
for (var entry in _textureCache.entries) {
|
||||
final expiredTextures = entry.value.where((texture) {
|
||||
return !texture.inUse &&
|
||||
texture.removalTime != null &&
|
||||
now.difference(texture.removalTime!).inSeconds > 5;
|
||||
}).toList();
|
||||
|
||||
if (expiredTextures.isNotEmpty) {
|
||||
entriesToRemove[entry.key] = expiredTextures;
|
||||
}
|
||||
}
|
||||
|
||||
for (var entry in entriesToRemove.entries) {
|
||||
for (var texture in entry.value) {
|
||||
await _destroyTexture(texture.flutterId, texture.hardwareId);
|
||||
_logger.info("Destroying texture: ${texture.flutterId}");
|
||||
_textureCache[entry.key]?.remove(texture);
|
||||
}
|
||||
if (_textureCache[entry.key]?.isEmpty ?? false) {
|
||||
_textureCache.remove(entry.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _destroyTexture(int flutterId, int hardwareId) async {
|
||||
try {
|
||||
await channel.invokeMethod("destroyTexture", [flutterId, hardwareId]);
|
||||
_logger.info("Destroyed old texture: flutter id $flutterId, hardware id $hardwareId");
|
||||
} catch (e) {
|
||||
_logger.severe("Failed to destroy texture: $e");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ abstract class ThermionFlutterMethodChannelInterface
|
||||
final _logger = Logger("ThermionFlutterMethodChannelInterface");
|
||||
|
||||
ThermionViewerFFI? viewer;
|
||||
SwapChain? _swapChain;
|
||||
|
||||
Future<ThermionViewer> createViewer({ThermionFlutterOptions? options}) async {
|
||||
if (viewer != null) {
|
||||
@@ -31,9 +32,13 @@ abstract class ThermionFlutterMethodChannelInterface
|
||||
if (resourceLoader == nullptr) {
|
||||
throw Exception("Failed to get resource loader");
|
||||
}
|
||||
|
||||
|
||||
// var renderCallbackResult = await _channel.invokeMethod("getRenderCallback");
|
||||
var renderCallback = nullptr;
|
||||
// Pointer<NativeFunction<Void Function(Pointer<Void>)>>.fromAddress(
|
||||
// renderCallbackResult[0]);
|
||||
var renderCallbackOwner = nullptr;
|
||||
// Pointer<Void>.fromAddress(renderCallbackResult[1]);
|
||||
|
||||
var driverPlatform = await _channel.invokeMethod("getDriverPlatform");
|
||||
var driverPtr = driverPlatform == null
|
||||
@@ -54,33 +59,33 @@ abstract class ThermionFlutterMethodChannelInterface
|
||||
sharedContext: sharedContextPtr,
|
||||
uberArchivePath: options?.uberarchivePath);
|
||||
await viewer!.initialized;
|
||||
|
||||
return viewer!;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class MethodChannelFlutterTexture extends ThermionFlutterTexture {
|
||||
final MethodChannel _channel;
|
||||
final MethodChannel channel;
|
||||
|
||||
MethodChannelFlutterTexture(
|
||||
this._channel, this.flutterId, this.hardwareId, this.width, this.height);
|
||||
MethodChannelFlutterTexture(this.channel);
|
||||
|
||||
Future destroy() async {
|
||||
await _channel.invokeMethod("destroyTexture", hardwareId);
|
||||
await channel.invokeMethod("destroyTexture", hardwareId);
|
||||
}
|
||||
|
||||
@override
|
||||
final int flutterId;
|
||||
int get flutterId;
|
||||
|
||||
@override
|
||||
final int hardwareId;
|
||||
int get hardwareId;
|
||||
|
||||
@override
|
||||
final int height;
|
||||
int get height;
|
||||
|
||||
@override
|
||||
final int width;
|
||||
int get width;
|
||||
|
||||
Future markFrameAvailable() async {
|
||||
await _channel.invokeMethod("markTextureFrameAvailable", this.flutterId);
|
||||
await channel.invokeMethod("markTextureFrameAvailable", this.flutterId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,6 @@ abstract class ThermionFlutterPlatform extends PlatformInterface {
|
||||
///
|
||||
///
|
||||
///
|
||||
Future resizeWindow(
|
||||
Future resizeWindow(
|
||||
int width, int height, int offsetTop, int offsetRight);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user