Skip to main content

Layout Script Protocol

Learning Objectives

  • Understand Layout Script lifecycle functions
  • Use measure() to propose sizes for Hug layouts
  • Use resize() to react to container size changes
  • Position children programmatically within layouts

AE/JS Syntax Comparison

ConceptCSS/JavaScriptLuau (Rive Layout Scripts)
Container query@container (min-width)resize(self, size) callback
Preferred sizewidth: fit-contentmeasure() returns Vector
Flexbox gapgap: 10pxManual positioning in resize()
Grid layoutdisplay: gridCustom logic in resize()
Key Insight

Layout Scripts let you implement custom layout algorithms. Unlike CSS where you pick from predefined models (flex, grid), in Rive you write the exact positioning logic yourself.


Overview

Layout Scripts extend Node Scripts to give you programmatic control over Layout components. They enable custom layout behaviors like masonry grids, carousels, and dynamic spacing logic.

When to use Layout Scripts:

  • Custom responsive layouts (masonry, carousel)
  • Dynamic spacing/positioning logic
  • Programmatic child arrangement
  • Data-driven layout systems

The Layout Script Template

--!strict

export type MyLayout = {
spacing: Input<number>,
columns: Input<number>,
}

function init(self: MyLayout, context: Context): boolean
-- Initialize layout state
return true
end

function advance(self: MyLayout, seconds: number): boolean
-- Per-frame updates (for animated layouts)
return true
end

function update(self: MyLayout)
-- Called when inputs (spacing, columns) change
end

function draw(self: MyLayout, renderer: Renderer)
-- Custom rendering (optional)
end

-- Propose ideal size (only for "Hug" fit type)
function measure(self: MyLayout): Vector
return Vector.xy(200, 150)
end

-- REQUIRED: React to container size changes
function resize(self: MyLayout, size: Vector)
-- Position children based on available size
local width = size.x
local height = size.y
-- ... layout logic here
end

return function(): Layout<MyLayout>
return {
init = init,
advance = advance,
update = update,
draw = draw,
measure = measure,
resize = resize,
spacing = 10,
columns = 3,
}
end

Type Signature Reference

Layout Protocol Functions

FunctionSignatureRequiredDescription
init(self: T, context: Context): booleanYesInitialize layout state
resize(self: T, size: Vector)YesReact to container size changes
measure(self: T): VectorNoPropose ideal size (Hug fit only)
update(self: T)NoRespond to input changes
advance(self: T, seconds: number): booleanNoPer-frame animation updates
draw(self: T, renderer: Renderer)NoCustom rendering

Factory Return Type

return function(): Layout<YourLayoutType>
return {
init = init,
resize = resize, -- Required
measure = measure, -- Optional
update = update, -- Optional
-- ... your inputs with defaults
}
end

Lifecycle Functions

resize(self, size: Vector)REQUIRED

Called whenever the layout container changes size. This is where you position children.

function resize(self: MyLayout, size: Vector)
print(`Container resized to: {size.x} x {size.y}`)
-- Position children here
end

When it's called:

  • On initial layout load
  • When the container resizes
  • When parent layout changes

measure(self): Vector — Optional

Proposes an ideal size for layouts with Hug fit type. Return a Vector with your preferred width and height.

function measure(self: MyLayout): Vector
-- Calculate ideal size based on content
local totalWidth = self.columns * (self.itemWidth + self.spacing)
local totalHeight = self.rows * (self.itemHeight + self.spacing)
return Vector.xy(totalWidth, totalHeight)
end

Important: measure() is only effective when the Layout's Fit type is set to Hug. For Fill or Fixed layouts, the return value is ignored.


Other Lifecycle Functions

Layout Scripts inherit all Node Script lifecycle functions:

FunctionPurpose
init(self, context)One-time initialization
advance(self, seconds)Per-frame updates (for animations)
update(self)React to Input changes
draw(self, renderer)Custom rendering

Fit Types and measure()

The effect of measure() depends on the Layout's Fit setting:

Fit TypeBehaviormeasure() Effect
HugContainer sizes to fit contentYour measure() return is used
FillContainer fills available spacemeasure() ignored
FixedContainer has explicit sizemeasure() ignored
-- measure() only matters for Hug layouts
function measure(self: MyLayout): Vector
if self.fitType == "hug" then
return Vector.xy(self.contentWidth, self.contentHeight)
end
-- For Fill/Fixed, this return is ignored anyway
return Vector.xy(0, 0)
end

Practice Exercises

Exercise 1: Simple Horizontal Layout ⭐

Premise

Layout Scripts give you precise control over positioning. Understanding resize() is fundamental—it's where all the layout magic happens.

Goal

By the end of this exercise, you will implement a simple horizontal layout that spaces items evenly.

Use Case

You're building a toolbar with evenly-spaced buttons. The toolbar width may change, and buttons should redistribute automatically.


Setup

In Rive Editor:

  1. Create the Layout:

    • Add a Layout component to the Artboard
    • Set Fit to "Fill" (width) and "Fixed" (height: 60)
  2. Create the script:

    • Assets panel → + → Script → Layout Script
    • Name it Exercise1_HorizontalLayout
  3. Attach and run:

    • Add the script as child of the Layout
    • Press Play

Starter Code

--!strict

export type Exercise1 = {
itemCount: Input<number>,
}

function resize(self: Exercise1, size: Vector)
local count = self.itemCount
local width = size.x

-- TODO: Calculate spacing between items
-- Formula: spacing = width / (count + 1)
local spacing = 0

print(`ANSWER: count={count},width={width},spacing={spacing}`)
end

