[dart:ui] Introduce PlatformDispatcher.implicitView (flutter/engine#39553)

This introduces `PlatformDispatcher.implicitView`, a low-level primitive for the framework's bootstrapping. Most code, including the framework after bootstrapping, will use `View.of(context)` instead of this new API. This new primitive will let us deprecate the `window` global.

Goals:

1. **Enable multi-window**. The `PlatformDispatcher.implicitView` is nullable. If `null`, the app must create a window to get a view it can draw into.
2. **Backwards compatibility**. For "single window" apps, `PlatformDispatcher.instance.implicitView` should behave as similar to `window` as possible.
    1. The `PlatformDispatcher.instance.implicitView.viewId` should be `0`.
    1. The `PlatformDispatcher.instance.implicitView` must be available synchronously at root isolate startup. This allows the framework to determine if it can make single window assumptions at startup.
    2. The `PlatformDispatcher.instance.implicitView` reference must not change after startup: if it is null at startup, it must always be null; if it is non-null at startup, it must always be non-null. If "single window" app enters headless mode, the implicit view must remain non-null.

In the future, the embedder will control whether an implicit view is created: mobile & legacy desktop apps will have an implicit view, multi-window desktop apps won't have an implicit view. This requires updating the engine's embedder API and is out-of-scope for this change. For now, all apps will have an implicit view.

Part of https://github.com/flutter/flutter/issues/120306
This commit is contained in:
Loïc Sharma 2023-02-16 16:32:14 -08:00 committed by GitHub
parent 0a6f4a106e
commit a9e12287f6
17 changed files with 160 additions and 9 deletions

View File

@ -95,6 +95,7 @@ typedef CanvasPath Path;
V(IsolateNameServerNatives::RemovePortNameMapping, 1) \
V(NativeStringAttribute::initLocaleStringAttribute, 4) \
V(NativeStringAttribute::initSpellOutStringAttribute, 3) \
V(PlatformConfigurationNativeApi::ImplicitViewEnabled, 0) \
V(PlatformConfigurationNativeApi::DefaultRouteName, 0) \
V(PlatformConfigurationNativeApi::ScheduleFrame, 0) \
V(PlatformConfigurationNativeApi::Render, 1) \

View File

@ -168,6 +168,36 @@ class PlatformDispatcher {
// A map of opaque platform view identifiers to view configurations.
final Map<Object, ViewConfiguration> _viewConfigurations = <Object, ViewConfiguration>{};
/// The [FlutterView] provided by the engine if the platform is unable to
/// create windows, or, for backwards compatibility.
///
/// If the platform provides an implicit view, it can be used to bootstrap
/// the framework. This is common for platforms designed for single-view
/// applications like mobile devices with a single display.
///
/// Applications and libraries must not rely on this property being set
/// as it may be null depending on the engine's configuration. Instead,
/// consider using [View.of] to lookup the [FlutterView] the current
/// [BuildContext] is drawing into.
///
/// While the properties on the referenced [FlutterView] may change,
/// the reference itself is guaranteed to never change over the lifetime
/// of the application: if this property is null at startup, it will remain
/// so throughout the entire lifetime of the application. If it points to a
/// specific [FlutterView], it will continue to point to the same view until
/// the application is shut down (although the engine may replace or remove
/// the underlying backing surface of the view at its discretion).
///
/// See also:
///
/// * [View.of], for accessing the current view.
/// * [PlatformDisptacher.views] for a list of all [FlutterView]s provided
/// by the platform.
FlutterView? get implicitView => _implicitViewEnabled() ? _views[0] : null;
@Native<Handle Function()>(symbol: 'PlatformConfigurationNativeApi::ImplicitViewEnabled')
external static bool _implicitViewEnabled();
/// A callback that is invoked whenever the [ViewConfiguration] of any of the
/// [views] changes.
///

View File

@ -896,6 +896,7 @@ enum Brightness {
/// * [PlatformDispatcher.views], contains the current list of Flutter windows
/// belonging to the application, including top level application windows like
/// this one.
/// * [PlatformDispatcher.implicitView], this window's view.
final SingletonFlutterWindow window = SingletonFlutterWindow._(0, PlatformDispatcher.instance);
/// Additional data available on each flutter frame.

View File

@ -22,6 +22,8 @@
namespace flutter {
namespace {
constexpr int kImplicitViewId = 0;
Dart_Handle ToByteData(const fml::Mapping& buffer) {
return tonic::DartByteData::Create(buffer.GetMapping(), buffer.GetSize());
}
@ -67,8 +69,13 @@ void PlatformConfiguration::DidCreateIsolate() {
Dart_GetField(library, tonic::ToDart("_drawFrame")));
report_timings_.Set(tonic::DartState::Current(),
Dart_GetField(library, tonic::ToDart("_reportTimings")));
windows_.insert(std::make_pair(
0, std::make_unique<Window>(0, ViewportMetrics{1.0, 0.0, 0.0, -1})));
// TODO(loicsharma): This should only be created if the embedder enables the
// implicit view.
// See: https://github.com/flutter/flutter/issues/120306
windows_.emplace(kImplicitViewId,
std::make_unique<Window>(
kImplicitViewId, ViewportMetrics{1.0, 0.0, 0.0, -1}));
}
void PlatformConfiguration::UpdateLocales(
@ -427,6 +434,16 @@ Dart_Handle PlatformConfigurationNativeApi::ComputePlatformResolvedLocale(
return tonic::DartConverter<std::vector<std::string>>::ToDart(results);
}
Dart_Handle PlatformConfigurationNativeApi::ImplicitViewEnabled() {
UIDartState::ThrowIfUIOperationsProhibited();
bool enabled = UIDartState::Current()
->platform_configuration()
->client()
->ImplicitViewEnabled();
return Dart_NewBoolean(enabled);
}
std::string PlatformConfigurationNativeApi::DefaultRouteName() {
UIDartState::ThrowIfUIOperationsProhibited();
return UIDartState::Current()

View File

@ -49,6 +49,16 @@ enum class AccessibilityFeatureFlag : int32_t {
///
class PlatformConfigurationClient {
public:
//--------------------------------------------------------------------------
/// @brief Whether the platform provides an implicit view. If true,
/// the Framework may assume that it can always render into
/// the view with ID 0.
///
/// This value must not change for the lifetime of the
/// application.
///
virtual bool ImplicitViewEnabled() = 0;
//--------------------------------------------------------------------------
/// @brief The route or path that the embedder requested when the
/// application was launched.
@ -71,7 +81,7 @@ class PlatformConfigurationClient {
virtual void Render(Scene* scene) = 0;
//--------------------------------------------------------------------------
/// @brief Receives a updated semantics tree from the Framework.
/// @brief Receives an updated semantics tree from the Framework.
///
/// @param[in] update The updated semantic tree to apply.
///
@ -469,6 +479,8 @@ class PlatformMessageHandlerStorage {
//----------------------------------------------------------------------------
class PlatformConfigurationNativeApi {
public:
static Dart_Handle ImplicitViewEnabled();
static std::string DefaultRouteName();
static void ScheduleFrame();

View File

@ -33,6 +33,8 @@ abstract class PlatformDispatcher {
Iterable<FlutterView> get views;
FlutterView? get implicitView;
VoidCallback? get onMetricsChanged;
set onMetricsChanged(VoidCallback? callback);

View File

@ -148,6 +148,34 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
final Map<Object, ui.ViewConfiguration> _windowConfigurations =
<Object, ui.ViewConfiguration>{};
/// The [FlutterView] provided by the engine if the platform is unable to
/// create windows, or, for backwards compatibility.
///
/// If the platform provides an implicit view, it can be used to bootstrap
/// the framework. This is common for platforms designed for single-view
/// applications like mobile devices with a single display.
///
/// Applications and libraries must not rely on this property being set
/// as it may be null depending on the engine's configuration. Instead,
/// consider using [View.of] to lookup the [FlutterView] the current
/// [BuildContext] is drawing into.
///
/// While the properties on the referenced [FlutterView] may change,
/// the reference itself is guaranteed to never change over the lifetime
/// of the application: if this property is null at startup, it will remain
/// so throughout the entire lifetime of the application. If it points to a
/// specific [FlutterView], it will continue to point to the same view until
/// the application is shut down (although the engine may replace or remove
/// the underlying backing surface of the view at its discretion).
///
/// See also:
///
/// * [View.of], for accessing the current view.
/// * [PlatformDisptacher.views] for a list of all [FlutterView]s provided
/// by the platform.
@override
ui.FlutterView? get implicitView => viewData[kImplicitViewId];
/// A callback that is invoked whenever the platform's [devicePixelRatio],
/// [physicalSize], [padding], [viewInsets], or [systemGestureInsets]
/// values change, for example when the device is rotated or when the
@ -474,7 +502,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
// TODO(a-wallen): As multi-window support expands, the pop call
// will need to include the view ID. Right now only one view is
// supported.
(viewData[kSingletonViewId]! as EngineFlutterWindow)
(viewData[kImplicitViewId]! as EngineFlutterWindow)
.browserHistory
.exit()
.then((_) {
@ -579,7 +607,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
// TODO(a-wallen): As multi-window support expands, the navigation call
// will need to include the view ID. Right now only one view is
// supported.
(viewData[kSingletonViewId]! as EngineFlutterWindow)
(viewData[kImplicitViewId]! as EngineFlutterWindow)
.handleNavigationMessage(data)
.then((bool handled) {
if (handled) {
@ -1179,7 +1207,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
@override
String get defaultRouteName {
return _defaultRouteName ??=
(viewData[kSingletonViewId]! as EngineFlutterWindow).browserHistory.currentPath;
(viewData[kImplicitViewId]! as EngineFlutterWindow).browserHistory.currentPath;
}
/// Lazily initialized when the `defaultRouteName` getter is invoked.

View File

@ -27,8 +27,8 @@ typedef _HandleMessageCallBack = Future<bool> Function();
/// When set to true, all platform messages will be printed to the console.
const bool debugPrintPlatformMessages = false;
/// The view ID for a singleton flutter window.
const int kSingletonViewId = 0;
/// The view ID for the implicit flutter view provided by the platform.
const int kImplicitViewId = 0;
/// Whether [_customUrlStrategy] has been set or not.
///
@ -349,7 +349,7 @@ class EngineSingletonFlutterWindow extends EngineFlutterWindow {
/// API surface, providing Web-specific functionality that the standard
/// `dart:ui` version does not.
final EngineSingletonFlutterWindow window =
EngineSingletonFlutterWindow(kSingletonViewId, EnginePlatformDispatcher.instance);
EngineSingletonFlutterWindow(kImplicitViewId, EnginePlatformDispatcher.instance);
/// The Web implementation of [ui.WindowPadding].
class WindowPadding implements ui.WindowPadding {

View File

@ -41,6 +41,13 @@ void testMain() {
await window.resetHistory();
});
// For now, web always has an implicit view provided by the web engine.
test('EnginePlatformDispatcher.instance.implicitView should be non-null', () async {
expect(EnginePlatformDispatcher.instance.implicitView, isNotNull);
expect(EnginePlatformDispatcher.instance.implicitView?.viewId, 0);
expect(window.viewId, 0);
});
test('window.defaultRouteName should work with JsUrlStrategy', () async {
dynamic state = <dynamic, dynamic>{};
final JsUrlStrategy jsUrlStrategy = JsUrlStrategy(

View File

@ -298,6 +298,11 @@ RuntimeController::GetPlatformConfigurationIfAvailable() {
return root_isolate ? root_isolate->platform_configuration() : nullptr;
}
// |PlatformConfigurationClient|
bool RuntimeController::ImplicitViewEnabled() {
return client_.ImplicitViewEnabled();
}
// |PlatformConfigurationClient|
std::string RuntimeController::DefaultRouteName() {
return client_.DefaultRouteName();

View File

@ -610,6 +610,9 @@ class RuntimeController : public PlatformConfigurationClient {
bool FlushRuntimeStateToIsolate();
// |PlatformConfigurationClient|
bool ImplicitViewEnabled() override;
// |PlatformConfigurationClient|
std::string DefaultRouteName() override;

View File

@ -21,6 +21,8 @@ namespace flutter {
class RuntimeDelegate {
public:
virtual bool ImplicitViewEnabled() = 0;
virtual std::string DefaultRouteName() = 0;
virtual void ScheduleFrame(bool regenerate_layer_tree = true) = 0;

View File

@ -431,6 +431,14 @@ void Engine::SetAccessibilityFeatures(int32_t flags) {
runtime_controller_->SetAccessibilityFeatures(flags);
}
bool Engine::ImplicitViewEnabled() {
// TODO(loicsharma): This value should be provided by the embedder
// when it launches the engine. For now, assume the embedder always creates a
// view.
// See: https://github.com/flutter/flutter/issues/120306
return true;
}
std::string Engine::DefaultRouteName() {
if (!initial_route_.empty()) {
return initial_route_;

View File

@ -888,6 +888,9 @@ class Engine final : public RuntimeDelegate, PointerDataDispatcher::Delegate {
const std::weak_ptr<VsyncWaiter> GetVsyncWaiter() const;
private:
// |RuntimeDelegate|
bool ImplicitViewEnabled() override;
// |RuntimeDelegate|
std::string DefaultRouteName() override;

View File

@ -48,6 +48,7 @@ class MockResponse : public PlatformMessageResponse {
class MockRuntimeDelegate : public RuntimeDelegate {
public:
MOCK_METHOD0(ImplicitViewEnabled, bool());
MOCK_METHOD0(DefaultRouteName, std::string());
MOCK_METHOD1(ScheduleFrame, void(bool));
MOCK_METHOD1(Render, void(std::shared_ptr<flutter::LayerTree>));

View File

@ -45,8 +45,15 @@ void executableNameNotNull() {
notifyStringValue(Platform.executable);
}
@pragma('vm:entry-point')
void implicitViewNotNull() {
notifyBoolValue(PlatformDispatcher.instance.implicitView != null);
}
@pragma('vm:external-name', 'NotifyStringValue')
external void notifyStringValue(String value);
@pragma('vm:external-name', 'NotifyBoolValue')
external void notifyBoolValue(bool value);
@pragma('vm:entry-point')
void invokePlatformTaskRunner() {

View File

@ -174,6 +174,30 @@ TEST_F(EmbedderTest, ExecutableNameNotNull) {
latch.Wait();
}
TEST_F(EmbedderTest, ImplicitViewNotNull) {
// TODO(loicsharma): Update this test when embedders can opt-out
// of the implicit view.
// See: https://github.com/flutter/flutter/issues/120306
auto& context = GetEmbedderContext(EmbedderTestContextType::kSoftwareContext);
bool implicitViewNotNull = false;
fml::AutoResetWaitableEvent latch;
context.AddNativeCallback(
"NotifyBoolValue", CREATE_NATIVE_ENTRY([&](Dart_NativeArguments args) {
implicitViewNotNull = tonic::DartConverter<bool>::FromDart(
Dart_GetNativeArgument(args, 0));
latch.Signal();
}));
EmbedderConfigBuilder builder(context);
builder.SetSoftwareRendererConfig();
builder.SetDartEntrypoint("implicitViewNotNull");
auto engine = builder.LaunchEngine();
latch.Wait();
EXPECT_TRUE(implicitViewNotNull);
}
std::atomic_size_t EmbedderTestTaskRunner::sEmbedderTaskRunnerIdentifiers = {};
TEST_F(EmbedderTest, CanSpecifyCustomPlatformTaskRunner) {