[story_shell_labs] Add deja layout and tiler presenter

TESTED=on device,deja_layout_test

Change-Id: Ie921b17f83b1992628f2721ed4f59e15f46cd772
diff --git a/packages/prod/BUILD.gn b/packages/prod/BUILD.gn
index 9822580..31b04cd 100644
--- a/packages/prod/BUILD.gn
+++ b/packages/prod/BUILD.gn
@@ -34,10 +34,10 @@
 group("flutter") {
   testonly = true
   public_deps = [
-    "//topaz/packages/prod:flutter_aot_product",
     "//topaz/packages/prod:flutter_aot",
-    "//topaz/packages/prod:flutter_jit_product",
+    "//topaz/packages/prod:flutter_aot_product",
     "//topaz/packages/prod:flutter_jit",
+    "//topaz/packages/prod:flutter_jit_product",
   ]
 }
 
@@ -55,7 +55,6 @@
     "//topaz/packages/prod:dart_jit_runner",
     "//topaz/packages/prod:dart_runner",
     "//topaz/packages/prod:datetime_settings",
-    "//topaz/packages/prod:story_shell_labs",
     "//topaz/packages/prod:device_settings",
     "//topaz/packages/prod:display_settings",
     "//topaz/packages/prod:ermine",
@@ -68,6 +67,7 @@
     "//topaz/packages/prod:mondrian",
     "//topaz/packages/prod:settings",
     "//topaz/packages/prod:skottie_viewer",
+    "//topaz/packages/prod:story_shell_labs",
     "//topaz/packages/prod:system_dashboard",
     "//topaz/packages/prod:term",
     "//topaz/packages/prod:userpicker_base_shell",
@@ -152,8 +152,8 @@
 group("userpicker_base_shell") {
   testonly = true
   public_deps = [
-    "//topaz/packages/prod:flutter",
     "//topaz/bin/userpicker_base_shell",
+    "//topaz/packages/prod:flutter",
   ]
 }
 
@@ -167,10 +167,10 @@
 group("dart_runner") {
   testonly = true
   public_deps = [
-    "//topaz/packages/prod:dart_aot_runner",
     "//topaz/packages/prod:dart_aot_product_runner",
-    "//topaz/packages/prod:dart_jit_runner",
+    "//topaz/packages/prod:dart_aot_runner",
     "//topaz/packages/prod:dart_jit_product_runner",
+    "//topaz/packages/prod:dart_jit_runner",
   ]
 }
 
@@ -192,10 +192,10 @@
 group("term") {
   testonly = true
   public_deps = [
-    "//garnet/packages/prod:fonts",
     "//garnet/bin/fonts:font_provider_tests",
-    "//topaz/app/term",
+    "//garnet/packages/prod:fonts",
     "//third_party/vulkan_loader_and_validation_layers/loader:vulkan_loader",
+    "//topaz/app/term",
   ]
 }
 
@@ -246,12 +246,12 @@
 group("xi") {
   testonly = true
   public_deps = [
-    "//topaz/bin/xi/xi_mod",
-    "//topaz/bin/xi/xi_embeddable",
-    "//topaz/bin/xi/xi_session_agent",
-    "//topaz/bin/xi/xi_session_demo",
     "//garnet/bin/xi_core",
     "//topaz/bin/xi",
+    "//topaz/bin/xi/xi_embeddable",
+    "//topaz/bin/xi/xi_mod",
+    "//topaz/bin/xi/xi_session_agent",
+    "//topaz/bin/xi/xi_session_demo",
   ]
 }
 
