[gnome-builder/wip/chergert/layout] wip on layout



commit 31b76774d8f8b986378a67d141ecc266a71d9a74
Author: Christian Hergert <chergert redhat com>
Date:   Sun Jun 25 23:41:05 2017 -0700

    wip on layout

 libide/editor/ide-editor-frame.c                   |    1 -
 libide/editor/ide-editor-layout-stack-addin.h      |    2 +-
 libide/editor/ide-editor-perspective.c             |   30 +-
 libide/editor/ide-editor-perspective.h             |    2 +-
 libide/editor/ide-editor-view-private.h            |    2 +-
 libide/editor/ide-editor-view.c                    |    1 +
 libide/editor/ide-editor-view.h                    |    2 +-
 libide/history/ide-back-forward-controls.c         |   81 ++
 .../ide-back-forward-controls.h}                   |   16 +-
 libide/ide-enums.c.in                              |    1 -
 libide/ide.h                                       |   13 +-
 libide/layout/ide-layout-grid-actions.c            |   71 ++
 libide/layout/ide-layout-grid.c                    |  445 ++++++++++
 libide/layout/ide-layout-grid.h                    |   52 ++
 libide/layout/ide-layout-pane.c                    |   64 ++
 libide/{workbench => layout}/ide-layout-pane.h     |    0
 libide/{workbench => layout}/ide-layout-pane.ui    |    0
 libide/layout/ide-layout-private.h                 |   50 ++
 libide/layout/ide-layout-stack-actions.c           |  175 ++++
 .../{workbench => layout}/ide-layout-stack-addin.c |    0
 .../{workbench => layout}/ide-layout-stack-addin.h |    4 +-
 libide/layout/ide-layout-stack-header.c            |  428 +++++++++
 libide/layout/ide-layout-stack-header.h            |   36 +
 libide/layout/ide-layout-stack-header.ui           |  186 ++++
 libide/layout/ide-layout-stack-shortcuts.c         |  103 +++
 libide/layout/ide-layout-stack.c                   |  823 +++++++++++++++++
 libide/layout/ide-layout-stack.h                   |   67 ++
 libide/layout/ide-layout-stack.ui                  |  137 +++
 libide/layout/ide-layout-view.c                    |  392 +++++++++
 libide/layout/ide-layout-view.h                    |   79 ++
 libide/{workbench => layout}/ide-layout.c          |    0
 libide/{workbench => layout}/ide-layout.h          |    0
 libide/layout/ide-shortcut-label.c                 |  267 ++++++
 libide/layout/ide-shortcut-label.h                 |   43 +
 libide/libide.gresource.xml                        |    9 +-
 libide/meson.build                                 |   45 +-
 libide/workbench/ide-layout-grid.c                 |  927 --------------------
 libide/workbench/ide-layout-grid.h                 |   50 --
 libide/workbench/ide-layout-pane.c                 |  137 ---
 libide/workbench/ide-layout-stack-actions.c        |  318 -------
 libide/workbench/ide-layout-stack-actions.h        |   30 -
 libide/workbench/ide-layout-stack-private.h        |   59 --
 libide/workbench/ide-layout-stack-split.h          |   44 -
 libide/workbench/ide-layout-stack.c                |  682 --------------
 libide/workbench/ide-layout-stack.h                |   45 -
 libide/workbench/ide-layout-stack.ui               |   25 -
 libide/workbench/ide-layout-tab-bar-private.h      |   46 -
 libide/workbench/ide-layout-tab-bar.c              |  420 ---------
 libide/workbench/ide-layout-tab-bar.h              |   36 -
 libide/workbench/ide-layout-tab-bar.ui             |   65 --
 libide/workbench/ide-layout-tab-private.h          |   47 -
 libide/workbench/ide-layout-tab.c                  |  249 ------
 libide/workbench/ide-layout-tab.ui                 |  140 ---
 libide/workbench/ide-layout-view.c                 |  310 -------
 libide/workbench/ide-layout-view.h                 |   81 --
 libide/workbench/ide-workbench.c                   |    7 +-
 56 files changed, 3552 insertions(+), 3793 deletions(-)
---
diff --git a/libide/editor/ide-editor-frame.c b/libide/editor/ide-editor-frame.c
index 661cf42..00f483b 100644
--- a/libide/editor/ide-editor-frame.c
+++ b/libide/editor/ide-editor-frame.c
@@ -33,7 +33,6 @@
 #include "history/ide-back-forward-list.h"
 #include "util/ide-dnd.h"
 #include "util/ide-gtk.h"
-#include "workbench/ide-layout-stack.h"
 #include "workbench/ide-workbench.h"
 
 #define MINIMAP_HIDE_DURATION 1000
diff --git a/libide/editor/ide-editor-layout-stack-addin.h b/libide/editor/ide-editor-layout-stack-addin.h
index 4fbeaf5..69c3a3c 100644
--- a/libide/editor/ide-editor-layout-stack-addin.h
+++ b/libide/editor/ide-editor-layout-stack-addin.h
@@ -21,7 +21,7 @@
 
 #include <gtk/gtk.h>
 
-#include "workbench/ide-layout-stack-addin.h"
+#include "layout/ide-layout-stack-addin.h"
 
 G_BEGIN_DECLS
 
diff --git a/libide/editor/ide-editor-perspective.c b/libide/editor/ide-editor-perspective.c
index 650da92..a6cb7b8 100644
--- a/libide/editor/ide-editor-perspective.c
+++ b/libide/editor/ide-editor-perspective.c
@@ -29,9 +29,9 @@
 #include "editor/ide-editor-perspective.h"
 #include "editor/ide-editor-spell-widget.h"
 #include "editor/ide-editor-view.h"
+#include "layout/ide-layout-grid.h"
+#include "layout/ide-layout-pane.h"
 #include "util/ide-gtk.h"
-#include "workbench/ide-layout-grid.h"
-#include "workbench/ide-layout-pane.h"
 #include "workbench/ide-workbench.h"
 #include "workbench/ide-workbench-header-bar.h"
 
