diff --git a/core/composer.json b/core/composer.json
index 7ae0ccf..2f9c959 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -95,6 +95,7 @@
         "drupal/editor": "self.version",
         "drupal/entity_reference": "self.version",
         "drupal/field": "self.version",
+        "drupal/field_layout": "self.version",
         "drupal/field_ui": "self.version",
         "drupal/file": "self.version",
         "drupal/filter": "self.version",
@@ -105,6 +106,7 @@
         "drupal/image": "self.version",
         "drupal/inline_form_errors": "self.version",
         "drupal/language": "self.version",
+        "drupal/layout_discovery": "self.version",
         "drupal/link": "self.version",
         "drupal/locale": "self.version",
         "drupal/minimal": "self.version",
diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 73db361..0fbeeed 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -361,6 +361,13 @@ display_variant.plugin:
       type: string
       label: 'UUID'
 
+layout_plugin.settings:
+  type: mapping
+  label: 'Layout settings'
+
+layout_plugin.settings.*:
+  type: layout_plugin.settings
+
 base_entity_reference_field_settings:
   type: mapping
   mapping:
diff --git a/core/core.api.php b/core/core.api.php
index d9cf734..b7262ae 100644
--- a/core/core.api.php
+++ b/core/core.api.php
@@ -2132,6 +2132,17 @@ function hook_display_variant_plugin_alter(array &$definitions) {
 }
 
 /**
+ * Allow modules to alter layout plugin definitions.
+ *
+ * @param \Drupal\Core\Layout\LayoutDefinition[] $definitions
+ *   The array of layout definitions, keyed by plugin ID.
+ */
+function hook_layout_alter(&$definitions) {
+  // Remove a layout.
+  unset($definitions['twocol']);
+}
+
+/**
  * Flush all persistent and static caches.
  *
  * This hook asks your module to clear all of its static caches,
diff --git a/core/lib/Drupal/Core/Layout/Annotation/Layout.php b/core/lib/Drupal/Core/Layout/Annotation/Layout.php
new file mode 100644
index 0000000..0d494c7
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/Annotation/Layout.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace Drupal\Core\Layout\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+use Drupal\Core\Layout\LayoutDefault;
+use Drupal\Core\Layout\LayoutDefinition;
+
+/**
+ * Defines a Layout annotation object.
+ *
+ * Layouts are used to define a list of regions and then output render arrays
+ * in each of the regions, usually using a template.
+ *
+ * Plugin namespace: Plugin\Layout
+ *
+ * @internal
+ *   The layout system is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ *
+ * @see \Drupal\Core\Layout\LayoutInterface
+ * @see \Drupal\Core\Layout\LayoutDefault
+ * @see \Drupal\Core\Layout\LayoutPluginManager
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class Layout extends Plugin {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name.
+   *
+   * @var string
+   *
+   * @ingroup plugin_translatable
+   */
+  public $label;
+
+  /**
+   * An optional description for advanced layouts.
+   *
+   * Sometimes layouts are so complex that the name is insufficient to describe
+   * a layout such that a visually impaired administrator could layout a page
+   * for a non-visually impaired audience. If specified, it will provide a
+   * description that is used for accessibility purposes.
+   *
+   * @var string
+   *
+   * @ingroup plugin_translatable
+   */
+  public $description;
+
+  /**
+   * The human-readable category.
+   *
+   * @var string
+   *
+   * @see \Drupal\Component\Plugin\CategorizingPluginManagerInterface
+   *
+   * @ingroup plugin_translatable
+   */
+  public $category;
+
+  /**
+   * The template file to render this layout (relative to the 'path' given).
+   *
+   * If specified, then the layout_discovery module will register the template
+   * with hook_theme() and the module or theme registering this layout does not
+   * need to do it.
+   *
+   * @var string optional
+   *
+   * @see hook_theme()
+   */
+  public $template;
+
+  /**
+   * The theme hook used to render this layout.
+   *
+   * If specified, it's assumed that the module or theme registering this layout
+   * will also register the theme hook with hook_theme() itself. This is
+   * mutually exclusive with 'template' - you can't specify both.
+   *
+   * @var string optional
+   *
+   * @see hook_theme()
+   */
+  public $theme_hook = 'layout';
+
+  /**
+   * Path (relative to the module or theme) to resources like icon or template.
+   *
+   * @var string optional
+   */
+  public $path;
+
+  /**
+   * The asset library.
+   *
+   * @var string optional
+   */
+  public $library;
+
+  /**
+   * The path to the preview image (relative to the 'path' given).
+   *
+   * @var string optional
+   */
+  public $icon;
+
+  /**
+   * An associative array of regions in this layout.
+   *
+   * The key of the array is the machine name of the region, and the value is
+   * an associative array with the following keys:
+   * - label: (string) The human-readable name of the region.
+   *
+   * Any remaining keys may have special meaning for the given layout plugin,
+   * but are undefined here.
+   *
+   * @var array
+   */
+  public $regions = [];
+
+  /**
+   * The default region.
+   *
+   * @var string
+   */
+  public $default_region;
+
+  /**
+   * The layout plugin class.
+   *
+   * This default value is used for plugins defined in layouts.yml that do not
+   * specify a class themselves.
+   *
+   * @var string
+   */
+  public $class = LayoutDefault::class;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get() {
+    return new LayoutDefinition($this->definition);
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Layout/DerivablePluginDefinitionInterface.php b/core/lib/Drupal/Core/Layout/DerivablePluginDefinitionInterface.php
new file mode 100644
index 0000000..ec6b675
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/DerivablePluginDefinitionInterface.php
@@ -0,0 +1,41 @@
+<?php
+
+namespace Drupal\Core\Layout;
+
+use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
+
+/**
+ * Provides an interface for a derivable plugin definition.
+ *
+ * @see \Drupal\Component\Plugin\Derivative\DeriverInterface
+ * @see \Drupal\Core\Layout\ObjectDefinitionContainerDerivativeDiscoveryDecorator
+ *
+ * @internal
+ *   The layout system is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ *
+ * @todo Move into \Drupal\Component\Plugin\Definition in
+ *   https://www.drupal.org/node/2821189.
+ */
+interface DerivablePluginDefinitionInterface extends PluginDefinitionInterface {
+
+  /**
+   * Gets the name of the deriver of this plugin definition, if it exists.
+   *
+   * @return string|null
+   *   Either the deriver class name, or NULL if the plugin is not derived.
+   */
+  public function getDeriver();
+
+  /**
+   * Sets the deriver of this plugin definition.
+   *
+   * @param string|null $deriver
+   *   Either the name of a class that implements
+   *   \Drupal\Component\Plugin\Derivative\DeriverInterface, or NULL.
+   *
+   * @return $this
+   */
+  public function setDeriver($deriver);
+
+}
diff --git a/core/lib/Drupal/Core/Layout/LayoutDefault.php b/core/lib/Drupal/Core/Layout/LayoutDefault.php
new file mode 100644
index 0000000..6b35f30
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/LayoutDefault.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\Core\Layout;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Plugin\PluginBase;
+
+/**
+ * Provides a default class for Layout plugins.
+ *
+ * @internal
+ *   The layout system is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ */
+class LayoutDefault extends PluginBase implements LayoutInterface {
+
+  /**
+   * The layout definition.
+   *
+   * @var \Drupal\Core\Layout\LayoutDefinition
+   */
+  protected $pluginDefinition;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+    $this->setConfiguration($configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build(array $regions) {
+    $build = array_intersect_key($regions, $this->pluginDefinition->getRegions());
+    $build['#settings'] = $this->getConfiguration();
+    $build['#layout'] = $this->pluginDefinition;
+    $build['#theme'] = $this->pluginDefinition->getThemeHook();
+    if ($library = $this->pluginDefinition->getLibrary()) {
+      $build['#attached']['library'][] = $library;
+    }
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfiguration() {
+    return $this->configuration;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConfiguration(array $configuration) {
+    $this->configuration = NestedArray::mergeDeep($this->defaultConfiguration(), $configuration);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\Core\Layout\LayoutDefinition
+   */
+  public function getPluginDefinition() {
+    return parent::getPluginDefinition();
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Layout/LayoutDefinition.php b/core/lib/Drupal/Core/Layout/LayoutDefinition.php
new file mode 100644
index 0000000..6ed0c89
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/LayoutDefinition.php
@@ -0,0 +1,551 @@
+<?php
+
+namespace Drupal\Core\Layout;
+
+use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
+
+/**
+ * Provides an implementation of a layout definition and its metadata.
+ *
+ * @internal
+ *   The layout system is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ */
+class LayoutDefinition implements PluginDefinitionInterface, DerivablePluginDefinitionInterface {
+
+  /**
+   * The plugin ID.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The name of the provider of this layout definition.
+   *
+   * @todo Make protected after https://www.drupal.org/node/2818653.
+   *
+   * @var string
+   */
+  public $provider;
+
+  /**
+   * The name of the deriver of this layout definition, if any.
+   *
+   * @var string|null
+   */
+  protected $deriver;
+
+  /**
+   * The dependencies of this layout definition.
+   *
+   * @todo Make protected after https://www.drupal.org/node/2821191.
+   *
+   * @var array
+   */
+  public $config_dependencies;
+
+  /**
+   * The human-readable name.
+   *
+   * @var string
+   */
+  protected $label;
+
+  /**
+   * An optional description for advanced layouts.
+   *
+   * @var string
+   */
+  protected $description;
+
+  /**
+   * The human-readable category.
+   *
+   * @var string
+   */
+  protected $category;
+
+  /**
+   * The template file to render this layout (relative to the 'path' given).
+   *
+   * @var string|null
+   */
+  protected $template;
+
+  /**
+   * The path to the template.
+   *
+   * @var string
+   */
+  protected $templatePath;
+
+  /**
+   * The theme hook used to render this layout.
+   *
+   * @var string|null
+   */
+  protected $theme_hook;
+
+  /**
+   * Path (relative to the module or theme) to resources like icon or template.
+   *
+   * @var string
+   */
+  protected $path;
+
+  /**
+   * The asset library.
+   *
+   * @var string|null
+   */
+  protected $library;
+
+  /**
+   * The path to the preview image.
+   *
+   * @var string
+   */
+  protected $icon;
+
+  /**
+   * An associative array of regions in this layout.
+   *
+   * The key of the array is the machine name of the region, and the value is
+   * an associative array with the following keys:
+   * - label: (string) The human-readable name of the region.
+   *
+   * Any remaining keys may have special meaning for the given layout plugin,
+   * but are undefined here.
+   *
+   * @var array
+   */
+  protected $regions = [];
+
+  /**
+   * The default region.
+   *
+   * @var string
+   */
+  protected $default_region;
+
+  /**
+   * The name of the layout class.
+   *
+   * @var string
+   */
+  protected $class;
+
+  /**
+   * Any additional properties and values.
+   *
+   * @var array
+   */
+  protected $additional = [];
+
+  /**
+   * LayoutDefinition constructor.
+   *
+   * @param array $definition
+   *   An array of values from the annotation.
+   */
+  public function __construct(array $definition) {
+    foreach ($definition as $property => $value) {
+      $this->set($property, $value);
+    }
+  }
+
+  /**
+   * Gets any arbitrary property.
+   *
+   * @param string $property
+   *   The property to retrieve.
+   *
+   * @return mixed
+   *   The value for that property, or NULL if the property does not exist.
+   */
+  public function get($property) {
+    if (property_exists($this, $property)) {
+      $value = isset($this->{$property}) ? $this->{$property} : NULL;
+    }
+    else {
+      $value = isset($this->additional[$property]) ? $this->additional[$property] : NULL;
+    }
+    return $value;
+  }
+
+  /**
+   * Sets a value to an arbitrary property.
+   *
+   * @param string $property
+   *   The property to use for the value.
+   * @param mixed $value
+   *   The value to set.
+   *
+   * @return $this
+   */
+  public function set($property, $value) {
+    if (property_exists($this, $property)) {
+      $this->{$property} = $value;
+    }
+    else {
+      $this->additional[$property] = $value;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the unique identifier of the layout definition.
+   *
+   * @return string
+   *   The unique identifier of the layout definition.
+   */
+  public function id() {
+    return $this->id;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getClass() {
+    return $this->class;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setClass($class) {
+    $this->class = $class;
+    return $this;
+  }
+
+  /**
+   * Gets the human-readable name of the layout definition.
+   *
+   * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The human-readable name of the layout definition.
+   */
+  public function getLabel() {
+    return $this->label;
+  }
+
+  /**
+   * Sets the human-readable name of the layout definition.
+   *
+   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $label
+   *   The human-readable name of the layout definition.
+   *
+   * @return $this
+   */
+  public function setLabel($label) {
+    $this->label = $label;
+    return $this;
+  }
+
+  /**
+   * Gets the description of the layout definition.
+   *
+   * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The description of the layout definition.
+   */
+  public function getDescription() {
+    return $this->description;
+  }
+
+  /**
+   * Sets the description of the layout definition.
+   *
+   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $description
+   *   The description of the layout definition.
+   *
+   * @return $this
+   */
+  public function setDescription($description) {
+    $this->description = $description;
+    return $this;
+  }
+
+  /**
+   * Gets the human-readable category of the layout definition.
+   *
+   * @return string|\Drupal\Core\StringTranslation\TranslatableMarkup
+   *   The human-readable category of the layout definition.
+   */
+  public function getCategory() {
+    return $this->category;
+  }
+
+  /**
+   * Sets the human-readable category of the layout definition.
+   *
+   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup $category
+   *   The human-readable category of the layout definition.
+   *
+   * @return $this
+   */
+  public function setCategory($category) {
+    $this->category = $category;
+    return $this;
+  }
+
+  /**
+   * Gets the template name.
+   *
+   * @return string|null
+   *   The template name, if it exists.
+   */
+  public function getTemplate() {
+    return $this->template;
+  }
+
+  /**
+   * Sets the template name.
+   *
+   * @param string|null $template
+   *   The template name.
+   *
+   * @return $this
+   */
+  public function setTemplate($template) {
+    $this->template = $template;
+    return $this;
+  }
+
+  /**
+   * Gets the template path.
+   *
+   * @return string
+   *   The template path.
+   */
+  public function getTemplatePath() {
+    return $this->templatePath;
+  }
+
+  /**
+   * Sets the template path.
+   *
+   * @param string $template_path
+   *   The template path.
+   *
+   * @return $this
+   */
+  public function setTemplatePath($template_path) {
+    $this->templatePath = $template_path;
+    return $this;
+  }
+
+  /**
+   * Gets the theme hook.
+   *
+   * @return string|null
+   *   The theme hook, if it exists.
+   */
+  public function getThemeHook() {
+    return $this->theme_hook;
+  }
+
+  /**
+   * Sets the theme hook.
+   *
+   * @param string $theme_hook
+   *   The theme hook.
+   *
+   * @return $this
+   */
+  public function setThemeHook($theme_hook) {
+    $this->theme_hook = $theme_hook;
+    return $this;
+  }
+
+  /**
+   * Gets the base path for this layout definition.
+   *
+   * @return string
+   *   The base path.
+   */
+  public function getPath() {
+    return $this->path;
+  }
+
+  /**
+   * Sets the base path for this layout definition.
+   *
+   * @param string $path
+   *   The base path.
+   *
+   * @return $this
+   */
+  public function setPath($path) {
+    $this->path = $path;
+    return $this;
+  }
+
+  /**
+   * Gets the asset library for this layout definition.
+   *
+   * @return string|null
+   *   The asset library, if it exists.
+   */
+  public function getLibrary() {
+    return $this->library;
+  }
+
+  /**
+   * Sets the asset library for this layout definition.
+   *
+   * @param string|null $library
+   *   The asset library.
+   *
+   * @return $this
+   */
+  public function setLibrary($library) {
+    $this->library = $library;
+    return $this;
+  }
+
+  /**
+   * Gets the icon path for this layout definition.
+   *
+   * @return string|null
+   *   The icon path, if it exists.
+   */
+  public function getIconPath() {
+    return $this->icon;
+  }
+
+  /**
+   * Sets the icon path for this layout definition.
+   *
+   * @param string|null $icon
+   *   The icon path.
+   *
+   * @return $this
+   */
+  public function setIconPath($icon) {
+    $this->icon = $icon;
+    return $this;
+  }
+
+  /**
+   * Gets the regions for this layout definition.
+   *
+   * @return array[]
+   *    The layout regions. The keys of the array are the machine names of the
+   *    regions, and the values are an associative array with the following
+   *    keys:
+   *     - label: (string) The human-readable name of the region.
+   *    Any remaining keys may have special meaning for the given layout plugin,
+   *    but are undefined here.
+   */
+  public function getRegions() {
+    return $this->regions;
+  }
+
+  /**
+   * Sets the regions for this layout definition.
+   *
+   * @param array[] $regions
+   *   An array of regions, see ::getRegions() for the format.
+   *
+   * @return $this
+   */
+  public function setRegions(array $regions) {
+    $this->regions = $regions;
+    return $this;
+  }
+
+  /**
+   * Gets the machine-readable region names.
+   *
+   * @return string[]
+   *   An array of machine-readable region names.
+   */
+  public function getRegionNames() {
+    return array_keys($this->getRegions());
+  }
+
+  /**
+   * Gets the human-readable region labels.
+   *
+   * @return string[]
+   *   An array of human-readable region labels.
+   */
+  public function getRegionLabels() {
+    $regions = $this->getRegions();
+    return array_combine(array_keys($regions), array_column($regions, 'label'));
+  }
+
+  /**
+   * Gets the default region.
+   *
+   * @return string
+   *   The machine-readable name of the default region.
+   */
+  public function getDefaultRegion() {
+    return $this->default_region;
+  }
+
+  /**
+   * Sets the default region.
+   *
+   * @param string $default_region
+   *   The machine-readable name of the default region.
+   *
+   * @return $this
+   */
+  public function setDefaultRegion($default_region) {
+    $this->default_region = $default_region;
+    return $this;
+  }
+
+  /**
+   * Gets the name of the provider of this layout definition.
+   *
+   * @return string
+   *   The name of the provider of this layout definition.
+   */
+  public function getProvider() {
+    return $this->provider;
+  }
+
+  /**
+   * Gets the config dependencies of this layout definition.
+   *
+   * @return array
+   *   An array of config dependencies.
+   *
+   * @see \Drupal\Core\Plugin\PluginDependencyTrait::calculatePluginDependencies()
+   */
+  public function getConfigDependencies() {
+    return $this->config_dependencies;
+  }
+
+  /**
+   * Sets the config dependencies of this layout definition.
+   *
+   * @param array $config_dependencies
+   *   An array of config dependencies.
+   *
+   * @return $this
+   */
+  public function setConfigDependencies(array $config_dependencies) {
+    $this->config_dependencies = $config_dependencies;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDeriver() {
+    return $this->deriver;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setDeriver($deriver) {
+    $this->deriver = $deriver;
+    return $this;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Layout/LayoutInterface.php b/core/lib/Drupal/Core/Layout/LayoutInterface.php
new file mode 100644
index 0000000..59a4fce
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/LayoutInterface.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Drupal\Core\Layout;
+
+use Drupal\Component\Plugin\DerivativeInspectionInterface;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+
+/**
+ * Provides an interface for static Layout plugins.
+ *
+ * @internal
+ *   The layout system is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ */
+interface LayoutInterface extends PluginInspectionInterface, DerivativeInspectionInterface, ConfigurablePluginInterface {
+
+  /**
+   * Build a render array for layout with regions.
+   *
+   * @param array $regions
+   *   An associative array keyed by region name, containing render arrays
+   *   representing the content that should be placed in each region.
+   *
+   * @return array
+   *   Render array for the layout with regions.
+   */
+  public function build(array $regions);
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\Core\Layout\LayoutDefinition
+   */
+  public function getPluginDefinition();
+
+}
diff --git a/core/lib/Drupal/Core/Layout/LayoutPluginManager.php b/core/lib/Drupal/Core/Layout/LayoutPluginManager.php
new file mode 100644
index 0000000..18840f5
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/LayoutPluginManager.php
@@ -0,0 +1,222 @@
+<?php
+
+namespace Drupal\Core\Layout;
+
+use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
+use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator;
+use Drupal\Core\Layout\Annotation\Layout;
+
+/**
+ * Provides a plugin manager for layouts.
+ *
+ * @internal
+ *   The layout system is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ */
+class LayoutPluginManager extends DefaultPluginManager implements LayoutPluginManagerInterface {
+
+  /**
+   * The theme handler.
+   *
+   * @var \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected $themeHandler;
+
+  /**
+   * LayoutPluginManager constructor.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to invoke the alter hook with.
+   * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
+   *   The theme handler to invoke the alter hook with.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
+    parent::__construct('Plugin/Layout', $namespaces, $module_handler, LayoutInterface::class, Layout::class);
+    $this->themeHandler = $theme_handler;
+
+    $this->setCacheBackend($cache_backend, 'layout');
+    $this->alterInfo('layout');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function providerExists($provider) {
+    return $this->moduleHandler->moduleExists($provider) || $this->themeHandler->themeExists($provider);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDiscovery() {
+    if (!$this->discovery) {
+      $discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
+      $discovery = new YamlDiscoveryDecorator($discovery, 'layouts', $this->moduleHandler->getModuleDirectories() + $this->themeHandler->getThemeDirectories());
+      $discovery = new ObjectDefinitionDiscoveryDecorator($discovery, $this->pluginDefinitionAnnotationName);
+      $discovery = new ObjectDefinitionContainerDerivativeDiscoveryDecorator($discovery);
+      $this->discovery = $discovery;
+    }
+    return $this->discovery;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processDefinition(&$definition, $plugin_id) {
+    parent::processDefinition($definition, $plugin_id);
+
+    if (!$definition instanceof LayoutDefinition) {
+      throw new InvalidPluginDefinitionException($plugin_id, sprintf('The "%s" layout definition must extend %s', $plugin_id, LayoutDefinition::class));
+    }
+
+    // Keep class definitions standard with no leading slash.
+    // @todo Remove this once https://www.drupal.org/node/2824655 is resolved.
+    $definition->setClass(ltrim($definition->getClass(), '\\'));
+
+    // Add the module or theme path to the 'path'.
+    $provider = $definition->getProvider();
+    if ($this->moduleHandler->moduleExists($provider)) {
+      $base_path = $this->moduleHandler->getModule($provider)->getPath();
+    }
+    elseif ($this->themeHandler->themeExists($provider)) {
+      $base_path = $this->themeHandler->getTheme($provider)->getPath();
+    }
+    else {
+      $base_path = '';
+    }
+
+    $path = $definition->getPath();
+    $path = !empty($path) ? $base_path . '/' . $path : $base_path;
+    $definition->setPath($path);
+
+    // Add the base path to the icon path.
+    if ($icon_path = $definition->getIconPath()) {
+      $definition->setIconPath($path . '/' . $icon_path);
+    }
+
+    // Add a dependency on the provider of the library.
+    if ($library = $definition->getLibrary()) {
+      $config_dependencies = $definition->getConfigDependencies();
+      list($library_provider) = explode('/', $library, 2);
+      if ($this->moduleHandler->moduleExists($library_provider)) {
+        $config_dependencies['module'][] = $library_provider;
+      }
+      elseif ($this->themeHandler->themeExists($library_provider)) {
+        $config_dependencies['theme'][] = $library_provider;
+      }
+      $definition->setConfigDependencies($config_dependencies);
+    }
+
+    // If 'template' is set, then we'll derive 'template_path' and 'theme_hook'.
+    $template = $definition->getTemplate();
+    if (!empty($template)) {
+      $template_parts = explode('/', $template);
+
+      $template = array_pop($template_parts);
+      $template_path = $path;
+      if (count($template_parts) > 0) {
+        $template_path .= '/' . implode('/', $template_parts);
+      }
+      $definition->setTemplate($template);
+      // Prepend 'layout__' so the base theme hook will be used.
+      $definition->setThemeHook('layout__' . strtr($template, '-', '_'));
+      $definition->setTemplatePath($template_path);
+    }
+
+    if (!$definition->getDefaultRegion()) {
+      $definition->setDefaultRegion(key($definition->getRegions()));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getThemeImplementations() {
+    $hooks = [];
+    $hooks['layout'] = [
+      'render element' => 'content',
+    ];
+    /** @var \Drupal\Core\Layout\LayoutDefinition[] $definitions */
+    $definitions = $this->getDefinitions();
+    foreach ($definitions as $definition) {
+      if ($template = $definition->getTemplate()) {
+        $hooks[$definition->getThemeHook()] = [
+          'render element' => 'content',
+          'base hook' => 'layout',
+          'template' => $template,
+          'path' => $definition->getTemplatePath(),
+        ];
+      }
+    }
+    return $hooks;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCategories() {
+    // Fetch all categories from definitions and remove duplicates.
+    $categories = array_unique(array_values(array_map(function (LayoutDefinition $definition) {
+      return $definition->getCategory();
+    }, $this->getDefinitions())));
+    natcasesort($categories);
+    return $categories;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\Core\Layout\LayoutDefinition[]
+   */
+  public function getSortedDefinitions(array $definitions = NULL, $label_key = 'label') {
+    // Sort the plugins first by category, then by label.
+    $definitions = isset($definitions) ? $definitions : $this->getDefinitions();
+    // Suppress errors because PHPUnit will indirectly modify the contents,
+    // triggering https://bugs.php.net/bug.php?id=50688.
+    @uasort($definitions, function (LayoutDefinition $a, LayoutDefinition $b) {
+      if ($a->getCategory() != $b->getCategory()) {
+        return strnatcasecmp($a->getCategory(), $b->getCategory());
+      }
+      return strnatcasecmp($a->getLabel(), $b->getLabel());
+    });
+    return $definitions;
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\Core\Layout\LayoutDefinition[][]
+   */
+  public function getGroupedDefinitions(array $definitions = NULL, $label_key = 'label') {
+    $definitions = $this->getSortedDefinitions(isset($definitions) ? $definitions : $this->getDefinitions(), $label_key);
+    $grouped_definitions = [];
+    foreach ($definitions as $id => $definition) {
+      $grouped_definitions[(string) $definition->getCategory()][$id] = $definition;
+    }
+    return $grouped_definitions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLayoutOptions() {
+    $layout_options = [];
+    foreach ($this->getGroupedDefinitions() as $category => $layout_definitions) {
+      foreach ($layout_definitions as $name => $layout_definition) {
+        $layout_options[$category][$name] = $layout_definition->getLabel();
+      }
+    }
+    return $layout_options;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php b/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php
new file mode 100644
index 0000000..e85c5cc
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/LayoutPluginManagerInterface.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\Core\Layout;
+
+use Drupal\Component\Plugin\CategorizingPluginManagerInterface;
+
+/**
+ * Provides the interface for a plugin manager of layouts.
+ *
+ * @internal
+ *   The layout system is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ */
+interface LayoutPluginManagerInterface extends CategorizingPluginManagerInterface {
+
+  /**
+   * Gets theme implementations for layouts.
+   *
+   * @return array
+   *   An associative array of the same format as returned by hook_theme().
+   *
+   * @see hook_theme()
+   */
+  public function getThemeImplementations();
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\Core\Layout\LayoutInterface
+   */
+  public function createInstance($plugin_id, array $configuration = []);
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\Core\Layout\LayoutDefinition|null
+   */
+  public function getDefinition($plugin_id, $exception_on_invalid = TRUE);
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\Core\Layout\LayoutDefinition[]
+   */
+  public function getDefinitions();
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\Core\Layout\LayoutDefinition[]
+   */
+  public function getSortedDefinitions(array $definitions = NULL);
+
+  /**
+   * {@inheritdoc}
+   *
+   * @return \Drupal\Core\Layout\LayoutDefinition[][]
+   */
+  public function getGroupedDefinitions(array $definitions = NULL);
+
+  /**
+   * Returns an array of layout labels grouped by category.
+   *
+   * @return string[][]
+   *   A nested array of labels suitable for #options.
+   */
+  public function getLayoutOptions();
+
+}
diff --git a/core/lib/Drupal/Core/Layout/ObjectDefinitionContainerDerivativeDiscoveryDecorator.php b/core/lib/Drupal/Core/Layout/ObjectDefinitionContainerDerivativeDiscoveryDecorator.php
new file mode 100644
index 0000000..ff27b83
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/ObjectDefinitionContainerDerivativeDiscoveryDecorator.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Core\Layout;
+
+use Drupal\Component\Plugin\Exception\InvalidDeriverException;
+use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
+
+/**
+ * Allows object-based definitions to use derivatives.
+ *
+ * @internal
+ *   The layout system is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ *
+ * @todo In https://www.drupal.org/node/2821189 merge into
+ *    \Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator.
+ */
+class ObjectDefinitionContainerDerivativeDiscoveryDecorator extends ContainerDerivativeDiscoveryDecorator {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getDeriverClass($base_definition) {
+    $class = NULL;
+    if ($base_definition instanceof DerivablePluginDefinitionInterface && $class = $base_definition->getDeriver()) {
+      if (!class_exists($class)) {
+        throw new InvalidDeriverException(sprintf('Plugin (%s) deriver "%s" does not exist.', $base_definition['id'], $class));
+      }
+      if (!is_subclass_of($class, '\Drupal\Component\Plugin\Derivative\DeriverInterface')) {
+        throw new InvalidDeriverException(sprintf('Plugin (%s) deriver "%s" must implement \Drupal\Component\Plugin\Derivative\DeriverInterface.', $base_definition['id'], $class));
+      }
+    }
+    return $class;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Layout/ObjectDefinitionDiscoveryDecorator.php b/core/lib/Drupal/Core/Layout/ObjectDefinitionDiscoveryDecorator.php
new file mode 100644
index 0000000..d86a5b3
--- /dev/null
+++ b/core/lib/Drupal/Core/Layout/ObjectDefinitionDiscoveryDecorator.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace Drupal\Core\Layout;
+
+use Drupal\Component\Plugin\Discovery\DiscoveryInterface;
+use Drupal\Component\Plugin\Discovery\DiscoveryTrait;
+
+/**
+ * Ensures that all array-based definitions are converted to objects.
+ *
+ * @internal
+ *   The layout system is currently experimental and should only be leveraged by
+ *   experimental modules and development releases of contributed modules.
+ *
+ * @todo Move into \Drupal\Component\Plugin\Discovery in
+ *   https://www.drupal.org/node/2822752.
+ */
+class ObjectDefinitionDiscoveryDecorator implements DiscoveryInterface {
+
+  use DiscoveryTrait;
+
+  /**
+   * The decorated plugin discovery.
+   *
+   * @var \Drupal\Component\Plugin\Discovery\DiscoveryInterface
+   */
+  protected $decorated;
+
+  /**
+   * The name of the annotation that contains the plugin definition.
+   *
+   * The class corresponding to this name must implement
+   * \Drupal\Component\Annotation\AnnotationInterface.
+   *
+   * @var string|null
+   */
+  protected $pluginDefinitionAnnotationName;
+
+  /**
+   * ObjectDefinitionDiscoveryDecorator constructor.
+   *
+   * @param \Drupal\Component\Plugin\Discovery\DiscoveryInterface $decorated
+   *   The discovery object that is being decorated.
+   * @param string $plugin_definition_annotation_name
+   *   The name of the annotation that contains the plugin definition.
+   */
+  public function __construct(DiscoveryInterface $decorated, $plugin_definition_annotation_name) {
+    $this->decorated = $decorated;
+    $this->pluginDefinitionAnnotationName = $plugin_definition_annotation_name;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDefinitions() {
+    $definitions = $this->decorated->getDefinitions();
+    foreach ($definitions as $id => $definition) {
+      if (is_array($definition)) {
+        $definitions[$id] = (new $this->pluginDefinitionAnnotationName($definition))->get();
+      }
+    }
+    return $definitions;
+  }
+
+  /**
+   * Passes through all unknown calls onto the decorated object.
+   *
+   * @param string $method
+   *   The method to call on the decorated plugin discovery.
+   * @param array $args
+   *   The arguments to send to the method.
+   *
+   * @return mixed
+   *   The method result.
+   */
+  public function __call($method, $args) {
+    return call_user_func_array([$this->decorated, $method], $args);
+  }
+
+}
diff --git a/core/modules/field_layout/config/schema/field_layout.schema.yml b/core/modules/field_layout/config/schema/field_layout.schema.yml
new file mode 100644
index 0000000..3b53b3f
--- /dev/null
+++ b/core/modules/field_layout/config/schema/field_layout.schema.yml
@@ -0,0 +1,16 @@
+core.entity_view_display.*.*.*.third_party.field_layout:
+  type: field_layout.third_party_settings
+
+core.entity_form_display.*.*.*.third_party.field_layout:
+  type: field_layout.third_party_settings
+
+field_layout.third_party_settings:
+  type: mapping
+  label: 'Per view mode field layout settings'
+  mapping:
+    id:
+      type: string
+      label: 'Layout ID'
+    settings:
+      type: layout_plugin.settings.[%parent.id]
+      label: 'Layout settings'
diff --git a/core/modules/field_layout/field_layout.info.yml b/core/modules/field_layout/field_layout.info.yml
new file mode 100644
index 0000000..237f18d
--- /dev/null
+++ b/core/modules/field_layout/field_layout.info.yml
@@ -0,0 +1,8 @@
+name: 'Field Layout'
+type: module
+description: 'Adds layout capabilities to the Field UI.'
+package: Core (Experimental)
+version: VERSION
+core: 8.x
+dependencies:
+  - layout_discovery
diff --git a/core/modules/field_layout/field_layout.install b/core/modules/field_layout/field_layout.install
new file mode 100644
index 0000000..6f94674
--- /dev/null
+++ b/core/modules/field_layout/field_layout.install
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains install and update functions for Field Layout.
+ */
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Implements hook_install().
+ */
+function field_layout_install() {
+  // Save each entity display in order to trigger ::preSave().
+  $entity_save = function (EntityInterface $entity) {
+    $entity->save();
+  };
+  array_map($entity_save, EntityViewDisplay::loadMultiple());
+  array_map($entity_save, EntityFormDisplay::loadMultiple());
+
+  // Invalidate the render cache since all content will now have a layout.
+  Cache::invalidateTags(['rendered']);
+}
diff --git a/core/modules/field_layout/field_layout.layouts.yml b/core/modules/field_layout/field_layout.layouts.yml
new file mode 100644
index 0000000..b8c6ef3
--- /dev/null
+++ b/core/modules/field_layout/field_layout.layouts.yml
@@ -0,0 +1,21 @@
+onecol:
+  label: 'One column'
+  path: layouts/onecol
+  template: field-layout--onecol
+  category: 'Columns: 1'
+  default_region: content
+  regions:
+    content:
+      label: Content
+twocol:
+  label: 'Two column'
+  path: layouts/twocol
+  template: field-layout--twocol
+  library: field_layout/drupal.field_layout.twocol
+  category: 'Columns: 2'
+  default_region: left
+  regions:
+    left:
+      label: Left
+    right:
+      label: Right
diff --git a/core/modules/field_layout/field_layout.libraries.yml b/core/modules/field_layout/field_layout.libraries.yml
new file mode 100644
index 0000000..d87df5e
--- /dev/null
+++ b/core/modules/field_layout/field_layout.libraries.yml
@@ -0,0 +1,5 @@
+drupal.field_layout.twocol:
+  version: VERSION
+  css:
+    layout:
+      layouts/twocol/twocol.layout.css: {}
diff --git a/core/modules/field_layout/field_layout.module b/core/modules/field_layout/field_layout.module
new file mode 100644
index 0000000..11dcdc1
--- /dev/null
+++ b/core/modules/field_layout/field_layout.module
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Provides hook implementations for Field Layout.
+ */
+
+use Drupal\Core\Entity\ContentEntityFormInterface;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+use Drupal\field_layout\Entity\FieldLayoutEntityFormDisplay;
+use Drupal\field_layout\Entity\FieldLayoutEntityViewDisplay;
+use Drupal\field_layout\FieldLayoutBuilder;
+use Drupal\field_layout\Form\FieldLayoutEntityFormDisplayEditForm;
+use Drupal\field_layout\Form\FieldLayoutEntityViewDisplayEditForm;
+
+/**
+ * Implements hook_help().
+ */
+function field_layout_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    case 'help.page.field_layout':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('The Field Layout module allows you to arrange fields into regions on forms and displays of entities such as nodes and users.') . '</p>';
+      $output .= '<p>' . t('For more information, see the <a href=":field-layout-documentation">online documentation for the Field Layout module</a>.', [':field-layout-documentation' => 'https://www.drupal.org/documentation/modules/@todo_once_module_name_is_decided_upon']) . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_entity_type_alter().
+ */
+function field_layout_entity_type_alter(array &$entity_types) {
+  /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
+  $entity_types['entity_view_display']->setClass(FieldLayoutEntityViewDisplay::class);
+  $entity_types['entity_form_display']->setClass(FieldLayoutEntityFormDisplay::class);
+
+  // The form classes are only needed when Field UI is installed.
+  if (\Drupal::moduleHandler()->moduleExists('field_ui')) {
+    $entity_types['entity_view_display']->setFormClass('edit', FieldLayoutEntityViewDisplayEditForm::class);
+    $entity_types['entity_form_display']->setFormClass('edit', FieldLayoutEntityFormDisplayEditForm::class);
+  }
+}
+
+/**
+ * Implements hook_entity_view_alter().
+ */
+function field_layout_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
+  if ($display instanceof EntityDisplayWithLayoutInterface) {
+    \Drupal::classResolver()->getInstanceFromDefinition(FieldLayoutBuilder::class)
+      ->build($build, $display, 'view');
+  }
+}
+
+/**
+ * Implements hook_form_alter().
+ */
+function field_layout_form_alter(&$form, FormStateInterface $form_state, $form_id) {
+  $form_object = $form_state->getFormObject();
+  if ($form_object instanceof ContentEntityFormInterface && $display = $form_object->getFormDisplay($form_state)) {
+    if ($display instanceof EntityDisplayWithLayoutInterface) {
+      \Drupal::classResolver()->getInstanceFromDefinition(FieldLayoutBuilder::class)
+        ->build($form, $display, 'form');
+    }
+  }
+}
diff --git a/core/modules/field_layout/layouts/onecol/field-layout--onecol.html.twig b/core/modules/field_layout/layouts/onecol/field-layout--onecol.html.twig
new file mode 100644
index 0000000..cce4893
--- /dev/null
+++ b/core/modules/field_layout/layouts/onecol/field-layout--onecol.html.twig
@@ -0,0 +1,24 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a one column layout.
+ *
+ * Available variables:
+ * - content: The content for this layout.
+ * - attributes: HTML attributes for the layout <div>.
+ *
+ * @ingroup themeable
+ */
+#}
+{%
+set classes = [
+'field-layout--onecol',
+]
+%}
+{% if content %}
+<div{{ attributes.addClass(classes) }}>
+  <div class="field-layout-region field-layout-region--content">
+    {{ content }}
+  </div>
+</div>
+{% endif %}
diff --git a/core/modules/field_layout/layouts/twocol/field-layout--twocol.html.twig b/core/modules/field_layout/layouts/twocol/field-layout--twocol.html.twig
new file mode 100644
index 0000000..4dffc01
--- /dev/null
+++ b/core/modules/field_layout/layouts/twocol/field-layout--twocol.html.twig
@@ -0,0 +1,28 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a two column layout.
+ *
+ * Available variables:
+ * - content: The content for this layout.
+ * - attributes: HTML attributes for the layout <div>.
+ *
+ * @ingroup themeable
+ */
+#}
+{%
+set classes = [
+'field-layout--twocol',
+]
+%}
+{% if content %}
+  <div{{ attributes.addClass(classes) }}>
+    <div class="field-layout-region field-layout-region--left">
+      {{ content.left }}
+    </div>
+
+    <div class="field-layout-region field-layout-region--right">
+      {{ content.right }}
+    </div>
+  </div>
+{% endif %}
diff --git a/core/modules/field_layout/layouts/twocol/twocol.layout.css b/core/modules/field_layout/layouts/twocol/twocol.layout.css
new file mode 100644
index 0000000..8e2f623
--- /dev/null
+++ b/core/modules/field_layout/layouts/twocol/twocol.layout.css
@@ -0,0 +1,14 @@
+.field-layout--twocol {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+}
+.field-layout--twocol > .field-layout-region {
+  flex: 0 1 50%;
+  max-width: 50%;
+}
+
+.field-layout--twocol > .field-layout-region--left {
+  max-width: calc(50% - 10px);
+  margin-right: 10px;
+}
diff --git a/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php
new file mode 100644
index 0000000..3623515
--- /dev/null
+++ b/core/modules/field_layout/src/Display/EntityDisplayWithLayoutInterface.php
@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\field_layout\Display;
+
+use Drupal\Core\Entity\Display\EntityDisplayInterface;
+use Drupal\Core\Layout\LayoutInterface;
+
+/**
+ * Provides a common interface for entity displays that have layout.
+ */
+interface EntityDisplayWithLayoutInterface extends EntityDisplayInterface {
+
+  /**
+   * Gets the default region.
+   *
+   * @return string
+   *   The default region for this display.
+   */
+  public function getDefaultRegion();
+
+  /**
+   * Gets the layout ID for this display.
+   *
+   * @return string
+   *   The layout ID.
+   */
+  public function getLayoutId();
+
+  /**
+   * Gets the layout settings for this display.
+   *
+   * @return mixed[]
+   *   The layout settings.
+   */
+  public function getLayoutSettings();
+
+  /**
+   * Sets the layout ID for this display.
+   *
+   * @param string|null $layout_id
+   *   Either a valid layout ID, or NULL to remove the layout setting.
+   * @param array $layout_settings
+   *   (optional) An array of settings for this layout.
+   *
+   * @return $this
+   */
+  public function setLayoutId($layout_id, array $layout_settings = []);
+
+  /**
+   * Sets the layout for this display.
+   *
+   * @param \Drupal\Core\Layout\LayoutInterface $layout
+   *   A layout.
+   *
+   * @return $this
+   */
+  public function setLayout(LayoutInterface $layout);
+
+  /**
+   * Gets the layout plugin for this display.
+   *
+   * @return \Drupal\Core\Layout\LayoutInterface
+   *   The layout plugin.
+   */
+  public function getLayoutPlugin();
+
+}
diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php
new file mode 100644
index 0000000..c5c9cda
--- /dev/null
+++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityDisplayTrait.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace Drupal\field_layout\Entity;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Layout\LayoutInterface;
+
+/**
+ * Provides shared code for entity displays.
+ */
+trait FieldLayoutEntityDisplayTrait {
+
+  /**
+   * Gets a layout definition.
+   *
+   * @param string $layout_id
+   *   The layout ID.
+   *
+   * @return \Drupal\Core\Layout\LayoutDefinition
+   *   The layout definition.
+   */
+  protected function getLayoutDefinition($layout_id) {
+    return \Drupal::service('plugin.manager.layout')->getDefinition($layout_id);
+  }
+
+  /**
+   * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayoutId().
+   */
+  public function getLayoutId() {
+    return $this->getThirdPartySetting('field_layout', 'id');
+  }
+
+  /**
+   * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayoutSettings().
+   */
+  public function getLayoutSettings() {
+    return $this->getThirdPartySetting('field_layout', 'settings', []);
+  }
+
+  /**
+   * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::setLayoutId().
+   */
+  public function setLayoutId($layout_id, array $layout_settings = []) {
+    $this->setThirdPartySetting('field_layout', 'id', $layout_id);
+    $this->setThirdPartySetting('field_layout', 'settings', $layout_settings);
+    return $this;
+  }
+
+  /**
+   * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::setLayout().
+   */
+  public function setLayout(LayoutInterface $layout) {
+    $this->setLayoutId($layout->getPluginId(), $layout->getConfiguration());
+    return $this;
+  }
+
+  /**
+   * Implements \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface::getLayoutPlugin().
+   */
+  public function getLayoutPlugin() {
+    return \Drupal::service('plugin.manager.layout')->createInstance($this->getLayoutId(), $this->getLayoutSettings());
+  }
+
+  /**
+   * Overrides \Drupal\Core\Entity\EntityDisplayBase::preSave().
+   */
+  public function preSave(EntityStorageInterface $storage) {
+    if (!$this->getLayoutId()) {
+      $this->setLayoutId('onecol');
+    }
+    else {
+      $this->setLayout($this->getLayoutPlugin());
+    }
+
+    parent::preSave($storage);
+  }
+
+  /**
+   * Overrides \Drupal\Core\Entity\EntityDisplayBase::calculateDependencies().
+   *
+   * @todo Remove once https://www.drupal.org/node/2821191 is resolved.
+   *
+   * @see \Drupal\Core\Plugin\PluginDependencyTrait::calculatePluginDependencies()
+   */
+  public function calculateDependencies() {
+    parent::calculateDependencies();
+
+    /** @var \Drupal\Core\Layout\LayoutInterface $layout */
+    if ($layout = $this->getLayoutPlugin()) {
+      $definition = $layout->getPluginDefinition();
+
+      if (!in_array($definition->getProvider(), ['core', 'component'])) {
+        $this->addDependency('module', $definition->getProvider());
+      }
+      if ($config_dependencies = $definition->getConfigDependencies()) {
+        $this->addDependencies($config_dependencies);
+      }
+      if ($layout_dependencies = $layout->calculateDependencies()) {
+        $this->addDependencies($layout_dependencies);
+      }
+    }
+  }
+
+}
diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php
new file mode 100644
index 0000000..2425eb7
--- /dev/null
+++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityFormDisplay.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\field_layout\Entity;
+
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+
+/**
+ * Provides an entity form display entity that has a layout.
+ */
+class FieldLayoutEntityFormDisplay extends EntityFormDisplay implements EntityDisplayWithLayoutInterface {
+
+  use FieldLayoutEntityDisplayTrait;
+
+  /**
+   * {@inheritdoc}
+   *
+   * This cannot be provided by the trait due to https://bugs.php.net/bug.php?id=71414
+   * which is fixed in PHP 7.0.6.
+   */
+  public function getDefaultRegion() {
+    return $this->getLayoutDefinition($this->getLayoutId() ?: 'onecol')->getDefaultRegion();
+  }
+
+}
diff --git a/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php b/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php
new file mode 100644
index 0000000..4e6e888
--- /dev/null
+++ b/core/modules/field_layout/src/Entity/FieldLayoutEntityViewDisplay.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\field_layout\Entity;
+
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+
+/**
+ * Provides an entity view display entity that has a layout.
+ */
+class FieldLayoutEntityViewDisplay extends EntityViewDisplay implements EntityDisplayWithLayoutInterface {
+
+  use FieldLayoutEntityDisplayTrait;
+
+  /**
+   * {@inheritdoc}
+   *
+   * This cannot be provided by the trait due to https://bugs.php.net/bug.php?id=71414
+   * which is fixed in PHP 7.0.6.
+   */
+  public function getDefaultRegion() {
+    return $this->getLayoutDefinition($this->getLayoutId() ?: 'onecol')->getDefaultRegion();
+  }
+
+}
diff --git a/core/modules/field_layout/src/FieldLayoutBuilder.php b/core/modules/field_layout/src/FieldLayoutBuilder.php
new file mode 100644
index 0000000..9b7d311
--- /dev/null
+++ b/core/modules/field_layout/src/FieldLayoutBuilder.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Drupal\field_layout;
+
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+use Drupal\Core\Layout\LayoutPluginManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Builds a field layout.
+ */
+class FieldLayoutBuilder implements ContainerInjectionInterface {
+
+  /**
+   * The layout plugin manager.
+   *
+   * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
+   */
+  protected $layoutPluginManager;
+
+  /**
+   * The entity field manager.
+   *
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * FieldLayoutBuilder constructor.
+   *
+   * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_plugin_manager
+   *   The layout plugin manager.
+   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+   *   The entity field manager.
+   */
+  public function __construct(LayoutPluginManagerInterface $layout_plugin_manager, EntityFieldManagerInterface $entity_field_manager) {
+    $this->layoutPluginManager = $layout_plugin_manager;
+    $this->entityFieldManager = $entity_field_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.layout'),
+      $container->get('entity_field.manager')
+    );
+  }
+
+  /**
+   * Applies the layout to an entity build.
+   *
+   * @param array $build
+   *   A renderable array representing the entity content or form.
+   * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display
+   *   The entity display holding the display options configured for the entity
+   *   components.
+   * @param string $display_context
+   *   The display context, either 'form' or 'view'. If in a 'form' context, an
+   *   alternate method will be used to render fields in their regions.
+   */
+  public function build(array &$build, EntityDisplayWithLayoutInterface $display, $display_context) {
+    $layout_definition = $this->layoutPluginManager->getDefinition($display->getLayoutId(), FALSE);
+    if ($layout_definition && $fields = $this->getFields($build, $display, $display_context)) {
+      // Add the regions to the $build in the correct order.
+      $fill = [];
+      if ($display_context === 'form') {
+        $fill['#process'][] = '\Drupal\Core\Render\Element\RenderElement::processGroup';
+        $fill['#pre_render'][] = '\Drupal\Core\Render\Element\RenderElement::preRenderGroup';
+      }
+      $regions = array_fill_keys($layout_definition->getRegionNames(), $fill);
+
+      foreach ($fields as $name => $field) {
+        // If this is a form, #group can be used to relocate the fields. This
+        // avoids breaking hook_form_alter() implementations by not actually
+        // moving the field in the form structure.
+        if ($display_context === 'form') {
+          $build[$name]['#group'] = $field['region'];
+        }
+        // Otherwise, move the field from the top-level of $build into a
+        // region-specific section.
+        else {
+          $regions[$field['region']][$name] = $build[$name];
+          unset($build[$name]);
+        }
+      }
+      $build['field_layout'] = $display->getLayoutPlugin()->build($regions);
+    }
+  }
+
+  /**
+   * Gets the fields that need to be processed.
+   *
+   * @param array $build
+   *   A renderable array representing the entity content or form.
+   * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $display
+   *   The entity display holding the display options configured for the entity
+   *   components.
+   * @param string $display_context
+   *   The display context, either 'form' or 'view'.
+   *
+   * @return array
+   *   An array of configurable fields present in the build.
+   */
+  protected function getFields(array $build, EntityDisplayWithLayoutInterface $display, $display_context) {
+    $components = $display->getComponents();
+
+    $field_definitions = $this->entityFieldManager->getFieldDefinitions($display->getTargetEntityTypeId(), $display->getTargetBundle());
+    $non_configurable_fields = array_filter($field_definitions, function (FieldDefinitionInterface $field_definition) use ($display_context) {
+      return !$field_definition->isDisplayConfigurable($display_context);
+    });
+    // Remove non-configurable fields.
+    $components = array_diff_key($components, $non_configurable_fields);
+
+    // Only include fields present in the build.
+    $components = array_intersect_key($components, $build);
+
+    return $components;
+  }
+
+}
diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php
new file mode 100644
index 0000000..6cfca76
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php
@@ -0,0 +1,205 @@
+<?php
+
+namespace Drupal\field_layout\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\SubformState;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+
+/**
+ * Provides shared code for entity display forms.
+ */
+trait FieldLayoutEntityDisplayFormTrait {
+
+  /**
+   * The field layout plugin manager.
+   *
+   * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
+   */
+  protected $layoutPluginManager;
+
+  /**
+   * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::getRegions().
+   */
+  public function getRegions() {
+    $regions = [];
+
+    $layout_definition = $this->layoutPluginManager->getDefinition($this->getEntity()->getLayoutId() ?: 'onecol');
+    foreach ($layout_definition->getRegions() as $name => $region) {
+      $regions[$name] = [
+        'title' => $region['label'],
+        'message' => $this->t('No field is displayed.'),
+      ];
+    }
+
+    $regions['hidden'] = [
+      'title' => $this->t('Disabled', [], ['context' => 'Plural']),
+      'message' => $this->t('No field is hidden.'),
+    ];
+
+    return $regions;
+  }
+
+  /**
+   * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::form().
+   */
+  public function form(array $form, FormStateInterface $form_state) {
+    $form = parent::form($form, $form_state);
+
+    $form['field_layouts'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Layout settings'),
+    ];
+
+    $layout_plugin = $this->getLayoutPlugin($this->getEntity(), $form_state);
+
+    $form['field_layouts']['field_layout'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Select a layout'),
+      '#options' => $this->layoutPluginManager->getLayoutOptions(),
+      '#default_value' => $layout_plugin->getPluginId(),
+      '#ajax' => [
+        'callback' => '::settingsAjax',
+        'wrapper' => 'field-layout-settings-wrapper',
+        'trigger_as' => ['name' => 'field_layout_change'],
+      ],
+    ];
+    $form['field_layouts']['submit'] = [
+      '#type' => 'submit',
+      '#name' => 'field_layout_change',
+      '#value' => $this->t('Change layout'),
+      '#submit' => ['::settingsAjaxSubmit'],
+      '#attributes' => ['class' => ['js-hide']],
+      '#ajax' => [
+        'callback' => '::settingsAjax',
+        'wrapper' => 'field-layout-settings-wrapper',
+      ],
+    ];
+
+    $form['field_layouts']['settings_wrapper'] = [
+      '#type' => 'container',
+      '#id' => 'field-layout-settings-wrapper',
+      '#tree' => TRUE,
+    ];
+
+    if ($layout_plugin instanceof PluginFormInterface) {
+      $form['field_layouts']['settings_wrapper']['layout_settings'] = [];
+      $subform_state = SubformState::createForSubform($form['field_layouts']['settings_wrapper']['layout_settings'], $form, $form_state);
+      $form['field_layouts']['settings_wrapper']['layout_settings'] = $layout_plugin->buildConfigurationForm($form['field_layouts']['settings_wrapper']['layout_settings'], $subform_state);
+    }
+
+    return $form;
+  }
+
+  /**
+   * Gets the layout plugin for the currently selected field layout.
+   *
+   * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $entity
+   *   The current form entity.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return \Drupal\Core\Layout\LayoutInterface
+   *   The layout plugin.
+   */
+  protected function getLayoutPlugin(EntityDisplayWithLayoutInterface $entity, FormStateInterface $form_state) {
+    if (!$layout_plugin = $form_state->get('layout_plugin')) {
+      $stored_layout_id = $entity->getLayoutId();
+      // If a new field layout was selected, use that. Otherwise try to use the
+      // stored layout. Finally, fall back to the one column layout.
+      $layout_id = $form_state->getValue('field_layout') ?: ($stored_layout_id ?: 'onecol');
+      // If the current layout is the stored layout, use the stored layout
+      // settings. Otherwise leave the settings empty.
+      $layout_settings = $layout_id === $stored_layout_id ? $entity->getLayoutSettings() : [];
+
+      $layout_plugin = $this->layoutPluginManager->createInstance($layout_id, $layout_settings);
+      $form_state->set('layout_plugin', $layout_plugin);
+    }
+    return $layout_plugin;
+  }
+
+  /**
+   * Ajax callback for the field layout settings form.
+   */
+  public static function settingsAjax($form, FormStateInterface $form_state) {
+    return $form['field_layouts']['settings_wrapper'];
+  }
+
+  /**
+   * Submit handler for the non-JS case.
+   */
+  public function settingsAjaxSubmit($form, FormStateInterface $form_state) {
+    $form_state->set('layout_plugin', NULL);
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::validateForm().
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    parent::validateForm($form, $form_state);
+
+    $layout_plugin = $this->getLayoutPlugin($this->getEntity(), $form_state);
+    if ($layout_plugin instanceof PluginFormInterface) {
+      $subform_state = SubformState::createForSubform($form['field_layouts']['settings_wrapper']['layout_settings'], $form, $form_state);
+      $layout_plugin->validateConfigurationForm($form['field_layouts']['settings_wrapper']['layout_settings'], $subform_state);
+    }
+  }
+
+  /**
+   * Overrides \Drupal\field_ui\Form\EntityDisplayFormBase::submitForm().
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+
+    $entity = $this->getEntity();
+    $layout_plugin = $this->getLayoutPlugin($entity, $form_state);
+    if ($layout_plugin instanceof PluginFormInterface) {
+      $layout_plugin->submitConfigurationForm($form['field_layouts']['settings_wrapper']['layout_settings'], SubformState::createForSubform($form['field_layouts']['settings_wrapper']['layout_settings'], $form, $form_state));
+    }
+
+    // If the layout is changing, reset all fields.
+    if ($this->updateLayout($entity, $form_state)) {
+      // @todo Devise a mechanism for mapping old regions to new ones in
+      //   https://www.drupal.org/node/2796877.
+      $new_region = $entity->getDefaultRegion();
+      foreach ($form_state->getValue('fields') as $field_name => $values) {
+        if (($component = $entity->getComponent($field_name)) && $new_region !== 'hidden') {
+          $component['region'] = $new_region;
+          $entity->setComponent($field_name, $component);
+        }
+        else {
+          $entity->removeComponent($field_name);
+        }
+      }
+    }
+  }
+
+  /**
+   * Updates the entity with a new layout.
+   *
+   * @param \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface $entity
+   *   The display entity.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current form state.
+   *
+   * @return bool
+   *   Returns TRUE if the layout has changed, FALSE if it is the same.
+   */
+  protected function updateLayout(EntityDisplayWithLayoutInterface $entity, FormStateInterface $form_state) {
+    $old_layout = $entity->getLayoutId();
+    $new_layout = $form_state->getValue('field_layout');
+    $entity->setLayout($this->getLayoutPlugin($entity, $form_state));
+    return $old_layout !== $new_layout;
+  }
+
+  /**
+   * Gets the form entity.
+   *
+   * @return \Drupal\field_layout\Display\EntityDisplayWithLayoutInterface
+   *   The current form entity.
+   */
+  abstract public function getEntity();
+
+}
diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php
new file mode 100644
index 0000000..6a145b3
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityFormDisplayEditForm.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\field_layout\Form;
+
+use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Layout\LayoutPluginManagerInterface;
+use Drupal\field_ui\Form\EntityFormDisplayEditForm;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Edit form for the EntityFormDisplay entity type.
+ */
+class FieldLayoutEntityFormDisplayEditForm extends EntityFormDisplayEditForm {
+
+  use FieldLayoutEntityDisplayFormTrait;
+
+  /**
+   * FieldLayoutEntityFormDisplayEditForm constructor.
+   *
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
+   *   The field type manager.
+   * @param \Drupal\Component\Plugin\PluginManagerBase $plugin_manager
+   *   The widget plugin manager.
+   * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_plugin_manager
+   *   The layout plugin manager.
+   */
+  public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, LayoutPluginManagerInterface $layout_plugin_manager) {
+    parent::__construct($field_type_manager, $plugin_manager);
+    $this->layoutPluginManager = $layout_plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.field.field_type'),
+      $container->get('plugin.manager.field.widget'),
+      $container->get('plugin.manager.layout')
+    );
+  }
+
+}
diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php
new file mode 100644
index 0000000..0cf98e3
--- /dev/null
+++ b/core/modules/field_layout/src/Form/FieldLayoutEntityViewDisplayEditForm.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\field_layout\Form;
+
+use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Core\Field\FieldTypePluginManagerInterface;
+use Drupal\Core\Layout\LayoutPluginManagerInterface;
+use Drupal\field_ui\Form\EntityViewDisplayEditForm;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Edit form for the EntityViewDisplay entity type.
+ */
+class FieldLayoutEntityViewDisplayEditForm extends EntityViewDisplayEditForm {
+
+  use FieldLayoutEntityDisplayFormTrait;
+
+  /**
+   * FieldLayoutEntityViewDisplayEditForm constructor.
+   *
+   * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
+   *   The field type manager.
+   * @param \Drupal\Component\Plugin\PluginManagerBase $plugin_manager
+   *   The formatter plugin manager.
+   * @param \Drupal\Core\Layout\LayoutPluginManagerInterface $layout_plugin_manager
+   *   The field layout plugin manager.
+   */
+  public function __construct(FieldTypePluginManagerInterface $field_type_manager, PluginManagerBase $plugin_manager, LayoutPluginManagerInterface $layout_plugin_manager) {
+    parent::__construct($field_type_manager, $plugin_manager);
+    $this->layoutPluginManager = $layout_plugin_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.field.field_type'),
+      $container->get('plugin.manager.field.formatter'),
+      $container->get('plugin.manager.layout')
+    );
+  }
+
+}
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/config/schema/field_layout_test.schema.yml b/core/modules/field_layout/tests/modules/field_layout_test/config/schema/field_layout_test.schema.yml
new file mode 100644
index 0000000..c397e73
--- /dev/null
+++ b/core/modules/field_layout/tests/modules/field_layout_test/config/schema/field_layout_test.schema.yml
@@ -0,0 +1,7 @@
+layout_plugin.settings.field_layout_test_plugin:
+  type: layout_plugin.settings
+  label: 'Layout test plugin settings'
+  mapping:
+    setting_1:
+      type: string
+      label: 'Setting 1'
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml
new file mode 100644
index 0000000..4d699e4
--- /dev/null
+++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.info.yml
@@ -0,0 +1,8 @@
+name: 'Field Layout test'
+type: module
+description: 'Support module for Field Layout tests.'
+core: 8.x
+package: Testing
+version: VERSION
+dependencies:
+  - entity_test
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml
new file mode 100644
index 0000000..bcea288
--- /dev/null
+++ b/core/modules/field_layout/tests/modules/field_layout_test/field_layout_test.routing.yml
@@ -0,0 +1,7 @@
+entity.entity_test.test_view_mode:
+  path: '/entity_test/{entity_test}/test'
+  defaults:
+    _entity_view: 'entity_test.test'
+    _title: 'Test test view mode'
+  requirements:
+    _entity_access: 'entity_test.view'
diff --git a/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/FieldLayoutTestPlugin.php b/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/FieldLayoutTestPlugin.php
new file mode 100644
index 0000000..ae49263
--- /dev/null
+++ b/core/modules/field_layout/tests/modules/field_layout_test/src/Plugin/Layout/FieldLayoutTestPlugin.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Drupal\field_layout_test\Plugin\Layout;
+
+use Drupal\Core\Layout\LayoutDefault;
+
+/**
+ * @todo.
+ *
+ * @Layout(
+ *   id = "field_layout_test_plugin",
+ *   label = @Translation("Layout plugin (with settings)"),
+ *   category = @Translation("Layout test"),
+ *   description = @Translation("Test layout"),
+ *   template = "templates/layout-test-plugin",
+ *   regions = {
+ *     "main" = {
+ *       "label" = @Translation("Main Region")
+ *     }
+ *   },
+ *   config_dependencies = {
+ *     "module" = {
+ *       "dependency_from_annotation",
+ *     },
+ *   },
+ * )
+ */
+class FieldLayoutTestPlugin extends LayoutDefault {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'setting_1' => 'Default',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function calculateDependencies() {
+    $dependencies = parent::calculateDependencies();
+    $dependencies['module'][] = 'dependency_from_calculateDependencies';
+    return $dependencies;
+  }
+
+}
diff --git a/core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php b/core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php
new file mode 100644
index 0000000..35b4bdd
--- /dev/null
+++ b/core/modules/field_layout/tests/src/Functional/FieldLayoutTest.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Drupal\Tests\field_layout\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests using field layout for entity displays.
+ *
+ * @group field_layout
+ */
+class FieldLayoutTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['field_layout', 'field_ui', 'node'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->createContentType([
+      'type' => 'article',
+    ]);
+    $this->createNode([
+      'type' => 'article',
+      'title' => 'The node title',
+      'body' => [[
+        'value' => 'The node body',
+      ]],
+    ]);
+    $this->drupalLogin($this->drupalCreateUser([
+      'access administration pages',
+      'administer content types',
+      'administer nodes',
+      'administer node fields',
+      'administer node display',
+      'administer node form display',
+      'view the administration theme',
+    ]));
+  }
+
+  /**
+   * Tests an entity type that has fields shown by default.
+   */
+  public function testNodeView() {
+    // By default, the one column layout is used.
+    $this->drupalGet('node/1');
+    $this->assertSession()->elementExists('css', '.field-layout--onecol');
+    $this->assertSession()->elementExists('css', '.field-layout-region--content .field--name-body');
+
+    $this->drupalGet('admin/structure/types/manage/article/display');
+    $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles());
+    $this->assertSession()->optionExists('fields[body][region]', 'content');
+  }
+
+  /**
+   * Gets the region titles on the page.
+   *
+   * @return string[]
+   *   An array of region titles.
+   */
+  protected function getRegionTitles() {
+    $region_titles = [];
+    $region_title_elements = $this->getSession()->getPage()->findAll('css', '.region-title td');
+    /** @var \Behat\Mink\Element\NodeElement[] $region_title_elements */
+    foreach ($region_title_elements as $region_title_element) {
+      $region_titles[] = $region_title_element->getText();
+    }
+    return $region_titles;
+  }
+
+}
diff --git a/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php
new file mode 100644
index 0000000..663c0d4
--- /dev/null
+++ b/core/modules/field_layout/tests/src/FunctionalJavascript/FieldLayoutTest.php
@@ -0,0 +1,266 @@
+<?php
+
+namespace Drupal\Tests\field_layout\FunctionalJavascript;
+
+use Drupal\entity_test\Entity\EntityTest;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests using field layout for entity displays.
+ *
+ * @group field_layout
+ */
+class FieldLayoutTest extends JavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['field_layout', 'field_ui', 'field_layout_test', 'layout_test'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $entity = EntityTest::create([
+      'name' => 'The name for this entity',
+      'field_test_text' => [[
+        'value' => 'The field test text value',
+      ]],
+    ]);
+    $entity->save();
+    $this->drupalLogin($this->drupalCreateUser([
+      'access administration pages',
+      'view test entity',
+      'administer entity_test content',
+      'administer entity_test fields',
+      'administer entity_test display',
+      'administer entity_test form display',
+      'view the administration theme',
+    ]));
+  }
+
+  /**
+   * Tests that layouts are unique per view mode.
+   */
+  public function testEntityViewModes() {
+    // By default, the field is not visible.
+    $this->drupalGet('entity_test/1/test');
+    $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text');
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text');
+
+    // Change the layout for the "test" view mode. See
+    // core.entity_view_mode.entity_test.test.yml.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->click('#edit-modes');
+    $this->getSession()->getPage()->checkField('display_modes_custom[test]');
+    $this->submitForm([], 'Save');
+    $this->clickLink('configure them');
+    $this->getSession()->getPage()->pressButton('Show row weights');
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+
+    // Each view mode has a different layout.
+    $this->drupalGet('entity_test/1/test');
+    $this->assertSession()->elementExists('css', '.field-layout-region--content .field--name-field-test-text');
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementNotExists('css', '.field-layout-region--content .field--name-field-test-text');
+  }
+
+  /**
+   * Tests the use of field layout for entity form displays.
+   */
+  public function testEntityForm() {
+    // By default, the one column layout is used.
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'content');
+
+    // The one column layout is in use.
+    $this->drupalGet('entity_test/structure/entity_test/form-display');
+    $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles());
+
+    // Switch the layout to two columns.
+    $this->click('#edit-field-layouts');
+    $this->getSession()->getPage()->selectFieldOption('field_layout', 'twocol');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+
+    // The field is moved to the default region for the new layout.
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+    $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+    $this->drupalGet('entity_test/manage/1/edit');
+    // No fields are visible, and the regions don't display when empty.
+    $this->assertFieldInRegion('field_test_text[0][value]', 'left');
+    $this->assertSession()->elementExists('css', '.field-layout-region--left .field--name-field-test-text');
+
+    // After a refresh the new regions are still there.
+    $this->drupalGet('entity_test/structure/entity_test/form-display');
+    $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+    // Drag the field to the right region.
+    $field_test_text_row = $this->getSession()->getPage()->find('css', '#field-test-text');
+    $right_region_row = $this->getSession()->getPage()->find('css', '.region-right-message');
+    $field_test_text_row->find('css', '.handle')->dragTo($right_region_row);
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+    // The new layout is used.
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'right');
+
+    // Move the field to the right region without tabledrag.
+    $this->drupalGet('entity_test/structure/entity_test/form-display');
+    $this->getSession()->getPage()->pressButton('Show row weights');
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'right');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+    // The updated region is used.
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'right');
+
+    // The layout is still in use without Field UI.
+    $this->container->get('module_installer')->uninstall(['field_ui']);
+    $this->drupalGet('entity_test/manage/1/edit');
+    $this->assertFieldInRegion('field_test_text[0][value]', 'right');
+  }
+
+  /**
+   * Tests the use of field layout for entity view displays.
+   */
+  public function testEntityView() {
+    // The one column layout is in use.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->assertEquals(['Content', 'Disabled'], $this->getRegionTitles());
+
+    // Switch the layout to two columns.
+    $this->click('#edit-field-layouts');
+    $this->getSession()->getPage()->selectFieldOption('field_layout', 'twocol');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+    $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+    $this->drupalGet('entity_test/1');
+    // No fields are visible, and the regions don't display when empty.
+    $this->assertSession()->elementNotExists('css', '.field-layout--twocol');
+    $this->assertSession()->elementNotExists('css', '.field-layout-region');
+    $this->assertSession()->elementNotExists('css', '.field--name-field-test-text');
+
+    // After a refresh the new regions are still there.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->assertEquals(['Left', 'Right', 'Disabled'], $this->getRegionTitles());
+
+    // Drag the field to the left region.
+    $this->assertTrue($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+    $field_test_text_row = $this->getSession()->getPage()->find('css', '#field-test-text');
+    $left_region_row = $this->getSession()->getPage()->find('css', '.region-left-message');
+    $field_test_text_row->find('css', '.handle')->dragTo($left_region_row);
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertFalse($this->assertSession()->optionExists('fields[field_test_text][region]', 'hidden')->isSelected());
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+    // The new layout is used.
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementExists('css', '.field-layout--twocol');
+    $this->assertSession()->elementExists('css', '.field-layout-region--left .field--name-field-test-text');
+
+    // Move the field to the right region without tabledrag.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->getSession()->getPage()->pressButton('Show row weights');
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'right');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+    $this->assertSession()->pageTextContains('Your settings have been saved.');
+
+    // The updated region is used.
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text');
+
+    // The layout is still in use without Field UI.
+    $this->container->get('module_installer')->uninstall(['field_ui']);
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->elementExists('css', '.field-layout--twocol');
+    $this->assertSession()->elementExists('css', '.field-layout-region--right .field--name-field-test-text');
+  }
+
+  /**
+   * Tests layout plugins with forms.
+   */
+  public function testLayoutForms() {
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    // Switch to a field layout with settings.
+    $this->click('#edit-field-layouts');
+
+    // Test switching between layouts with and without forms.
+    $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_plugin');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->fieldExists('settings_wrapper[layout_settings][setting_1]');
+
+    $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_2col');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->fieldNotExists('settings_wrapper[layout_settings][setting_1]');
+
+    $this->getSession()->getPage()->selectFieldOption('field_layout', 'layout_test_plugin');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->fieldExists('settings_wrapper[layout_settings][setting_1]');
+
+    // Move the test field to the content region.
+    $this->getSession()->getPage()->pressButton('Show row weights');
+    $this->getSession()->getPage()->selectFieldOption('fields[field_test_text][region]', 'content');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->submitForm([], 'Save');
+
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->pageTextContains('Blah: Default');
+
+    // Update the field layout settings.
+    $this->drupalGet('entity_test/structure/entity_test/display');
+    $this->click('#edit-field-layouts');
+    $this->getSession()->getPage()->fillField('settings_wrapper[layout_settings][setting_1]', 'Test text');
+    $this->submitForm([], 'Save');
+
+    $this->drupalGet('entity_test/1');
+    $this->assertSession()->pageTextContains('Blah: Test text');
+  }
+
+  /**
+   * Gets the region titles on the page.
+   *
+   * @return string[]
+   *   An array of region titles.
+   */
+  protected function getRegionTitles() {
+    $region_titles = [];
+    $region_title_elements = $this->getSession()->getPage()->findAll('css', '.region-title td');
+    /** @var \Behat\Mink\Element\NodeElement[] $region_title_elements */
+    foreach ($region_title_elements as $region_title_element) {
+      $region_titles[] = $region_title_element->getText();
+    }
+    return $region_titles;
+  }
+
+  /**
+   * Asserts that a field exists in a given region.
+   *
+   * @param string $field_selector
+   *   The field selector, one of field id|name|label|value.
+   * @param string $region_name
+   *   The machine name of the region.
+   */
+  protected function assertFieldInRegion($field_selector, $region_name) {
+    $region_element = $this->getSession()->getPage()->find('css', ".field-layout-region--$region_name");
+    $this->assertNotNull($region_element);
+    $this->assertSession()->fieldExists($field_selector, $region_element);
+  }
+
+}
diff --git a/core/modules/field_layout/tests/src/Kernel/FieldLayoutEntityDisplayTest.php b/core/modules/field_layout/tests/src/Kernel/FieldLayoutEntityDisplayTest.php
new file mode 100644
index 0000000..5405214
--- /dev/null
+++ b/core/modules/field_layout/tests/src/Kernel/FieldLayoutEntityDisplayTest.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace Drupal\Tests\field_layout\Kernel;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\field_layout\Entity\FieldLayoutEntityViewDisplay;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * @coversDefaultClass \Drupal\field_layout\Entity\FieldLayoutEntityDisplayTrait
+ * @group field_layout
+ */
+class FieldLayoutEntityDisplayTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['layout_discovery', 'field_layout', 'entity_test', 'field_layout_test'];
+
+  /**
+   * @covers ::preSave
+   * @covers ::calculateDependencies
+   */
+  public function testPreSave() {
+    $entity_display = FieldLayoutEntityViewDisplay::create([
+      'targetEntityType' => 'entity_test',
+      'bundle' => 'entity_test',
+      'mode' => 'default',
+      'status' => TRUE,
+      'content' => [
+        'foo' => ['type' => 'visible'],
+        'bar' => ['type' => 'hidden'],
+        'name' => ['type' => 'hidden', 'region' => 'content'],
+      ],
+    ]);
+
+    $expected = [
+      'langcode' => 'en',
+      'status' => TRUE,
+      'dependencies' => [],
+      'id' => 'entity_test.entity_test.default',
+      'targetEntityType' => 'entity_test',
+      'bundle' => 'entity_test',
+      'mode' => 'default',
+      'content' => [
+        'foo' => [
+          'type' => 'visible',
+        ],
+        'bar' => [
+          'type' => 'hidden',
+        ],
+      ],
+      'hidden' => [],
+    ];
+    $this->assertEntityValues($expected, $entity_display);
+
+    $entity_display->save();
+
+    $expected = [
+      'langcode' => 'en',
+      'status' => TRUE,
+      'dependencies' => [
+        'module' => [
+          'entity_test',
+          'field_layout',
+        ],
+      ],
+      'third_party_settings' => [
+        'field_layout' => [
+          'id' => 'onecol',
+          'settings' => [],
+        ],
+        'entity_test' => [
+          'foo' => 'bar',
+        ],
+      ],
+      'id' => 'entity_test.entity_test.default',
+      'targetEntityType' => 'entity_test',
+      'bundle' => 'entity_test',
+      'mode' => 'default',
+      'content' => [
+        'foo' => [
+          'type' => 'visible',
+          'region' => 'content',
+        ],
+      ],
+      'hidden' => [
+        'bar' => TRUE,
+      ],
+    ];
+    $this->assertEntityValues($expected, $entity_display);
+
+    $entity_display->setLayoutId('field_layout_test_plugin');
+    $entity_display->save();
+
+    $expected = [
+      'langcode' => 'en',
+      'status' => TRUE,
+      'dependencies' => [
+        'module' => [
+          'dependency_from_annotation',
+          'dependency_from_calculateDependencies',
+          'entity_test',
+          'field_layout',
+          'field_layout_test',
+        ],
+      ],
+      'third_party_settings' => [
+        'field_layout' => [
+          'id' => 'field_layout_test_plugin',
+          'settings' => [
+            'setting_1' => 'Default',
+          ],
+        ],
+        'entity_test' => [
+          'foo' => 'bar',
+        ],
+      ],
+      'id' => 'entity_test.entity_test.default',
+      'targetEntityType' => 'entity_test',
+      'bundle' => 'entity_test',
+      'mode' => 'default',
+      'content' => [
+        'foo' => [
+          'type' => 'visible',
+          'region' => 'content',
+        ],
+      ],
+      'hidden' => [
+        'bar' => TRUE,
+      ],
+    ];
+
+    $this->assertEntityValues($expected, $entity_display);
+
+    $entity_display->setLayoutId('field_layout_test_plugin', ['setting_1' => 'foobar']);
+    $entity_display->save();
+
+    $expected = [
+      'langcode' => 'en',
+      'status' => TRUE,
+      'dependencies' => [
+        'module' => [
+          'dependency_from_annotation',
+          'dependency_from_calculateDependencies',
+          'entity_test',
+          'field_layout',
+          'field_layout_test',
+        ],
+      ],
+      'third_party_settings' => [
+        'field_layout' => [
+          'id' => 'field_layout_test_plugin',
+          'settings' => [
+            'setting_1' => 'foobar',
+          ],
+        ],
+        'entity_test' => [
+          'foo' => 'bar',
+        ],
+      ],
+      'id' => 'entity_test.entity_test.default',
+      'targetEntityType' => 'entity_test',
+      'bundle' => 'entity_test',
+      'mode' => 'default',
+      'content' => [
+        'foo' => [
+          'type' => 'visible',
+          'region' => 'content',
+        ],
+      ],
+      'hidden' => [
+        'bar' => TRUE,
+      ],
+    ];
+
+    $this->assertEntityValues($expected, $entity_display);
+  }
+
+  /**
+   * Asserts than an entity has the correct values.
+   *
+   * @param mixed $expected
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   * @param string $message
+   */
+  public static function assertEntityValues($expected, EntityInterface $entity, $message = '') {
+    $values = $entity->toArray();
+
+    static::assertArrayHasKey('uuid', $values);
+    unset($values['uuid']);
+
+    static::assertSame($expected, $values, $message);
+  }
+
+}
diff --git a/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php b/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php
new file mode 100644
index 0000000..5cab6aa
--- /dev/null
+++ b/core/modules/field_layout/tests/src/Unit/FieldLayoutBuilderTest.php
@@ -0,0 +1,277 @@
+<?php
+
+namespace Drupal\Tests\field_layout\Unit;
+
+use Drupal\Core\Entity\EntityFieldManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\field_layout\Display\EntityDisplayWithLayoutInterface;
+use Drupal\field_layout\FieldLayoutBuilder;
+use Drupal\Core\Layout\LayoutPluginManagerInterface;
+use Drupal\Core\Layout\LayoutDefault;
+use Drupal\Core\Layout\LayoutDefinition;
+use Drupal\Tests\UnitTestCase;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\field_layout\FieldLayoutBuilder
+ * @group field_layout
+ */
+class FieldLayoutBuilderTest extends UnitTestCase {
+
+  /**
+   * @var \Drupal\Core\Layout\LayoutPluginManager|\Prophecy\Prophecy\ProphecyInterface
+   */
+  protected $layoutPluginManager;
+
+  /**
+   * @var \Drupal\Core\Entity\EntityFieldManagerInterface|\Prophecy\Prophecy\ProphecyInterface
+   */
+  protected $entityFieldManager;
+
+  /**
+   * @var \Drupal\field_layout\FieldLayoutBuilder
+   */
+  protected $fieldLayoutBuilder;
+
+  /**
+   * @var \Drupal\Core\Layout\LayoutInterface
+   */
+  protected $layoutPlugin;
+
+  /**
+   * @var \Drupal\Core\Layout\LayoutDefinition
+   */
+  protected $pluginDefinition;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->pluginDefinition = new LayoutDefinition([
+      'library' => 'field_layout/drupal.field_layout.twocol',
+      'theme_hook' => 'field_layout__twocol',
+      'regions' => [
+        'left' => [
+          'label' => 'Left',
+        ],
+        'right' => [
+          'label' => 'Right',
+        ],
+      ],
+    ]);
+    $this->layoutPlugin = new LayoutDefault([], 'twocol', $this->pluginDefinition);
+
+    $this->layoutPluginManager = $this->prophesize(LayoutPluginManagerInterface::class);
+    $this->layoutPluginManager->getDefinition('unknown', FALSE)->willReturn(NULL);
+    $this->layoutPluginManager->getDefinition('twocol', FALSE)->willReturn($this->pluginDefinition);
+
+    $this->entityFieldManager = $this->prophesize(EntityFieldManagerInterface::class);
+
+    $this->fieldLayoutBuilder = new FieldLayoutBuilder($this->layoutPluginManager->reveal(), $this->entityFieldManager->reveal());
+  }
+
+  /**
+   * @covers ::build
+   * @covers ::getFields
+   */
+  public function testBuildView() {
+    $definitions = [];
+    $non_configurable_field_definition = $this->prophesize(FieldDefinitionInterface::class);
+    $non_configurable_field_definition->isDisplayConfigurable('view')->willReturn(FALSE);
+    $definitions['non_configurable_field'] = $non_configurable_field_definition->reveal();
+    $this->entityFieldManager->getFieldDefinitions('the_entity_type_id', 'the_entity_type_bundle')->willReturn($definitions);
+
+    $build = [
+      'test1' => [
+        '#markup' => 'Test1',
+      ],
+      'non_configurable_field' => [
+        '#markup' => 'Non-configurable',
+      ],
+    ];
+
+    $display = $this->prophesize(EntityDisplayWithLayoutInterface::class);
+    $display->getTargetEntityTypeId()->willReturn('the_entity_type_id');
+    $display->getTargetBundle()->willReturn('the_entity_type_bundle');
+    $display->getLayoutPlugin()->willReturn($this->layoutPlugin);
+    $display->getLayoutId()->willReturn('twocol');
+    $display->getLayoutSettings()->willReturn([]);
+    $display->getComponents()->willReturn([
+      'test1' => [
+        'region' => 'right',
+      ],
+      'non_configurable_field' => [
+        'region' => 'left',
+      ],
+    ]);
+
+    $display_context = 'view';
+
+    $expected = [
+      'non_configurable_field' => [
+        '#markup' => 'Non-configurable',
+      ],
+      'field_layout' => [
+        'left' => [],
+        'right' => [
+          'test1' => [
+            '#markup' => 'Test1',
+          ],
+        ],
+        '#settings' => [],
+        '#layout' => $this->pluginDefinition,
+        '#theme' => 'field_layout__twocol',
+        '#attached' => [
+          'library' => [
+            'field_layout/drupal.field_layout.twocol',
+          ],
+        ],
+      ],
+    ];
+    $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context);
+    $this->assertEquals($expected, $build);
+    $this->assertSame($expected, $build);
+  }
+
+  /**
+   * @covers ::build
+   * @covers ::getFields
+   */
+  public function testBuildForm() {
+    $definitions = [];
+    $non_configurable_field_definition = $this->prophesize(FieldDefinitionInterface::class);
+    $non_configurable_field_definition->isDisplayConfigurable('form')->willReturn(FALSE);
+    $definitions['non_configurable_field'] = $non_configurable_field_definition->reveal();
+    $this->entityFieldManager->getFieldDefinitions('the_entity_type_id', 'the_entity_type_bundle')->willReturn($definitions);
+
+    $build = [
+      'test1' => [
+        '#markup' => 'Test1',
+      ],
+      'non_configurable_field' => [
+        '#markup' => 'Non-configurable',
+      ],
+    ];
+
+    $display = $this->prophesize(EntityDisplayWithLayoutInterface::class);
+    $display->getTargetEntityTypeId()->willReturn('the_entity_type_id');
+    $display->getTargetBundle()->willReturn('the_entity_type_bundle');
+    $display->getLayoutPlugin()->willReturn($this->layoutPlugin);
+    $display->getLayoutId()->willReturn('twocol');
+    $display->getLayoutSettings()->willReturn([]);
+    $display->getComponents()->willReturn([
+      'test1' => [
+        'region' => 'right',
+      ],
+      'non_configurable_field' => [
+        'region' => 'left',
+      ],
+    ]);
+
+    $display_context = 'form';
+
+    $expected = [
+      'test1' => [
+        '#markup' => 'Test1',
+        '#group' => 'right',
+      ],
+      'non_configurable_field' => [
+        '#markup' => 'Non-configurable',
+      ],
+      'field_layout' => [
+        'left' => [
+          '#process' => ['\Drupal\Core\Render\Element\RenderElement::processGroup'],
+          '#pre_render' => ['\Drupal\Core\Render\Element\RenderElement::preRenderGroup'],
+        ],
+        'right' => [
+          '#process' => ['\Drupal\Core\Render\Element\RenderElement::processGroup'],
+          '#pre_render' => ['\Drupal\Core\Render\Element\RenderElement::preRenderGroup'],
+        ],
+        '#settings' => [],
+        '#layout' => $this->pluginDefinition,
+        '#theme' => 'field_layout__twocol',
+        '#attached' => [
+          'library' => [
+            'field_layout/drupal.field_layout.twocol',
+          ],
+        ],
+      ],
+    ];
+    $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context);
+    $this->assertEquals($expected, $build);
+    $this->assertSame($expected, $build);
+  }
+
+  /**
+   * @covers ::build
+   */
+  public function testBuildEmpty() {
+    $definitions = [];
+    $non_configurable_field_definition = $this->prophesize(FieldDefinitionInterface::class);
+    $non_configurable_field_definition->isDisplayConfigurable('form')->willReturn(FALSE);
+    $definitions['non_configurable_field'] = $non_configurable_field_definition->reveal();
+    $this->entityFieldManager->getFieldDefinitions('the_entity_type_id', 'the_entity_type_bundle')->willReturn($definitions);
+
+    $build = [
+      'non_configurable_field' => [
+        '#markup' => 'Non-configurable',
+      ],
+    ];
+
+    $display = $this->prophesize(EntityDisplayWithLayoutInterface::class);
+    $display->getTargetEntityTypeId()->willReturn('the_entity_type_id');
+    $display->getTargetBundle()->willReturn('the_entity_type_bundle');
+    $display->getLayoutPlugin()->willReturn($this->layoutPlugin);
+    $display->getLayoutId()->willReturn('twocol');
+    $display->getLayoutSettings()->willReturn([]);
+    $display->getComponents()->willReturn([
+      'test1' => [
+        'region' => 'right',
+      ],
+      'non_configurable_field' => [
+        'region' => 'left',
+      ],
+    ]);
+
+    $display_context = 'form';
+
+    $expected = [
+      'non_configurable_field' => [
+        '#markup' => 'Non-configurable',
+      ],
+    ];
+    $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context);
+    $this->assertSame($expected, $build);
+  }
+
+  /**
+   * @covers ::build
+   */
+  public function testBuildNoLayout() {
+    $this->entityFieldManager->getFieldDefinitions(Argument::any(), Argument::any())->shouldNotBeCalled();
+
+    $build = [
+      'test1' => [
+        '#markup' => 'Test1',
+      ],
+    ];
+
+    $display = $this->prophesize(EntityDisplayWithLayoutInterface::class);
+    $display->getLayoutId()->willReturn('unknown');
+    $display->getLayoutSettings()->willReturn([]);
+    $display->getComponents()->shouldNotBeCalled();
+
+    $display_context = 'form';
+
+    $expected = [
+      'test1' => [
+        '#markup' => 'Test1',
+      ],
+    ];
+    $this->fieldLayoutBuilder->build($build, $display->reveal(), $display_context);
+    $this->assertSame($expected, $build);
+  }
+
+}
diff --git a/core/modules/layout_discovery/layout_discovery.info.yml b/core/modules/layout_discovery/layout_discovery.info.yml
new file mode 100644
index 0000000..a9a4139
--- /dev/null
+++ b/core/modules/layout_discovery/layout_discovery.info.yml
@@ -0,0 +1,6 @@
+name: 'Layout Discovery'
+type: module
+description: 'Provides a way for modules or themes to register layouts.'
+package: Core (Experimental)
+version: VERSION
+core: 8.x
diff --git a/core/modules/layout_discovery/layout_discovery.module b/core/modules/layout_discovery/layout_discovery.module
new file mode 100644
index 0000000..3cffee9
--- /dev/null
+++ b/core/modules/layout_discovery/layout_discovery.module
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Provides hook implementations for Layout Discovery.
+ */
+
+/**
+ * Implements hook_help().
+ */
+function layout_discovery_help($route_name) {
+  switch ($route_name) {
+    case 'help.page.layout_discovery':
+      $output = '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('Layout Discovery allows modules or themes to register layouts, and for other modules to list the available layouts and render them.') . '</p>';
+      $output .= '<p>' . t('For more information, see the <a href=":layout-discovery-documentation">online documentation for the Layout Discovery module</a>.', [':layout-discovery-documentation' => 'https://www.drupal.org/node/2619128']) . '</p>';
+      return $output;
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function layout_discovery_theme() {
+  return \Drupal::service('plugin.manager.core.layout')->getThemeImplementations();
+}
+
+/**
+ * Prepares variables for layout templates.
+ *
+ * @param array &$variables
+ *   An associative array containing:
+ *   - content: An associative array containing the properties of the element.
+ *     Properties used: #settings, #layout.
+ */
+function template_preprocess_layout(&$variables) {
+  $variables['settings'] = isset($variables['content']['#settings']) ? $variables['content']['#settings'] : [];
+  $variables['layout'] = isset($variables['content']['#layout']) ? $variables['content']['#layout'] : [];
+}
diff --git a/core/modules/layout_discovery/layout_discovery.services.yml b/core/modules/layout_discovery/layout_discovery.services.yml
new file mode 100644
index 0000000..1e24db4
--- /dev/null
+++ b/core/modules/layout_discovery/layout_discovery.services.yml
@@ -0,0 +1,4 @@
+services:
+  plugin.manager.core.layout:
+    class: Drupal\Core\Layout\LayoutPluginManager
+    arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@theme_handler']
diff --git a/core/modules/layout_discovery/templates/layout.html.twig b/core/modules/layout_discovery/templates/layout.html.twig
new file mode 100644
index 0000000..88ea745
--- /dev/null
+++ b/core/modules/layout_discovery/templates/layout.html.twig
@@ -0,0 +1,18 @@
+{#
+/**
+ * @file
+ * Template for a generic layout.
+ */
+#}
+{% set classes = [
+  'layout--' ~ layout.id|clean_class,
+] %}
+{% if content %}
+<div{{ attributes.addClass(classes) }}>
+{% for region in layout.getRegionNames %}
+  <div class="{{ 'region--' ~ region|clean_class }}">
+    {{ content[region] }}
+  </div>
+{% endfor %}
+</div>
+{% endif %}
diff --git a/core/modules/system/tests/modules/layout_test/config/schema/layout_test.schema.yml b/core/modules/system/tests/modules/layout_test/config/schema/layout_test.schema.yml
new file mode 100644
index 0000000..521faaf
--- /dev/null
+++ b/core/modules/system/tests/modules/layout_test/config/schema/layout_test.schema.yml
@@ -0,0 +1,7 @@
+layout_plugin.settings.layout_test_plugin:
+  type: layout_plugin.settings
+  label: 'Layout test plugin settings'
+  mapping:
+    setting_1:
+      type: string
+      label: 'Setting 1'
diff --git a/core/modules/system/tests/modules/layout_test/css/layout-test-2col.css b/core/modules/system/tests/modules/layout_test/css/layout-test-2col.css
new file mode 100644
index 0000000..d5c05b9
--- /dev/null
+++ b/core/modules/system/tests/modules/layout_test/css/layout-test-2col.css
@@ -0,0 +1,16 @@
+
+.layout-example-2col .region-left {
+  float: left;
+  width: 50%;
+}
+* html .layout-example-2col .region-left {
+  width: 49.9%;
+}
+
+.layout-example-2col .region-right {
+  float: left;
+  width: 50%;
+}
+* html .layout-example-2col .region-right {
+  width: 49.9%;
+}
diff --git a/core/modules/system/tests/modules/layout_test/layout_test.info.yml b/core/modules/system/tests/modules/layout_test/layout_test.info.yml
new file mode 100644
index 0000000..bfed2a5
--- /dev/null
+++ b/core/modules/system/tests/modules/layout_test/layout_test.info.yml
@@ -0,0 +1,6 @@
+name: 'Layout test'
+type: module
+description: 'Support module for testing layouts.'
+package: Testing
+version: VERSION
+core: 8.x
diff --git a/core/modules/system/tests/modules/layout_test/layout_test.layouts.yml b/core/modules/system/tests/modules/layout_test/layout_test.layouts.yml
new file mode 100644
index 0000000..f8fcf5b
--- /dev/null
+++ b/core/modules/system/tests/modules/layout_test/layout_test.layouts.yml
@@ -0,0 +1,29 @@
+layout_test_1col:
+  label: 1 column layout
+  category: Layout test
+  template: templates/layout-test-1col
+  regions:
+    top:
+      label: Top region
+    bottom:
+      label: Bottom region
+
+layout_test_2col:
+  label: 2 column layout
+  category: Layout test
+  template: templates/layout-test-2col
+  library: layout_test/layout_test_2col
+  regions:
+    left:
+      label: Left region
+    right:
+      label: Right region
+
+layout_test_1col_no_template:
+  label: 1 column layout (No template)
+  category: Layout test
+  regions:
+    top:
+      label: Top region
+    bottom:
+      label: Bottom region
diff --git a/core/modules/system/tests/modules/layout_test/layout_test.libraries.yml b/core/modules/system/tests/modules/layout_test/layout_test.libraries.yml
new file mode 100644
index 0000000..a696c0f
--- /dev/null
+++ b/core/modules/system/tests/modules/layout_test/layout_test.libraries.yml
@@ -0,0 +1,5 @@
+layout_test_2col:
+  version: 1.x
+  css:
+    theme:
+      css/layout-test-2col.css: {}
diff --git a/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php b/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php
new file mode 100644
index 0000000..e678bfa
--- /dev/null
+++ b/core/modules/system/tests/modules/layout_test/src/Plugin/Layout/LayoutTestPlugin.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\layout_test\Plugin\Layout;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Layout\LayoutDefault;
+use Drupal\Core\Plugin\PluginFormInterface;
+
+/**
+ * The plugin that handles the default layout template.
+ *
+ * @Layout(
+ *   id = "layout_test_plugin",
+ *   label = @Translation("Layout plugin (with settings)"),
+ *   category = @Translation("Layout test"),
+ *   description = @Translation("Test layout"),
+ *   template = "templates/layout-test-plugin",
+ *   regions = {
+ *     "main" = {
+ *       "label" = @Translation("Main Region")
+ *     }
+ *   }
+ * )
+ */
+class LayoutTestPlugin extends LayoutDefault implements PluginFormInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'setting_1' => 'Default',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form['setting_1'] = [
+      '#type' => 'textfield',
+      '#title' => 'Blah',
+      '#default_value' => $this->configuration['setting_1'],
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->configuration['setting_1'] = $form_state->getValue('setting_1');
+  }
+
+}
diff --git a/core/modules/system/tests/modules/layout_test/templates/layout-test-1col.html.twig b/core/modules/system/tests/modules/layout_test/templates/layout-test-1col.html.twig
new file mode 100644
index 0000000..e7a7eb5
--- /dev/null
+++ b/core/modules/system/tests/modules/layout_test/templates/layout-test-1col.html.twig
@@ -0,0 +1,14 @@
+{#
+/**
+ * @file
+ * Template for an example 1 column layout.
+ */
+#}
+<div class="layout-example-1col clearfix">
+  <div class="region-top">
+    {{ content.top }}
+  </div>
+  <div class="region-bottom">
+    {{ content.bottom }}
+  </div>
+</div>
diff --git a/core/modules/system/tests/modules/layout_test/templates/layout-test-2col.html.twig b/core/modules/system/tests/modules/layout_test/templates/layout-test-2col.html.twig
new file mode 100644
index 0000000..11433ee
--- /dev/null
+++ b/core/modules/system/tests/modules/layout_test/templates/layout-test-2col.html.twig
@@ -0,0 +1,14 @@
+{#
+/**
+ * @file
+ * Template for an example 2 column layout.
+ */
+#}
+<div class="layout-example-2col clearfix">
+  <div class="region-left">
+    {{ content.left }}
+  </div>
+  <div class="region-right">
+    {{ content.right }}
+  </div>
+</div>
diff --git a/core/modules/system/tests/modules/layout_test/templates/layout-test-plugin.html.twig b/core/modules/system/tests/modules/layout_test/templates/layout-test-plugin.html.twig
new file mode 100644
index 0000000..e49942c
--- /dev/null
+++ b/core/modules/system/tests/modules/layout_test/templates/layout-test-plugin.html.twig
@@ -0,0 +1,15 @@
+{#
+/**
+ * @file
+ * Template for layout_test_plugin layout.
+ */
+#}
+<div class="layout-test-plugin clearfix">
+  <div>
+    <span class="setting-1-label">Blah: </span>
+    {{ settings.setting_1 }}
+  </div>
+  <div class="region-main">
+    {{ content.main }}
+  </div>
+</div>
diff --git a/core/tests/Drupal/KernelTests/Core/Layout/LayoutTest.php b/core/tests/Drupal/KernelTests/Core/Layout/LayoutTest.php
new file mode 100644
index 0000000..2f90824
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Core/Layout/LayoutTest.php
@@ -0,0 +1,192 @@
+<?php
+
+namespace Drupal\KernelTests\Core\Layout;
+
+use Drupal\Core\Form\FormState;
+use Drupal\KernelTests\KernelTestBase;
+
+/**
+ * Tests Layout functionality.
+ *
+ * @group Layout
+ */
+class LayoutTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['system', 'layout_discovery', 'layout_test'];
+
+  /**
+   * The layout plugin manager.
+   *
+   * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
+   */
+  protected $layoutPluginManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->layoutPluginManager = $this->container->get('plugin.manager.core.layout');
+  }
+
+  /**
+   * Test rendering a layout.
+   *
+   * @dataProvider renderLayoutData
+   */
+  public function testRenderLayout($layout_id, $config, $regions, $html) {
+    $layout = $this->layoutPluginManager->createInstance($layout_id, $config);
+    $built['layout'] = $layout->build($regions);
+
+    // Assume each layout is contained by a form, in order to ensure the
+    // building of the layout does not interfere with form processing.
+    $form_state = new FormState();
+    $form_builder = $this->container->get('form_builder');
+    $form_builder->prepareForm('the_form_id', $built, $form_state);
+    $form_builder->processForm('the_form_id', $built, $form_state);
+
+    $this->render($built);
+    $this->assertRaw($html);
+    $this->assertRaw('<input data-drupal-selector="edit-the-form-id" type="hidden" name="form_id" value="the_form_id" />');
+  }
+
+  /**
+   * Data provider for testRenderLayout().
+   */
+  public function renderLayoutData() {
+    $data['layout_test_1col_with_form'] = [
+      'layout_test_1col',
+      [],
+      [
+        'top' => [
+          '#process' => [[static::class, 'processCallback']],
+        ],
+        'bottom' => [
+          '#markup' => 'This is the bottom',
+        ],
+      ],
+    ];
+
+    $data['layout_test_1col'] = [
+      'layout_test_1col',
+      [],
+      [
+        'top' => [
+          '#markup' => 'This is the top',
+        ],
+        'bottom' => [
+          '#markup' => 'This is the bottom',
+        ],
+      ],
+    ];
+
+    $data['layout_test_1col_no_template'] = [
+      'layout_test_1col_no_template',
+      [],
+      [
+        'top' => [
+          '#markup' => 'This is the top',
+        ],
+        'bottom' => [
+          '#markup' => 'This is the bottom',
+        ],
+      ],
+    ];
+
+    $data['layout_test_2col'] = [
+      'layout_test_2col',
+      [],
+      [
+        'left' => [
+          '#markup' => 'This is the left',
+        ],
+        'right' => [
+          '#markup' => 'This is the right',
+        ],
+      ],
+    ];
+
+    $data['layout_test_plugin'] = [
+      'layout_test_plugin',
+      [
+        'setting_1' => 'Config value',
+      ],
+      [
+        'main' => [
+          '#markup' => 'Main region',
+        ],
+      ],
+    ];
+
+    $data['layout_test_1col_with_form'][] = <<<'EOD'
+<div class="layout-example-1col clearfix">
+  <div class="region-top">
+    This string added by #process.
+  </div>
+  <div class="region-bottom">
+    This is the bottom
+  </div>
+</div>
+EOD;
+
+    $data['layout_test_1col'][] = <<<'EOD'
+<div class="layout-example-1col clearfix">
+  <div class="region-top">
+    This is the top
+  </div>
+  <div class="region-bottom">
+    This is the bottom
+  </div>
+</div>
+EOD;
+
+    $data['layout_test_1col_no_template'][] = <<<'EOD'
+<div data-drupal-selector="edit-layout" class="layout--layout-test-1col-no-template">
+  <div class="region--top">
+    This is the top
+  </div>
+  <div class="region--bottom">
+    This is the bottom
+  </div>
+</div>
+EOD;
+
+    $data['layout_test_2col'][] = <<<'EOD'
+<div class="layout-example-2col clearfix">
+  <div class="region-left">
+    This is the left
+  </div>
+  <div class="region-right">
+    This is the right
+  </div>
+</div>
+EOD;
+
+    $data['layout_test_plugin'][] = <<<'EOD'
+<div class="layout-test-plugin clearfix">
+  <div>
+    <span class="setting-1-label">Blah: </span>
+    Config value
+  </div>
+  <div class="region-main">
+    Main region
+  </div>
+</div>
+EOD;
+
+    return $data;
+  }
+
+  /**
+   * Provides a test #process callback.
+   */
+  public static function processCallback($element) {
+    $element['#markup'] = 'This string added by #process.';
+    return $element;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php b/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php
new file mode 100644
index 0000000..d892b47
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Layout/LayoutPluginManagerTest.php
@@ -0,0 +1,390 @@
+<?php
+
+namespace Drupal\Tests\Core\Layout;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Extension\ThemeHandlerInterface;
+use Drupal\Core\Layout\LayoutDefault;
+use Drupal\Core\Layout\LayoutDefinition;
+use Drupal\Core\Layout\LayoutPluginManager;
+use Drupal\Tests\UnitTestCase;
+use org\bovigo\vfs\vfsStream;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\Core\Layout\LayoutPluginManager
+ * @group Layout
+ */
+class LayoutPluginManagerTest extends UnitTestCase {
+
+  /**
+   * The module handler.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface
+   */
+  protected $moduleHandler;
+
+  /**
+   * The theme handler.
+   *
+   * @var \Drupal\Core\Extension\ThemeHandlerInterface
+   */
+  protected $themeHandler;
+
+  /**
+   * Cache backend instance.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cacheBackend;
+
+  /**
+   * The layout plugin manager.
+   *
+   * @var \Drupal\Core\Layout\LayoutPluginManagerInterface
+   */
+  protected $layoutPluginManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->setUpFilesystem();
+
+    $container = new ContainerBuilder();
+    $container->set('string_translation', $this->getStringTranslationStub());
+    \Drupal::setContainer($container);
+
+    $this->moduleHandler = $this->prophesize(ModuleHandlerInterface::class);
+
+    $this->moduleHandler->moduleExists('module_a')->willReturn(TRUE);
+    $this->moduleHandler->moduleExists('theme_a')->willReturn(FALSE);
+    $this->moduleHandler->moduleExists('core')->willReturn(FALSE);
+    $this->moduleHandler->moduleExists('invalid_provider')->willReturn(FALSE);
+
+    $module_a = new Extension('/', 'module', vfsStream::url('root/modules/module_a/module_a.layouts.yml'));
+    $this->moduleHandler->getModule('module_a')->willReturn($module_a);
+    $this->moduleHandler->getModuleDirectories()->willReturn(['module_a' => vfsStream::url('root/modules/module_a')]);
+    $this->moduleHandler->alter('layout', Argument::type('array'))->shouldBeCalled();
+
+    $this->themeHandler = $this->prophesize(ThemeHandlerInterface::class);
+
+    $this->themeHandler->themeExists('theme_a')->willReturn(TRUE);
+    $this->themeHandler->themeExists('core')->willReturn(FALSE);
+    $this->themeHandler->themeExists('invalid_provider')->willReturn(FALSE);
+
+    $theme_a = new Extension('/', 'theme', vfsStream::url('root/themes/theme_a/theme_a.layouts.yml'));
+    $this->themeHandler->getTheme('theme_a')->willReturn($theme_a);
+    $this->themeHandler->getThemeDirectories()->willReturn(['theme_a' => vfsStream::url('root/themes/theme_a')]);
+
+    $this->cacheBackend = $this->prophesize(CacheBackendInterface::class);
+
+    $namespaces = new \ArrayObject(['Drupal\Core' => vfsStream::url('root/core/lib/Drupal/Core')]);
+    $this->layoutPluginManager = new LayoutPluginManager($namespaces, $this->cacheBackend->reveal(), $this->moduleHandler->reveal(), $this->themeHandler->reveal(), $this->getStringTranslationStub());
+  }
+
+  /**
+   * @covers ::getDefinitions
+   * @covers ::providerExists
+   */
+  public function testGetDefinitions() {
+    $expected = [
+      'module_a_provided_layout',
+      'theme_a_provided_layout',
+      'plugin_provided_layout',
+    ];
+
+    $layout_definitions = $this->layoutPluginManager->getDefinitions();
+    $this->assertEquals($expected, array_keys($layout_definitions));
+    $this->assertContainsOnlyInstancesOf(LayoutDefinition::class, $layout_definitions);
+  }
+
+  /**
+   * @covers ::getDefinition
+   * @covers ::processDefinition
+   */
+  public function testGetDefinition() {
+    $theme_a_path = vfsStream::url('root/themes/theme_a');
+    $layout_definition = $this->layoutPluginManager->getDefinition('theme_a_provided_layout');
+    $this->assertSame('theme_a_provided_layout', $layout_definition->id());
+    $this->assertSame('2 column layout', $layout_definition->getLabel());
+    $this->assertSame('Columns: 2', $layout_definition->getCategory());
+    $this->assertSame('twocol', $layout_definition->getTemplate());
+    $this->assertSame("$theme_a_path/templates", $layout_definition->getPath());
+    $this->assertSame('theme_a/twocol', $layout_definition->getLibrary());
+    $this->assertSame('layout__twocol', $layout_definition->getThemeHook());
+    $this->assertSame("$theme_a_path/templates", $layout_definition->getTemplatePath());
+    $this->assertSame('theme_a', $layout_definition->getProvider());
+    $this->assertSame('right', $layout_definition->getDefaultRegion());
+    $this->assertSame(LayoutDefault::class, $layout_definition->getClass());
+    $expected_regions = [
+      'left' => [
+        'label' => 'Left region',
+      ],
+      'right' => [
+        'label' => 'Right region',
+      ],
+    ];
+    $this->assertSame($expected_regions, $layout_definition->getRegions());
+
+    $module_a_path = vfsStream::url('root/modules/module_a');
+    $layout_definition = $this->layoutPluginManager->getDefinition('module_a_provided_layout');
+    $this->assertSame('module_a_provided_layout', $layout_definition->id());
+    $this->assertSame('1 column layout', $layout_definition->getLabel());
+    $this->assertSame('Columns: 1', $layout_definition->getCategory());
+    $this->assertSame(NULL, $layout_definition->getTemplate());
+    $this->assertSame("$module_a_path/layouts", $layout_definition->getPath());
+    $this->assertSame('module_a/onecol', $layout_definition->getLibrary());
+    $this->assertSame('onecol', $layout_definition->getThemeHook());
+    $this->assertSame(NULL, $layout_definition->getTemplatePath());
+    $this->assertSame('module_a', $layout_definition->getProvider());
+    $this->assertSame('top', $layout_definition->getDefaultRegion());
+    $this->assertSame(LayoutDefault::class, $layout_definition->getClass());
+    $expected_regions = [
+      'top' => [
+        'label' => 'Top region',
+      ],
+      'bottom' => [
+        'label' => 'Bottom region',
+      ],
+    ];
+    $this->assertSame($expected_regions, $layout_definition->getRegions());
+
+    $core_path = '/core/lib/Drupal/Core';
+    $layout_definition = $this->layoutPluginManager->getDefinition('plugin_provided_layout');
+    $this->assertSame('plugin_provided_layout', $layout_definition->id());
+    $this->assertEquals('Layout plugin', $layout_definition->getLabel());
+    $this->assertEquals('Columns: 1', $layout_definition->getCategory());
+    $this->assertSame('plugin-provided-layout', $layout_definition->getTemplate());
+    $this->assertSame($core_path, $layout_definition->getPath());
+    $this->assertSame(NULL, $layout_definition->getLibrary());
+    $this->assertSame('layout__plugin_provided_layout', $layout_definition->getThemeHook());
+    $this->assertSame("$core_path/templates", $layout_definition->getTemplatePath());
+    $this->assertSame('core', $layout_definition->getProvider());
+    $this->assertSame('main', $layout_definition->getDefaultRegion());
+    $this->assertSame('Drupal\Core\Plugin\Layout\TestLayout', $layout_definition->getClass());
+    $expected_regions = [
+      'main' => [
+        'label' => 'Main Region',
+      ],
+    ];
+    $this->assertEquals($expected_regions, $layout_definition->getRegions());
+  }
+
+  /**
+   * @covers ::processDefinition
+   */
+  public function testProcessDefinition() {
+    $this->moduleHandler->alter('layout', Argument::type('array'))->shouldNotBeCalled();
+    $this->setExpectedException(InvalidPluginDefinitionException::class, 'The "module_a_derived_layout:array_based" layout definition must extend ' . LayoutDefinition::class);
+    $module_a_provided_layout = <<<'EOS'
+module_a_derived_layout:
+  deriver: \Drupal\Tests\Core\Layout\LayoutDeriver
+  array_based: true
+EOS;
+    vfsStream::create([
+      'modules' => [
+        'module_a' => [
+          'module_a.layouts.yml' => $module_a_provided_layout,
+        ],
+      ],
+    ]);
+    $this->layoutPluginManager->getDefinitions();
+  }
+
+  /**
+   * @covers ::getThemeImplementations
+   */
+  public function testGetThemeImplementations() {
+    $core_path = '/core/lib/Drupal/Core';
+    $theme_a_path = vfsStream::url('root/themes/theme_a');
+    $expected = [
+      'layout' => [
+        'render element' => 'content',
+      ],
+      'layout__twocol' => [
+        'render element' => 'content',
+        'base hook' => 'layout',
+        'template' => 'twocol',
+        'path' => "$theme_a_path/templates",
+      ],
+      'layout__plugin_provided_layout' => [
+        'render element' => 'content',
+        'base hook' => 'layout',
+        'template' => 'plugin-provided-layout',
+        'path' => "$core_path/templates",
+      ],
+    ];
+    $theme_implementations = $this->layoutPluginManager->getThemeImplementations();
+    $this->assertEquals($expected, $theme_implementations);
+  }
+
+  /**
+   * @covers ::getCategories
+   */
+  public function testGetCategories() {
+    $expected = [
+      'Columns: 1',
+      'Columns: 2',
+    ];
+    $categories = $this->layoutPluginManager->getCategories();
+    $this->assertEquals($expected, $categories);
+  }
+
+  /**
+   * @covers ::getSortedDefinitions
+   */
+  public function testGetSortedDefinitions() {
+    $expected = [
+      'module_a_provided_layout',
+      'plugin_provided_layout',
+      'theme_a_provided_layout',
+    ];
+
+    $layout_definitions = $this->layoutPluginManager->getSortedDefinitions();
+    $this->assertEquals($expected, array_keys($layout_definitions));
+    $this->assertContainsOnlyInstancesOf(LayoutDefinition::class, $layout_definitions);
+  }
+
+  /**
+   * @covers ::getGroupedDefinitions
+   */
+  public function testGetGroupedDefinitions() {
+    $category_expected = [
+      'Columns: 1' => [
+        'module_a_provided_layout',
+        'plugin_provided_layout',
+      ],
+      'Columns: 2' => [
+        'theme_a_provided_layout',
+      ],
+    ];
+
+    $definitions = $this->layoutPluginManager->getGroupedDefinitions();
+    $this->assertEquals(array_keys($category_expected), array_keys($definitions));
+    foreach ($category_expected as $category => $expected) {
+      $this->assertArrayHasKey($category, $definitions);
+      $this->assertEquals($expected, array_keys($definitions[$category]));
+      $this->assertContainsOnlyInstancesOf(LayoutDefinition::class, $definitions[$category]);
+    }
+  }
+
+  /**
+   * Sets up the filesystem with YAML files and annotated plugins.
+   */
+  protected function setUpFilesystem() {
+    $module_a_provided_layout = <<<'EOS'
+module_a_provided_layout:
+  label: 1 column layout
+  category: 'Columns: 1'
+  theme_hook: onecol
+  path: layouts
+  library: module_a/onecol
+  regions:
+    top:
+      label: Top region
+    bottom:
+      label: Bottom region
+module_a_derived_layout:
+  deriver: \Drupal\Tests\Core\Layout\LayoutDeriver
+  invalid_provider: true
+EOS;
+    $theme_a_provided_layout = <<<'EOS'
+theme_a_provided_layout:
+  class: '\Drupal\Core\Layout\LayoutDefault'
+  label: 2 column layout
+  category: 'Columns: 2'
+  template: twocol
+  path: templates
+  library: theme_a/twocol
+  default_region: right
+  regions:
+    left:
+      label: Left region
+    right:
+      label: Right region
+EOS;
+    $plugin_provided_layout = <<<'EOS'
+<?php
+namespace Drupal\Core\Plugin\Layout;
+use Drupal\Core\Layout\LayoutDefault;
+/**
+ * @Layout(
+ *   id = "plugin_provided_layout",
+ *   label = @Translation("Layout plugin"),
+ *   category = @Translation("Columns: 1"),
+ *   description = @Translation("Test layout"),
+ *   path = "core/lib/Drupal/Core",
+ *   template = "templates/plugin-provided-layout",
+ *   regions = {
+ *     "main" = {
+ *       "label" = @Translation("Main Region")
+ *     }
+ *   }
+ * )
+ */
+class TestLayout extends LayoutDefault {}
+EOS;
+    vfsStream::setup('root');
+    vfsStream::create([
+      'modules' => [
+        'module_a' => [
+          'module_a.layouts.yml' => $module_a_provided_layout,
+        ],
+      ],
+    ]);
+    vfsStream::create([
+      'themes' => [
+        'theme_a' => [
+          'theme_a.layouts.yml' => $theme_a_provided_layout,
+        ],
+      ],
+    ]);
+    vfsStream::create([
+      'core' => [
+        'lib' => [
+          'Drupal' => [
+            'Core' => [
+              'Plugin' => [
+                'Layout' => [
+                  'TestLayout.php' => $plugin_provided_layout,
+                ],
+              ],
+            ],
+          ],
+        ],
+      ],
+    ]);
+  }
+
+}
+
+/**
+ * Provides a dynamic layout deriver for the test.
+ */
+class LayoutDeriver extends DeriverBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDerivativeDefinitions($base_plugin_definition) {
+    if ($base_plugin_definition->get('array_based')) {
+      $this->derivatives['array_based'] = [];
+    }
+    if ($base_plugin_definition->get('invalid_provider')) {
+      $this->derivatives['invalid_provider'] = new LayoutDefinition([
+        'id' => 'invalid_provider',
+        'provider' => 'invalid_provider',
+      ]);
+    }
+    return $this->derivatives;
+  }
+
+}
