[ermine] launch a module to ask for input
Use a flutter module to ask for a package name and launch it.
Testing: manual
Change-Id: I0884251f2d6fc1102b1b92634fd17305ffc6b20a
diff --git a/bin/session_shell/ermine_session_shell/BUILD.gn b/bin/session_shell/ermine_session_shell/BUILD.gn
index a667e43..58e3688 100644
--- a/bin/session_shell/ermine_session_shell/BUILD.gn
+++ b/bin/session_shell/ermine_session_shell/BUILD.gn
@@ -30,6 +30,7 @@
"//third_party/rust-crates/rustc_deps:itertools",
"//third_party/rust-crates/rustc_deps:lazy_static",
"//third_party/rust-crates/rustc_deps:parking_lot",
+ "//topaz/bin/ui/text_input_mod/public/fidl:fuchsia.textinputmod-rustc",
]
}
diff --git a/bin/session_shell/ermine_session_shell/README.md b/bin/session_shell/ermine_session_shell/README.md
index 4adf269..cb5b0a2 100644
--- a/bin/session_shell/ermine_session_shell/README.md
+++ b/bin/session_shell/ermine_session_shell/README.md
@@ -1,6 +1,11 @@
Ermine is a Fuchsia Session Shell intended to provide developers with a
lovable environment for testing mods on Fuchsia.
+Pressing command-space (circle key in caps lock position on Eve, not sure
+what key on a Windows-style keyboard) will bring up a text entry box. Enter
+the name of a package (i.e. noodles) and it will be launched into a new
+story.
+
This initial version implements the same approach for view hosting,
layout and control as ``//garnet/bin/developer/[tiles|tiles_ctl]``. For
simplicity it re-uses
diff --git a/bin/session_shell/ermine_session_shell/meta/ermine.cmx b/bin/session_shell/ermine_session_shell/meta/ermine.cmx
index 641f8c4..8e49025 100644
--- a/bin/session_shell/ermine_session_shell/meta/ermine.cmx
+++ b/bin/session_shell/ermine_session_shell/meta/ermine.cmx
@@ -5,6 +5,8 @@
"sandbox": {
"services": [
"fuchsia.modular.SessionShellContext",
+ "fuchsia.modular.PuppetMaster",
+ "fuchsia.sys.Launcher",
"fuchsia.ui.scenic.Scenic",
"fuchsia.ui.viewsv1.ViewManager"
]
diff --git a/bin/session_shell/ermine_session_shell/src/ask_box.rs b/bin/session_shell/ermine_session_shell/src/ask_box.rs
new file mode 100644
index 0000000..77b7036
--- /dev/null
+++ b/bin/session_shell/ermine_session_shell/src/ask_box.rs
@@ -0,0 +1,138 @@
+use crate::APP;
+use failure::Error;
+use fidl::encoding::OutOfLine;
+use fidl::endpoints::create_endpoints;
+use fidl_fuchsia_math::{InsetF, SizeF};
+use fidl_fuchsia_textinputmod::{TextInputModMarker, TextInputModProxy, TextInputModReceiverMarker,
+ TextInputModReceiverRequest};
+use fidl_fuchsia_ui_viewsv1::{CustomFocusBehavior, ViewLayout, ViewProperties, ViewProviderMarker};
+use fidl_fuchsia_ui_viewsv1token::ViewOwnerMarker;
+use fuchsia_app::client::{App, Launcher};
+use fuchsia_async as fasync;
+use fuchsia_scenic::{EntityNode, ImportNode, SessionPtr};
+use futures::{TryFutureExt, TryStreamExt};
+
+pub struct AskBox {
+ _app: App,
+ key: u32,
+ pub host_node: EntityNode,
+ pub text_input_mod: TextInputModProxy,
+}
+
+impl AskBox {
+ fn setup_view(
+ app: &App, key: u32, session: &SessionPtr,
+ view_container: &fidl_fuchsia_ui_viewsv1::ViewContainerProxy, import_node: &ImportNode,
+ ) -> Result<EntityNode, Error> {
+ let view_provider = app.connect_to_service(ViewProviderMarker)?;
+ let (view_owner_client, view_owner_server) = create_endpoints::<ViewOwnerMarker>()?;
+ view_provider.create_view(view_owner_server, None)?;
+ let host_node = EntityNode::new(session.clone());
+ let host_import_token = host_node.export_as_request();
+
+ view_container.add_child(key, view_owner_client, host_import_token)?;
+ //view_container.request_focus(key)?;
+ import_node.add_child(&host_node);
+ Ok(host_node)
+ }
+
+ fn setup_text_mod_receiver(app: &App) -> Result<TextInputModProxy, Error> {
+ let text_input_mod = app.connect_to_service(TextInputModMarker)?;
+
+ let (text_input_receiver, text_input_receiver_request) =
+ create_endpoints::<TextInputModReceiverMarker>()?;
+
+ fasync::spawn(
+ text_input_receiver_request
+ .into_stream()
+ .unwrap()
+ .map_ok(move |request| match request {
+ TextInputModReceiverRequest::UserEnteredText { text, responder } => {
+ APP.lock()
+ .handle_suggestion(Some(&text))
+ .unwrap_or_else(|e| eprintln!("handle_suggestion error: {:?}", e));
+ responder
+ .send()
+ .unwrap_or_else(|e| eprintln!("UserEnteredText send failed: {:?}", e));
+ }
+ TextInputModReceiverRequest::UserCanceled { responder } => {
+ APP.lock()
+ .handle_suggestion(None)
+ .unwrap_or_else(|e| eprintln!("handle_suggestion error: {:?}", e));
+ responder
+ .send()
+ .unwrap_or_else(|e| eprintln!("UserCanceled send failed: {:?}", e));
+ }
+ })
+ .try_collect::<()>()
+ .unwrap_or_else(|e| eprintln!("text input receiver error: {:?}", e)),
+ );
+
+ let f = text_input_mod.listen_for_text_input(text_input_receiver);
+ fasync::spawn(f.unwrap_or_else(|e| eprintln!("listen_for_text_input error: {:?}", e)));
+
+ Ok(text_input_mod)
+ }
+
+ pub fn focus(
+ &mut self, view_container: &fidl_fuchsia_ui_viewsv1::ViewContainerProxy,
+ ) -> Result<(), Error> {
+ println!("Want to focus {}", self.key);
+ view_container.request_focus(self.key)?;
+ Ok(())
+ }
+
+ pub fn remove(
+ &mut self, view_container: &fidl_fuchsia_ui_viewsv1::ViewContainerProxy,
+ ) -> Result<(), Error> {
+ view_container.remove_child(self.key, None)?;
+ Ok(())
+ }
+
+ pub fn new(
+ key: u32, session: &SessionPtr,
+ view_container: &fidl_fuchsia_ui_viewsv1::ViewContainerProxy, import_node: &ImportNode,
+ ) -> Result<AskBox, Error> {
+ let app = Launcher::new()?.launch(
+ "fuchsia-pkg://fuchsia.com/text_input_mod#meta/text_input_mod.cmx".to_string(),
+ None,
+ )?;
+
+ let host_node = Self::setup_view(&app, key, session, view_container, import_node)?;
+ let text_input_mod = Self::setup_text_mod_receiver(&app)?;
+
+ Ok(AskBox {
+ _app: app,
+ key,
+ host_node,
+ text_input_mod,
+ })
+ }
+
+ pub fn layout(
+ &self, view_container: &fidl_fuchsia_ui_viewsv1::ViewContainerProxy, width: f32,
+ height: f32,
+ ) {
+ let x_inset = width * 0.1;
+ let y_inset = height * 0.4;
+ let mut view_properties = ViewProperties {
+ custom_focus_behavior: Some(Box::new(CustomFocusBehavior { allow_focus: true })),
+ view_layout: Some(Box::new(ViewLayout {
+ inset: InsetF {
+ bottom: 0.0,
+ left: 0.0,
+ right: 0.0,
+ top: 0.0,
+ },
+ size: SizeF {
+ width: width - 2.0 * x_inset,
+ height: height - 2.0 * y_inset,
+ },
+ })),
+ };
+ view_container
+ .set_child_properties(self.key, Some(OutOfLine(&mut view_properties)))
+ .unwrap();
+ self.host_node.set_translation(x_inset, y_inset, 10.0);
+ }
+}
diff --git a/bin/session_shell/ermine_session_shell/src/main.rs b/bin/session_shell/ermine_session_shell/src/main.rs
index 41e6c08..fd3a515 100644
--- a/bin/session_shell/ermine_session_shell/src/main.rs
+++ b/bin/session_shell/ermine_session_shell/src/main.rs
@@ -7,11 +7,11 @@
ServiceMarker};
use fidl_fuchsia_developer_tiles as tiles;
use fidl_fuchsia_math::SizeF;
-use fidl_fuchsia_modular::{StoryProviderProxy, StoryProviderWatcherMarker,
- StoryProviderWatcherRequest, StoryState, SessionShellContextMarker,
- SessionShellContextProxy};
-use fidl_fuchsia_ui_input::{KeyboardEvent, KeyboardEventPhase, MODIFIER_LEFT_CONTROL,
- MODIFIER_RIGHT_CONTROL};
+use fidl_fuchsia_modular::{SessionShellContextMarker, SessionShellContextProxy,
+ StoryProviderProxy, StoryProviderWatcherMarker,
+ StoryProviderWatcherRequest, StoryState};
+use fidl_fuchsia_ui_input::{KeyboardEvent, KeyboardEventPhase, MODIFIER_LEFT_SUPER,
+ MODIFIER_RIGHT_SUPER};
use fidl_fuchsia_ui_policy::{KeyboardCaptureListenerHackMarker,
KeyboardCaptureListenerHackRequest, PresentationProxy};
use fidl_fuchsia_ui_viewsv1::{ViewManagerMarker, ViewManagerProxy, ViewProviderMarker,
@@ -25,6 +25,7 @@
use parking_lot::Mutex;
use std::sync::Arc;
+mod ask_box;
mod view;
use crate::view::{ErmineView, ErmineViewPtr};
@@ -99,20 +100,14 @@
Ok(())
}
- pub fn setup_keyboard_hack(&mut self) -> Result<(), Error> {
- let (presentation_proxy, presentation_request) = create_proxy()?;
- self.session_shell_context
- .clone()
- .get_presentation(presentation_request)?;
- self.presentation_proxy = Some(presentation_proxy);
-
+ pub fn watch_for_key_event(&mut self, code_point: u32, modifiers: u32) -> Result<(), Error> {
let mut hotkey_event = KeyboardEvent {
event_time: 0,
device_id: 0,
phase: KeyboardEventPhase::Released,
hid_usage: 0,
- code_point: 0x67,
- modifiers: MODIFIER_LEFT_CONTROL | MODIFIER_RIGHT_CONTROL,
+ code_point: code_point,
+ modifiers: modifiers,
};
let (event_listener_client, event_listener_server) = zx::Channel::create()?;
let event_listener = ClientEnd::new(event_listener_client);
@@ -129,8 +124,8 @@
.into_stream()
.unwrap()
.try_for_each(move |event| match event {
- KeyboardCaptureListenerHackRequest::OnEvent { .. } => {
- println!("ermine: hotkey support goes here");
+ KeyboardCaptureListenerHackRequest::OnEvent { event, .. } => {
+ APP.lock().handle_hot_key(&event).expect("handle hot key");
futures::future::ready(Ok(()))
}
})
@@ -140,6 +135,19 @@
Ok(())
}
+ pub fn setup_keyboard_hack(&mut self) -> Result<(), Error> {
+ let (presentation_proxy, presentation_request) = create_proxy()?;
+ self.session_shell_context
+ .clone()
+ .get_presentation(presentation_request)?;
+ self.presentation_proxy = Some(presentation_proxy);
+
+ self.watch_for_key_event(0x20, MODIFIER_LEFT_SUPER)?;
+ self.watch_for_key_event(0x20, MODIFIER_RIGHT_SUPER)?;
+
+ Ok(())
+ }
+
fn next_story_key(&mut self) -> u32 {
let next_key = self.next_key;
self.next_key += 1;
@@ -263,6 +271,21 @@
);
Ok(())
}
+
+ pub fn handle_hot_key(&mut self, event: &KeyboardEvent) -> Result<(), Error> {
+ let key_to_use = self.next_story_key();
+ self.views[0].lock().handle_hot_key(event, key_to_use)
+ }
+
+ pub fn handle_suggestion(&mut self, text: Option<&str>) -> Result<(), Error> {
+ let mut view = self.views[0].lock();
+ if let Some(text) = text {
+ view.handle_suggestion(text)
+ .unwrap_or_else(|e| eprintln!("handle_suggestion error: {:?}", e));
+ }
+ view.remove_ask_box();
+ Ok(())
+ }
}
fn main() -> Result<(), Error> {
diff --git a/bin/session_shell/ermine_session_shell/src/view.rs b/bin/session_shell/ermine_session_shell/src/view.rs
index 64a556f..61b2fd5 100644
--- a/bin/session_shell/ermine_session_shell/src/view.rs
+++ b/bin/session_shell/ermine_session_shell/src/view.rs
@@ -2,18 +2,22 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-use failure::Error;
+use crate::ask_box::AskBox;
+use failure::{Error, ResultExt};
use fidl::encoding::OutOfLine;
use fidl::endpoints::{create_proxy, ClientEnd, ServerEnd, ServiceMarker};
use fidl_fuchsia_math::{InsetF, RectF, SizeF};
-use fidl_fuchsia_modular::{Intent, StoryProviderProxy};
+use fidl_fuchsia_modular::{AddMod, Intent, PuppetMasterMarker, PuppetMasterProxy, StoryCommand,
+ StoryProviderProxy, StoryPuppetMasterProxy, SurfaceArrangement,
+ SurfaceDependency, SurfaceRelation};
use fidl_fuchsia_ui_gfx::{self as gfx, ColorRgba};
use fidl_fuchsia_ui_input::{InputConnectionMarker, InputConnectionProxy, InputListenerMarker,
- InputListenerRequest};
+ InputListenerRequest, KeyboardEvent};
use fidl_fuchsia_ui_scenic::{SessionListenerMarker, SessionListenerRequest};
use fidl_fuchsia_ui_viewsv1::{CustomFocusBehavior, ViewContainerListenerMarker,
ViewContainerListenerRequest, ViewLayout, ViewListenerMarker,
ViewListenerRequest, ViewProperties};
+use fuchsia_app::client::connect_to_service;
use fuchsia_async as fasync;
use fuchsia_scenic::{EntityNode, ImportNode, Material, Rectangle, Session, SessionPtr, ShapeNode};
use fuchsia_zircon::{self as zx, Channel};
@@ -22,6 +26,32 @@
use parking_lot::Mutex;
use std::collections::BTreeMap;
use std::sync::Arc;
+use std::time::SystemTime;
+
+fn random_story_name() -> String {
+ let secs = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
+ Ok(n) => n.as_secs(),
+ Err(_) => panic!("SystemTime before UNIX EPOCH!"),
+ };
+ format!("ermine-story-{}", secs)
+}
+
+fn random_mod_name() -> String {
+ let secs = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
+ Ok(n) => n.as_secs(),
+ Err(_) => panic!("SystemTime before UNIX EPOCH!"),
+ };
+ format!("ermine-mod-{}", secs)
+}
+
+fn inset(rect: &mut RectF, border: f32) {
+ let inset = border.min(rect.width / 0.3).min(rect.height / 0.3);
+ rect.x += inset;
+ rect.y += inset;
+ let inset_width = inset * 2.0;
+ rect.width = rect.width - inset_width;
+ rect.height = rect.height - inset_width;
+}
struct ViewData {
key: u32,
@@ -50,6 +80,8 @@
pub struct ErmineView {
// Must keep the view proxy alive or the view goes away.
_view: fidl_fuchsia_ui_viewsv1::ViewProxy,
+ puppet_master: PuppetMasterProxy,
+ story_puppet_masters: BTreeMap<String, StoryPuppetMasterProxy>,
view_container: fidl_fuchsia_ui_viewsv1::ViewContainerProxy,
input_connection_proxy: InputConnectionProxy,
session: SessionPtr,
@@ -59,6 +91,7 @@
views: BTreeMap<u32, ViewData>,
width: f32,
height: f32,
+ ask_box: Option<AskBox>,
}
pub type ErmineViewPtr = Arc<Mutex<ErmineView>>;
@@ -90,8 +123,12 @@
input_connection_request.into_channel(),
)?;
+ let puppet_master = connect_to_service::<PuppetMasterMarker>()?;
+
let view_controller = ErmineView {
_view: view,
+ puppet_master,
+ story_puppet_masters: BTreeMap::new(),
view_container: view_container_proxy,
input_connection_proxy: input_connection_proxy,
session: session.clone(),
@@ -101,6 +138,7 @@
views: BTreeMap::new(),
width: 0.0,
height: 0.0,
+ ask_box: None,
};
let view_controller = Arc::new(Mutex::new(view_controller));
@@ -386,66 +424,125 @@
(keys, urls, sizes, fs)
}
- fn inset(rect: &mut RectF, border: f32) {
- let inset = border.min(rect.width / 0.3).min(rect.height / 0.3);
- rect.x += inset;
- rect.y += inset;
- let inset_width = inset * 2.0;
- rect.width = rect.width - inset_width;
- rect.height = rect.height - inset_width;
+ pub fn handle_hot_key(&mut self, event: &KeyboardEvent, key_to_use: u32) -> Result<(), Error> {
+ if event.code_point == 0x20 {
+ if let Some(ask_box) = self.ask_box.as_mut() {
+ ask_box.focus(&self.view_container)?;
+ } else {
+ self.ask_box = Some(AskBox::new(
+ key_to_use,
+ &self.session,
+ &self.view_container,
+ &self.import_node,
+ )?);
+ self.update();
+ self.layout();
+ }
+ }
+ Ok(())
+ }
+
+ pub fn remove_ask_box(&mut self) {
+ if let Some(mut ask_box) = self.ask_box.take() {
+ ask_box
+ .remove(&self.view_container)
+ .unwrap_or_else(|e| eprintln!("ask_box.remove error: {:?}", e));
+ }
+ }
+
+ pub fn handle_suggestion(&mut self, text: &str) -> Result<(), Error> {
+ let story_name = random_story_name();
+ let package = format!("fuchsia-pkg://fuchsia.com/{}#meta/{}.cmx", text, text);
+ let (story_puppet_master, story_puppet_master_end) =
+ create_proxy().context("handle_suggestion control_story")?;
+ self.puppet_master
+ .control_story(&story_name, story_puppet_master_end)?;
+ let mut commands = [StoryCommand::AddMod(AddMod {
+ mod_name: vec![random_mod_name()],
+ intent: Intent {
+ action: None,
+ handler: Some(package),
+ parameters: None,
+ },
+ surface_parent_mod_name: None,
+ surface_relation: SurfaceRelation {
+ arrangement: SurfaceArrangement::None,
+ dependency: SurfaceDependency::None,
+ emphasis: 1.0,
+ },
+ })];
+ story_puppet_master
+ .enqueue(&mut commands.iter_mut())
+ .context("handle_suggestion story_puppet_master.enqueue")?;
+ let f = story_puppet_master.execute();
+ fasync::spawn(
+ f.map_ok(move |_| {})
+ .unwrap_or_else(|e| eprintln!("puppetmaster error: {:?}", e)),
+ );
+ self.story_puppet_masters
+ .insert(story_name, story_puppet_master);
+
+ Ok(())
}
pub fn layout(&mut self) {
- if self.views.is_empty() {
- return;
+ if !self.views.is_empty() {
+ let num_tiles = self.views.len();
+
+ let columns = (num_tiles as f32).sqrt().ceil() as usize;
+ let rows = (columns + num_tiles - 1) / columns;
+ let tile_height = (self.height / rows as f32).floor();
+
+ for (row_index, view_chunk) in self
+ .views
+ .iter_mut()
+ .chunks(columns)
+ .into_iter()
+ .enumerate()
+ {
+ let tiles_in_row = if row_index == rows - 1 && (num_tiles % columns) != 0 {
+ num_tiles % columns
+ } else {
+ columns
+ };
+ let tile_width = (self.width / tiles_in_row as f32).floor();
+ for (column_index, (_key, view)) in view_chunk.enumerate() {
+ let mut tile_bounds = RectF {
+ height: tile_height,
+ width: tile_width,
+ x: column_index as f32 * tile_width,
+ y: row_index as f32 * tile_height,
+ };
+ inset(&mut tile_bounds, 10.0);
+ let mut view_properties = ViewProperties {
+ custom_focus_behavior: Some(Box::new(CustomFocusBehavior {
+ allow_focus: view.allow_focus,
+ })),
+ view_layout: Some(Box::new(ViewLayout {
+ inset: InsetF {
+ bottom: 0.0,
+ left: 0.0,
+ right: 0.0,
+ top: 0.0,
+ },
+ size: SizeF {
+ width: tile_bounds.width,
+ height: tile_bounds.height,
+ },
+ })),
+ };
+ self.view_container
+ .set_child_properties(view.key, Some(OutOfLine(&mut view_properties)))
+ .unwrap();
+ view.host_node
+ .set_translation(tile_bounds.x, tile_bounds.y, 0.0);
+ view.bounds = Some(tile_bounds);
+ }
+ }
}
- let num_tiles = self.views.len();
-
- let columns = (num_tiles as f32).sqrt().ceil() as usize;
- let rows = (columns + num_tiles - 1) / columns;
- let tile_height = (self.height / rows as f32).floor();
-
- for (row_index, view_chunk) in itertools::enumerate(&self.views.iter_mut().chunks(columns))
- {
- let tiles_in_row = if row_index == rows - 1 && (num_tiles % columns) != 0 {
- num_tiles % columns
- } else {
- columns
- };
- let tile_width = (self.width / tiles_in_row as f32).floor();
- for (column_index, (_key, view)) in view_chunk.enumerate() {
- let mut tile_bounds = RectF {
- height: tile_height,
- width: tile_width,
- x: column_index as f32 * tile_width,
- y: row_index as f32 * tile_height,
- };
- Self::inset(&mut tile_bounds, 10.0);
- let mut view_properties = ViewProperties {
- custom_focus_behavior: Some(Box::new(CustomFocusBehavior {
- allow_focus: view.allow_focus,
- })),
- view_layout: Some(Box::new(ViewLayout {
- inset: InsetF {
- bottom: 0.0,
- left: 0.0,
- right: 0.0,
- top: 0.0,
- },
- size: SizeF {
- width: tile_bounds.width,
- height: tile_bounds.height,
- },
- })),
- };
- self.view_container
- .set_child_properties(view.key, Some(OutOfLine(&mut view_properties)))
- .unwrap();
- view.host_node
- .set_translation(tile_bounds.x, tile_bounds.y, 0.0);
- view.bounds = Some(tile_bounds);
- }
+ if let Some(ask_box) = self.ask_box.as_ref() {
+ ask_box.layout(&self.view_container, self.width, self.height);
}
}
}
diff --git a/bin/ui/text_input_mod/BUILD.gn b/bin/ui/text_input_mod/BUILD.gn
new file mode 100644
index 0000000..d7b2dbc
--- /dev/null
+++ b/bin/ui/text_input_mod/BUILD.gn
@@ -0,0 +1,38 @@
+# Copyright 2018 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("//topaz/runtime/flutter_runner/flutter_app.gni")
+
+flutter_app("text_input_mod") {
+ main_dart = "lib/main.dart"
+ package_name = "text_input_mod"
+
+ fuchsia_package_name = "text_input_mod"
+
+ sources = []
+ deps = [
+ "//third_party/dart-pkg/git/flutter/packages/flutter",
+ "//topaz/public/dart/widgets:lib.widgets",
+ "//topaz/examples/modular/models",
+ "//topaz/public/dart/fuchsia_logger",
+ "//topaz/public/dart/fuchsia_modular",
+ "//topaz/public/dart/fidl",
+ "//topaz/public/dart/widgets:lib.widgets",
+ "//topaz/public/lib/app/dart",
+ "//topaz/public/lib/app_driver/dart",
+ "//topaz/public/lib/module/dart",
+ "//topaz/public/lib/module_resolver/dart",
+ "//topaz/public/lib/ui/flutter",
+ "//topaz/public/lib/widgets/dart",
+ "//topaz/bin/ui/text_input_mod/public/fidl:fuchsia.textinputmod",
+ ]
+
+ meta = [
+ {
+ path = rebase_path("meta/text_input_mod.cmx")
+ dest = "text_input_mod.cmx"
+ },
+ ]
+}
+
diff --git a/bin/ui/text_input_mod/README.md b/bin/ui/text_input_mod/README.md
new file mode 100644
index 0000000..2c2d604
--- /dev/null
+++ b/bin/ui/text_input_mod/README.md
@@ -0,0 +1 @@
+A simple module which shows a text input
\ No newline at end of file
diff --git a/bin/ui/text_input_mod/analysis_options.yaml b/bin/ui/text_input_mod/analysis_options.yaml
new file mode 100644
index 0000000..f0cea8a
--- /dev/null
+++ b/bin/ui/text_input_mod/analysis_options.yaml
@@ -0,0 +1,5 @@
+# Copyright 2018 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: ../../../tools/analysis_options.yaml
diff --git a/bin/ui/text_input_mod/lib/main.dart b/bin/ui/text_input_mod/lib/main.dart
new file mode 100644
index 0000000..d620434
--- /dev/null
+++ b/bin/ui/text_input_mod/lib/main.dart
@@ -0,0 +1,130 @@
+// Copyright 2018 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 'dart:async';
+
+import 'package:fidl/fidl.dart';
+import 'package:flutter/material.dart';
+import 'package:fidl_fuchsia_textinputmod/fidl_async.dart';
+import 'package:lib.app.dart/app_async.dart';
+
+class ErmineTextInputMod implements TextInputMod {
+ TextInputModReceiverProxy textInputModReceiverProxy;
+ Completer completer;
+ void sendData(String text) {
+ if (completer != null) {
+ textInputModReceiverProxy.userEnteredText(text).then((_) {
+ completer.complete();
+ });
+ }
+ }
+
+ void cancel() {
+ if (completer != null) {
+ textInputModReceiverProxy.userCanceled().then((_) {
+ completer.complete();
+ });
+ }
+ }
+
+ @override
+ Future<void> listenForTextInput(
+ InterfaceHandle<TextInputModReceiver> receiver) {
+ textInputModReceiverProxy = TextInputModReceiverProxy();
+
+ textInputModReceiverProxy.ctrl.bind(receiver);
+ completer = Completer<void>();
+
+ return completer.future;
+ }
+}
+
+void main() {
+ TextInputMod textInputMod = ErmineTextInputMod();
+
+ StartupContext startupContext = StartupContext.fromStartupInfo();
+
+ startupContext.outgoingServices.addServiceForName(
+ (InterfaceRequest<TextInputMod> request) {
+ TextInputModBinding().bind(textInputMod, request);
+ },
+ TextInputMod.$serviceName,
+ );
+
+ ValueNotifier<String> text = ValueNotifier('null');
+
+ final FocusNode focusNode = FocusNode();
+
+ runApp(MyApp(focusNode: focusNode, textInputMod: textInputMod, text: text));
+}
+
+class MyApp extends StatelessWidget {
+ final FocusNode focusNode;
+ final ErmineTextInputMod textInputMod;
+ final ValueNotifier<String> text;
+
+ /// Constructor
+ const MyApp({
+ @required this.focusNode,
+ @required this.textInputMod,
+ @required this.text,
+ Key key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ FocusScope.of(context).requestFocus(
+ focusNode,
+ );
+ return MaterialApp(
+ title: 'text input',
+ theme: ThemeData(
+ primarySwatch: Colors.blue,
+ ),
+ home: Scaffold(
+ backgroundColor: Colors.blueGrey,
+ body: Container(
+ child: Center(
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ padding: EdgeInsets.all(5),
+ child: IconButton(
+ icon: Icon(Icons.cancel),
+ iconSize: 30,
+ color: Colors.grey[100],
+ disabledColor: Colors.grey[500],
+ onPressed: textInputMod.cancel,
+ ),
+ ),
+ Flexible(
+ child: TextField(
+ focusNode: focusNode,
+ onChanged: (v) {
+ text.value = v;
+ },
+ onSubmitted: textInputMod.sendData,
+ decoration: InputDecoration(hintText: 'Enter Mod URL'),
+ ),
+ ),
+ Container(
+ padding: EdgeInsets.all(5),
+ child: IconButton(
+ icon: Icon(Icons.send),
+ iconSize: 30,
+ color: Colors.grey[100],
+ disabledColor: Colors.grey[500],
+ onPressed: () {
+ textInputMod.sendData(text.value);
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/bin/ui/text_input_mod/manifest.json b/bin/ui/text_input_mod/manifest.json
new file mode 100644
index 0000000..06ea4de
--- /dev/null
+++ b/bin/ui/text_input_mod/manifest.json
@@ -0,0 +1,6 @@
+{
+ "@version": 2,
+ "binary": "text_input_mod",
+ "suggestion_headline": "Show a text input",
+ "intent_filters": []
+}
\ No newline at end of file
diff --git a/bin/ui/text_input_mod/meta/text_input_mod.cmx b/bin/ui/text_input_mod/meta/text_input_mod.cmx
new file mode 100644
index 0000000..2aec1c3
--- /dev/null
+++ b/bin/ui/text_input_mod/meta/text_input_mod.cmx
@@ -0,0 +1,22 @@
+{
+ "program": {
+ "data": "data/text_input_mod"
+ },
+ "sandbox": {
+ "services": [
+ "fuchsia.fonts.Provider",
+ "fuchsia.sys.Environment",
+ "fuchsia.logger.LogSink",
+ "fuchsia.cobalt.LoggerFactory",
+ "fuchsia.modular.ContextWriter",
+ "fuchsia.ui.viewsv1.ViewManager",
+ "fuchsia.ui.scenic.Scenic",
+ "fuchsia.ui.input.ImeService",
+ "fuchsia.ui.policy.Presenter",
+ "fuchsia.modular.Clipboard",
+ "fuchsia.modular.ComponentContext",
+ "fuchsia.modular.ModuleContext",
+ "fuchsia.textinputmod.TextInputModReceiver"
+ ]
+ }
+}
diff --git a/bin/ui/text_input_mod/public/fidl/BUILD.gn b/bin/ui/text_input_mod/public/fidl/BUILD.gn
new file mode 100644
index 0000000..f4c5987
--- /dev/null
+++ b/bin/ui/text_input_mod/public/fidl/BUILD.gn
@@ -0,0 +1,11 @@
+# Copyright 2018 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/fidl/fidl.gni")
+
+fidl("fuchsia.textinputmod") {
+ sources = [
+ "textinputmod.fidl",
+ ]
+}
diff --git a/bin/ui/text_input_mod/public/fidl/textinputmod.fidl b/bin/ui/text_input_mod/public/fidl/textinputmod.fidl
new file mode 100644
index 0000000..f8cf2dc
--- /dev/null
+++ b/bin/ui/text_input_mod/public/fidl/textinputmod.fidl
@@ -0,0 +1,16 @@
+// Copyright 2018 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.
+
+library fuchsia.textinputmod;
+
+[Discoverable]
+interface TextInputModReceiver {
+ 1: UserEnteredText(string text) -> ();
+ 2: UserCanceled() -> ();
+};
+
+[Discoverable]
+interface TextInputMod {
+ 1: ListenForTextInput(TextInputModReceiver receiver) -> ();
+};
diff --git a/bin/ui/text_input_mod/pubspec.yaml b/bin/ui/text_input_mod/pubspec.yaml
new file mode 100644
index 0000000..de784fc
--- /dev/null
+++ b/bin/ui/text_input_mod/pubspec.yaml
@@ -0,0 +1,3 @@
+# Copyright 2018 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.
diff --git a/examples/ui/text_flutter/BUILD.gn b/examples/ui/text_flutter/BUILD.gn
index 203e272..4a974f8 100644
--- a/examples/ui/text_flutter/BUILD.gn
+++ b/examples/ui/text_flutter/BUILD.gn
@@ -15,6 +15,7 @@
"//garnet/public/fidl/fuchsia.ui.viewsv1",
"//third_party/dart-pkg/git/flutter/packages/flutter",
"//topaz/public/dart/widgets:lib.widgets",
+ "//topaz/public/lib/app_driver/dart",
"//topaz/public/lib/ui/flutter",
]
diff --git a/packages/products/ermine b/packages/products/ermine
index d828258..9644279 100644
--- a/packages/products/ermine
+++ b/packages/products/ermine
@@ -13,5 +13,9 @@
"topaz/packages/prod/sysui",
"topaz/packages/prod/term",
"topaz/packages/prod/web_runner_prototype"
+ ],
+
+ "packages": [
+ "//topaz/bin/ui/text_input_mod"
]
}