Skip to main content
Fern’s layout engine provides automatic widget positioning and sizing through a hierarchy of layout widgets. This system enables responsive, maintainable UIs without manual coordinate calculations.

Layout Architecture

The layout system is built on three core concepts:
  1. Layout Widgets: Containers that arrange child widgets
  2. Layout Algorithms: Methods for calculating child positions
  3. Layout Hierarchy: Trees of layouts containing other layouts or widgets

Base Layout Widget

All layout widgets inherit from LayoutWidget:
class LayoutWidget : public Widget {
public:
    LayoutWidget(int x, int y, int width, int height);
    
    void render() override;           // Renders all children
    bool handleInput(const InputState& input) override;  // Forwards to children
    void setPosition(int x, int y) override;  // Moves all children
    void resize(int width, int height) override;  // Triggers rearrangement
    void updateLayout();              // Manually trigger layout update
    
protected:
    virtual void arrangeChildren() = 0;  // Layout algorithm
    std::vector<std::shared_ptr<Widget>> children_;
};

Key Responsibilities

Rendering: LayoutWidget renders all children in order:
void LayoutWidget::render() {
    for (auto& child : children_) {
        child->render();
    }
}
Input Forwarding: Input is forwarded to children in reverse Z-order:
bool LayoutWidget::handleInput(const InputState& input) {
    for (auto it = children_.rbegin(); it != children_.rend(); ++it) {
        if ((*it)->handleInput(input)) {
            return true;
        }
    }
    return false;
}
Position Management: Moving a layout moves all children:
void LayoutWidget::setPosition(int x, int y) {
    int deltaX = x - x_;
    int deltaY = y - y_;
    
    x_ = x;
    y_ = y;
    
    for (auto& child : children_) {
        child->setPosition(
            child->getX() + deltaX,
            child->getY() + deltaY
        );
    }
}

Layout Algorithms

Alignment Enumerations

enum class MainAxisAlignment {
    Start,         // Align to beginning
    Center,        // Center items
    End,           // Align to end
    SpaceBetween,  // Distribute with no edge spacing
    SpaceAround,   // Distribute with half spacing at edges
    SpaceEvenly    // Distribute with equal spacing everywhere
};

enum class CrossAxisAlignment {
    Start,    // Align to start of cross axis
    Center,   // Center on cross axis
    End,      // Align to end of cross axis
    Stretch   // Fill cross axis dimension
};

Column Layout

Arranges children vertically:
class ColumnWidget : public LayoutWidget {
public:
    ColumnWidget(
        int x, int y, int width, int height,
        std::vector<std::shared_ptr<Widget>> children,
        bool fullWidth = false,
        MainAxisAlignment mainAlign = MainAxisAlignment::Start,
        CrossAxisAlignment crossAlign = CrossAxisAlignment::Start
    );
    
    void setMainAxisAlignment(MainAxisAlignment align);
    void setCrossAxisAlignment(CrossAxisAlignment align);
    
protected:
    void arrangeChildren() override;
    
private:
    bool fullWidth_;
    MainAxisAlignment mainAxisAlignment_;
    CrossAxisAlignment crossAxisAlignment_;
};
Arrangement Algorithm:
void ColumnWidget::arrangeChildren() {
    if (children_.empty()) return;
    
    // Calculate total height needed
    int totalHeight = 0;
    for (auto& child : children_) {
        totalHeight += child->getHeight();
    }
    
    // Calculate starting Y based on main axis alignment
    int currentY = y_;
    switch (mainAxisAlignment_) {
        case MainAxisAlignment::Start:
            currentY = y_;
            break;
        case MainAxisAlignment::Center:
            currentY = y_ + (height_ - totalHeight) / 2;
            break;
        case MainAxisAlignment::End:
            currentY = y_ + height_ - totalHeight;
            break;
        case MainAxisAlignment::SpaceBetween:
            // Calculate spacing between children
            break;
        // Other cases...
    }
    
    // Position each child
    for (auto& child : children_) {
        int childX = x_;
        
        // Calculate X based on cross axis alignment
        switch (crossAxisAlignment_) {
            case CrossAxisAlignment::Start:
                childX = x_;
                break;
            case CrossAxisAlignment::Center:
                childX = x_ + (width_ - child->getWidth()) / 2;
                break;
            case CrossAxisAlignment::End:
                childX = x_ + width_ - child->getWidth();
                break;
            case CrossAxisAlignment::Stretch:
                childX = x_;
                child->resize(width_, child->getHeight());
                break;
        }
        
        child->setPosition(childX, currentY);
        currentY += child->getHeight();
    }
}

