[release] Snap to 56f0418878
Change-Id: I7992f8f072f57c8fbc40ded5850d598c0b271caa
diff --git a/examples/localized_flutter/localized_flutter_app/meta/localized_flutter_app.cml b/examples/localized_flutter/localized_flutter_app/meta/localized_flutter_app.cml
index 98ed8e3..7f61e83 100644
--- a/examples/localized_flutter/localized_flutter_app/meta/localized_flutter_app.cml
+++ b/examples/localized_flutter/localized_flutter_app/meta/localized_flutter_app.cml
@@ -20,7 +20,6 @@
use: [
{
protocol: [
- "fuchsia.cobalt.LoggerFactory",
"fuchsia.fonts.Provider",
"fuchsia.intl.PropertyProvider",
"fuchsia.sysmem.Allocator",
diff --git a/session_shells/BUILD.gn b/session_shells/BUILD.gn
index ed3b982..c4ac1f4 100644
--- a/session_shells/BUILD.gn
+++ b/session_shells/BUILD.gn
@@ -25,5 +25,5 @@
group("rust_unittests") {
testonly = true
- deps = [ "gazelle/appkit:appkit-tests" ]
+ deps = [ "gazelle:tests" ]
}
diff --git a/session_shells/development_flags.gni b/session_shells/development_flags.gni
new file mode 100644
index 0000000..6cd397f
--- /dev/null
+++ b/session_shells/development_flags.gni
@@ -0,0 +1,14 @@
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# These arguments are here for local development ONLY. They MUST NOT be
+# referenced in a product.gni file or modified by anyone other than a developer
+# configuring their `fx set`.
+declare_args() {
+ # Whether to use "ermine" or "gazelle" as the application shell in
+ # workstation.
+ #
+ # TODO(fxbug.dev/110571): Remove this.
+ application_shell = "ermine"
+}
diff --git a/session_shells/ermine/fidl/BUILD.gn b/session_shells/ermine/fidl/BUILD.gn
index 4b07a13..92c1c93 100644
--- a/session_shells/ermine/fidl/BUILD.gn
+++ b/session_shells/ermine/fidl/BUILD.gn
@@ -4,7 +4,7 @@
import("//build/fidl/fidl.gni")
group("fidl") {
- public_deps = [ ":ermine.tools" ]
+ public_deps = [ ":ermine.tools_dart($dart_toolchain)" ]
}
fidl("ermine.tools") {
diff --git a/session_shells/ermine/keyboard_shortcuts/BUILD.gn b/session_shells/ermine/keyboard_shortcuts/BUILD.gn
index 9fc739f..85c40a4 100644
--- a/session_shells/ermine/keyboard_shortcuts/BUILD.gn
+++ b/session_shells/ermine/keyboard_shortcuts/BUILD.gn
@@ -17,9 +17,9 @@
deps = [
"//sdk/dart/fuchsia_services",
"//sdk/dart/zircon",
- "//sdk/fidl/fuchsia.input",
- "//sdk/fidl/fuchsia.ui.shortcut",
- "//sdk/fidl/fuchsia.ui.views",
+ "//sdk/fidl/fuchsia.input:fuchsia.input_dart",
+ "//sdk/fidl/fuchsia.ui.shortcut:fuchsia.ui.shortcut_dart",
+ "//sdk/fidl/fuchsia.ui.views:fuchsia.ui.views_dart",
"//third_party/dart-pkg/git/flutter/packages/flutter",
"//third_party/dart-pkg/pub/meta",
]
@@ -34,9 +34,9 @@
":keyboard_shortcuts",
"//sdk/dart/fidl",
"//sdk/dart/zircon",
- "//sdk/fidl/fuchsia.input",
- "//sdk/fidl/fuchsia.ui.shortcut",
- "//sdk/fidl/fuchsia.ui.views",
+ "//sdk/fidl/fuchsia.input:fuchsia.input_dart",
+ "//sdk/fidl/fuchsia.ui.shortcut:fuchsia.ui.shortcut_dart",
+ "//sdk/fidl/fuchsia.ui.views:fuchsia.ui.views_dart",
"//third_party/dart-pkg/git/flutter/packages/flutter_test",
"//third_party/dart-pkg/pub/mockito",
"//third_party/dart-pkg/pub/test",
diff --git a/session_shells/ermine/login/BUILD.gn b/session_shells/ermine/login/BUILD.gn
index f02031c..3a658a8 100644
--- a/session_shells/ermine/login/BUILD.gn
+++ b/session_shells/ermine/login/BUILD.gn
@@ -7,6 +7,7 @@
import("//build/fidl/fidl.gni")
import("//build/flutter/flutter_component.gni")
import("//build/testing/flutter_driver.gni")
+import("//src/experiences/session_shells/development_flags.gni")
declare_args() {
# Whether or not to provide the data sharing consent step in OOBE
@@ -62,24 +63,24 @@
"//sdk/dart/fuchsia_services",
"//sdk/dart/fuchsia_vfs",
"//sdk/dart/zircon",
- "//sdk/fidl/fuchsia.component",
- "//sdk/fidl/fuchsia.component.decl",
- "//sdk/fidl/fuchsia.element",
- "//sdk/fidl/fuchsia.feedback",
- "//sdk/fidl/fuchsia.hardware.power.statecontrol",
- "//sdk/fidl/fuchsia.identity.account",
- "//sdk/fidl/fuchsia.intl",
- "//sdk/fidl/fuchsia.io",
- "//sdk/fidl/fuchsia.mem",
- "//sdk/fidl/fuchsia.recovery",
- "//sdk/fidl/fuchsia.settings",
- "//sdk/fidl/fuchsia.ssh",
- "//sdk/fidl/fuchsia.sys",
- "//sdk/fidl/fuchsia.ui.app",
- "//sdk/fidl/fuchsia.ui.focus",
- "//sdk/fidl/fuchsia.ui.scenic",
- "//sdk/fidl/fuchsia.ui.views",
- "//sdk/fidl/fuchsia.update.channelcontrol",
+ "//sdk/fidl/fuchsia.component:fuchsia.component_dart",
+ "//sdk/fidl/fuchsia.component.decl:fuchsia.component.decl_dart",
+ "//sdk/fidl/fuchsia.element:fuchsia.element_dart",
+ "//sdk/fidl/fuchsia.feedback:fuchsia.feedback_dart",
+ "//sdk/fidl/fuchsia.hardware.power.statecontrol:fuchsia.hardware.power.statecontrol_dart",
+ "//sdk/fidl/fuchsia.identity.account:fuchsia.identity.account_dart",
+ "//sdk/fidl/fuchsia.intl:fuchsia.intl_dart",
+ "//sdk/fidl/fuchsia.io:fuchsia.io_dart",
+ "//sdk/fidl/fuchsia.mem:fuchsia.mem_dart",
+ "//sdk/fidl/fuchsia.recovery:fuchsia.recovery_dart",
+ "//sdk/fidl/fuchsia.settings:fuchsia.settings_dart",
+ "//sdk/fidl/fuchsia.ssh:fuchsia.ssh_dart",
+ "//sdk/fidl/fuchsia.sys:fuchsia.sys_dart",
+ "//sdk/fidl/fuchsia.ui.app:fuchsia.ui.app_dart",
+ "//sdk/fidl/fuchsia.ui.focus:fuchsia.ui.focus_dart",
+ "//sdk/fidl/fuchsia.ui.scenic:fuchsia.ui.scenic_dart",
+ "//sdk/fidl/fuchsia.ui.views:fuchsia.ui.views_dart",
+ "//sdk/fidl/fuchsia.update.channelcontrol:fuchsia.update.channelcontrol_dart",
"//src/experiences/session_shells/ermine/fidl",
"//src/experiences/session_shells/ermine/internationalization",
"//src/experiences/session_shells/ermine/utils:ermine_utils",
@@ -103,7 +104,13 @@
component_name = "login"
- manifest = "meta/login.cml"
+ # TODO(fxbug.dev/100284): Make Ermine/Gazelle a dynamic child, rather than
+ # requiring these separate variants.
+ if (application_shell == "ermine") {
+ manifest = "meta/login_ermine.cml"
+ } else if (application_shell == "gazelle") {
+ manifest = "meta/login_gazelle.cml"
+ }
deps = [
":default_config",
diff --git a/session_shells/ermine/login/meta/login.cml b/session_shells/ermine/login/meta/login_common.shard.cml
similarity index 96%
rename from session_shells/ermine/login/meta/login.cml
rename to session_shells/ermine/login/meta/login_common.shard.cml
index a9142ba..17dbc69 100644
--- a/session_shells/ermine/login/meta/login.cml
+++ b/session_shells/ermine/login/meta/login_common.shard.cml
@@ -12,13 +12,6 @@
args: [ "--expose_dirs=hosted_directories" ],
data: "data/login",
},
- children: [
- {
- name: "ermine_shell",
- url: "fuchsia-pkg://fuchsia.com/ermine#meta/ermine.cm",
- startup: "lazy",
- },
- ],
capabilities: [
{
protocol: [
@@ -68,12 +61,12 @@
{
protocol: [
"fuchsia.accessibility.semantics.SemanticsManager",
- "fuchsia.cobalt.LoggerFactory",
"fuchsia.feedback.CrashReporter",
"fuchsia.fonts.Provider",
"fuchsia.hardware.power.statecontrol.Admin",
"fuchsia.identity.account.AccountManager",
"fuchsia.intl.PropertyProvider",
+ "fuchsia.metrics.MetricEventLoggerFactory",
"fuchsia.recovery.FactoryReset",
"fuchsia.settings.Intl",
"fuchsia.settings.Privacy",
@@ -99,7 +92,6 @@
"fuchsia.accessibility.semantics.SemanticsManager",
"fuchsia.buildinfo.Provider",
"fuchsia.camera3.DeviceWatcher",
- "fuchsia.cobalt.LoggerFactory",
"fuchsia.element.Manager",
"fuchsia.feedback.CrashReporter",
"fuchsia.fonts.Provider",
@@ -114,6 +106,7 @@
"fuchsia.mediacodec.CodecFactory",
"fuchsia.memory.Monitor",
"fuchsia.memorypressure.Provider",
+ "fuchsia.metrics.MetricEventLoggerFactory",
"fuchsia.net.interfaces.State",
"fuchsia.net.name.Lookup",
"fuchsia.posix.socket.Provider",
diff --git a/session_shells/ermine/login/meta/login_ermine.cml b/session_shells/ermine/login/meta/login_ermine.cml
new file mode 100644
index 0000000..3eadba9
--- /dev/null
+++ b/session_shells/ermine/login/meta/login_ermine.cml
@@ -0,0 +1,13 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+{
+ include: [ "//src/experiences/session_shells/ermine/login/meta/login_common.shard.cml" ],
+ children: [
+ {
+ name: "ermine_shell",
+ url: "fuchsia-pkg://fuchsia.com/ermine#meta/ermine.cm",
+ startup: "lazy",
+ },
+ ],
+}
diff --git a/session_shells/ermine/login/meta/login_gazelle.cml b/session_shells/ermine/login/meta/login_gazelle.cml
new file mode 100644
index 0000000..f1e5db5
--- /dev/null
+++ b/session_shells/ermine/login/meta/login_gazelle.cml
@@ -0,0 +1,18 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+{
+ include: [ "//src/experiences/session_shells/ermine/login/meta/login_common.shard.cml" ],
+ children: [
+ // Even though this is gazelle, we call it `ermine_shell` because it
+ // acts as a drop-in replacement.
+ //
+ // TODO(fxbug.dev/100284): Resolve this confusion by calling them both
+ // something like `application_shell`.
+ {
+ name: "ermine_shell",
+ url: "fuchsia-pkg://fuchsia.com/gazelle_shell#meta/gazelle_shell.cm",
+ startup: "lazy",
+ },
+ ],
+}
diff --git a/session_shells/ermine/session/BUILD.gn b/session_shells/ermine/session/BUILD.gn
index 8583e34..928735a 100644
--- a/session_shells/ermine/session/BUILD.gn
+++ b/session_shells/ermine/session/BUILD.gn
@@ -18,13 +18,13 @@
"//sdk/dart/fuchsia_services",
"//sdk/dart/fuchsia_vfs",
"//sdk/dart/zircon",
- "//sdk/fidl/fuchsia.component",
- "//sdk/fidl/fuchsia.component.decl",
- "//sdk/fidl/fuchsia.io",
- "//sdk/fidl/fuchsia.session.scene",
- "//sdk/fidl/fuchsia.ui.app",
- "//sdk/fidl/fuchsia.ui.input",
- "//sdk/fidl/fuchsia.ui.views",
+ "//sdk/fidl/fuchsia.component:fuchsia.component_dart",
+ "//sdk/fidl/fuchsia.component.decl:fuchsia.component.decl_dart",
+ "//sdk/fidl/fuchsia.io:fuchsia.io_dart",
+ "//sdk/fidl/fuchsia.session.scene:fuchsia.session.scene_dart",
+ "//sdk/fidl/fuchsia.ui.app:fuchsia.ui.app_dart",
+ "//sdk/fidl/fuchsia.ui.input:fuchsia.ui.input_dart",
+ "//sdk/fidl/fuchsia.ui.views:fuchsia.ui.views_dart",
]
}
diff --git a/session_shells/ermine/shell/BUILD.gn b/session_shells/ermine/shell/BUILD.gn
index 74cee4f..160cf2f 100644
--- a/session_shells/ermine/shell/BUILD.gn
+++ b/session_shells/ermine/shell/BUILD.gn
@@ -100,31 +100,31 @@
"//sdk/dart/fuchsia_scenic_flutter",
"//sdk/dart/fuchsia_services",
"//sdk/dart/zircon",
- "//sdk/fidl/fuchsia.buildinfo",
- "//sdk/fidl/fuchsia.element",
- "//sdk/fidl/fuchsia.feedback",
- "//sdk/fidl/fuchsia.hardware.power.statecontrol",
- "//sdk/fidl/fuchsia.input",
- "//sdk/fidl/fuchsia.intl",
- "//sdk/fidl/fuchsia.media",
- "//sdk/fidl/fuchsia.media.audio",
- "//sdk/fidl/fuchsia.memory",
- "//sdk/fidl/fuchsia.net.interfaces",
- "//sdk/fidl/fuchsia.power.battery",
- "//sdk/fidl/fuchsia.power.button",
- "//sdk/fidl/fuchsia.settings",
- "//sdk/fidl/fuchsia.ssh",
- "//sdk/fidl/fuchsia.ui.activity",
- "//sdk/fidl/fuchsia.ui.app",
- "//sdk/fidl/fuchsia.ui.brightness",
- "//sdk/fidl/fuchsia.ui.focus",
- "//sdk/fidl/fuchsia.ui.input",
- "//sdk/fidl/fuchsia.ui.shortcut",
- "//sdk/fidl/fuchsia.ui.views",
- "//sdk/fidl/fuchsia.update",
- "//sdk/fidl/fuchsia.update.channelcontrol",
- "//sdk/fidl/fuchsia.wlan.common",
- "//sdk/fidl/fuchsia.wlan.policy",
+ "//sdk/fidl/fuchsia.buildinfo:fuchsia.buildinfo_dart",
+ "//sdk/fidl/fuchsia.element:fuchsia.element_dart",
+ "//sdk/fidl/fuchsia.feedback:fuchsia.feedback_dart",
+ "//sdk/fidl/fuchsia.hardware.power.statecontrol:fuchsia.hardware.power.statecontrol_dart",
+ "//sdk/fidl/fuchsia.input:fuchsia.input_dart",
+ "//sdk/fidl/fuchsia.intl:fuchsia.intl_dart",
+ "//sdk/fidl/fuchsia.media:fuchsia.media_dart",
+ "//sdk/fidl/fuchsia.media.audio:fuchsia.media.audio_dart",
+ "//sdk/fidl/fuchsia.memory:fuchsia.memory_dart",
+ "//sdk/fidl/fuchsia.net.interfaces:fuchsia.net.interfaces_dart",
+ "//sdk/fidl/fuchsia.power.battery:fuchsia.power.battery_dart",
+ "//sdk/fidl/fuchsia.power.button:fuchsia.power.button_dart",
+ "//sdk/fidl/fuchsia.settings:fuchsia.settings_dart",
+ "//sdk/fidl/fuchsia.ssh:fuchsia.ssh_dart",
+ "//sdk/fidl/fuchsia.ui.activity:fuchsia.ui.activity_dart",
+ "//sdk/fidl/fuchsia.ui.app:fuchsia.ui.app_dart",
+ "//sdk/fidl/fuchsia.ui.brightness:fuchsia.ui.brightness_dart",
+ "//sdk/fidl/fuchsia.ui.focus:fuchsia.ui.focus_dart",
+ "//sdk/fidl/fuchsia.ui.input:fuchsia.ui.input_dart",
+ "//sdk/fidl/fuchsia.ui.shortcut:fuchsia.ui.shortcut_dart",
+ "//sdk/fidl/fuchsia.ui.views:fuchsia.ui.views_dart",
+ "//sdk/fidl/fuchsia.update:fuchsia.update_dart",
+ "//sdk/fidl/fuchsia.update.channelcontrol:fuchsia.update.channelcontrol_dart",
+ "//sdk/fidl/fuchsia.wlan.common:fuchsia.wlan.common_dart",
+ "//sdk/fidl/fuchsia.wlan.policy:fuchsia.wlan.policy_dart",
"//src/experiences/session_shells/ermine/fidl",
"//src/experiences/session_shells/ermine/internationalization",
"//src/experiences/session_shells/ermine/keyboard_shortcuts",
diff --git a/session_shells/ermine/shell/lib/src/states/app_state_impl.dart b/session_shells/ermine/shell/lib/src/states/app_state_impl.dart
index 2f98553..8701fc3 100644
--- a/session_shells/ermine/shell/lib/src/states/app_state_impl.dart
+++ b/session_shells/ermine/shell/lib/src/states/app_state_impl.dart
@@ -43,6 +43,7 @@
static const kFeedbackUrl =
'https://fuchsia.dev/fuchsia-src/contribute/report-issue';
+ static const kChromeElementManager = 'fuchsia.element.Manager-chrome';
static const kLicenseUrl =
'fuchsia-pkg://fuchsia.com/license_settings#meta/license_settings.cm';
static const kScreenSaverUrl =
@@ -408,6 +409,11 @@
(String title, String url, String? alternateServiceName) async {
try {
_clearError(url, 'ProposeElementError');
+
+ // For web urls use Chrome's element manager service.
+ if (url.startsWith('http')) {
+ alternateServiceName ??= kChromeElementManager;
+ }
await launchService.launch(title, url,
alternateServiceName: alternateServiceName);
// Hide app launcher unless we had an error presenting the view.
diff --git a/session_shells/ermine/shell/meta/ermine.cml b/session_shells/ermine/shell/meta/ermine.cml
index bbe1766..427ce8b 100644
--- a/session_shells/ermine/shell/meta/ermine.cml
+++ b/session_shells/ermine/shell/meta/ermine.cml
@@ -42,7 +42,6 @@
protocol: [
"fuchsia.accessibility.semantics.SemanticsManager",
"fuchsia.buildinfo.Provider",
- "fuchsia.cobalt.LoggerFactory",
"fuchsia.feedback.CrashReporter",
"fuchsia.fonts.Provider",
"fuchsia.hardware.power.statecontrol.Admin",
@@ -51,6 +50,7 @@
"fuchsia.media.Audio",
"fuchsia.media.AudioCore",
"fuchsia.memory.Monitor",
+ "fuchsia.metrics.MetricEventLoggerFactory",
"fuchsia.net.interfaces.State",
"fuchsia.power.battery.BatteryManager",
"fuchsia.power.button.Monitor",
diff --git a/session_shells/ermine/utils/BUILD.gn b/session_shells/ermine/utils/BUILD.gn
index 6134b1e..b32cf83 100644
--- a/session_shells/ermine/utils/BUILD.gn
+++ b/session_shells/ermine/utils/BUILD.gn
@@ -29,9 +29,9 @@
"//sdk/dart/fuchsia_scenic_flutter",
"//sdk/dart/fuchsia_services",
"//sdk/dart/zircon",
- "//sdk/fidl/fuchsia.feedback",
- "//sdk/fidl/fuchsia.mem",
- "//sdk/fidl/fuchsia.ui.views",
+ "//sdk/fidl/fuchsia.feedback:fuchsia.feedback_dart",
+ "//sdk/fidl/fuchsia.mem:fuchsia.mem_dart",
+ "//sdk/fidl/fuchsia.ui.views:fuchsia.ui.views_dart",
"//src/experiences/session_shells/ermine/internationalization",
"//third_party/dart-pkg/git/flutter/packages/flutter",
"//third_party/dart-pkg/pub/flutter_mobx",
diff --git a/session_shells/gazelle/BUILD.gn b/session_shells/gazelle/BUILD.gn
index c46275d..f4e5186 100644
--- a/session_shells/gazelle/BUILD.gn
+++ b/session_shells/gazelle/BUILD.gn
@@ -6,12 +6,16 @@
group("gazelle") {
public_deps = [
- "session",
+ "shell",
"wm",
]
}
group("tests") {
testonly = true
- deps = [ "wm:tests" ]
+ deps = [
+ "appkit:tests",
+ "pointer_fusion:tests",
+ "wm:tests",
+ ]
}
diff --git a/session_shells/gazelle/README.md b/session_shells/gazelle/README.md
index ae43fd8..610c0cf 100644
--- a/session_shells/gazelle/README.md
+++ b/session_shells/gazelle/README.md
@@ -7,8 +7,10 @@
To run it:
- fx set workstation_eng_paused.BOARD --with //src/experiences/session_shells/gazelle
+ fx set workstation_eng_paused.BOARD \
+ --with //src/experiences/session_shells/{ermine,gazelle} \
+ '--args=application_shell="gazelle"'
fx build
- ffx session launch fuchsia-pkg://fuchsia.com/gazelle_session#meta/gazelle_session.cm
+ ffx session launch fuchsia-pkg://fuchsia.com/workstation_routing#meta/workstation_routing.cm
[wiki-gazelle]: https://en.wikipedia.org/wiki/Gazelle
diff --git a/session_shells/gazelle/appkit/BUILD.gn b/session_shells/gazelle/appkit/BUILD.gn
index 064e7dd..d8dda0d 100644
--- a/session_shells/gazelle/appkit/BUILD.gn
+++ b/session_shells/gazelle/appkit/BUILD.gn
@@ -3,6 +3,7 @@
# found in the LICENSE file.
import("//build/components.gni")
import("//build/rust/rustc_library.gni")
+import("//src/lib/vulkan/vulkan.gni")
rustc_library("appkit") {
name = "appkit"
@@ -22,6 +23,7 @@
"//sdk/fidl/fuchsia.math:fuchsia.math_rust",
"//sdk/fidl/fuchsia.ui.composition:fuchsia.ui.composition_rust",
"//sdk/fidl/fuchsia.ui.input3:fuchsia.ui.input3_rust",
+ "//sdk/fidl/fuchsia.ui.pointer:fuchsia.ui.pointer_rust",
"//sdk/fidl/fuchsia.ui.views:fuchsia.ui.views_rust",
"//src/lib/async-utils",
"//src/lib/fidl/rust/fidl",
@@ -33,7 +35,9 @@
"//third_party/rust_crates:tracing",
]
test_deps = [
+ "//sdk/fidl/fuchsia.input:fuchsia.input_rust",
"//sdk/fidl/fuchsia.ui.app:fuchsia.ui.app_rust",
+ "//sdk/fidl/fuchsia.ui.test.input:fuchsia.ui.test.input_rust",
"//sdk/fidl/fuchsia.ui.test.scene:fuchsia.ui.test.scene_rust",
"//src/lib/fuchsia",
]
@@ -52,13 +56,13 @@
log_settings = {
max_severity = "ERROR"
}
- environments = [
- {
- dimensions = {
- # Ensure the device has Vulkan.
- device_type = "AEMU"
- }
- },
- ]
+
+ # Ensure the device has Vulkan.
+ environments = vulkan_envs
}
}
+
+group("tests") {
+ testonly = true
+ deps = [ ":appkit-tests" ]
+}
diff --git a/session_shells/gazelle/appkit/meta/appkit_lib_test.cml b/session_shells/gazelle/appkit/meta/appkit_lib_test.cml
index 14c8a1b..040c366 100644
--- a/session_shells/gazelle/appkit/meta/appkit_lib_test.cml
+++ b/session_shells/gazelle/appkit/meta/appkit_lib_test.cml
@@ -25,6 +25,7 @@
"fuchsia.session.scene.Manager",
"fuchsia.ui.composition.Flatland",
"fuchsia.ui.input3.Keyboard",
+ "fuchsia.ui.test.input.Registry",
"fuchsia.ui.test.scene.Controller",
],
from: "#test-ui-stack",
diff --git a/session_shells/gazelle/appkit/src/child_view.rs b/session_shells/gazelle/appkit/src/child_view.rs
index a55d7d4..ce4b782 100644
--- a/session_shells/gazelle/appkit/src/child_view.rs
+++ b/session_shells/gazelle/appkit/src/child_view.rs
@@ -5,13 +5,13 @@
use {
anyhow::{format_err, Error},
fidl::endpoints::{create_proxy, Proxy},
- fidl_fuchsia_math as fmath, fidl_fuchsia_ui_composition as ui_comp,
- futures::future::AbortHandle,
+ fidl_fuchsia_math as fmath, fidl_fuchsia_ui_composition as ui_comp, fuchsia_async as fasync,
+ tracing::*,
};
use crate::{
event::{ChildViewEvent, Event, ViewSpecHolder},
- utils::{spawn_abortable, EventSender},
+ utils::EventSender,
window::WindowId,
};
@@ -32,13 +32,7 @@
viewport_content_id: ui_comp::ContentId,
_window_id: WindowId,
_event_sender: EventSender<T>,
- services_abort: Option<AbortHandle>,
-}
-
-impl<T> Drop for ChildView<T> {
- fn drop(&mut self) {
- self.services_abort.take().map(|a| a.abort());
- }
+ _running_task: fasync::Task<()>,
}
impl<T> ChildView<T> {
@@ -74,7 +68,9 @@
child_view_watcher_request,
)?;
- view_spec_holder.responder.send(&mut Ok(()))?;
+ if let Some(responder) = view_spec_holder.responder {
+ responder.send(&mut Ok(())).expect("Failed to respond to GraphicalPresent.present")
+ }
let child_view_id = ChildViewId::from_viewport_content_id(viewport_content_id);
let child_view_watcher_fut = Self::start_child_view_watcher(
@@ -84,14 +80,13 @@
event_sender.clone(),
);
- let abort_handle = spawn_abortable(child_view_watcher_fut);
- let services_abort = Some(abort_handle);
+ let _running_task = fasync::Task::spawn(child_view_watcher_fut);
Ok(ChildView {
viewport_content_id,
_window_id: window_id,
_event_sender: event_sender,
- services_abort,
+ _running_task,
})
}
@@ -109,19 +104,25 @@
window_id: WindowId,
event_sender: EventSender<T>,
) {
- if let Ok(_) = child_view_watcher_proxy.get_status().await {
- event_sender
- .send(Event::ChildViewEvent(child_view_id, window_id, ChildViewEvent::Available))
- .expect("Failed to send ChildView::Available event");
- }
- if let Ok(view_ref) = child_view_watcher_proxy.get_view_ref().await {
- event_sender
- .send(Event::ChildViewEvent(
+ match child_view_watcher_proxy.get_status().await {
+ Ok(_) => event_sender
+ .send(Event::ChildViewEvent {
child_view_id,
window_id,
- ChildViewEvent::Attached(view_ref),
- ))
- .expect("Failed to send ChildView::Attached event");
+ event: ChildViewEvent::Available,
+ })
+ .expect("Failed to send ChildView::Available event"),
+ Err(err) => error!("ChildViewWatcher.get_status return error: {:?}", err),
+ }
+ match child_view_watcher_proxy.get_view_ref().await {
+ Ok(view_ref) => event_sender
+ .send(Event::ChildViewEvent {
+ child_view_id,
+ window_id,
+ event: ChildViewEvent::Attached { view_ref },
+ })
+ .expect("Failed to send ChildView::Attached event"),
+ Err(err) => error!("ChildViewWatcher.get_view_ref return error: {:?}", err),
}
// After retrieving status and viewRef, we can only wait for the channel to close. This is a
@@ -129,7 +130,11 @@
// [felement::ViewController]'s dismiss method.
let _ = child_view_watcher_proxy.on_closed().await;
event_sender
- .send(Event::ChildViewEvent(child_view_id, window_id, ChildViewEvent::Detached))
+ .send(Event::ChildViewEvent {
+ child_view_id,
+ window_id,
+ event: ChildViewEvent::Detached,
+ })
.expect("Failed to send ChildView::Detached event");
}
}
diff --git a/session_shells/gazelle/appkit/src/event.rs b/session_shells/gazelle/appkit/src/event.rs
index 0e6d593..8a96b46 100644
--- a/session_shells/gazelle/appkit/src/event.rs
+++ b/session_shells/gazelle/appkit/src/event.rs
@@ -5,7 +5,7 @@
use {
fidl::endpoints::{ClientEnd, ServerEnd},
fidl_fuchsia_element as felement, fidl_fuchsia_ui_input3 as ui_input3,
- fidl_fuchsia_ui_views as ui_views,
+ fidl_fuchsia_ui_pointer as fptr, fidl_fuchsia_ui_views as ui_views,
};
use crate::{child_view::ChildViewId, window::WindowId};
@@ -16,13 +16,13 @@
/// Use Init to perform one-time app initialization.
Init,
/// Set of system level events that are window agnostic.
- SystemEvent(SystemEvent),
+ SystemEvent { event: SystemEvent },
/// Set of device level events that are window agnostic.
DeviceEvent,
/// Set of events that apply to a window instance.
- WindowEvent(WindowId, WindowEvent),
+ WindowEvent { window_id: WindowId, event: WindowEvent },
/// Set of events that apply to embedded child views.
- ChildViewEvent(ChildViewId, WindowId, ChildViewEvent),
+ ChildViewEvent { child_view_id: ChildViewId, window_id: WindowId, event: ChildViewEvent },
/// Used to route application specific events T.
UserEvent(T),
/// Use to notify the event processing loop to terminate.
@@ -34,9 +34,9 @@
pub enum SystemEvent {
/// Can be used to create a window from a [ui_views::ViewCreationToken]. This is useful only
/// for ViewProvider based applications.
- ViewCreationToken(ui_views::ViewCreationToken),
+ ViewCreationToken { token: ui_views::ViewCreationToken },
/// Used for creating a child view given a ViewSpecHolder using [window.create_child_view].
- PresentViewSpec(ViewSpecHolder),
+ PresentViewSpec { view_spec_holder: ViewSpecHolder },
}
/// The next future presentation time, expressed in nanoseconds in the `CLOCK_MONOTONIC` timebase.
@@ -46,13 +46,20 @@
#[derive(Debug)]
pub enum WindowEvent {
/// Window is resized. This is also the first event sent upon window creation.
- Resized(u32, u32),
+ Resized { width: u32, height: u32 },
/// Window needs to be redrawn. Sent upon receiving the next frame request from Flatland.
- NeedsRedraw(NextPresentTimeInNanos),
+ NeedsRedraw { next_present_time: NextPresentTimeInNanos },
/// Window has received or lost focus.
- Focused(bool),
+ Focused { focused: bool },
/// A keyboard key event when the window is in focus.
- Keyboard(ui_input3::KeyEvent, ui_input3::KeyboardListenerOnKeyEventResponder),
+ Keyboard {
+ event: ui_input3::KeyEvent,
+ responder: ui_input3::KeyboardListenerOnKeyEventResponder,
+ },
+ /// A mouse event received for this window.
+ Mouse { event: fptr::MouseEvent },
+ /// A touch event received for this window.
+ Touch { event: fptr::TouchEvent },
/// Window was closed by the [GraphicalPresenter] presenting this window.
Closed,
}
@@ -63,7 +70,7 @@
/// Child view is created but not attached to the view tree yet.
Available,
/// Child view is attached to the view tree.
- Attached(ui_views::ViewRef),
+ Attached { view_ref: ui_views::ViewRef },
/// Child view is detached from the view tree.
Detached,
}
@@ -75,5 +82,5 @@
pub view_spec: felement::ViewSpec,
pub annotation_controller: Option<ClientEnd<felement::AnnotationControllerMarker>>,
pub view_controller_request: Option<ServerEnd<felement::ViewControllerMarker>>,
- pub responder: felement::GraphicalPresenterPresentViewResponder,
+ pub responder: Option<felement::GraphicalPresenterPresentViewResponder>,
}
diff --git a/session_shells/gazelle/appkit/src/tests.rs b/session_shells/gazelle/appkit/src/tests.rs
index 2b4bc67..ee861ee 100644
--- a/session_shells/gazelle/appkit/src/tests.rs
+++ b/session_shells/gazelle/appkit/src/tests.rs
@@ -4,9 +4,13 @@
use {
anyhow::Error,
- fidl::endpoints::{create_proxy_and_stream, create_request_stream},
- fidl_fuchsia_element as felement, fidl_fuchsia_ui_app as ui_app,
- fidl_fuchsia_ui_test_scene as ui_test_scene, fuchsia_async as fasync,
+ fidl::endpoints::{create_proxy, create_proxy_and_stream, create_request_stream},
+ fidl_fuchsia_element as felement,
+ fidl_fuchsia_input::Key,
+ fidl_fuchsia_ui_app as ui_app,
+ fidl_fuchsia_ui_input3::{KeyEvent, KeyEventStatus},
+ fidl_fuchsia_ui_test_input as ui_test_input, fidl_fuchsia_ui_test_scene as ui_test_scene,
+ fuchsia_async as fasync,
fuchsia_component::client::connect_to_protocol,
fuchsia_scenic::flatland::ViewCreationTokenPair,
futures::future::{AbortHandle, Abortable},
@@ -19,7 +23,7 @@
child_view::{ChildView, ChildViewId},
event::{ChildViewEvent, Event, SystemEvent, ViewSpecHolder, WindowEvent},
utils::EventSender,
- window::{Window, WindowBuilder, WindowId},
+ window::{Window, WindowId},
};
#[derive(Debug)]
@@ -64,40 +68,51 @@
info!("------ParentView {:?}", event);
match event {
Event::Init => {}
- Event::WindowEvent(id, window_event) => match window_event {
- WindowEvent::Resized(width, height) => {
+ Event::WindowEvent { window_id: id, event: window_event } => match window_event {
+ WindowEvent::Resized { width, height } => {
app.width = width;
app.height = height;
if app.active_window.is_none() {
app.active_window = Some(id);
let cloned_graphical_presenter = graphical_presenter_proxy.clone();
+ let cloned_event_sender = event_sender.clone();
fasync::Task::spawn(async move {
- create_child_view_spec(cloned_graphical_presenter)
+ create_child_view_spec(cloned_graphical_presenter, cloned_event_sender)
.await
.expect("Failed to create_child_view");
})
.detach();
}
}
- WindowEvent::NeedsRedraw(_) => {
+ WindowEvent::NeedsRedraw { .. } => {
assert!(
app.width > 0 && app.height > 0,
"Redraw event received before window was resized"
);
}
+ WindowEvent::Keyboard { event, responder } => {
+ if let KeyEvent { key: Some(Key::Q), .. } = event {
+ event_sender.send(Event::Exit).expect("Failed to send Event::Exit event");
+ responder
+ .send(KeyEventStatus::Handled)
+ .expect("Failed to respond to keyboard event");
+ } else {
+ responder
+ .send(KeyEventStatus::NotHandled)
+ .expect("Failed to respond to keyboard event");
+ }
+ }
_ => {}
},
- Event::SystemEvent(system_event) => match system_event {
- SystemEvent::ViewCreationToken(view_creation_token) => {
- let mut window = WindowBuilder::new()
- .with_view_creation_token(view_creation_token)
- .build(event_sender.clone())
- .unwrap();
+ Event::SystemEvent { event: system_event } => match system_event {
+ SystemEvent::ViewCreationToken { token: view_creation_token } => {
+ let mut window = Window::new(event_sender.clone())
+ .with_view_creation_token(view_creation_token);
window.create_view().expect("Failed to create view for window");
app.windows.insert(window.id(), window);
}
- SystemEvent::PresentViewSpec(view_spec_holder) => {
+ SystemEvent::PresentViewSpec { view_spec_holder } => {
let window = app.windows.get_mut(&app.active_window.unwrap()).unwrap();
let child_view = window
.create_child_view(
@@ -110,7 +125,7 @@
app.child_views.insert(child_view.id(), child_view);
}
},
- Event::ChildViewEvent(child_view_id, window_id, child_view_event) => {
+ Event::ChildViewEvent { child_view_id, window_id, event: child_view_event } => {
let window = app.windows.get_mut(&window_id).unwrap();
let child_view = app.child_views.get_mut(&child_view_id).unwrap();
@@ -122,9 +137,9 @@
);
window.redraw();
}
- ChildViewEvent::Attached(view_ref) => {
+ ChildViewEvent::Attached { view_ref } => {
+ // Set focus to child view.
window.request_focus(view_ref);
- event_sender.send(Event::Exit).expect("Failed to send Event::Exit event");
}
ChildViewEvent::Detached => {}
}
@@ -178,14 +193,21 @@
};
let view_provider_fut = async move {
- match view_provider_request_stream.next().await.unwrap() {
+ match view_provider_request_stream
+ .next()
+ .await
+ .expect("Failed to read ViewProvider request stream")
+ {
Ok(ui_app::ViewProviderRequest::CreateView2 { args, .. }) => {
event_sender
- .send(Event::SystemEvent(SystemEvent::ViewCreationToken(
- args.view_creation_token.unwrap(),
- )))
+ .send(Event::SystemEvent {
+ event: SystemEvent::ViewCreationToken {
+ token: args.view_creation_token.unwrap(),
+ },
+ })
.expect("Failed to send SystemEvent::ViewCreationToken event");
}
+ // Panic for all other CreateView requests and errors to fail the test.
_ => panic!("ViewProvider impl only handles CreateView2()"),
}
};
@@ -210,12 +232,16 @@
responder,
} => {
event_sender
- .send(Event::SystemEvent(SystemEvent::PresentViewSpec(ViewSpecHolder {
- view_spec,
- annotation_controller,
- view_controller_request,
- responder,
- })))
+ .send(Event::SystemEvent {
+ event: SystemEvent::PresentViewSpec {
+ view_spec_holder: ViewSpecHolder {
+ view_spec,
+ annotation_controller,
+ view_controller_request,
+ responder: Some(responder),
+ },
+ },
+ })
.expect("Failed to send SystemEvent::PresentViewSpec event");
}
}
@@ -224,6 +250,7 @@
async fn create_child_view_spec(
graphical_presenter: felement::GraphicalPresenterProxy,
+ parent_sender: EventSender<TestEvent>,
) -> Result<(), Error> {
let ViewCreationTokenPair { view_creation_token, viewport_creation_token } =
ViewCreationTokenPair::new()?;
@@ -233,6 +260,23 @@
};
let _ = graphical_presenter.present_view(view_spec, None, None).await;
+ let (keyboard, keyboard_server) = create_proxy::<ui_test_input::KeyboardMarker>()?;
+ let input_registry = connect_to_protocol::<ui_test_input::RegistryMarker>()?;
+ input_registry
+ .register_keyboard(ui_test_input::RegistryRegisterKeyboardRequest {
+ device: Some(keyboard_server),
+ ..ui_test_input::RegistryRegisterKeyboardRequest::EMPTY
+ })
+ .await?;
+
+ let (mouse, mouse_server) = create_proxy::<ui_test_input::MouseMarker>()?;
+ input_registry
+ .register_mouse(ui_test_input::RegistryRegisterMouseRequest {
+ device: Some(mouse_server),
+ ..ui_test_input::RegistryRegisterMouseRequest::EMPTY
+ })
+ .await?;
+
fasync::Task::local(async move {
let (sender, mut receiver) = futures::channel::mpsc::unbounded::<Event<TestEvent>>();
let event_sender = EventSender::<TestEvent>(sender);
@@ -244,13 +288,37 @@
info!("------ChildView {:?}", event);
match event {
Event::Init => {
- let mut window = WindowBuilder::new()
- .with_view_creation_token(view_creation_token.take().unwrap())
- .build(event_sender.clone())
- .unwrap();
+ let mut window = Window::new(event_sender.clone())
+ .with_view_creation_token(view_creation_token.take().unwrap());
window.create_view().expect("Failed to create window for child view");
_window_holder = Some(window);
}
+ Event::WindowEvent { event: window_event, .. } => match window_event {
+ WindowEvent::Focused { focused } => {
+ if focused {
+ tap(mouse.clone());
+ }
+ }
+ WindowEvent::Mouse { .. } => {
+ // Inject 'q' to quit.
+ inject_text("q".to_string(), keyboard.clone());
+ }
+ WindowEvent::Keyboard { event, responder } => {
+ if let KeyEvent { key: Some(Key::Q), .. } = event {
+ parent_sender
+ .send(Event::Exit)
+ .expect("Failed to send Event::Exit event");
+ responder
+ .send(KeyEventStatus::Handled)
+ .expect("Failed to respond to keyboard event");
+ } else {
+ responder
+ .send(KeyEventStatus::NotHandled)
+ .expect("Failed to respond to keyboard event");
+ }
+ }
+ _ => {}
+ },
_ => {}
}
}
@@ -259,3 +327,35 @@
Ok(())
}
+
+fn inject_text(text: String, keyboard: ui_test_input::KeyboardProxy) {
+ fasync::Task::local(async move {
+ keyboard
+ .simulate_us_ascii_text_entry(ui_test_input::KeyboardSimulateUsAsciiTextEntryRequest {
+ text: Some(text),
+ ..ui_test_input::KeyboardSimulateUsAsciiTextEntryRequest::EMPTY
+ })
+ .await
+ .expect("Failed to inject text using fuchsia.ui.test.input.Keyboard");
+ })
+ .detach();
+}
+
+fn tap(mouse: ui_test_input::MouseProxy) {
+ fasync::Task::local(async move {
+ mouse
+ .simulate_mouse_event(ui_test_input::MouseSimulateMouseEventRequest {
+ pressed_buttons: Some(vec![ui_test_input::MouseButton::First]),
+ ..ui_test_input::MouseSimulateMouseEventRequest::EMPTY
+ })
+ .await
+ .expect("Failed to tap using fuchsia.ui.test.input.Mouse");
+ mouse
+ .simulate_mouse_event(ui_test_input::MouseSimulateMouseEventRequest {
+ ..ui_test_input::MouseSimulateMouseEventRequest::EMPTY
+ })
+ .await
+ .expect("Failed to tap using fuchsia.ui.test.input.Mouse");
+ })
+ .detach();
+}
diff --git a/session_shells/gazelle/appkit/src/utils.rs b/session_shells/gazelle/appkit/src/utils.rs
index fd6e53f..26be474 100644
--- a/session_shells/gazelle/appkit/src/utils.rs
+++ b/session_shells/gazelle/appkit/src/utils.rs
@@ -4,9 +4,8 @@
use {
anyhow::{anyhow, Error},
- fidl_fuchsia_ui_composition as ui_comp, fuchsia_async as fasync,
+ fidl_fuchsia_ui_composition as ui_comp,
futures::channel::mpsc::{UnboundedReceiver, UnboundedSender},
- futures::stream::{AbortHandle, Abortable},
};
use crate::event::Event;
@@ -77,18 +76,3 @@
Ok(())
}
}
-
-pub fn spawn_abortable<T>(task: T) -> AbortHandle
-where
- T: 'static + Send + futures::Future,
-{
- let (abort_handle, abort_registration) = AbortHandle::new_pair();
- let abortable_fut = Abortable::new(task, abort_registration);
-
- fasync::Task::spawn(async move {
- let _ = abortable_fut.await;
- })
- .detach();
-
- abort_handle
-}
diff --git a/session_shells/gazelle/appkit/src/window.rs b/session_shells/gazelle/appkit/src/window.rs
index 9546c11..719a5e7 100644
--- a/session_shells/gazelle/appkit/src/window.rs
+++ b/session_shells/gazelle/appkit/src/window.rs
@@ -10,10 +10,10 @@
},
fidl::AsHandleRef,
fidl_fuchsia_element as felement, fidl_fuchsia_ui_composition as ui_comp,
- fidl_fuchsia_ui_input3 as ui_input3, fidl_fuchsia_ui_views as ui_views,
+ fidl_fuchsia_ui_input3 as ui_input3, fidl_fuchsia_ui_pointer as fptr,
+ fidl_fuchsia_ui_views as ui_views, fuchsia_async as fasync,
fuchsia_component::client::connect_to_protocol,
fuchsia_scenic::flatland::{IdGenerator, ViewCreationTokenPair},
- futures::future::AbortHandle,
futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt},
std::sync::{Arc, Mutex},
tracing::*,
@@ -22,7 +22,7 @@
use crate::{
child_view::ChildView,
event::{Event, ViewSpecHolder, WindowEvent},
- utils::{spawn_abortable, EventSender, Presenter},
+ utils::{EventSender, Presenter},
};
/// Defines a type to hold an id to the window. This implementation uses the value of
@@ -37,106 +37,71 @@
}
/// Defines a struct to hold window attributes used to create the window.
+#[derive(Default)]
pub(crate) struct WindowAttributes {
/// The title of the window. Only used when presented to the system's GraphicalPresenter.
- pub title: String,
+ pub title: Option<String>,
/// The [ViewCreationToken] passed to the application's [ViewProvider]. Unused for windows
/// presented to the system's GraphicalPresenter.
pub view_creation_token: Option<ui_views::ViewCreationToken>,
}
-impl Default for WindowAttributes {
- fn default() -> Self {
- WindowAttributes { title: "appkit window".to_owned(), view_creation_token: None }
- }
-}
-
-/// Defines a builder used to collect [WindowAttributes] before building the window.
-#[derive(Default)]
-pub struct WindowBuilder {
- pub(crate) attributes: WindowAttributes,
-}
-
-impl WindowBuilder {
- pub fn new() -> Self {
- Default::default()
- }
-
- pub fn with_title(mut self, title: String) -> WindowBuilder {
- self.attributes.title = title;
- self
- }
-
- pub fn with_view_creation_token(mut self, token: ui_views::ViewCreationToken) -> WindowBuilder {
- self.attributes.view_creation_token = Some(token);
- self
- }
-
- pub fn build<T>(self, event_sender: EventSender<T>) -> Result<Window<T>, Error> {
- Window::from_attributes(self.attributes, event_sender)
- }
-}
-
const ROOT_TRANSFORM_ID: ui_comp::TransformId = ui_comp::TransformId { value: 1 };
/// Defines a struct to hold [Window] state.
pub struct Window<T> {
+ attributes: WindowAttributes,
id: WindowId,
id_generator: IdGenerator,
flatland: ui_comp::FlatlandProxy,
- view_creation_token: Option<ui_views::ViewCreationToken>,
annotations: Option<Vec<felement::Annotation>>,
annotation_controller_request_stream: Option<felement::AnnotationControllerRequestStream>,
view_controller_proxy: Option<felement::ViewControllerProxy>,
focuser: Option<ui_views::FocuserProxy>,
event_sender: EventSender<T>,
- abortable_futures: Vec<AbortHandle>,
+ running_tasks: Vec<fasync::Task<()>>,
presenter: Arc<Mutex<Presenter>>,
}
-impl<T> Drop for Window<T> {
- fn drop(&mut self) {
- for abortable_fut in &self.abortable_futures {
- abortable_fut.abort();
- }
- self.abortable_futures.clear();
- }
-}
-
impl<T> Window<T> {
- pub fn new(event_sender: EventSender<T>) -> Result<Window<T>, Error> {
- let builder = WindowBuilder::new();
- builder.build(event_sender)
- }
-
- /// Creates a [Window] from [attributes].
- pub(crate) fn from_attributes(
- mut attributes: WindowAttributes,
- event_sender: EventSender<T>,
- ) -> Result<Window<T>, Error> {
+ pub fn new(event_sender: EventSender<T>) -> Window<T> {
let id_generator = IdGenerator::new_with_first_id(ROOT_TRANSFORM_ID.value);
- let flatland = connect_to_protocol::<ui_comp::FlatlandMarker>()?;
- flatland.create_transform(&mut ROOT_TRANSFORM_ID.clone())?;
- flatland.set_root_transform(&mut ROOT_TRANSFORM_ID.clone())?;
+ let flatland = connect_to_protocol::<ui_comp::FlatlandMarker>()
+ .expect("Failed to connect to fuchsia.ui.comp.Flatland");
+ flatland
+ .create_transform(&mut ROOT_TRANSFORM_ID.clone())
+ .expect("Failed to create transform");
+ flatland
+ .set_root_transform(&mut ROOT_TRANSFORM_ID.clone())
+ .expect("Failed to set root transform");
- let view_creation_token = attributes.view_creation_token.take();
let id = WindowId(0);
let presenter = Arc::new(Mutex::new(Presenter::new(flatland.clone())));
- let annotations = Self::annotations_from_window_attributes(&attributes);
+ let attributes = WindowAttributes::default();
- Ok(Window {
+ Self {
+ attributes,
id,
id_generator,
flatland,
- view_creation_token,
- annotations,
+ annotations: None,
annotation_controller_request_stream: None,
view_controller_proxy: None,
focuser: None,
event_sender,
- abortable_futures: vec![],
+ running_tasks: vec![],
presenter,
- })
+ }
+ }
+
+ pub fn with_title(mut self, title: String) -> Window<T> {
+ self.attributes.title = Some(title);
+ self
+ }
+
+ pub fn with_view_creation_token(mut self, token: ui_views::ViewCreationToken) -> Window<T> {
+ self.attributes.view_creation_token = Some(token);
+ self
}
pub fn id(&self) -> WindowId {
@@ -180,12 +145,12 @@
if let Some(focuser) = self.focuser.clone() {
let mut dup_view_ref = fuchsia_scenic::duplicate_view_ref(&view_ref)
.expect("Failed to duplicate view_ref for request_focus");
- let abort_handle = spawn_abortable(async move {
+ let task = fasync::Task::spawn(async move {
if let Err(error) = focuser.request_focus(&mut dup_view_ref).await {
error!("Failed to request focus on a view: {:?}", error);
}
});
- self.abortable_futures.push(abort_handle);
+ self.running_tasks.push(task);
}
}
@@ -211,7 +176,7 @@
let (mut view_creation_token, viewport_creation_token) =
// Check if view_creation_token was passed from ViewProvider.
- match self.view_creation_token.take() {
+ match self.attributes.view_creation_token.take() {
Some(view_creation_token) => (view_creation_token, None),
None => {
// Create a pair of view creation token to present to GraphicalPresenter.
@@ -220,13 +185,17 @@
(view_creation_token, Some(viewport_creation_token))
}
};
+ let (focused, focused_request) = create_proxy::<ui_views::ViewRefFocusedMarker>()?;
let (view_focuser, view_focuser_request) = create_proxy::<ui_views::FocuserMarker>()?;
+ let (mouse, mouse_request) = create_proxy::<fptr::MouseSourceMarker>()?;
self.id = WindowId::from_view_creation_token(&view_creation_token);
self.focuser = Some(view_focuser);
let view_bound_protocols = ui_comp::ViewBoundProtocols {
+ view_ref_focused: Some(focused_request),
view_focuser: Some(view_focuser_request),
+ mouse_source: Some(mouse_request),
..ui_comp::ViewBoundProtocols::EMPTY
};
@@ -246,6 +215,9 @@
self.event_sender.clone(),
);
+ let viewref_focused_fut =
+ Self::serve_view_ref_focused_watcher(self.id(), focused, self.event_sender.clone());
+
// If we created a viewport_creation_token earlier, we intend to present to the system's
// GraphicalPresenter. Connect to it to present the window.
let graphical_presenter_fut = match viewport_creation_token {
@@ -257,6 +229,7 @@
self.annotation_controller_request_stream =
Some(annotation_controller_server_end.into_stream().unwrap());
self.view_controller_proxy = Some(view_controller_proxy.clone());
+ self.annotations = Self::annotations_from_window_attributes(&self.attributes);
Self::connect_to_graphical_presenter(
self.id(),
self.annotations.take(),
@@ -279,11 +252,20 @@
)
.boxed();
+ let mouse_fut =
+ Self::serve_mouse_source_watcher(self.id(), mouse, self.event_sender.clone());
+
// Collect all futures into an abortable spawned task. The task is aborted in [Drop].
- let abort_handle = spawn_abortable(async move {
- futures::join!(flatland_and_layout_watcher_fut, graphical_presenter_fut, keyboard_fut)
+ let task = fasync::Task::spawn(async move {
+ futures::join!(
+ flatland_and_layout_watcher_fut,
+ viewref_focused_fut,
+ graphical_presenter_fut,
+ keyboard_fut,
+ mouse_fut,
+ );
});
- self.abortable_futures.push(abort_handle);
+ self.running_tasks.push(task);
Ok(())
}
@@ -312,9 +294,36 @@
Ok(child_view)
}
+ /// Creates an instance of [ChildView] given a [ViewportCreationToken].
+ pub fn create_child_view_from_viewport(
+ &mut self,
+ viewport_creation_token: ui_views::ViewportCreationToken,
+ width: u32,
+ height: u32,
+ event_sender: EventSender<T>,
+ ) -> Result<ChildView<T>, Error>
+ where
+ T: 'static + Sync + Send,
+ {
+ self.create_child_view(
+ ViewSpecHolder {
+ view_spec: felement::ViewSpec {
+ viewport_creation_token: Some(viewport_creation_token),
+ ..felement::ViewSpec::EMPTY
+ },
+ annotation_controller: None,
+ view_controller_request: None,
+ responder: None,
+ },
+ width,
+ height,
+ event_sender,
+ )
+ }
+
// Waits for first layout event before monitoring flatland events and layout changes.
async fn serve_flatland_events_and_layout_watcher(
- id: WindowId,
+ window_id: WindowId,
flatland: ui_comp::FlatlandProxy,
presenter: Arc<Mutex<Presenter>>,
parent_viewport_watcher: ui_comp::ParentViewportWatcherProxy,
@@ -326,20 +335,26 @@
let width = logical_size.width;
let height = logical_size.height;
event_sender
- .send(Event::WindowEvent(id, WindowEvent::Resized(width, height)))
+ .send(Event::WindowEvent {
+ window_id,
+ event: WindowEvent::Resized { width, height },
+ })
.expect("Failed to send WindowEvent::Resized event");
}
let flatland_events_fut =
- Self::serve_flatland_events(id, flatland, presenter, event_sender.clone());
- let layout_watcher_fut =
- Self::serve_layout_info_watcher(id, parent_viewport_watcher, event_sender.clone());
+ Self::serve_flatland_events(window_id, flatland, presenter, event_sender.clone());
+ let layout_watcher_fut = Self::serve_layout_info_watcher(
+ window_id,
+ parent_viewport_watcher,
+ event_sender.clone(),
+ );
futures::join!(flatland_events_fut, layout_watcher_fut);
}
async fn serve_flatland_events(
- id: WindowId,
+ window_id: WindowId,
flatland: ui_comp::FlatlandProxy,
presenter: Arc<Mutex<Presenter>>,
event_sender: EventSender<T>,
@@ -350,7 +365,7 @@
.try_for_each(move |event| {
match event {
ui_comp::FlatlandEvent::OnNextFrameBegin { values } => {
- let next_presentation_time = values
+ let next_present_time = values
.future_presentation_infos
.as_ref()
.and_then(|infos| infos.first())
@@ -361,10 +376,10 @@
.map(|mut presenter| presenter.on_next_frame(values))
.expect("Failed to call on_next_frame on presenter");
event_sender
- .send(Event::WindowEvent(
- id,
- WindowEvent::NeedsRedraw(next_presentation_time),
- ))
+ .send(Event::WindowEvent {
+ window_id,
+ event: WindowEvent::NeedsRedraw { next_present_time },
+ })
.expect("Failed to send WindowEvent::NeedsRedraw event");
}
ui_comp::FlatlandEvent::OnFramePresented { .. } => {}
@@ -379,7 +394,7 @@
}
async fn serve_layout_info_watcher(
- id: WindowId,
+ window_id: WindowId,
parent_viewport_watcher: ui_comp::ParentViewportWatcherProxy,
event_sender: EventSender<T>,
) {
@@ -398,13 +413,16 @@
height = logical_size.height;
}
event_sender
- .send(Event::WindowEvent(id, WindowEvent::Resized(width, height)))
+ .send(Event::WindowEvent {
+ window_id,
+ event: WindowEvent::Resized { width, height },
+ })
.expect("Failed to send WindowEvent::Resized event");
}
Err(fidl::Error::ClientChannelClosed { .. }) => {
info!("ParentViewportWatcher connection closed.");
event_sender
- .send(Event::WindowEvent(id, WindowEvent::Closed))
+ .send(Event::WindowEvent { window_id, event: WindowEvent::Closed })
.expect("Failed to send WindowEvent::Closed event");
break;
}
@@ -415,8 +433,72 @@
}
}
+ async fn serve_view_ref_focused_watcher(
+ window_id: WindowId,
+ focused: ui_views::ViewRefFocusedProxy,
+ event_sender: EventSender<T>,
+ ) {
+ let mut focused_stream =
+ HangingGetStream::new(focused, ui_views::ViewRefFocusedProxy::watch);
+ while let Some(result) = focused_stream.next().await {
+ match result {
+ Ok(ui_views::FocusState { focused: Some(focused), .. }) => {
+ event_sender
+ .send(Event::WindowEvent {
+ window_id,
+ event: WindowEvent::Focused { focused },
+ })
+ .expect("Failed to send WindowEvent::Focused event");
+ }
+ Ok(ui_views::FocusState { focused: None, .. }) => {
+ error!("Missing required field FocusState.focused");
+ }
+ Err(fidl::Error::ClientChannelClosed { .. }) => {
+ error!("ViewRefFocused connection closed.");
+ break;
+ }
+ Err(fidl_error) => {
+ error!("ViewRefFocused fidl error: {:?}", fidl_error);
+ break;
+ }
+ }
+ }
+ }
+
+ async fn serve_mouse_source_watcher(
+ window_id: WindowId,
+ mouse_source: fptr::MouseSourceProxy,
+ event_sender: EventSender<T>,
+ ) {
+ let mut mouse_source_stream =
+ HangingGetStream::new(mouse_source, fptr::MouseSourceProxy::watch);
+
+ while let Some(result) = mouse_source_stream.next().await {
+ match result {
+ Ok(events) => {
+ for event in events.iter() {
+ event_sender
+ .send(Event::WindowEvent {
+ window_id,
+ event: WindowEvent::Mouse { event: event.clone() },
+ })
+ .expect("failed to send Message.");
+ }
+ }
+ Err(fidl::Error::ClientChannelClosed { .. }) => {
+ error!("MouseSource connection closed.");
+ break;
+ }
+ Err(fidl_error) => {
+ warn!("MouseSource Watch() error: {:?}", fidl_error);
+ break;
+ }
+ }
+ }
+ }
+
async fn connect_to_graphical_presenter(
- id: WindowId,
+ window_id: WindowId,
annotations: Option<Vec<felement::Annotation>>,
viewport_creation_token: ui_views::ViewportCreationToken,
view_ref: ui_views::ViewRef,
@@ -449,12 +531,12 @@
let stream = view_controller_proxy.take_event_stream();
let _ = stream.collect::<Vec<_>>().await;
event_sender
- .send(Event::WindowEvent(id, WindowEvent::Closed))
+ .send(Event::WindowEvent { window_id, event: WindowEvent::Closed })
.expect("Failed to send WindowEvent::Closed event");
}
async fn serve_keyboard_listener(
- id: WindowId,
+ window_id: WindowId,
mut view_ref: ui_views::ViewRef,
event_sender: EventSender<T>,
) {
@@ -466,11 +548,14 @@
match keyboard.add_listener(&mut view_ref, listener_client_end).await {
Ok(()) => {
- while let Ok(event) = listener_stream.next().await.unwrap() {
+ while let Some(Ok(event)) = listener_stream.next().await {
let ui_input3::KeyboardListenerRequest::OnKeyEvent { event, responder, .. } =
event;
event_sender
- .send(Event::WindowEvent(id, WindowEvent::Keyboard(event, responder)))
+ .send(Event::WindowEvent {
+ window_id,
+ event: WindowEvent::Keyboard { event, responder },
+ })
.expect("Failed to send WindowEvent::Keyboard event");
}
}
@@ -484,12 +569,13 @@
attributes: &WindowAttributes,
) -> Option<Vec<felement::Annotation>> {
// TODO(https://fxbug.dev/108345): Stop hardcoding namespace for ermine shell.
+ let title = attributes.title.clone()?;
let annotations = vec![felement::Annotation {
key: felement::AnnotationKey {
namespace: "ermine".to_owned(),
value: "name".to_owned(),
},
- value: felement::AnnotationValue::Text(attributes.title.clone()),
+ value: felement::AnnotationValue::Text(title),
}];
Some(annotations)
}
diff --git a/session_shells/gazelle/examples/bouncing_box/src/main.rs b/session_shells/gazelle/examples/bouncing_box/src/main.rs
index a7e5582..0812780 100644
--- a/session_shells/gazelle/examples/bouncing_box/src/main.rs
+++ b/session_shells/gazelle/examples/bouncing_box/src/main.rs
@@ -4,7 +4,7 @@
use {
anyhow::Error,
- appkit::{Event, EventSender, Window, WindowBuilder, WindowEvent, WindowId},
+ appkit::{Event, EventSender, Window, WindowEvent, WindowId},
fidl_fuchsia_input::Key,
fidl_fuchsia_math as fmath, fidl_fuchsia_ui_composition as ui_comp,
fidl_fuchsia_ui_input3::{KeyEvent, KeyEventStatus, KeyEventType},
@@ -83,10 +83,8 @@
match event {
Event::Init => {
// Create the application's window.
- let mut window = WindowBuilder::new()
- .with_title("Bouncing Box".to_owned())
- .build(self.event_sender.clone())
- .unwrap();
+ let mut window =
+ Window::new(self.event_sender.clone()).with_title("Bouncing Box".to_owned());
window.create_view()?;
// Create the bouncer.
@@ -96,20 +94,20 @@
self.windows.insert(window.id(), window);
}
- Event::WindowEvent(id, window_event) => {
+ Event::WindowEvent { window_id: id, event: window_event } => {
let window = self.windows.get_mut(&id).unwrap();
match window_event {
- WindowEvent::Resized(width, height) => {
+ WindowEvent::Resized { width, height } => {
// Set the bouncer's size.
let size = fmath::SizeU { width, height };
self.bouncer.as_mut().map(|bouncer| bouncer.size = size);
}
- WindowEvent::NeedsRedraw(_presentation_time) => {
+ WindowEvent::NeedsRedraw { .. } => {
// Update bouncer's position on every frame.
self.bouncer.as_mut().map(|bouncer| bouncer.update());
window.redraw();
}
- WindowEvent::Keyboard(event, responder) => {
+ WindowEvent::Keyboard { event, responder } => {
// Quit app on 'q' is pressed.
if let KeyEvent {
type_: Some(KeyEventType::Pressed),
diff --git a/session_shells/gazelle/pointer_fusion/BUILD.gn b/session_shells/gazelle/pointer_fusion/BUILD.gn
new file mode 100644
index 0000000..09bcb62
--- /dev/null
+++ b/session_shells/gazelle/pointer_fusion/BUILD.gn
@@ -0,0 +1,35 @@
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+import("//build/components.gni")
+import("//build/rust/rustc_library.gni")
+
+rustc_library("pointer_fusion") {
+ name = "pointer_fusion"
+ with_unit_tests = true
+ version = "0.1.0"
+ edition = "2018"
+ sources = [
+ "src/lib.rs",
+ "src/pointer/mod.rs",
+ "src/pointer/mouse.rs",
+ "src/pointer/touch.rs",
+ "src/tests.rs",
+ ]
+ deps = [
+ "//sdk/fidl/fuchsia.ui.pointer:fuchsia.ui.pointer_rust",
+ "//src/lib/zircon/rust:fuchsia-zircon",
+ "//third_party/rust_crates:futures",
+ "//third_party/rust_crates:num",
+ ]
+ test_deps = [ "//src/lib/fuchsia" ]
+}
+
+fuchsia_unittest_package("pointer_fusion_tests") {
+ deps = [ ":pointer_fusion_test" ]
+}
+
+group("tests") {
+ testonly = true
+ deps = [ ":pointer_fusion_tests" ]
+}
diff --git a/session_shells/gazelle/pointer_fusion/src/lib.rs b/session_shells/gazelle/pointer_fusion/src/lib.rs
new file mode 100644
index 0000000..6689e40
--- /dev/null
+++ b/session_shells/gazelle/pointer_fusion/src/lib.rs
@@ -0,0 +1,113 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+mod pointer;
+
+#[cfg(test)]
+mod tests;
+
+use {
+ crate::pointer::PointerFusionState,
+ fidl_fuchsia_ui_pointer as fptr, fuchsia_zircon as zx,
+ futures::{
+ channel::mpsc::{self, UnboundedSender},
+ stream, Stream, StreamExt,
+ },
+};
+
+#[derive(Clone, Default, Debug)]
+pub enum DeviceKind {
+ #[default]
+ Touch,
+ Mouse,
+ Stylus,
+ InvertedStylus,
+ Trackpad,
+}
+
+#[derive(Clone, Default, Debug)]
+pub enum Phase {
+ #[default]
+ Cancel,
+ Add,
+ Remove,
+ Hover,
+ Down,
+ Move,
+ Up,
+}
+
+#[derive(Clone, Default, Debug)]
+pub enum SignalKind {
+ #[default]
+ None,
+ Scroll,
+ ScrollInertiaCancel,
+}
+
+pub const POINTER_BUTTON_1: i64 = 1 << 0;
+pub const POINTER_BUTTON_2: i64 = 1 << 1;
+pub const POINTER_BUTTON_3: i64 = 1 << 2;
+pub const POINTER_BUTTON_4: i64 = 1 << 3;
+pub const POINTER_BUTTON_5: i64 = 1 << 4;
+
+/// Information about the state of a pointer.
+#[derive(Clone, Default, Debug)]
+pub struct PointerEvent {
+ /// The monotonically increasing identifier that is present only on 'Down' events and
+ /// is 0 otherwise.
+ pub id: i64,
+ /// The kind of input device.
+ pub kind: DeviceKind,
+ /// The timestamp when the event originated. This is monotonically increasing for the same
+ /// [DeviceKind]. Timestamp for synthesized events is same as event synthesized from.
+ pub timestamp: zx::Time,
+ /// The current [Phase] of pointer event.
+ pub phase: Phase,
+ /// The unique device identifier.
+ pub device_id: u32,
+ /// The x position of the device, in the viewport's coordinate system, as reported by the raw
+ /// device event.
+ pub physical_x: f32,
+ /// The y position of the device, in the viewport's coordinate system, as reported by the raw
+ /// device event.
+ pub physical_y: f32,
+ /// The relative change in x position of the device from previous event in sequence.
+ pub physical_delta_x: f32,
+ /// The relative change in y position of the device from previous event in sequence.
+ pub physical_delta_y: f32,
+ /// The buttons pressed on the device represented as bitflags.
+ pub buttons: i64,
+ /// The event [SignalKind] for scroll events.
+ pub signal_kind: SignalKind,
+ /// The amount of scroll in x direction, in physical pixels.
+ pub scroll_delta_y: f64,
+ /// The amount of scroll in y direction, in physical pixels.
+ pub scroll_delta_x: f64,
+ /// Set if this [PointerEvent] was synthesized for maintaining legal input sequence.
+ pub synthesized: bool,
+}
+
+#[derive(Debug)]
+pub enum InputEvent {
+ MouseEvent(fptr::MouseEvent),
+ TouchEvent(fptr::TouchEvent),
+}
+
+/// Provides a stream of [PointerEvent] fused from [InputEvent::MouseEvent] and
+/// [InputEvent::TouchEvent].
+///
+/// * `pixel_ratio` - The device pixel ratio used to convert from logical to physical coordinates.
+///
+/// Returns a tuple of [UnboundedSender] to send [InputEvent]s to and a [Stream] to read fused
+/// [PointerEvent]s from.
+pub fn pointer_fusion(
+ pixel_ratio: f32,
+) -> (UnboundedSender<InputEvent>, impl Stream<Item = PointerEvent>) {
+ let mut state = PointerFusionState::new(pixel_ratio);
+ let (input_sender, receiver) = mpsc::unbounded::<InputEvent>();
+ let pointer_stream = receiver.map(move |input| stream::iter(state.fuse_input(input))).flatten();
+
+ (input_sender, pointer_stream)
+}
diff --git a/session_shells/gazelle/pointer_fusion/src/pointer/mod.rs b/session_shells/gazelle/pointer_fusion/src/pointer/mod.rs
new file mode 100644
index 0000000..5d90b91
--- /dev/null
+++ b/session_shells/gazelle/pointer_fusion/src/pointer/mod.rs
@@ -0,0 +1,76 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+mod mouse;
+mod touch;
+
+use {
+ super::*,
+ fidl_fuchsia_ui_pointer as fptr, num,
+ std::collections::{HashMap, HashSet},
+ std::f32::EPSILON,
+};
+
+pub struct PointerFusionState {
+ pixel_ratio: f32,
+ mouse_device_info: HashMap<u32, fptr::MouseDeviceInfo>,
+ mouse_down: HashSet<u32>,
+ mouse_view_parameters: Option<fptr::ViewParameters>,
+ pointer_states: HashMap<u32, PointerState>,
+ next_pointer_id: i64,
+}
+
+impl PointerFusionState {
+ /// Constructs a [PointerFusionState] with a display [pixel_ratio] used to convert logical
+ /// coordinates into physical coordinates.
+ pub fn new(pixel_ratio: f32) -> Self {
+ PointerFusionState {
+ pixel_ratio,
+ mouse_device_info: HashMap::new(),
+ mouse_down: HashSet::new(),
+ mouse_view_parameters: None,
+ pointer_states: HashMap::new(),
+ next_pointer_id: 0,
+ }
+ }
+
+ pub fn fuse_input(&mut self, input: InputEvent) -> Vec<PointerEvent> {
+ match input {
+ InputEvent::MouseEvent(mouse_event) => self.fuse_mouse(mouse_event),
+ InputEvent::TouchEvent(touch_event) => self.fuse_touch(touch_event),
+ }
+ }
+}
+
+// The current information about a pointer derived from previous [PointerEvent]s. This is used to
+// sanitized the pointer stream and synthesize addition data like `physical_delta_x`.
+#[derive(Copy, Clone, Default)]
+struct PointerState {
+ id: i64,
+ is_down: bool,
+ physical_x: f32,
+ physical_y: f32,
+ buttons: i64,
+}
+
+impl PointerState {
+ pub(crate) fn from_event(event: &PointerEvent) -> Self {
+ let mut state = PointerState::default();
+ state.physical_x = event.physical_x;
+ state.physical_y = event.physical_y;
+ state
+ }
+
+ pub(crate) fn is_location_changed(&self, event: &PointerEvent) -> bool {
+ self.physical_x != event.physical_x || self.physical_y != event.physical_y
+ }
+
+ pub(crate) fn is_button_state_changed(&self, event: &PointerEvent) -> bool {
+ self.buttons != event.buttons
+ }
+
+ pub(crate) fn compute_delta(&self, event: &PointerEvent) -> (f32, f32) {
+ (event.physical_x - self.physical_x, event.physical_y - self.physical_y)
+ }
+}
diff --git a/session_shells/gazelle/pointer_fusion/src/pointer/mouse.rs b/session_shells/gazelle/pointer_fusion/src/pointer/mouse.rs
new file mode 100644
index 0000000..fed6308
--- /dev/null
+++ b/session_shells/gazelle/pointer_fusion/src/pointer/mouse.rs
@@ -0,0 +1,366 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use {
+ super::*, fidl_fuchsia_ui_pointer as fptr, fuchsia_zircon as zx, num, std::collections::HashSet,
+};
+
+const SCROLL_OFFSET_MULTIPLIER: i64 = 20;
+
+impl PointerFusionState {
+ // Converts raw [fptr::MouseEvent]s to one or more [PointerEvent]s.
+ pub(super) fn fuse_mouse(&mut self, event: fptr::MouseEvent) -> Vec<PointerEvent> {
+ if let Some(ref device_info) = event.device_info {
+ self.mouse_device_info.insert(device_info.id.unwrap_or(0), device_info.clone());
+ }
+
+ if event.view_parameters.is_some() {
+ self.mouse_view_parameters = event.view_parameters;
+ }
+
+ if has_valid_mouse_sample(&event) && self.mouse_view_parameters.is_some() {
+ let sample = event.pointer_sample.as_ref().unwrap();
+ let id = sample.device_id.unwrap();
+ if self.mouse_device_info.contains_key(&id) {
+ let any_button_down = sample.pressed_buttons.is_some();
+ let phase = compute_mouse_phase(any_button_down, &mut self.mouse_down, id);
+
+ let pointer_event = create_mouse_draft(
+ &event,
+ phase,
+ self.mouse_view_parameters.as_ref().unwrap(),
+ self.mouse_device_info.get(&id).unwrap(),
+ self.pixel_ratio,
+ );
+
+ let sanitized_events = self.sanitize_pointer(pointer_event);
+ return sanitized_events;
+ }
+ }
+
+ vec![]
+ }
+
+ // Sanitizes the [PointerEvent] draft such that the resulting event stream is contextually
+ // correct. It may drop events or synthesize new events to keep the event stream sane.
+ //
+ // Note: It is still possible to craft an event stream that will cause an assert check to fail
+ // on debug builds.
+ fn sanitize_pointer(&mut self, mut event: PointerEvent) -> Vec<PointerEvent> {
+ let mut converted_pointers = vec![];
+ match event.signal_kind {
+ SignalKind::None => match event.phase {
+ // Drops the Cancel if the pointer is not previously added.
+ Phase::Cancel => {
+ if let Some(state) = self.pointer_states.get_mut(&event.device_id) {
+ assert!(state.is_down);
+
+ event.id = state.id;
+ // Synthesize a move event if the location does not match.
+ if state.is_location_changed(&event) {
+ let (physical_delta_x, physical_delta_y) = state.compute_delta(&event);
+ let move_event = PointerEvent {
+ physical_delta_x,
+ physical_delta_y,
+ phase: Phase::Move,
+ synthesized: true,
+ ..event.clone()
+ };
+
+ state.physical_x = move_event.physical_x;
+ state.physical_y = move_event.physical_y;
+
+ converted_pointers.push(move_event);
+ }
+ state.is_down = false;
+ converted_pointers.push(event);
+ }
+ }
+ Phase::Add => {
+ assert!(!self.pointer_states.contains_key(&event.device_id));
+ let state = PointerState::from_event(&event);
+ self.pointer_states.insert(event.device_id, state);
+
+ converted_pointers.push(event);
+ }
+ Phase::Remove => {
+ assert!(self.pointer_states.contains_key(&event.device_id));
+ if let Some(state) = self.pointer_states.get_mut(&event.device_id) {
+ // Synthesize a Cancel event if pointer is down.
+ if state.is_down {
+ let mut cancel_event = event.clone();
+ cancel_event.phase = Phase::Cancel;
+ cancel_event.synthesized = true;
+ cancel_event.id = state.id;
+
+ state.is_down = false;
+ converted_pointers.push(cancel_event);
+ }
+
+ // Synthesize a hover event if the location does not match.
+ if state.is_location_changed(&event) {
+ let (physical_delta_x, physical_delta_y) = state.compute_delta(&event);
+ let hover_event = PointerEvent {
+ physical_delta_x,
+ physical_delta_y,
+ phase: Phase::Hover,
+ synthesized: true,
+ ..event.clone()
+ };
+
+ state.physical_x = hover_event.physical_x;
+ state.physical_y = hover_event.physical_y;
+
+ converted_pointers.push(hover_event);
+ }
+ }
+ self.pointer_states.remove(&event.device_id);
+ converted_pointers.push(event);
+ }
+ Phase::Hover => {
+ let mut state = match self.pointer_states.get_mut(&event.device_id) {
+ Some(state) => *state,
+ None => {
+ // Synthesize add event if the pointer is not previously added.
+ let mut add_event = event.clone();
+ add_event.phase = Phase::Add;
+ add_event.synthesized = true;
+ let state = PointerState::from_event(&add_event);
+ self.pointer_states.insert(add_event.device_id, state);
+
+ converted_pointers.push(add_event);
+ state
+ }
+ };
+
+ assert!(!state.is_down);
+ if state.is_location_changed(&event) {
+ let (physical_delta_x, physical_delta_y) = state.compute_delta(&event);
+ event.physical_delta_x = physical_delta_x;
+ event.physical_delta_y = physical_delta_y;
+
+ state.physical_x = event.physical_x;
+ state.physical_y = event.physical_y;
+ converted_pointers.push(event);
+ }
+ }
+ Phase::Down => {
+ let mut state = match self.pointer_states.get_mut(&event.device_id) {
+ Some(state) => *state,
+ None => {
+ // Synthesize add event if the pointer is not previously added.
+ let mut add_event = event.clone();
+ add_event.phase = Phase::Add;
+ add_event.synthesized = true;
+ let state = PointerState::from_event(&add_event);
+ self.pointer_states.insert(add_event.device_id, state);
+
+ converted_pointers.push(add_event);
+ state
+ }
+ };
+
+ assert!(!state.is_down);
+ // Synthesize a hover event if the location does not match.
+ if state.is_location_changed(&event) {
+ let (physical_delta_x, physical_delta_y) = state.compute_delta(&event);
+ let hover_event = PointerEvent {
+ physical_delta_x,
+ physical_delta_y,
+ phase: Phase::Hover,
+ synthesized: true,
+ ..event.clone()
+ };
+
+ state.physical_x = hover_event.physical_x;
+ state.physical_y = hover_event.physical_y;
+
+ converted_pointers.push(hover_event);
+ }
+ self.next_pointer_id += 1;
+ state.id = self.next_pointer_id;
+ state.is_down = true;
+ state.buttons = event.buttons;
+ self.pointer_states.insert(event.device_id, state);
+ converted_pointers.push(event);
+ }
+ Phase::Move => {
+ // Makes sure we have an existing pointer in down state
+ let mut state =
+ self.pointer_states.get_mut(&event.device_id).expect("State should exist");
+ assert!(state.is_down);
+ event.id = state.id;
+
+ // Skip this event if location does not change.
+ if state.is_location_changed(&event) || state.is_button_state_changed(&event) {
+ let (physical_delta_x, physical_delta_y) = state.compute_delta(&event);
+ event.physical_delta_x = physical_delta_x;
+ event.physical_delta_y = physical_delta_y;
+
+ state.physical_x = event.physical_x;
+ state.physical_y = event.physical_y;
+ state.buttons = event.buttons;
+ converted_pointers.push(event);
+ }
+ }
+ Phase::Up => {
+ // Makes sure we have an existing pointer in down state
+ let mut state =
+ self.pointer_states.get_mut(&event.device_id).expect("State should exist");
+ assert!(state.is_down);
+ event.id = state.id;
+
+ // Up phase should include which buttons where released.
+ let new_buttons = event.buttons;
+ event.buttons = state.buttons;
+
+ // Synthesize a move event if the location does not match.
+ if state.is_location_changed(&event) {
+ let (physical_delta_x, physical_delta_y) = state.compute_delta(&event);
+ let move_event = PointerEvent {
+ physical_delta_x,
+ physical_delta_y,
+ phase: Phase::Move,
+ synthesized: true,
+ ..event.clone()
+ };
+
+ state.physical_x = move_event.physical_x;
+ state.physical_y = move_event.physical_y;
+
+ converted_pointers.push(move_event);
+ }
+ state.is_down = false;
+ state.buttons = new_buttons;
+ converted_pointers.push(event);
+ }
+ },
+ // Handle scroll events.
+ _ => {}
+ }
+ converted_pointers
+ }
+}
+
+fn compute_mouse_phase(any_button_down: bool, mouse_down: &mut HashSet<u32>, id: u32) -> Phase {
+ if !mouse_down.contains(&id) && !any_button_down {
+ return Phase::Hover;
+ } else if !mouse_down.contains(&id) && any_button_down {
+ mouse_down.insert(id);
+ return Phase::Down;
+ } else if mouse_down.contains(&id) && any_button_down {
+ return Phase::Move;
+ } else if mouse_down.contains(&id) && !any_button_down {
+ mouse_down.remove(&id);
+ return Phase::Up;
+ } else {
+ return Phase::Cancel;
+ }
+}
+
+fn create_mouse_draft(
+ event: &fptr::MouseEvent,
+ phase: Phase,
+ view_parameters: &fptr::ViewParameters,
+ device_info: &fptr::MouseDeviceInfo,
+ pixel_ratio: f32,
+) -> PointerEvent {
+ assert!(has_valid_mouse_sample(event));
+
+ let sample = event.pointer_sample.as_ref().unwrap();
+
+ let mut pointer = PointerEvent::default();
+ pointer.timestamp = zx::Time::from_nanos(event.timestamp.unwrap_or(0));
+ pointer.phase = phase;
+ pointer.kind = DeviceKind::Mouse;
+ pointer.device_id = sample.device_id.unwrap_or(0);
+
+ let [logical_x, logical_y] =
+ viewport_to_view_coordinates(sample.position_in_viewport.unwrap(), view_parameters);
+ pointer.physical_x = logical_x * pixel_ratio;
+ pointer.physical_y = logical_y * pixel_ratio;
+
+ if sample.pressed_buttons.is_some() && device_info.buttons.is_some() {
+ let mut pointer_buttons: i64 = 0;
+ let pressed = sample.pressed_buttons.as_ref().unwrap();
+ let device_buttons = device_info.buttons.as_ref().unwrap();
+ for button_id in pressed {
+ if let Some(index) = device_buttons.iter().position(|&r| r == *button_id) {
+ pointer_buttons |= 1 << index;
+ }
+ }
+ pointer.buttons = pointer_buttons;
+ }
+
+ if sample.scroll_h.is_some()
+ || sample.scroll_v.is_some()
+ || sample.scroll_h_physical_pixel.is_some()
+ || sample.scroll_v_physical_pixel.is_some()
+ {
+ let tick_x_20ths = sample.scroll_h.unwrap_or(0) * SCROLL_OFFSET_MULTIPLIER;
+ let tick_y_20ths = sample.scroll_v.unwrap_or(0) * SCROLL_OFFSET_MULTIPLIER;
+ let offset_x = sample.scroll_h_physical_pixel.unwrap_or(tick_x_20ths as f64);
+ let offset_y = sample.scroll_v_physical_pixel.unwrap_or(tick_y_20ths as f64);
+
+ pointer.scroll_delta_x = offset_x;
+ pointer.scroll_delta_y = offset_y;
+ }
+
+ pointer
+}
+
+fn has_valid_mouse_sample(event: &fptr::MouseEvent) -> bool {
+ if event.pointer_sample.is_none() {
+ return false;
+ }
+ let sample = event.pointer_sample.as_ref().unwrap();
+ sample.device_id.is_some()
+ && sample.position_in_viewport.is_some()
+ && (sample.pressed_buttons.is_none()
+ || !sample.pressed_buttons.as_ref().unwrap().is_empty())
+}
+
+fn viewport_to_view_coordinates(
+ viewport_coordinates: [f32; 2],
+ view_parameters: &fptr::ViewParameters,
+) -> [f32; 2] {
+ let viewport_to_view_transform = view_parameters.viewport_to_view_transform;
+ // The transform matrix is a FIDL array with matrix data in column-major
+ // order. For a matrix with data [a b c d e f g h i], and with the viewport
+ // coordinates expressed as homogeneous coordinates, the logical view
+ // coordinates are obtained with the following formula:
+ // |a d g| |x| |x'|
+ // |b e h| * |y| = |y'|
+ // |c f i| |1| |w'|
+ // which we then normalize based on the w component:
+ // if z' not zero: (x'/w', y'/w')
+ // else (x', y')
+ let m = viewport_to_view_transform;
+ let x = viewport_coordinates[0];
+ let y = viewport_coordinates[1];
+ let xp = m[0] * x + m[3] * y + m[6];
+ let yp = m[1] * x + m[4] * y + m[7];
+ let wp = m[2] * x + m[5] * y + m[8];
+ let [x, y] = if wp > EPSILON { [xp / wp, yp / wp] } else { [xp, yp] };
+
+ clamp_to_view_space(x, y, view_parameters)
+}
+
+fn clamp_to_view_space(x: f32, y: f32, p: &fptr::ViewParameters) -> [f32; 2] {
+ let min_x = p.view.min[0];
+ let min_y = p.view.min[1];
+ let max_x = p.view.max[0];
+ let max_y = p.view.max[1];
+ if min_x <= x && x < max_x && min_y <= y && y < max_y {
+ return [x, y]; // No clamping to perform.
+ }
+
+ // View boundary is [min_x, max_x) x [min_y, max_y). Note that min is
+ // inclusive, but max is exclusive - so we subtract epsilon.
+ let max_x_inclusive = max_x - EPSILON;
+ let max_y_inclusive = max_y - EPSILON;
+ let clamped_x = num::clamp(x, min_x, max_x_inclusive);
+ let clamped_y = num::clamp(y, min_y, max_y_inclusive);
+ return [clamped_x, clamped_y];
+}
diff --git a/session_shells/gazelle/pointer_fusion/src/pointer/touch.rs b/session_shells/gazelle/pointer_fusion/src/pointer/touch.rs
new file mode 100644
index 0000000..4b35963
--- /dev/null
+++ b/session_shells/gazelle/pointer_fusion/src/pointer/touch.rs
@@ -0,0 +1,12 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use {super::*, fidl_fuchsia_ui_pointer as fptr};
+
+impl PointerFusionState {
+ pub(super) fn fuse_touch(&mut self, _event: fptr::TouchEvent) -> Vec<PointerEvent> {
+ // TODO(fxb/110099): Fuse touch events.
+ todo!("Fuse touch events");
+ }
+}
diff --git a/session_shells/gazelle/pointer_fusion/src/tests.rs b/session_shells/gazelle/pointer_fusion/src/tests.rs
new file mode 100644
index 0000000..5a3fa42
--- /dev/null
+++ b/session_shells/gazelle/pointer_fusion/src/tests.rs
@@ -0,0 +1,250 @@
+// Copyright 2022 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use {
+ super::*,
+ fidl_fuchsia_ui_pointer as fptr,
+ // fidl_fuchsia_input_report as input_report,
+ futures::FutureExt,
+};
+
+#[fuchsia::test]
+async fn test_mouse_event_without_view_parameters() {
+ // Fusing mouse event without [fptr::ViewParameters] should not result in a PointerEvent.
+ let mouse_event = InputEvent::mouse();
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+ let pointer_event = receiver.next().now_or_never();
+ assert!(pointer_event.is_none(), "Received {:?}", pointer_event);
+}
+
+#[fuchsia::test]
+async fn test_mouse_event_without_mouse_sample() {
+ // Fusing mouse event without [fptr::MousePointerSample] should not result in a PointerEvent.
+ let mouse_event = InputEvent::mouse().view(1024.0, 600.0);
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+ let pointer_event = receiver.next().now_or_never();
+ assert!(pointer_event.is_none(), "Received {:?}", pointer_event);
+}
+
+#[fuchsia::test]
+async fn test_mouse_event_without_device_info() {
+ // Fusing mouse event without [fptr::MouseDeviceInfo] should not result in a PointerEvent.
+ let mouse_event = InputEvent::mouse().view(1024.0, 600.0).position(512.0, 300.0);
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+ let pointer_event = receiver.next().now_or_never();
+ assert!(pointer_event.is_none(), "Received {:?}", pointer_event);
+}
+
+#[fuchsia::test]
+async fn test_pixel_ratio() {
+ let mouse_event = InputEvent::mouse()
+ .view(1024.0, 600.0)
+ .device_info(42)
+ .position(512.0, 300.0)
+ .button_down();
+
+ let (sender, mut receiver) = pointer_fusion(2.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Add));
+ assert!(pointer_event.physical_x - 1024.0 < std::f32::EPSILON);
+ assert!(pointer_event.physical_y - 600.0 < std::f32::EPSILON);
+}
+
+#[fuchsia::test]
+async fn test_mouse_starts_with_add_event() {
+ let mouse_event =
+ InputEvent::mouse().view(1024.0, 600.0).device_info(42).position(512.0, 300.0);
+
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Add));
+}
+
+#[fuchsia::test]
+async fn test_mouse_tap() {
+ let mouse_event = InputEvent::mouse()
+ .view(1024.0, 600.0)
+ .device_info(42)
+ .position(512.0, 300.0)
+ .button_down();
+
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Add));
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Down));
+
+ let mouse_event = InputEvent::mouse().device_info(42).position(512.0, 300.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Up));
+}
+
+#[fuchsia::test]
+async fn test_mouse_hover() {
+ let mouse_event =
+ InputEvent::mouse().view(1024.0, 600.0).device_info(42).position(512.0, 300.0);
+
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Add));
+
+ // Changing mouse x position should result in Hover event.
+ let mouse_event = InputEvent::mouse().device_info(42).position(540.0, 300.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Hover));
+ assert!(pointer_event.physical_x - 540.0 < std::f32::EPSILON);
+
+ // Changing mouse y position should result in Hover event.
+ let mouse_event = InputEvent::mouse().device_info(42).position(540.0, 320.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Hover));
+ assert!(pointer_event.physical_y - 320.0 < std::f32::EPSILON);
+}
+
+#[fuchsia::test]
+async fn test_mouse_move() {
+ let mouse_event = InputEvent::mouse()
+ .view(1024.0, 600.0)
+ .device_info(42)
+ .position(512.0, 300.0)
+ .button_down();
+
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Add));
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Down));
+
+ // Changing the position should result in Move event.
+ let mouse_event = InputEvent::mouse().device_info(42).position(540.0, 320.0).button_down();
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Move));
+ assert!(pointer_event.physical_delta_x == 28.0);
+ assert!(pointer_event.physical_delta_y == 20.0);
+
+ // Keeping the same position should not result in Move event.
+ let mouse_event = InputEvent::mouse().device_info(42).position(540.0, 320.0).button_down();
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().now_or_never();
+ assert!(pointer_event.is_none(), "Received {:?}", pointer_event);
+}
+
+#[fuchsia::test]
+async fn test_mouse_no_spurious_hovers() {
+ let mouse_event =
+ InputEvent::mouse().view(1024.0, 600.0).device_info(42).position(512.0, 300.0);
+
+ let (sender, mut receiver) = pointer_fusion(1.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().await.unwrap();
+ assert!(matches!(pointer_event.phase, Phase::Add));
+
+ // Same position should not result in any hover event.
+ let mouse_event = InputEvent::mouse().device_info(42).position(512.0, 300.0);
+ sender.unbounded_send(mouse_event).unwrap();
+
+ let pointer_event = receiver.next().now_or_never();
+ assert!(pointer_event.is_none(), "Received {:?}", pointer_event);
+}
+
+trait TestMouseEvent {
+ fn mouse() -> Self;
+ fn view(self, width: f32, height: f32) -> Self;
+ fn device_info(self, id: u32) -> Self;
+ fn position(self, x: f32, y: f32) -> Self;
+ fn button_down(self) -> Self;
+}
+
+impl TestMouseEvent for InputEvent {
+ fn mouse() -> Self {
+ InputEvent::MouseEvent(fptr::MouseEvent { ..fptr::MouseEvent::EMPTY })
+ }
+
+ fn view(mut self, width: f32, height: f32) -> Self {
+ match self {
+ InputEvent::MouseEvent(ref mut event) => {
+ event.view_parameters = Some(fptr::ViewParameters {
+ view: fptr::Rectangle { min: [0.0, 0.0], max: [width, height] },
+ viewport: fptr::Rectangle { min: [0.0, 0.0], max: [width, height] },
+ viewport_to_view_transform: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
+ });
+ }
+ _ => {}
+ }
+ self
+ }
+
+ fn device_info(mut self, id: u32) -> Self {
+ match self {
+ InputEvent::MouseEvent(ref mut event) => {
+ event.device_info = Some(fptr::MouseDeviceInfo {
+ id: Some(id),
+ buttons: Some([0, 1, 2].to_vec()),
+ relative_motion_range: None,
+ ..fptr::MouseDeviceInfo::EMPTY
+ });
+ }
+ _ => {}
+ }
+ self
+ }
+
+ fn position(mut self, x: f32, y: f32) -> Self {
+ match self {
+ InputEvent::MouseEvent(ref mut event) => {
+ let device_id = event
+ .device_info
+ .as_ref()
+ .unwrap_or(&fptr::MouseDeviceInfo {
+ id: Some(0),
+ ..fptr::MouseDeviceInfo::EMPTY
+ })
+ .id;
+ event.pointer_sample = Some(fptr::MousePointerSample {
+ device_id,
+ position_in_viewport: Some([x, y]),
+ ..fptr::MousePointerSample::EMPTY
+ });
+ }
+ _ => {}
+ }
+ self
+ }
+
+ fn button_down(mut self) -> Self {
+ match self {
+ InputEvent::MouseEvent(ref mut event) => {
+ if let Some(ref mut pointer_sample) = event.pointer_sample {
+ pointer_sample.pressed_buttons = Some(vec![0]);
+ }
+ }
+ _ => {}
+ }
+ self
+ }
+}
diff --git a/session_shells/gazelle/session/BUILD.gn b/session_shells/gazelle/session/BUILD.gn
deleted file mode 100644
index 855ff07..0000000
--- a/session_shells/gazelle/session/BUILD.gn
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright 2022 The Fuchsia Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import("//build/components.gni")
-import("//build/dart/dart_component.gni")
-
-fuchsia_component("gazelle_session") {
- assert(
- dart_default_build_cfg.is_aot == flutter_default_build_cfg.is_aot &&
- dart_default_build_cfg.is_product ==
- flutter_default_build_cfg.is_product,
- "gazelle_session requires dart and flutter runtimes use the same build configuration. Make sure 'fx set ... --args=(dart|flutter)_force_product=<bool>' flags are omitted, or match, for both dart and flutter.")
- if (!dart_default_build_cfg.is_aot && !dart_default_build_cfg.is_product) {
- manifest = "meta/gazelle_session_jit.cml"
- } else if (!dart_default_build_cfg.is_aot &&
- dart_default_build_cfg.is_product) {
- manifest = "meta/gazelle_session_jit_product.cml"
- } else if (dart_default_build_cfg.is_aot &&
- !dart_default_build_cfg.is_product) {
- manifest = "meta/gazelle_session_aot.cml"
- } else if (dart_default_build_cfg.is_aot &&
- dart_default_build_cfg.is_product) {
- manifest = "meta/gazelle_session_aot_product.cml"
- }
-}
-
-fuchsia_package("gazelle_session_pkg") {
- package_name = "gazelle_session"
- deps = [ ":gazelle_session" ]
-}
-
-group("session") {
- public_deps = [ ":gazelle_session_pkg" ]
-}
diff --git a/session_shells/gazelle/session/meta/gazelle_session_aot.cml b/session_shells/gazelle/session/meta/gazelle_session_aot.cml
deleted file mode 100644
index 491f826..0000000
--- a/session_shells/gazelle/session/meta/gazelle_session_aot.cml
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2022 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-{
- include: [ "//src/experiences/session_shells/gazelle/session/meta/gazelle_session_common.shard.cml" ],
- children: [
- {
- name: "dart_aot_runner",
- url: "fuchsia-pkg://fuchsia.com/dart_aot_runner#meta/dart_aot_runner.cm",
- startup: "lazy",
- },
- {
- name: "flutter_aot_runner",
- url: "fuchsia-pkg://fuchsia.com/flutter_aot_runner#meta/flutter_aot_runner.cm",
- startup: "lazy",
- },
- ],
- offer: [
- {
- protocol: [
- "fuchsia.feedback.CrashReporter",
- "fuchsia.intl.PropertyProvider",
- "fuchsia.logger.LogSink",
- "fuchsia.posix.socket.Provider",
- "fuchsia.tracing.provider.Registry",
- ],
- from: "parent",
- to: [
- "#dart_aot_runner",
- "#flutter_aot_runner",
- ],
- },
- {
- protocol: [
- "fuchsia.accessibility.semantics.SemanticsManager",
- "fuchsia.fonts.Provider",
- "fuchsia.memorypressure.Provider",
- "fuchsia.settings.Intl",
- "fuchsia.sysmem.Allocator",
- "fuchsia.ui.composition.Allocator",
- "fuchsia.ui.composition.Flatland",
- "fuchsia.ui.input.ImeService",
- "fuchsia.ui.input3.Keyboard",
- "fuchsia.ui.scenic.Scenic",
- "fuchsia.vulkan.loader.Loader",
- ],
- from: "parent",
- to: [ "#flutter_aot_runner" ],
- },
- {
- directory: "config-data",
- from: "parent",
- to: [
- "#dart_aot_runner",
- "#flutter_aot_runner",
- ],
- },
- ],
- environments: [
- {
- name: "application_env",
- extends: "realm",
- runners: [
- {
- runner: "dart_aot_runner",
- from: "#dart_aot_runner",
- },
- {
- runner: "flutter_aot_runner",
- from: "#flutter_aot_runner",
- },
- ],
- },
- ],
-}
diff --git a/session_shells/gazelle/session/meta/gazelle_session_aot_product.cml b/session_shells/gazelle/session/meta/gazelle_session_aot_product.cml
deleted file mode 100644
index 679efe8..0000000
--- a/session_shells/gazelle/session/meta/gazelle_session_aot_product.cml
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2022 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-{
- include: [ "//src/experiences/session_shells/gazelle/session/meta/gazelle_session_common.shard.cml" ],
- children: [
- {
- name: "dart_aot_product_runner",
- url: "fuchsia-pkg://fuchsia.com/dart_aot_product_runner#meta/dart_aot_product_runner.cm",
- startup: "lazy",
- },
- {
- name: "flutter_aot_product_runner",
- url: "fuchsia-pkg://fuchsia.com/flutter_aot_product_runner#meta/flutter_aot_product_runner.cm",
- startup: "lazy",
- },
- ],
- offer: [
- {
- protocol: [
- "fuchsia.feedback.CrashReporter",
- "fuchsia.intl.PropertyProvider",
- "fuchsia.logger.LogSink",
- "fuchsia.posix.socket.Provider",
- "fuchsia.tracing.provider.Registry",
- ],
- from: "parent",
- to: [
- "#dart_aot_product_runner",
- "#flutter_aot_product_runner",
- ],
- },
- {
- protocol: [
- "fuchsia.accessibility.semantics.SemanticsManager",
- "fuchsia.fonts.Provider",
- "fuchsia.memorypressure.Provider",
- "fuchsia.settings.Intl",
- "fuchsia.sysmem.Allocator",
- "fuchsia.ui.composition.Allocator",
- "fuchsia.ui.composition.Flatland",
- "fuchsia.ui.input.ImeService",
- "fuchsia.ui.input3.Keyboard",
- "fuchsia.ui.scenic.Scenic",
- "fuchsia.vulkan.loader.Loader",
- ],
- from: "parent",
- to: [ "#flutter_aot_product_runner" ],
- },
- {
- directory: "config-data",
- from: "parent",
- to: [
- "#dart_aot_product_runner",
- "#flutter_aot_product_runner",
- ],
- },
- ],
- environments: [
- {
- name: "application_env",
- extends: "realm",
- runners: [
- {
- runner: "dart_aot_product_runner",
- from: "#dart_aot_product_runner",
- },
- {
- runner: "flutter_aot_product_runner",
- from: "#flutter_aot_product_runner",
- },
- ],
- },
- ],
-}
diff --git a/session_shells/gazelle/session/meta/gazelle_session_jit.cml b/session_shells/gazelle/session/meta/gazelle_session_jit.cml
deleted file mode 100644
index ccdf9d7..0000000
--- a/session_shells/gazelle/session/meta/gazelle_session_jit.cml
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2022 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-{
- include: [ "//src/experiences/session_shells/gazelle/session/meta/gazelle_session_common.shard.cml" ],
- children: [
- {
- name: "dart_jit_runner",
- url: "fuchsia-pkg://fuchsia.com/dart_jit_runner#meta/dart_jit_runner.cm",
- startup: "lazy",
- },
- {
- name: "flutter_jit_runner",
- url: "fuchsia-pkg://fuchsia.com/flutter_jit_runner#meta/flutter_jit_runner.cm",
- startup: "lazy",
- },
- ],
- offer: [
- {
- protocol: [
- "fuchsia.feedback.CrashReporter",
- "fuchsia.intl.PropertyProvider",
- "fuchsia.logger.LogSink",
- "fuchsia.posix.socket.Provider",
- "fuchsia.tracing.provider.Registry",
- ],
- from: "parent",
- to: [
- "#dart_jit_runner",
- "#flutter_jit_runner",
- ],
- },
- {
- protocol: [
- "fuchsia.accessibility.semantics.SemanticsManager",
- "fuchsia.fonts.Provider",
- "fuchsia.memorypressure.Provider",
- "fuchsia.settings.Intl",
- "fuchsia.sysmem.Allocator",
- "fuchsia.ui.composition.Allocator",
- "fuchsia.ui.composition.Flatland",
- "fuchsia.ui.input.ImeService",
- "fuchsia.ui.input3.Keyboard",
- "fuchsia.ui.scenic.Scenic",
- "fuchsia.vulkan.loader.Loader",
- ],
- from: "parent",
- to: [ "#flutter_jit_runner" ],
- },
- {
- directory: "config-data",
- from: "parent",
- to: [
- "#dart_jit_runner",
- "#flutter_jit_runner",
- ],
- },
- ],
- environments: [
- {
- name: "application_env",
- extends: "realm",
- runners: [
- {
- runner: "dart_jit_runner",
- from: "#dart_jit_runner",
- },
- {
- runner: "flutter_jit_runner",
- from: "#flutter_jit_runner",
- },
- ],
- },
- ],
-}
diff --git a/session_shells/gazelle/session/meta/gazelle_session_jit_product.cml b/session_shells/gazelle/session/meta/gazelle_session_jit_product.cml
deleted file mode 100644
index e52e289..0000000
--- a/session_shells/gazelle/session/meta/gazelle_session_jit_product.cml
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2022 The Fuchsia Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-{
- include: [ "//src/experiences/session_shells/gazelle/session/meta/gazelle_session_common.shard.cml" ],
- children: [
- {
- name: "dart_jit_product_runner",
- url: "fuchsia-pkg://fuchsia.com/dart_jit_product_runner#meta/dart_jit_product_runner.cm",
- startup: "lazy",
- },
- {
- name: "flutter_jit_product_runner",
- url: "fuchsia-pkg://fuchsia.com/flutter_jit_product_runner#meta/flutter_jit_product_runner.cm",
- startup: "lazy",
- },
- ],
- offer: [
- {
- protocol: [
- "fuchsia.feedback.CrashReporter",
- "fuchsia.intl.PropertyProvider",
- "fuchsia.logger.LogSink",
- "fuchsia.posix.socket.Provider",
- "fuchsia.tracing.provider.Registry",
- ],
- from: "parent",
- to: [
- "#dart_jit_product_runner",
- "#flutter_jit_product_runner",
- ],
- },
- {
- protocol: [
- "fuchsia.accessibility.semantics.SemanticsManager",
- "fuchsia.fonts.Provider",
- "fuchsia.memorypressure.Provider",
- "fuchsia.settings.Intl",
- "fuchsia.sysmem.Allocator",
- "fuchsia.ui.composition.Allocator",
- "fuchsia.ui.composition.Flatland",
- "fuchsia.ui.input.ImeService",
- "fuchsia.ui.input3.Keyboard",
- "fuchsia.ui.scenic.Scenic",
- "fuchsia.vulkan.loader.Loader",
- ],
- from: "parent",
- to: [ "#flutter_jit_product_runner" ],
- },
- {
- directory: "config-data",
- from: "parent",
- to: [
- "#dart_jit_product_runner",
- "#flutter_jit_product_runner",
- ],
- },
- ],
- environments: [
- {
- name: "application_env",
- extends: "realm",
- runners: [
- {
- runner: "dart_jit_product_runner",
- from: "#dart_jit_product_runner",
- },
- {
- runner: "flutter_jit_product_runner",
- from: "#flutter_jit_product_runner",
- },
- ],
- },
- ],
-}
diff --git a/session_shells/gazelle/shell/BUILD.gn b/session_shells/gazelle/shell/BUILD.gn
new file mode 100644
index 0000000..a6e7d03
--- /dev/null
+++ b/session_shells/gazelle/shell/BUILD.gn
@@ -0,0 +1,19 @@
+# Copyright 2022 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/components.gni")
+import("//build/dart/dart_component.gni")
+
+fuchsia_component("gazelle_shell") {
+ manifest = "meta/gazelle_shell.cml"
+}
+
+fuchsia_package("gazelle_shell_pkg") {
+ package_name = "gazelle_shell"
+ deps = [ ":gazelle_shell" ]
+}
+
+group("shell") {
+ public_deps = [ ":gazelle_shell_pkg" ]
+}
diff --git a/session_shells/gazelle/session/meta/gazelle_session_common.shard.cml b/session_shells/gazelle/shell/meta/gazelle_shell.cml
similarity index 96%
rename from session_shells/gazelle/session/meta/gazelle_session_common.shard.cml
rename to session_shells/gazelle/shell/meta/gazelle_shell.cml
index a4f5290..d4b1343 100644
--- a/session_shells/gazelle/session/meta/gazelle_session_common.shard.cml
+++ b/session_shells/gazelle/shell/meta/gazelle_shell.cml
@@ -20,7 +20,6 @@
collections: [
{
name: "elements",
- environment: "#application_env",
durability: "transient",
},
],
@@ -28,7 +27,6 @@
{
protocol: [
"fuchsia.logger.LogSink",
- "fuchsia.session.scene.Manager",
"fuchsia.ui.composition.Flatland",
],
from: "parent",
@@ -149,7 +147,10 @@
from: "framework",
},
{
- protocol: [ "fuchsia.element.GraphicalPresenter" ],
+ protocol: [
+ "fuchsia.element.GraphicalPresenter",
+ "fuchsia.ui.app.ViewProvider",
+ ],
from: "#wm",
},
{
diff --git a/session_shells/gazelle/wm/BUILD.gn b/session_shells/gazelle/wm/BUILD.gn
index 8be505c..c4bbcd0 100644
--- a/session_shells/gazelle/wm/BUILD.gn
+++ b/session_shells/gazelle/wm/BUILD.gn
@@ -4,6 +4,7 @@
import("//build/components.gni")
import("//build/rust/rustc_binary.gni")
+import("//src/lib/vulkan/vulkan.gni")
rustc_binary("bin") {
output_name = "wm"
@@ -20,7 +21,7 @@
deps = [
"//sdk/fidl/fuchsia.element:fuchsia.element_rust",
"//sdk/fidl/fuchsia.math:fuchsia.math_rust",
- "//sdk/fidl/fuchsia.session.scene:fuchsia.session.scene_rust",
+ "//sdk/fidl/fuchsia.ui.app:fuchsia.ui.app_rust",
"//sdk/fidl/fuchsia.ui.composition:fuchsia.ui.composition_rust",
"//sdk/fidl/fuchsia.ui.views:fuchsia.ui.views_rust",
"//src/lib/fidl/rust/fidl",
@@ -33,11 +34,9 @@
]
test_deps = [
- "//sdk/fidl/fuchsia.ui.app:fuchsia.ui.app_rust",
"//sdk/fidl/fuchsia.ui.test.scene:fuchsia.ui.test.scene_rust",
"//src/lib/fuchsia-async",
"//src/lib/fuchsia-component-test",
- "//third_party/rust_crates:assert_matches",
]
}
@@ -52,6 +51,9 @@
log_settings = {
max_severity = "ERROR"
}
+
+ # Ensure the device has Vulkan.
+ environments = vulkan_envs
}
}
diff --git a/session_shells/gazelle/wm/meta/wm.cml b/session_shells/gazelle/wm/meta/wm.cml
index c6d60ff..368df70 100644
--- a/session_shells/gazelle/wm/meta/wm.cml
+++ b/session_shells/gazelle/wm/meta/wm.cml
@@ -12,20 +12,23 @@
},
capabilities: [
{
- protocol: [ "fuchsia.element.GraphicalPresenter" ],
+ protocol: [
+ "fuchsia.element.GraphicalPresenter",
+ "fuchsia.ui.app.ViewProvider",
+ ],
},
],
use: [
{
- protocol: [
- "fuchsia.session.scene.Manager",
- "fuchsia.ui.composition.Flatland",
- ],
+ protocol: [ "fuchsia.ui.composition.Flatland" ],
},
],
expose: [
{
- protocol: [ "fuchsia.element.GraphicalPresenter" ],
+ protocol: [
+ "fuchsia.element.GraphicalPresenter",
+ "fuchsia.ui.app.ViewProvider",
+ ],
from: "self",
},
],
diff --git a/session_shells/gazelle/wm/src/main.rs b/session_shells/gazelle/wm/src/main.rs
index ede6e76..68462b9 100644
--- a/session_shells/gazelle/wm/src/main.rs
+++ b/session_shells/gazelle/wm/src/main.rs
@@ -2,54 +2,127 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-use {
- anyhow::{anyhow, Context},
- fidl_fuchsia_element as felement, fidl_fuchsia_session_scene as fscene,
- fidl_fuchsia_ui_composition as ui_comp,
- fuchsia_component::client::connect_to_protocol,
- fuchsia_component::server::ServiceFs,
- fuchsia_scenic::flatland,
- futures::{stream, StreamExt},
-};
+use anyhow::Context;
+use fidl_fuchsia_element as felement;
+use fidl_fuchsia_ui_app as ui_app;
+use fidl_fuchsia_ui_composition as ui_comp;
+use fidl_fuchsia_ui_views as ui_views;
+use fuchsia_component::{client, server};
+use fuchsia_scenic::flatland;
+use futures::{stream, StreamExt};
mod wm;
+// A fun picture to print to the log when gazelle launches. This is derived from
+// a drawing in the public domain, found at
+// https://www.rawpixel.com/image/6502476/png-sticker-vintage.
+const WELCOME_LOG: &'static str = "
+ â–’â–’â–‘ â–‘â–‘â–’
+ â–’â–’â–’ â–‘â–’â–‘
+ â–‘â–’â–’â–‘ â–’â–’â–’â–‘ GAZELLE
+ â–’â–’â–’â–‘ â–’â–’â–’
+ â–‘â–“â–’â–‘ â–’â–’â–“â–‘ â–‘â–‘â–‘â–‘
+ â–‘â–‘â–‘â–‘ â–“â–’â–’ â–‘â–’â–’â–’ â–‘â–‘â–‘â–‘â–’â–‘
+ â–‘â–’â–‘â–‘â–‘â–‘ â–’â–’â–’â–‘ â–‘â–’â–’â–“ â–‘â–‘â–’â–‘â–‘â–‘â–‘â–‘â–‘
+ â–‘â–‘ â–‘â–’â–’â–’â–‘ â–“â–’â–’â–‘ â–’â–’â–’â–’ â–‘â–’â–’â–’â–’â–‘â–‘â–‘
+ â–‘â–‘â–’â–’â–“â–’â–’â–‘â–‘â–“â–“â–“â–’â–‘â–’â–“â–“â–’â–’â–’â–’â–’â–‘â–’â–’â–’â–‘â–‘â–‘
+ â–‘â–‘â–‘â–’â–’â–‘â–’â–’â–“â–“â–“â–“â–“â–“â–“â–“â–‘â–‘â–“â–‘â–‘â–’â–’â–‘â–‘
+ â–‘â–’â–’â–’â–‘â–‘â–“â–“â–’â–“â–“â–’â–‘ â–’â–‘â–’â–‘â–‘
+ â–‘â–“â–“â–‘â–’â–“â–“â–“â–‘ â–‘â–’â–“â–“â–’â–‘
+ â–’â–’â–’â–‘â–“â–“â–’ â–‘â–’â–’â–“â–’
+ â–’â–’â–“â–“â–’â–’ â–’â–’â–‘â–’â–“â–’â–‘
+ â–‘â–’â–“â–’â–’â–’â–’â–’â–‘â–’â–“â–“â–‘â–’â–‘
+ â–’â–’â–’â–‘â–‘â–’â–’â–“â–“â–“â–“â–’â–’â–‘
+ â–‘â–“â–“â–“â–’â–‘â–‘â–“â–“â–“â–“â–“â–’â–’â–’ â–‘â–‘â–‘ â–‘â–‘â–‘â–‘â–‘
+ â–’â–“â–“â–’â–’â–“â–“â–’â–’â–’â–“â–’â–‘â–’â–‘ â–‘â–‘â–‘ â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–‘â–‘â–‘
+ â–’â–“â–“â–“â–“â–“â–’â–‘â–’â–“â–’â–’â–‘â–“â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–“â–’â–’â–’â–’â–’â–‘â–‘â–‘â–‘â–’â–’â–‘â–‘â–‘â–‘â–‘â–’â–’â–’â–’â–“â–“â–’â–’â–’â–‘â–‘â–‘â–‘â–‘
+ â–‘â–“â–“â–“â–“â–“â–’â–‘â–’â–“â–“â–‘â–’â–’â–“â–’â–’â–’â–‘â–‘â–‘â–’â–’â–’â–’â–’â–’â–‘â–‘â–‘â–‘â–‘â–‘ â–‘â–‘ â–‘â–‘â–‘â–‘â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–‘â–’â–’â–“â–’â–‘
+ â–‘â–“â–“â–“â–“â–“â–’â–’â–’â–“â–“â–’â–’â–‘â–’â–’â–’â–’â–’â–‘ â–‘â–‘â–‘â–’â–’â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘ â–‘â–‘â–’â–’â–’â–’â–’â–‘â–‘â–‘â–‘â–’â–‘â–‘â–‘â–’â–’â–’â–’â–‘
+ â–’â–“â–“â–“â–“â–“â–’â–‘â–’â–’â–’â–’â–’â–’â–’â–’â–‘â–‘ â–‘â–‘ â–‘â–’â–’â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘ â–‘â–‘â–‘â–‘â–’â–’â–’â–’â–‘â–‘â–‘â–’â–‘â–‘â–’â–“â–’â–“â–‘
+ â–’â–“â–“â–“â–“â–’â–‘â–‘â–‘â–’â–’â–’â–“â–’â–‘ â–‘â–‘â–‘â–‘â–‘â–’â–’â–‘â–’â–’â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘ â–‘â–’â–’â–’â–’â–‘ â–‘â–’â–‘â–‘â–“â–’â–“â–‘
+ â–‘â–’â–“â–“â–“â–’â–’â–‘â–‘â–‘â–‘â–‘â–’â–’â–‘â–‘ â–‘â–‘â–‘â–‘â–‘â–’â–‘â–‘â–‘â–’â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘ â–‘â–“â–“â–’â–‘ â–‘â–‘â–‘â–‘â–’â–“â–’â–“â–’
+ â–‘â–’â–’â–“â–“â–“â–’â–’â–‘â–‘â–‘â–‘â–’â–‘â–‘ â–‘â–‘ â–‘â–‘â–’â–‘â–‘â–‘â–’â–’â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–’â–’â–’â–“â–“â–“â–’â–‘â–‘â–‘â–‘â–’â–‘â–‘â–’â–“â–’â–“â–“â–‘
+ â–‘â–“â–“â–“â–“â–’â–’â–‘â–‘â–‘â–’â–’â–‘â–‘â–‘â–‘â–‘ â–‘â–‘â–’â–‘â–‘ â–‘â–’â–’â–’â–’â–’â–’â–’â–’â–’â–’â–“â–“â–“â–“â–“â–’â–“â–“â–‘â–‘â–‘â–‘â–’â–‘â–‘â–’â–“â–‘â–’â–“â–’
+ â–‘â–’â–“â–“â–’â–’â–’â–’â–’â–‘â–’â–’â–‘ â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–“â–’â–‘â–‘â–‘â–’â–‘â–‘â–‘â–’â–’â–‘â–’â–’â–’ â–‘â–‘
+ â–‘â–‘â–’â–’â–’â–’â–’â–’â–’â–’â–“â–’â–‘â–‘â–’â–‘â–‘â–‘â–‘â–‘ â–‘â–’â–’â–“â–“â–“â–“â–“â–“â–“â–“â–’â–’â–’â–‘ â–‘â–’â–“â–’â–’â–’â–‘â–’â–’â–‘â–‘â–’â–“â–’
+ â–‘â–’â–’â–‘â–‘ â–‘â–‘â–’â–’â–’â–’â–’â–‘â–’â–’â–’â–’â–’â–’â–“â–“â–’â–‘â–‘â–‘â–‘ â–‘â–‘â–’â–“â–“â–“â–“â–’â–’â–“â–“â–’â–’â–’â–“â–“â–’
+ â–‘â–‘â–’â–‘â–‘ â–‘â–‘â–’â–’â–‘â–’â–’â–‘â–’â–’â–’â–“â–“â–“â–“â–“â–’â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–’â–‘â–’â–“â–“â–“â–“â–“â–’â–’â–“â–“â–’â–’â–’â–“â–“â–’â–‘
+ â–‘â–‘â–’â–’â–’â–‘â–‘â–’â–‘â–’â–“â–’â–’â–‘â–’â–’â–“â–“â–“â–“â–’â–‘â–‘â–‘â–‘â–‘â–‘â–‘â–‘ â–‘â–’â–“â–“â–“â–“â–“â–’â–“â–“â–’â–’â–“â–“â–“â–’â–‘
+ â–‘â–‘â–“â–“â–’â–’â–’â–’â–’â–’â–’â–‘â–‘â–‘â–’â–“â–“â–’â–‘ â–‘â–’â–’â–’â–“â–“â–“â–“â–“â–“â–“â–’â–“â–“â–“â–’
+ â–’â–“â–“â–“â–’â–’ â–‘â–‘â–’â–“â–“â–’ â–‘â–’â–“â–“â–“â–’â–’â–“â–’â–“â–“â–“â–’
+ â–’â–’â–“â–’â–’â–‘ â–‘â–’â–’â–’â–“â–’ â–‘â–‘â–“â–’â–’â–‘â–’â–‘â–’â–’â–‘â–“";
+
enum IncomingService {
GraphicalPresenter(felement::GraphicalPresenterRequestStream),
+ ViewProvider(ui_app::ViewProviderRequestStream),
+}
+
+/// Reads incoming connection requests from the given Stream until it finds a
+/// request to connect to a `ViewProvider`. Meanwhile, it queues up other
+/// incoming connections in a Vec. It then returns both.
+///
+/// This panics if the stream closes without an incoming `ViewPresenter`
+/// connection.
+async fn first_view_provider_request_stream(
+ connections: &mut (impl stream::Stream<Item = IncomingService> + Unpin),
+) -> (ui_app::ViewProviderRequestStream, Vec<IncomingService>) {
+ let mut queue = Vec::new();
+
+ while let Some(connection) = connections.next().await {
+ match connection {
+ IncomingService::ViewProvider(stream) => return (stream, queue),
+ other => queue.push(other),
+ }
+ }
+ panic!("incoming connections stream closed without any ViewProvider connection")
+}
+
+/// Returns the `ViewCreationToken` and `ViewProviderControlHandle` from the
+/// first request on the given `ViewProviderRequestStream`. Panics if the first
+/// request isn't a call to `CreateView2` which passes a `ViewCreationToken`.
+async fn get_create_view2_request(
+ stream: &mut ui_app::ViewProviderRequestStream,
+) -> (ui_views::ViewCreationToken, ui_app::ViewProviderControlHandle) {
+ let first_request = stream
+ .next()
+ .await
+ .expect("ViewProviderRequestStream was empty")
+ .expect("reading from ViewProviderRequestStream");
+
+ match first_request {
+ ui_app::ViewProviderRequest::CreateView2 { args, control_handle } => (
+ args.view_creation_token.expect("first request did not contain a ViewCreationToken"),
+ control_handle,
+ ),
+ _ => panic!("Only CreateView2 is supported"),
+ }
}
#[fuchsia::main(logging = true)]
async fn main() -> anyhow::Result<()> {
- let flatland = connect_to_protocol::<flatland::FlatlandMarker>()?;
- let scene_manager = connect_to_protocol::<fscene::ManagerMarker>()?;
+ tracing::info!("{}", WELCOME_LOG);
+ let flatland = client::connect_to_protocol::<flatland::FlatlandMarker>()?;
- let mut fs = ServiceFs::new();
+ let mut fs = server::ServiceFs::new();
fs.dir("svc").add_fidl_service(IncomingService::GraphicalPresenter);
+ fs.dir("svc").add_fidl_service(IncomingService::ViewProvider);
fs.take_and_serve_directory_handle()?;
- let mut view_creation_token_pair = flatland::ViewCreationTokenPair::new()?;
-
- // This future can only be polled after the first call to `present`.
- let present_root_result =
- scene_manager.present_root_view(&mut view_creation_token_pair.viewport_creation_token);
+ // NOTE: `view_provider_request_stream` needs to stay alive or the caller
+ // gets unhappy.
+ let (mut view_provider_request_stream, queued_connections) =
+ first_view_provider_request_stream(&mut fs).await;
+ let (view_creation_token, _control_handle) =
+ get_create_view2_request(&mut view_provider_request_stream).await;
let mut flatland_events = flatland.take_event_stream();
- let mut incoming_connections = fs.fuse();
+ let mut incoming_connections = stream::iter(queued_connections.into_iter()).chain(fs).fuse();
let mut graphical_presenter_requests = stream::SelectAll::new();
- let mut events = stream::SelectAll::new();
- let mut server =
- wm::WindowManager::new(flatland.clone(), view_creation_token_pair.view_creation_token)
- .await?;
-
+ let mut manager = wm::Manager::new(wm::View::new(flatland.clone(), view_creation_token).await?);
flatland.present(flatland::PresentArgs::EMPTY)?;
- present_root_result
- .await
- .context("presenting root view")?
- .map_err(|err| anyhow!("presenting root view err: {:?}", err))?;
-
let mut presentation_budget = 0;
let mut presentation_requested = false;
@@ -81,16 +154,28 @@
match connection_request {
IncomingService::GraphicalPresenter(stream) =>
graphical_presenter_requests.push(stream),
+ _ => {
+ tracing::warn!("received a second attempt to connect to
+ ViewProvider. Ignoring it.")
+ }
},
request = graphical_presenter_requests.select_next_some() => {
- let present_view_request = request.context("getting PresentView request")?;
- events.push(server.present_view(present_view_request)?);
+ let felement::GraphicalPresenterRequest::PresentView {
+ view_spec,
+ annotation_controller,
+ view_controller_request,
+ responder,
+ } = request.context("getting PresentView request")?;
+ manager.present_view(
+ view_spec, annotation_controller, view_controller_request)?;
+
+ responder.send(&mut Ok(())).context("while replying to PresentView")?;
presentation_requested = true;
},
- event = events.select_next_some() => {
- events.push(server.handle_event(event));
+ background_result = manager.select_background_task() => {
+ let () = background_result.expect("while doing background work");
presentation_requested = true;
- }
+ }
}
}
}
diff --git a/session_shells/gazelle/wm/src/wm.rs b/session_shells/gazelle/wm/src/wm.rs
index d3a3748..89e5fa3 100644
--- a/session_shells/gazelle/wm/src/wm.rs
+++ b/session_shells/gazelle/wm/src/wm.rs
@@ -2,15 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-use {
- anyhow::{anyhow, Context},
- fidl::endpoints,
- fidl_fuchsia_element as felement, fidl_fuchsia_math as fmath,
- fidl_fuchsia_ui_composition as ui_comp, fidl_fuchsia_ui_views as ui_views,
- fuchsia_scenic::flatland,
- fuchsia_scenic::ViewRefPair,
- futures::{stream, FutureExt, StreamExt},
- ui_views::{FocuserProxy, ViewCreationToken, ViewRef},
+use anyhow::{anyhow, bail, Context};
+use fidl::endpoints::{self, Proxy};
+use fidl_fuchsia_element as felement;
+use fidl_fuchsia_math as fmath;
+use fidl_fuchsia_ui_composition as ui_comp;
+use fidl_fuchsia_ui_views as ui_views;
+use fuchsia_scenic::flatland;
+use futures::{
+ future,
+ stream::{self, FusedStream},
+ FutureExt, StreamExt,
};
/// Width of the border around the screen.
@@ -20,22 +22,32 @@
const BG_COLOR: ui_comp::ColorRgba =
ui_comp::ColorRgba { red: 0.41, green: 0.24, blue: 0.21, alpha: 1.0 };
-/// Main server for the gazelle window manager.
-pub struct WindowManager {
+/// `View` implements the view in the Model-View-Controller sense - it allows
+/// manipulation of the graphics on-screen without worrying about all the
+/// low-level details.
+///
+/// Methods are all synchronous but may return Futures, indicating things that
+/// will happen eventually.
+pub struct View {
flatland: flatland::FlatlandProxy,
id_generator: flatland::IdGenerator,
- root_focuser: FocuserProxy,
+ root_focuser: ui_views::FocuserProxy,
viewport_size: fmath::SizeU,
frame_transform_id: flatland::TransformId,
window: Option<Window>,
}
-impl WindowManager {
+struct Window {
+ window_id: WindowId,
+ child_view: Option<ui_views::ViewRef>,
+}
+
+impl View {
/// Create a new WindowManager, build the UI, and attach to the given
/// `view_creation_token`.
pub async fn new(
flatland: flatland::FlatlandProxy,
- mut view_creation_token: ViewCreationToken,
+ mut view_creation_token: ui_views::ViewCreationToken,
) -> anyhow::Result<Self> {
let (parent_viewport_watcher, parent_viewport_watcher_request) =
endpoints::create_proxy::<flatland::ParentViewportWatcherMarker>()
@@ -46,7 +58,7 @@
flatland
.create_view2(
&mut view_creation_token,
- &mut ui_views::ViewIdentityOnCreation::from(ViewRefPair::new()?),
+ &mut ui_views::ViewIdentityOnCreation::from(fuchsia_scenic::ViewRefPair::new()?),
flatland::ViewBoundProtocols {
view_focuser: Some(view_focuser_request),
..flatland::ViewBoundProtocols::EMPTY
@@ -97,11 +109,18 @@
.set_solid_fill(
&mut root_content_id.clone(),
&mut BG_COLOR.clone(),
- &mut viewport_size.clone(),
+ // TODO(fxbug.dev/110653): Mysteriously, Scenic blows up when
+ // you make a rectangle the size of the viewport, under very
+ // specific circumstances. When that bug is fixed, change this
+ // to just `viewport_size.clone()`.
+ &mut fmath::SizeU {
+ width: viewport_size.width - 1,
+ height: viewport_size.height - 1,
+ },
)
.context("filling desktop")?;
- Ok(WindowManager {
+ Ok(View {
flatland,
id_generator,
root_focuser: view_focuser,
@@ -112,16 +131,18 @@
}
/// Create a window for an application.
- //
- // TODO(hjfreyer@google.com): Consider supporting applications that don't
- // supply a `view_controller_server`.
+ ///
+ /// Returns an error if there's already an open window.
pub fn create_window(
&mut self,
mut viewport_creation_token: ui_views::ViewportCreationToken,
- _annotation_controller: Option<endpoints::ClientEnd<felement::AnnotationControllerMarker>>,
- view_controller_server: endpoints::ServerEnd<felement::ViewControllerMarker>,
- ) -> anyhow::Result<EventStream> {
+ ) -> anyhow::Result<CreateWindowResponse> {
+ if self.window.is_some() {
+ bail!("Only one window supported!")
+ }
+
let content_id = self.id_generator.next_content_id();
+ let window_id = WindowId(content_id.value);
let (child_view_watcher, child_view_watcher_server) =
endpoints::create_proxy::<flatland::ChildViewWatcherMarker>()
@@ -145,75 +166,217 @@
.set_content(&mut self.frame_transform_id.clone(), &mut content_id.clone())
.context("attaching window viewport to frame")?;
- self.window = Some(Window { _annotation_controller, content_id });
+ self.window = Some(Window { window_id, child_view: None });
- // Stream that notifies us when the child actually attaches to the
+ // Future that resolves when the child actually attaches to the
// viewport.
//
- // TODO(hjfreyer@google.com): Handle the case where `child_view_watcher`
- // closes. That's the last opportunity for the shell to cleanup any
- // state related to that child view.
- //
// TODO(hjfreyer@google.com): Use `child_view_watcher.get_status()` to
// delay showing the window until the child is "ready" (i.e., it has
// rendered its first frame).
- let attached_event = child_view_watcher
+ let on_child_view_attached = child_view_watcher
.get_view_ref()
- .map(move |res| {
- let child_view_ref = res.context("waiting for application's view_ref")?;
- Ok(Event::ChildAttached { window_content_id: content_id, child_view_ref })
- })
- .map(Event::from)
- .into_stream()
- .boxed();
+ .map(|res| res.context("waiting for application's view_ref"))
+ .boxed_local();
- // Stream that notifies us when the child requests that the window be
- // dismissed OR when the view_controller closes.
- let dismiss_event = view_controller_server
- .into_stream()?
- // Read the stream, forwarding any errors along, but end the stream
- // as soon as we get a call to Dismiss.
- .scan((), |_, request| async move {
- match request.context("while reading ViewController request") {
- Ok(felement::ViewControllerRequest::Dismiss { .. }) => None,
- Err(err) => Some(Event::Err(err)),
- }
- })
- // Tack a DismissRequested event onto the end of the stream.
- .chain(stream::iter([Event::DismissRequested { window_content_id: content_id }]))
- .boxed();
-
- Ok(stream::select_all([attached_event, dismiss_event]).boxed())
- }
-
- /// Dismiss the active window, if any. If there isn't one, do nothing.
- fn dismiss_window(&mut self) -> EventStream {
- let maybe_window = std::mem::replace(&mut self.window, None);
- if let Some(mut window) = maybe_window {
- let release_viewport_result =
- self.flatland.release_viewport(&mut window.content_id).map(|result| {
- Event::from(result.map(|_| Event::Ok).context("while releasing viewport"))
- });
-
- release_viewport_result.into_stream().boxed()
- } else {
- stream::empty().boxed()
+ // Future that resolves when the child view is closed.
+ let on_child_view_closed = async move {
+ match child_view_watcher.on_closed().await {
+ Ok(_) => Ok(()),
+ Err(status) => Err(anyhow!("error while awaiting channel close: {:?}", status)),
+ }
}
+ .boxed_local();
+
+ Ok(CreateWindowResponse { window_id, on_child_view_attached, on_child_view_closed })
}
- /// Handle a call to GraphicalPresenter/PresentView by closing the active
- /// window (if any), and creating a new window.
+ /// Associate a child `ViewRef` with the given `window_id`. This must be
+ /// done before you can call functions like `focus_window`.
+ pub fn register_window(
+ &mut self,
+ window_id: WindowId,
+ child_view: ui_views::ViewRef,
+ ) -> anyhow::Result<()> {
+ let window = match self.window.as_mut() {
+ Some(window) => window,
+ None => bail!(
+ "Tried to register window with ID {}, but there's no active window",
+ window_id.0
+ ),
+ };
+
+ if window.window_id != window_id {
+ bail!(
+ "Tried to register window with ID {}, but the active window has ID {}",
+ window_id.0,
+ window.window_id.0
+ );
+ }
+
+ if window.child_view.is_some() {
+ bail!(
+ "Tried to associate view with window {}, which already has view {:?}",
+ window_id.0,
+ &window.child_view
+ )
+ }
+
+ window.child_view = Some(child_view);
+ Ok(())
+ }
+
+ /// Delegate focus to the view corresponding to the window with the given
+ /// `window_id`. Requires `register_view` to have already been called.
+ pub fn focus_window(&mut self, window_id: WindowId) -> anyhow::Result<FocusWindowResponse> {
+ let window = match self.window.as_mut() {
+ Some(window) => window,
+ None => bail!(
+ "Tried to give focus to window with ID {}, but there's no active window",
+ window_id.0
+ ),
+ };
+
+ if window.window_id != window_id {
+ bail!(
+ "Tried to give focus to window with ID {}, but the active window has ID {}",
+ window_id.0,
+ window.window_id.0
+ );
+ }
+
+ let child_view = match window.child_view.as_ref() {
+ Some(child_view) => child_view,
+ None => bail!(
+ "Tried to give focus to window with ID {}, but it hasn't been registered",
+ window_id.0
+ ),
+ };
+
+ let set_auto_focus_result =
+ self.root_focuser.set_auto_focus(ui_views::FocuserSetAutoFocusRequest {
+ view_ref: Some(fuchsia_scenic::duplicate_view_ref(child_view)?),
+ ..ui_views::FocuserSetAutoFocusRequest::EMPTY
+ });
+
+ let on_completed = async move {
+ set_auto_focus_result
+ .await
+ .context("setting auto_focus")?
+ .map_err(|err| anyhow!("auto focus error: {:?}", err))
+ }
+ .boxed_local();
+
+ Ok(FocusWindowResponse { on_completed })
+ }
+
+ /// Dismiss the window with the given `window_id`.
+ pub fn dismiss_window(&mut self, window_id: WindowId) -> anyhow::Result<DismissWindowResponse> {
+ let window = match self.window.as_ref() {
+ Some(window) => window,
+ None => bail!(
+ "Tried to dismiss window with ID {}, but there's no active window",
+ window_id.0
+ ),
+ };
+
+ if window.window_id != window_id {
+ bail!(
+ "Tried to dismiss window with ID {}, but the active window has ID {}",
+ window_id.0,
+ window.window_id.0
+ );
+ }
+
+ self.flatland
+ .set_content(&mut self.frame_transform_id.clone(), &mut ui_comp::ContentId { value: 0 })
+ .context("detaching window viewport from frame")?;
+
+ let release_viewport_result =
+ self.flatland.release_viewport(&mut window_id.into_content_id());
+
+ let on_completed = async move {
+ let _token: ui_views::ViewportCreationToken =
+ release_viewport_result.await.context("while releasing viewport")?;
+ Ok(())
+ }
+ .boxed_local();
+
+ self.window = None;
+ Ok(DismissWindowResponse { on_completed })
+ }
+
+ pub fn active_window_id(&self) -> Option<WindowId> {
+ let window = self.window.as_ref()?;
+ Some(window.window_id)
+ }
+}
+
+#[derive(Debug, Copy, Clone, Hash, Ord, PartialOrd, Eq, PartialEq)]
+pub struct WindowId(u64);
+
+impl WindowId {
+ pub fn into_content_id(self) -> ui_comp::ContentId {
+ ui_comp::ContentId { value: self.0 }
+ }
+}
+
+/// Response for the `create_window` call.
+pub struct CreateWindowResponse {
+ /// ID for the window that was created.
+ pub window_id: WindowId,
+
+ /// A future that resolves when a child view has actually been attached to
+ /// the window. This `ViewRef` should then be passed back into
+ /// `register_window`, so we can do things like give it focus.
+ pub on_child_view_attached: future::LocalBoxFuture<'static, anyhow::Result<ui_views::ViewRef>>,
+
+ /// A future that resolves when the `ChildViewWatcher` associated with the
+ /// window closes.
+ pub on_child_view_closed: future::LocalBoxFuture<'static, anyhow::Result<()>>,
+}
+
+/// Response for the `focus_window` call.
+pub struct FocusWindowResponse {
+ /// A Future indicating the success/failure of the call.
+ pub on_completed: future::LocalBoxFuture<'static, anyhow::Result<()>>,
+}
+
+/// Response for the `dismiss_window` call.
+pub struct DismissWindowResponse {
+ /// A Future indicating the success/failure of the call.
+ pub on_completed: future::LocalBoxFuture<'static, anyhow::Result<()>>,
+}
+
+/// `Manager` implements business logic for the window manager. It responds to
+/// user requests, manipulates the `View`, and handles a queue of background
+/// tasks.
+///
+/// Users of `Manager` must call `select_background_task` regularly to drive
+/// internal background tasks and observe any errors going on in the background.
+pub struct Manager {
+ view: View,
+ background_tasks: stream::FuturesUnordered<
+ future::LocalBoxFuture<'static, Box<dyn FnOnce(&mut Manager) -> anyhow::Result<()>>>,
+ >,
+}
+
+impl Manager {
+ /// Create a new `Manager` that manipulates the given `View`.
+ pub fn new(view: View) -> Self {
+ Self { view, background_tasks: stream::FuturesUnordered::new() }
+ }
+
+ /// Handle a `GraphicalPresenter::PresentView` request.
+ //
+ // TODO(hjfreyer@google.com): Consider supporting applications that don't
+ // supply a `view_controller_request`.
pub fn present_view(
&mut self,
- request: felement::GraphicalPresenterRequest,
- ) -> anyhow::Result<EventStream> {
- let felement::GraphicalPresenterRequest::PresentView {
- view_spec,
- annotation_controller,
- view_controller_request,
- responder,
- } = request;
-
+ view_spec: felement::ViewSpec,
+ _annotation_controller: Option<endpoints::ClientEnd<felement::AnnotationControllerMarker>>,
+ view_controller_request: Option<endpoints::ServerEnd<felement::ViewControllerMarker>>,
+ ) -> anyhow::Result<()> {
let viewport_creation_token = view_spec
.viewport_creation_token
.ok_or(anyhow!("view_spec didn't include viewport_creation_token"))?;
@@ -221,131 +384,186 @@
let view_controller_server = view_controller_request
.ok_or(anyhow!("request didn't include view_controller_request"))?;
- let dismiss_window_events = self.dismiss_window();
+ // Dismiss the existing window, if any.
+ if let Some(window_id) = self.view.active_window_id() {
+ let dismiss_window_response = self.view.dismiss_window(window_id)?;
+ self.background_result(dismiss_window_response.on_completed);
+ }
- let create_window_events = self.create_window(
- viewport_creation_token,
- annotation_controller,
- view_controller_server,
- )?;
+ let CreateWindowResponse { window_id, on_child_view_attached, on_child_view_closed } =
+ self.view.create_window(viewport_creation_token)?;
- responder.send(&mut Ok(())).context("while replying to PresentView")?;
- Ok(stream::select_all([dismiss_window_events, create_window_events]).boxed())
- }
+ // Register the child view once it is attached to the window.
+ self.and_then_background_task(
+ on_child_view_attached,
+ move |this: &mut Manager, child_view_ref: ui_views::ViewRef| {
+ if this.view.active_window_id() != Some(window_id) {
+ tracing::warn!(
+ "Trying to register child view for {:?}, but the active window is {:?}",
+ window_id,
+ this.view.active_window_id()
+ );
+ return Ok(());
+ }
- /// Handle an asynchronous event returned by another method.
- pub fn handle_event(&mut self, event: Event) -> EventStream {
- match event {
- Event::Ok => stream::empty().boxed(),
- Event::Err(err) => {
- tracing::error!("async error: {}", err);
- stream::empty().boxed()
+ this.view.register_window(window_id, child_view_ref)?;
+ let response = this.view.focus_window(window_id)?;
+
+ this.background_result(response.on_completed);
+ Ok(())
+ },
+ );
+
+ // A Future that resolves with `true` when the `ViewController` client
+ // calls `ViewController::Dismiss`, or `false` if the channel closes
+ // without a call to `Dismiss`. Logs any errors encountered along the
+ // way.
+ //
+ // We observe this, and call `View::dismiss_window` when requested.
+ let was_dismissed = view_controller_server.into_stream()?.any(|request| match request {
+ Ok(felement::ViewControllerRequest::Dismiss { .. }) => futures::future::ready(true),
+ Err(err) => {
+ tracing::warn!("while reading ViewController request: {}", err);
+ futures::future::ready(false)
}
- Event::ChildAttached { window_content_id, child_view_ref } => match &mut self.window {
- Some(window) if window.content_id == window_content_id => self
- .root_focuser
- .set_auto_focus(ui_views::FocuserSetAutoFocusRequest {
- view_ref: Some(child_view_ref),
- ..ui_views::FocuserSetAutoFocusRequest::EMPTY
- })
- .map(|res| {
- Event::from(res.context("setting auto_focus").map(|inner_res| {
- match inner_res {
- Ok(()) => Event::Ok,
- Err(err) => Event::Err(anyhow!("auto focus error: {:?}", err)),
- }
- }))
- })
- .into_stream()
- .boxed(),
- _ => stream::empty().boxed(),
- },
- Event::DismissRequested { window_content_id } => match &mut self.window {
- Some(window) if window.content_id == window_content_id => self.dismiss_window(),
- _ => stream::empty().boxed(),
- },
- }
+ });
+
+ self.background_task(was_dismissed, move |this: &mut Manager, was_dismissed| {
+ if was_dismissed && this.view.active_window_id() == Some(window_id) {
+ tracing::info!("Dismiss for window {:?} requested", window_id);
+ let dismiss_window_response = this.view.dismiss_window(window_id)?;
+ this.background_result(dismiss_window_response.on_completed);
+ }
+ Ok(())
+ });
+
+ // We also dismiss and clean-up the window if the child view is closed.
+ self.and_then_background_task(on_child_view_closed, move |this: &mut Manager, ()| {
+ if this.view.active_window_id() != Some(window_id) {
+ return Ok(());
+ }
+ let dismiss_window_response = this.view.dismiss_window(window_id)?;
+ this.background_result(dismiss_window_response.on_completed);
+ Ok(())
+ });
+
+ Ok(())
+ }
+
+ /// A Future that resolves when the next background task has been completed.
+ /// The Future returns a `Result` indicating whether the background task
+ /// succeeded or failed. If there is no background work, this blocks
+ /// forever.
+ ///
+ /// This is intended to be used in a `select!{}` block.
+ pub fn select_background_task(&mut self) -> SelectBackgroundTask<'_> {
+ return SelectBackgroundTask { manager: self };
+ }
+
+ /// Enqueues a background task from a Future and a closure. Note that `fut`
+ /// has a static lifetime, and therefore cannot depend on `self`. The
+ /// closure takes a `&mut Manager` and the output of `fut`.
+ ///
+ /// `fut` will be polled whenever the result of `select_background_task` is
+ /// polled, and once `fut` completes, `work` will be called on the result.
+ fn background_task<Fut, Work>(&mut self, fut: Fut, work: Work)
+ where
+ Fut: futures::Future + 'static,
+ Work: FnOnce(&mut Manager, Fut::Output) -> anyhow::Result<()> + 'static,
+ {
+ self.background_tasks.push(
+ fut.map(|res| -> Box<dyn FnOnce(&mut Manager) -> anyhow::Result<()>> {
+ Box::new(move |wrapper: &mut Manager| -> anyhow::Result<()> { work(wrapper, res) })
+ })
+ .boxed_local(),
+ );
+ }
+
+ /// Version of `background_task` that passes through any errors returned by
+ /// `fut`. This is to `background_task` what `and_then` is to `map`.
+ fn and_then_background_task<Ok, Fut, Work>(&mut self, fut: Fut, work: Work)
+ where
+ Fut: futures::Future<Output = anyhow::Result<Ok>> + 'static,
+ Work: FnOnce(&mut Manager, Ok) -> anyhow::Result<()> + 'static,
+ {
+ self.background_task(fut, |this, result| {
+ let ok = result?;
+ work(this, ok)
+ })
+ }
+
+ /// Version of `background_task` that does no work.
+ /// `select_background_task()` simply polls `fut` and then returns the
+ /// result once it's done.
+ fn background_result<Fut>(&mut self, fut: Fut)
+ where
+ Fut: futures::Future<Output = anyhow::Result<()>> + 'static,
+ {
+ self.background_task(fut, |_, result| result)
}
}
-/// An asynchronous event, generally returned by Flatland or another dependency.
+/// Future for the `select_background_task` method.
///
-/// Most methods in WindowManager are feed-forward and synchronous. When work
-/// needs to happen in response to something else, a method will return a stream
-/// of Events. That Stream should be polled by the main loop, and any events it
-/// emits should be passed back into WindowManager::handle_event.
-#[derive(Debug)]
-pub enum Event {
- /// No-op. Indicates that some background work has completed successfully.
- Ok,
-
- /// Indicates that some background work encountered an error that we don't
- /// know how to act on.
- Err(anyhow::Error),
-
- /// Indicates that a child view has attached to the viewport.
- ChildAttached { window_content_id: flatland::ContentId, child_view_ref: ViewRef },
-
- /// Indicates that the child has requested that the window be dismissed, or
- /// has dropped its end of the ViewController for whatever other reason.
- DismissRequested { window_content_id: flatland::ContentId },
+/// Based on `futures::stream::SelectNextSome`.
+pub struct SelectBackgroundTask<'a> {
+ manager: &'a mut Manager,
}
-impl From<Result<Event, anyhow::Error>> for Event {
- fn from(result: Result<Event, anyhow::Error>) -> Self {
- match result {
- Ok(event) => event,
- Err(err) => Event::Err(err),
- }
+impl<'a> future::FusedFuture for SelectBackgroundTask<'a> {
+ fn is_terminated(&self) -> bool {
+ self.manager.background_tasks.is_terminated()
}
}
-/// A boxed stream of Events with a static lifetime.
-pub type EventStream = stream::BoxStream<'static, Event>;
+impl<'a> futures::Future for SelectBackgroundTask<'a> {
+ type Output = anyhow::Result<()>;
-/// A helper structure for storing state associated with a (full screen) Window.
-struct Window {
- _annotation_controller: Option<endpoints::ClientEnd<felement::AnnotationControllerMarker>>,
- content_id: ui_comp::ContentId,
+ fn poll(
+ mut self: std::pin::Pin<&mut Self>,
+ cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll<Self::Output> {
+ assert!(
+ !self.manager.background_tasks.is_terminated(),
+ "SelectBackgroundTask polled after terminated"
+ );
+
+ if let Some(item) = futures::ready!((*self).manager.background_tasks.poll_next_unpin(cx)) {
+ std::task::Poll::Ready(item(&mut self.manager))
+ } else {
+ debug_assert!(self.manager.background_tasks.is_terminated());
+ cx.waker().wake_by_ref();
+ std::task::Poll::Pending
+ }
+ }
}
#[cfg(test)]
mod tests {
- use {
- super::*,
- anyhow::Error,
- assert_matches::assert_matches,
- fidl::endpoints::{create_proxy_and_stream, create_request_stream},
- fidl_fuchsia_element as felement, fidl_fuchsia_ui_app as ui_app,
- fidl_fuchsia_ui_composition as ui_comp, fidl_fuchsia_ui_test_scene as ui_test_scene,
- fidl_fuchsia_ui_views as ui_views, fuchsia_async as fasync,
- fuchsia_component_test::{
- Capability, ChildOptions, LocalComponentHandles, RealmBuilder, Ref, Route,
- },
- fuchsia_scenic::flatland,
- fuchsia_scenic::flatland::ViewCreationTokenPair,
- futures::future::{AbortHandle, Abortable},
- futures::{StreamExt, TryStreamExt},
- std::collections::HashMap,
- };
+ use anyhow::Error;
+ use fidl::endpoints::{self, Proxy};
+ use fidl_fuchsia_element as felement;
+ use fidl_fuchsia_ui_app as ui_app;
+ use fidl_fuchsia_ui_composition as ui_comp;
+ use fidl_fuchsia_ui_test_scene as ui_test_scene;
+ use fidl_fuchsia_ui_views as ui_views;
+ use fuchsia_async as fasync;
+ use fuchsia_component_test::{Capability, ChildOptions, RealmBuilder, Ref, Route};
+ use fuchsia_scenic::flatland;
+ use futures::{select, StreamExt};
- // A stream that emits an empty value for each allowed call to present.
- // Panics on any errors encountered.
- fn present_budget_stream(
- events: flatland::FlatlandEventStream,
- ) -> impl stream::Stream<Item = ()> {
- // You get one token for free.
- stream::iter([()]).chain(events.filter_map(|event| {
- futures::future::ready({
- match event.unwrap() {
- ui_comp::FlatlandEvent::OnNextFrameBegin { .. } => Some(()),
- ui_comp::FlatlandEvent::OnFramePresented { .. } => None,
- ui_comp::FlatlandEvent::OnError { error } => {
- panic!("flatland error: {:?}", error)
- }
+ use super::*;
+
+ async fn await_next_on_frame_begin(events: &mut flatland::FlatlandEventStream) {
+ loop {
+ match events.next().await.unwrap().unwrap() {
+ ui_comp::FlatlandEvent::OnNextFrameBegin { .. } => return,
+ ui_comp::FlatlandEvent::OnFramePresented { .. } => (),
+ ui_comp::FlatlandEvent::OnError { error } => {
+ panic!("flatland error: {:?}", error)
}
- })
- }))
+ }
+ }
}
#[fuchsia::test]
@@ -381,118 +599,158 @@
let realm = builder.build().await?;
let flatland =
realm.root.connect_to_protocol_at_exposed_dir::<flatland::FlatlandMarker>()?;
- let mut budget = present_budget_stream(flatland.take_event_stream());
-
- let (view_provider, mut view_provider_request_stream) =
- create_request_stream::<ui_app::ViewProviderMarker>()
- .expect("failed to create ViewProvider request stream");
-
let scene_controller =
realm.root.connect_to_protocol_at_exposed_dir::<ui_test_scene::ControllerMarker>()?;
- fasync::Task::spawn(async move {
- let _view_ref_koid = scene_controller
- .attach_client_view(ui_test_scene::ControllerAttachClientViewRequest {
- view_provider: Some(view_provider),
- ..ui_test_scene::ControllerAttachClientViewRequest::EMPTY
- })
- .await
- .expect("failed to attach root client view");
- })
- .detach();
- // Set up the window manager.
- let view_creation_token = view_provider_request_stream
- .map(|request| {
- if let Ok(ui_app::ViewProviderRequest::CreateView2 { args, .. }) = request {
- args.view_creation_token.unwrap()
- } else {
- panic!("Unexpected request: {:?}", request)
- }
+ let (graphical_presenter_proxy, graphical_presenter_request_stream) =
+ endpoints::create_proxy_and_stream::<felement::GraphicalPresenterMarker>()?;
+
+ // A test server that loops on every frame.
+ async fn test_server(
+ flatland: flatland::FlatlandProxy,
+ scene_controller: ui_test_scene::ControllerProxy,
+ mut graphical_presenter_request_stream: felement::GraphicalPresenterRequestStream,
+ ) {
+ let (view_provider, view_provider_request_stream) =
+ endpoints::create_request_stream::<ui_app::ViewProviderMarker>()
+ .expect("failed to create ViewProvider request stream");
+
+ fasync::Task::spawn(async move {
+ let _view_ref_koid = scene_controller
+ .attach_client_view(ui_test_scene::ControllerAttachClientViewRequest {
+ view_provider: Some(view_provider),
+ ..ui_test_scene::ControllerAttachClientViewRequest::EMPTY
+ })
+ .await
+ .expect("failed to attach root client view");
})
- .into_future()
- .await
- .0
- .unwrap();
+ .detach();
- let mut server = WindowManager::new(flatland.clone(), view_creation_token).await?;
+ // Set up the window manager.
+ let view_creation_token = view_provider_request_stream
+ .map(|request| {
+ if let Ok(ui_app::ViewProviderRequest::CreateView2 { args, .. }) = request {
+ args.view_creation_token.unwrap()
+ } else {
+ panic!("Unexpected request: {:?}", request)
+ }
+ })
+ .into_future()
+ .await
+ .0
+ .unwrap();
- budget.next().await;
- flatland.present(flatland::PresentArgs::EMPTY)?;
+ let mut server =
+ Manager::new(View::new(flatland.clone(), view_creation_token).await.unwrap());
+ let mut flatland_events = flatland.take_event_stream();
- // Create a window.
- let ViewCreationTokenPair { mut view_creation_token, viewport_creation_token } =
- ViewCreationTokenPair::new()?;
+ loop {
+ flatland.present(flatland::PresentArgs::EMPTY).unwrap();
- let flatland2 =
- realm.root.connect_to_protocol_at_exposed_dir::<flatland::FlatlandMarker>()?;
- let mut budget2 = present_budget_stream(flatland2.take_event_stream());
+ await_next_on_frame_begin(&mut flatland_events).await;
+ select! {
+ req = graphical_presenter_request_stream.next() => {
+ if req.is_none() {
+ return
+ }
- let (parent_viewport_watcher, parent_viewport_watcher_request) =
- endpoints::create_proxy::<flatland::ParentViewportWatcherMarker>().unwrap();
+ let felement::GraphicalPresenterRequest::PresentView {
+ view_spec,
+ annotation_controller,
+ view_controller_request,
+ responder,
+ } = req.unwrap().unwrap();
+ server.present_view(
+ view_spec,
+ annotation_controller,
+ view_controller_request
+ )
+ .unwrap();
+ responder.send(&mut Ok(())).unwrap();
+ }
+ bg = server.select_background_task() => {
+ bg.unwrap();
+ }
+ }
+ }
+ }
- flatland2
- .create_view2(
- &mut view_creation_token,
- &mut ui_views::ViewIdentityOnCreation::from(ViewRefPair::new().unwrap()),
- flatland::ViewBoundProtocols::EMPTY,
- parent_viewport_watcher_request,
- )
- .unwrap();
+ async fn test_client(
+ flatland2: flatland::FlatlandProxy,
+ graphical_presenter_proxy: felement::GraphicalPresenterProxy,
+ ) {
+ // Create a window.
+ let flatland::ViewCreationTokenPair {
+ mut view_creation_token,
+ viewport_creation_token,
+ } = flatland::ViewCreationTokenPair::new().unwrap();
- let (view_controller_client, view_controller_server) =
- endpoints::create_proxy::<felement::ViewControllerMarker>().unwrap();
- let mut creation_events =
- server.create_window(viewport_creation_token, None, view_controller_server)?;
+ let (view_controller_client, view_controller_server) =
+ endpoints::create_proxy::<felement::ViewControllerMarker>().unwrap();
- budget.next().await;
- flatland.present(flatland::PresentArgs::EMPTY)?;
- budget2.next().await;
- flatland2.present(flatland::PresentArgs::EMPTY)?;
+ let view_spec = felement::ViewSpec {
+ viewport_creation_token: Some(viewport_creation_token),
+ ..felement::ViewSpec::EMPTY
+ };
- // Observe that the child was attached.
- let event = creation_events.next().await.unwrap();
- assert_matches!(
- &event,
- Event::ChildAttached { window_content_id: flatland::ContentId { value: 4 }, .. }
- );
+ let () = graphical_presenter_proxy
+ .present_view(view_spec, None, Some(view_controller_server))
+ .await
+ .unwrap()
+ .unwrap();
- let attached_events: Vec<_> = server.handle_event(event).collect().await;
- assert_eq!(attached_events.len(), 1);
- assert_matches!(attached_events[0], Event::Ok);
+ let (parent_viewport_watcher, parent_viewport_watcher_request) =
+ endpoints::create_proxy::<flatland::ParentViewportWatcherMarker>().unwrap();
- assert_matches!(
- parent_viewport_watcher.get_status().await,
- Ok(ui_comp::ParentViewportStatus::ConnectedToDisplay)
- );
+ let (view_ref_focused_proxy, view_ref_focused_server) =
+ endpoints::create_proxy::<fidl_fuchsia_ui_views::ViewRefFocusedMarker>().unwrap();
- // Close the window.
- view_controller_client.dismiss()?;
+ flatland2
+ .create_view2(
+ &mut view_creation_token,
+ &mut ui_views::ViewIdentityOnCreation::from(
+ fuchsia_scenic::ViewRefPair::new().unwrap(),
+ ),
+ flatland::ViewBoundProtocols {
+ view_ref_focused: Some(view_ref_focused_server),
+ ..flatland::ViewBoundProtocols::EMPTY
+ },
+ parent_viewport_watcher_request,
+ )
+ .unwrap();
- budget.next().await;
- flatland.present(flatland::PresentArgs::EMPTY)?;
- budget2.next().await;
- flatland2.present(flatland::PresentArgs::EMPTY)?;
+ let mut events = flatland2.take_event_stream();
+ flatland2.present(flatland::PresentArgs::EMPTY).unwrap();
+ await_next_on_frame_begin(&mut events).await;
- let event = creation_events.next().await;
- assert_matches!(
- &event,
- Some(Event::DismissRequested { window_content_id: flatland::ContentId { value: 4 } })
- );
+ // Wait for the child to be attached.
+ while parent_viewport_watcher.get_status().await.unwrap()
+ != ui_comp::ParentViewportStatus::ConnectedToDisplay
+ {}
- let dismiss_events = server.handle_event(event.unwrap());
+ // Wait for the child to get focus.
+ while view_ref_focused_proxy.watch().await.unwrap().focused != Some(true) {}
- budget.next().await;
- flatland.present(flatland::PresentArgs::EMPTY)?;
- budget2.next().await;
- flatland2.present(flatland::PresentArgs::EMPTY)?;
+ // Dismiss the view, and wait for the parent_viewport_watcher to close.
+ view_controller_client.dismiss().unwrap();
+ parent_viewport_watcher.on_closed().await.unwrap();
+ }
- let dismiss_events: Vec<_> = dismiss_events.collect().await;
- assert_eq!(dismiss_events.len(), 1);
- assert_matches!(dismiss_events[0], Event::Ok);
+ let server_task = fasync::Task::local(test_server(
+ flatland,
+ scene_controller,
+ graphical_presenter_request_stream,
+ ));
- // Check that the parent_viewport_watcher stream closes.
- assert_matches!(parent_viewport_watcher.take_event_stream().into_future().await.0, None);
+ let client_task = fasync::Task::local(test_client(
+ realm.root.connect_to_protocol_at_exposed_dir::<flatland::FlatlandMarker>()?,
+ graphical_presenter_proxy,
+ ));
+ // client_task completing deletes the `GraphicalPresenterProxy`, which
+ // tells the server to shut down.
+ client_task.await;
+ server_task.await;
realm.destroy().await?;
Ok(())
}
diff --git a/tests/BUILD.gn b/tests/BUILD.gn
index f1424e3..4ce3d93 100644
--- a/tests/BUILD.gn
+++ b/tests/BUILD.gn
@@ -14,9 +14,9 @@
sources = [ "ermine_driver.dart" ]
deps = [
- "//sdk/fidl/fuchsia.input",
- "//sdk/fidl/fuchsia.ui.input",
- "//sdk/fidl/fuchsia.ui.input3",
+ "//sdk/fidl/fuchsia.input:fuchsia.input_dart",
+ "//sdk/fidl/fuchsia.ui.input:fuchsia.ui.input_dart",
+ "//sdk/fidl/fuchsia.ui.input3:fuchsia.ui.input3_dart",
"//sdk/testing/sl4f/client",
"//sdk/testing/sl4f/flutter_driver_sl4f",
"//third_party/dart-pkg/git/flutter/packages/flutter_driver",
diff --git a/tests/chrome/BUILD.gn b/tests/chrome/BUILD.gn
index 9c8298e..5ebe014 100644
--- a/tests/chrome/BUILD.gn
+++ b/tests/chrome/BUILD.gn
@@ -20,7 +20,7 @@
deps = [
"//sdk/dart/fuchsia_logger",
- "//sdk/fidl/fuchsia.input",
+ "//sdk/fidl/fuchsia.input:fuchsia.input_dart",
"//sdk/testing/sl4f/client",
"//sdk/testing/sl4f/flutter_driver_sl4f",
"//src/experiences/tests:ermine_driver",
@@ -52,7 +52,7 @@
deps = [
"//sdk/dart/fuchsia_logger",
- "//sdk/fidl/fuchsia.input",
+ "//sdk/fidl/fuchsia.input:fuchsia.input_dart",
"//sdk/testing/sl4f/client",
"//sdk/testing/sl4f/flutter_driver_sl4f",
"//src/experiences/tests:ermine_driver",
diff --git a/tests/e2e/BUILD.gn b/tests/e2e/BUILD.gn
index be5a790..e27411b 100644
--- a/tests/e2e/BUILD.gn
+++ b/tests/e2e/BUILD.gn
@@ -32,9 +32,9 @@
]
deps = [
- "//sdk/fidl/fuchsia.input",
- "//sdk/fidl/fuchsia.ui.input",
- "//sdk/fidl/fuchsia.ui.input3",
+ "//sdk/fidl/fuchsia.input:fuchsia.input_dart",
+ "//sdk/fidl/fuchsia.ui.input:fuchsia.ui.input_dart",
+ "//sdk/fidl/fuchsia.ui.input3:fuchsia.ui.input3_dart",
"//sdk/testing/sl4f/client",
"//sdk/testing/sl4f/flutter_driver_sl4f",
"//src/experiences/tests:ermine_driver",
@@ -70,9 +70,9 @@
sources = [ "ermine_terminal_test.dart" ]
deps = [
- "//sdk/fidl/fuchsia.input",
- "//sdk/fidl/fuchsia.ui.input",
- "//sdk/fidl/fuchsia.ui.input3",
+ "//sdk/fidl/fuchsia.input:fuchsia.input_dart",
+ "//sdk/fidl/fuchsia.ui.input:fuchsia.ui.input_dart",
+ "//sdk/fidl/fuchsia.ui.input3:fuchsia.ui.input3_dart",
"//sdk/testing/sl4f/client",
"//sdk/testing/sl4f/flutter_driver_sl4f",
"//src/experiences/tests:ermine_driver",
@@ -107,9 +107,9 @@
sources = [ "ermine_smoke_test.dart" ]
deps = [
- "//sdk/fidl/fuchsia.input",
- "//sdk/fidl/fuchsia.ui.input",
- "//sdk/fidl/fuchsia.ui.input3",
+ "//sdk/fidl/fuchsia.input:fuchsia.input_dart",
+ "//sdk/fidl/fuchsia.ui.input:fuchsia.ui.input_dart",
+ "//sdk/fidl/fuchsia.ui.input3:fuchsia.ui.input3_dart",
"//sdk/testing/sl4f/client",
"//sdk/testing/sl4f/flutter_driver_sl4f",
"//src/experiences/tests:ermine_driver",
@@ -157,7 +157,9 @@
deps = [
":experiences_ermine_session_shell_e2e_test($host_toolchain)",
":experiences_ermine_smoke_e2e_test($host_toolchain)",
- ":experiences_ermine_terminal_e2e_test($host_toolchain)",
+
+ # TODO(fxbug.dev/110585): Re-enable once terminal is supported.
+ # ":experiences_ermine_terminal_e2e_test($host_toolchain)",
]
}
}
diff --git a/tests/lib/ermine_driver.dart b/tests/lib/ermine_driver.dart
index 2a2ba0f..f6c7d37 100644
--- a/tests/lib/ermine_driver.dart
+++ b/tests/lib/ermine_driver.dart
@@ -8,7 +8,8 @@
import 'dart:math';
// ignore_for_file: import_of_legacy_library_into_null_safe
-
+import 'package:fidl_fuchsia_input/fidl_async.dart' as input;
+import 'package:fidl_fuchsia_ui_input3/fidl_async.dart' as input3;
import 'package:fidl_fuchsia_input/fidl_async.dart';
import 'package:fidl_fuchsia_ui_input3/fidl_async.dart' hide KeyEvent;
import 'package:flutter_driver/flutter_driver.dart';
@@ -186,12 +187,12 @@
const key1Release = Duration(milliseconds: 600);
final input = Input(sl4f);
- await input.keyEvents([
- KeyEvent(modifier, key1Press, KeyEventType.pressed),
- KeyEvent(key, key2Press, KeyEventType.pressed),
- KeyEvent(key, key2Release, KeyEventType.released),
- KeyEvent(modifier, key1Release, KeyEventType.released),
- ]);
+ await input.sendKeyEvents([
+ InputKeyEvent(modifier, key1Press, KeyEventType.pressed),
+ InputKeyEvent(key, key2Press, KeyEventType.pressed),
+ InputKeyEvent(key, key2Release, KeyEventType.released),
+ InputKeyEvent(modifier, key1Release, KeyEventType.released),
+ ].map((e) => e.toJson()).toList());
await driver.waitUntilNoTransientCallbacks();
}
@@ -205,14 +206,14 @@
const key1Release = Duration(milliseconds: 700);
final input = Input(sl4f);
- await input.keyEvents([
- KeyEvent(modifier1, key1Press, KeyEventType.pressed),
- KeyEvent(modifier2, key2Press, KeyEventType.pressed),
- KeyEvent(key, key3Press, KeyEventType.pressed),
- KeyEvent(key, key3Release, KeyEventType.released),
- KeyEvent(modifier2, key2Release, KeyEventType.released),
- KeyEvent(modifier1, key1Release, KeyEventType.released),
- ]);
+ await input.sendKeyEvents([
+ InputKeyEvent(modifier1, key1Press, KeyEventType.pressed),
+ InputKeyEvent(modifier2, key2Press, KeyEventType.pressed),
+ InputKeyEvent(key, key3Press, KeyEventType.pressed),
+ InputKeyEvent(key, key3Release, KeyEventType.released),
+ InputKeyEvent(modifier2, key2Release, KeyEventType.released),
+ InputKeyEvent(modifier1, key1Release, KeyEventType.released),
+ ].map((e) => e.toJson()).toList());
await driver.waitUntilNoTransientCallbacks();
}
@@ -617,3 +618,20 @@
return Rectangle(0, 0, 0, 0);
}
}
+
+/// Describes single key event to pass into input_facade.
+class InputKeyEvent {
+ final input.Key _key;
+ final Duration _durationSinceStart;
+ final input3.KeyEventType _type;
+
+ /// Creates a new [KeyEvent].
+ InputKeyEvent(this._key, this._durationSinceStart, this._type);
+
+ /// Return this as a primitive object that can be JSON-encoded.
+ Map<String, dynamic> toJson() => {
+ 'key': _key.$value,
+ 'duration_millis': _durationSinceStart.inMilliseconds,
+ 'type': _type.$value,
+ };
+}
diff --git a/tests/performance/BUILD.gn b/tests/performance/BUILD.gn
index 549d900..f81dd7c 100644
--- a/tests/performance/BUILD.gn
+++ b/tests/performance/BUILD.gn
@@ -22,9 +22,9 @@
sources = [ "workstation_performance_navigation_test.dart" ]
deps = [
- "//sdk/fidl/fuchsia.input",
- "//sdk/fidl/fuchsia.ui.input",
- "//sdk/fidl/fuchsia.ui.input3",
+ "//sdk/fidl/fuchsia.input:fuchsia.input_dart",
+ "//sdk/fidl/fuchsia.ui.input:fuchsia.ui.input_dart",
+ "//sdk/fidl/fuchsia.ui.input3:fuchsia.ui.input3_dart",
"//sdk/testing/sl4f/client",
"//sdk/testing/sl4f/flutter_driver_sl4f",
"//src/experiences/tests:ermine_driver",