[ermine] Add libstoryshell to sessionshell.

- Use story lab UX for story chrome.

Change-Id: I9db42c83f3775fb493317ff084156e28428ae74f
diff --git a/bin/flutter_screencap_test/test/flutter_screencap_test.dart b/bin/flutter_screencap_test/test/flutter_screencap_test.dart
index 2ff7158..4c22985 100644
--- a/bin/flutter_screencap_test/test/flutter_screencap_test.dart
+++ b/bin/flutter_screencap_test/test/flutter_screencap_test.dart
@@ -32,13 +32,19 @@
     intent: modular.Intent(action: 'action', handler: _testAppUrl),
     surfaceRelation: modular.SurfaceRelation());
 
-final _ermineConfig = BasemgrConfig(sessionShellMap: [
-  SessionShellMapEntry(
+final _ermineConfig = BasemgrConfig(
+  sessionShellMap: [
+    SessionShellMapEntry(
       name: 'Ermine',
       config: SessionShellConfig(
-          appConfig: AppConfig(
-              url: 'fuchsia-pkg://fuchsia.com/ermine#meta/ermine.cmx')))
-]);
+        appConfig: AppConfig(
+          url: 'fuchsia-pkg://fuchsia.com/ermine#meta/ermine.cmx',
+        ),
+      ),
+    ),
+  ],
+  useSessionShellForStoryShellFactory: true,
+);
 
 // Use a custom timeout rather than the test framework's timeout so that we can
 // output a sensible failure message.
diff --git a/public/dart/story_shell_labs/lib/src/layout/deja_layout/deja_layout.dart b/public/dart/story_shell_labs/lib/src/layout/deja_layout/deja_layout.dart
index 34c1aa6..c9added 100644
--- a/public/dart/story_shell_labs/lib/src/layout/deja_layout/deja_layout.dart
+++ b/public/dart/story_shell_labs/lib/src/layout/deja_layout/deja_layout.dart
@@ -98,8 +98,8 @@
   void _userLayoutRequest(TilerModel<ModuleInfo> model) {
     final modsToRemove = getModsDifference(_tilerModel, model);
     _tilerModel = model;
-    removeSurface(modsToRemove);
     if (modsToRemove.isNotEmpty) {
+      removeSurface(modsToRemove);
       // Regenerate Layout Suggestions.
       final tilerModels = _layoutPolicy.getLayout(_tilerModel);
       _tilerModel = tilerModels.first;
diff --git a/public/dart/story_shell_labs/lib/src/layout/deja_layout/layout_store.dart b/public/dart/story_shell_labs/lib/src/layout/deja_layout/layout_store.dart
index 26e5a21..548187c 100644
--- a/public/dart/story_shell_labs/lib/src/layout/deja_layout/layout_store.dart
+++ b/public/dart/story_shell_labs/lib/src/layout/deja_layout/layout_store.dart
@@ -20,7 +20,7 @@
   final int size;
 
   /// Constructor for layout storage.
-  LayoutStore({this.directory = '/data/layouts', this.size = 100});
+  LayoutStore({this.directory = '/pkg/data/layouts', this.size = 100});
 
   /// Clears the storage.
   ///
diff --git a/public/dart/story_shell_labs/lib/src/layout/tile_presenter/tile_presenter.dart b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/tile_presenter.dart
index 598d891..73e6be7 100644
--- a/public/dart/story_shell_labs/lib/src/layout/tile_presenter/tile_presenter.dart
+++ b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/tile_presenter.dart
@@ -31,7 +31,7 @@
   // Stream controllers
   final _layoutSuggestionController =
       StreamController<LayoutSuggestionUpdate>.broadcast();
-  final _updateController = StreamController<TileLayoutModel>();
+  final _updateController = StreamController<TileLayoutModel>.broadcast();
 
   /// Streams the current layout.
   Stream<TileLayoutModel> get update => _updateController.stream;
diff --git a/shell/ermine/BUILD.gn b/shell/ermine/BUILD.gn
index b35249e..ec7d8f3 100644
--- a/shell/ermine/BUILD.gn
+++ b/shell/ermine/BUILD.gn
@@ -55,7 +55,9 @@
     "src/widgets/cluster.dart",
     "src/widgets/clusters.dart",
     "src/widgets/fullscreen_story.dart",
+    "src/widgets/remove_button_target_widget.dart",
     "src/widgets/status.dart",
+    "src/widgets/story_widget.dart",
     "src/widgets/tile_chrome.dart",
     "src/widgets/tile_sizer.dart",
     "src/widgets/tile_tab.dart",
@@ -79,6 +81,7 @@
     "//topaz/public/dart/fuchsia_modular_flutter",
     "//topaz/public/dart/fuchsia_scenic_flutter",
     "//topaz/public/dart/fuchsia_services",
+    "//topaz/public/dart/story_shell_labs:story_shell_labs_lib",
     "//topaz/public/dart/widgets:lib.widgets",
   ]
 }
