Piu is a declarative UI framework for building user interfaces in Alloy apps. It provides a component-based architecture with automatic layout, styling, and animation support.
Note: All code in this guide runs on the watch in
src/embeddedjs/main.js.
For apps using advanced Piu features (behaviors, templates, containers), include the Piu runtime:
import {} from "piu/MC";
A basic Piu application creates an Application object that fills the screen:
const application = new Application(null, {
skin: new Skin({ fill: "white" })
});
Content objects are the basic building blocks of Piu UIs. The main types are:
| Type | Description |
|---|---|
Content |
Basic rectangular element (base class for most UI objects) |
Container |
Holds other content objects |
Column |
Vertical layout container |
Row |
Horizontal layout container |
Label |
Single line of text |
Text |
Multi-line formatted text |
Application |
Root container (one per app) |
Skins define the visual appearance of content objects:
// Solid color skin
const whiteSkin = new Skin({ fill: "white" });
// Skin with stroke/border
const borderedSkin = new Skin({
fill: "transparent",
stroke: "black",
borders: { left: 2, right: 2, top: 2, bottom: 2 }
});
// Texture-based skin (for images)
const ballTexture = new Texture("balls.png");
const ballSkin = new Skin({
texture: ballTexture,
x: 0, y: 0,
width: 30, height: 30,
variants: 30 // for sprite sheets
});
Styles define text appearance. Styles cascade, similar to CSS - you can create a base style and let child content objects inherit from it, making it easy to maintain a consistent look:
const textStyle = new Style({
font: "OpenSans-Regular-15",
color: "black",
horizontal: "center", // "left", "right", "center", "justify"
left: 10, right: 10,
top: 15, bottom: 15
});
// Style with state colors (normal, active)
const buttonStyle = new Style({
font: "bold 18px Gothic",
color: ["black", "gray"] // [normal, active]
});
Content objects are positioned using constraints:
// Absolute positioning
Content($, { left: 20, top: 20, width: 80, height: 80 });
// Anchored to edges
Content($, { right: 20, bottom: 20, width: 80, height: 80 });
// Fill available space
Content($, { left: 0, right: 0, top: 0, bottom: 0 });
// Centered (no position constraints)
Content($, { width: 80, height: 80 });
Behaviors add interactivity and logic to content objects. They are essential for handling events and updating the UI:
class BallBehavior extends Behavior {
onCreate(ball, delta) {
// delta is the data passed to the content
this.dx = delta;
this.dy = delta;
}
onDisplaying(ball) {
// Store initial position and bounds
this.x = ball.x;
this.y = ball.y;
this.width = ball.container.width - ball.width;
this.height = ball.container.height - ball.height;
ball.start(); // Start time-based updates
}
onTimeChanged(ball) {
// Move the ball
ball.moveBy(this.dx, this.dy);
// Update position and check bounds
this.x += this.dx;
this.y += this.dy;
// Bounce off walls
if (this.x < 0 || this.x > this.width) this.dx = -this.dx;
if (this.y < 0 || this.y > this.height) this.dy = -this.dy;
}
}
// Attach behavior to content - the first argument (6) is passed to onCreate
Content(6, {
left: 0, top: 0,
width: 30, height: 30,
skin: ballSkin,
variant: 0,
Behavior: BallBehavior
});
| Event | Description |
|---|---|
onCreate(content, data) |
Content created with data |
onDisplaying(content) |
Content added to display |
onTimeChanged(content) |
Animation frame (after start()) |
onFinished(content) |
Animation/duration completed |
Pebble button presses are handled using the Button class rather than touch
events. See the Sensors and Input guide for
details on button handling. Piu also uses the Button class internally for
button input.
Templates create reusable component definitions. They are most useful when you need to create multiple instances of the same component:
// Define a template
const Square = Content.template($ => ({
width: 80, height: 80,
skin: new Skin({ fill: $ }) // $ is the data passed to template
}));
// Use the template
const redSquare = new Square("red", { left: 20, top: 20 });
const blueSquare = new Square("blue", { right: 20, bottom: 20 });
application.add(redSquare);
application.add(blueSquare);
Note: For one-off content, create instances directly rather than defining a template first. Templates use more code and RAM than direct instantiation when you only need a single instance.
Here's a complete example with multiple components:
const backgroundSkin = new Skin({ fill: "silver" });
const headerSkin = new Skin({ fill: "white" });
const headerStyle = new Style({ font: "bold 18px Gothic", color: "black" });
class HeaderBehavior extends Behavior {
onDisplaying(label) {
label.string = "My App";
}
}
const application = new Application(null, {
skin: backgroundSkin,
contents: [
new Column(null, {
top: 0, bottom: 0, left: 0, right: 0,
contents: [
new Label(null, {
top: 0, height: 30, left: 0, right: 0,
skin: headerSkin,
style: headerStyle,
Behavior: HeaderBehavior
}),
new Content(null, {
top: 10, bottom: 10, left: 10, right: 10,
skin: new Skin({ fill: "gray" })
})
]
})
]
});
Use anchors to reference content objects from behaviors:
const MyApp = Application.template($ => ({
contents: [
Label($, {
anchor: "TITLE", // Creates $.TITLE reference
string: "Hello"
})
],
Behavior: class extends Behavior {
onCreate(app, data) {
this.data = data;
}
updateTitle(app, newTitle) {
this.data.TITLE.string = newTitle;
}
}
}));
The Timeline class creates smooth animations:
import Timeline from "piu/Timeline";
class AnimatedBehavior extends Behavior {
onDisplaying(content) {
let timeline = new Timeline();
// Animate 'y' property over 750ms
timeline.to(content, { y: 100 }, 750, Math.quadEaseOut, 0);
content.duration = timeline.duration;
timeline.seekTo(0);
content.time = 0;
content.start();
}
onTimeChanged(content) {
this.timeline.seekTo(content.time);
}
}
Available easing functions on the Math object:
Math.backEaseIn / Math.backEaseOutMath.bounceEaseIn / Math.bounceEaseOutMath.circularEaseIn / Math.circularEaseOutMath.cubicEaseIn / Math.cubicEaseOutMath.elasticEaseIn / Math.elasticEaseOutMath.exponentialEaseIn / Math.exponentialEaseOutMath.quadEaseIn / Math.quadEaseOutMath.quartEaseIn / Math.quartEaseOutMath.quintEaseIn / Math.quintEaseOutMath.sineEaseIn / Math.sineEaseOutFor more animation techniques including chaining and looping, see the Animations guide.
Load and display images using Texture and Skin:
// Load a texture from resources
const logoTexture = new Texture("logo.png");
// Create a skin from the texture
const logoSkin = new Skin({
texture: logoTexture,
x: 0, y: 0,
width: 64, height: 64
});
// Display the image
application.add(new Content(null, {
skin: logoSkin
}));
application.add(new Label(null, {
left: 0, right: 0, top: 50,
style: new Style({ font: "bold 24px Gothic", color: "black" }),
string: "Hello, World!"
}));
application.add(new Text(null, {
left: 10, right: 10, top: 10,
style: textStyle,
blocks: [
{ spans: "First paragraph of text." },
{ spans: [
"Second paragraph with ",
{ style: boldStyle, spans: "bold text" },
" inline."
]}
]
}));
Access built-in Pebble fonts with CSS-like syntax:
// Format: "[style] [size]px [family]"
const headerStyle = new Style({
font: "bold 14px Gothic",
color: "black"
});
const titleStyle = new Style({
font: "black 30px Bitham",
color: "blue"
});
const bodyStyle = new Style({
font: "24px Leco",
color: "white"
});
Available Pebble fonts include:
- Gothic - Regular and Bold weights
- Bitham - Black and Bold weights
- Roboto - Condensed weight
- Leco - Regular weight (great for numbers)
- Droid - Serif style
The available sizes are limited for each font family. See the System Fonts reference for the full table of all fonts, styles, and sizes.
Load images from Pebble resources by ID:
// Load texture by resource ID
const backgroundTexture = new Texture(2); // Resource ID 2
// Create a tiled background skin
const backgroundSkin = new Skin({
texture: backgroundTexture,
x: 0, y: 0,
width: 40, height: 40,
tiles: { left: 0, right: 0, top: 0, bottom: 0 }
});
// Load and display an image
const iconTexture = new Texture(3);
application.add(new Content(null, {
skin: new Skin({
texture: iconTexture,
width: iconTexture.width,
height: iconTexture.height
})
}));
Here's a complete animated app with multiple bouncing balls:
import {} from "piu/MC";
const backgroundSkin = new Skin({ fill: "silver" });
const ballTexture = new Texture("balls.png");
const ballSkin = new Skin({
texture: ballTexture,
x: 0, y: 0,
width: 30, height: 30,
variants: 30 // sprite sheet with 30px variants
});
class BallBehavior extends Behavior {
onCreate(ball, delta) {
this.dx = delta;
this.dy = delta;
}
onDisplaying(ball) {
this.x = ball.x;
this.y = ball.y;
this.width = ball.container.width - ball.width;
this.height = ball.container.height - ball.height;
ball.start();
}
onTimeChanged(ball) {
ball.moveBy(this.dx, this.dy);
this.x += this.dx;
this.y += this.dy;
if (this.x < 0 || this.x > this.width) this.dx = -this.dx;
if (this.y < 0 || this.y > this.height) this.dy = -this.dy;
}
}
const BallApplication = Application.template($ => ({
skin: backgroundSkin,
contents: [
Content(6, { left: 0, top: 0, skin: ballSkin, variant: 0, Behavior: BallBehavior }),
Content(5, { right: 0, top: 0, skin: ballSkin, variant: 1, Behavior: BallBehavior }),
Content(4, { right: 0, bottom: 0, skin: ballSkin, variant: 2, Behavior: BallBehavior }),
Content(3, { left: 0, bottom: 0, skin: ballSkin, variant: 3, Behavior: BallBehavior }),
]
}));
export default new BallApplication(null, { pixels: screen.width * 4 });
The Pebble Examples repository includes several Piu examples:
hellopiu-balls - animated bouncing balls with Behavior and texture variantshellopiu-coloredsquares - basic layout with colored skinshellopiu-gbitmap - displaying Pebble GBitmap PNG imageshellopiu-jsicon - displaying a Moddable SDK bitmap from a PNG resourcehellopiu-text - dynamic text layout with different fonts and alignmenthellopiu-pebbletext - text rendering with Pebble's built-in fonts