From b569adfa3fb744c4e65b47299549b6de5c072e25 Mon Sep 17 00:00:00 2001 From: Nick Fisher Date: Tue, 1 Sep 2026 22:30:32 +0800 Subject: [PATCH] feat: initialize viewers through ThermionWidget --- .../lib/src/widgets/src/thermion_widget.dart | 180 ++++++++++++++++-- .../test/texture_bootstrap_test.dart | 60 ++++++ 2 files changed, 219 insertions(+), 21 deletions(-) diff --git a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget.dart b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget.dart index dbf43361a..1018a8f03 100644 --- a/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget.dart +++ b/thermion_flutter/thermion_flutter/lib/src/widgets/src/thermion_widget.dart @@ -4,43 +4,181 @@ import 'package:flutter/material.dart' hide View; import '../../platform/src/platform_texture_descriptor.dart'; import 'thermion_widget_internal/surface_widget_builder.dart'; +import 'texture_bootstrap.dart'; import 'package:thermion_flutter/thermion_flutter.dart'; +typedef ThermionViewerFactory = Future Function(); + class ThermionWidget extends StatefulWidget { - // The viewer whose content will be rendered into this widget. - final ThermionViewer viewer; + /// Renders an existing [viewer]. Its creation and disposal remain the + /// caller's responsibility. + const ThermionWidget({Key? key, required ThermionViewer viewer}) + : this._(key: key, viewer: viewer); + + /// Creates and owns a viewer when this widget enters the tree. + /// + /// On platforms such as Linux OpenGL, [viewerFactory] is invoked only after + /// Flutter has composited the context-bootstrap texture required by the + /// native renderer. Callers therefore do not need to coordinate platform + /// texture initialization themselves. + /// + /// The returned viewer is disposed when this widget is removed. Configuration + /// that must happen before the first rendered frame can be performed inside + /// [viewerFactory] before returning the viewer. + const ThermionWidget.create({ + Key? key, + required ThermionViewerFactory viewerFactory, + Widget initial = const SizedBox.shrink(), + }) : this._(key: key, viewerFactory: viewerFactory, initial: initial); + + const ThermionWidget._({ + super.key, + this.viewer, + this.viewerFactory, + this.initial = const SizedBox.shrink(), + }); - const ThermionWidget({Key? key, required this.viewer}) : super(key: key); + /// The viewer whose content will be rendered, when supplied by the caller. + final ThermionViewer? viewer; + + /// Creates a viewer owned by this widget, when using [ThermionWidget.create]. + final ThermionViewerFactory? viewerFactory; + + /// Displayed until a viewer returned by [viewerFactory] is ready. + final Widget initial; @override State createState() => _ThermionWidgetState(); } class _ThermionWidgetState extends State { + ThermionViewer? _ownedViewer; + Future? _initialization; + Future? _teardown; + bool _disposing = false; + late final bool _requiresContextBootstrap; + @override - Widget build(BuildContext context) { - return ThermionWidgetInternal( - view: widget.viewer.view, - surfaceWidgetBuilder: surfaceWidgetBuilder, - onTexturePreparing: (descriptor) async { - final view = widget.viewer.view; - var camera = await view.getCamera(); - var near = await camera.getNear(); - var far = await camera.getCullingFar(); - var focalLength = await camera.getFocalLength(); - - await camera.setLensProjection( - near: near, - far: far, - focalLength: focalLength, - aspect: descriptor.width.toDouble() / descriptor.height.toDouble(), + void initState() { + super.initState(); + _requiresContextBootstrap = + widget.viewerFactory != null && + ThermionFlutterPlugin.instance.requiresContextBootstrap; + if (widget.viewerFactory != null && !_requiresContextBootstrap) { + _startInitialization(); + } + } + + Future _initializeViewer() => + _initialization ??= _createAndPublishViewer(); + + void _startInitialization() { + unawaited( + _initializeViewer().catchError((Object error, StackTrace stack) { + if (_disposing) return; + FlutterError.reportError( + FlutterErrorDetails( + exception: error, + stack: stack, + library: 'thermion_flutter', + context: ErrorDescription('while creating a Thermion viewer'), + ), ); + }), + ); + } - await view.setViewport(descriptor.width, descriptor.height); - }, + Future _createAndPublishViewer() async { + final viewer = await widget.viewerFactory!(); + if (_disposing) { + await _disposeViewer(viewer); + return; + } + _ownedViewer = viewer; + if (mounted) setState(() {}); + } + + Future _disposeViewer(ThermionViewer viewer) async { + try { + await ThermionFlutterPlugin.instance.destroyTextureForView(viewer.view); + } finally { + try { + await viewer.dispose(); + } finally { + await ThermionFlutterPlugin.instance.onViewerDisposed(viewer.view); + } + } + } + + Future _disposeOwnedViewer() async { + try { + await _initialization; + } finally { + final viewer = _ownedViewer; + _ownedViewer = null; + if (viewer != null) await _disposeViewer(viewer); + } + } + + @override + void dispose() { + _disposing = true; + if (widget.viewerFactory != null) { + _teardown ??= _disposeOwnedViewer(); + unawaited( + _teardown!.catchError((Object error, StackTrace stack) { + FlutterError.reportError( + FlutterErrorDetails( + exception: error, + stack: stack, + library: 'thermion_flutter', + context: ErrorDescription('while disposing a Thermion viewer'), + ), + ); + }), + ); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final viewer = widget.viewer ?? _ownedViewer; + final child = viewer == null + ? widget.initial + : ThermionWidgetInternal( + view: viewer.view, + surfaceWidgetBuilder: surfaceWidgetBuilder, + onTexturePreparing: (descriptor) => + _prepareTexture(viewer, descriptor), + ); + if (!_requiresContextBootstrap) return child; + return ThermionTextureBootstrap( + initialize: _initializeViewer, + child: child, ); } + + Future _prepareTexture( + ThermionViewer viewer, + PlatformTextureDescriptor descriptor, + ) async { + final view = viewer.view; + var camera = await view.getCamera(); + var near = await camera.getNear(); + var far = await camera.getCullingFar(); + var focalLength = await camera.getFocalLength(); + + await camera.setLensProjection( + near: near, + far: far, + focalLength: focalLength, + aspect: descriptor.width.toDouble() / descriptor.height.toDouble(), + ); + + await view.setViewport(descriptor.width, descriptor.height); + } } // Inserts [view] into the widget tree by allocating a hardware surface diff --git a/thermion_flutter/thermion_flutter/test/texture_bootstrap_test.dart b/thermion_flutter/thermion_flutter/test/texture_bootstrap_test.dart index cf5a1255f..7eb86b954 100644 --- a/thermion_flutter/thermion_flutter/test/texture_bootstrap_test.dart +++ b/thermion_flutter/thermion_flutter/test/texture_bootstrap_test.dart @@ -1,8 +1,12 @@ import 'dart:async'; +import 'dart:io'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:thermion_flutter/src/platform/src/platform_texture_descriptor_registry_native.dart'; import 'package:thermion_flutter/src/widgets/src/texture_bootstrap.dart'; +import 'package:thermion_flutter/thermion_flutter.dart' hide Texture; void main() { testWidgets( @@ -83,4 +87,60 @@ void main() { expect(initializeCount, 0); expect(tester.takeException(), isNull); }); + + testWidgets('ThermionWidget.create waits for context bootstrap', ( + tester, + ) async { + final ready = Completer(); + final viewer = Completer(); + final events = []; + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + const channel = NativePlatformTextureDescriptorRegistry.channel; + messenger.setMockMethodCallHandler(channel, (call) async { + switch (call.method) { + case 'createContextBootstrap': + events.add('create'); + return 7; + case 'awaitTextureReady': + events.add('await'); + await ready.future; + return 11; + case 'destroyTexture': + events.add('destroy'); + return null; + default: + throw MissingPluginException('Unexpected method ${call.method}'); + } + }); + addTearDown(() => messenger.setMockMethodCallHandler(channel, null)); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: ThermionWidget.create( + viewerFactory: () { + events.add('viewer'); + return viewer.future; + }, + initial: const SizedBox(key: Key('initial')), + ), + ), + ); + await tester.pump(); + + expect(find.byKey(const Key('initial')), findsOneWidget); + expect(find.byType(Texture), findsOneWidget); + expect(events, ['create', 'await']); + + ready.complete(); + await tester.pump(); + + expect(events, ['create', 'await', 'viewer']); + + await tester.pumpWidget(const SizedBox.shrink()); + await tester.pump(); + expect(events, ['create', 'await', 'viewer', 'destroy']); + expect(tester.takeException(), isNull); + }, skip: !Platform.isLinux); }