diff --git a/shell/ermine/config/modular_config.json b/shell/ermine/config/modular_config.json
index d40d39b..595376b 100644
--- a/shell/ermine/config/modular_config.json
+++ b/shell/ermine/config/modular_config.json
@@ -9,7 +9,7 @@
         "display_usage": "near"
       }
     ],
-    "story_shell_url": "fuchsia-pkg://fuchsia.com/story_shell_labs#meta/story_shell_labs.cmx"
+    "use_session_shell_for_story_shell_factory": true
   },
   "sessionmgr": {
     "startup_agents": [
diff --git a/shell/ermine/lib/src/models/app_model.dart b/shell/ermine/lib/src/models/app_model.dart
index 7f4a0c2..f6b4909 100644
--- a/shell/ermine/lib/src/models/app_model.dart
+++ b/shell/ermine/lib/src/models/app_model.dart
@@ -20,6 +20,7 @@
     show ViewToken, ViewHolderToken;
 import 'package:flutter/material.dart';
 import 'package:fuchsia_modular_flutter/session_shell.dart' show SessionShell;
+import 'package:fuchsia_modular_flutter/story_shell.dart' show StoryShell;
 import 'package:fuchsia_scenic_flutter/child_view_connection.dart'
     show ChildViewConnection;
 import 'package:fuchsia_services/services.dart' show Incoming, StartupContext;
@@ -70,6 +71,11 @@
       onStoryDeleted: clustersModel.removeStory,
     )..start();
 
+    StoryShell.advertise(
+      startupContext: _startupContext,
+      onStoryAttached: clustersModel.getStory,
+    );
+
     // Load the ask bar.
     _loadAskBar();
   }
@@ -104,6 +110,16 @@
 
     // Display the Ask bar after a brief duration.
     Timer(Duration(milliseconds: 500), onMeta);