Row Layout

Arranges children horizontally:
class RowWidget : public LayoutWidget {
public:
    RowWidget(
        int x, int y, int width, int height,
        std::vector<std::shared_ptr<Widget>> children,
        MainAxisAlignment mainAlign = MainAxisAlignment::Start,
        CrossAxisAlignment crossAlign = CrossAxisAlignment::Start
    );
    
protected:
    void arrangeChildren() override;
};
Arrangement Algorithm (mirrors Column but on horizontal axis):
void RowWidget::arrangeChildren() {
    if (children_.empty()) return;
    
    // Calculate total width needed
    int totalWidth = 0;
    for (auto& child : children_) {
        totalWidth += child->getWidth();
    }
    
    // Calculate starting X based on main axis alignment
    int currentX = calculateStartPosition(totalWidth);
    
    // Position each child
    for (auto& child : children_) {
        int childY = calculateCrossAxisPosition(child);
        child->setPosition(currentX, childY);
        currentX += child->getWidth();
    }
}

Center Layout

Centers a single child within available space:
class CenterWidget : public LayoutWidget, public ResponsiveWidget {
public:
    CenterWidget(int x, int y, int width, int height);
    
    void add(std::shared_ptr<Widget> child);
    void onWindowResize(int newWidth, int newHeight) override;
    
protected:
    void arrangeChildren() override;
};
Centering Algorithm:
void CenterWidget::arrangeChildren() {
    if (children_.empty()) return;
    
    auto& child = children_[0];
    
    int centerX = x_ + (width_ - child->getWidth()) / 2;
    int centerY = y_ + (height_ - child->getHeight()) / 2;
    
    child->setPosition(centerX, centerY);
}

Layout Composition

Layouts can be nested to create complex arrangements:
void createComplexUI() {
    // Header row
    auto headerRow = std::make_shared<RowWidget>(
        0, 0, 800, 50,
        std::vector<std::shared_ptr<Widget>>{
            Text(Point(0, 0), "MyApp", 3, Colors::White),
            Button(ButtonConfig(0, 0, 80, 30, "Menu"))
        },
        MainAxisAlignment::SpaceBetween,
        CrossAxisAlignment::Center
    );
    
    // Main content column
    auto contentColumn = std::make_shared<ColumnWidget>(
        0, 50, 800, 500,
        std::vector<std::shared_ptr<Widget>>{
            Text(Point(0, 0), "Welcome", 4, Colors::White),
            SizedBox(0, 20),  // Spacing
            // More content...
        },
        true,  // fullWidth
        MainAxisAlignment::Start,
        CrossAxisAlignment::Center
    );
    
    // Footer
    auto footer = Text(Point(0, 0), "© 2025", 1, Colors::Gray);
    
    // Combine in main column
    auto mainLayout = std::make_shared<ColumnWidget>(
        0, 0, 800, 600,
        std::vector<std::shared_ptr<Widget>>{
            headerRow,
            contentColumn,
            footer
        },
        true,
        MainAxisAlignment::SpaceBetween
    );
    
    addWidget(mainLayout);
}

Helper Widgets

SizedBox

Creates spacing or fixed-size areas:
class SizedBox : public Widget {
public:
    SizedBox(int width, int height)
        : width_(width), height_(height) {}
    
    void render() override {
        // Renders nothing - just takes up space
    }
    
    bool handleInput(const InputState& input) override {
        return false;  // No interaction
    }
    
    int getWidth() const override { return width_; }
    int getHeight() const override { return height_; }
};

// Usage:
auto column = Column({
    Text(Point(0, 0), "First", 2, Colors::White),
    SizedBox(0, 20),  // 20 pixels vertical spacing
    Text(Point(0, 0), "Second", 2, Colors::White)
});

Expanded

