Minimal Skin Creation Guide¶
This guide walks through creating a minimal but functional Movian skin, explaining each component and the essential patterns needed for skin development.
Overview¶
A minimal Movian skin requires:
- Entry point (
universe.view) - Root configuration and layout - Macro definitions (
theme.view) - Reusable UI components - Background component (
background.view) - Visual backdrop - Loading screen (
loading.view) - Loading state indicator - At least one page (
pages/home.view) - Content display
File Structure¶
minimal-skin/
├── universe.view # Main entry point
├── theme.view # Macro definitions
├── background.view # Background component
├── loading.view # Loading screen
└── pages/
└── home.view # Home page
Core Components¶
1. universe.view - The Entry Point¶
The universe.view file is the root of your skin. Movian loads this file first and it sets up the entire UI structure.
Key Responsibilities¶
Global Configuration:
// UI sizing and layout
$ui.sizeOffset = 4;
$ui.xmargin = select($ui.aspect > 1, $ui.width / 100, 0.2em);
$ui.orientation = select($ui.aspect > 1, "landscape", "portrait");
// Color scheme
$ui.color1 = "#4192ff"; // Primary color
$ui.color2 = "#306cbe"; // Secondary color
$ui.color3 = "#c2ddff"; // Accent color
Event Handlers:
// Global keyboard/remote shortcuts
onEvent(sysinfo, {
toggle($ui.sysinfo);
});
onEvent(mediastats, {
toggle($ui.mediainfo);
});
Style Definitions:
// Define global styles that apply to specific widget types
style(PageContainer, {
alpha: 1 - iir(clamp(getLayer(), 0, 1), 4) * 0.9;
});
style(NavSelectedText, {
color: select(isNavFocused(), 1, 0.8);
});
Component Loading:
// Load background
widget(loader, {
source: "background.view";
});
// Load loading indicator
widget(loader, {
hidden: iir($nav.currentpage.model.loading, 8) < 0.001;
source: "loading.view";
});
Page System:
// The page system manages navigation between different views
widget(playfield, {
cloner($nav.pages, container_z, {
widget(loader, {
source: "skin://pages/" + $self.model.type + ".view";
});
});
});
2. theme.view - Macro Definitions¶
The theme.view file defines reusable UI macros that ensure consistent styling across your skin.
Essential Macros¶
ListItemBevel() - Visual Depth
Creates a subtle 3D effect for list items:
#define ListItemBevel() {
widget(container_y, {
filterConstraintY: true;
filterConstraintX: true;
// Light line on top
widget(quad, {
height: 1;
alpha: 0.15;
});
space(1);
// Dark line on bottom
widget(quad, {
height: 1;
color: 0;
alpha: 0.15;
});
});
}
Usage:
ListItemHighlight() - Interactive Feedback
Highlights items on hover and focus:
#define ListItemHighlight() {
widget(quad, {
fhpSpill: true;
additive: true;
alpha: 0.1 * isHovered() + 0.2 * isNavFocused();
});
}
Key Features:
- fhpSpill: true - Allows the highlight to extend beyond parent bounds
- additive: true - Uses additive blending for a glow effect
- isHovered() - Returns 1 when mouse is over the widget
- isNavFocused() - Returns 1 when widget has keyboard/remote focus
BackButton() - Navigation
Creates a consistent back button:
#define BackButton(ENABLED=true, EVENT=event("back")) {
widget(container_y, {
align: center;
width: 4em;
clickable: $ui.pointerVisible || ($ui.touch && ENABLED);
alpha: iir($ui.pointerVisible || ($ui.touch && ENABLED), 4);
onEvent(activate, EVENT);
navFocusable: false;
widget(icon, {
color: 0.5 + iir(isHovered(), 4);
size: 2em;
source: "dataroot://res/svg/Left.svg";
});
});
}
Parameters:
- ENABLED - Whether the button is enabled (default: true)
- EVENT - Event to trigger on activation (default: back event)
PageHeader() - Standardized Headers
Creates a page header with title and back button:
#define PageHeader(TITLE) {
widget(container_z, {
height: 3em;
zoffset: 10;
widget(quad, {
color: 0;
alpha: 0.2;
});
widget(label, {
padding: [3em, 0];
align: center;
caption: TITLE;
size: 1.5em;
});
widget(container_x, {
hidden: !$nav.canGoBack;
BackButton();
space(1);
});
});
}
Usage:
3. background.view - Visual Backdrop¶
The background component provides the visual backdrop for your skin:
widget(container_z, {
widget(backdrop, {
source: $nav.currentpage.model.metadata.background ??
$nav.currentpage.glw.background ??
$core.glw.background ??
"pixmap:gradient:30,30,30:10,10,10";
zoffset: -1500;
alpha: iir(1 - $ui.fullwindow, 4);
maxIntensity: 0.4;
});
});
Key Features:
- Fallback chain: Uses page background, then global background, then gradient
- Z-ordering: zoffset: -1500 ensures it's behind all content
- Fade effect: alpha: iir(1 - $ui.fullwindow, 4) fades out in fullscreen
- Intensity limit: maxIntensity: 0.4 prevents overly bright backgrounds
4. loading.view - Loading State¶
The loading screen displays while content is being fetched:
widget(container_y, {
space(1);
// Animated spinner
widget(throbber, {
});
widget(container_y, {
weight: 1;
filterConstraintY: true;
align: center;
// Status text
widget(label, {
align: center;
caption: $nav.currentpage.model.loadingStatus;
bold: true;
});
// Bitrate info (if available)
widget(container_x, {
spacing: 0.5em;
hidden: !$nav.currentpage.model.io.bitrateValid;
widget(label, {
filterConstraintX: true;
align: right;
color: 0.6;
caption: _("Bitrate:");
});
widget(label, {
filterConstraintX: true;
caption: fmt(_("%d kb/s"), $nav.currentpage.model.io.bitrate);
});
});
});
});
Components: - Throbber: Animated loading spinner - Status text: Shows what's being loaded - Bitrate display: Shows network speed (when available) - Info nodes: Additional loading information
5. pages/home.view - Content Page¶
The home page displays available services in a grid:
#import "skin://theme.view"
widget(container_z, {
widget(layer, {
widget(clip, {
widget(container_z, {
widget(array, {
id: "main";
clipOffsetTop: 3em;
scrollThresholdTop: 3em;
margin: [$ui.xmargin, 0em];
// Grid layout
childTilesX: $ui.aspect > 1 ? 5 : 2;
childTilesY: 4;
// Service items
cloner($core.services.stable, container_z, {
focusable: true;
ListItemHighlight();
onEvent(activate, navOpen($self.url, void, $self));
widget(container_y, {
spacing: 0.5em;
padding: 0.5em;
widget(displacement, {
scaling: [1,1,1] - [0.11,0.1,0] * iir(isPressed(), 4, true);
widget(icon, {
source: $self.icon ?? "dataroot://res/svg/Settings.svg";
saturation: iir(isPressed(), 4, true) * 0.66;
size: 3em;
});
});
widget(label, {
align: center;
caption: $self.title;
size: 1.2em;
});
});
});
});
});
});
});
widget(container_y, {
align: top;
PageHeader(_("Home"));
});
});
Key Patterns:
Grid Layout:
widget(array, {
childTilesX: $ui.aspect > 1 ? 5 : 2; // 5 columns landscape, 2 portrait
childTilesY: 4;
});
Data Binding:
Press Effect:
widget(displacement, {
scaling: [1,1,1] - [0.11,0.1,0] * iir(isPressed(), 4, true);
// Content scales down when pressed
});
Essential Patterns¶
1. Smooth Animations with iir()¶
The iir() function creates smooth interpolated animations:
- First parameter: Target value (0 or 1, or any expression)
- Second parameter: Speed (higher = slower, 4 is a good default)
- Third parameter (optional): Absolute mode (true/false)
Examples:
// Fade in/out
alpha: iir($visible, 4);
// Smooth color transition
color: iir(isHovered(), 4);
// Smooth scaling
scaling: [1,1,1] * (1 + 0.1 * iir(isNavFocused(), 4));
2. Conditional Display¶
Using hidden:
Using select:
Using translate:
3. Layout Containers¶
Horizontal layout:
Vertical layout:
Stacked layout:
4. Focus and Interaction¶
Making widgets focusable:
Focus indicators:
Hover effects:
5. Data Binding with Cloner¶
The cloner widget creates instances from data:
cloner($core.services.stable, container_z, {
// $self refers to each service item
widget(label, {
caption: $self.title;
});
});
Common data sources:
- $core.services.stable - Available services
- $nav.pages - Navigation pages
- $core.notifications.nodes - Notifications
- $self.model.nodes - Page-specific data
Installation and Testing¶
1. Create Skin Directory¶
Create your skin in the Movian skins directory:
Linux:
Windows:
macOS:
2. Copy Files¶
Copy all the view files to your skin directory:
- universe.view
- theme.view
- background.view
- loading.view
- pages/home.view
3. Activate Skin¶
- Start Movian
- Go to Settings → User Interface → Skin
- Select "minimal-skin"
- Restart Movian
4. Debugging¶
Enable debug logging:
1. Press Ctrl+D (or equivalent) to open debug menu
2. Enable "GLW debug" to see widget tree
3. Check console for error messages
Common issues:
- Syntax errors: Check for missing braces, semicolons
- Missing files: Verify file paths and names
- Layout issues: Use widget(border, {}) to visualize boundaries
Customization¶
Changing Colors¶
Edit universe.view:
$ui.color1 = "#ff4192"; // Pink
$ui.color2 = "#be3068"; // Dark pink
$ui.color3 = "#ffc2dd"; // Light pink
Adding New Macros¶
Edit theme.view:
#define MyCustomButton(CAPTION, ACTION) {
widget(container_z, {
focusable: true;
onEvent(activate, ACTION);
ListItemHighlight();
widget(label, {
padding: [1em, 0.5em];
caption: CAPTION;
});
});
}
Creating New Pages¶
Create pages/list.view:
#import "skin://theme.view"
widget(container_z, {
widget(layer, {
widget(list_y, {
cloner($self.model.nodes, container_z, {
focusable: true;
ListItemHighlight();
onEvent(activate, navOpen($self.url));
widget(label, {
padding: [1em, 0.5em];
caption: $self.title;
});
});
});
});
widget(container_y, {
align: top;
PageHeader($self.model.metadata.title);
});
});
Next Steps¶
To create a complete skin, add:
- More page types: list, grid, video, settings, directory
- Navigation menu: Sidebar or top navigation bar
- Media controls: Playdeck for audio/video playback
- OSD system: On-screen display for video settings
- Popup dialogs: Authentication, messages, file picker
- Custom graphics: Icons, backgrounds, textures
- Advanced animations: Transitions, effects, transformations
Reference¶
- Full example: See
movian-docs/docs/ui/theming/examples/minimal-skin/ - Complete skin: Study
movian/glwskins/flat/for advanced patterns - Best practices: See Skin Performance Best Practices Guide
- Documentation: Refer to the complete Movian theming guide
Summary¶
A minimal Movian skin requires:
- universe.view - Entry point with global configuration
- theme.view - Reusable macro definitions
- background.view - Visual backdrop
- loading.view - Loading state indicator
- pages/home.view - At least one content page
With these components and the essential patterns (macros, animations, data binding, focus handling), you have a solid foundation for creating custom Movian skins.