diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 1ba7206b10..1b7d14d0b5 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -929,6 +929,7 @@ drupal.dialog.off_canvas: misc/dialog/off-canvas.details.css: {} misc/dialog/off-canvas.tabledrag.css: {} misc/dialog/off-canvas.dropbutton.css: {} + misc/dialog/off-canvas.layout.css: {} dependencies: - core/jquery - core/drupal diff --git a/core/lib/Drupal/Core/Layout/Annotation/Layout.php b/core/lib/Drupal/Core/Layout/Annotation/Layout.php index d2072c249a..eae930cf4e 100644 --- a/core/lib/Drupal/Core/Layout/Annotation/Layout.php +++ b/core/lib/Drupal/Core/Layout/Annotation/Layout.php @@ -111,6 +111,15 @@ class Layout extends Plugin { */ public $icon; + /** + * The icon map. + * + * @var string[][] optional + * + * @see \Drupal\Core\Layout\Icon\IconBuilderInterface::build() + */ + public $icon_map; + /** * An associative array of regions in this layout. * diff --git a/core/lib/Drupal/Core/Layout/Icon/IconBuilderInterface.php b/core/lib/Drupal/Core/Layout/Icon/IconBuilderInterface.php new file mode 100644 index 0000000000..de2f82a441 --- /dev/null +++ b/core/lib/Drupal/Core/Layout/Icon/IconBuilderInterface.php @@ -0,0 +1,99 @@ +calculateSvgValues($icon_map, $this->width, $this->height, $this->strokeWidth, $this->padding); + return $this->buildRenderArray($regions, $this->width, $this->height, $this->strokeWidth); + } + + /** + * Builds a render array representation of an SVG. + * + * @param mixed[] $regions + * An array keyed by region name, with each element containing the 'height', + * 'width', and 'x' and 'y' offsets of each region. + * @param int $width + * The width of the SVG. + * @param int $height + * The height of the SVG. + * @param int|null $stroke_width + * The width of region borders. + * + * @return array + * A render array representing a SVG icon. + */ + protected function buildRenderArray(array $regions, $width, $height, $stroke_width) { + $build = [ + '#type' => 'html_tag', + '#tag' => 'svg', + '#attributes' => [ + 'width' => $width, + 'height' => $height, + 'class' => [ + 'layout-icon', + ], + ], + ]; + + if ($this->id) { + $build['#attributes']['class'][] = Html::getClass('layout-icon--' . $this->id); + } + + if ($this->label) { + $build['title'] = [ + '#type' => 'html_tag', + '#tag' => 'title', + '#value' => $this->label, + ]; + } + + // Append each polygon to the SVG. + foreach ($regions as $region => $attributes) { + // Wrapping with a element allows for metadata to exist alongside the + // rectangle. + $build['region'][$region] = [ + '#type' => 'html_tag', + '#tag' => 'g', + ]; + + $build['region'][$region]['title'] = [ + '#type' => 'html_tag', + '#tag' => 'title', + '#value' => $region, + ]; + + // Assemble the rectangle SVG element. + $build['region'][$region]['rect'] = [ + '#type' => 'html_tag', + '#tag' => 'rect', + '#attributes' => [ + 'x' => $attributes['x'], + 'y' => $attributes['y'], + 'width' => $attributes['width'], + 'height' => $attributes['height'], + 'stroke-width' => $stroke_width, + 'class' => [ + 'layout-icon__region', + Html::getClass('layout-icon__region--' . $region), + ], + ], + ]; + } + + return $build; + } + + /** + * Calculates the dimensions and offsets of all regions. + * + * @param string[][] $rows + * A two-dimensional array representing the visual output of the layout. See + * the documentation for the $icon_map parameter of + * \Drupal\Core\Layout\Icon\IconBuilderInterface::build(). + * @param int $width + * The width of the SVG. + * @param int $height + * The height of the SVG. + * @param int $stroke_width + * The width of region borders. + * @param int $padding + * The padding between regions. + * + * @return mixed[][] + * An array keyed by region name, with each element containing the 'height', + * 'width', and 'x' and 'y' offsets of each region. + */ + protected function calculateSvgValues(array $rows, $width, $height, $stroke_width, $padding) { + $region_rects = []; + + $row_height = $this->getLength(count($rows), $height, $stroke_width, $padding); + foreach ($rows as $row => $cols) { + $column_width = $this->getLength(count($cols), $width, $stroke_width, $padding); + $vertical_offset = $this->getOffset($row, $row_height, $stroke_width, $padding); + foreach ($cols as $col => $region) { + $horizontal_offset = $this->getOffset($col, $column_width, $stroke_width, $padding); + + // Check if this region is new, or already exists in the rectangle. + if (!isset($region_rects[$region])) { + $region_rects[$region] = [ + 'x' => $horizontal_offset, + 'y' => $vertical_offset, + 'width' => $column_width, + 'height' => $row_height, + ]; + } + else { + // In order to include the area of the previous region and any padding + // or border, subtract the calculated offset from the original offset. + $region_rects[$region]['width'] = $column_width + ($horizontal_offset - $region_rects[$region]['x']); + $region_rects[$region]['height'] = $row_height + ($vertical_offset - $region_rects[$region]['y']); + } + } + } + + return $region_rects; + } + + /** + * Gets the offset for this region. + * + * @param int $delta + * The zero-based delta of the region. + * @param int $length + * The height or width of the region. + * @param int $stroke_width + * The width of the region borders. + * @param int $padding + * The padding between regions. + * + * @return int + * The offset for this region. + */ + protected function getOffset($delta, $length, $stroke_width, $padding) { + // Half of the stroke width is drawn outside the dimensions. + $stroke_width /= 2; + // For every region in front of this add two strokes, as well as one + // directly in front. + $num_of_strokes = 2 * $delta + 1; + return ($num_of_strokes * $stroke_width) + ($delta * ($length + $padding)); + } + + /** + * Gets the height or width of a region. + * + * @param int $number_of_regions + * The total number of regions. + * @param int $length + * The total height or width of the icon. + * @param int $stroke_width + * The width of the region borders. + * @param int $padding + * The padding between regions. + * + * @return float|int + * The height or width of a region. + */ + protected function getLength($number_of_regions, $length, $stroke_width, $padding) { + if ($number_of_regions === 0) { + return 0; + } + + // Half of the stroke width is drawn outside the dimensions. + $total_stroke = $number_of_regions * $stroke_width; + // Padding does not precede the first region. + $total_padding = ($number_of_regions - 1) * $padding; + // Divide the remaining length by the number of regions. + return ($length - $total_padding - $total_stroke) / $number_of_regions; + } + + /** + * {@inheritdoc} + */ + public function setId($id) { + $this->id = $id; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setLabel($label) { + $this->label = $label; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setWidth($width) { + $this->width = $width; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setHeight($height) { + $this->height = $height; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setPadding($padding) { + $this->padding = $padding; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setStrokeWidth($stroke_width) { + $this->strokeWidth = $stroke_width; + return $this; + } + +} diff --git a/core/lib/Drupal/Core/Layout/LayoutDefinition.php b/core/lib/Drupal/Core/Layout/LayoutDefinition.php index c804776840..c87b618d11 100644 --- a/core/lib/Drupal/Core/Layout/LayoutDefinition.php +++ b/core/lib/Drupal/Core/Layout/LayoutDefinition.php @@ -85,6 +85,15 @@ class LayoutDefinition extends PluginDefinition implements PluginDefinitionInter */ protected $icon; + /** + * An array defining the regions of a layout. + * + * @var string[][]|null + * + * @see \Drupal\Core\Layout\Icon\IconBuilderInterface::build() + */ + protected $icon_map; + /** * An associative array of regions in this layout. * @@ -371,6 +380,85 @@ public function setIconPath($icon) { return $this; } + /** + * Gets the icon map for this layout definition. + * + * This should not be used if an icon path is specified. See ::getIcon(). + * + * @return string[][]|null + * The icon map, if it exists. + */ + public function getIconMap() { + return $this->icon_map; + } + + /** + * Sets the icon map for this layout definition. + * + * @param string[][]|null $icon_map + * The icon map. + * + * @return $this + */ + public function setIconMap($icon_map) { + $this->icon_map = $icon_map; + return $this; + } + + /** + * Builds a render array for an icon representing the layout. + * + * @param int $width + * (optional) The width of the icon. Defaults to 125. + * @param int $height + * (optional) The height of the icon. Defaults to 150. + * @param int $stroke_width + * (optional) If an icon map is used, the width of region borders. + * @param int $padding + * (optional) If an icon map is used, the padding between regions. Any + * value above 0 is valid. + * + * @return array + * A render array for the icon. + */ + public function getIcon($width = 125, $height = 150, $stroke_width = NULL, $padding = NULL) { + $icon = []; + if ($icon_path = $this->getIconPath()) { + $icon = [ + '#theme' => 'image', + '#uri' => $icon_path, + '#width' => $width, + '#height' => $height, + '#alt' => $this->getLabel(), + ]; + } + elseif ($icon_map = $this->getIconMap()) { + $icon_builder = $this->getIconBuilder() + ->setId($this->id()) + ->setLabel($this->getLabel()) + ->setWidth($width) + ->setHeight($height); + if ($padding) { + $icon_builder->setPadding($padding); + } + if ($stroke_width) { + $icon_builder->setStrokeWidth($stroke_width); + } + $icon = $icon_builder->build($icon_map); + } + return $icon; + } + + /** + * Wraps the icon builder. + * + * @return \Drupal\Core\Layout\Icon\IconBuilderInterface + * The icon builder. + */ + protected function getIconBuilder() { + return \Drupal::service('layout.icon_builder'); + } + /** * Gets the regions for this layout definition. * diff --git a/core/misc/dialog/off-canvas.layout.css b/core/misc/dialog/off-canvas.layout.css new file mode 100644 index 0000000000..aa3a5373ea --- /dev/null +++ b/core/misc/dialog/off-canvas.layout.css @@ -0,0 +1,11 @@ +/** + * @file + * Visual styling for layouts in the off-canvas dialog. + * + * See seven/css/layout/layout.css + */ + +.layout-icon__region { + fill: #f5f5f2; + stroke: #666; +} diff --git a/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php index 043e5c7397..170bcd093a 100644 --- a/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php +++ b/core/modules/field_layout/src/Form/FieldLayoutEntityDisplayFormTrait.php @@ -87,6 +87,8 @@ public function form(array $form, FormStateInterface $form_state) { '#tree' => TRUE, ]; + $form['field_layouts']['settings_wrapper']['icon'] = $layout_plugin->getPluginDefinition()->getIcon(); + 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); diff --git a/core/modules/layout_discovery/layout_discovery.layouts.yml b/core/modules/layout_discovery/layout_discovery.layouts.yml index d1b0e5a3a8..755a96b5f7 100644 --- a/core/modules/layout_discovery/layout_discovery.layouts.yml +++ b/core/modules/layout_discovery/layout_discovery.layouts.yml @@ -5,6 +5,8 @@ layout_onecol: library: layout_discovery/onecol category: 'Columns: 1' default_region: content + icon_map: + - [content] regions: content: label: Content @@ -16,6 +18,10 @@ layout_twocol: library: layout_discovery/twocol category: 'Columns: 2' default_region: first + icon_map: + - [top] + - [first, second] + - [bottom] regions: top: label: Top @@ -33,6 +39,12 @@ layout_twocol_bricks: library: layout_discovery/twocol_bricks category: 'Columns: 2' default_region: middle + icon_map: + - [top] + - [first_above, second_above] + - [middle] + - [first_below, second_below] + - [bottom] regions: top: label: Top @@ -56,6 +68,10 @@ layout_threecol_25_50_25: library: layout_discovery/threecol_25_50_25 category: 'Columns: 3' default_region: second + icon_map: + - [top] + - [first, second, second, third] + - [bottom] regions: top: label: Top @@ -75,6 +91,10 @@ layout_threecol_33_34_33: library: layout_discovery/threecol_33_34_33 category: 'Columns: 3' default_region: first + icon_map: + - [top] + - [first, second, third] + - [bottom] regions: top: label: Top diff --git a/core/modules/layout_discovery/layout_discovery.services.yml b/core/modules/layout_discovery/layout_discovery.services.yml index 1e24db4d6f..48d32292cc 100644 --- a/core/modules/layout_discovery/layout_discovery.services.yml +++ b/core/modules/layout_discovery/layout_discovery.services.yml @@ -2,3 +2,6 @@ services: plugin.manager.core.layout: class: Drupal\Core\Layout\LayoutPluginManager arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@theme_handler'] + layout.icon_builder: + class: Drupal\Core\Layout\Icon\SvgIconBuilder + shared: false diff --git a/core/tests/Drupal/KernelTests/Core/Layout/IconBuilderTest.php b/core/tests/Drupal/KernelTests/Core/Layout/IconBuilderTest.php new file mode 100644 index 0000000000..92ac3cec7a --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Layout/IconBuilderTest.php @@ -0,0 +1,137 @@ +container->get('renderer'); + + $build = $icon_builder->build($icon_map); + + $output = (string) $renderer->executeInRenderContext(new RenderContext(), function () use ($build, $renderer) { + return $renderer->render($build); + }); + $this->assertSame($expected, $output); + } + + public function providerTestBuild() { + $data = []; + $data['empty'][] = (new SvgIconBuilder()); + $data['empty'][] = []; + $data['empty'][] = <<<'EOD' + + +EOD; + + $data['two_column'][] = (new SvgIconBuilder()) + ->setId('two_column') + ->setLabel('Two Column') + ->setWidth(250) + ->setHeight(300) + ->setStrokeWidth(2); + $data['two_column'][] = [['left', 'right']]; + $data['two_column'][] = <<<'EOD' +Two Column +left + + +right + + + + +EOD; + + $data['two_column_no_stroke'][] = (new SvgIconBuilder()) + ->setWidth(250) + ->setHeight(300) + ->setStrokeWidth(NULL); + $data['two_column_no_stroke'][] = [['left', 'right']]; + $data['two_column_no_stroke'][] = <<<'EOD' +left + + +right + + + + +EOD; + + $data['two_column_border_collapse'][] = (new SvgIconBuilder()) + ->setWidth(250) + ->setHeight(300) + ->setStrokeWidth(2) + ->setPadding(-2); + $data['two_column_border_collapse'][] = [['left', 'right']]; + $data['two_column_border_collapse'][] = <<<'EOD' +left + + +right + + + + +EOD; + + $data['stacked'][] = (new SvgIconBuilder()) + ->setStrokeWidth(2); + $data['stacked'][] = [ + ['sidebar', 'top', 'top'], + ['sidebar', 'left', 'right'], + ['sidebar', 'middle', 'middle'], + ['footer_left', 'footer_right'], + ['footer_full'], + ]; + $data['stacked'][] = <<<'EOD' +sidebar + + +top + + +left + + +right + + +middle + + +footer_left + + +footer_right + + +footer_full + + + + +EOD; + + return $data; + } + +} diff --git a/core/themes/seven/css/layout/layout.css b/core/themes/seven/css/layout/layout.css index eb7c2bf0b3..39649e43c1 100644 --- a/core/themes/seven/css/layout/layout.css +++ b/core/themes/seven/css/layout/layout.css @@ -4,3 +4,11 @@ .page-content { margin-bottom: 80px; } + +/** + * Add color to layout icons. + */ +.layout-icon__region { + fill: #f5f5f2; + stroke: #666; +} diff --git a/core/themes/stable/css/core/dialog/off-canvas.layout.css b/core/themes/stable/css/core/dialog/off-canvas.layout.css new file mode 100644 index 0000000000..aa3a5373ea --- /dev/null +++ b/core/themes/stable/css/core/dialog/off-canvas.layout.css @@ -0,0 +1,11 @@ +/** + * @file + * Visual styling for layouts in the off-canvas dialog. + * + * See seven/css/layout/layout.css + */ + +.layout-icon__region { + fill: #f5f5f2; + stroke: #666; +} diff --git a/core/themes/stable/stable.info.yml b/core/themes/stable/stable.info.yml index 1e437f9d4c..957a65cf35 100644 --- a/core/themes/stable/stable.info.yml +++ b/core/themes/stable/stable.info.yml @@ -72,6 +72,7 @@ libraries-override: misc/dialog/off-canvas.details.css: css/core/dialog/off-canvas.details.css misc/dialog/off-canvas.tabledrag.css: css/core/dialog/off-canvas.tabledrag.css misc/dialog/off-canvas.dropbutton.css: css/core/dialog/off-canvas.dropbutton.css + misc/dialog/off-canvas.layout.css: css/core/dialog/off-canvas.layout.css core/drupal.dropbutton: css: