Repository navigation
Skin Design Guide
How to structure the screens of a SkinForge skin so that every screen of every plugin looks the same, can be restyled from one place and can be checked by a tool. The rules are the ones the Cockpit skins follow; the mechanisms they rest on (xmlinc, frames, $vars) are described in The XML+ compiler.
The short version:
A screen is a frame (header with the title, footer with the buttons) plus a body (the content). The body is its own file, included at a position and size that come from global variables, and everything in it is positioned relative to the body, at
0,0and$width/$height.
| Part | Provided by | What you write |
|---|---|---|
| Header | the frame (ScreenHeader widget and clock) |
the title= attribute of the <screen>
|
| Footer | the frame (ScreenButtonsBar widget) |
nothing: it shows the keys the screen's code defines (key_red, ...) |
| Body | the screen | one file, screenpart_<Screen>Body.ymlinc
|
The frame also draws the window itself (the rounded surface and its border), so a screen never draws its own background rectangle.
screen:
backgroundColor: "md_transparent"
flags: "wfNoBorder"
name: "About"
position: "fill"
children:
- xmlinc:
file: "screenpart_Window.ymlinc" # 1. the frame, centred on the screen ...
position: "center"
size: "$window_large_width,$window_large_height" # ... in the size of its kind
window_inset_x: "$window_large_body_x" # ... with header and footer in line with the body
- xmlinc:
file: "screenpart_AboutBody.ymlinc" # 2. the body ...
position: "$window_large_body_x,$window_large_body_y" # ... where the frame says it is
size: "$window_large_body_width,$window_large_body_height"and the body, which only knows its own coordinate system:
- widget:
font: "md_body_medium"
name: "AboutScrollLabel"
position: "0,0"
size: "$width,726"
- widget:
addon: "Pager"
connection: "AboutScrollLabel"
position: "0,$height-25"
size: "$width,25"- The frame is included first and centred. A frame has
frame="true", so everything else the screen places is relative to the frame's origin (see Frames in the compiler page): the screen never contains the offset of the window on the display. - The body is included at the body rectangle of that frame: four global variables per frame, defined once (see below). Its
size=becomes$widthand$heightinside the body. - In the body,
0,0is the top left of the body and$width,$heightits bottom right. A list that fills the width is$widthwide, a pager at the bottom edge is at$height-25. Nothing in the body mentions the window, its header or its paddings.
Everything that is not content stays in the screen next to the two includes: the title= and the other screen attributes, widgets that belong to the header or the footer area, window backdrops (see What stays in the screen).
There is one frame file in the shared Common directory: screenpart_Window.ymlinc. It draws the window (the outer and inner surface, the header with title, path and clock from screenpart_WindowHeader.ymlinc, and the buttons bar) in the size= of the include that places it, so small, medium, large, full and dialog are not files of their own but sizes. A widget of your own in the header band (next to the title) needs a zPosition above 20, as the header covers everything below it, and has to stay clear of the clock, which takes the right 140 pixels. The sizes and body rectangles are the global variables of frames.ymlinc, for a 1920x1080 output:
| Frame | File | Window (size=) |
Padding x / top / bottom | Body: x, y, width, height |
|---|---|---|---|---|
| small | Window |
$window_small_width,$window_small_height = 932 x 825 |
19 / 24 / 5 | 21, 86, 890 x 652 |
| medium | Window |
$window_medium_width,$window_medium_height = 1180 x 858 |
24 / 30 / 20 | 26, 92, 1128 x 664 |
| large | Window |
$window_large_width,$window_large_height = 1728 x 992 |
27 / 34 / 24 | 29, 96, 1670 x 790 |
| full | Window |
$window_full_width,$window_full_height = 1912 x 1091, which draws a 1912 x 1071 window (the output minus 8 and 9) |
24 / 24 / 14 | 26, 86, 1860 x 909 |
| dialog | Window |
$dialog_width,$dialog_height, given by the screen |
14 / 12 / 12 | see Dialogs |
The variables for a frame <f> (small, medium, large, full, and dialog without the size and the two body sizes) are:
| Variable | Meaning |
|---|---|
$window_<f>_width, $window_<f>_height
|
the size of the window |
$window_<f>_padding_x, _padding_top, _padding_bottom
|
the distance of the body from the surface of the window: the three numbers that control a frame |
$window_inset_x |
the distance of the header and of the footer from the side edges of the window; not set by a screen, every include of screenpart_Window.ymlinc passes window_inset_x="$window_<f>_body_x", so header, footer and body of a frame always start at the same x |
$window_<f>_trim_x, _trim_y
|
how much smaller the body is than the window: twice the surface inset and padding in x, the surface trim and the top and bottom padding in y |
$window_<f>_body_x, _body_y, _body_width, _body_height
|
the body rectangle, derived from the above (_body_width is _width minus _trim_x, _body_height is _height minus _trim_y) |
The body rectangle is the surface of the window (the area between the header and the footer, inset by $window_surface_x = 2 on both sides, $window_surface_trim = 144 shorter than the window, starting below the header, $window_header_height = 62) inset by the padding. Change a padding and the body of every screen with that frame follows; change a window size and the bodies follow that. No screen contains a number that belongs to the frame.
In the Cockpit skins the frames are used like this: a list or a form is a Medium or Large window, a text or info screen with two panes a Large one, a context menu a Small one, a screen that needs the whole display (movie and media lists, EPG grids, galleries) a Full one, and a popup with a progress bar a dialog. The choice follows the content; a screen does not draw a bigger window of its own.
Size from the body, never from the window. A body is the same file whatever frame it ends up in, as long as the frame gives it room. So:
| You want | Write |
|---|---|
| fill the width | size: "$width,..." |
| fill the height |
size: "...,$height" or $height-N below something of height N
|
| stick to the right edge | position: "$width-170,0" |
| stick to the bottom edge | position: "0,$height-25" |
| N equal columns |
position: "eval($i*$width/N),0", size: "eval($width/N),..." (division is integer division) |
| a fixed offset from the origin | plain numbers: position: "0,64"
|
A formula that uses a variable ($width-170) is wrapped in eval(...) by the YAML converter; writing the eval(...) yourself (as ZAML sources usually do) is the same thing. Write the design, not the arithmetic of the current window: $height-25 says "the pager is at the bottom", 1170 says nothing.
Fixed sizes are for things that have one: an icon (150,150), a button, a poster (468,675), the height of a text line or of 13*45 list rows. Anything that is "as wide as the screen", "as high as what is left" or "one third" is written with $width/$height.
Stay inside. Everything in a body lies in 0,0 to $width,$height. If a screen needs more room, it needs a bigger frame, not a body that spills over the header or the footer.
Order is the z-order inside the same zPosition, as everywhere in enigma2; the body is included where its widgets used to be, so the order does not change.
An element that is not body content is written in the screen, in the coordinates of the frame, next to the two includes. These are:
-
Header and footer area. A status line or a selector in the header (
day_selectorof TVMagazineCockpit at1280,22), an extra icon in the footer. -
The backdrop of the window: a full-window image with a negative
zPosition(abackdropwidget, abackdrop.jpgePixmap). It belongs to the window, so it is not shrunk to the body. -
Widgets that are deliberately off the window: hidden helper widgets parked at
1920,..or1824,96(a 1x1 marker pixmap). They stay where they are. -
Elements with
origin="screen": positioned relative to the display, not to the frame.
A body may include parts (<xmlinc>): a cell, a pager, a block repeated in several screens. The rules are those of the compiler, but a few matter for the design:
-
position=of the include is added to everything in the part,size=sets$width/$heightfor the part. A part is adaptive only if it uses them:size: "$width,25"is,size: "1350,25"is not, and then thesize=of the include does nothing. - Other attributes of an include are variables of the part.
screenpart_ListPageris sized bywidth="1350"(it sayssize: "$width,25") and takes the list it pages fromconnection="list". Give such a part its width explicitly; do not rely on whatever$widthwas left over. - A part that is used by one screen only is not a part: its content belongs to the body file of that screen. A fixed-size panel next to a body cannot adapt to the frame, and a screen made of an include with a position and no size is exactly the situation this guide avoids.
- A part that is used by several screens is written like a body: elements at
0,0, sizes from$width/$height, and nothing that knows about the window. -
The page indicator of a list is the shared part
screenpart_Pager.ymlinc, not aPagerwidget of its own:file: screenpart_Pager.ymlinc,position: "0,$height-25",size: "$width,25",connection: "list"puts it at the bottom edge of the body, and a pager that sits elsewhere only changespositionandsize. The part also setsconditional, so a screen without that list skips the pager instead of failing. A pager with its ownmaxPagesor a background of its own stays a widget. -
A popup whose size follows its content (ChoiceBox, MessageBoxSimple) sizes itself in an
appletand then includesscreenpart_PopupLayers.ymlincwithpopup_bottom(how much lower than the screen the bottom of the window is). The part draws the same two surfaces asscreenpart_Window.ymlinc, from the globals offrames.ymlinc($window_popup_header_height,$window_footer_height).
A dialog is a window the screen sizes itself, with dialog_width and dialog_height on the include of the frame (and size= taken from them, as size= comes last of the attributes). A global variable cannot depend on those (it is evaluated before any screen exists), so the dialog's body rectangle is given as an origin and as the amounts the body is smaller than the dialog:
- xmlinc:
file: "screenpart_Window.ymlinc"
position: "510,365"
dialog_height: "350"
dialog_width: "900"
size: "$dialog_width,$dialog_height"
window_inset_x: "$window_dialog_body_x"
- xmlinc:
file: "screenpart_PiconDownloadProgressBody.ymlinc"
position: "$window_dialog_body_x,$window_dialog_body_y"
size: "$dialog_width-$window_dialog_trim_x,$dialog_height-$window_dialog_trim_y"$window_dialog_body_x/_y are 16 and 74, the trims 32 and 168, from the paddings $window_dialog_padding_x/_top/_bottom (14 / 12 / 12), controlled like the other frames. The dialog itself is placed by the screen (position="510,365" above), like any other include.
-
A part that includes a frame (
screenpart_ConfigScreen) is a frame plus a body: it includes the frame and its own body. A screen that includes this part gets the frame origin, and puts what it adds in a body of its own, at the same body rectangle, so that its widgets and the part's line up. It is a part like any other (there is no separate kind of file for it), named for what it is. -
A family (
screenfamily, see the compiler page) is for screens that differ only in name and title: one body, several screens. A screen that is nothing but a frame and its own body is a member of the family of its frame (screenpart_ScreenLarge,_Medium,_Small,_Full:content="screenpart_<Screen>Body"), not ascreen_*file of its own; a screen with anything of its own (a widget, an applet, another body position) keeps its file. -
ZAML bodies are written in ZAML as well: the body of a screen with a loop is a
.zmlinc(screenpart_<Screen>Body.zmlinc), the generated.ymlincis not edited. A six-column screen:- for: var: "$i" range: [0, 5] body: - widget: position: "eval($i*$width/6),206" size: "eval($width/6),$height-206" source: "list$i"
The skin that is the primary skin of the box declares the output, in an output.ymlinc of its own that its skin.yml includes first (before frames.ymlinc, whose full-screen window is sized from it):
output:
id: "0"
children:
- resolution:
bpp: "32"
xres: "1920"
yres: "1080"A plugin skin does not declare it. enigma2 does not only read the <output id="0"> of the primary skin: it reads it in every skin it loads, and each time it re-applies its built-in defaults for a set of font aliases (Body, SelectionList, ...) and list parameters (SelectionListDescr, SelectionListLock, ExpandableListIcon, ...) on top of what the primary skin has set. A plugin skin with an <output> would reset those values of the primary skin the moment the plugin loads. The compiler does not need the tag from a plugin: a skin that has read no <resolution> is compiled as 1920x1080, which is what $output_width/$output_height (the full-screen window) and $screen_width/$screen_height of a position="fill" screen are calculated from. A plugin designed for another output says so with resolution="..." on its screens, which makes enigma2 scale them.
A display skin is the exception: the loader ignores the <output id="1"> of a display skin, so DisplaySkinCockpit can declare its 396x240 (display_output.ymlinc) without side effects.
Give the screens of a plugin resolution="1920,1080", so that enigma2 scales them when the display is another one.
Inside a screen there are no runtime tokens: the anchors e, c, center and % in a position/size are resolved by the compiler, from the frame, the screen size and the output, so the numbers in skin.xml are the numbers that are drawn. What is left is the placement of whole screens on the display: position="fill" (nearly all of them) and position="center,center" (the player and progress overlays of Common).
| File | Content |
|---|---|
screen_<Name>.ymlinc (.zmlinc) |
the screen: attributes, the frame, the body, what stays in the screen |
screenpart_<Name>Body.ymlinc (.zmlinc) |
the body of that screen |
screenpart_*.ymlinc |
a part shared by several screens |
fonts.ymlinc, colors.ymlinc, frames.ymlinc, radii.ymlinc
|
the global definitions; Common has the shared ones, a skin directory overrides |
output.ymlinc |
the output (resolution) of a primary skin; not in a plugin skin, see above |
A skin directory lists its screens in skin.yml; bodies are not listed there, they are included by their screen.
Each draws the first-level elements of the screens as labelled boxes in one SVG, without a box:
zml2svg <file> [dst .svg] [--size WxH] [--include file ...] [-c commondir] # the highest level available
yml2svg <file> [dst .svg] [--size WxH] [--include file ...] [-c commondir] [--srcdir dir]
xml2svg <skin.xml | .xmlinc> [dst .svg] [--size WxH] ...
- A
.ymlincor.zmlincis converted on the fly. Includes are looked up in the directory of the source and then in the sharedCommon/src/skin(-coverrides it); the sharedfonts,colors,radiiandframesare included automatically. A part drawn on its own (no<screen>) is given the canvas with--size. - Every box shows
#<n>: the line of the element in its YAML source. If the screen is made of several files (frame, body, parts), the id isfile:line, and the legend in the header of the SVG names the files (1=screenpart_Window.ymlinc 5=screenpart_AboutBody.ymlinc). The same information is in the tooltip of a box and in itsdata-srcattribute. Elements that do not come from a YAML source have no id. - Labels do not overlap: they are drawn above all boxes with a halo, small boxes first; a label that does not fit inside its box is shortened, then reduced to the id, then put next to the box.
- An unknown
$variableis reported only where it matters, in apositionor asize.
What to look for: the body must lie in the body rectangle; the elements of a body must not touch the header or the footer; something that looks right at one window size should still be right when the frame is changed, which is the point of $width/$height.
svg2xml <svg> <dst .xml/.xmlinc> reads back an SVG drawn by xml2svg that was edited in a vector editor (boxes moved or resized) and writes the XML+ source with the changed position=/size= as plain numbers. A box nobody touched keeps its original, possibly symbolic, position. The result is XML+: bring the numbers over to the YAML source by hand. (The labels are not attached to their boxes in the SVG, so a moved box leaves its label behind; redraw to get them back.)
A refactoring of a skin must not change what is drawn, except where it is meant to. Compile the skin before and after and compare the two skin.xml: an identical file proves nothing moved, a difference is the list of what did. Compare the compiled geometry, not the sources: the same element has different source text before and after ($width for 1696) and the same numbers if the design did not change.
The SVG of a screen is more than a picture: every box knows which line of which source it was drawn from, and the picture is made in seconds, from the sources, without a box. That is what makes the SVGs the tool for changing a skin. This is how they are used.
zml2svg screen_About.ymlinc. The legend in the header names the files, the id on a box says which one it came from and the line in it.
The big box in the picture is #5:1 AboutScrollLabel: line 1 of file 5, which the legend says is screenpart_AboutBody.ymlinc. The pager is #5:12. You do not search the skin for the widget: open that file at that line (Ctrl+G in most editors). The tooltip of a box shows the same file:line and the compiled element with its attributes as numbers, so you also see what your formula $width,726 came out as.
The ids tell you whose element you are looking at, too: #1...#4 are the parts of the frame (screenpart_WindowBase, ...ButtonsBar, ...Clock, ...WindowChrome). If a box of the frame is wrong, it is wrong in all screens: change the frame (Common, frames.ymlinc), not the screen.
Edit, draw again, compare. The tool resolves the includes and the $variables the way the real build does and converts YAML and ZAML on the fly, so a change to a body is visible a few seconds later, in the whole screen, in its frame.
In the picture the label stops about 40 px above the pager. Change the body:
- widget:
name: "AboutScrollLabel"
position: "0,0"
size: "$width,$height-30" # was: "$width,726"draw again, and the box now ends just above the pager, whatever the size of the window is. Because the picture is made of the sources, a wrong guess costs nothing and there is nothing to deploy.
A body or a part has no window around it, but it can be drawn on its own, on a canvas of the size the frame will give it:
zml2svg screenpart_AboutBody.ymlinc --size 1670x790 # the body rectangle of the large frame
--size is $width/$height of the part. Boxes that leave the canvas, or that do not reach the edge they should be anchored to, are visible at once. Do the same with --size of the next smaller frame to see whether the body still holds, or use it to draw a part (screenpart_PrimeCell.ymlinc --size 310x300) at the size of the cell it is used in.
Change a padding in frames.ymlinc or a size in a shared part, and redraw some of the screens that use it: the boxes follow, and an overlap or a box that now sticks out is easy to see. For the whole skin, gitupdateskin <domain> 1 (the full build, with SVGs) writes the SVG of every source next to it; a part that needs variables from the screen that includes it is skipped, it is drawn with that screen. An SVG is text, one line per box with its x, y, width and height, so the diff of the regenerated SVGs shows which boxes moved, and by how much, in every screen, without opening one: a change that should affect only the bodies of the Large screens does not show up in the others.
When you do not know the numbers, open the SVG in a vector editor (Inkscape), move and resize the boxes and save: svg2xml writes the changed position/size as XML+ (see Edit it above). The SVG knows the numbers, not the design: take them over by hand as $width/$height expressions, anchored to the edge the box belongs to, not as the pixels you happened to drop it at.
- Only the first-level elements of a screen (after the includes are resolved) are boxes. What a widget contains, such as the cells of a list in its
<convert>, is not drawn. - It is geometry, not rendering: no fonts, pixmaps or corner radii. The colour of a box is the
backgroundColorof the element, or one of a palette. - Font metrics (
w,hin a coordinate) are not known offline and count as 0. - Only YAML and ZAML sources have ids; an element that comes from a hand-written XML+ file is a box without one.
- The checks of the real build (font heights, colours, unknown variables) belong to the compile; the SVG tools report an unknown
$variableonly where it makes a box wrong, in apositionor asize.
-
A typo in an attribute name is not an error.
pposition="135,160"compiled and ran, with a widget at0,0. A compiled skin of widgets without apositionis worth a look. - A widget taller than its window spills over the footer; one wider than the window is cut by the display. The body check above finds them.
-
A list whose height is not a multiple of its
itemHeightshows whole rows only and leaves the rest as a blank strip at the bottom (a list of 635 pixels with 45 pixel rows shows 14 rows and 5 pixels of nothing). The opposite is worse: a list of 703 pixels with 141 pixel rows clips the fifth row. Write the height as rows times the row height: the standard row is$list_item_height(45), a global oflists.ymlinc, sosize="$width,13*$list_item_height"is 13 rows and a change of the row height cannot leave a half row. Where the list fills the body, make the height a whole number of rows:size="$width,eval(($height-206)/140*140)". The same goes for a cell that has its own width and height in aTemplatedMultiContent-like convert (TVMagazineCockpit): derive them from the list (width="eval($width/6)") instead of repeating the pixels of an older frame. -
Numbers that belong to the frame (
96as a top margin,1696as the width of the window) in a screen. They are the reason the same window looks different in two screens. -
A part without
$width/$heightincluded with asize=. The size is ignored; the part is as big as it is. -
$widththat is "whatever was left over" from an earlier include. Passwidth=orsize=to every part that needs one. -
A full-bleed layout in a padded body. EPG grids that run to the edge of the window have two choices: a
Fullframe with a smaller padding, or the grid inside the body with an even margin. Not edge-to-edge numbers in the body. -
A small overlay with
backgroundColor="md_transparent"punches a hole in the screen below it (an icon over a settings screen shows the live TV through it). A screen paints its background by overwriting the framebuffer, and a transparent colour overwrites it with alpha 0, which is what the video plane shows through (executeClear()of the GLES renderer). Give the overlay an opaque tile that covers the whole window, likeMuteandUnhandledKey(a roundedmd_surfaceeLabel, the screen withcornerRadius="$radius_popup"), or setalphaBlend="1"on the screen (tested on the box withUnhandledKey: the hand icon is drawn over the settings screen and the live TV no longer shows through). Nottransparent="1"on the screen:eWindowdoes not pass that on to the widget that paints the background, so it does nothing. A transparent background is right only where the video is meant to show: the infobar, a PiG.
- Pick the frame; include it with
position="center"(a dialog: where it goes). - Set
title=; do not draw a title. - Include
screenpart_<Name>Bodyat$window_<frame>_body_x,_ywith$window_<frame>_body_width,_height. - In the body: origin
0,0, widths from$width, heights from$height, edges with$width-N/$height-N. - Keep header/footer-area widgets, window backdrops and hidden widgets in the screen.
- Use a part only if two screens need it, and write it like a body.
- Draw it (
zml2svg) and check the boxes against the body rectangle. - Compile the skin and look at the messages: a clean build has none.