[mondrian_lib] Add support multiple surfaces on same layer

Surfaces that are in the same spanning tree are put in the same layer.
Only width is adjusted and all surfaces are given the same size.

TEST=includes unit tests

Change-Id: I60f9c688b118b073f501257feabda32693329c10
diff --git a/public/lib/mondrian/dart/lib/src/composer.dart b/public/lib/mondrian/dart/lib/src/composer.dart
index 2099343..1686fb8 100644
--- a/public/lib/mondrian/dart/lib/src/composer.dart
+++ b/public/lib/mondrian/dart/lib/src/composer.dart
@@ -2,6 +2,7 @@
 // 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 'layout/layout_context.dart';
@@ -21,7 +22,7 @@
   /// The focus order of surfaces being laid out in the experience. Hiding
   /// removes a surface from the _focusedSurfaces Set and adds it to the
   /// _hiddenSurfaces Set.
-  final Set<String> _focusedSurfaces;
+  final LinkedHashSet<String> _focusedSurfaces = LinkedHashSet<String>();
 
   /// The context of the layout
   LayoutContext layoutContext;
@@ -39,7 +40,6 @@
   Composer({
     this.layoutContext = defaultContext,
   })  : _hiddenSurfaces = <String>{},
-        _focusedSurfaces = <String>{},
         _surfaceTree = new SurfaceTree();
 
   /// Add a Surface to the tree. [parentId] is an optional paramater used when
@@ -134,23 +134,55 @@
   ///     {"x":640","y":0,"w":640,"h":800,"surfaceId":"B"} // SurfaceLayout
   ///   ] // Layer
   /// ] // List<Layer>'
-
   List<Layer> getLayout({List<Layer> previousLayout}) {
     // TODO(djmurphy): complete logic - this is placeholder to unblock work
-    List<Layer> layout = <Layer>[];
-    for (String id in _focusedSurfaces.toList()) {
-      layout.add(
-        Layer(
-          element: SurfaceLayout(
-            x: 0,
-            y: 0,
-            w: layoutContext.size.width,
-            h: layoutContext.size.height,
-            surfaceId: id,
-          ),
-        ),
-      );
+    if (_surfaceTree.isEmpty) {
+      return <Layer>[];
     }
+    List<Layer> layout = <Layer>[];
+    SurfaceTree spanningTree = _surfaceTree.spanningTree(
+      startNodeId: _focusedSurfaces.last,
+      condition: (node) => true,
+    );
+    int spanningTreeSize = spanningTree.length;
+    if (spanningTreeSize > 1) {
+      _splitEvenly(layout, spanningTree, spanningTreeSize);
+    } else {
+      // Relies purely on the focused surfaces rather than what's in the graph.
+      // TODO(jphsiao/djmurphy) expand this to take into account the surface graph.
+      for (String id in _focusedSurfaces.toList()) {
+        layout.add(
+          Layer(
+            element: SurfaceLayout(
+              x: 0.0,
+              y: 0.0,
+              w: layoutContext.size.width,
+              h: layoutContext.size.height,
+              surfaceId: id,
+            ),
+          ),
+        );
+      }
+    }
+    return layout;
+  }
+
+  List<Layer> _splitEvenly(
+      List<Layer> layout, SurfaceTree spanningTree, int spanningTreeSize) {
+    Layer layer = new Layer();
+    int surfaceIndex = 0;
+    double splitSize = layoutContext.size.width / spanningTreeSize;
+    for (Surface surface in spanningTree) {
+      layer.add(SurfaceLayout(
+        x: surfaceIndex * splitSize,
+        y: 0.0,
+        w: splitSize,
+        h: layoutContext.size.height,
+        surfaceId: surface.surfaceId,
+      ));
+      surfaceIndex += 1;
+    }
+    layout.add(layer);
     return layout;
   }
 }