@@ -259,12 +259,13 @@
   testonly = true
   public_deps = [
     "//topaz/packages/prod:chromium",
-    "//topaz/packages/prod:dart_jit_runner",
     "//topaz/packages/prod:dart_jit_product_runner",
+    "//topaz/packages/prod:dart_jit_runner",
     "//topaz/packages/prod:datetime_settings",
     "//topaz/packages/prod:display_settings",
     "//topaz/packages/prod:mondrian",
     "//topaz/packages/prod:settings",
+    "//topaz/packages/prod:story_shell_labs",
     "//topaz/packages/prod:userpicker_base_shell",
     "//topaz/packages/prod:wifi_settings",
     "//topaz/shell/ermine:ermine",
diff --git a/packages/tests/BUILD.gn b/packages/tests/BUILD.gn
index f942071..bf0d49a 100644
--- a/packages/tests/BUILD.gn
+++ b/packages/tests/BUILD.gn
@@ -28,6 +28,7 @@
     "//topaz/public/dart/fuchsia_scenic_flutter:fuchsia_scenic_flutter_unittests($host_toolchain)",
     "//topaz/public/dart/fuchsia_services:fuchsia_services_package_unittests($host_toolchain)",
     "//topaz/public/dart/sledge:dart_sledge_tests($host_toolchain)",
+    "//topaz/public/dart/story_shell_labs:layout_unittests($host_toolchain)",
     "//topaz/public/dart/widgets:dart_widget_tests($host_toolchain)",
     "//topaz/public/lib/app/dart:dart_app_tests($host_toolchain)",
     "//topaz/public/lib/display/flutter:display_test($host_toolchain)",
diff --git a/public/dart/story_shell_labs/.gitignore b/public/dart/story_shell_labs/.gitignore
new file mode 100644
index 0000000..49ce72d
--- /dev/null
+++ b/public/dart/story_shell_labs/.gitignore
@@ -0,0 +1,3 @@
+.dart_tool/
+.packages
+pubspec.lock
diff --git a/public/dart/story_shell_labs/BUILD.gn b/public/dart/story_shell_labs/BUILD.gn
new file mode 100644
index 0000000..b6c1144
--- /dev/null
+++ b/public/dart/story_shell_labs/BUILD.gn
@@ -0,0 +1,52 @@
+# 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("//build/dart/dart_library.gni")
+import("//topaz/runtime/dart/flutter_test.gni")
+
+dart_library("story_shell_labs_lib") {
+  package_name = "story_shell_labs_lib"
+
+  sources = [
+    "layout/deja_layout.dart",
+    "layout/tile_model.dart",
+    "layout/tile_presenter.dart",
+    "src/layout/deja_layout/deja_layout.dart",
+    "src/layout/deja_layout/layout_policy.dart",
+    "src/layout/deja_layout/layout_store.dart",
+    "src/layout/deja_layout/layout_utils.dart",
+    "src/layout/layout.dart",
+    "src/layout/presenter.dart",
+    "src/layout/tile_model/module_info.dart",
+    "src/layout/tile_model/module_info.g.dart",
+    "src/layout/tile_model/tile_layout_model.dart",
+    "src/layout/tile_model/tile_model_serializer.dart",
+    "src/layout/tile_presenter/layout_suggestions_update.dart",
+    "src/layout/tile_presenter/tile_presenter.dart",
+    "src/layout/tile_presenter/tile_presenter_suggestions_widget.dart",
+    "src/layout/tile_presenter/tile_presenter_widget.dart",
+    "src/layout/tile_presenter/widgets/drop_target_widget.dart",
+    "src/layout/tile_presenter/widgets/editing_tile_chrome.dart",
+  ]
+
+  deps = [
+    "//third_party/dart-pkg/git/flutter/packages/flutter",
+    "//third_party/dart-pkg/pub/built_collection",
+    "//third_party/dart-pkg/pub/json_annotation",
+    "//topaz/lib/tiler:tiler",
+    "//topaz/public/dart/fuchsia_scenic_flutter",
+  ]
+}
+
+flutter_test("layout_unittests") {
+  sources = [
+    "deja_layout_test.dart",
+  ]
+
+  deps = [
+    ":story_shell_labs_lib",
+    "//third_party/dart-pkg/pub/mockito",
+    "//third_party/dart-pkg/pub/test",
+  ]
+}
diff --git a/public/dart/story_shell_labs/OWNERS b/public/dart/story_shell_labs/OWNERS
new file mode 100644
index 0000000..c2d31a5
--- /dev/null
+++ b/public/dart/story_shell_labs/OWNERS
@@ -0,0 +1,3 @@
+ahetzroni@google.com
+miguelfrde@google.com
+schilit@google.com
diff --git a/public/dart/story_shell_labs/analysis_options.yaml b/public/dart/story_shell_labs/analysis_options.yaml
new file mode 100644
index 0000000..73da07f
--- /dev/null
+++ b/public/dart/story_shell_labs/analysis_options.yaml
@@ -0,0 +1,5 @@
+# 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.
+
+include: ../analysis_options.yaml
diff --git a/public/dart/story_shell_labs/lib/layout/deja_layout.dart b/public/dart/story_shell_labs/lib/layout/deja_layout.dart
new file mode 100644
index 0000000..cb423c7
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/layout/deja_layout.dart
@@ -0,0 +1,7 @@
+// 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.
+
+export '../src/layout/deja_layout/deja_layout.dart';
+export '../src/layout/deja_layout/layout_policy.dart';
+export '../src/layout/deja_layout/layout_store.dart';
diff --git a/public/dart/story_shell_labs/lib/layout/tile_model.dart b/public/dart/story_shell_labs/lib/layout/tile_model.dart
new file mode 100644
index 0000000..865c552
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/layout/tile_model.dart
@@ -0,0 +1,7 @@
+// 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.
+
+export '../src/layout/tile_model/module_info.dart';
+export '../src/layout/tile_model/tile_layout_model.dart';
+export '../src/layout/tile_model/tile_model_serializer.dart';
diff --git a/public/dart/story_shell_labs/lib/layout/tile_presenter.dart b/public/dart/story_shell_labs/lib/layout/tile_presenter.dart
new file mode 100644
index 0000000..eb10654
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/layout/tile_presenter.dart
@@ -0,0 +1,7 @@
+// 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.
+
+export '../src/layout/tile_presenter/tile_presenter.dart';
+export '../src/layout/tile_presenter/tile_presenter_suggestions_widget.dart';
+export '../src/layout/tile_presenter/tile_presenter_widget.dart';
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
new file mode 100644
index 0000000..34c1aa6
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/deja_layout/deja_layout.dart
@@ -0,0 +1,112 @@
+// 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:collection';
+
+import 'package:built_collection/built_collection.dart';
+import 'package:fuchsia_scenic_flutter/child_view_connection.dart';
+import 'package:tiler/tiler.dart';
+
+import '../layout.dart';
+import '../tile_model/module_info.dart';
+import '../tile_model/tile_layout_model.dart';
+import '../tile_presenter/tile_presenter.dart';
+import 'layout_policy.dart';
+import 'layout_store.dart';
+import 'layout_utils.dart';
+
+// The user has requested a different layout.
+typedef UserLayoutRequestCallback = void Function(TilerModel<ModuleInfo>);
+
+/// The layout strategy manages a model of the layout that is shared with the
+/// Presenter through the TileLayoutModel.
+class DejaLayout extends Layout<TileLayoutModel> {
+  var _tilerModel = TilerModel<ModuleInfo>();
+  List<TilerModel<ModuleInfo>> _tilerModelSuggestions = [];
+  final _connections = <String, ChildViewConnection>{};
+  final _layoutPolicy = LayoutPolicy(layoutStore: LayoutStore());
+
+  /// Tiling layout presenter
+  TilePresenter presenter;
+
+  /// Construct a Layout Manage that uses the Deja layout algorithm.
+  /// Deja stores past layouts and matches them for the
+  /// new layout when views are added or removed.
+  DejaLayout({
+    RemoveSurfaceCallback removeSurface,
+    FocusChangeCallback changeFocus,
+  }) : super(
+          removeSurface: removeSurface,
+          changeFocus: changeFocus,
+        ) {
+    presenter = TilePresenter(
+        removeSurfaceCallback: removeSurface,
+        changeFocusCallback: changeFocus,
+        requestLayoutCallback: _userLayoutRequest);
+  }
+
+  /// Called by Modular
+  @override
+  void addSurface({
+    String surfaceId,
+    String intent,
+    ChildViewConnection view,
+    UnmodifiableListView<String> parameters,
+  }) {
+    final content = ModuleInfo(
+      modName: surfaceId,
+      intent: intent,
+      parameters: parameters,
+    );
+
+    // Find the largest tile in the _tilerModel and have our content split
+    // and insert itself there.
+    splitLargestContent(_tilerModel, content);
+    final tilerModels = _layoutPolicy.getLayout(_tilerModel);
+    _tilerModel = tilerModels.first;
+    _tilerModelSuggestions = tilerModels.take(4).toList();
+    _connections[surfaceId] = view;
+    _onLayoutChange();
+    presenter.onSuggestionChange(_tilerModelSuggestions);
+  }
+
+  /// Called by Modular
+  @override
+  void deleteSurface(String surfaceId) {
+    final tile = findContent(_tilerModel, surfaceId);
+    if (tile != null) {
+      _tilerModel.remove(tile);
+    }
+    _connections.remove(surfaceId);
+    // Regenerate Layout Suggestions.
+    final tilerModels = _layoutPolicy.getLayout(_tilerModel);
+    _tilerModel = tilerModels.first;
+    _tilerModelSuggestions = tilerModels.take(4).toList();
+    _onLayoutChange();
+    presenter.onSuggestionChange(_tilerModelSuggestions);
+  }
+
+  /// A utility to send the layout to the presenter
+  void _onLayoutChange() {
+    presenter.onLayoutChange(TileLayoutModel(
+        model: _tilerModel, connections: BuiltMap(_connections)));
+  }
+
+  /// Presenter is asking that this new layout be made active. This follows some
+  /// user editing or re-arranging.
+  void _userLayoutRequest(TilerModel<ModuleInfo> model) {
+    final modsToRemove = getModsDifference(_tilerModel, model);
+    _tilerModel = model;
+    removeSurface(modsToRemove);
+    if (modsToRemove.isNotEmpty) {
+      // Regenerate Layout Suggestions.
+      final tilerModels = _layoutPolicy.getLayout(_tilerModel);
+      _tilerModel = tilerModels.first;
+      _tilerModelSuggestions = tilerModels.take(4).toList();
+      presenter.onSuggestionChange(_tilerModelSuggestions);
+    }
+    _onLayoutChange();
+    _layoutPolicy.write(_tilerModel);
+  }
+}
diff --git a/public/dart/story_shell_labs/lib/src/layout/deja_layout/layout_policy.dart b/public/dart/story_shell_labs/lib/src/layout/deja_layout/layout_policy.dart
new file mode 100644
index 0000000..ee79dfa
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/deja_layout/layout_policy.dart
@@ -0,0 +1,110 @@
+// 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:collection';
+import 'dart:io';
+import 'dart:math';
+
+import 'package:tiler/tiler.dart';
+
+import '../tile_model/module_info.dart';
+import 'layout_store.dart';
+import 'layout_utils.dart';
+
+const _kGroups = 4;
+
+/// Layout Policy for adding new content to a [TilerModel].
+///
+/// The strategy is to first find prior [TilerModel]s that have
+/// the same intents.  Then group these candidates by equivalent layout
+/// geometry (ignoring flex). Proceed by taking the largest group and picking
+/// the most popular dimensional layout (looking at flex) within that group.
+///
+/// Second and third choice layouts are found in the smaller equivalent
+/// geometry groups.
+///
+/// If there are no prior examples with similar intents then the policy
+/// uses "split largest."
+class LayoutPolicy {
+  /// Storage for layout history data.
+  final LayoutStore layoutStore;
+
+  /// A tiling layout policy.
+  LayoutPolicy({this.layoutStore});
+
+  /// Returns new layouts suggestions for either add mod or remove mod.
+  List<TilerModel<ModuleInfo>> getLayout(TilerModel<ModuleInfo> a) {
+    var candidates = layoutStore.listSync();
+
+    // Collect the trees with same intents
+    candidates = _intentReduce(a, candidates);
+    print('Candidates intent count ${candidates.length}');
+    if (candidates.isEmpty) {
+      return [a];
+    }
+    // Group into lists of trees with the equivalent geometry
+    final gGroups =
+        _hashGroup(layoutFiles: candidates, includeFlex: false, n: _kGroups);
+    print('Geometry group ${gGroups.length}');
+    if (gGroups.isEmpty) {
+      return [a];
+    }
+    // For the top N geometry groups, find a single "popular" tree to return
+    final result = <TilerModel<ModuleInfo>>[];
+    for (int i = 0; i < min(gGroups.length, _kGroups); i++) {
+      // Group the trees having equivalent geometry and flex (trees are the same)
+      final fGroups =
+          _hashGroup(layoutFiles: gGroups[i], includeFlex: true, n: 1);
+      print('Flex group count ${fGroups.length}');
+      // THe result is the most popular layout with flex
+      result.add(layoutStore.read(fGroups.first.first));
+    }
+    // A list of trees, each with a unique geometry.
+    // Update the modName references and return.
+    for (final tileModel in result) {
+      updateModNames(a, tileModel);
+    }
+    return result;
+  }
+
+  /// Write a model to the storage.
+  void write(TilerModel a) {
+    layoutStore.write(a);
+  }
+
+  /// Reduce the candidate layouts to ones where the intents match.
+  List<File> _intentReduce(TilerModel<ModuleInfo> a, List<File> candidates) {
+    return candidates
+        .where((file) => compareIntents(a, layoutStore.read(file)))
+        .toList();
+  }
+
+  /// Return N largest hash equivalent groups among the layouts files.
+  ///
+  /// The input is a list of layout files, and the output is a
+  /// list of hash equivalent groups in length order.
+  ///
+  /// For example, can return the layout [File]s for the N most popular
+  /// geometrically equivalent layouts.
+  List<List<File>> _hashGroup(
+      {List<File> layoutFiles, bool includeFlex, int n}) {
+    if (layoutFiles.length <= 1) {
+      return [layoutFiles];
+    }
+    final map = HashMap<int, List<File>>();
+    // Accumulate the files for each group
+    for (final file in layoutFiles) {
+      int hashCode = treeHashCode(
+          model: layoutStore.read(file), includeFlex: includeFlex);
+      // Self initializing hash table idiom
+      final value = map[hashCode];
+      map[hashCode] = (value == null) ? [file] : value + [file];
+    }
+
+    // Sort by descending occurence count.
+    return map.values.toList()
+      ..sort((b, a) => a.length.compareTo(b.length))
+      ..take(n);
+  }
+}
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
new file mode 100644
index 0000000..26e5a21
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/deja_layout/layout_store.dart
@@ -0,0 +1,84 @@
+// 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:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as path;
+import 'package:tiler/tiler.dart';
+
+import '../tile_model/module_info.dart';
+import '../tile_model/tile_model_serializer.dart' show fromJson, toJson;
+
+/// Persistent storage for layouts
+class LayoutStore {
+  /// Directory path where the data will be persisted.
+  final String directory;
+
+  /// Maximum amount of files to store.
+  final int size;
+
+  /// Constructor for layout storage.
+  LayoutStore({this.directory = '/data/layouts', this.size = 100});
+
+  /// Clears the storage.
+  ///
+  /// Synchronously deletes all the [File]s in the layout storage.
+  void deleteSync() {
+    try {
+      print('LayoutStore deleting $directory');
+      File(directory).deleteSync(recursive: true);
+    } on FileSystemException catch (e) {
+      print('LayoutStore Failed to delete $directory: $e');
+    }
+  }
+
+  /// Get a list of file names in the storage containg [TilerModel]s.
+  ///
+  /// Returns a List containing [File] objects.
+  List<File> listSync() {
+    try {
+      final result = Directory(directory)
+          .listSync()
+          .whereType<File>()
+          .cast<File>()
+          .toList();
+      print('listSync: ${result.length} ${result.map((f) => f.path)}');
+      return result;
+    } on FileSystemException catch (_) {
+      // No such file or directory
+      return [];
+    }
+  }
+
+  /// Write the [TilerModel] to persistent storage.
+  void write(TilerModel<ModuleInfo> a) {
+    if (a.root != null) {
+      String now = DateTime.now().toIso8601String();
+      File(path.join(directory, now))
+        ..createSync(recursive: true)
+        ..writeAsString(json.encode(toJson(a)));
+      _prune();
+    }
+  }
+
+  /// Read the [TilerModel] from persistent storage.
+  TilerModel<ModuleInfo> read(File f) {
+    String s = f.readAsStringSync();
+    return fromJson(jsonDecode(s));
+  }
+
+  // Prune old files from the storage.
+  void _prune() {
+    int _compare(File a, File b) =>
+        a.lastModifiedSync().compareTo(b.lastModifiedSync());
+
+    final files = listSync();
+    print('Pruning file(s): $files');
+    files.sort(_compare);
+    for (final file in files.skip(size)) {
+      file.deleteSync();
+    }
+  }
+}
diff --git a/public/dart/story_shell_labs/lib/src/layout/deja_layout/layout_utils.dart b/public/dart/story_shell_labs/lib/src/layout/deja_layout/layout_utils.dart
new file mode 100644
index 0000000..cabe5ba
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/deja_layout/layout_utils.dart
@@ -0,0 +1,234 @@
+// 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:collection';
+
+import 'package:tiler/tiler.dart';
+import '../tile_model/module_info.dart';
+
+/// Utility for storing the size of a model.
+class TileModelSize {
+  /// The tiling model.
+  final TileModel tile;
+
+  /// Height ratio.
+  final double heightFlex;
+
+  /// Width ratio.
+  final double widthFlex;
+
+  /// Area of the til.
+  double get area => heightFlex * widthFlex;
+
+  /// Construtor
+  TileModelSize(this.tile, this.heightFlex, this.widthFlex);
+
+  @override
+  String toString() => '$tile, $widthFlex, $heightFlex';
+}
+
+/// Find the [TileMode] that is holding the content or null if none.
+TileModel findContent(TilerModel<ModuleInfo> a, String modName) {
+  TileModel _recurse(TileModel<ModuleInfo> t) {
+    if (t.type == TileType.content && t.content.modName == modName) {
+      return t;
+    } else {
+      for (var child in t.tiles) {
+        final t = _recurse(child);
+        if (t != null) {
+          return t;
+        }
+      }
+    }
+    return null;
+  }
+
+  return a.root != null ? _recurse(a.root) : null;
+}
+
+/// Find the largest tile by area in the tree
+TileModel findLargestContent(TilerModel a) {
+  TileModelSize _recurse(TileModelSize tms) {
+    final tile = tms.tile;
+    if (tile.type == TileType.content) {
+      return tms;
+    }
+    double maxArea = 0.0;
+    TileModelSize maxTms;
+    // flex values for children need to be normalized by flex of all children.
+    final sumFlex =
+        tile.tiles.fold<double>(0.0, (total, child) => total + child.flex);
+    for (final child in tile.tiles) {
+      TileModelSize childTms;
+      if (tile.type == TileType.row) {
+        childTms = TileModelSize(
+            child, tms.heightFlex, tms.widthFlex * child.flex / sumFlex);
+      } else if (tile.type == TileType.column) {
+        childTms = TileModelSize(
+            child, tms.heightFlex * child.flex / sumFlex, tms.widthFlex);
+      }
+      final maxChildTms = _recurse(childTms);
+      if (maxArea < maxChildTms.area) {
+        maxTms = maxChildTms;
+        maxArea = maxChildTms.area;
+      }
+    }
+    return maxTms;
+  }
+
+  if (a.root.isEmpty) {
+    return null;
+  }
+
+  final ts = _recurse(TileModelSize(a.root, 1.0, 1.0));
+  return ts?.tile;
+}
+
+/// Returns a new tile after splitting the largest [tile] in the tree.
+void splitLargestContent(TilerModel<ModuleInfo> a, ModuleInfo content) {
+  if (a.root.isEmpty) {
+    a.add(content: content);
+  } else {
+    final tile = findLargestContent(a);
+    print('splitLargestContent - $tile - $content');
+    a.split(tile: findLargestContent(a), content: content);
+  }
+  print('tiles are ${a.root}');
+}
+
+/// Returns a hashCode for the [TilerModel] with or without flex values
+///
+/// Note that dart does not include a hash combiner function so hashing of
+/// objects is achieved by converting to a [String] which
+/// has a content (not address) based [hashCode].
+int treeHashCode({TilerModel<ModuleInfo> model, bool includeFlex}) {
+  void _recurse(TileModel<ModuleInfo> tile, StringBuffer sb) {
+    // Hash the type
+    sb.write('t:${tile.type.index}');
+    if (includeFlex) {
+      sb.write('f:${tile.flex}');
+    }
+    if (tile.type == TileType.content) {
+      // do not hash the module name
+      sb.write('c:${tile.content.intent}');
+    } else {
+      sb.write('[');
+      for (final t in tile.tiles) {
+        _recurse(t, sb);
+      }
+      sb.write(']');
+    }
+  }
+
+  final strBuffer = StringBuffer();
+  _recurse(model.root, strBuffer);
+  // String uses content for the hash
+  return strBuffer.toString().hashCode;
+}
+
+/// Determine if two trees have the same geometry, ignoring flex.
+bool compareGeometry(TileModel<ModuleInfo> a, TileModel<ModuleInfo> b) {
+  if (a.type != b.type) {
+    return false;
+  } else if (a.type == TileType.content) {
+    return true;
+  } else {
+    if (a.tiles.length != b.tiles.length) {
+      return false;
+    }
+    for (int i = 0; i < a.tiles.length; i++) {
+      if (!compareGeometry(a.tiles[i], b.tiles[i])) {
+        return false;
+      }
+    }
+  }
+  return true;
+}
+
+/// Determine if two trees have the same geometry and flex.
+bool compareFlex(TilerModel<ModuleInfo> a, TilerModel<ModuleInfo> b) {
+  bool _recurse(TileModel<ModuleInfo> a, TileModel<ModuleInfo> b) {
+    if (a.type != b.type) {
+      return false;
+    }
+    if (a.type == TileType.content) {
+      return true; // possibly a.flex == b.flex
+    }
+    if (a.flex != b.flex || a.tiles.length != b.tiles.length) {
+      return false;
+    }
+    for (int i = 0; i < a.tiles.length; i++) {
+      if (!_recurse(a.tiles[i], b.tiles[i])) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  if (!compareGeometry(a.root, b.root)) {
+    return false;
+  }
+  return _recurse(a.root, b.root);
+}
+
+/// Determine if two trees have the same intents.
+bool compareIntents(TilerModel<ModuleInfo> a, TilerModel<ModuleInfo> b) {
+  final aIntents = _getIntents(a);
+  final bIntents = _getIntents(b);
+  if (aIntents.length != bIntents.length) {
+    return false;
+  }
+  aIntents.forEach(bIntents.remove);
+  return bIntents.isEmpty;
+}
+
+/// Gets the mods that have been removed from the old TileModel.
+/// In editing mode, mods cannot be added.
+Set<String> getModsDifference(
+        TilerModel<ModuleInfo> oldTree, TilerModel<ModuleInfo> newTree) =>
+    _getMods(oldTree).difference(_getMods(newTree));
+
+/// Update modNames [from] [to], using intent as key and updating modName
+void updateModNames(TilerModel<ModuleInfo> from, TilerModel<ModuleInfo> to) {
+  final toTiles = getTileContent(to);
+  final fromTiles = getTileContent(from);
+  for (final toTile in toTiles) {
+    final toIntent = toTile.content.intent;
+    final fromTile = fromTiles.firstWhere((t) => t.content.intent == toIntent);
+    fromTiles.remove(fromTile);
+    final modName = fromTile.content.modName;
+    final parameters = fromTile.content.parameters;
+    toTile.content = ModuleInfo(
+      intent: toIntent,
+      modName: modName,
+      parameters: parameters,
+    );
+  }
+}
+
+/// A co-routine to iterate over the TilerModel tree.
+Iterable<TileModel> tilerWalker(TilerModel a) sync* {
+  final nodes = ListQueue<TileModel>()..add(a.root);
+  while (nodes.isNotEmpty) {
+    nodes.addAll(nodes.first.tiles);
+    yield nodes.removeFirst();
+  }
+}
+
+/// Get the content nodes of a layout tree.
+List<TileModel<ModuleInfo>> getTileContent(TilerModel<ModuleInfo> a) =>
+    tilerWalker(a)
+        .where((t) => t.type == TileType.content)
+        .fold(<TileModel<ModuleInfo>>[], (l, t) => l..add(t));
+
+// Get the modNames for the mods in the layout tree aka TileModel.
+Set<String> _getMods(TilerModel<ModuleInfo> a) => tilerWalker(a)
+    .where((t) => t.type == TileType.content)
+    .fold(<String>{}, (s, t) => s..add(t.content.modName));
+
+// Get the intents in a layout tree as an ordered list.
+List<String> _getIntents(TilerModel<ModuleInfo> a) => tilerWalker(a)
+    .where((t) => t.type == TileType.content)
+    .fold(<String>[], (l, t) => l..add(t.content.intent))
+      ..sort();
diff --git a/public/dart/story_shell_labs/lib/src/layout/layout.dart b/public/dart/story_shell_labs/lib/src/layout/layout.dart
new file mode 100644
index 0000000..cb959bc
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/layout.dart
@@ -0,0 +1,42 @@
+// 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:collection';
+
+import 'package:fuchsia_scenic_flutter/child_view_connection.dart';
+
+/// Allow presenters to request removal of surfaces.
+typedef RemoveSurfaceCallback = void Function(Iterable<String>);
+
+/// Allow presenters to notify changes on focused surfaces to modular for purposes
+/// of ranking in context and suggestions.
+typedef FocusChangeCallback = void Function(String, bool);
+
+/// The layout strategy manages a model of the layout that is shared with the
+/// Presenter through the LayoutModel.
+abstract class Layout<T> {
+  /// Called when a surface is removed
+  RemoveSurfaceCallback removeSurface;
+
+  /// Called when the focus of a surface changes.
+  FocusChangeCallback changeFocus;
+
+  /// Constructor for a layout strategy.
+  Layout({
+    this.removeSurface,
+    this.changeFocus,
+  });
+
+  /// These fields depend on the host environment. If this is used
+  /// outside of Fuchsia, change ChildViewConnection to flutter Widget.
+  void addSurface({
+    String surfaceId,
+    String intent,
+    ChildViewConnection view,
+    UnmodifiableListView<String> parameters,
+  });
+
+  /// Instructs to delete a surface.
+  void deleteSurface(String surfaceId);
+}
diff --git a/public/dart/story_shell_labs/lib/src/layout/presenter.dart b/public/dart/story_shell_labs/lib/src/layout/presenter.dart
new file mode 100644
index 0000000..031b2f5
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/presenter.dart
@@ -0,0 +1,32 @@
+// Depends on the implementation of the Layout and the Presenter.
+// Owned by the Presenter because multiple layouts can use the same
+// Presenter.
+
+import 'layout.dart';
+
+/// Renders the layout. Can be used by multiple strategies that use the same
+/// PresentationModel.
+abstract class Presenter<T> {
+  /// Called when a surface is removed.
+  RemoveSurfaceCallback removeSurfaceCallback;
+
+  /// Called when the focus changes.
+  FocusChangeCallback changeFocusCallback;
+
+  /// Constructor for a presenter.
+  Presenter({
+    this.removeSurfaceCallback,
+    this.changeFocusCallback,
+  });
+
+  /// Notify the presenter of a layout change.
+  void onLayoutChange(T layoutModer);
+
+  /// Instructs to remove a surface.
+  void removeSurface(Iterable<String> surfaces) =>
+      removeSurfaceCallback(surfaces);
+
+  /// Instructs to change the focus of a surface.
+  void changeFocus(String surface, {bool focus = false}) =>
+      changeFocusCallback(surface, focus);
+}
diff --git a/public/dart/story_shell_labs/lib/src/layout/tile_model/module_info.dart b/public/dart/story_shell_labs/lib/src/layout/tile_model/module_info.dart
new file mode 100644
index 0000000..7f52e1d
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/tile_model/module_info.dart
@@ -0,0 +1,51 @@
+// 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:collection';
+
+import 'package:json_annotation/json_annotation.dart';
+import 'package:meta/meta.dart';
+
+part 'module_info.g.dart';
+
+/// A class that stores information about a module.
+@JsonSerializable()
+class ModuleInfo {
+  /// The surface id.
+  final String modName;
+
+  /// The intent that triggered the creation of this mod.
+  final String intent;
+
+  /// List of parameter ids input to this mod.
+  @JsonKey(
+      fromJson: _unmodifiableListViewFromJson,
+      toJson: _unmodifiableListViewToJson)
+  final UnmodifiableListView<String> parameters;
+
+  /// Constructor for the module info information object.
+  ModuleInfo({
+    @required this.modName,
+    @required this.intent,
+    @required this.parameters,
+  });
+
+  static UnmodifiableListView<String> _unmodifiableListViewFromJson(
+          List<dynamic> parameters) =>
+      UnmodifiableListView<String>(parameters.cast<String>());
+
+  static List<String> _unmodifiableListViewToJson(
+          UnmodifiableListView<String> parameters) =>
+      parameters.toList();
+
+  @override
+  String toString() => 'modName: $modName, intent: $intent';
+
+  /// Load this model from a json object.
+  factory ModuleInfo.fromJson(Map<String, dynamic> json) =>
+      _$ModuleInfoFromJson(json);
+
+  /// Serialize this model as json
+  Map<String, dynamic> toJson() => _$ModuleInfoToJson(this);
+}
diff --git a/public/dart/story_shell_labs/lib/src/layout/tile_model/module_info.g.dart b/public/dart/story_shell_labs/lib/src/layout/tile_model/module_info.g.dart
new file mode 100644
index 0000000..83a8bae
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/tile_model/module_info.g.dart
@@ -0,0 +1,27 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: avoid_as
+
+part of 'module_info.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+ModuleInfo _$ModuleInfoFromJson(Map<String, dynamic> json) {
+  return ModuleInfo(
+      modName: json['modName'] as String,
+      intent: json['intent'] as String,
+      parameters: json['parameters'] == null
+          ? null
+          : ModuleInfo._unmodifiableListViewFromJson(
+              json['parameters'] as List));
+}
+
+Map<String, dynamic> _$ModuleInfoToJson(ModuleInfo instance) =>
+    <String, dynamic>{
+      'modName': instance.modName,
+      'intent': instance.intent,
+      'parameters': instance.parameters == null
+          ? null
+          : ModuleInfo._unmodifiableListViewToJson(instance.parameters)
+    };
diff --git a/public/dart/story_shell_labs/lib/src/layout/tile_model/tile_layout_model.dart b/public/dart/story_shell_labs/lib/src/layout/tile_model/tile_layout_model.dart
new file mode 100644
index 0000000..9a84b0f
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/tile_model/tile_layout_model.dart
@@ -0,0 +1,24 @@
+// 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 'package:fuchsia_scenic_flutter/child_view_connection.dart'
+    show ChildViewConnection;
+import 'package:built_collection/built_collection.dart';
+import 'package:meta/meta.dart';
+import 'package:tiler/tiler.dart';
+import 'module_info.dart';
+
+/// Depends on the implementation of the Layout and the Presenter.
+/// Declared by the Presenter because multiple layouts can use the same
+/// Presenter.
+class TileLayoutModel {
+  /// The tiling layout model.
+  final TilerModel<ModuleInfo> model;
+
+  /// Maps a surface id to its view.
+  final BuiltMap<String, ChildViewConnection> connections;
+
+  /// Constructor for a tiling layout model.
+  TileLayoutModel({@required this.model, @required this.connections});
+}
diff --git a/public/dart/story_shell_labs/lib/src/layout/tile_model/tile_model_serializer.dart b/public/dart/story_shell_labs/lib/src/layout/tile_model/tile_model_serializer.dart
new file mode 100644
index 0000000..e59a811
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/tile_model/tile_model_serializer.dart
@@ -0,0 +1,62 @@
+// 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 'package:tiler/tiler.dart';
+
+import 'module_info.dart';
+
+/// Convert the tiler model to json.
+Map<String, dynamic> toJson(TilerModel<ModuleInfo> model) => {
+      'root': model.root == null ? null : _tileToJson(model.root),
+    };
+
+/// Parse the given JSON into a tiler model.
+TilerModel<ModuleInfo> fromJson(Map<String, dynamic> json) =>
+    TilerModel(root: _tileFromJson(json['root']));
+
+Map<String, dynamic> _tileToJson(TileModel model) => {
+      'content': model.content?.toJson(),
+      'type': model.type.index,
+      'flex': model.flex,
+      'tiles': model.tiles.map(_tileToJson).toList(),
+    };
+
+TileModel<ModuleInfo> _tileFromJson(
+  Map<String, dynamic> json, {
+  TileModel parent,
+}) {
+  return TileModel(
+    parent: parent,
+    type: TileType.values[json['type']],
+    content:
+        (json['content'] == null) ? null : ModuleInfo.fromJson(json['content']),
+    flex: json['flex'],
+    tiles: _listTileFromJson(json['tiles']),
+  );
+}
+
+List<TileModel<ModuleInfo>> _listTileFromJson(
+  List<dynamic> json, {
+  TileModel<ModuleInfo> parent,
+}) {
+  if (json != null) {
+    return json.map((data) => _tileFromJson(data, parent: parent)).toList();
+  }
+  return [];
+}
+
+/// Creates a copy of the given tiler model.
+TilerModel<ModuleInfo> cloneTiler(TilerModel<ModuleInfo> model) =>
+    TilerModel<ModuleInfo>(
+      root: _cloneTile(model.root),
+    );
+
+TileModel<ModuleInfo> _cloneTile(TileModel<ModuleInfo> model) => model == null
+    ? null
+    : TileModel(
+        content: model.content,
+        type: model.type,
+        flex: model.flex,
+        tiles: model.tiles.map(_cloneTile).toList(),
+      );
diff --git a/public/dart/story_shell_labs/lib/src/layout/tile_presenter/layout_suggestions_update.dart b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/layout_suggestions_update.dart
new file mode 100644
index 0000000..0a31812
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/layout_suggestions_update.dart
@@ -0,0 +1,18 @@
+// 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:collection';
+
+import 'package:meta/meta.dart';
+import 'package:tiler/tiler.dart';
+import '../tile_model/module_info.dart';
+
+/// Recommend alternative layouts
+class LayoutSuggestionUpdate {
+  /// List of suggestions for new [TilerModel]s
+  final UnmodifiableListView<TilerModel<ModuleInfo>> models;
+
+  /// Constructor for a layout suggestions update.
+  LayoutSuggestionUpdate({@required this.models});
+}
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
new file mode 100644
index 0000000..598d891
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/tile_presenter.dart
@@ -0,0 +1,91 @@
+// 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 'dart:collection';
+
+import 'package:built_collection/built_collection.dart';
+import 'package:fuchsia_scenic_flutter/child_view_connection.dart';
+import 'package:tiler/tiler.dart';
+
+import '../deja_layout/deja_layout.dart';
+import '../layout.dart';
+import '../presenter.dart';
+import '../tile_model/module_info.dart';
+import '../tile_model/tile_layout_model.dart';
+import 'layout_suggestions_update.dart';
+
+/// This class is part of a flutter-based presenter.
+/// It converts callbacks from [Layout] into broadcast streams which are
+/// easier for Flutter Widgets to work with.
+class TilePresenter extends Presenter<TileLayoutModel> {
+  /// Called when a layout is requested.
+  UserLayoutRequestCallback requestLayoutCallback;
+
+  TileLayoutModel _current = TileLayoutModel(
+      model: TilerModel<ModuleInfo>(),
+      connections: BuiltMap(<String, ChildViewConnection>{}));
+  List<TilerModel<ModuleInfo>> _suggestions = [];
+
+  // Stream controllers
+  final _layoutSuggestionController =
+      StreamController<LayoutSuggestionUpdate>.broadcast();
+  final _updateController = StreamController<TileLayoutModel>();
+
+  /// Streams the current layout.
+  Stream<TileLayoutModel> get update => _updateController.stream;
+
+  /// Get the current layout.
+  TileLayoutModel get currentState => _current;
+
+  /// Constructor for a tiling presenter.
+  TilePresenter({
+    RemoveSurfaceCallback removeSurfaceCallback,
+    FocusChangeCallback changeFocusCallback,
+    this.requestLayoutCallback,
+  }) : super(
+            removeSurfaceCallback: removeSurfaceCallback,
+            changeFocusCallback: changeFocusCallback);
+
+  /// Call when the presenter is no longer needed to close streams.
+  void dispose() {
+    _updateController.close();
+  }
+
+  /// Streams layout suggestion updates.
+  Stream<LayoutSuggestionUpdate> get suggestionsUpdate =>
+      _layoutSuggestionController.stream;
+
+  /// Get current layoutsuggestions.
+  LayoutSuggestionUpdate get currentSuggestionsState => LayoutSuggestionUpdate(
+        models: UnmodifiableListView(_suggestions),
+      );
+
+  @override
+  void onLayoutChange(TileLayoutModel layoutModel) {
+    // publish on stream for ease of use on Flutter side
+    _current = layoutModel;
+    _updateController.add(layoutModel);
+  }
+
+  /// Called with new layout suggestions.
+  void onSuggestionChange(Iterable<TilerModel<ModuleInfo>> models) {
+    _suggestions = models;
+    // publish on stream for ease of use on Flutter side
+    _layoutSuggestionController
+        .add(LayoutSuggestionUpdate(models: UnmodifiableListView(models)));
+  }
+
+  /// Request a layout.
+  void requestLayout(TilerModel<ModuleInfo> model) =>
+      requestLayoutCallback(model);
+
+  @override
+  void changeFocus(String modName, {bool focus = false}) =>
+      changeFocusCallback(modName, focus);
+
+  @override
+  void removeSurface(Iterable<String> surfaces) =>
+      removeSurfaceCallback(surfaces);
+}
diff --git a/public/dart/story_shell_labs/lib/src/layout/tile_presenter/tile_presenter_suggestions_widget.dart b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/tile_presenter_suggestions_widget.dart
new file mode 100644
index 0000000..df7a06d
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/tile_presenter_suggestions_widget.dart
@@ -0,0 +1,87 @@
+// 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 'package:flutter/material.dart';
+import 'package:tiler/tiler.dart';
+
+import '../tile_model/module_info.dart';
+import 'layout_suggestions_update.dart';
+import 'tile_presenter.dart';
+
+/// Widget for displaying layout suggestions.
+@immutable
+class LayoutSuggestionsWidget extends StatelessWidget {
+  /// Presenter for a tile.
+  final TilePresenter presenter;
+
+  /// Returns border color for a given surface id.
+  final Color Function(String modName) colorForMod;
+
+  /// Called when a suggestion is selected.
+  final void Function(TilerModel) onSelect;
+
+  /// Constructor for a layout suggestions widget.
+  const LayoutSuggestionsWidget({
+    @required this.presenter,
+    @required this.onSelect,
+    @required this.colorForMod,
+  });
+
+  @override
+  Widget build(BuildContext context) => StreamBuilder<LayoutSuggestionUpdate>(
+        stream: presenter.suggestionsUpdate,
+        initialData: presenter.currentSuggestionsState,
+        builder: (context, snapshot) {
+          print('suggested models: ${snapshot.data.models}');
+          return Row(
+            mainAxisSize: MainAxisSize.min,
+            children: snapshot.data.models.map(_buildSuggestion).toList(),
+          );
+        },
+      );
+
+  Widget _buildSuggestion(TilerModel<ModuleInfo> model) {
+    return Padding(
+      padding: EdgeInsets.symmetric(horizontal: 16.0),
+      child: Material(
+        elevation: 24,
+        color: Colors.white,
+        child: AspectRatio(
+          aspectRatio: 4.0 / 3.0,
+          child: Stack(
+            children: <Widget>[
+              Container(
+                color: Color(0xFFFAFAFA),
+                margin: EdgeInsets.all(1.0),
+                padding: EdgeInsets.all(1.0),
+                child: Tiler(
+                  sizerThickness: 0,
+                  model: model,
+                  chromeBuilder: (BuildContext context, TileModel tile) {
+                    return Padding(
+                      padding: EdgeInsets.all(1),
+                      child: Container(
+                        color: colorForMod(tile.content.modName),
+                      ),
+                    );
+                  },
+                ),
+              ),
+              Positioned.fill(
+                child: Material(
+                  color: Colors.transparent,
+                  child: InkWell(
+                    onTap: () {
+                      onSelect(model);
+                    },
+                  ),
+                ),
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/public/dart/story_shell_labs/lib/src/layout/tile_presenter/tile_presenter_widget.dart b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/tile_presenter_widget.dart
new file mode 100644
index 0000000..ddcca40
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/tile_presenter_widget.dart
@@ -0,0 +1,188 @@
+// 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 'package:built_collection/built_collection.dart';
+import 'package:flutter/material.dart';
+import 'package:fuchsia_scenic_flutter/child_view.dart' show ChildView;
+import 'package:fuchsia_scenic_flutter/child_view_connection.dart'
+    show ChildViewConnection;
+import 'package:tiler/tiler.dart';
+
+import '../tile_model/module_info.dart';
+import 'widgets/drop_target_widget.dart';
+import 'widgets/editing_tile_chrome.dart';
+
+const _kSizerThickness = 24.0;
+const _kSizerHandleThickness = 4.0;
+
+final _kSizerHandleDecoration = BoxDecoration(
+  border: Border.all(color: Color(0xFFBDBDBD)),
+  borderRadius: BorderRadius.circular(2),
+);
+
+/// Tiling layout Layout presenter widget.
+@immutable
+class LayoutPresenter extends StatelessWidget {
+  /// The model being rendered.
+  final TilerModel tilerModel;
+
+  /// Whether edit mode is on or not.
+  final bool isEditing;
+
+  /// Maps a surface id to the view.
+  final BuiltMap<String, ChildViewConnection> connections;
+
+  /// Border color for the mod.
+  final Color Function(String modName) colorForMod;
+
+  /// Maps a parameter id to a color.
+  final Map<String, Color> parametersToColors;
+
+  /// Currently focused mod.
+  final ValueNotifier focusedMod;
+
+  /// Constructor for a tiling layout presenter.
+  const LayoutPresenter({
+    @required this.tilerModel,
+    @required this.isEditing,
+    @required this.connections,
+    @required this.colorForMod,
+    @required this.parametersToColors,
+    @required this.focusedMod,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final tiler = Tiler(
+      model: tilerModel,
+      sizerThickness: isEditing ? _kSizerThickness : 0,
+      sizerBuilder: isEditing ? _sizerBuilder : null,
+      chromeBuilder: _buildChrome,
+    );
+
+    if (!isEditing) {
+      return tiler;
+    }
+
+    return AnimatedBuilder(
+      animation: tilerModel,
+      builder: (context, child) {
+        return Column(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: <Widget>[
+            _addTarget(tileAfter: _getRoot, axis: Axis.horizontal),
+            Expanded(
+              child: Row(
+                crossAxisAlignment: CrossAxisAlignment.stretch,
+                children: <Widget>[
+                  _addTarget(tileAfter: _getRoot, axis: Axis.vertical),
+                  child,
+                  _addTarget(tileBefore: _getRoot, axis: Axis.vertical),
+                ],
+              ),
+            ),
+            _addTarget(tileBefore: _getRoot, axis: Axis.horizontal),
+          ],
+        );
+      },
+      child: Expanded(child: tiler),
+    );
+  }
+
+  TileModel _getRoot() => tilerModel.root;
+
+  Widget _sizerBuilder(
+    BuildContext context,
+    Axis axis,
+    TileModel tileBefore,
+    TileModel tileAfter,
+  ) =>
+      Stack(
+        children: <Widget>[
+          Container(
+            color: Colors.transparent,
+            // TODO(ahetzroni): wrap in AnimatedSize when variably sized sizers becomes available and adding drop targets
+            child: SizedBox(
+              width: axis == Axis.horizontal ? null : _kSizerThickness,
+              height: axis == Axis.vertical ? null : _kSizerThickness,
+              child: Center(
+                child: Container(
+                  width: axis == Axis.horizontal
+                      ? _kSizerThickness
+                      : _kSizerHandleThickness,
+                  height: axis == Axis.vertical
+                      ? _kSizerThickness
+                      : _kSizerHandleThickness,
+                  decoration: _kSizerHandleDecoration,
+                ),
+              ),
+            ),
+          ),
+          Positioned.fill(
+            child: _addTarget(
+              tileBefore: () => tileBefore,
+              tileAfter: () => tileAfter,
+              axis: axis,
+            ),
+          ),
+        ],
+      );
+
+  Widget _buildChrome(BuildContext context, TileModel tile) {
+    ModuleInfo content = tile.content;
+    final modName = content.modName;
+    final connection = connections[modName];
+
+    if (!isEditing) {
+      return ChildView(connection: connection);
+    }
+
+    return LayoutBuilder(
+        builder: (context, constraints) => EditingTileChrome(
+              focusedMod: focusedMod,
+              borderColor: colorForMod(modName),
+              parameterColors:
+                  content.parameters.map((p) => parametersToColors[p]),
+              tilerModel: tilerModel,
+              tile: tile,
+              modName: modName,
+              childView: ChildView(
+                focusable: false,
+                hitTestable: false,
+                connection: connection,
+              ),
+              editingSize: constraints.biggest,
+              originalSize: constraints.biggest +
+                  Offset(_kSizerThickness, _kSizerThickness),
+            ));
+  }
+
+  Widget _addTarget({
+    TileModel Function() tileBefore,
+    TileModel Function() tileAfter,
+    Axis axis,
+  }) =>
+      DropTargetWidget(
+        onAccept: (tile) {
+          tilerModel
+            ..remove(tile)
+            ..add(
+              content: tile.content,
+              nearTile: (tileBefore ?? tileAfter)(),
+              direction: tileBefore != null
+                  ? (axis == Axis.horizontal
+                      ? AxisDirection.down
+                      : AxisDirection.right)
+                  : (axis == Axis.horizontal
+                      ? AxisDirection.up
+                      : AxisDirection.left),
+            );
+        },
+        onWillAccept: (tile) =>
+            tile != tileBefore?.call() && tile != tileAfter?.call(),
+        axis: axis == Axis.horizontal ? Axis.vertical : Axis.horizontal,
+        baseSize: _kSizerThickness,
+        hoverSize: _kSizerThickness,
+      );
+}
diff --git a/public/dart/story_shell_labs/lib/src/layout/tile_presenter/widgets/drop_target_widget.dart b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/widgets/drop_target_widget.dart
new file mode 100644
index 0000000..09251fc
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/widgets/drop_target_widget.dart
@@ -0,0 +1,65 @@
+// 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 'package:flutter/material.dart';
+import 'package:tiler/tiler.dart';
+
+/// Widget for drag and drop target.
+class DropTargetWidget extends StatefulWidget {
+  /// COnstructor for the widget.
+  const DropTargetWidget({
+    @required this.onAccept,
+    @required this.onWillAccept,
+    @required this.axis,
+    @required this.baseSize,
+    @required this.hoverSize,
+  });
+
+  /// Axis
+  final Axis axis;
+
+  /// On drop accept callback
+  final DragTargetAccept<TileModel> onAccept;
+
+  /// On drop will accept callback
+  final DragTargetWillAccept<TileModel> onWillAccept;
+
+  /// Base size
+  final double baseSize;
+
+  /// Hover size
+  final double hoverSize;
+
+  @override
+  _DropTargetWidgetState createState() => _DropTargetWidgetState();
+}
+
+class _DropTargetWidgetState extends State<DropTargetWidget>
+    with SingleTickerProviderStateMixin {
+  @override
+  Widget build(BuildContext context) {
+    return DragTarget<TileModel>(
+      builder: (context, candidateData, rejectedData) {
+        final hovering = candidateData.isNotEmpty;
+        final size = hovering ? widget.hoverSize : widget.baseSize;
+        final color = hovering ? Colors.purple : Colors.transparent;
+        return AnimatedSize(
+          duration: Duration(milliseconds: 200),
+          curve: Curves.ease,
+          vsync: this,
+          child: Container(
+            width: widget.axis == Axis.horizontal ? size : null,
+            height: widget.axis == Axis.vertical ? size : null,
+            child: Material(
+              elevation: 8,
+              color: color,
+            ),
+          ),
+        );
+      },
+      onWillAccept: widget.onWillAccept,
+      onAccept: widget.onAccept,
+    );
+  }
+}
diff --git a/public/dart/story_shell_labs/lib/src/layout/tile_presenter/widgets/editing_tile_chrome.dart b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/widgets/editing_tile_chrome.dart
new file mode 100644
index 0000000..8bdcd11
--- /dev/null
+++ b/public/dart/story_shell_labs/lib/src/layout/tile_presenter/widgets/editing_tile_chrome.dart
@@ -0,0 +1,257 @@
+// 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 'package:flutter/material.dart';
+import 'package:tiler/tiler.dart';
+import 'drop_target_widget.dart';
+
+const _kHighlightedBorderWidth = 3.0;
+const _kBorderWidth = 1.0;
+const _kBorderWidthDiff = _kHighlightedBorderWidth - _kBorderWidth;
+
+const _kTilePlaceholderWhenDragging = DecoratedBox(
+  decoration: BoxDecoration(color: Color(0xFFFAFAFA)),
+);
+
+/// Chrome for a tiling layout presenter.
+class EditingTileChrome extends StatefulWidget {
+  /// Constructor for a tiling layout presenter.
+  const EditingTileChrome({
+    @required this.focusedMod,
+    @required this.borderColor,
+    @required this.parameterColors,
+    @required this.tilerModel,
+    @required this.tile,
+    @required this.childView,
+    @required this.modName,
+    @required this.originalSize,
+    @required this.editingSize,
+  });
+
+  /// Currently focused mod.
+  final ValueNotifier focusedMod;
+
+  /// Chrome border color.
+  final Color borderColor;
+
+  /// Intent parameter circle colors.
+  final Iterable<Color> parameterColors;
+
+  /// The model currently being displayed.
+  final TilerModel tilerModel;
+
+  /// The tile being showed on this chrome.
+  final TileModel tile;
+
+  /// Content of the chrome.
+  final Widget childView;
+
+  /// Surface id of the view displayed here.
+  final String modName;
+
+  /// Original size
+  final Size originalSize;
+
+  /// Eiditing size
+  final Size editingSize;
+
+  @override
+  _EditingTileChromeState createState() => _EditingTileChromeState();
+}
+
+class _EditingTileChromeState extends State<EditingTileChrome> {
+  final _isDragging = ValueNotifier(false);
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: AspectRatio(
+        aspectRatio:
+            (widget.originalSize + Offset(_kBorderWidth, _kBorderWidth) * 2.0)
+                .aspectRatio,
+        child: Stack(
+          children: <Widget>[
+            Positioned.fill(
+              child: Draggable(
+                onDragStarted: () {
+                  widget.focusedMod.value = widget.modName;
+                  _isDragging.value = true;
+                },
+                onDragEnd: (_) {
+                  _isDragging.value = false;
+                },
+                key: Key(widget.modName),
+                data: widget.tile,
+                feedback: _buildFeedback(),
+                childWhenDragging: _kTilePlaceholderWhenDragging,
+                child: Container(
+                  decoration: BoxDecoration(
+                    border: Border.all(
+                      color: widget.borderColor,
+                      width: _kBorderWidth,
+                    ),
+                  ),
+                  child: Stack(
+                    children: [
+                      Positioned.fill(
+                        child: FittedBox(
+                          fit: BoxFit.contain,
+                          child: SizedBox.fromSize(
+                            size: widget.originalSize,
+                            child: widget.childView,
+                          ),
+                        ),
+                      )
+                    ]..addAll(_buildSplitTargets(widget.editingSize)),
+                  ),
+                ),
+              ),
+            ),
+            _buildCornerItems(),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildFeedback() {
+    // to get the offset between the draggable object and the feeback
+
+    // we need the difference between the border width of the original widget and the feedback
+    final borderWidthDifference =
+        Offset(-_kBorderWidthDiff, -_kBorderWidthDiff);
+    // and half the space lost to containing the original size within the editing size (the tile after reducing sizers)
+    final boxFitDifference = (widget.editingSize * .5 -
+        applyBoxFit(BoxFit.contain, widget.originalSize, widget.editingSize)
+                .destination *
+            0.5);
+
+    return Transform.translate(
+      offset: borderWidthDifference - boxFitDifference,
+      child: SizedBox.fromSize(
+        size: widget.editingSize +
+            Offset(_kBorderWidthDiff, _kBorderWidthDiff) * 2,
+        child: Center(
+          child: AspectRatio(
+            // aspect ratio of original box plus highlighted border
+            aspectRatio: (widget.originalSize +
+                    Offset(_kHighlightedBorderWidth, _kHighlightedBorderWidth) *
+                        2.0)
+                .aspectRatio,
+            child: Material(
+              elevation: 16.0,
+              child: Container(
+                decoration: BoxDecoration(
+                  border: Border.all(
+                    color: widget.borderColor,
+                    width: _kHighlightedBorderWidth,
+                  ),
+                ),
+                child: FittedBox(
+                  fit: BoxFit.contain,
+                  child: SizedBox.fromSize(
+                    size: widget.originalSize,
+                    child: widget.childView,
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _buildCornerItems() {
+    final parameterIndicators = Row(
+      children: widget.parameterColors
+          .expand((color) => [
+                Material(
+                  elevation: 4.0,
+                  clipBehavior: Clip.antiAlias,
+                  shape: CircleBorder(),
+                  color: color,
+                  child: SizedBox(width: 24, height: 24),
+                ),
+                SizedBox(width: 8.0),
+              ])
+          .toList(),
+    );
+
+    return Positioned(
+      top: 8,
+      right: 8,
+      child: AnimatedBuilder(
+        animation: _isDragging,
+        builder: (_, child) =>
+            Offstage(offstage: _isDragging.value, child: child),
+        child: Row(
+          children: <Widget>[
+            parameterIndicators,
+            Material(
+              elevation: 4.0,
+              clipBehavior: Clip.antiAlias,
+              shape: CircleBorder(),
+              child: InkWell(
+                onTap: () {
+                  widget.tilerModel.remove(widget.tile);
+                },
+                child: Icon(Icons.close),
+              ),
+            )
+          ],
+        ),
+      ),
+    );
+  }
+
+  List<Widget> _buildSplitTargets(Size size) => <Widget>[
+        _splitTarget(
+          nearTile: widget.tile,
+          direction: AxisDirection.up,
+          parentSizeOnAxis: size.height,
+        ),
+        _splitTarget(
+          nearTile: widget.tile,
+          direction: AxisDirection.down,
+          parentSizeOnAxis: size.height,
+        ),
+        _splitTarget(
+          nearTile: widget.tile,
+          direction: AxisDirection.left,
+          parentSizeOnAxis: size.width,
+        ),
+        _splitTarget(
+          nearTile: widget.tile,
+          direction: AxisDirection.right,
+          parentSizeOnAxis: size.width,
+        ),
+      ];
+
+  Widget _splitTarget({
+    TileModel nearTile,
+    AxisDirection direction,
+    double parentSizeOnAxis,
+  }) =>
+      Positioned(
+        top: direction == AxisDirection.down ? null : 0,
+        bottom: direction == AxisDirection.up ? null : 0,
+        left: direction == AxisDirection.right ? null : 0,
+        right: direction == AxisDirection.left ? null : 0,
+        child: DropTargetWidget(
+          onAccept: (tile) {
+            widget.tilerModel.remove(tile);
+            widget.tilerModel.split(
+              content: tile.content,
+              direction: direction,
+              tile: nearTile,
+            );
+          },
+          onWillAccept: (_) => true,
+          axis: axisDirectionToAxis(direction),
+          baseSize: 50.0,
+          hoverSize: parentSizeOnAxis * .33,
+        ),
+      );
+}
diff --git a/public/dart/story_shell_labs/pubspec.yaml b/public/dart/story_shell_labs/pubspec.yaml
new file mode 100644
index 0000000..754277a
--- /dev/null
+++ b/public/dart/story_shell_labs/pubspec.yaml
@@ -0,0 +1,15 @@
+# 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.
+
+name: layout
+description: Library for story shell labs
+
+dependencies:
+  flutter:
+    sdk: flutter
+  json_annotation: ^2.0.0
+
+dev_dependencies:
+  json_serializable: ^2.0.0
+  build_runner: ^1.0.0
diff --git a/public/dart/story_shell_labs/test/deja_layout_test.dart b/public/dart/story_shell_labs/test/deja_layout_test.dart
new file mode 100644
index 0000000..61e2349
--- /dev/null
+++ b/public/dart/story_shell_labs/test/deja_layout_test.dart
@@ -0,0 +1,520 @@
+// 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:collection';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:story_shell_labs_lib/layout/deja_layout.dart';
+import 'package:story_shell_labs_lib/layout/tile_model.dart';
+import 'package:meta/meta.dart';
+import 'package:mockito/mockito.dart';
+import 'package:test/test.dart';
+import 'package:tiler/tiler.dart';
+
+class MockFile extends Mock implements File {}
+
+class MockLayoutStore extends Mock implements LayoutStore {}
+
+final _kViewBookContent = ModuleInfo(
+  modName: 'books_mod',
+  intent: 'VIEW_BOOK',
+  parameters: UnmodifiableListView<String>([]),
+);
+
+final _kViewCollectionContent = ModuleInfo(
+  modName: 'collections_mod',
+  intent: 'VIEW_COLLECTION',
+  parameters: UnmodifiableListView<String>([]),
+);
+
+final _kNonMatchingIntentContent = ModuleInfo(
+  modName: 'misc_mod',
+  intent: 'NON_MATCHING_INTENT',
+  parameters: UnmodifiableListView<String>([]),
+);
+
+MockFile _createLayoutFile(TilerModel<ModuleInfo> layout) {
+  final layoutFile = MockFile();
+
+  when(layoutFile.readAsString())
+      .thenAnswer((_) => Future.value(json.encode(toJson(layout))));
+  when(layoutFile.readAsStringSync())
+      .thenAnswer((_) => json.encode(toJson(layout)));
+
+  return layoutFile;
+}
+
+//          *
+//        /   \
+//    node1  node2
+TilerModel<ModuleInfo> _genLayout2Mods({
+  @required Map node1,
+  @required Map node2,
+}) {
+  return TilerModel<ModuleInfo>(
+    root: TileModel<ModuleInfo>(
+      type: TileType.column,
+      tiles: [
+        TileModel<ModuleInfo>(
+            flex: node1['flex'],
+            type: TileType.content,
+            content: node1['content'] ?? _kViewBookContent),
+        TileModel<ModuleInfo>(
+            flex: node2['flex'],
+            type: TileType.content,
+            content: node2['content'] ?? _kViewBookContent),
+      ],
+    ),
+  );
+}
+
+//          *
+//        /   \
+//   node1     *
+//            /  \
+//        node2   node3
+TilerModel<ModuleInfo> _genLayout3ModsA({
+  @required Map node1,
+  @required Map node2,
+  @required Map node3,
+}) {
+  return TilerModel<ModuleInfo>(
+    root: TileModel<ModuleInfo>(
+      type: TileType.row,
+      tiles: [
+        TileModel<ModuleInfo>(
+            flex: node1['flex'],
+            type: TileType.content,
+            content: node1['content'] ?? _kViewCollectionContent),
+        TileModel<ModuleInfo>(
+          type: TileType.column,
+          tiles: [
+            TileModel<ModuleInfo>(
+                flex: node2['flex'],
+                type: TileType.content,
+                content: node2['content'] ?? _kViewCollectionContent),
+            TileModel<ModuleInfo>(
+                flex: node3['flex'],
+                type: TileType.content,
+                content: node3['content'] ?? _kViewBookContent),
+          ],
+        )
+      ],
+    ),
+  );
+}
+
+//          *
+//        /   \
+//   node1     *
+//            /  \
+//         node2  node3
+TilerModel<ModuleInfo> _genLayout3ModsB({
+  @required Map node1,
+  @required Map node2,
+  @required Map node3,
+}) {
+  return TilerModel<ModuleInfo>(
+    root: TileModel<ModuleInfo>(
+      type: TileType.column,
+      tiles: [
+        TileModel<ModuleInfo>(
+            flex: node1['flex'],
+            type: TileType.content,
+            content: node1['content'] ?? _kViewCollectionContent),
+        TileModel<ModuleInfo>(
+          type: TileType.row,
+          tiles: [
+            TileModel<ModuleInfo>(
+                flex: node2['flex'],
+                type: TileType.content,
+                content: node2['content'] ?? _kViewCollectionContent),
+            TileModel<ModuleInfo>(
+                flex: node3['flex'],
+                type: TileType.content,
+                content: node3['content'] ?? _kViewBookContent),
+          ],
+        )
+      ],
+    ),
+  );
+}
+
+//                *
+//              /   \
+//             *    node1
+//            /  \
+//       node2    node3
+TilerModel<ModuleInfo> _genLayout3ModsC({
+  @required Map node1,
+  @required Map node2,
+  @required Map node3,
+}) {
+  return TilerModel<ModuleInfo>(
+    root: TileModel<ModuleInfo>(
+      type: TileType.row,
+      tiles: [
+        TileModel<ModuleInfo>(
+          type: TileType.column,
+          tiles: [
+            TileModel<ModuleInfo>(
+                flex: node2['flex'],
+                type: TileType.content,
+                content: node2['content'] ?? _kViewCollectionContent),
+            TileModel<ModuleInfo>(
+                flex: node3['flex'],
+                type: TileType.content,
+                content: node3['content'] ?? _kViewBookContent),
+          ],
+        ),
+        TileModel<ModuleInfo>(
+            flex: node1['flex'],
+            type: TileType.content,
+            content: node1['content'] ?? _kViewCollectionContent),
+      ],
+    ),
+  );
+}
+
+//                *
+//              /   \
+//             *    node1
+//            /  \
+//       node2    node3
+TilerModel<ModuleInfo> _genLayout3ModsD({
+  @required Map node1,
+  @required Map node2,
+  @required Map node3,
+}) {
+  return TilerModel<ModuleInfo>(
+    root: TileModel<ModuleInfo>(
+      type: TileType.column,
+      tiles: [
+        TileModel<ModuleInfo>(
+          type: TileType.row,
+          tiles: [
+            TileModel<ModuleInfo>(
+                flex: node2['flex'],
+                type: TileType.content,
+                content: node2['content'] ?? _kViewBookContent),
+            TileModel<ModuleInfo>(
+                flex: node3['flex'],
+                type: TileType.content,
+                content: node3['content'] ?? _kViewCollectionContent),
+          ],
+        ),
+        TileModel<ModuleInfo>(
+            flex: node1['flex'],
+            type: TileType.content,
+            content: node1['content'] ?? _kViewCollectionContent),
+      ],
+    ),
+  );
+}
+
+List<MockFile> _fillLayoutFiles(
+    List<int> fills, List<TilerModel<ModuleInfo>> layouts) {
+  assert(fills.length == layouts.length);
+
+  final layoutFiles = <MockFile>[];
+  for (int i = 0; i < fills.length; i++) {
+    layoutFiles
+        .addAll(List.filled(fills[i], layouts[i]).map(_createLayoutFile));
+  }
+
+  layoutFiles.shuffle();
+  return layoutFiles;
+}
+
+// Layout Suggestions are sorted descending by occurrence count.
+// Each layout suggestion is different geometric layout.
+//
+// Top N suggestions are determined by:
+// 1. Matching Intents
+//    2. Group by equivalent geometric layout.
+//       Sort this Group by occurrence count.
+//.     3. In a geometric layout group. Further Group by equivalent flex
+//         layouts. Sort this Group by occurrence count.
+//
+//
+// Additional Details: Geometric layout.
+// Flex is not a property that changes the geometric layout.
+//
+// A geometric layout could not be equivalent because of different layout tree
+// structure
+//          *                     *
+//         /  \                  /  \
+//        t     *               *    t
+//             / \             / \
+//            t   t           t   t
+//
+// Or a geometric layout could be different because of the Tile orientation
+// TileType.row, TileType.column. But the tree layout structure could be
+// the same.
+//
+//          * (TileType.row)              * (TileType.column)
+//         /  \                         /   \
+//        t     * (TileType.column)    t      * (TileType.row)
+//             / \                          /  \
+//            t   t                        t    t
+//
+void main() {
+  MockLayoutStore mockLayoutStore;
+
+  setUp(() {
+    mockLayoutStore = MockLayoutStore();
+    when(mockLayoutStore.read(any)).thenAnswer((Invocation i) {
+      final MockFile f = i.positionalArguments[0];
+      final s = f.readAsStringSync();
+      return fromJson(jsonDecode(s));
+    });
+  });
+
+  test(
+      'Expect current layout as the only suggestion'
+      ' if no stored matching geometric layouts.', () {
+    final layouts = [
+      _genLayout3ModsA(
+        node1: {'flex': 0.5},
+        node2: {'flex': 0.5},
+        node3: {'flex': 0.5},
+      ), // appears 3 times
+      _genLayout3ModsA(
+        node1: {'flex': 0.2},
+        node2: {'flex': 0.4},
+        node3: {'flex': 0.6},
+      ), // appears 2 times
+    ];
+
+    final layoutFiles = _fillLayoutFiles([3, 2], layouts);
+    when(mockLayoutStore.listSync()).thenReturn(layoutFiles);
+    final layoutPolicy = LayoutPolicy(layoutStore: mockLayoutStore);
+
+    final currentLayout = _genLayout2Mods(
+      node1: {'flex': 0.5},
+      node2: {'flex': 0.5},
+    );
+
+    final tilerModelsSuggestions = layoutPolicy.getLayout(currentLayout);
+    expect(tilerModelsSuggestions.length, 1);
+    expect(tilerModelsSuggestions[0], currentLayout);
+  });
+
+  test(
+      'Expect current layout as the only suggestion'
+      ' if no stored layouts with matching intents.', () {
+    final layouts = [
+      _genLayout3ModsA(
+        node1: {'flex': 0.5, 'content': _kNonMatchingIntentContent},
+        node2: {'flex': 0.5, 'content': _kNonMatchingIntentContent},
+        node3: {'flex': 0.5, 'content': _kNonMatchingIntentContent},
+      ), // appears 3 times
+      _genLayout3ModsA(
+        node1: {'flex': 0.2, 'content': _kNonMatchingIntentContent},
+        node2: {'flex': 0.4, 'content': _kNonMatchingIntentContent},
+        node3: {'flex': 0.6, 'content': _kNonMatchingIntentContent},
+      ), // appears 2 times
+    ];
+
+    final layoutFiles = _fillLayoutFiles([3, 2], layouts);
+    when(mockLayoutStore.listSync()).thenReturn(layoutFiles);
+    final layoutPolicy = LayoutPolicy(layoutStore: mockLayoutStore);
+
+    final currentLayout = _genLayout3ModsB(
+      node1: {'flex': 0.5},
+      node2: {'flex': 0.5},
+      node3: {'flex': 0.5},
+    );
+
+    final tilerModelsSuggestions = layoutPolicy.getLayout(currentLayout);
+    expect(tilerModelsSuggestions.length, 1);
+    expect(tilerModelsSuggestions[0], currentLayout);
+  });
+
+  test(
+      'Expect current layout is not part of the suggestions if there are'
+      ' any stored layouts with matching geometry and intents', () {
+    final layouts = [
+      _genLayout3ModsA(
+        node1: {'flex': 0.2},
+        node2: {'flex': 0.3},
+        node3: {'flex': 0.7},
+      ),
+    ];
+
+    final layoutFiles = [_createLayoutFile(layouts[0])];
+    when(mockLayoutStore.listSync()).thenReturn(layoutFiles);
+    final layoutPolicy = LayoutPolicy(layoutStore: mockLayoutStore);
+
+    final currentLayout = _genLayout3ModsD(
+      node1: {'flex': 0.8},
+      node2: {'flex': 0.2},
+      node3: {'flex': 0.8},
+    );
+
+    final tilerModelsSuggestions = layoutPolicy.getLayout(currentLayout);
+    expect(tilerModelsSuggestions.length, 1);
+    expect(toJson(tilerModelsSuggestions[0]), toJson(layouts[0]));
+  });
+
+  test(
+      'Expect only 1 suggestion generated if all stored layouts are'
+      ' geometric equivalent even if flex amounts differ', () {
+    // All stored layouts below are all geometrically equivalent.
+    // Therefore only 1 layout suggestion is generated since layout
+    // suggestion must each be a different geometry.
+    final layouts = [
+      // Top 1 layout is the one below since it occurs most often.
+      _genLayout3ModsA(
+        node1: {'flex': 0.5},
+        node2: {'flex': 0.5},
+        node3: {'flex': 0.5},
+      ), // appears 6 times
+      _genLayout3ModsA(
+        node1: {'flex': 0.2},
+        node2: {'flex': 0.3},
+        node3: {'flex': 0.7},
+      ), // appears 5 times
+      _genLayout3ModsA(
+        node1: {'flex': 0.3},
+        node2: {'flex': 0.4},
+        node3: {'flex': 0.6},
+      ), // appears 4 times
+      _genLayout3ModsA(
+        node1: {'flex': 0.4},
+        node2: {'flex': 0.8},
+        node3: {'flex': 0.2},
+      ), // appears 3 times
+    ];
+
+    final layoutFiles = _fillLayoutFiles([6, 5, 4, 3], layouts);
+    when(mockLayoutStore.listSync()).thenReturn(layoutFiles);
+    final layoutPolicy = LayoutPolicy(layoutStore: mockLayoutStore);
+
+    final currentLayout = _genLayout3ModsA(
+      node1: {'flex': 0.3},
+      node2: {'flex': 0.2},
+      node3: {'flex': 0.8},
+    );
+
+    final tilerModelsSuggestions = layoutPolicy.getLayout(currentLayout);
+    expect(tilerModelsSuggestions.length, 1);
+    expect(toJson(tilerModelsSuggestions[0]), toJson(layouts[0]));
+  });
+
+  test(
+      'Expect top N layouts sorted by occurence for geometric and flex.'
+      ' Each suggestion is a different geometric layout with'
+      ' matching intents', () {
+    final layouts = [
+      // Top 4 layouts below.
+      _genLayout3ModsA(
+        node1: {'flex': 0.5},
+        node2: {'flex': 0.5},
+        node3: {'flex': 0.5},
+      ), // appears 9 times
+      _genLayout3ModsB(
+        node1: {'flex': 0.2},
+        node2: {'flex': 0.4},
+        node3: {'flex': 0.6},
+      ), // appears 8 times
+      _genLayout3ModsC(
+        node1: {'flex': 0.1},
+        node2: {'flex': 0.3},
+        node3: {'flex': 0.7},
+      ), // appears 7 times
+      _genLayout3ModsD(
+        node1: {'flex': 0.1},
+        node2: {'flex': 0.3},
+        node3: {'flex': 0.7},
+      ), // appears 6 times
+      // The layouts below are layouts that are geometrically equivalent to
+      // the top ranking layouts, but their flex amounts differ. The number of
+      // times these layouts occurs also differs.
+      _genLayout3ModsA(
+        node1: {'flex': 0.1},
+        node2: {'flex': 0.1},
+        node3: {'flex': 0.9},
+      ), // appears 5 times
+      _genLayout3ModsB(
+        node1: {'flex': 0.2},
+        node2: {'flex': 0.2},
+        node3: {'flex': 0.8},
+      ), // appears 4 times
+      _genLayout3ModsC(
+        node1: {'flex': 0.3},
+        node2: {'flex': 0.8},
+        node3: {'flex': 0.2},
+      ), // appears 3 times
+      _genLayout3ModsD(
+        node1: {'flex': 0.4},
+        node2: {'flex': 0.2},
+        node3: {'flex': 0.8},
+      ), // appears 2 times
+    ];
+
+    final layoutFiles = _fillLayoutFiles([9, 8, 7, 6, 5, 4, 3, 2], layouts);
+    when(mockLayoutStore.listSync()).thenReturn(layoutFiles);
+    final layoutPolicy = LayoutPolicy(layoutStore: mockLayoutStore);
+
+    final currentLayout = _genLayout3ModsA(
+      node1: {'flex': 0.7},
+      node2: {'flex': 0.2},
+      node3: {'flex': 0.8},
+    );
+
+    final top4layouts = layouts.sublist(0, 4);
+    final tilerModelsSuggestions = layoutPolicy.getLayout(currentLayout);
+    expect(tilerModelsSuggestions.length, 4);
+    expect(tilerModelsSuggestions.map(toJson), top4layouts.map(toJson));
+  });
+
+  test('Expect only matching intents layouts to appear in top N layouts', () {
+    final layouts = [
+      // Top 2 layouts below.
+      _genLayout3ModsA(
+        node1: {'flex': 0.5},
+        node2: {'flex': 0.5},
+        node3: {'flex': 0.5},
+      ), // appears 4 times
+      _genLayout3ModsB(
+        node1: {'flex': 0.2},
+        node2: {'flex': 0.4},
+        node3: {'flex': 0.6},
+      ), // appears 3 times
+      // Layouts below are those with non-matching intents to current layout.
+      _genLayout3ModsD(
+        node1: {'flex': 0.6, 'content': _kNonMatchingIntentContent},
+        node2: {'flex': 0.1, 'content': _kNonMatchingIntentContent},
+        node3: {'flex': 0.9, 'content': _kNonMatchingIntentContent},
+      ), // appears 7 times
+      _genLayout3ModsC(
+        node1: {'flex': 0.8, 'content': _kNonMatchingIntentContent},
+        node2: {'flex': 0.8, 'content': _kNonMatchingIntentContent},
+        node3: {'flex': 0.2, 'content': _kNonMatchingIntentContent},
+      ), // appears 6 times
+      _genLayout3ModsB(
+        node1: {'flex': 0.7, 'content': _kNonMatchingIntentContent},
+        node2: {'flex': 0.4, 'content': _kNonMatchingIntentContent},
+        node3: {'flex': 0.6, 'content': _kNonMatchingIntentContent},
+      ), // appears 5 times
+    ];
+
+    final layoutFiles = _fillLayoutFiles([4, 3, 7, 6, 5], layouts);
+    when(mockLayoutStore.listSync()).thenReturn(layoutFiles);
+    final layoutPolicy = LayoutPolicy(layoutStore: mockLayoutStore);
+
+    final currentLayout = _genLayout3ModsA(
+      node1: {'flex': 0.3},
+      node2: {'flex': 0.2},
+      node3: {'flex': 0.8},
+    );
+
+    final top2layouts = layouts.sublist(0, 2);
+    final tilerModelsSuggestions = layoutPolicy.getLayout(currentLayout);
+    expect(tilerModelsSuggestions.length, 2);
+    expect(tilerModelsSuggestions.map(toJson), top2layouts.map(toJson));
+  });
+}
diff --git a/shell/story_shell_labs/BUILD.gn b/shell/story_shell_labs/BUILD.gn
index 7477cd0..561eb2b 100644
--- a/shell/story_shell_labs/BUILD.gn
+++ b/shell/story_shell_labs/BUILD.gn
@@ -24,8 +24,8 @@
     "//sdk/fidl/fuchsia.modular",
     "//third_party/dart-pkg/git/flutter/packages/flutter",
     "//topaz/public/dart/fidl",
-    "//topaz/public/dart/fuchsia_modular",
     "//topaz/public/dart/fuchsia_logger",
+    "//topaz/public/dart/fuchsia_modular",
     "//topaz/public/dart/fuchsia_services",
   ]
 }