return function(): Layout<Exercise1>
return {
resize = resize,
itemCount = 4,
}
end

Assignment

  1. Calculate spacing using the formula: width / (itemCount + 1)
  2. Run with default itemCount of 4 and a container width of 500

Expected Output

Your console output should display the item count, container width, and calculated spacing between items.


Verify Your Answer

Verify Your Answer

Checklist

  • spacing is calculated correctly
  • Formula divides width by (count + 1)
  • Output matches expected line

Exercise 2: Grid Layout Calculation ⭐⭐

Premise

Grid layouts require calculating both row and column positions. The resize() callback gives you the container dimensions to work with.

Goal

By the end of this exercise, you will calculate row and column counts for a responsive grid.

Use Case

You're building a responsive image gallery. As the container resizes, the grid should adjust column count to fit available space.


Setup

In Rive Editor:

  1. Create the script:

    • Assets panel → + → Script → Layout Script
    • Name it Exercise2_GridLayout
  2. Attach and run:

    • Add to a Layout component
    • Press Play

Starter Code

--!strict

export type Exercise2 = {
itemSize: Input<number>,
itemCount: Input<number>,
}

function resize(self: Exercise2, size: Vector)
local itemSize = self.itemSize
local itemCount = self.itemCount
local width = size.x

-- TODO 1: Calculate columns that fit in width
-- Use: math.floor(width / itemSize)
local columns = 0

-- TODO 2: Calculate rows needed for all items
-- Use: math.ceil(itemCount / columns)
local rows = 0

print(`ANSWER: cols={columns},rows={rows}`)
end

return function(): Layout<Exercise2>
return {
resize = resize,
itemSize = 100,
itemCount = 10,
}
end

Assignment

  1. Calculate columns using math.floor(width / itemSize)
  2. Calculate rows using math.ceil(itemCount / columns)
  3. Container width is 450, itemSize is 100, itemCount is 10

Expected Output

Your console output should display the calculated column and row counts for the grid.


Verify Your Answer

Verify Your Answer

Checklist

  • Columns calculated with math.floor
  • Rows calculated with math.ceil
  • Output matches expected line

Exercise 3: Measure for Hug Layout ⭐⭐

Premise

When a Layout uses "Hug" fit type, your measure() function proposes the ideal size. The container then sizes to fit.

Goal

By the end of this exercise, you will implement measure() to propose a size based on content.

Use Case

You're building a dynamic tag cloud where the container should grow to fit all tags. The layout "hugs" its content.


Setup

In Rive Editor:

  1. Create the Layout:

    • Add a Layout component
    • Set Fit to "Hug" for both width and height
  2. Create the script:

    • Assets panel → + → Script → Layout Script
    • Name it Exercise3_HugLayout
  3. Attach and run


Starter Code

--!strict

export type Exercise3 = {
tagWidth: Input<number>,
tagHeight: Input<number>,
tagCount: Input<number>,
columns: Input<number>,
}

function measure(self: Exercise3): Vector
local tagW = self.tagWidth
local tagH = self.tagHeight
local count = self.tagCount
local cols = self.columns

-- TODO 1: Calculate total width needed
-- Width = columns * tagWidth
local totalWidth = 0

-- TODO 2: Calculate rows needed
local rows = math.ceil(count / cols)

-- TODO 3: Calculate total height needed
-- Height = rows * tagHeight
local totalHeight = 0

print(`ANSWER: w={totalWidth},h={totalHeight}`)
return Vector.xy(totalWidth, totalHeight)
end

function resize(self: Exercise3, size: Vector)
-- Layout positioning would go here
end

return function(): Layout<Exercise3>
return {
measure = measure,
resize = resize,
tagWidth = 80,
tagHeight = 30,
tagCount = 12,
columns = 4,
}
end

Assignment

  1. Calculate totalWidth as columns * tagWidth
  2. Calculate totalHeight as rows * tagHeight
  3. With tagWidth=80, tagHeight=30, tagCount=12, columns=4

Expected Output

Your console output should display the proposed width and height for the hug layout.


Verify Your Answer

Verify Your Answer

Checklist

  • totalWidth = columns × tagWidth
  • rows = ceil(tagCount / columns)
  • totalHeight = rows × tagHeight
  • Output matches expected line

Knowledge Check

Q:When is measure() called and what does it do?
Q:What is the key difference between resize() and measure()?
Q:Which lifecycle function is REQUIRED in a Layout Script?
Q:For which Fit type does measure() have an effect?

Common Mistakes

Avoid These Errors
  1. Forgetting resize() is required

    -- WRONG: No resize function
    return function(): Layout<MyLayout>
    return { measure = measure } -- Error!
    end

    -- CORRECT: Always include resize
    return function(): Layout<MyLayout>
    return { resize = resize, measure = measure }
    end
  2. Expecting measure() to work with Fill layouts

    -- measure() is ignored for Fill fit type
    -- The container size is determined by parent, not your measure()
  3. Not using the size parameter in resize()

    -- WRONG: Ignoring container size
    function resize(self: MyLayout, size: Vector)
    positionAt(100, 100) -- Hardcoded, doesn't adapt!
    end

    -- CORRECT: Use size to calculate positions
    function resize(self: MyLayout, size: Vector)
    local centerX = size.x / 2
    local centerY = size.y / 2
    positionAt(centerX, centerY)
    end

Self-Assessment Checklist

  • I understand when resize() is called
  • I know the difference between measure() and resize()
  • I can implement a simple grid layout in resize()
  • I understand which Fit type uses measure()
  • I can calculate positions based on container size

Next Steps