@@ -357,10 +357,7 @@ ide_editor_perspective_add (GtkContainer *container,
 
   if (IDE_IS_LAYOUT_VIEW (widget))
     {
-      GtkWidget *last_focus;
-
-      last_focus = ide_layout_grid_get_last_focus (self->grid);
-      gtk_container_add (GTK_CONTAINER (last_focus), widget);
+      gtk_container_add (GTK_CONTAINER (self->grid), widget);
       g_signal_connect_object (widget,
                                "destroy",
                                G_CALLBACK (ide_editor_perspective_view_destroyed),
@@ -374,21 +371,6 @@ ide_editor_perspective_add (GtkContainer *container,
 }
 
 static void
-ide_editor_perspective_grid_empty (IdeEditorPerspective *self,
-                                   IdeLayoutGrid        *grid)
-{
-  GtkWidget *stack;
-
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (self));
-  g_assert (IDE_IS_LAYOUT_GRID (grid));
-
-  stack = gtk_widget_get_ancestor (GTK_WIDGET (grid), GTK_TYPE_STACK);
-
-  if (stack != NULL)
-    gtk_stack_set_visible_child_name (GTK_STACK (stack), "empty_state");
-}
-
-static void
 ide_editor_perspective_grab_focus (GtkWidget *widget)
 {
   IdeEditorPerspective *self = (IdeEditorPerspective *)widget;
@@ -510,12 +492,6 @@ ide_editor_perspective_init (IdeEditorPerspective *self)
 
   gtk_widget_init_template (GTK_WIDGET (self));
 
-  g_signal_connect_object (self->grid,
-                           "empty",
-                           G_CALLBACK (ide_editor_perspective_grid_empty),
-                           self,
-                           G_CONNECT_SWAPPED);
-
   g_action_map_add_action_entries (G_ACTION_MAP (self->actions), entries,
                                    G_N_ELEMENTS (entries), self);
 
diff --git a/libide/editor/ide-editor-perspective.h b/libide/editor/ide-editor-perspective.h
index a16741c..3af0dd5 100644
--- a/libide/editor/ide-editor-perspective.h
+++ b/libide/editor/ide-editor-perspective.h
@@ -23,8 +23,8 @@
 #include <gtk/gtk.h>
 
 #include "diagnostics/ide-source-location.h"
+#include "layout/ide-layout.h"
 #include "sourceview/ide-source-view.h"
-#include "workbench/ide-layout.h"
 #include "workbench/ide-perspective.h"
 
 G_BEGIN_DECLS
diff --git a/libide/editor/ide-editor-view-private.h b/libide/editor/ide-editor-view-private.h
index 1666bdb..e9fd2be 100644
--- a/libide/editor/ide-editor-view-private.h
+++ b/libide/editor/ide-editor-view-private.h
@@ -26,7 +26,7 @@
 #include "buffers/ide-buffer.h"
 #include "editor/ide-editor-frame.h"
 #include "editor/ide-editor-tweak-widget.h"
-#include "workbench/ide-layout-view.h"
+#include "layout/ide-layout-view.h"
 
 G_BEGIN_DECLS
 
diff --git a/libide/editor/ide-editor-view.c b/libide/editor/ide-editor-view.c
index 9d873ab..c173358 100644
--- a/libide/editor/ide-editor-view.c
+++ b/libide/editor/ide-editor-view.c
@@ -26,6 +26,7 @@
 #include "ide-macros.h"
 
 #include "buffers/ide-buffer-manager.h"
+#include "diagnostics/ide-source-location.h"
 #include "editor/ide-editor-frame-private.h"
 #include "editor/ide-editor-view-actions.h"
 #include "editor/ide-editor-view-addin.h"
diff --git a/libide/editor/ide-editor-view.h b/libide/editor/ide-editor-view.h
index 5e69786..28a4402 100644
--- a/libide/editor/ide-editor-view.h
+++ b/libide/editor/ide-editor-view.h
@@ -20,8 +20,8 @@
 #define IDE_EDITOR_VIEW_H
 
 #include "buffers/ide-buffer.h"
+#include "layout/ide-layout-view.h"
 #include "sourceview/ide-source-view.h"
-#include "workbench/ide-layout-view.h"
 
 G_BEGIN_DECLS
 
diff --git a/libide/history/ide-back-forward-controls.c b/libide/history/ide-back-forward-controls.c
new file mode 100644
index 0000000..92f78c8
--- /dev/null
+++ b/libide/history/ide-back-forward-controls.c
@@ -0,0 +1,81 @@
+/* ide-back-forward-controls.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-back-forward-controls"
+
+#include <dazzle.h>
+
+#include "ide-back-forward-controls.h"
+
+struct _IdeBackForwardControls
+{
+  GtkBox parent_instance;
+
+  GtkButton *previous_button;
+  GtkButton *next_button;
+};
+
+G_DEFINE_TYPE (IdeBackForwardControls, ide_back_forward_controls, GTK_TYPE_BOX)
+
+static void
+ide_back_forward_controls_class_init (IdeBackForwardControlsClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_css_name (widget_class, "idebackforwardcontrols");
+}
+
+static void
+ide_back_forward_controls_init (IdeBackForwardControls *self)
+{
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self), "linked");
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
+
+  self->previous_button = g_object_new (GTK_TYPE_BUTTON,
+                                        "child", g_object_new (GTK_TYPE_IMAGE,
+                                                               "icon-name", "pan-start-symbolic",
+                                                               "visible", TRUE,
+                                                               NULL),
+                                        "visible", TRUE,
+                                        NULL);
+  g_signal_connect (self->previous_button,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->previous_button);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->previous_button));
+
+  self->next_button = g_object_new (GTK_TYPE_BUTTON,
+                                    "child", g_object_new (GTK_TYPE_IMAGE,
+                                                           "icon-name", "pan-end-symbolic",
+                                                           "visible", TRUE,
+                                                           NULL),
+                                    "visible", TRUE,
+                                    NULL);
+  g_signal_connect (self->next_button,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->next_button);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->next_button));
+}
+
+GtkWidget *
+ide_back_forward_controls_new (void)
+{
+  return g_object_new (IDE_TYPE_BACK_FORWARD_CONTROLS, NULL);
+}
diff --git a/libide/workbench/ide-layout-tab.h b/libide/history/ide-back-forward-controls.h
similarity index 63%
rename from libide/workbench/ide-layout-tab.h
rename to libide/history/ide-back-forward-controls.h
index 8179213..608e1a8 100644
--- a/libide/workbench/ide-layout-tab.h
+++ b/libide/history/ide-back-forward-controls.h
@@ -1,6 +1,6 @@
-/* ide-layout-tab.h
+/* ide-back-forward-controls.h
  *
- * Copyright (C) 2015 Christian Hergert <chergert redhat com>
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,20 +16,16 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifndef IDE_LAYOUT_TAB_H
-#define IDE_LAYOUT_TAB_H
+#pragma once
 
 #include <gtk/gtk.h>
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_LAYOUT_TAB (ide_layout_tab_get_type())
+#define IDE_TYPE_BACK_FORWARD_CONTROLS (ide_back_forward_controls_get_type())
 
-G_DECLARE_FINAL_TYPE (IdeLayoutTab, ide_layout_tab, IDE, LAYOUT_TAB, GtkEventBox)
+G_DECLARE_FINAL_TYPE (IdeBackForwardControls, ide_back_forward_controls, IDE, BACK_FORWARD_CONTROLS, GtkBox)
 
-void ide_layout_tab_set_view (IdeLayoutTab *self,
-                              GtkWidget    *view);
+GtkWidget *ide_back_forward_controls_new (void);
 
 G_END_DECLS
-
-#endif /* IDE_LAYOUT_TAB_H */
diff --git a/libide/ide-enums.c.in b/libide/ide-enums.c.in
index f23475a..e1a9c93 100644
--- a/libide/ide-enums.c.in
+++ b/libide/ide-enums.c.in
@@ -19,7 +19,6 @@
 #include "threading/ide-thread-pool.h"
 #include "transfers/ide-transfer.h"
 #include "vcs/ide-vcs-config.h"
-#include "workbench/ide-layout-stack-split.h"
 
 /*** END file-header ***/
 
diff --git a/libide/ide.h b/libide/ide.h
index c4787c7..ef52ccf 100644
--- a/libide/ide.h
+++ b/libide/ide.h
@@ -81,6 +81,7 @@ G_BEGIN_DECLS
 #include "highlighting/ide-highlight-engine.h"
 #include "highlighting/ide-highlight-index.h"
 #include "highlighting/ide-highlighter.h"
+#include "history/ide-back-forward-controls.h"
 #include "history/ide-back-forward-item.h"
 #include "history/ide-back-forward-list.h"
 #include "langserv/ide-langserv-client.h"
@@ -89,6 +90,13 @@ G_BEGIN_DECLS
 #include "langserv/ide-langserv-rename-provider.h"
 #include "langserv/ide-langserv-symbol-resolver.h"
 #include "langserv/ide-langserv-util.h"
+#include "layout/ide-layout-grid.h"
+#include "layout/ide-layout-pane.h"
+#include "layout/ide-layout-stack-addin.h"
+#include "layout/ide-layout-stack-header.h"
+#include "layout/ide-layout-stack.h"
+#include "layout/ide-layout-view.h"
+#include "layout/ide-layout.h"
 #include "local/ide-local-device.h"
 #include "logging/ide-log.h"
 #include "preferences/ide-preferences-addin.h"
@@ -150,11 +158,6 @@ G_BEGIN_DECLS
 #include "vcs/ide-vcs-initializer.h"
 #include "vcs/ide-vcs-uri.h"
 #include "vcs/ide-vcs.h"
-#include "workbench/ide-layout-grid.h"
-#include "workbench/ide-layout-pane.h"
-#include "workbench/ide-layout-stack.h"
-#include "workbench/ide-layout-view.h"
-#include "workbench/ide-layout.h"
 #include "workbench/ide-perspective.h"
 #include "workbench/ide-workbench-addin.h"
 #include "workbench/ide-workbench-message.h"
diff --git a/libide/layout/ide-layout-grid-actions.c b/libide/layout/ide-layout-grid-actions.c
new file mode 100644
index 0000000..5c54009
--- /dev/null
+++ b/libide/layout/ide-layout-grid-actions.c
@@ -0,0 +1,71 @@
+/* ide-layout-grid-actions.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-layout-grid-actions"
+
+#include "ide-layout-private.h"
+
+static void
+ide_layout_grid_action_close_stack (GSimpleAction *action,
+                                    GVariant      *variant,
+                                    gpointer       user_data)
+{
+  IdeLayoutGrid *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+
+  /*
+   * Clicking the close button should have caused the stack to become the
+   * current stack, so we can rely on that.
+   */
+
+  _ide_layout_grid_close_current_stack (self);
+}
+
+static const GActionEntry grid_actions[] = {
+  { "close-stack", ide_layout_grid_action_close_stack },
+};
+
+void
+_ide_layout_grid_init_actions (IdeLayoutGrid *self)
+{
+  g_autoptr(GSimpleActionGroup) group = NULL;
+
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   grid_actions,
+                                   G_N_ELEMENTS (grid_actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "layoutgrid", G_ACTION_GROUP (group));
+}
+
+void
+_ide_layout_grid_update_actions (IdeLayoutGrid *self)
+{
+  gboolean enabled;
+
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+
+  enabled = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)) > 1;
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "layoutgrid", "close-stack",
+                             "enabled", enabled,
+                             NULL);
+}
diff --git a/libide/layout/ide-layout-grid.c b/libide/layout/ide-layout-grid.c
new file mode 100644
index 0000000..e571159
--- /dev/null
+++ b/libide/layout/ide-layout-grid.c
@@ -0,0 +1,445 @@
+/* ide-layout-grid.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-layout-grid"
+
+#include "ide-layout-grid.h"
+#include "ide-layout-private.h"
+
+typedef struct
+{
+  DzlSignalGroup *toplevel_signals;
+  GQueue          focus_stack;
+} IdeLayoutGridPrivate;
+
+enum {
+  PROP_0,
+  PROP_CURRENT_STACK,
+  N_PROPS
+};
+
+enum {
+  CREATE_STACK,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeLayoutGrid, ide_layout_grid, DZL_TYPE_MULTI_PANED)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static IdeLayoutStack *
+ide_layout_grid_real_create_stack (IdeLayoutGrid *self)
+{
+  return g_object_new (IDE_TYPE_LAYOUT_STACK,
+                       "expand", TRUE,
+                       "visible", TRUE,
+                       NULL);
+}
+
+static GtkWidget *
+ide_layout_grid_create_stack (IdeLayoutGrid *self)
+{
+  IdeLayoutStack *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+  g_signal_emit (self, signals [CREATE_STACK], 0, &ret);
+  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (ret), NULL);
+
+  return GTK_WIDGET (ret);
+}
+
+static void
+ide_layout_grid_after_set_focus (IdeLayoutGrid *self,
+                                 GtkWidget     *widget,
+                                 GtkWidget     *toplevel)
+{
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (!widget || GTK_IS_WIDGET (widget));
+  g_assert (GTK_IS_WINDOW (toplevel));
+
+  if (widget != NULL)
+    {
+      if (gtk_widget_is_ancestor (widget, GTK_WIDGET (self)))
+        {
+          GtkWidget *stack = gtk_widget_get_ancestor (widget, IDE_TYPE_LAYOUT_STACK);
+
+          if (stack != NULL)
+            ide_layout_grid_set_current_stack (self, IDE_LAYOUT_STACK (stack));
+        }
+    }
+}
+
+static void
+ide_layout_grid_hierarchy_changed (GtkWidget *widget,
+                                   GtkWidget *old_toplevel)
+{
+  IdeLayoutGrid *self = (IdeLayoutGrid *)widget;
+  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));
+
+  /*
+   * Setup focus tracking so that we can update our "current stack" when the
+   * user selected focus changes.
+   */
+
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  if (GTK_IS_WINDOW (toplevel))
+    dzl_signal_group_set_target (priv->toplevel_signals, toplevel);
+
+  /*
+   * If we've been added to a widget and still do not have a stack added, then
+   * we'll emit our ::create-stack signal to create that now. We do this here
+   * to allow the consumer to connect to ::create-stack before adding the
+   * widget to the hierarchy.
+   */
+
+  if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (widget)) == 0)
+    {
+      GtkWidget *stack = ide_layout_grid_create_stack (self);
+
+      if (stack != NULL)
+        gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (stack));
+    }
+}
+
+static void
+ide_layout_grid_add (GtkContainer *container,
+                     GtkWidget    *widget)
+{
+  IdeLayoutGrid *self = (IdeLayoutGrid *)container;
+
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  if (IDE_IS_LAYOUT_STACK (widget))
+    {
+      GTK_CONTAINER_CLASS (ide_layout_grid_parent_class)->add (container, widget);
+      ide_layout_grid_set_current_stack (self, IDE_LAYOUT_STACK (widget));
+    }
+  else if (IDE_IS_LAYOUT_VIEW (widget))
+    {
+      IdeLayoutStack *stack = NULL;
+      guint n_stacks;
+
+      /* If we have an empty layout stack, we'll prefer to add the
+       * view to that. If we don't find an empty stack, we'll add
+       * the view to the most recently focused stack.
+       */
+
+      n_stacks = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
+
+      for (guint i = 0; i < n_stacks; i++)
+        {
+          GtkWidget *ele = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i);
+
+         g_assert (IDE_IS_LAYOUT_STACK (ele));
+
+          if (!ide_layout_stack_get_has_view (IDE_LAYOUT_STACK (ele)))
+            {
+              stack = IDE_LAYOUT_STACK (ele);
+              break;
+            }
+        }
+
+      if (stack == NULL)
+        stack = ide_layout_grid_get_current_stack (self);
+
+      g_assert (IDE_IS_LAYOUT_STACK (stack));
+
+      gtk_container_add (GTK_CONTAINER (stack), widget);
+    }
+  else
+    {
+      g_warning ("%s only accepts IdeLayoutStack or IdeLayoutView widgets",
+                 G_OBJECT_TYPE_NAME (self));
+      return;
+    }
+
+  _ide_layout_grid_update_actions (self);
+}
+
+static void
+ide_layout_grid_remove (GtkContainer *container,
+                        GtkWidget    *widget)
+{
+  IdeLayoutGrid *self = (IdeLayoutGrid *)container;
+  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  g_queue_remove (&priv->focus_stack, widget);
+
+  GTK_CONTAINER_CLASS (ide_layout_grid_parent_class)->remove (container, widget);
+
+  _ide_layout_grid_update_actions (self);
+}
+
+static void
+ide_layout_grid_destroy (GtkWidget *widget)
+{
+  IdeLayoutGrid *self = (IdeLayoutGrid *)widget;
+  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+
+  g_queue_clear (&priv->focus_stack);
+  g_clear_object (&priv->toplevel_signals);
+
+  GTK_WIDGET_CLASS (ide_layout_grid_parent_class)->destroy (widget);
+}
+
+static void
+ide_layout_grid_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeLayoutGrid *self = IDE_LAYOUT_GRID (object);
+
+  switch (prop_id)
+    {
+    case PROP_CURRENT_STACK:
+      g_value_set_object (value, ide_layout_grid_get_current_stack (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_layout_grid_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeLayoutGrid *self = IDE_LAYOUT_GRID (object);
+
+  switch (prop_id)
+    {
+    case PROP_CURRENT_STACK:
+      ide_layout_grid_set_current_stack (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_layout_grid_class_init (IdeLayoutGridClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->get_property = ide_layout_grid_get_property;
+  object_class->set_property = ide_layout_grid_set_property;
+
+  widget_class->destroy = ide_layout_grid_destroy;
+  widget_class->hierarchy_changed = ide_layout_grid_hierarchy_changed;
+
+  container_class->add = ide_layout_grid_add;
+  container_class->remove = ide_layout_grid_remove;
+
+  klass->create_stack = ide_layout_grid_real_create_stack;
+
+  properties [PROP_CURRENT_STACK] =
+    g_param_spec_object ("current-stack",
+                         "Current Stack",
+                        "The most recent focused IdeLayoutStack",
+                        IDE_TYPE_LAYOUT_STACK,
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "idelayoutgrid");
+
+  signals [CREATE_STACK] =
+    g_signal_new ("create-stack",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeLayoutGridClass, create_stack),
+                  g_signal_accumulator_first_wins, NULL, NULL,
+                  IDE_TYPE_LAYOUT_STACK, 0);
+}
+
+static void
+ide_layout_grid_init (IdeLayoutGrid *self)
+{
+  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+
+  _ide_layout_grid_init_actions (self);
+
+  priv->toplevel_signals = dzl_signal_group_new (GTK_TYPE_WINDOW);
+
+  dzl_signal_group_connect_object (priv->toplevel_signals,
+                                   "set-focus",
+                                   G_CALLBACK (ide_layout_grid_after_set_focus),
+                                   self,
+                                   G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+}
+
+GtkWidget *
+ide_layout_grid_new (void)
+{
+  return g_object_new (IDE_TYPE_LAYOUT_GRID, NULL);
+}
+
+/**
+ * ide_layout_grid_get_current_stack:
+ * @self: a #IdeLayoutGrid
+ *
+ * Gets the most recently focused stack. This is useful when you want to open
+ * a document on the stack the user last focused.
+ *
+ * Returns: (transfer none) (nullable): A #IdeLayoutStack or %NULL.
+ *
+ * Since: 3.26
+ */
+IdeLayoutStack *
+ide_layout_grid_get_current_stack (IdeLayoutGrid *self)
+{
+  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+
+  return priv->focus_stack.head ? priv->focus_stack.head->data : NULL;
+}
+
+void
+ide_layout_grid_set_current_stack (IdeLayoutGrid  *self,
+                                   IdeLayoutStack *stack)
+{
+  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+  g_return_if_fail (!stack || IDE_IS_LAYOUT_STACK (stack));
+
+  if (stack == NULL)
+    return;
+
+  g_queue_remove (&priv->focus_stack, stack);
+
+  if (gtk_widget_get_parent (GTK_WIDGET (stack)) != GTK_WIDGET (self))
+    {
+      g_warning ("Attempt to set stack focus to stack not owned by this grid.");
+      return;
+    }
+
+  g_queue_push_head (&priv->focus_stack, stack);
+}
+
+static void
+ide_layout_grid_stack_close_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeLayoutStack *stack = (IdeLayoutStack *)object;
+  g_autoptr(IdeLayoutGrid) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_LAYOUT_STACK (stack));
+  g_assert (IDE_IS_LAYOUT_GRID (self));
+
+  /* If the stack says we cannot close now, ignore the request */
+  if (!ide_layout_stack_agree_to_close_finish (stack, result, &error))
+    {
+      g_debug ("Cannot close stack: %s", error->message);
+      return;
+    }
+
+  /*
+   * State could have changed during the async operation. Make sure that
+   * this is not the last stack before we attempt to remove it.
+   */
+
+  if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)) == 1)
+    {
+      g_debug ("Cannot remove last stack from grid");
+      return;
+    }
+
+  /* It's okay to close this stack now */
+  gtk_widget_destroy (GTK_WIDGET (stack));
+}
+
+void
+_ide_layout_grid_close_current_stack (IdeLayoutGrid *self)
+{
+  IdeLayoutGridPrivate *priv = ide_layout_grid_get_instance_private (self);
+  IdeLayoutStack *head;
+
+  g_return_if_fail (IDE_IS_LAYOUT_GRID (self));
+
+  if (priv->focus_stack.head == NULL)
+    return;
+
+  head = priv->focus_stack.head->data;
+  g_assert (IDE_IS_LAYOUT_STACK (head));
+
+  ide_layout_stack_agree_to_close_async (head,
+                                         NULL,
+                                         ide_layout_grid_stack_close_cb,
+                                         g_object_ref (self));
+}
+
+/*
+ * _ide_layout_grid_get_nth_stack:
+ *
+ * This will get the @nth stack. If it does not yet exist,
+ * it will be created.
+ *
+ * If nth == -1, a new stack will be created at index 0.
+ *
+ * If nth >= the number of stacks, a new stack will be created
+ * at the end of the grid.
+ */
+IdeLayoutStack *
+_ide_layout_grid_get_nth_stack (IdeLayoutGrid *self,
+                                gint           nth)
+{
+  GtkWidget *stack;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_GRID (self), NULL);
+
+  if (nth < 0)
+    {
+      stack = ide_layout_grid_create_stack (self);
+      gtk_container_add_with_properties (GTK_CONTAINER (self), stack,
+                                         "index", 0,
+                                         NULL);
+    }
+  else if (nth >= dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)))
+    {
+      stack = ide_layout_grid_create_stack (self);
+      gtk_container_add (GTK_CONTAINER (self), stack);
+    }
+  else
+    {
+      stack = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), nth);
+    }
+
+  return IDE_LAYOUT_STACK (stack);
+}
diff --git a/libide/layout/ide-layout-grid.h b/libide/layout/ide-layout-grid.h
new file mode 100644
index 0000000..ece1ef0
--- /dev/null
+++ b/libide/layout/ide-layout-grid.h
@@ -0,0 +1,52 @@
+/* ide-layout-grid.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <dazzle.h>
+
+#include "ide-layout-stack.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LAYOUT_GRID (ide_layout_grid_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (IdeLayoutGrid, ide_layout_grid, IDE, LAYOUT_GRID, DzlMultiPaned)
+
+struct _IdeLayoutGridClass
+{
+  DzlMultiPanedClass parent_class;
+
+  IdeLayoutStack *(*create_stack) (IdeLayoutGrid *self);
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+};
+
+GtkWidget      *ide_layout_grid_new               (void);
+IdeLayoutStack *ide_layout_grid_get_current_stack (IdeLayoutGrid  *self);
+void            ide_layout_grid_set_current_stack (IdeLayoutGrid  *self,
+                                                   IdeLayoutStack *stack);
+
+G_END_DECLS
diff --git a/libide/layout/ide-layout-pane.c b/libide/layout/ide-layout-pane.c
new file mode 100644
index 0000000..f1ca4ef
--- /dev/null
+++ b/libide/layout/ide-layout-pane.c
@@ -0,0 +1,64 @@
+/* ide-layout-pane.c
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <glib/gi18n.h>
+
+#include "dazzle.h"
+
+#include "ide-layout-pane.h"
+#include "ide-macros.h"
+
+struct _IdeLayoutPane
+{
+  DzlDockBinEdge  parent_instance;
+  DzlDockStack   *dock_stack;
+};
+
+G_DEFINE_TYPE (IdeLayoutPane, ide_layout_pane, DZL_TYPE_DOCK_BIN_EDGE)
+
+static void
+ide_layout_pane_add (GtkContainer *container,
+                     GtkWidget    *widget)
+{
+  IdeLayoutPane *self = (IdeLayoutPane *)container;
+
+  g_assert (IDE_IS_LAYOUT_PANE (self));
+
+  if (DZL_IS_DOCK_WIDGET (widget))
+    gtk_container_add (GTK_CONTAINER (self->dock_stack), widget);
+  else
+    GTK_CONTAINER_CLASS (ide_layout_pane_parent_class)->add (container, widget);
+}
+
+static void
+ide_layout_pane_class_init (IdeLayoutPaneClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  container_class->add = ide_layout_pane_add;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/ide-layout-pane.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutPane, dock_stack);
+}
+
+static void
+ide_layout_pane_init (IdeLayoutPane *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/libide/workbench/ide-layout-pane.h b/libide/layout/ide-layout-pane.h
similarity index 100%
rename from libide/workbench/ide-layout-pane.h
rename to libide/layout/ide-layout-pane.h
diff --git a/libide/workbench/ide-layout-pane.ui b/libide/layout/ide-layout-pane.ui
similarity index 100%
rename from libide/workbench/ide-layout-pane.ui
rename to libide/layout/ide-layout-pane.ui
diff --git a/libide/layout/ide-layout-private.h b/libide/layout/ide-layout-private.h
new file mode 100644
index 0000000..43ce44e
--- /dev/null
+++ b/libide/layout/ide-layout-private.h
@@ -0,0 +1,50 @@
+/* ide-layout-private.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "ide-layout-grid.h"
+#include "ide-layout-stack.h"
+#include "ide-layout-stack-header.h"
+#include "ide-layout-view.h"
+
+G_BEGIN_DECLS
+
+void            _ide_layout_stack_init_actions       (IdeLayoutStack       *self);
+void            _ide_layout_stack_init_shortcuts     (IdeLayoutStack       *self);
+void            _ide_layout_stack_update_actions     (IdeLayoutStack       *self);
+void            _ide_layout_stack_transfer           (IdeLayoutStack       *self,
+                                                      IdeLayoutStack       *dest,
+                                                      IdeLayoutView        *view);
+void            _ide_layout_grid_init_actions        (IdeLayoutGrid        *self);
+void            _ide_layout_grid_update_actions      (IdeLayoutGrid        *self);
+void            _ide_layout_grid_close_current_stack (IdeLayoutGrid        *self);
+IdeLayoutStack *_ide_layout_grid_get_nth_stack       (IdeLayoutGrid        *self,
+                                                      gint                  nth);
+void            _ide_layout_stack_request_close      (IdeLayoutStack       *stack,
+                                                      IdeLayoutView        *view);
+void            _ide_layout_stack_header_update      (IdeLayoutStackHeader *self,
+                                                      IdeLayoutView        *view);
+void            _ide_layout_stack_header_hide        (IdeLayoutStackHeader *self);
+void            _ide_layout_stack_header_popdown     (IdeLayoutStackHeader *self);
+void            _ide_layout_stack_header_set_views   (IdeLayoutStackHeader *self,
+                                                      GListModel           *model);
+
+G_END_DECLS
diff --git a/libide/layout/ide-layout-stack-actions.c b/libide/layout/ide-layout-stack-actions.c
new file mode 100644
index 0000000..05c8e79
--- /dev/null
+++ b/libide/layout/ide-layout-stack-actions.c
@@ -0,0 +1,175 @@
+/* ide-layout-stack-actions.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ide-layout-stack.h"
+#include "ide-layout-private.h"
+
+static void
+ide_layout_stack_actions_next_view (GSimpleAction *action,
+                                    GVariant      *variant,
+                                    gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  g_signal_emit_by_name (self, "change-current-page", 1);
+}
+
+static void
+ide_layout_stack_actions_previous_view (GSimpleAction *action,
+                                        GVariant      *variant,
+                                        gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  g_signal_emit_by_name (self, "change-current-page", -1);
+}
+
+static void
+ide_layout_stack_actions_close_view (GSimpleAction *action,
+                                     GVariant      *variant,
+                                     gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+  IdeLayoutView *view;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  view = ide_layout_stack_get_visible_child (self);
+  if (view != NULL)
+    _ide_layout_stack_request_close (self, view);
+}
+
+static void
+ide_layout_stack_action_move_right (GSimpleAction *action,
+                                    GVariant      *variant,
+                                    gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+  IdeLayoutView *view;
+  IdeLayoutStack *dest;
+  GtkWidget *grid;
+  gint index = 0;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  view = ide_layout_stack_get_visible_child (self);
+
+  g_return_if_fail (view != NULL);
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (view));
+
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_GRID);
+
+  g_return_if_fail (grid != NULL);
+  g_return_if_fail (IDE_IS_LAYOUT_GRID (grid));
+
+  gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (self),
+                           "index", &index,
+                           NULL);
+
+  dest = _ide_layout_grid_get_nth_stack (IDE_LAYOUT_GRID (grid), ++index);
+
+  g_return_if_fail (dest != NULL);
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (dest));
+
+  _ide_layout_stack_transfer (self, dest, view);
+}
+
+static void
+ide_layout_stack_action_move_left (GSimpleAction *action,
+                                   GVariant      *variant,
+                                   gpointer       user_data)
+{
+  IdeLayoutStack *self = user_data;
+  IdeLayoutView *view;
+  IdeLayoutStack *dest;
+  GtkWidget *grid;
+  gint index = 0;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  view = ide_layout_stack_get_visible_child (self);
+
+  g_return_if_fail (view != NULL);
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (view));
+
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_GRID);
+
+  g_return_if_fail (grid != NULL);
+  g_return_if_fail (IDE_IS_LAYOUT_GRID (grid));
+
+  gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (self),
+                           "index", &index,
+                           NULL);
+
+  dest = _ide_layout_grid_get_nth_stack (IDE_LAYOUT_GRID (grid), --index);
+
+  g_return_if_fail (dest != NULL);
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (dest));
+
+  _ide_layout_stack_transfer (self, dest, view);
+}
+
+static const GActionEntry actions[] = {
+  { "close-view",    ide_layout_stack_actions_close_view },
+  { "next-view",     ide_layout_stack_actions_next_view },
+  { "previous-view", ide_layout_stack_actions_previous_view },
+  { "move-right",    ide_layout_stack_action_move_right },
+  { "move-left",     ide_layout_stack_action_move_left },
+};
+
+void
+_ide_layout_stack_update_actions (IdeLayoutStack *self)
+{
+  gboolean has_view;
+
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+
+  has_view = ide_layout_stack_get_has_view (self);
+
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "layoutstack", "move-right",
+                             "enabled", has_view,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "layoutstack", "move-left",
+                             "enabled", has_view,
+                             NULL);
+}
+
+void
+_ide_layout_stack_init_actions (IdeLayoutStack *self)
+{
+  g_autoptr(GSimpleActionGroup) group = NULL;
+
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self),
+                                  "layoutstack",
+                                  G_ACTION_GROUP (group));
+
+  _ide_layout_stack_update_actions (self);
+}
diff --git a/libide/workbench/ide-layout-stack-addin.c b/libide/layout/ide-layout-stack-addin.c
similarity index 100%
rename from libide/workbench/ide-layout-stack-addin.c
rename to libide/layout/ide-layout-stack-addin.c
diff --git a/libide/workbench/ide-layout-stack-addin.h b/libide/layout/ide-layout-stack-addin.h
similarity index 95%
rename from libide/workbench/ide-layout-stack-addin.h
rename to libide/layout/ide-layout-stack-addin.h
index 9ee8ca2..e021760 100644
--- a/libide/workbench/ide-layout-stack-addin.h
+++ b/libide/layout/ide-layout-stack-addin.h
@@ -21,8 +21,8 @@
 
 #include <gtk/gtk.h>
 
-#include "workbench/ide-layout-stack.h"
-#include "workbench/ide-layout-view.h"
+#include "layout/ide-layout-stack.h"
+#include "layout/ide-layout-view.h"
 
 G_BEGIN_DECLS
 
diff --git a/libide/layout/ide-layout-stack-header.c b/libide/layout/ide-layout-stack-header.c
new file mode 100644
index 0000000..76145c5
--- /dev/null
+++ b/libide/layout/ide-layout-stack-header.c
@@ -0,0 +1,428 @@
+/* ide-layout-stack-header.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-layout-stack-header"
+
+#include <glib/gi18n.h>
+
+#include "ide-layout-private.h"
+#include "ide-layout-stack-header.h"
+
+struct _IdeLayoutStackHeader
+{
+  DzlPriorityBox  parent_instance;
+
+  GtkButton      *close_button;
+  GtkMenuButton  *document_button;
+  GtkPopover     *document_popover;
+  GtkMenuButton  *title_button;
+  GtkPopover     *title_popover;
+  GtkListBox     *title_list_box;
+  DzlPriorityBox *title_box;
+  GtkLabel       *title_label;
+  GtkLabel       *title_modified;
+  GtkBox         *title_views_box;
+
+  DzlJoinedMenu  *menu;
+};
+
+enum {
+  PROP_0,
+  PROP_MODIFIED,
+  PROP_SHOW_CLOSE_BUTTON,
+  PROP_TITLE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeLayoutStackHeader, ide_layout_stack_header, DZL_TYPE_PRIORITY_BOX)
+
+static GParamSpec *properties [N_PROPS];
+
+void
+_ide_layout_stack_header_hide (IdeLayoutStackHeader *self)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_STACK_HEADER (self));
+
+  /* This is like _ide_layout_stack_header_popdown() but we hide the
+   * popovers immediately without performing the popdown animation.
+   */
+
+  gtk_widget_hide (GTK_WIDGET (self->document_popover));
+  gtk_widget_hide (GTK_WIDGET (self->title_popover));
+}
+
+void
+_ide_layout_stack_header_popdown (IdeLayoutStackHeader *self)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_STACK_HEADER (self));
+
+  gtk_popover_popdown (self->document_popover);
+  gtk_popover_popdown (self->title_popover);
+}
+
+void
+_ide_layout_stack_header_update (IdeLayoutStackHeader *self,
+                                 IdeLayoutView        *view)
+{
+  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (!view || IDE_IS_LAYOUT_VIEW (view));
+
+  /*
+   * Update our menus for the document to include the menu type needed for the
+   * newly focused view. Make sure we keep the Frame section at the end which
+   * is always the last section in the joined menus.
+   */
+
+  while (dzl_joined_menu_get_n_joined (self->menu) > 1)
+    dzl_joined_menu_remove_index (self->menu, 0);
+
+  if (view != NULL)
+    {
+      const gchar *menu_id = ide_layout_view_get_menu_id (view);
+
+      if (menu_id != NULL)
+        {
+          GMenu *menu = dzl_application_get_menu_by_id (DZL_APPLICATION_DEFAULT, menu_id);
+
+          dzl_joined_menu_prepend_menu (self->menu, G_MENU_MODEL (menu));
+        }
+    }
+
+  /*
+   * Hide the document selectors if there are no views to select (which is
+   * indicated by us having a NULL view here.
+   */
+  gtk_widget_set_visible (GTK_WIDGET (self->title_views_box), view != NULL);
+
+  /*
+   * The close button acts differently when there are no views displayed. It
+   * closes the frame instead. We need to update the action name for that.
+   */
+
+  gtk_actionable_set_action_name (GTK_ACTIONABLE (self->close_button),
+                                  view ? "layoutstack.close-view" : "layoutgrid.close-stack");
+
+  /*
+   * Hide any popovers that we know about. If we got here from closing
+   * documents, we should hide the popover after the last document is closed
+   * (inidicated by NULL view).
+   */
+  if (view == NULL)
+    _ide_layout_stack_header_popdown (self);
+}
+
+static void
+close_view_cb (GtkButton            *button,
+               IdeLayoutStackHeader *self)
+{
+  GtkWidget *stack;
+  GtkWidget *row;
+  GtkWidget *view;
+
+  g_assert (GTK_IS_BUTTON (button));
+  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+
+  row = gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_LIST_BOX_ROW);
+  if (row == NULL)
+    return;
+
+  view = g_object_get_data (G_OBJECT (row), "IDE_LAYOUT_VIEW");
+  if (view == NULL)
+    return;
+
+  stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_STACK);
+  if (stack == NULL)
+    return;
+
+  _ide_layout_stack_request_close (IDE_LAYOUT_STACK (stack), IDE_LAYOUT_VIEW (view));
+}
+
+static GtkWidget *
+create_document_row (gpointer item,
+                     gpointer user_data)
+{
+  IdeLayoutStackHeader *self = user_data;
+  GtkListBoxRow *row;
+  GtkButton *close_button;
+  GtkLabel *label;
+  GtkImage *image;
+  GtkBox *box;
+
+  g_assert (IDE_IS_LAYOUT_VIEW (item));
+  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "visible", TRUE,
+                      NULL);
+  box = g_object_new (GTK_TYPE_BOX,
+                      "spacing", 6,
+                      "visible", TRUE,
+                      NULL);
+  image = g_object_new (GTK_TYPE_IMAGE,
+                        "icon-size", GTK_ICON_SIZE_MENU,
+                        "visible", TRUE,
+                        NULL);
+  label = g_object_new (DZL_TYPE_BOLDING_LABEL,
+                        "hexpand", TRUE,
+                        "xalign", 0.0f,
+                        "visible", TRUE,
+                        NULL);
+  close_button = g_object_new (GTK_TYPE_BUTTON,
+                               "child", g_object_new (GTK_TYPE_IMAGE,
+                                                      "icon-name", "window-close-symbolic",
+                                                      "visible", TRUE,
+                                                      NULL),
+                               "visible", TRUE,
+                               NULL);
+  g_signal_connect (close_button,
+                    "clicked",
+                    G_CALLBACK (close_view_cb),
+                    self);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (close_button), "image-button");
+
+  g_object_bind_property (item, "icon-name", image, "icon-name", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (item, "modified", label, "bold", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (item, "title", label, "label", G_BINDING_SYNC_CREATE);
+  g_object_set_data (G_OBJECT (row), "IDE_LAYOUT_VIEW", item);
+
+  gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (box));
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (image));
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (label));
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (close_button));
+
+  return GTK_WIDGET (row);
+}
+
+void
+_ide_layout_stack_header_set_views (IdeLayoutStackHeader *self,
+                                    GListModel           *model)
+{
+  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_assert (!model || G_IS_LIST_MODEL (model));
+
+  gtk_list_box_bind_model (self->title_list_box,
+                           model,
+                           create_document_row,
+                           self, NULL);
+}
+
+static void
+ide_layout_stack_header_view_row_activated (GtkListBox           *list_box,
+                                            GtkListBoxRow        *row,
+                                            IdeLayoutStackHeader *self)
+{
+  GtkWidget *stack;
+  GtkWidget *view;
+
+  g_assert (GTK_IS_LIST_BOX (list_box));
+  g_assert (GTK_IS_LIST_BOX_ROW (row));
+  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+
+  stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_STACK);
+  view = g_object_get_data (G_OBJECT (row), "IDE_LAYOUT_VIEW");
+
+  if (stack != NULL && view != NULL)
+    ide_layout_stack_set_visible_child (IDE_LAYOUT_STACK (stack),
+                                        IDE_LAYOUT_VIEW (view));
+}
+
+static void
+ide_layout_stack_header_destroy (GtkWidget *widget)
+{
+  IdeLayoutStackHeader *self = (IdeLayoutStackHeader *)widget;
+
+  g_assert (IDE_IS_LAYOUT_STACK_HEADER (self));
+
+  g_clear_object (&self->menu);
+
+  GTK_WIDGET_CLASS (ide_layout_stack_header_parent_class)->destroy (widget);
+}
+
+static void
+ide_layout_stack_header_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeLayoutStackHeader *self = IDE_LAYOUT_STACK_HEADER (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODIFIED:
+      g_value_set_boolean (value, gtk_widget_get_visible (GTK_WIDGET (self->title_modified)));
+      break;
+
+    case PROP_SHOW_CLOSE_BUTTON:
+      g_value_set_boolean (value, gtk_widget_get_visible (GTK_WIDGET (self->close_button)));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, gtk_label_get_label (GTK_LABEL (self->title_label)));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_layout_stack_header_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  IdeLayoutStackHeader *self = IDE_LAYOUT_STACK_HEADER (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODIFIED:
+      gtk_widget_set_visible (GTK_WIDGET (self->title_modified), g_value_get_boolean (value));
+      break;
+
+    case PROP_SHOW_CLOSE_BUTTON:
+      gtk_widget_set_visible (GTK_WIDGET (self->close_button), g_value_get_boolean (value));
+      break;
+
+    case PROP_TITLE:
+      ide_layout_stack_header_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_layout_stack_header_class_init (IdeLayoutStackHeaderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = ide_layout_stack_header_get_property;
+  object_class->set_property = ide_layout_stack_header_set_property;
+
+  widget_class->destroy = ide_layout_stack_header_destroy;
+
+  properties [PROP_SHOW_CLOSE_BUTTON] =
+    g_param_spec_boolean ("show-close-button",
+                          "Show Close Button",
+                          "If the close button should be displayed",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MODIFIED] =
+    g_param_spec_boolean ("modified",
+                          "Modified",
+                          "If the current document is modified",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the current document or view",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "idelayoutstackheader");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-layout-stack-header.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, close_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, document_popover);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, document_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_list_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_modified);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_popover);
+  gtk_widget_class_bind_template_child (widget_class, IdeLayoutStackHeader, title_views_box);
+}
+
+static void
+ide_layout_stack_header_init (IdeLayoutStackHeader *self)
+{
+  GMenu *frame_section;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  /*
+   * Create our menu for the document controls popover. It has two sections.
+   * The top section is based on the document and is updated whenever the
+   * visible child is changed. The bottom, are the frame controls are and
+   * static, but setup by us here.
+   */
+
+  self->menu = dzl_joined_menu_new ();
+  gtk_popover_bind_model (self->document_popover, G_MENU_MODEL (self->menu), NULL);
+  frame_section = dzl_application_get_menu_by_id (DZL_APPLICATION_DEFAULT, "ide-layout-stack-frame-menu");
+  dzl_joined_menu_append_menu (self->menu, G_MENU_MODEL (frame_section));
+
+  /*
+   * When a row is selected, we want to change the current view and
+   * hide the popover.
+   */
+
+  g_signal_connect (self->title_list_box,
+                    "row-activated",
+                    G_CALLBACK (ide_layout_stack_header_view_row_activated),
+                    self);
+}
+
+GtkWidget *
+ide_layout_stack_header_new (void)
+{
+  return g_object_new (IDE_TYPE_LAYOUT_STACK_HEADER, NULL);
+}
+
+/**
+ * ide_layout_stack_header_add_custom_title:
+ * @self: a #IdeLayoutStackHeader
+ * @widget: A #GtkWidget
+ *
+ * This will add @widget to the title area with @priority determining the
+ * sort order of the child.
+ *
+ * All "title" widgets in the #IdeLayoutStackHeader are expanded to the
+ * same size. So if you don't need that, you should just use the normal
+ * gtk_container_add_with_properties() API to specify your widget with
+ * a given priority.
+ */
+void
+ide_layout_stack_header_add_custom_title (IdeLayoutStackHeader *self,
+                                          GtkWidget            *widget,
+                                          gint                  priority)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_STACK_HEADER (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->title_box), widget,
+                                     "priority", priority,
+                                     NULL);
+}
+
+void
+ide_layout_stack_header_set_title (IdeLayoutStackHeader *self,
+                                   const gchar          *title)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_STACK_HEADER (self));
+
+  gtk_label_set_label (GTK_LABEL (self->title_label), title);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+}
diff --git a/libide/layout/ide-layout-stack-header.h b/libide/layout/ide-layout-stack-header.h
new file mode 100644
index 0000000..15ea9f0
--- /dev/null
+++ b/libide/layout/ide-layout-stack-header.h
@@ -0,0 +1,36 @@
+/* ide-layout-stack-header.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <dazzle.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LAYOUT_STACK_HEADER (ide_layout_stack_header_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeLayoutStackHeader, ide_layout_stack_header, IDE, LAYOUT_STACK_HEADER, 
DzlPriorityBox)
+
+GtkWidget *ide_layout_stack_header_new              (void);
+void       ide_layout_stack_header_set_title        (IdeLayoutStackHeader *self,
+                                                     const gchar          *title);
+void       ide_layout_stack_header_add_custom_title (IdeLayoutStackHeader *self,
+                                                     GtkWidget            *custom_title,
+                                                     gint                  priority);
+
+G_END_DECLS
diff --git a/libide/layout/ide-layout-stack-header.ui b/libide/layout/ide-layout-stack-header.ui
new file mode 100644
index 0000000..24c6fc5
--- /dev/null
+++ b/libide/layout/ide-layout-stack-header.ui
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkPopover" id="document_popover">
+    <property name="width-request">250</property>
+    <property name="border-width">10</property>
+    <style>
+      <class name="document-popover"/>
+    </style>
+  </object>
+  <object class="GtkPopover" id="title_popover">
+    <property name="width-request">350</property>
+    <property name="border-width">18</property>
+    <style>
+      <class name="title-popover"/>
+    </style>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="visible">true</property>
+        <property name="spacing">12</property>
+        <child>
+          <object class="GtkBox" id="title_views_box">
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+            <property name="spacing">12</property>
+            <child>
+              <object class="GtkLabel">
+                <property name="label" translatable="yes">Open Pages</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow">
+                <property name="propagate-natural-height">true</property>
+                <property name="propagate-natural-width">true</property>
+                <property name="max-content-height">600</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkListBox" id="title_list_box">
+                    <property name="selection-mode">none</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="homogeneous">true</property>
+            <property name="spacing">6</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkButton">
+                <property name="action-name">editor.open-file</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="image-button"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">document-open-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="visible">true</property>
+                <property name="action-name">editor.new-file</property>
+                <style>
+                  <class name="image-button"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">document-new-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="action-name">editor.new-terminal</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="image-button"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">utilities-terminal-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="action-name">win.new-devhelp</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="image-button"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">devhelp-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+  <template class="IdeLayoutStackHeader" parent="DzlPriorityBox">
+    <child>
+      <object class="DzlPriorityBox" id="title_box">
+        <property name="hexpand">true</property>
+        <property name="homogeneous">true</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkMenuButton" id="title_button">
+            <property name="popover">title_popover</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">true</property>
+                <child type="center">
+                  <object class="GtkLabel" id="title_label">
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="title_modified">
+                    <property name="halign">start</property>
+                    <property name="hexpand">true</property>
+                    <property name="margin-start">8</property>
+                    <property name="margin-end">8</property>
+                    <property name="label">ā€¢</property>
+                  </object>
+                  <packing>
+                    <property name="pack-type">end</property>
+                  </packing>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="close_button">
+        <property name="action-name">layoutgrid.close-stack</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkImage">
+            <property name="icon-name">window-close-symbolic</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="pack-type">end</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkMenuButton" id="document_button">
+        <property name="focus-on-click">false</property>
+        <property name="popover">document_popover</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkImage">
+            <property name="icon-name">pan-down-symbolic</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="pack-type">end</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/libide/layout/ide-layout-stack-shortcuts.c b/libide/layout/ide-layout-stack-shortcuts.c
new file mode 100644
index 0000000..581f607
--- /dev/null
+++ b/libide/layout/ide-layout-stack-shortcuts.c
@@ -0,0 +1,103 @@
+/* ide-layout-stack-shortcuts.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-layout-stack.h"
+#include "ide-layout-private.h"
+
+static const DzlShortcutEntry stack_shortcuts[] = {
+  { "org.gnome.builder.layoutstack.move-right",
+    NULL,
+    N_("Editing"),
+    N_("Navigation"),
+    N_("Move document right"),
+    N_("Move the document to the frame on the right") },
+
+  { "org.gnome.builder.layoutstack.move-left",
+    NULL,
+    N_("Editing"),
+    N_("Navigation"),
+    N_("Move document left"),
+    N_("Move the document to the frame on the left") },
+
+  { "org.gnome.builder.layoutstack.previous-document",
+    NULL,
+    N_("Editing"),
+    N_("Navigation"),
+    N_("Focus next document"),
+    N_("Focus the next document in the stack") },
+
+  { "org.gnome.builder.layoutstack.previous-document",
+    NULL,
+    N_("Editing"),
+    N_("Navigation"),
+    N_("Focus next document"),
+    N_("Focus the next document in the stack") },
+
+  { "org.gnome.builder.layoutstack.close-view",
+    NULL,
+    N_("Editing"),
+    N_("Navigation"),
+    N_("Close current view"),
+    N_("Closes the currently focused view") },
+};
+
+void
+_ide_layout_stack_init_shortcuts (IdeLayoutStack *self)
+{
+  DzlShortcutController *controller;
+
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             stack_shortcuts,
+                                             G_N_ELEMENTS (stack_shortcuts),
+                                             GETTEXT_PACKAGE);
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              "org.gnome.builder.layoutstack.move-right",
+                                              "<Control><Alt>Page_Down",
+                                              "layoutstack.move-right");
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              "org.gnome.builder.layoutstack.move-left",
+                                              "<Control><Alt>Page_Up",
+                                              "layoutstack.move-left");
+
+  dzl_shortcut_controller_add_command_signal (controller,
+                                              "org.gnome.builder.layoutstack.next-document",
+                                              "<Control><Shift>Page_Down",
+                                              "change-current-page", 1,
+                                              G_TYPE_INT, 1);
+
+  dzl_shortcut_controller_add_command_signal (controller,
+                                              "org.gnome.builder.layoutstack.previous-document",
+                                              "<Control><Shift>Page_Up",
+                                              "change-current-page", 1,
+                                              G_TYPE_INT, -1);
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              "org.gnome.builder.layoutstack.close-view",
+                                              "<Control>w",
+                                              "layoutstack.close-view");
+}
diff --git a/libide/layout/ide-layout-stack.c b/libide/layout/ide-layout-stack.c
new file mode 100644
index 0000000..ffb9e75
--- /dev/null
+++ b/libide/layout/ide-layout-stack.c
@@ -0,0 +1,823 @@
+/* ide-layout-stack.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-layout-stack"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "ide-layout-stack.h"
+#include "ide-layout-stack-header.h"
+#include "ide-layout-private.h"
+#include "ide-shortcut-label.h"
+
+#define TRANSITION_DURATION 300
+
+/**
+ * SECTION:ide-layout-stack
+ * @title: IdeLayoutStack
+ * @short_description: A stack of #IdeLayoutView
+ *
+ * This widget is used to represent a stack of #IdeLayoutView widgets.  it
+ * includes an #IdeLayoutStackHeader at the top, and then a stack of views
+ * below.
+ *
+ * If there are no #IdeLayoutView visibile, then an empty state widget is
+ * displayed with some common information for the user.
+ *
+ * To simplify integration with other systems, #IdeLayoutStack implements
+ * the #GListModel interface for each of the #IdeLayoutView.
+ */
+
+typedef struct
+{
+  DzlBindingGroup      *bindings;
+  DzlSignalGroup       *signals;
+  GPtrArray            *views;
+
+  DzlBox               *empty_state;
+  DzlEmptyState        *failed_state;
+  IdeLayoutStackHeader *header;
+  GtkStack             *stack;
+  GtkStack             *top_stack;
+} IdeLayoutStackPrivate;
+
+typedef struct
+{
+  IdeLayoutStack *source;
+  IdeLayoutStack *dest;
+  IdeLayoutView  *view;
+  DzlBoxTheatric *theatric;
+} AnimationState;
+
+enum {
+  PROP_0,
+  PROP_HAS_VIEW,
+  PROP_VISIBLE_CHILD,
+  N_PROPS
+};
+
+enum {
+  CHNAGE_CURRENT_PAGE,
+  N_SIGNALS
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeLayoutStack, ide_layout_stack, GTK_TYPE_BOX,
+                         G_ADD_PRIVATE (IdeLayoutStack)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_layout_stack_view_failed (IdeLayoutStack *self,
+                              GParamSpec     *pspec,
+                              IdeLayoutView  *view)
+{
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_LAYOUT_VIEW (view));
+
+  if (ide_layout_view_get_failed (view))
+    gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->failed_state));
+  else
+    gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->stack));
+}
+
+static void
+ide_layout_stack_bindings_notify_source (IdeLayoutStack  *self,
+                                         GParamSpec      *pspec,
+                                         DzlBindingGroup *bindings)
+{
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  GObject *source;
+
+  g_assert (DZL_IS_BINDING_GROUP (bindings));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  source = dzl_binding_group_get_source (bindings);
+  if (source == NULL)
+    ide_layout_stack_header_set_title (priv->header, _("No Open Pages"));
+}
+
+static void
+ide_layout_stack_notify_visible_child (IdeLayoutStack *self,
+                                       GParamSpec     *pspec,
+                                       GtkStack       *stack)
+{
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  GtkWidget *visible_child;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (GTK_IS_STACK (stack));
+
+  visible_child = gtk_stack_get_visible_child (priv->stack);
+
+  /*
+   * Mux/Proxy actions to our level so that they also be activated
+   * from the header bar without any weirdness by the View.
+   */
+  dzl_gtk_widget_mux_action_groups (GTK_WIDGET (self), visible_child,
+                                    "IDE_LAYOUT_STACK_MUXED_ACTION");
+
+  /* Update our bindings targets */
+  dzl_binding_group_set_source (priv->bindings, visible_child);
+  dzl_signal_group_set_target (priv->signals, visible_child);
+
+  /* Show either the empty state, failed state, or actual view */
+  if (visible_child != NULL &&
+      ide_layout_view_get_failed (IDE_LAYOUT_VIEW (visible_child)))
+    gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->failed_state));
+  else if (visible_child != NULL)
+    gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->stack));
+  else
+    gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->empty_state));
+
+  /* Allow the header to update settings */
+  _ide_layout_stack_header_update (priv->header, IDE_LAYOUT_VIEW (visible_child));
+
+  /* Ensure action state is up to date */
+  _ide_layout_stack_update_actions (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VISIBLE_CHILD]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_VIEW]);
+}
+
+static void
+collect_widgets (GtkWidget *widget,
+                 gpointer   user_data)
+{
+  g_ptr_array_add (user_data, widget);
+}
+
+static void
+ide_layout_stack_change_current_page (IdeLayoutStack *self,
+                                      gint            direction)
+{
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  g_autoptr(GPtrArray) ar = NULL;
+  GtkWidget *visible_child;
+  gint position = 0;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  visible_child = gtk_stack_get_visible_child (priv->stack);
+
+  if (visible_child == NULL)
+    return;
+
+  gtk_container_child_get (GTK_CONTAINER (priv->stack), visible_child,
+                           "position", &position,
+                           NULL);
+
+  ar = g_ptr_array_new ();
+  gtk_container_foreach (GTK_CONTAINER (priv->stack), collect_widgets, ar);
+  if (ar->len == 0)
+    g_return_if_reached ();
+
+  visible_child = g_ptr_array_index (ar, (position + direction) % ar->len);
+  gtk_stack_set_visible_child (priv->stack, visible_child);
+}
+
+static void
+ide_layout_stack_add (GtkContainer *container,
+                      GtkWidget    *widget)
+{
+  IdeLayoutStack *self = (IdeLayoutStack *)container;
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  if (IDE_IS_LAYOUT_VIEW (widget))
+    gtk_container_add (GTK_CONTAINER (priv->stack), widget);
+  else
+    GTK_CONTAINER_CLASS (ide_layout_stack_parent_class)->add (container, widget);
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+ide_layout_stack_view_added (IdeLayoutStack *self,
+                             IdeLayoutView  *view)
+{
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  gint position;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_LAYOUT_VIEW (view));
+
+  /*
+   * Make sure that the header has dismissed all of the popovers immediately.
+   * We don't want them lingering while we do other UI work which might want to
+   * grab focus, etc.
+   */
+  _ide_layout_stack_header_popdown (priv->header);
+
+  /* Notify GListModel consumers of the new view and it's position within
+   * our stack of view widgets.
+   */
+  gtk_container_child_get (GTK_CONTAINER (priv->stack), GTK_WIDGET (view),
+                           "position", &position,
+                           NULL);
+  g_ptr_array_insert (priv->views, position, view);
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+
+  /*
+   * Now ensure that the view is displayed and focus the widget so the
+   * user can immediately start typing.
+   */
+  ide_layout_stack_set_visible_child (self, view);
+  gtk_widget_grab_focus (GTK_WIDGET (view));
+}
+
+static void
+ide_layout_stack_view_removed (IdeLayoutStack *self,
+                               IdeLayoutView  *view)
+{
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (IDE_IS_LAYOUT_VIEW (view));
+
+  if (priv->views != NULL)
+    {
+      /* If this is the last view, hide the popdown now.  We use our hide
+       * variant instead of popdown so that we don't have jittery animations.
+       */
+      if (priv->views->len == 1)
+        _ide_layout_stack_header_hide (priv->header);
+
+      for (guint i = 0; i < priv->views->len; i++)
+        {
+          if (view == (IdeLayoutView *)g_ptr_array_index (priv->views, i))
+            {
+              g_ptr_array_remove_index (priv->views, i);
+              g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+            }
+        }
+    }
+}
+
+static void
+ide_layout_stack_real_agree_to_close_async (IdeLayoutStack      *self,
+                                            GCancellable        *cancellable,
+                                            GAsyncReadyCallback  callback,
+                                            gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_layout_stack_real_agree_to_close_async);
+  g_task_set_priority (task, G_PRIORITY_LOW);
+  g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_layout_stack_real_agree_to_close_finish (IdeLayoutStack *self,
+                                             GAsyncResult   *result,
+                                             GError        **error)
+{
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_layout_stack_destroy (GtkWidget *widget)
+{
+  IdeLayoutStack *self = (IdeLayoutStack *)widget;
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  if (priv->bindings != NULL)
+    {
+      dzl_binding_group_set_source (priv->bindings, NULL);
+      g_clear_object (&priv->bindings);
+    }
+
+  if (priv->signals != NULL)
+    {
+      dzl_signal_group_set_target (priv->signals, NULL);
+      g_clear_object (&priv->signals);
+    }
+
+  g_clear_pointer (&priv->views, g_ptr_array_unref);
+
+  GTK_WIDGET_CLASS (ide_layout_stack_parent_class)->destroy (widget);
+}
+
+static void
+ide_layout_stack_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  IdeLayoutStack *self = IDE_LAYOUT_STACK (object);
+
+  switch (prop_id)
+    {
+    case PROP_HAS_VIEW:
+      g_value_set_boolean (value, ide_layout_stack_get_has_view (self));
+      break;
+
+    case PROP_VISIBLE_CHILD:
+      g_value_set_object (value, ide_layout_stack_get_visible_child (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_layout_stack_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  IdeLayoutStack *self = IDE_LAYOUT_STACK (object);
+
+  switch (prop_id)
+    {
+    case PROP_VISIBLE_CHILD:
+      ide_layout_stack_set_visible_child (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_layout_stack_class_init (IdeLayoutStackClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->get_property = ide_layout_stack_get_property;
+  object_class->set_property = ide_layout_stack_set_property;
+
+  widget_class->destroy = ide_layout_stack_destroy;
+
+  container_class->add = ide_layout_stack_add;
+
+  klass->agree_to_close_async = ide_layout_stack_real_agree_to_close_async;
+  klass->agree_to_close_finish = ide_layout_stack_real_agree_to_close_finish;
+
+  properties [PROP_HAS_VIEW] =
+    g_param_spec_boolean ("has-view", NULL, NULL,
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_VISIBLE_CHILD] =
+    g_param_spec_object ("visible-child",
+                         "Visible Child",
+                         "The current view to be displayed",
+                         IDE_TYPE_LAYOUT_VIEW,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [CHNAGE_CURRENT_PAGE] =
+    g_signal_new_class_handler ("change-current-page",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (ide_layout_stack_change_current_page),
+                                NULL, NULL,
+                                g_cclosure_marshal_VOID__INT,
+                                G_TYPE_NONE, 1, G_TYPE_INT);
+
+  gtk_widget_class_set_css_name (widget_class, "idelayoutstack");
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/ide-layout-stack.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutStack, empty_state);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutStack, failed_state);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutStack, header);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutStack, stack);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeLayoutStack, top_stack);
+
+  g_type_ensure (IDE_TYPE_LAYOUT_STACK_HEADER);
+  g_type_ensure (IDE_TYPE_SHORTCUT_LABEL);
+}
+
+static void
+ide_layout_stack_init (IdeLayoutStack *self)
+{
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  _ide_layout_stack_init_actions (self);
+  _ide_layout_stack_init_shortcuts (self);
+
+  priv->views = g_ptr_array_new ();
+
+  priv->signals = dzl_signal_group_new (IDE_TYPE_LAYOUT_VIEW);
+
+  dzl_signal_group_connect_swapped (priv->signals,
+                                    "notify::failed",
+                                    G_CALLBACK (ide_layout_stack_view_failed),
+                                    self);
+
+  priv->bindings = dzl_binding_group_new ();
+
+  g_signal_connect_object (priv->bindings,
+                           "notify::source",
+                           G_CALLBACK (ide_layout_stack_bindings_notify_source),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  dzl_binding_group_bind (priv->bindings, "title",
+                          priv->header, "title",
+                          G_BINDING_SYNC_CREATE);
+
+  dzl_binding_group_bind (priv->bindings, "modified",
+                          priv->header, "modified",
+                          G_BINDING_SYNC_CREATE);
+
+  g_signal_connect_object (priv->stack,
+                           "notify::visible-child",
+                           G_CALLBACK (ide_layout_stack_notify_visible_child),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->stack,
+                           "add",
+                           G_CALLBACK (ide_layout_stack_view_added),
+                           self,
+                           G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+
+  g_signal_connect_object (priv->stack,
+                           "remove",
+                           G_CALLBACK (ide_layout_stack_view_removed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  _ide_layout_stack_header_set_views (priv->header, G_LIST_MODEL (self));
+  _ide_layout_stack_header_update (priv->header, NULL);
+}
+
+GtkWidget *
+ide_layout_stack_new (void)
+{
+  return g_object_new (IDE_TYPE_LAYOUT_STACK, NULL);
+}
+
+/**
+ * ide_layout_stack_set_visible_child:
+ * @self: a #IdeLayoutStack
+ *
+ * Sets the current view for the stack.
+ *
+ * Since: 3.26
+ */
+void
+ide_layout_stack_set_visible_child (IdeLayoutStack *self,
+                                    IdeLayoutView  *view)
+{
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (view));
+  g_return_if_fail (gtk_widget_get_parent (GTK_WIDGET (view)) == (GtkWidget *)priv->stack);
+
+  gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (view));
+}
+
+/**
+ * ide_layout_stack_get_visible_child:
+ * @self: a #IdeLayoutStack
+ *
+ * Gets the visible #IdeLayoutView if there is one; otherwise %NULL.
+ *
+ * Returns: (nullable) (transfer none): An #IdeLayoutView or %NULL
+ *
+ * Since: 3.26
+ */
+IdeLayoutView *
+ide_layout_stack_get_visible_child (IdeLayoutStack *self)
+{
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (self), NULL);
+
+  return IDE_LAYOUT_VIEW (gtk_stack_get_visible_child (priv->stack));
+}
+
+/**
+ * ide_layout_stack_get_titlebar:
+ * @self: a #IdeLayoutStack
+ *
+ * Gets the #IdeLayoutStackHeader header that is at the top of the stack.
+ *
+ * Returns: (transfer none) (type Ide.LayoutStackHeader): The layout stack header.
+ *
+ * Since: 3.26
+ */
+GtkWidget *
+ide_layout_stack_get_titlebar (IdeLayoutStack *self)
+{
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (self), NULL);
+
+  return GTK_WIDGET (priv->header);
+}
+
+/**
+ * ide_layout_stack_get_has_view:
+ * @self: A #IdeLayoutStack
+ *
+ * Gets the "has-view" property.
+ *
+ * This property is a convenience to allow widgets to easily bind
+ * properties based on whether or not a view is visible in the stack.
+ *
+ * Returns: %TRUE if the stack has a view
+ *
+ * Since: 3.26
+ */
+gboolean
+ide_layout_stack_get_has_view (IdeLayoutStack *self)
+{
+  IdeLayoutView *visible_child;
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (self), FALSE);
+
+  visible_child = ide_layout_stack_get_visible_child (self);
+
+  return visible_child != NULL;
+}
+
+static void
+ide_layout_stack_close_view_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeLayoutView *view = (IdeLayoutView *)object;
+  g_autoptr(IdeLayoutStack) self = user_data;
+  g_autoptr(GError) error = NULL;
+  GtkWidget *toplevel;
+  GtkWidget *focus;
+  gboolean had_focus = FALSE;
+
+  g_assert (IDE_IS_LAYOUT_VIEW (view));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  if (!ide_layout_view_agree_to_close_finish (view, result, &error))
+    {
+      g_message ("%s", error->message);
+      return;
+    }
+
+  /* Keep track of whether or not the widget had focus (which
+   * would happen if we were activated from a keybinding.
+   */
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view));
+  if (GTK_IS_WINDOW (toplevel) &&
+      NULL != (focus = gtk_window_get_focus (GTK_WINDOW (toplevel))) &&
+      (focus == GTK_WIDGET (view) ||
+       gtk_widget_is_ancestor (focus, GTK_WIDGET (view))))
+    had_focus = TRUE;
+
+  /* Now we can destroy the child */
+  gtk_widget_destroy (GTK_WIDGET (view));
+
+  /* We don't want to leave the widget focus in an indeterminate
+   * state so we immediately focus the next child in the stack.
+   * But only do so if we had focus previously.
+   */
+  if (had_focus)
+    {
+      IdeLayoutView *visible_child = ide_layout_stack_get_visible_child (self);
+
+      if (visible_child != NULL)
+        gtk_widget_grab_focus (GTK_WIDGET (visible_child));
+    }
+}
+
+void
+_ide_layout_stack_request_close (IdeLayoutStack *self,
+                                 IdeLayoutView  *view)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (view));
+
+  ide_layout_view_agree_to_close_async (view,
+                                        NULL,
+                                        ide_layout_stack_close_view_cb,
+                                        g_object_ref (self));
+}
+
+static GType
+ide_layout_stack_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_LAYOUT_VIEW;
+}
+
+static guint
+ide_layout_stack_get_n_items (GListModel *model)
+{
+  IdeLayoutStack *self = (IdeLayoutStack *)model;
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+
+  return priv->views ? priv->views->len : 0;
+}
+
+static gpointer
+ide_layout_stack_get_item (GListModel *model,
+                           guint       position)
+{
+  IdeLayoutStack *self = (IdeLayoutStack *)model;
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+
+  g_assert (IDE_IS_LAYOUT_STACK (self));
+  g_assert (position < priv->views->len);
+
+  return g_object_ref (g_ptr_array_index (priv->views, position));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_n_items = ide_layout_stack_get_n_items;
+  iface->get_item = ide_layout_stack_get_item;
+  iface->get_item_type = ide_layout_stack_get_item_type;
+}
+
+void
+ide_layout_stack_agree_to_close_async (IdeLayoutStack      *self,
+                                       GCancellable        *cancellable,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_LAYOUT_STACK_GET_CLASS (self)->agree_to_close_async (self, cancellable, callback, user_data);
+}
+
+gboolean
+ide_layout_stack_agree_to_close_finish (IdeLayoutStack *self,
+                                        GAsyncResult   *result,
+                                        GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_LAYOUT_STACK (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_LAYOUT_STACK_GET_CLASS (self)->agree_to_close_finish (self, result, error);
+}
+
+static void
+animation_state_complete (gpointer data)
+{
+  AnimationState *state = data;
+
+  g_assert (state != NULL);
+
+  gtk_container_add (GTK_CONTAINER (state->dest), GTK_WIDGET (state->view));
+
+  g_clear_object (&state->source);
+  g_clear_object (&state->dest);
+  g_clear_object (&state->view);
+  g_clear_object (&state->theatric);
+  g_slice_free (AnimationState, state);
+}
+
+static inline gboolean
+is_uninitialized (GtkAllocation *alloc)
+{
+  return (alloc->x == -1 && alloc->y == -1 &&
+          alloc->width == 1 && alloc->height == 1);
+}
+
+void
+_ide_layout_stack_transfer (IdeLayoutStack *self,
+                            IdeLayoutStack *dest,
+                            IdeLayoutView  *view)
+{
+  IdeLayoutStackPrivate *priv = ide_layout_stack_get_instance_private (self);
+  IdeLayoutStackPrivate *dest_priv = ide_layout_stack_get_instance_private (dest);
+
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (self));
+  g_return_if_fail (IDE_IS_LAYOUT_STACK (dest));
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (view));
+  g_return_if_fail (GTK_WIDGET (priv->stack) == gtk_widget_get_parent (GTK_WIDGET (view)));
+
+  /*
+   * If both the old and the new stacks are mapped, we can animate
+   * between them using a snapshot of the view. Well, we also need
+   * to be sure they have a valid allocation, but that check is done
+   * slightly after this because it makes things easier.
+   */
+  if (gtk_widget_get_mapped (GTK_WIDGET (self)) &&
+      gtk_widget_get_mapped (GTK_WIDGET (dest)) &&
+      gtk_widget_get_mapped (GTK_WIDGET (view)))
+    {
+      GtkAllocation alloc, dest_alloc;
+      cairo_surface_t *surface = NULL;
+      GdkWindow *window;
+      GtkWidget *grid;
+      gboolean enable_animations;
+
+      grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_LAYOUT_GRID);
+
+      gtk_widget_get_allocation (GTK_WIDGET (view), &alloc);
+      gtk_widget_get_allocation (GTK_WIDGET (dest), &dest_alloc);
+
+      g_object_get (gtk_settings_get_default (),
+                    "gtk-enable-animations", &enable_animations,
+                    NULL);
+
+      if (enable_animations &&
+          grid != NULL &&
+          !is_uninitialized (&alloc) &&
+          !is_uninitialized (&dest_alloc) &&
+          dest_alloc.width > 0 && dest_alloc.height > 0 &&
+          NULL != (window = gtk_widget_get_window (GTK_WIDGET (view))) &&
+          NULL != (surface = gdk_window_create_similar_surface (window,
+                                                                CAIRO_CONTENT_COLOR,
+                                                                alloc.width,
+                                                                alloc.height)))
+        {
+          DzlBoxTheatric *theatric = NULL;
+          AnimationState *state;
+          cairo_t *cr;
+
+          cr = cairo_create (surface);
+          gtk_widget_draw (GTK_WIDGET (view), cr);
+          cairo_destroy (cr);
+
+          gtk_widget_translate_coordinates (GTK_WIDGET (priv->stack), grid, 0, 0,
+                                            &alloc.x, &alloc.y);
+          gtk_widget_translate_coordinates (GTK_WIDGET (dest_priv->stack), grid, 0, 0,
+                                            &dest_alloc.x, &dest_alloc.y);
+
+          theatric = g_object_new (DZL_TYPE_BOX_THEATRIC,
+                                   "surface", surface,
+                                   "height", alloc.height,
+                                   "target", grid,
+                                   "width", alloc.width,
+                                   "x", alloc.x,
+                                   "y", alloc.y,
+                                   NULL);
+
+          state = g_slice_new0 (AnimationState);
+          state->source = g_object_ref (self);
+          state->dest = g_object_ref (dest);
+          state->view = g_object_ref (view);
+          state->theatric = theatric;
+
+          dzl_object_animate_full (theatric,
+                                   DZL_ANIMATION_EASE_IN_OUT_CUBIC,
+                                   TRANSITION_DURATION,
+                                   gtk_widget_get_frame_clock (GTK_WIDGET (self)),
+                                   animation_state_complete,
+                                   state,
+                                   "x", dest_alloc.x,
+                                   "width", dest_alloc.width,
+                                   "y", dest_alloc.y,
+                                   "height", dest_alloc.height,
+                                   NULL);
+
+          gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (view));
+
+          cairo_surface_destroy (surface);
+
+          return;
+        }
+    }
+
+  g_object_ref (view);
+  gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (view));
+  gtk_container_add (GTK_CONTAINER (dest_priv->stack), GTK_WIDGET (view));
+  g_object_unref (view);
+}
diff --git a/libide/layout/ide-layout-stack.h b/libide/layout/ide-layout-stack.h
new file mode 100644
index 0000000..c487d09
--- /dev/null
+++ b/libide/layout/ide-layout-stack.h
@@ -0,0 +1,67 @@
+/* ide-layout-stack.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+#include "ide-layout-view.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LAYOUT_STACK (ide_layout_stack_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (IdeLayoutStack, ide_layout_stack, IDE, LAYOUT_STACK, GtkBox)
+
+struct _IdeLayoutStackClass
+{
+  GtkBoxClass parent_class;
+
+  void     (*agree_to_close_async)  (IdeLayoutStack       *stack,
+                                     GCancellable         *cancellable,
+                                     GAsyncReadyCallback   callback,
+                                     gpointer              user_data);
+  gboolean (*agree_to_close_finish) (IdeLayoutStack       *stack,
+                                     GAsyncResult         *result,
+                                     GError              **error);
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+};
+
+GtkWidget     *ide_layout_stack_new                   (void);
+GtkWidget     *ide_layout_stack_get_titlebar          (IdeLayoutStack       *self);
+IdeLayoutView *ide_layout_stack_get_visible_child     (IdeLayoutStack       *self);
+void           ide_layout_stack_set_visible_child     (IdeLayoutStack       *self,
+                                                       IdeLayoutView        *view);
+gboolean       ide_layout_stack_get_has_view          (IdeLayoutStack       *self);
+void           ide_layout_stack_agree_to_close_async  (IdeLayoutStack       *self,
+                                                       GCancellable         *cancellable,
+                                                       GAsyncReadyCallback   callback,
+                                                       gpointer              user_data);
+gboolean       ide_layout_stack_agree_to_close_finish (IdeLayoutStack       *self,
+                                                       GAsyncResult         *result,
+                                                       GError              **error);
+
+G_END_DECLS
diff --git a/libide/layout/ide-layout-stack.ui b/libide/layout/ide-layout-stack.ui
new file mode 100644
index 0000000..017a161
--- /dev/null
+++ b/libide/layout/ide-layout-stack.ui
@@ -0,0 +1,137 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeLayoutStack" parent="GtkBox">
+    <property name="orientation">vertical</property>
+    <child>
+      <object class="IdeLayoutStackHeader" id="header">
+        <property name="show-close-button">true</property>
+        <property name="title" translatable="yes">No Open Pages</property>
+        <property name="visible">true</property>
+      </object>
+    </child>
+    <child>
+      <object class="GtkStack" id="top_stack">
+        <property name="expand">true</property>
+        <property name="homogeneous">false</property>
+        <property name="interpolate-size">false</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="DzlBox" id="empty_state">
+            <property name="expand">false</property>
+            <property name="halign">center</property>
+            <property name="orientation">vertical</property>
+            <property name="valign">center</property>
+            <property name="max-width-request">275</property>
+            <property name="margin">32</property>
+            <property name="spacing">6</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkLabel">
+                <property name="label" translatable="yes">Open a File or Terminal</property>
+                <property name="margin-bottom">6</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+                <attributes>
+                  <attribute name="weight" value="bold"/>
+                  <attribute name="scale" value="1.2"/>
+                </attributes>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="label" translatable="yes">Use the page switcher above or use one of the 
following:</property>
+                <property name="justify">center</property>
+                <property name="margin-bottom">18</property>
+                <property name="visible">true</property>
+                <property name="wrap">true</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+                <attributes>
+                  <attribute name="scale" value=".909"/>
+                </attributes>
+              </object>
+            </child>
+            <child>
+              <object class="IdeShortcutLabel">
+                <property name="title" translatable="yes">Search</property>
+                <property name="action">win.global-search</property>
+                <property name="visible">true</property>
+                <!-- Remove after auto accel tracking -->
+                <property name="accel">Ctrl+.</property>
+              </object>
+            </child>
+            <child>
+              <object class="IdeShortcutLabel">
+                <property name="title" translatable="yes">Project sidebar</property>
+                <property name="action">editor.sidebar</property>
+                <property name="visible">true</property>
+                <!-- Remove after auto accel tracking -->
+                <property name="accel">F9</property>
+              </object>
+            </child>
+            <child>
+              <object class="IdeShortcutLabel">
+                <property name="title" translatable="yes">File chooser</property>
+                <property name="action">editor.open-file</property>
+                <property name="visible">true</property>
+                <!-- Remove after auto accel tracking -->
+                <property name="accel">Ctrl+O</property>
+              </object>
+            </child>
+            <child>
+              <object class="IdeShortcutLabel">
+                <property name="title" translatable="yes">New terminal</property>
+                <property name="action">editor.new-terminal</property>
+                <property name="visible">true</property>
+                <!-- Remove after auto accel tracking -->
+                <property name="accel">Ctrl+Shift+T</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="homogeneous">true</property>
+                <property name="margin-top">18</property>
+                <property name="spacing">6</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkButton">
+                    <property name="action-name">editor.open-file</property>
+                    <property name="label" translatable="yes">Open Fileā€¦</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkButton">
+                    <property name="action-name">editor.new-terminal</property>
+                    <property name="label" translatable="yes">New Terminal</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="DzlEmptyState" id="failed_state">
+            <property name="icon-name">computer-fail-symbolic</property>
+            <property name="pixel-size">160</property>
+            <property name="title" translatable="yes">Uh oh, something went wrong</property>
+            <property name="subtitle" translatable="yes">There was a failure while trying to perform the 
operation.</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="expand">true</property>
+            <property name="homogeneous">false</property>
+            <property name="interpolate-size">false</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/libide/layout/ide-layout-view.c b/libide/layout/ide-layout-view.c
new file mode 100644
index 0000000..5040c30
--- /dev/null
+++ b/libide/layout/ide-layout-view.c
@@ -0,0 +1,392 @@
+/* ide-layout-view.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-layout-view"
+
+#include "ide-layout-view.h"
+
+typedef struct
+{
+  const gchar *menu_id;
+  const gchar *icon_name;
+  gchar       *title;
+  guint        failed : 1;
+  guint        modified : 1;
+} IdeLayoutViewPrivate;
+
+enum {
+  PROP_0,
+  PROP_ICON_NAME,
+  PROP_MENU_ID,
+  PROP_MODIFIED,
+  PROP_FAILED,
+  PROP_TITLE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeLayoutView, ide_layout_view, GTK_TYPE_BOX)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_layout_view_real_agree_to_close_async (IdeLayoutView       *self,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (IDE_IS_LAYOUT_VIEW (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_priority (task, G_PRIORITY_LOW);
+  g_task_set_source_tag (task, ide_layout_view_agree_to_close_async);
+  g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_layout_view_real_agree_to_close_finish (IdeLayoutView  *self,
+                                            GAsyncResult   *result,
+                                            GError        **error)
+{
+  g_assert (IDE_IS_LAYOUT_VIEW (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+find_focus_child (GtkWidget *widget,
+                  gboolean  *handled)
+{
+  if (!*handled)
+    *handled = gtk_widget_child_focus (widget, GTK_DIR_TAB_FORWARD);
+}
+
+static void
+ide_layout_view_grab_focus (GtkWidget *widget)
+{
+  gboolean handled = FALSE;
+
+  g_assert (IDE_IS_LAYOUT_VIEW (widget));
+
+  /*
+   * This default grab_focus override just looks for the first child (generally
+   * something like a scrolled window) and tries to move forward on focusing
+   * the child widget. In most cases, this should work without intervention
+   * from the child subclass.
+   */
+
+  gtk_container_foreach (GTK_CONTAINER (widget), (GtkCallback) find_focus_child, &handled);
+}
+
+static void
+ide_layout_view_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeLayoutView *self = IDE_LAYOUT_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_FAILED:
+      g_value_set_boolean (value, ide_layout_view_get_failed (self));
+      break;
+
+    case PROP_ICON_NAME:
+      g_value_set_static_string (value, ide_layout_view_get_icon_name (self));
+      break;
+
+    case PROP_MENU_ID:
+      g_value_set_static_string (value, ide_layout_view_get_menu_id (self));
+      break;
+
+    case PROP_MODIFIED:
+      g_value_set_boolean (value, ide_layout_view_get_modified (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, ide_layout_view_get_title (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_layout_view_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeLayoutView *self = IDE_LAYOUT_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_FAILED:
+      ide_layout_view_set_failed (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_ICON_NAME:
+      ide_layout_view_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_MENU_ID:
+      ide_layout_view_set_menu_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_MODIFIED:
+      ide_layout_view_set_modified (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_TITLE:
+      ide_layout_view_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_layout_view_class_init (IdeLayoutViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = ide_layout_view_get_property;
+  object_class->set_property = ide_layout_view_set_property;
+
+  widget_class->grab_focus = ide_layout_view_grab_focus;
+
+  klass->agree_to_close_async = ide_layout_view_real_agree_to_close_async;
+  klass->agree_to_close_finish = ide_layout_view_real_agree_to_close_finish;
+
+  properties [PROP_FAILED] =
+    g_param_spec_boolean ("failed",
+                          "Failed",
+                          "If the view has failed or crashed",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "The icon-name describing the view content",
+                         "text-x-generic-symbolic",
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MENU_ID] =
+    g_param_spec_string ("menu-id",
+                         "Menu ID",
+                         "The identifier of the GMenu to use in the document popover",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MODIFIED] =
+    g_param_spec_boolean ("modified",
+                          "Modified",
+                          "If the view has been modified from the saved content",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the document or view",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_layout_view_init (IdeLayoutView *self)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_VERTICAL);
+
+  priv->icon_name = g_intern_string ("text-x-generic-symbolic");
+}
+
+GtkWidget *
+ide_layout_view_new (void)
+{
+  return g_object_new (IDE_TYPE_LAYOUT_VIEW, NULL);
+}
+
+const gchar *
+ide_layout_view_get_title (IdeLayoutView *self)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+
+  return priv->title;
+}
+
+void
+ide_layout_view_set_title (IdeLayoutView *self,
+                           const gchar   *title)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+
+  if (g_strcmp0 (title, priv->title) != 0)
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+}
+
+const gchar *
+ide_layout_view_get_menu_id (IdeLayoutView *self)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+
+  return priv->menu_id;
+}
+
+void
+ide_layout_view_set_menu_id (IdeLayoutView *self,
+                             const gchar   *menu_id)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+
+  menu_id = g_intern_string (menu_id);
+
+  if (menu_id != priv->menu_id)
+    {
+      priv->menu_id = menu_id;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MENU_ID]);
+    }
+}
+
+void
+ide_layout_view_agree_to_close_async (IdeLayoutView       *self,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_LAYOUT_VIEW_GET_CLASS (self)->agree_to_close_async (self, cancellable, callback, user_data);
+}
+
+gboolean
+ide_layout_view_agree_to_close_finish (IdeLayoutView  *self,
+                                       GAsyncResult   *result,
+                                       GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_LAYOUT_VIEW_GET_CLASS (self)->agree_to_close_finish (self, result, error);
+}
+
+gboolean
+ide_layout_view_get_failed (IdeLayoutView *self)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), FALSE);
+
+  return priv->failed;
+}
+
+void
+ide_layout_view_set_failed (IdeLayoutView *self,
+                            gboolean       failed)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+
+  failed = !!failed;
+
+  if (failed != priv->failed)
+    {
+      priv->failed = failed;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FAILED]);
+    }
+}
+
+gboolean
+ide_layout_view_get_modified (IdeLayoutView *self)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), FALSE);
+
+  return priv->modified;
+}
+
+void
+ide_layout_view_set_modified (IdeLayoutView *self,
+                              gboolean       modified)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+
+  modified = !!modified;
+
+  if (priv->modified != modified)
+    {
+      priv->modified = modified;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODIFIED]);
+    }
+}
+
+const gchar *
+ide_layout_view_get_icon_name (IdeLayoutView *self)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LAYOUT_VIEW (self), NULL);
+
+  return priv->icon_name;
+}
+
+void
+ide_layout_view_set_icon_name (IdeLayoutView *self,
+                               const gchar   *icon_name)
+{
+  IdeLayoutViewPrivate *priv = ide_layout_view_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LAYOUT_VIEW (self));
+
+  icon_name = g_intern_string (icon_name);
+
+  if (icon_name != priv->icon_name)
+    {
+      priv->icon_name = icon_name;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
+    }
+}
diff --git a/libide/layout/ide-layout-view.h b/libide/layout/ide-layout-view.h
new file mode 100644
index 0000000..589a8d6
--- /dev/null
+++ b/libide/layout/ide-layout-view.h
@@ -0,0 +1,79 @@
+/* ide-layout-view.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LAYOUT_VIEW (ide_layout_view_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (IdeLayoutView, ide_layout_view, IDE, LAYOUT_VIEW, GtkBox)
+
+struct _IdeLayoutViewClass
+{
+  GtkBoxClass parent_class;
+
+  void     (*agree_to_close_async)  (IdeLayoutView        *self,
+                                     GCancellable         *cancellable,
+                                     GAsyncReadyCallback   callback,
+                                     gpointer              user_data);
+  gboolean (*agree_to_close_finish) (IdeLayoutView        *self,
+                                     GAsyncResult         *result,
+                                     GError              **error);
+
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+  gpointer _reserved9;
+  gpointer _reserved10;
+  gpointer _reserved11;
+  gpointer _reserved12;
+};
+
+GtkWidget   *ide_layout_view_new                   (void);
+const gchar *ide_layout_view_get_icon_name         (IdeLayoutView        *self);
+void         ide_layout_view_set_icon_name         (IdeLayoutView        *self,
+                                                    const gchar          *icon_name);
+gboolean     ide_layout_view_get_failed            (IdeLayoutView        *self);
+void         ide_layout_view_set_failed            (IdeLayoutView        *self,
+                                                    gboolean              failed);
+const gchar *ide_layout_view_get_menu_id           (IdeLayoutView        *self);
+void         ide_layout_view_set_menu_id           (IdeLayoutView        *self,
+                                                    const gchar          *menu_id);
+gboolean     ide_layout_view_get_modified          (IdeLayoutView        *self);
+void         ide_layout_view_set_modified          (IdeLayoutView        *self,
+                                                    gboolean              modified);
+const gchar *ide_layout_view_get_title             (IdeLayoutView        *self);
+void         ide_layout_view_set_title             (IdeLayoutView        *self,
+                                                    const gchar          *title);
+void         ide_layout_view_agree_to_close_async  (IdeLayoutView        *self,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+gboolean     ide_layout_view_agree_to_close_finish (IdeLayoutView        *self,
+                                                    GAsyncResult         *result,
+                                                    GError              **error);
+
+G_END_DECLS
diff --git a/libide/workbench/ide-layout.c b/libide/layout/ide-layout.c
similarity index 100%
rename from libide/workbench/ide-layout.c
rename to libide/layout/ide-layout.c
diff --git a/libide/workbench/ide-layout.h b/libide/layout/ide-layout.h
similarity index 100%
rename from libide/workbench/ide-layout.h
rename to libide/layout/ide-layout.h
diff --git a/libide/layout/ide-shortcut-label.c b/libide/layout/ide-shortcut-label.c
new file mode 100644
index 0000000..df65ffe
--- /dev/null
+++ b/libide/layout/ide-shortcut-label.c
@@ -0,0 +1,267 @@
+/* ide-shortcut-label.c
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#define G_LOG_DOMAIN "ide-shortcut-label"
+
+#include <dazzle.h>
+
+#include "ide-shortcut-label.h"
+
+struct _IdeShortcutLabel
+{
+  GtkBox       parent_instance;
+
+  GtkLabel    *accel_label;
+  GtkLabel    *title;
+
+  const gchar *accel;
+  const gchar *action;
+  const gchar *command;
+};
+
+enum {
+  PROP_0,
+  PROP_ACCEL,
+  PROP_ACTION,
+  PROP_COMMAND,
+  PROP_TITLE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeShortcutLabel, ide_shortcut_label, GTK_TYPE_BOX)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_shortcut_label_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeShortcutLabel *self = IDE_SHORTCUT_LABEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCEL:
+      g_value_set_static_string (value, ide_shortcut_label_get_accel (self));
+      break;
+
+    case PROP_ACTION:
+      g_value_set_static_string (value, ide_shortcut_label_get_action (self));
+      break;
+
+    case PROP_COMMAND:
+      g_value_set_static_string (value, ide_shortcut_label_get_command (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, ide_shortcut_label_get_title (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_label_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeShortcutLabel *self = IDE_SHORTCUT_LABEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCEL:
+      ide_shortcut_label_set_accel (self, g_value_get_string (value));
+      break;
+
+    case PROP_ACTION:
+      ide_shortcut_label_set_action (self, g_value_get_string (value));
+      break;
+
+    case PROP_COMMAND:
+      ide_shortcut_label_set_command (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      ide_shortcut_label_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_label_class_init (IdeShortcutLabelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_shortcut_label_get_property;
+  object_class->set_property = ide_shortcut_label_set_property;
+
+  properties [PROP_ACTION] =
+    g_param_spec_string ("action",
+                         "Action",
+                         "Action",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ACCEL] =
+    g_param_spec_string ("accel",
+                         "Accel",
+                         "The accel label to override the discovered accel",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_COMMAND] =
+    g_param_spec_string ("command",
+                         "Command",
+                         "Command",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_shortcut_label_init (IdeShortcutLabel *self)
+{
+  self->title = g_object_new (GTK_TYPE_LABEL,
+                              "visible", TRUE,
+                              "xalign", 0.0f,
+                              NULL);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self->title), "dim-label");
+  gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (self->title),
+                                     "fill", TRUE,
+                                     "pack-type", GTK_PACK_START,
+                                     NULL);
+
+  self->accel_label = g_object_new (GTK_TYPE_LABEL,
+                                    "visible", TRUE,
+                                    "xalign", 1.0f,
+                                    NULL);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self->accel_label), "dim-label");
+  gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (self->accel_label),
+                                     "fill", TRUE,
+                                     "pack-type", GTK_PACK_END,
+                                     NULL);
+}
+
+GtkWidget *
+ide_shortcut_label_new (void)
+{
+  return g_object_new (IDE_TYPE_SHORTCUT_LABEL, NULL);
+}
+
+const gchar *
+ide_shortcut_label_get_accel (IdeShortcutLabel *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_LABEL (self), NULL);
+
+  return self->accel;
+}
+
+const gchar *
+ide_shortcut_label_get_action (IdeShortcutLabel *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_LABEL (self), NULL);
+
+  return self->action;
+}
+
+const gchar *
+ide_shortcut_label_get_command (IdeShortcutLabel *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_LABEL (self), NULL);
+
+  return self->command;
+}
+
+const gchar *
+ide_shortcut_label_get_title (IdeShortcutLabel *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_LABEL (self), NULL);
+
+  return gtk_label_get_label (self->title);
+}
+
+void
+ide_shortcut_label_set_accel (IdeShortcutLabel *self,
+                              const gchar      *accel)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_LABEL (self));
+
+  accel = g_intern_string (accel);
+
+  if (accel != self->accel)
+    {
+      self->accel = accel;
+      gtk_label_set_label (self->accel_label, accel);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACCEL]);
+    }
+}
+
+void
+ide_shortcut_label_set_action (IdeShortcutLabel *self,
+                               const gchar      *action)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_LABEL (self));
+
+  action = g_intern_string (action);
+
+  if (action != self->action)
+    {
+      self->action = action;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTION]);
+    }
+}
+
+void
+ide_shortcut_label_set_command (IdeShortcutLabel *self,
+                                const gchar      *command)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_LABEL (self));
+
+  command = g_intern_string (command);
+
+  if (command != self->command)
+    {
+      self->command = command;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMMAND]);
+    }
+}
+
+void
+ide_shortcut_label_set_title (IdeShortcutLabel *self,
+                              const gchar      *title)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_LABEL (self));
+
+  gtk_label_set_label (self->title, title);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+}
diff --git a/libide/layout/ide-shortcut-label.h b/libide/layout/ide-shortcut-label.h
new file mode 100644
index 0000000..7abd53f
--- /dev/null
+++ b/libide/layout/ide-shortcut-label.h
@@ -0,0 +1,43 @@
+/* ide-shortcut-label.h
+ *
+ * Copyright (C) 2017 Christian Hergert <chergert redhat com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <dazzle.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_LABEL (ide_shortcut_label_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutLabel, ide_shortcut_label, IDE, SHORTCUT_LABEL, GtkBox)
+
+GtkWidget   *ide_shortcut_label_new             (void);
+const gchar *ide_shortcut_label_get_accel       (IdeShortcutLabel *self);
+void         ide_shortcut_label_set_accel       (IdeShortcutLabel *self,
+                                                 const gchar      *accel);
+const gchar *ide_shortcut_label_get_action      (IdeShortcutLabel *self);
+void         ide_shortcut_label_set_action      (IdeShortcutLabel *self,
+                                                 const gchar      *action);
+const gchar *ide_shortcut_label_get_command     (IdeShortcutLabel *self);
+void         ide_shortcut_label_set_command     (IdeShortcutLabel *self,
+                                                 const gchar      *command);
+const gchar *ide_shortcut_label_get_title       (IdeShortcutLabel *self);
+void         ide_shortcut_label_set_title       (IdeShortcutLabel *self,
+                                                 const gchar      *title);
+
+G_END_DECLS
diff --git a/libide/libide.gresource.xml b/libide/libide.gresource.xml
index ba545cf..d4ee7a3 100644
--- a/libide/libide.gresource.xml
+++ b/libide/libide.gresource.xml
@@ -50,18 +50,17 @@
   </gresource>
 
   <gresource prefix="/org/gnome/builder/ui">
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-layout-pane.ui">layout/ide-layout-pane.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-layout-stack-header.ui">layout/ide-layout-stack-header.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-layout-stack.ui">layout/ide-layout-stack.ui</file>
+
     <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-editor-frame.ui">editor/ide-editor-frame.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-editor-layout-stack-controls.ui">editor/ide-editor-layout-stack-controls.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-editor-perspective.ui">editor/ide-editor-perspective.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-editor-spell-widget.ui">editor/ide-editor-spell-widget.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-editor-tweak-widget.ui">editor/ide-editor-tweak-widget.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-editor-view.ui">editor/ide-editor-view.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-greeter-perspective.ui">greeter/ide-greeter-perspective.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-greeter-project-row.ui">greeter/ide-greeter-project-row.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-layout-tab.ui">workbench/ide-layout-tab.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-layout-tab-bar.ui">workbench/ide-layout-tab-bar.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-layout-pane.ui">workbench/ide-layout-pane.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-layout-stack.ui">workbench/ide-layout-stack.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-omni-bar.ui">workbench/ide-omni-bar.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-omni-bar-row.ui">workbench/ide-omni-bar-row.ui</file>
     <file compressed="true" preprocess="xml-stripblanks" 
alias="ide-perspective-menu-button.ui">workbench/ide-perspective-menu-button.ui</file>
diff --git a/libide/meson.build b/libide/meson.build
index ec91c81..4c98bd3 100644
--- a/libide/meson.build
+++ b/libide/meson.build
@@ -38,7 +38,6 @@ libide_enum_headers = [
   'symbols/ide-symbol.h',
   'threading/ide-thread-pool.h',
   'vcs/ide-vcs-config.h',
-  'workbench/ide-layout-stack-split.h',
 ]
 
 libide_enums = gnome.mkenums('ide-enums',
@@ -111,6 +110,7 @@ libide_public_headers = [
   'highlighting/ide-highlight-engine.h',
   'highlighting/ide-highlight-index.h',
   'highlighting/ide-highlighter.h',
+  'history/ide-back-forward-controls.h',
   'history/ide-back-forward-item.h',
   'history/ide-back-forward-list.h',
   'langserv/ide-langserv-client.h',
@@ -123,6 +123,13 @@ libide_public_headers = [
   'langserv/ide-langserv-symbol-resolver.h',
   'langserv/ide-langserv-symbol-tree.h',
   'langserv/ide-langserv-util.h',
+  'layout/ide-layout-grid.h',
+  'layout/ide-layout-stack.h',
+  'layout/ide-layout-stack-addin.h',
+  'layout/ide-layout-stack-header.h',
+  'layout/ide-layout-view.h',
+  'layout/ide-layout-pane.h',
+  'layout/ide-layout.h',
   'local/ide-local-device.h',
   'logging/ide-log.h',
   'plugins/ide-extension-adapter.h',
@@ -197,13 +204,6 @@ libide_public_headers = [
   'vcs/ide-vcs-initializer.h',
   'vcs/ide-vcs-uri.h',
   'vcs/ide-vcs.h',
-  'workbench/ide-layout-grid.h',
-  'workbench/ide-layout-pane.h',
-  'workbench/ide-layout-stack-split.h',
-  'workbench/ide-layout-stack.h',
-  'workbench/ide-layout-stack-addin.h',
-  'workbench/ide-layout-view.h',
-  'workbench/ide-layout.h',
   'workbench/ide-omni-bar.h',
   'workbench/ide-perspective.h',
   'workbench/ide-workbench-addin.h',
@@ -300,6 +300,7 @@ libide_public_sources = [
   'highlighting/ide-highlight-engine.c',
   'highlighting/ide-highlight-index.c',
   'highlighting/ide-highlighter.c',
+  'history/ide-back-forward-controls.c',
   'history/ide-back-forward-item.c',
   'history/ide-back-forward-list-load.c',
   'history/ide-back-forward-list-save.c',
@@ -320,6 +321,13 @@ libide_public_sources = [
   'langserv/ide-langserv-symbol-tree.c',
   'langserv/ide-langserv-symbol-tree-private.h',
   'langserv/ide-langserv-util.c',
+  'layout/ide-layout-grid.c',
+  'layout/ide-layout-pane.c',
+  'layout/ide-layout-stack-addin.c',
+  'layout/ide-layout-stack-header.c',
+  'layout/ide-layout-stack.c',
+  'layout/ide-layout-view.c',
+  'layout/ide-layout.c',
   'local/ide-local-device.c',
   'logging/ide-log.c',
   'plugins/ide-extension-adapter.c',
@@ -393,12 +401,6 @@ libide_public_sources = [
   'vcs/ide-vcs-initializer.c',
   'vcs/ide-vcs-uri.c',
   'vcs/ide-vcs.c',
-  'workbench/ide-layout-grid.c',
-  'workbench/ide-layout-pane.c',
-  'workbench/ide-layout-stack.c',
-  'workbench/ide-layout-stack-addin.c',
-  'workbench/ide-layout-view.c',
-  'workbench/ide-layout.c',
   'workbench/ide-omni-bar.c',
   'workbench/ide-perspective.c',
   'workbench/ide-workbench-addin.c',
@@ -501,6 +503,12 @@ libide_sources = libide_generated_headers + libide_public_sources + [
   'keybindings/ide-keybindings.h',
   'keybindings/ide-shortcuts-window.c',
   'keybindings/ide-shortcuts-window.h',
+  'layout/ide-layout-grid-actions.c',
+  'layout/ide-layout-private.h',
+  'layout/ide-layout-stack-actions.c',
+  'layout/ide-layout-stack-shortcuts.c',
+  'layout/ide-shortcut-label.c', # todo: libdazzle
+  'layout/ide-shortcut-label.h', # todo: libdazzle
   'modelines/ide-modelines-file-settings.c',
   'modelines/ide-modelines-file-settings.h',
   'modelines/modeline-parser.c',
@@ -551,15 +559,6 @@ libide_sources = libide_generated_headers + libide_public_sources + [
   'util/ide-ref-ptr.h',
   'util/ide-window-settings.c',
   'util/ide-window-settings.h',
-  'workbench/ide-layout-stack-actions.c',
-  'workbench/ide-layout-stack-actions.h',
-  'workbench/ide-layout-stack-private.h',
-  'workbench/ide-layout-tab-bar.c',
-  'workbench/ide-layout-tab-bar.h',
-  'workbench/ide-layout-tab-bar-private.h',
-  'workbench/ide-layout-tab.c',
-  'workbench/ide-layout-tab.h',
-  'workbench/ide-layout-tab-private.h',
   'workbench/ide-omni-bar-row.c',
   'workbench/ide-omni-bar-row.h',
   'workbench/ide-perspective-menu-button.c',
diff --git a/libide/workbench/ide-workbench.c b/libide/workbench/ide-workbench.c
index 8d4063a..8370f51 100644
--- a/libide/workbench/ide-workbench.c
+++ b/libide/workbench/ide-workbench.c
@@ -26,14 +26,13 @@
 #include "application/ide-application.h"
 #include "application/ide-application-actions.h"
 #include "editor/ide-editor-perspective.h"
+#include "layout/ide-layout-pane.h"
+#include "layout/ide-layout-view.h"
+#include "layout/ide-layout.h"
 #include "greeter/ide-greeter-perspective.h"
 #include "preferences/ide-preferences-perspective.h"
 #include "util/ide-gtk.h"
 #include "util/ide-window-settings.h"
-#include "workbench/ide-layout-pane.h"
-#include "workbench/ide-layout-stack.h"
-#include "workbench/ide-layout-view.h"
-#include "workbench/ide-layout.h"
 #include "workbench/ide-workbench-addin.h"
 #include "workbench/ide-workbench-header-bar.h"
 #include "workbench/ide-workbench-private.h"



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]