Layout#
Elsie contains a layout system which allows you to quickly build scenarios that are common in presentations, while still providing the option of finely tuned customization for situations where every pixel placement matters.
The central element of the layout system is the Box.
Each Elsie slide contains a layout hierarchy tree. The internal nodes of the tree are boxes and the leaves are box items. Boxes are layout containers, which do not produce any visual content, but they dictate how are their children laid out on a slide. Box items are individual paintable items, such as text, images, shapes, etc. Anything that can be rendered by Elsie thus has an accompanied parent box which decides its size and position on a slide.
Creating boxes#
To create a new box, you can call the box
method on an existing
Box
. This will return a new box which will be a child of the box object
on which you call the box
method. The root box of the slide layout hierarchy is available to you
as the return value of the new_slide
method or the
slide
decorator.
Here we create three boxes as children of the top-level slide box and create a child text item in each box.
slide.box().text("Box 1")
slide.box().text("Box 2")
slide.box().text("Box 3")
Note: for brevity, most code snippets in this user guide assume that there is a slides
variable
containing a SlideDeck
object and a slide
variable containing a Slide
object. The render
method call is also omitted from most of the examples that display rendered slide output.
The output from the code snippets is rendered into PNG images instead of SVG images to ensure that
they will be displayed consistently on each device.
Debug draw mode#
The boxes themselves are invisible, but they have caused the three text items to be rendered
below one another. If you are fine-tuning or debugging the layout of your slide, and you want to see
the extents and bounds of your boxes, you can use the
debug_boxes
parameter when creating a slide:
@slides.slide(debug_boxes=True)
def three_boxes_debug(slide):
slide.box().text("Box 1")
slide.box().text("Box 2")
slide.box().text("Box 3")
You can see that there is a single root box that wraps the whole slide and then there are three individual boxes in the middle. The box debug draw mode will be used in some examples on this page to demonstrate the extents of individual boxes.
Default box layout properties#
Newly created boxes have the following behavior by default:
- They occupy as few space as possible. You can change this by modifying their size.
- Children are placed vertically in a column, in the order in which they were created. You can change this by modifying their axis.
- Children are centered vertically and horizontally. You can change this by modifying their position.
In the following sections below we will see how these default positioning and sizing rules can be changed.
Box naming#
Boxes can be named with the name
parameter (.box(name="My box")
). It has no impact on normal
rendering of the slide, but the name will be shown if the box debug draw mode is enabled.
If you use the @slides.slide()
decorator, the name of the top-level slide will be set to the
name of the function on which the decorator was used.
Box axis#
Boxes can either be vertical or horizontal:
- Vertical boxes place its child items vertically in a column. Their main axis is vertical and their cross axis is horizontal.
- Horizontal boxes place its child items horizontally in a row. Their main axis is horizontal and their cross axis is vertical.
Boxes are vertical by default, if you want to create a horizontal box, use the horizontal=True
parameter when creating a box:
box = slide.box(horizontal=True)
box.box().text("Box 1")
box.box().text("Box 2")
Composing boxes#
By composing boxes, you can create complex hierarchical row and column layouts.
row = slide.box(horizontal=True)
col_a = row.box()
col_a.box().text("Col. A/1")
col_a.box().text("Col. A/2")
col_b = row.box()
col_b.box().text("Col. B/1")
Note that almost all methods on a box will create a new box (box
)
or an item (e.g. text
or
rect
). Leaf items cannot contain children, but for
convenience they also offer most of the methods available on boxes, which they delegate to their
parent. Therefore, the following two snippets will create the same slide content:
# A: create text on its parent box
box = slide.box()
box.rect(bg_color="#aaf")
box.text("Hello!")
# B: create text on its sibling rectangle
slide.box().rect(bg_color="#aaf").text("Hello!")
Sizing boxes#
You can change the width and height of a box by using the width
and height
parameters of the
box
method.
Here we create three boxes (A
, B
and C
). Box A
has a default size, which is set according
to the required size of its text child. Box B
has a width of 300
pixels and height of 100
pixels. Box C
has width equal to the full width of its parent and height is again set to the height
of its child text item.
slide.box(name="A").text("Box 1")
slide.box(name="B", width=300, height=100).text("Box 2")
slide.box(name="C", width="100%").text("Box 3")
The width
and height
parameters define the minimal size of the box. Therefore, if its
children (child boxes, text items, etc.) request a larger size, the box will use the requested
size of its content.
Size value formats#
You can enter width
and height
values in several formats:
None
(the default): No minimal size request.int
,float
or a string containg only digits: Exact minimal size defined in pixels."<number>%"
(e.g."50%"
): Minimal size in percentage of the parent box size."fill"
or"fill(<number>)"
: The box will fill all available space of the parent box. If there are more boxes on the same level which use fill, then the size will be distributed amongst them, while respecting the ratio of thefill
parameter. For example, when one box has argumentfill(2)
and the second onefill(3)
, the remaining size will be divided using the ratio2:3
. Using just"fill"
is a shortcut for"fill(1)"
.
The following code shows an example of "fill"
usage:
slide.box(width="fill").text("Box 1")
box = slide.box(width=300, height=180)
box.box(height="fill(1)").text("Box 2")
box.box(height="fill(2)").text("Box 3")
slide.box(height="fill").text("Box 4")
Aliases for commonly sized boxes#
Elsie contains three shortcuts for creating boxes with common minimal size requirements:
fbox
(fill-box): shortcut forbox(width="fill", height="fill")
.sbox
(stretch-box): shortcut forbox(width="fill")
if the parent box has a vertical layout orbox(height="fill")
if the parent box has a horizontal layout. In other words, it fills the box in the cross axis.overlay
: shortcut forbox(x=0, y=0, width="100%", height="100%)
. This can be used if you want to overlay several boxes on top of each other, which is useful especially in combination with revealing.
Padding#
By default, each box gives all of its space to its children. This can be modified by padding.
There are four padding values: left
, right
, top
, and bottom
. They can be modified with the
p_left
, p_right
, p_top
, and p_bottom
parameters of the
box
method.
After the layout of the parent box is computed and the final size and position of a box is known, the padding will shrink its size in the specified directions.
slide.box(width=160, height=100, p_left=40, name="Top box")
slide.box(width=160, height=150, p_y=50, name="Bottom box")
In the above example, the top box is shrunk by 40
pixels from the left. Note that the padding is
applied after the layout was calculated, therefore the box was first centered horizontally, and
then the padding reduced its size. The bottom box is shrunk by 50
pixels from the top and from
the bottom.
You can also use the following padding shortcut parameters of the box
method:
p_x
sets bothp_left
andp_right
.p_y
sets bothp_top
andp_bottom
.padding
sets all four paddings at once.
Positioning boxes#
You can set the position of the top-left corner of a box via the x
and y
parameters of the box
method. You can enter the x
and y
positions in several formats:
None
(default): Set the default position (see below).int
,float
or a string containing only digits: Set absolute position in pixels. Coordinates are relative to the top-left corner of the parent box."<number>%
: Set position relative to the parent box."0%"
represents the left (x
) or top (y
) edge of the parent and"100%"
the right (x
) or bottom (y
) edge of the parent."[<number>%]
: Align the box in the parent box."[0%]"
is left (x
) or top (y
),"[50%]"
is middle and"[100%]"
is right (x
) or bottom (y
) alignment.- Dynamically defined position: See below.
Here is an example of using absolute and relative position coordinates:
slide.box(x=0, y=10).text("Box 1")
row = slide.box(width="fill")
row.box(x="20%").text("Box 2")
row.box(x="60%").text("Box 3")
Default position#
The default positioning of a box depends on the axis of its parent. The following explanation assumes a vertical box. For a horizontal box, the main and cross axes would be swapped.
Children of a box will be by default centered along the cross axis. In the case of a vertical
parent box, the x
attribute would be set to "[50%]"
, i.e. child boxes will be horizontally
centered.
For the main axis, the behaviour is more complex. When a box has its main axis position
set to None
, it is a managed box. A parent box stacks all of its managed children boxes along
its main axis one-by-one. In addition, it also centers all of its children together along the
main axis.
slide.box().text("Box 1")
slide.box().text("Box 2")
slide.box().text("Box 3")
In the above example, the slide
is a parent vertical box. Its three children will thus be laid
below one another, they will be centered horizontally, and all of them together will also be centered
vertically.
Here is the same situation with a horizontal parent box:
row = slide.box(horizontal=True)
row.box().text("Box 1")
row.box().text("Box 2")
row.box().text("Box 3")
Dynamic positions#
Box position can also be defined dynamically with respect to other boxes. You can get a position
that is relative to the final position of a box using the x
or
y
methods. The returned value of these methods will be a proxy
object that will resolve the actual final position after layout is computed.
The reference point of these two methods is the top-left corner of the target box. The parameter can be either:
int
orfloat
: Resolves to the given number of pixels from the reference point. The value can be negative."<number>%"
: Resolves to the ratio of the size of the box added to the reference point.
For example:
.x("0%")
returns the left-mostx
coordinate of the box..x("50%")
returns thex
coordinate of the middle of the box..x("100%")
return the right-mostx
coordinate of the box..x(10)
returns the left-mostx
coordinate of the box, moved by10
pixels to the right.
Here you can observe dynamic positions in action:
b = slide.box(width=100, height=100, name="First")
slide.box(x=b.x("50%"), y=b.y("50%"), width=100, height=100, name="Second")
slide.box(x=0, y=b.y("100%"), width="100%", height=200, name="Third")
In addition to using the individual x
and y
methods, you can also use the
p
method to create a dynamic point, which will again be resolved
after the layout is fully computed. You can also further move this point via the
add
method. This is mostly useful for defining points of
lines and polygons.
Modifying render order#
By default, boxes are rendered by performing a depth-first walk through the layout tree. Each child is visited in the order in which it was defined.
In the following example, the blue box is rendered over all the previous boxes as it was defined the last.
slide.box(x=40, y=20, width=80, height=120).rect(bg_color="red")
slide.box(x=50, y=30, width=80, height=80).rect(bg_color="green")
slide.box(x=60, y=40, width=80, height=80).rect(bg_color="blue")
You have several options how to change the rendering order:
- Use the
prepend
parameter when creating a box. This will insert it as the first child of the parent box, instead of the last.
slide.box(x=40, y=20, width=80, height=120).rect(bg_color="red")
slide.box(x=50, y=30, width=80, height=80).rect(bg_color="green")
slide.box(x=60, y=40, width=80, height=80, prepend=True).rect(bg_color="blue")
- Use the
below
orabove
parameters when creating a box to place the newly created box above/below the box passed in the parameter.
a = slide.box(x=40, y=20, width=80, height=120)
a.rect(bg_color="red")
slide.box(x=50, y=30, width=80, height=80, below=a).rect(bg_color="green")
slide.box(x=60, y=40, width=80, height=80).rect(bg_color="blue")
- You can also move boxes in the
z
axis using thez_level
parameter. Before the final paint, all drawing elements will be sorted using a stable sort by theirz_level
. An element with a largerz_level
will be drawn after an element with a smallz_level
. If thez_level
is not specified, it is inherited from the parent box. The root box hasz_level
set to0
.
slide.box(x=40, y=20, width=80, height=120, z_level=3).rect(bg_color="red")
slide.box(x=50, y=30, width=80, height=80, z_level=2).rect(bg_color="green")
slide.box(x=60, y=40, width=80, height=80, z_level=1).rect(bg_color="blue")