diff --git a/public/lib/mondrian/dart/lib/src/layout/layout_context.dart b/public/lib/mondrian/dart/lib/src/layout/layout_context.dart
index 7f9ef7d..27c5448 100644
--- a/public/lib/mondrian/dart/lib/src/layout/layout_context.dart
+++ b/public/lib/mondrian/dart/lib/src/layout/layout_context.dart
@@ -14,10 +14,10 @@
 /// Simple class for capturing 2D size of boxes in layout.
 class Size {
   /// height
-  final int height;
+  final double height;
 
   /// width
-  final int width;
+  final double width;
 
   /// constructor
   const Size(this.width, this.height);
diff --git a/public/lib/mondrian/dart/lib/src/layout/layout_types.dart b/public/lib/mondrian/dart/lib/src/layout/layout_types.dart
index 1f1e653..5a06046 100644
--- a/public/lib/mondrian/dart/lib/src/layout/layout_types.dart
+++ b/public/lib/mondrian/dart/lib/src/layout/layout_types.dart
@@ -9,7 +9,7 @@
 /// viewport position and the element to place at that position.
 abstract class LayoutElement<T> {
   /// Box layout for this Element
-  final int x, y, w, h;
+  final double x, y, w, h;
 
   /// The element positioned at this location
   final T element;
@@ -32,7 +32,7 @@
   @override
   int get hashCode {
     return hash4(
-        (w - x).hashCode, (h - y).hashCode, (x ^ y).hashCode, element.hashCode);
+        (w - x).hashCode, (h - y).hashCode, (x * y).hashCode, element.hashCode);
   }
 }
 
@@ -44,10 +44,12 @@
 
   /// Constructor for adding a single element to a Layer
   Layer({LayoutElement element}) {
-    _innerList.add(element);
+    if (element != null) {
+      _innerList.add(element);
+    }
   }
 
-  /// Constructor for adding a list of [LayoutElement] to a layer
+  /// Constructor for adding a list of [LayoutElement]s to a layer
   Layer.fromList({List<LayoutElement> elements}) {
     _innerList.addAll(elements);
   }
