Canvas layout
Positions its children in depth: each child is placed independently of the others, by its distances to the edges of the canvas, and children can overlap.
Node name : canvas-layout
Warning
Use a canvas-layout only when elements must overlap: a badge on a picture, a layer over a card, a marker on a gauge... Everything else is laid out with vertical-layout, horizontal-layout and grid-layout, which adapt to the content, the window size and the language of the texts.
Never use a canvas-layout to lay out a whole screen or a whole area of a screen with fixed positions: such a layout does not adapt to the size of its content, of the window or of the translated texts, and breaks as soon as one of them changes.
Introduction
horizontal-layout, vertical-layout and grid-layout place their children one after the other: each child takes its own space and pushes its siblings. A canvas-layout works the other way around: every child is placed on top of the canvas, at a position of its own, without taking any space from the others. It is the right layout to decorate an element with a badge, a ribbon or a caption, to put a layer over a picture, or to draw a schema, a ruler or a gauge.
Each child is positioned with the canvas:top, canvas:right, canvas:bottom and canvas:left directives: they give the distance between an edge of the canvas and the same edge of the child.
<canvas-layout layout:width="320" layout:height="160">
<input-image value="@Fields.Photo" readonly="true" fit="cover" canvas:top="0" canvas:right="0" canvas:bottom="0" canvas:left="0" />
<badge value="New" severity="danger" canvas:top="8" canvas:right="8" />
<text canvas:left="8" canvas:bottom="8">Bamboo watch</text>
</canvas-layout>
In this example, the picture fills the canvas (its four distances are 0, see Covering the whole canvas), the badge sits 8 pixels from the top and right edges, and the text sits 8 pixels from the left and bottom edges. The badge and the text are drawn over the picture.
Important
The children of a canvas never give it a size: a canvas without a size of its own collapses to a height of 0, and its children are then drawn outside of it. Always give the canvas a size, with layout:width and layout:height (a number of pixels, or fill to take the space available in its parent).
In the schemas of this page, the dashed rectangle is the canvas and each colored rectangle is one of its children. A child shows the text it displays in the template, and a picture shows the name of its field (Photo for @Fields.Photo).
Attributes
| Attribute | Type | Required | Default value | Description |
|---|---|---|---|---|
| align-horizontal | string (left, center, right, stretch) |
false |
left |
Default horizontal alignment of the children that have no horizontal distance and no horizontal alignment. |
| align-vertical | string (top, center, bottom, stretch) |
false |
top |
Default vertical alignment of the children that have no vertical distance and no vertical alignment. |
Note
Children overflow the canvas by default: a child placed partly outside of the canvas stays visible. Use style:overflow="hidden" on the canvas to cut them at its edges (see Overflow).
Positioning directives
These directives are used on the children of a canvas-layout:
| Directive | Description |
|---|---|
canvas:top |
Distance between the top edge of the canvas and the top edge of the child. |
canvas:right |
Distance between the right edge of the canvas and the right edge of the child. |
canvas:bottom |
Distance between the bottom edge of the canvas and the bottom edge of the child. |
canvas:left |
Distance between the left edge of the canvas and the left edge of the child. |
A distance accepts the following values:
| Value | Example | Meaning |
|---|---|---|
| Number | 24, 12.5 |
A distance in pixels, without a unit. Decimals are allowed. |
| Negative number | -8 |
The child goes past the edge of the canvas by this distance. |
| Percentage | 30%, 33.33% |
A percentage of the canvas size: of its width for left and right, of its height for top and bottom. |
| Negative percentage | -10% |
The child goes past the edge of the canvas by this percentage of the canvas size. |
| Bound value | @Computeds.BadgeTop, $Item.Top |
A value computed by the page, in one of the formats above (see Bound distances). |
Any other value, such as 12px, auto or a CSS expression like calc(100% - 16px), is rejected when the template is generated.
Warning
The canvas:* directives only apply to the direct children of a canvas-layout, or to the children of an if, if-not or repeat (without virtual="true", see Children in a condition or a repeat) placed directly in it. Used anywhere else, they are ignored and the generation reports a warning.
Examples
Anchoring a child to an edge or a corner
A single distance on an axis anchors the child to that edge. The child keeps its own size: a text takes the size of its content, a component with a layout:width or a layout:height keeps them.
<canvas-layout layout:width="320" layout:height="160">
<text canvas:top="8" canvas:left="8">Top left</text>
<text canvas:top="8" canvas:right="8">Top right</text>
<text canvas:bottom="8" canvas:left="8">Bottom left</text>
<text canvas:bottom="8" canvas:right="8">Bottom right</text>
<text canvas:top="64" canvas:left="120">Anywhere</text>
</canvas-layout>
Distances in percent
A percentage places the child relatively to the size of the canvas, so the child keeps the same relative place when the canvas is resized. canvas:left="50%" places the left edge of the child in the middle of the canvas: the child itself starts in the middle.
<canvas-layout layout:width="320" layout:height="160">
<text canvas:top="25%" canvas:left="50%">Starts in the middle</text>
<text canvas:bottom="10%" canvas:right="33.33%">A third from the right</text>
</canvas-layout>
To put the center of the child at a given position, see Centering a child on a position.
Negative distances
A negative distance makes the child go past the edge of the canvas, for example to put a badge astride the corner of a card. The part outside of the canvas stays visible.
<canvas-layout layout:width="320" layout:height="160">
<input-image value="@Fields.Photo" readonly="true" fit="cover" canvas:top="0" canvas:right="0" canvas:bottom="0" canvas:left="0" />
<badge value="3" severity="danger" canvas:top="-10" canvas:right="-10" />
</canvas-layout>
Stretching a child between two edges
Two opposite distances on the same axis (canvas:left and canvas:right, or canvas:top and canvas:bottom) stretch the child between them: its size on that axis is given by the distances, whatever its content. Do not give the child a layout:width (or a layout:height) on that axis as well: see Size of the children.
<canvas-layout layout:width="320" layout:height="160">
<input-image value="@Fields.Photo" readonly="true" fit="cover" canvas:top="0" canvas:right="0" canvas:bottom="0" canvas:left="0" />
<!-- A banner across the whole width, at the bottom -->
<horizontal-layout style:background="neutral-900/0.6" layout:padding="small" canvas:left="0" canvas:right="0" canvas:bottom="0">
<text style:color="neutral-0">In stock</text>
</horizontal-layout>
<!-- A side bar along the right edge, with a margin -->
<vertical-layout layout:width="40" style:background="primary-500" canvas:top="8" canvas:right="8" canvas:bottom="48" />
</canvas-layout>
Covering the whole canvas
The four distances at 0 make the child cover the whole canvas, for example to put a semi-transparent layer over a picture. A child drawn after it stays over it.
<canvas-layout layout:width="320" layout:height="160">
<input-image value="@Fields.Photo" readonly="true" fit="cover" canvas:top="0" canvas:right="0" canvas:bottom="0" canvas:left="0" />
<vertical-layout style:background="neutral-900/0.4" canvas:top="0" canvas:right="0" canvas:bottom="0" canvas:left="0" />
<text style:color="neutral-0" canvas:left="16" canvas:bottom="16">Summer collection</text>
</canvas-layout>
Tip
layout:align-self-horizontal="stretch" and layout:align-self-vertical="stretch" give the same result as the four distances at 0 (see Aligning a child).
Default alignment of the children
align-horizontal and align-vertical set the alignment of every child that has no distance and no alignment of its own on that axis. With center, the child is centered on the canvas whatever its size.
<canvas-layout layout:width="320" layout:height="160" align-horizontal="center" align-vertical="center">
<input-image value="@Fields.Photo" readonly="true" fit="cover" canvas:top="0" canvas:right="0" canvas:bottom="0" canvas:left="0" />
<button type="method" method-name="PlayVideo" icon="Play" />
</canvas-layout>
The values of both attributes, and where they put a child:
<canvas-layout layout:width="320" layout:height="160" align-horizontal="right" align-vertical="bottom">
<text>right / bottom</text>
</canvas-layout>
<canvas-layout layout:width="320" layout:height="160" align-horizontal="stretch" align-vertical="top">
<text>stretch / top</text>
</canvas-layout>
<canvas-layout layout:width="320" layout:height="160" align-horizontal="left" align-vertical="stretch">
<text>left / stretch</text>
</canvas-layout>
Aligning a child
layout:align-self-horizontal and layout:align-self-vertical align one child differently from the default alignment of the canvas. They accept the same values as align-horizontal and align-vertical.
<canvas-layout layout:width="320" layout:height="160" align-horizontal="center" align-vertical="center">
<text>Centered by the canvas</text>
<text layout:align-self-horizontal="right" layout:align-self-vertical="top">Top right</text>
<text layout:align-self-vertical="bottom">Centered at the bottom</text>
</canvas-layout>
In this example, the last text only overrides the vertical alignment: it keeps the default horizontal alignment of the canvas (center).
Warning
A child cannot have both an alignment and a distance on the same axis (for example layout:align-self-horizontal with canvas:left or canvas:right): the generation reports an error. Both are only allowed when both are bound, for a page that lets the user choose either an alignment or distances: the page must then give a value to only one of them at a time: an empty value (null or "") removes the alignment, exactly like it removes a distance. If both have a value, the alignment applies and the distance is ignored.
A distance wins over the default alignment
On a given axis, a child with a distance ignores the default alignment of the canvas on that axis. The other axis keeps the default alignment.
<canvas-layout layout:width="320" layout:height="160" align-horizontal="center" align-vertical="center">
<text>Centered</text>
<text canvas:left="8">Left 8, centered vertically</text>
<text canvas:top="8">Top 8, centered horizontally</text>
</canvas-layout>
Overflow
A child going past the edges of the canvas stays visible by default. style:overflow="hidden" on the canvas cuts the children at its edges.
<canvas-layout layout:width="320" layout:height="160">
<input-image value="@Fields.Photo" readonly="true" fit="cover" canvas:top="0" canvas:right="0" canvas:bottom="0" canvas:left="0" />
<badge value="New" canvas:top="-10" canvas:right="-12" />
</canvas-layout>
<canvas-layout layout:width="320" layout:height="160" style:overflow="hidden">
<input-image value="@Fields.Photo" readonly="true" fit="cover" canvas:top="0" canvas:right="0" canvas:bottom="0" canvas:left="0" />
<badge value="New" canvas:top="-10" canvas:right="-12" />
</canvas-layout>
Note
A child going past the edges never adds a scroll bar to the canvas, even when the canvas has a layout:width or a layout:height of fill.
Stacking order
Children are drawn in the order of the template: a child is drawn over all the children written before it. There is no attribute to change this order: to bring a child to the front, write it after the others.
<canvas-layout layout:width="320" layout:height="160">
<horizontal-layout layout:width="160" layout:height="90" style:background="primary-200" canvas:top="16" canvas:left="16">
<text>First</text>
</horizontal-layout>
<horizontal-layout layout:width="160" layout:height="90" style:background="warning-200" canvas:top="52" canvas:left="128">
<text>Second</text>
</horizontal-layout>
</canvas-layout>
The second rectangle is drawn over the first one.
Note
This order holds whatever the content of the children: each child of the canvas is drawn as a whole, so an element inside a child (the sticky header of a datagrid, a badge...) never goes over a child written after it. Popovers, menus and drop-down lists are displayed above the page, so they are not concerned.
Centering a child on a position
style:translate moves a child from the position given by its distances. A percentage in style:translate is a percentage of the child's own size (unlike the canvas:* percentages, which are a percentage of the canvas size). style:translate="-50% -50%" therefore moves the child back by half its width and half its height: the center of the child is then at the position given by canvas:left and canvas:top, whatever its size.
<canvas-layout layout:width="320" layout:height="160">
<horizontal-layout layout:width="16" layout:height="16" style:corner-radius="full" style:background="primary-500" canvas:top="25%" canvas:left="75%" style:translate="-50% -50%" />
<text canvas:top="60%" canvas:left="25%" style:translate="-50% 0">Centered on 25%</text>
</canvas-layout>
In this schema, the dashed lines show the positions at 25% and 75% of the width.
Moving a child with style:translate
A translation in pixels moves the child by a fixed distance. It combines with the alignment and with the other transformations of the child (style:rotate, style:scale): a centered child stays centered, then is moved, then is rotated.
<canvas-layout layout:width="320" layout:height="160" align-horizontal="center" align-vertical="center">
<text style:translate="0 -24">Centered, then 24 pixels higher</text>
<text style:translate="0 24" style:rotate="-8">Centered, lower, then rotated</text>
</canvas-layout>
Bound distances
A distance can be bound to a field, a computed or a property of the iterated item. The page computes the value and gives it to the canvas as a number of pixels or a percentage, in the formats described in Positioning directives: a number (24, -8, 12.5) or a string ("24", "30%").
# UIViewComputeds
- Name: BadgeTop
DotNetDataType: string
Getter: return Fields.ShowBadgeAtTheBottom ? "75%" : "8";
<canvas-layout layout:width="320" layout:height="160">
<input-image value="@Fields.Photo" readonly="true" fit="cover" canvas:top="0" canvas:right="0" canvas:bottom="0" canvas:left="0" />
<badge value="New" canvas:top="@Computeds.BadgeTop" canvas:right="8" />
</canvas-layout>
- An empty value (
nullor"") removes the distance. With no distance left on that axis, the child goes back to the start of the axis (the left or top edge of the canvas). - A value in any other format is ignored, as if it were empty. When a position depends on a calculation (a size, a ratio, a score...), the page makes the whole calculation and gives the canvas the result: the canvas never computes an expression itself.
Animated positions
The transition directives animate a change of distance instead of moving the child at once: list the name of the edge (top, right, bottom or left) in transition:properties.
<canvas-layout layout:width="fill" layout:height="16" style:background="neutral-200" style:corner-radius="full">
<horizontal-layout
layout:width="16"
layout:height="16"
style:corner-radius="full"
style:background="primary-500"
style:translate="-50% 0"
canvas:top="0"
canvas:left="@Computeds.ProgressPercent"
transition:properties="left"
transition:duration="700"
transition:easing="ease-out"
/>
</canvas-layout>
In this example, when ProgressPercent changes from "20%" to "65%", the marker slides along the bar in 700 milliseconds.
Note
While the canvas itself is resized (a window or a splitter being resized, for example), the transitions of its children are suspended: the children follow the new size at once, instead of running behind it.
Hovering overlapping children
The children of a canvas are neighbors, not children of one another, even when one is drawn over the other. Moving the mouse pointer from a child onto a neighbor drawn over it therefore triggers event:mouseleave on the first child, although the pointer stays in its area.
When a child reveals another one on hover, for example a band that unfolds and shows a button over it, give both children the same methods:
<canvas-layout layout:width="320" layout:height="160">
<input-image value="@Fields.Photo" readonly="true" fit="cover" canvas:top="0" canvas:right="0" canvas:bottom="0" canvas:left="0" />
<vertical-layout layout:height="@Computeds.BandHeight" style:background="neutral-900/0.6" canvas:left="0" canvas:right="0" canvas:bottom="0"
transition:properties="height" transition:duration="300" event:mouseenter="ExpandBand" event:mouseleave="CollapseBand">
<text style:color="neutral-0">Bamboo watch</text>
</vertical-layout>
<!-- Drawn over the band, but not a child of it: it needs the same methods as the band -->
<button type="method" method-name="AddToCart" label="Add to cart" style:opacity="@Computeds.ButtonOpacity" canvas:right="8" canvas:bottom="8"
event:mouseenter="ExpandBand" event:mouseleave="CollapseBand" />
</canvas-layout>
In this example, ExpandBand and CollapseBand set the same field, from which BandHeight and ButtonOpacity are computed. Moving from the band onto the button triggers CollapseBand then ExpandBand at once, so the band stays unfolded. Without the two directives on the button, the band would fold back and hide the button just before the user could click it.
Children in a condition or a repeat
The children of an if, an if-not or a repeat placed directly in a canvas are children of the canvas: they accept the canvas:* directives and follow the default alignment of the canvas. In a repeat, each item usually takes its distances from the iterated item.
<canvas-layout layout:width="fill" layout:height="40">
<horizontal-layout style:background="neutral-300" layout:height="2" canvas:top="19" canvas:left="0" canvas:right="0" />
<repeat values="@Fields.Steps" item="Step">
<horizontal-layout
layout:width="12"
layout:height="12"
style:corner-radius="full"
style:background="primary-500"
style:translate="-50% 0"
canvas:top="14"
canvas:left="$Step.Position"
/>
</repeat>
<if condition="@Computeds.IsLate">
<badge value="Late" severity="danger" canvas:top="-12" canvas:right="0" />
</if>
</canvas-layout>
In this example, each step of the Steps list has a Position property (for example "10%", "40%" and "85%"), and the badge only appears when the condition is true.
Warning
A repeat with virtual="true" cannot be placed in a canvas: a virtual repeat displays its items inside its own scrolling list, so they would not be children of the canvas and could not be positioned. The generation reports an error. Use a repeat without virtual.
Partial templates in a canvas
A partial-template placed directly in a canvas is not positioned by the canvas: the canvas:* directives written on the root of the partial template are ignored, with a warning at generation, and the default alignment of the canvas does not apply to that root. A partial template is analyzed on its own, without knowing where it is used (the same limit applies to the grid:* directives of a partial template placed in a grid-layout).
To position a partial template, wrap it in an element that carries the canvas:* directives:
<canvas-layout layout:width="320" layout:height="160">
<input-image value="@Fields.Photo" readonly="true" fit="cover" canvas:top="0" canvas:right="0" canvas:bottom="0" canvas:left="0" />
<vertical-layout canvas:top="8" canvas:right="8">
<partial-template name="StockBadge" />
</vertical-layout>
</canvas-layout>
Nested canvases
A child of a canvas can itself be a canvas-layout. The distances of a child are always relative to its own canvas, the closest one.
<canvas-layout layout:width="320" layout:height="160">
<canvas-layout layout:width="160" layout:height="96" style:background="neutral-0" canvas:top="32" canvas:left="32">
<badge value="2" canvas:top="-8" canvas:right="-8" />
<text canvas:left="8" canvas:bottom="8">Card</text>
</canvas-layout>
<text canvas:top="8" canvas:right="8">Page</text>
</canvas-layout>
A canvas filling its parent
A canvas with a layout:width or a layout:height of fill takes the space available in its parent, so its size is only known once the page is displayed. When the position of a child depends on that size, layout:rendered-size gives it to the page, which computes the distances from it.
# UIViewFields
- Name: PageSize
DotNetDataType: GroupeIsa.Neos.Designer.UIAbstractions.IUIElementSize?
# UIViewComputeds
- Name: PanelLeft
Description: Keeps a 400 pixels wide panel on the right, 16 pixels from the edge, without ever going past the left edge.
DotNetDataType: decimal
Getter: |-
decimal pageWidth = Fields.PageSize?.Width ?? 0;
return pageWidth > 432 ? pageWidth - 416 : 16;
<canvas-layout layout:width="fill" layout:height="fill" layout:rendered-size="@Fields.PageSize">
<vertical-layout canvas:top="0" canvas:left="0" canvas:right="0" canvas:bottom="0">
<text>Page</text>
</vertical-layout>
<vertical-layout style:background="neutral-0" canvas:top="16" canvas:bottom="16" canvas:right="16" canvas:left="@Computeds.PanelLeft">
<text>Panel</text>
</vertical-layout>
</canvas-layout>
In this example, the panel always ends 16 pixels from the right edge, starts 400 pixels before it, and shrinks instead of going past the left edge when the window is too narrow.
Size of the children
On an axis with a single distance (or with the left, center, right, top or bottom alignment), a child keeps its own size:
- a child with a
layout:widthor alayout:heightkeeps them, - a child without a size takes the size of its content (a text, a button, a badge),
- an
input-imageis as large as the canvas by default: without any distance, it covers the whole canvas.
On an axis with two distances (or with the stretch alignment, its own or the default alignment of the canvas), the size of the child is given by the distances, whatever its own size. A layout:width or a layout:height written in the template on such an axis would therefore never apply: the generation reports an error. A bound size is ignored for as long as the axis is stretched, and applies again as soon as one of the two distances is removed. layout:min-width, layout:max-width, layout:min-height and layout:max-height still apply.
<canvas-layout layout:width="320" layout:height="160" style:overflow="hidden">
<!-- Only anchored on the left: keeps the width of the canvas, and slides with style:translate -->
<input-image value="@Fields.Photo" readonly="true" fit="cover" canvas:top="0" canvas:bottom="0" canvas:left="0" style:translate="-30% 0" />
<!-- Anchored on both sides: as wide as the space between the two distances -->
<input-image value="@Fields.Logo" readonly="true" fit="contain" canvas:top="16" canvas:bottom="16" canvas:left="70%" canvas:right="16" />
</canvas-layout>
See also
- Layout directives:
layout:align-self-horizontal,layout:align-self-vertical,layout:rendered-size - Style directives:
style:translate,style:rotate - Transition directives