Fills available space in layouts:
class Expanded : public Widget {
public:
    Expanded(std::shared_ptr<Widget> child, int flex = 1)
        : child_(child), flex_(flex) {}
    
    void render() override {
        child_->render();
    }
    
    bool handleInput(const InputState& input) override {
        return child_->handleInput(input);
    }
    
    int getFlex() const { return flex_; }
    
private:
    std::shared_ptr<Widget> child_;
    int flex_;
};

// Layout handles Expanded specially:
int totalFlex = calculateTotalFlex();
int availableSpace = width_ - fixedWidthUsed;

for (auto& child : children_) {
    auto expanded = dynamic_cast<Expanded*>(child.get());
    if (expanded) {
        int flexWidth = (availableSpace * expanded->getFlex()) / totalFlex;
        child->resize(flexWidth, height_);
    }
}

Responsive Layouts

Layouts can respond to window resize:
class ResponsiveLayout : public ColumnWidget, public ResponsiveWidget {
public:
    void onWindowResize(int newWidth, int newHeight) override {
        resize(newWidth, newHeight);
        arrangeChildren();
        
        // Notify children if they're responsive
        for (auto& child : children_) {
            auto responsive = dynamic_cast<ResponsiveWidget*>(child.get());
            if (responsive) {
                responsive->onWindowResize(newWidth, newHeight);
            }
        }
    }
};

Layout Best Practices

Use Layouts for Positioning

// Good: Let layout handle positions
auto column = Column({
    Text(Point(0, 0), "Item 1", 2, Colors::White),
    Text(Point(0, 0), "Item 2", 2, Colors::White),
    Text(Point(0, 0), "Item 3", 2, Colors::White)
});

// Avoid: Manual positioning
auto text1 = Text(Point(100, 50), "Item 1", 2, Colors::White);
auto text2 = Text(Point(100, 80), "Item 2", 2, Colors::White);
auto text3 = Text(Point(100, 110), "Item 3", 2, Colors::White);

Spacing with SizedBox

// Good: Explicit spacing control
auto column = Column({
    Text(Point(0, 0), "Title", 3, Colors::White),
    SizedBox(0, 20),
    Text(Point(0, 0), "Content", 2, Colors::Gray)
});

// Avoid: Magic number positioning

Composition over Complexity

// Good: Compose simple layouts
auto header = Row({logo, menuButton});
auto content = Column({title, description, buttons});
auto page = Column({header, content, footer});

// Avoid: Single complex layout

Performance Considerations

Layout Calculation Cost

  • Layout arrangement is O(n) where n = number of children
  • Triggered on: resize, position change, manual update
  • Cache layout results when possible

Nested Layout Overhead

// Efficient: Shallow hierarchy
Column({ Row({a, b}), Row({c, d}) })

// Less efficient: Deep nesting
Column({ Column({ Column({ Row({a}) }) }) })

Avoid Unnecessary Updates

// Good: Update once after changes
layout->add(child1);
layout->add(child2);
layout->add(child3);
layout->updateLayout();  // Single update

// Avoid: Update after each change
layout->add(child1);
layout->updateLayout();
layout->add(child2);
layout->updateLayout();

Advanced Layout Patterns

Conditional Layout

void createResponsiveLayout(int screenWidth) {
    std::vector<std::shared_ptr<Widget>> items = getItems();
    
    std::shared_ptr<LayoutWidget> layout;
    
    if (screenWidth < 600) {
        // Mobile: vertical layout
        layout = std::make_shared<ColumnWidget>(0, 0, screenWidth, 800, items);
    } else {
        // Desktop: horizontal layout
        layout = std::make_shared<RowWidget>(0, 0, screenWidth, 600, items);
    }
    
    addWidget(layout);
}

Dynamic Layout Updates

class DynamicList : public ColumnWidget {
public:
    void addItem(std::shared_ptr<Widget> item) {
        children_.push_back(item);
        arrangeChildren();
    }
    
    void removeItem(std::shared_ptr<Widget> item) {
        children_.erase(
            std::remove(children_.begin(), children_.end(), item),
            children_.end()
        );
        arrangeChildren();
    }
};

See Also

Build docs developers (and LLMs) love