+
+    // Hide the ask bar when:
+    // - a story is started from outside of ask bar.
+    // - a story toggles fullscreen state.
+    // - story cluster changes.
+    Listenable.merge([
+      clustersModel,
+      clustersModel.currentCluster,
+      clustersModel.fullscreenStoryNotifier,
+    ]).addListener(onCancel);
   }
 
   void _loadAskBar() {
diff --git a/shell/ermine/lib/src/models/cluster_model.dart b/shell/ermine/lib/src/models/cluster_model.dart
index 57e28b7..e76f045 100644
--- a/shell/ermine/lib/src/models/cluster_model.dart
+++ b/shell/ermine/lib/src/models/cluster_model.dart
@@ -126,7 +126,7 @@
 
   /// Set's the [currentCluster] to the next cluster, if available.
   void nextCluster() {
-    if (currentCluster.value != clusters.last) {
+    if (fullscreenStory == null && currentCluster.value != clusters.last) {
       currentCluster.value =
           clusters[clusters.indexOf(currentCluster.value) + 1];
     }
@@ -134,7 +134,7 @@
 
   /// Set's the [currentCluster] to the previous cluster, if available.
   void previousCluster() {
-    if (currentCluster.value != clusters.first) {
+    if (fullscreenStory == null && currentCluster.value != clusters.first) {
       currentCluster.value =
           clusters[clusters.indexOf(currentCluster.value) - 1];
     }
diff --git a/shell/ermine/lib/src/models/ermine_story.dart b/shell/ermine/lib/src/models/ermine_story.dart
index 06d9ce5..ebc39ae 100644
--- a/shell/ermine/lib/src/models/ermine_story.dart
+++ b/shell/ermine/lib/src/models/ermine_story.dart
@@ -2,18 +2,22 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:collection';
 import 'package:flutter/foundation.dart';
 
 import 'package:fidl_fuchsia_modular/fidl_async.dart'
     show StoryInfo, StoryController, StoryState, StoryVisibilityState;
 import 'package:fuchsia_modular_flutter/session_shell.dart'
     show SessionShell, Story;
+import 'package:fuchsia_modular_flutter/story_shell.dart'
+    show StoryShell, Surface;
 import 'package:fuchsia_scenic_flutter/child_view_connection.dart';
+import 'package:story_shell_labs_lib/layout/deja_layout.dart';
 
 import 'cluster_model.dart';
 
 /// Defines a concrete implementation for [Story] for Ermine.
-class ErmineStory implements Story {
+class ErmineStory implements Story, StoryShell {
   @override
   final StoryInfo info;
 
@@ -23,6 +27,8 @@
 
   final ClustersModel clustersModel;
 
+  final DejaLayout layoutManager = DejaLayout();
+
   ErmineStory({
     this.info,
     this.sessionShell,
@@ -86,4 +92,34 @@
   void restore() {
     clustersModel.restore(id);
   }
+
+  ValueNotifier<bool> editStateNotifier = ValueNotifier(false);
+  void edit() => editStateNotifier.value = !editStateNotifier.value;
+
+  final _surfaces = <Surface>[];
+
+  @override
+  void onSurfaceAdded(Surface surface) {
+    _surfaces.add(surface);
+    layoutManager.addSurface(
+      // TODO: get the intent and parameters from the addSurface call.
+      intent: 'no action yet',
+      parameters: UnmodifiableListView<String>([]),
+      surfaceId: surface.id,
+      view: surface.childViewConnection,
+    );
+  }
+
+  @override
+  void onSurfaceFocusChange(Surface surface, {bool focus = false}) {}
+
+  @override
+  void onSurfaceRemoved(Surface surface) {
+    _surfaces.remove(surface);
+    layoutManager.removeSurface([surface.id]);
+  }
+
+  @override
+  // TODO: implement surfaces
+  Iterable<Surface> get surfaces => _surfaces;
 }
diff --git a/shell/ermine/lib/src/widgets/cluster.dart b/shell/ermine/lib/src/widgets/cluster.dart
index ca1a9e8..6874c53 100644
--- a/shell/ermine/lib/src/widgets/cluster.dart
+++ b/shell/ermine/lib/src/widgets/cluster.dart
@@ -4,11 +4,11 @@
 
 import 'package:flutter/material.dart';
 import 'package:tiler/tiler.dart' show Tiler, TileModel;
-import 'package:fuchsia_scenic_flutter/child_view.dart' show ChildView;
 
 import '../models/cluster_model.dart';
 import '../models/ermine_story.dart';
 
+import 'story_widget.dart';
 import 'tile_chrome.dart';
 import 'tile_sizer.dart';
 import 'tile_tab.dart';
@@ -49,33 +49,38 @@
   Widget _chromeBuilder(BuildContext context, TileModel<ErmineStory> tile,
       {bool custom = false}) {
     final story = tile.content;
+    final confirmEditNotifier = ValueNotifier<bool>(null);
     return AnimatedBuilder(
       animation: story.childViewConnectionNotifier,
       builder: (context, child) {
         return story.childViewConnection != null
             ? AnimatedBuilder(
-                animation: story.focusedNotifier,
+                animation: Listenable.merge([
+                  story.focusedNotifier,
+                  story.editStateNotifier,
+                ]),
                 builder: (context, child) => TileChrome(
                       name: story.id ?? '<title>',
                       showTitle: !custom,
+                      editing: story.editStateNotifier.value,
                       focused: story.focused,
                       child: AnimatedBuilder(
                         animation: story.visibilityStateNotifier,
                         builder: (context, child) => story.isImmersive
                             ? Offstage()
-                            : GestureDetector(
-                                behavior: HitTestBehavior.translucent,
-                                // Disable listview scrolling on top of cluster.
-                                onHorizontalDragStart: (_) {},
-                                onTap: story.focus,
-                                child: ChildView(
-                                  connection: story.childViewConnection,
-                                ),
+                            : StoryWidget(
+                                editing: story.editStateNotifier.value,
+                                confirmEdit: confirmEditNotifier,
+                                presenter: story.layoutManager.presenter,
                               ),
                       ),
+                      onTap: story.focus,
                       onDelete: story.delete,
                       onFullscreen: story.maximize,
                       onMinimize: story.restore,
+                      onEdit: story.edit,
+                      onCancelEdit: () => confirmEditNotifier.value = false,
+                      onConfirmEdit: () => confirmEditNotifier.value = true,
                     ),
               )
             : Offstage();
diff --git a/shell/ermine/lib/src/widgets/clusters.dart b/shell/ermine/lib/src/widgets/clusters.dart
index 291b012..2340e88 100644
--- a/shell/ermine/lib/src/widgets/clusters.dart
+++ b/shell/ermine/lib/src/widgets/clusters.dart
@@ -20,11 +20,16 @@
       if (pageController.hasClients) {
         int index = model.clusters.indexOf(model.currentCluster.value);
         if (index != pageController.page) {
-          pageController.animateToPage(
-            index,
-            duration: Duration(milliseconds: 200),
-            curve: Curves.easeOut,
-          );
+          if (model.clusters[index].isEmpty &&
+              model.clusters[pageController.page.toInt()].isEmpty) {
+            pageController.jumpToPage(index);
+          } else {
+            pageController.animateToPage(
+              index,
+              duration: Duration(milliseconds: 200),
+              curve: Curves.easeOut,
+            );
+          }
         }
       }
     });
diff --git a/shell/ermine/lib/src/widgets/fullscreen_story.dart b/shell/ermine/lib/src/widgets/fullscreen_story.dart
index 3b1bad1..3834148 100644
--- a/shell/ermine/lib/src/widgets/fullscreen_story.dart
+++ b/shell/ermine/lib/src/widgets/fullscreen_story.dart
@@ -3,9 +3,9 @@
 // found in the LICENSE file.
 
 import 'package:flutter/material.dart';
-import 'package:fuchsia_scenic_flutter/child_view.dart' show ChildView;
 
 import '../models/app_model.dart';
+import 'story_widget.dart';
 import 'tile_chrome.dart';
 
 /// Defines a widget to display a story fullscreen.
@@ -22,13 +22,14 @@
       animation: model.clustersModel.fullscreenStoryNotifier,
       builder: (context, child) {
         final story = model.clustersModel.fullscreenStory;
+        final confirmEditNotifier = ValueNotifier<bool>(null);
         return story != null
             ? AnimatedBuilder(
-                animation: showFullscreenTitle,
-                child: ChildView(
-                  connection: story.childViewConnection,
-                ),
-                builder: (context, child) {
+                animation: Listenable.merge([
+                  showFullscreenTitle,
+                  story.editStateNotifier,
+                ]),
+                builder: (context, _) {
                   return Listener(
                     onPointerHover: (event) {
                       if (event.position.dy == 0) {
@@ -41,10 +42,18 @@
                       name: story.id,
                       focused: story.focused,
                       showTitle: showFullscreenTitle.value,
+                      editing: story.editStateNotifier.value,
                       fullscreen: true,
-                      child: child,
+                      child: StoryWidget(
+                        editing: story.editStateNotifier.value,
+                        presenter: story.layoutManager.presenter,
+                        confirmEdit: confirmEditNotifier,
+                      ),
                       onDelete: story.delete,
                       onMinimize: story.restore,
+                      onEdit: story.edit,
+                      onCancelEdit: () => confirmEditNotifier.value = false,
+                      onConfirmEdit: () => confirmEditNotifier.value = true,
                     ),
                   );
                 })
diff --git a/shell/ermine/lib/src/widgets/remove_button_target_widget.dart b/shell/ermine/lib/src/widgets/remove_button_target_widget.dart
new file mode 100644
index 0000000..b382285
--- /dev/null
+++ b/shell/ermine/lib/src/widgets/remove_button_target_widget.dart
@@ -0,0 +1,73 @@
+import 'package:flutter/material.dart';
+import 'package:tiler/tiler.dart' show TileModel;
+
+/// Widget that acts as a button to remove currently focused mod's tile,
+/// and drop target to remove a tile by drag-and-drop
+class RemoveButtonTargetWidget extends StatefulWidget {
+  const RemoveButtonTargetWidget({
+    @required this.onTap,
+  });
+
+  /// Callback when button is tapped
+  final VoidCallback onTap;
+
+  @override
+  _RemoveButtonTargetWidgetState createState() =>
+      _RemoveButtonTargetWidgetState();
+}
+
+class _RemoveButtonTargetWidgetState extends State<RemoveButtonTargetWidget> {
+  final _touching = ValueNotifier<bool>(false);
+
+  @override
+  Widget build(BuildContext context) {
+    return DragTarget<TileModel>(
+      builder: (_, candidateData, ___) {
+        return GestureDetector(
+          onTap: widget.onTap,
+          onTapDown: (_) {
+            _touching.value = true;
+          },
+          onTapCancel: () {
+            _touching.value = false;
+          },
+          onTapUp: (_) {
+            _touching.value = false;
+          },
+          child: AnimatedBuilder(
+            animation: _touching,
+            builder: (context, snapshot) {
+              final hovering = candidateData.isNotEmpty || _touching.value;
+              final foreground = hovering ? Colors.white : Colors.black;
+              final background = hovering ? Colors.black : Colors.white;
+              return Material(
+                color: background,
+                elevation: 24,
+                child: AspectRatio(
+                  aspectRatio: 1,
+                  child: Container(
+                    decoration: BoxDecoration(
+                      border: Border.all(
+                        width: 2.0,
+                        color: foreground,
+                      ),
+                    ),
+                    child: FractionallySizedBox(
+                      widthFactor: .75,
+                      child: Center(
+                        child: Container(
+                          height: 2.0,
+                          color: foreground,
+                        ),
+                      ),
+                    ),
+                  ),
+                ),
+              );
+            },
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/shell/ermine/lib/src/widgets/story_widget.dart b/shell/ermine/lib/src/widgets/story_widget.dart
new file mode 100644
index 0000000..ad31d32
--- /dev/null
+++ b/shell/ermine/lib/src/widgets/story_widget.dart
@@ -0,0 +1,200 @@
+// Copyright 2019 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:built_collection/built_collection.dart';
+import 'package:flutter/material.dart';
+import 'package:fuchsia_scenic_flutter/child_view_connection.dart'
+    show ChildViewConnection;
+import 'package:tiler/tiler.dart';
+import 'package:story_shell_labs_lib/layout/tile_model.dart';
+import 'package:story_shell_labs_lib/layout/tile_presenter.dart';
+
+import 'remove_button_target_widget.dart';
+
+final List<Color> _kColors = [
+  Colors.red,
+  Colors.blue,
+  Colors.yellow,
+  Colors.green,
+  Colors.pink,
+  Colors.orange,
+  Colors.purple,
+];
+
+class StoryWidget extends StatefulWidget {
+  final TilePresenter presenter;
+  final ValueNotifier<bool> confirmEdit;
+  final bool editing;
+
+  const StoryWidget({
+    @required this.presenter,
+    this.confirmEdit,
+    this.editing = false,
+  });
+
+  @override
+  _StoryWidgetState createState() => _StoryWidgetState();
+}
+
+class _StoryWidgetState extends State<StoryWidget> {
+  /// Used for resizing locally, moving, etc when in edit mode.
+  /// Once out of edit mode, LayoutBloc is notified with the updated model.
+  TilerModel<ModuleInfo> _tilerModel;
+  BuiltMap<String, ChildViewConnection> _connections;
+  StreamSubscription _tilerUpdateListener;
+  bool _isEditing = false;
+  OverlayEntry _layoutSuggestionsOverlay;
+  Map<String, Color> _parametersToColors;
+  final ValueNotifier _focusedMod = ValueNotifier<String>(null);
+
+  @override
+  void initState() {
+    _resetTilerModel();
+    _isEditing = widget.editing;
+    widget.confirmEdit.addListener(_confirmEditListener);
+    _tilerUpdateListener = widget.presenter.update.listen((update) {
+      setState(() {
+        _isEditing = false;
+        _resetTilerModel(update: update);
+      });
+      updateLayoutSuggestionsOverlayVisibility();
+    });
+    super.initState();
+  }
+
+  @override
+  void didUpdateWidget(StoryWidget oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    _isEditing = widget.editing;
+    oldWidget.confirmEdit.removeListener(_confirmEditListener);
+    widget.confirmEdit.addListener(_confirmEditListener);
+  }
+
+  @override
+  void dispose() {
+    _tilerUpdateListener.cancel();
+    super.dispose();
+  }
+
+  void _resetTilerModel({TileLayoutModel update}) {
+    update ??= widget.presenter.currentState;
+    _tilerModel = update.model;
+    _connections = update.connections;
+    _parametersToColors = _mapFromKeysAndCircularValues(
+      _allParametersInModel(_tilerModel),
+      _kColors,
+    );
+  }
+
+  Iterable<ModuleInfo> _flattenTileModel(TileModel tile) => tile == null
+      ? []
+      : (tile.tiles.expand(_flattenTileModel).toList()..add(tile.content));
+
+  Iterable<String> _allParametersInModel(TilerModel model) =>
+      _flattenTileModel(model.root)
+          .expand((ModuleInfo content) => content?.parameters ?? <String>[])
+          .toSet();
+
+  Map<K, V> _mapFromKeysAndCircularValues<K, V>(
+    Iterable<K> keys,
+    Iterable<V> values,
+  ) =>
+      Map.fromIterables(
+        keys,
+        List.generate(keys.length, (i) => values.elementAt(i % values.length)),
+      );
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: <Widget>[
+        Positioned.fill(
+          child: LayoutPresenter(
+            tilerModel: _tilerModel,
+            connections: _connections,
+            isEditing: _isEditing,
+            focusedMod: _focusedMod,
+            parametersToColors: _parametersToColors,
+            setTilerModel: (model) {
+              setState(() {
+                _tilerModel = cloneTiler(model);
+              });
+            },
+          ),
+        ),
+      ],
+    );
+  }
+
+  void _endEditing() {
+    widget.presenter.requestLayout(_tilerModel);
+  }
+
+  void _cancelEditing() {
+    setState(() {
+      _isEditing = false;
+      _resetTilerModel();
+    });
+    updateLayoutSuggestionsOverlayVisibility();
+  }
+
+  void _confirmEditListener() {
+    if (_isEditing) {
+      return;
+    }
+    if (widget.confirmEdit.value) {
+      _endEditing();
+    } else {
+      _cancelEditing();
+    }
+  }
+
+  void updateLayoutSuggestionsOverlayVisibility() {
+    if (_isEditing && _layoutSuggestionsOverlay == null) {
+      _layoutSuggestionsOverlay = OverlayEntry(
+        builder: (context) {
+          return Positioned(
+            left: 0,
+            right: 0,
+            bottom: 8,
+            child: Align(
+              alignment: Alignment.bottomCenter,
+              child: SizedBox(
+                height: 32,
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: <Widget>[
+                    LayoutSuggestionsWidget(
+                      presenter: widget.presenter,
+                      focusedMod: _focusedMod,
+                      onSelect: (model) {
+                        setState(() {
+                          _tilerModel = cloneTiler(model);
+                        });
+                      },
+                    ),
+                    RemoveButtonTargetWidget(
+                      onTap: () {
+                        getTileContent(_tilerModel)
+                            .where((TileModel tile) =>
+                                tile.content.modName == _focusedMod.value)
+                            .forEach(_tilerModel.remove);
+                      },
+                    ),
+                  ],
+                ),
+              ),
+            ),
+          );
+        },
+      );
+      Overlay.of(context).insert(_layoutSuggestionsOverlay);
+    }
+    if (!_isEditing && _layoutSuggestionsOverlay != null) {
+      _layoutSuggestionsOverlay.remove();
+      _layoutSuggestionsOverlay = null;
+    }
+  }
+}
diff --git a/shell/ermine/lib/src/widgets/tile_chrome.dart b/shell/ermine/lib/src/widgets/tile_chrome.dart
index 2a862ee..13c4c7a 100644
--- a/shell/ermine/lib/src/widgets/tile_chrome.dart
+++ b/shell/ermine/lib/src/widgets/tile_chrome.dart
@@ -8,9 +8,10 @@
 
 /// Defines a widget that builds the tile chrome for a story.
 class TileChrome extends StatelessWidget {
-  static const _kBorderSize = 16.0;
+  static const _kBorderSize = 20.0;
 
   final bool focused;
+  final bool editing;
   final bool showTitle;
   final bool fullscreen;
   final bool draggable;
@@ -22,10 +23,15 @@
   final VoidCallback onDelete;
   final VoidCallback onFullscreen;
   final VoidCallback onMinimize;
+  final VoidCallback onTap;
+  final VoidCallback onEdit;
+  final VoidCallback onCancelEdit;
+  final VoidCallback onConfirmEdit;
 
   const TileChrome({
     @required this.name,
     this.child,
+    this.editing = false,
     this.showTitle = true,
     this.fullscreen = false,
     this.focused = false,
@@ -36,44 +42,54 @@
     this.onDelete,
     this.onFullscreen,
     this.onMinimize,
+    this.onTap,
+    this.onEdit,
+    this.onCancelEdit,
+    this.onConfirmEdit,
   });
 
   @override
   Widget build(BuildContext context) {
-    Widget chrome = Stack(
-      children: [
-        // Border.
-        Positioned.fill(
-          child: Container(
-            decoration: showTitle && !fullscreen
-                ? BoxDecoration(
-                    border: Border.all(
-                      color: focused ? Colors.white : Colors.grey,
-                      width: _kBorderSize,
-                    ),
-                  )
-                : null,
-            child: child ?? Container(color: Colors.transparent),
-          ),
-        ),
-
-        // Title.
-        Positioned(
-          left: 0,
-          top: 0,
-          right: 0,
-          height: _kBorderSize,
-          child: showTitle
-              ? fullscreen // Display title bar on top of story.
-                  ? Material(
-                      elevation: elevations.systemOverlayElevation,
-                      color: focused ? Colors.white : Colors.grey,
-                      child: _buildTitlebar(context),
+    Widget chrome = GestureDetector(
+      behavior: HitTestBehavior.translucent,
+      // Disable listview scrolling on top of story.
+      onHorizontalDragStart: (_) {},
+      onTap: onTap,
+      child: Stack(
+        children: [
+          // Border.
+          Positioned.fill(
+            child: Container(
+              decoration: showTitle && !fullscreen
+                  ? BoxDecoration(
+                      border: Border.all(
+                        color: focused ? Colors.white : Colors.grey,
+                        width: _kBorderSize,
+                      ),
                     )
-                  : _buildTitlebar(context)
-              : Offstage(),
-        )
-      ],
+                  : null,
+              child: child ?? Container(color: Colors.transparent),
+            ),
+          ),
+
+          // Title.
+          Positioned(
+            left: 0,
+            top: 0,
+            right: 0,
+            height: _kBorderSize,
+            child: showTitle
+                ? fullscreen // Display title bar on top of story.
+                    ? Material(
+                        elevation: elevations.systemOverlayElevation,
+                        color: focused ? Colors.white : Colors.grey,
+                        child: _buildTitlebar(context),
+                      )
+                    : _buildTitlebar(context)
+                : Offstage(),
+          )
+        ],
+      ),
     );
     return draggable
         ? Draggable(
@@ -97,51 +113,84 @@
           Padding(
             padding: EdgeInsets.only(left: 8),
           ),
+
+          // Cancel edit button.
+          if (editing)
+            _buildTitleBarTextButton(context, 'Cancel', () {
+              onEdit?.call();
+              onCancelEdit?.call();
+            }),
+
+          // Story name.
           Expanded(
-            child: Text(
-              name ?? '<>',
-              textAlign: TextAlign.center,
-              style: Theme.of(context).textTheme.caption.copyWith(
-                    color: focused ? Colors.black : Colors.white,
-                  ),
+            child: _buildTitleBarTextButton(context, name ?? '<>', onEdit),
+          ),
+
+          // Minimize button.
+          if (!editing)
+            _buildIconButton(context, Icons.remove, onMinimize),
+
+          if (!editing)
+            Padding(
+              padding: EdgeInsets.only(left: 8),
             ),
-          ),
-          Padding(
-            padding: EdgeInsets.only(left: 8),
-          ),
-          GestureDetector(
-            child: Icon(
-              Icons.remove,
-              size: _kBorderSize,
-              color: focused ? Colors.black : Colors.white,
+
+          // Maximize button.
+          if (!editing)
+            _buildIconButton(context, Icons.add, onFullscreen),
+
+          if (!editing)
+            Padding(
+              padding: EdgeInsets.only(left: 8),
             ),
-            onTap: onMinimize?.call,
-          ),
-          Padding(
-            padding: EdgeInsets.only(left: 8),
-          ),
-          GestureDetector(
-            child: Icon(
-              Icons.add,
-              size: _kBorderSize,
-              color: focused ? Colors.black : Colors.white,
-            ),
-            onTap: onFullscreen?.call,
-          ),
-          Padding(
-            padding: EdgeInsets.only(left: 8),
-          ),
-          GestureDetector(
-            child: Icon(
-              Icons.clear,
-              size: _kBorderSize,
-              color: focused ? Colors.black : Colors.white,
-            ),
-            onTap: onDelete?.call,
-          ),
+
+          // Close button.
+          if (!editing)
+            _buildIconButton(context, Icons.clear, onDelete),
+
+          // Done edit button.
+          if (editing)
+            _buildTitleBarTextButton(context, 'Done', () {
+              onEdit?.call();
+              onConfirmEdit?.call();
+            }),
+
           Padding(
             padding: EdgeInsets.only(left: 8),
           ),
         ],
       );
+
+  Widget _buildTitleBarTextButton(
+    BuildContext context,
+    String title,
+    VoidCallback onTap,
+  ) =>
+      GestureDetector(
+        onTap: onTap,
+        child: Center(
+          child: Text(
+            title,
+            textAlign: TextAlign.center,
+            style: Theme.of(context)
+                .textTheme
+                .caption
+                .copyWith(color: focused ? Colors.black : Colors.white),
+          ),
+        ),
+      );
+
+  Widget _buildIconButton(
+    BuildContext context,
+    IconData icon,
+    VoidCallback onTap,
+  ) =>
+      GestureDetector(
+        child: Icon(
+          icon,
+          size: _kBorderSize,
+          color: focused ? Colors.black : Colors.white,
+        ),
+        onTap: onTap?.call,
+      );
 }