@@ -110,7 +112,8 @@
 /// dimensions.
 class StackLayout extends LayoutElement {
   /// Constructor
-  StackLayout({int x, int y, int w, int h, List<String> surfaceStack})
+  StackLayout(
+      {double x, double y, double w, double h, List<String> surfaceStack})
       : super(x: x, y: y, w: w, h: h, element: surfaceStack);
 
   /// Export to JSON
diff --git a/public/lib/mondrian/dart/test/encode_decode_test.dart b/public/lib/mondrian/dart/test/encode_decode_test.dart
index 9f1c5f5..94eb8e5 100644
--- a/public/lib/mondrian/dart/test/encode_decode_test.dart
+++ b/public/lib/mondrian/dart/test/encode_decode_test.dart
@@ -6,13 +6,13 @@
 import 'package:test/test.dart';
 
 void main() {
-  SurfaceLayout layout =
-      SurfaceLayout(x: 1, y: 2, w: 3, h: 1000000000000, surfaceId: 'first');
+  SurfaceLayout layout = SurfaceLayout(
+      x: 1.0, y: 2.0, w: 3.0, h: 1000000000000.0, surfaceId: 'first');
   Map<String, dynamic> jsonEncodedLayout = {
-    'x': 1,
-    'y': 2,
-    'w': 3,
-    'h': 1000000000000,
+    'x': 1.0,
+    'y': 2.0,
+    'w': 3.0,
+    'h': 1000000000000.0,
     'surfaceId': 'first'
   };
   group('test encoding of types to JSON', () {
diff --git a/public/lib/mondrian/dart/test/layout_test.dart b/public/lib/mondrian/dart/test/layout_test.dart
index 9cdb676..e6a2d29 100644
--- a/public/lib/mondrian/dart/test/layout_test.dart
+++ b/public/lib/mondrian/dart/test/layout_test.dart
@@ -6,42 +6,115 @@
 import 'package:test/test.dart';
 
 void main() {
-  Composer composer = new Composer(
-    layoutContext: new LayoutContext(size: Size(1280, 800)),
-  );
+  Composer setupComposer() {
+    Composer composer =
+        new Composer(layoutContext: new LayoutContext(size: Size(1280, 800)));
+    return composer;
+  }
 
   group(
-    'Test layout for unrelated Surfaces',
+    'Test layout determination',
     () {
-      test('Layout determination for no Surfaces', () {
+      test('For no Surfaces is empty', () {
+        Composer composer = setupComposer();
         expect(composer.getLayout(), equals([]));
       });
-      test('Layout determination for one Surface', () {
+
+      test('For one Surface is full screen', () {
         // expect a List, with one Layer, with one SurfaceLayout
+        Composer composer = setupComposer();
+
         List<Layer> expectedLayout = <Layer>[
           Layer(
               element: SurfaceLayout(
-                  x: 0, y: 0, w: 1280, h: 800, surfaceId: 'first'))
+                  x: 0.0, y: 0.0, w: 1280.0, h: 800.0, surfaceId: 'first'))
         ];
         composer
           ..addSurface(surface: Surface(surfaceId: 'first'))
           ..focusSurface(surfaceId: 'first');
         expect(composer.getLayout(), equals(expectedLayout));
       });
-      test('Layout determination for two Surfaces', () {
+
+      test('For two Surfaces with no relationship is stacked', () {
+        Composer composer = setupComposer();
+
         Layer expectedUpper = Layer(
             element: SurfaceLayout(
-                x: 0, y: 0, w: 1280, h: 800, surfaceId: 'second'));
+                x: 0.0, y: 0.0, w: 1280.0, h: 800.0, surfaceId: 'second'));
         Layer expectedLower = Layer(
-            element:
-                SurfaceLayout(x: 0, y: 0, w: 1280, h: 800, surfaceId: 'first'));
+            element: SurfaceLayout(
+                x: 0.0, y: 0.0, w: 1280.0, h: 800.0, surfaceId: 'first'));
         // expect a List of two Layers, with one SurfaceLayout in each
         List<Layer> expectedLayout = [expectedLower, expectedUpper];
+        // This test relies on the surface being focused in the correct order as the focus list is
+        // maintained separaretly from what has been added to the tree.
         composer
+          ..addSurface(surface: Surface(surfaceId: 'first'))
+          ..focusSurface(surfaceId: 'first')
           ..addSurface(surface: Surface(surfaceId: 'second'))
           ..focusSurface(surfaceId: 'second');
         expect(composer.getLayout(), equals(expectedLayout));
       });
+
+      test('For two related Surfaces is a 50/50 split', () {
+        Composer composer = setupComposer();
+        List<SurfaceLayout> surfaces = [
+          SurfaceLayout(x: 0.0, y: 0.0, w: 640.0, h: 800.0, surfaceId: 'first'),
+          SurfaceLayout(
+              x: 640.0, y: 0.0, w: 640.0, h: 800.0, surfaceId: 'second'),
+        ];
+
+        Layer expectedLayout = Layer.fromList(elements: surfaces);
+        composer
+          ..addSurface(surface: Surface(surfaceId: 'first'))
+          ..addSurface(
+              surface: Surface(
+                surfaceId: 'second',
+                metadata: {},
+              ),
+              parentId: 'first')
+          ..focusSurface(surfaceId: 'second');
+        expect(composer.getLayout(), equals([expectedLayout]));
+      });
+
+      test(
+          'For four Surfaces, 3 children with the same parent, is split into quarters vertically',
+          () {
+        Composer composer = setupComposer();
+        List<SurfaceLayout> surfaces = [
+          SurfaceLayout(x: 0.0, y: 0.0, w: 320.0, h: 800.0, surfaceId: 'first'),
+          SurfaceLayout(
+              x: 320.0, y: 0.0, w: 320.0, h: 800.0, surfaceId: 'second'),
+          SurfaceLayout(
+              x: 640.0, y: 0.0, w: 320.0, h: 800.0, surfaceId: 'third'),
+          SurfaceLayout(
+              x: 960.0, y: 0.0, w: 320.0, h: 800.0, surfaceId: 'fourth'),
+        ];
+
+        Layer expectedLayout = Layer.fromList(elements: surfaces);
+        composer
+          ..addSurface(surface: Surface(surfaceId: 'first'))
+          ..addSurface(
+              surface: Surface(
+                surfaceId: 'second',
+                metadata: {},
+              ),
+              parentId: 'first')
+          ..addSurface(
+              surface: Surface(
+                surfaceId: 'third',
+                metadata: {},
+              ),
+              parentId: 'first')
+          ..addSurface(
+              surface: Surface(
+                surfaceId: 'fourth',
+                metadata: {},
+              ),
+              parentId: 'first')
+          ..focusSurface(surfaceId: 'fourth');
+        expect(composer.getLayout(), equals([expectedLayout]));
+      });
     },
   );
 }