Skip to main content

Overview

Responsive design ensures your UI adapts gracefully to different window sizes and resize events. Fern provides layouts, responsive widgets, and callbacks to build flexible interfaces.

Window Resize Handling

Setting Up Resize Callback

Register a callback to handle window resize events:
#include <fern/fern.hpp>

using namespace Fern;

int main() {
    initialize();
    
    setupUI();
    
    // Handle window resize
    setWindowResizeCallback([](int newWidth, int newHeight) {
        std::cout << "Window resized to: " 
                  << newWidth << "x" << newHeight << std::endl;
        
        // Update widget positions/sizes
        WidgetManager::getInstance().handleWindowResize(newWidth, newHeight);
    });
    
    setDrawCallback(draw);
    startRenderLoop();
    return 0;
}

Getting Canvas Size

// Get current canvas dimensions
int width = Fern::getWidth();
int height = Fern::getHeight();

// Get as Point
Fern::Point size = Fern::getCanvasSize();

Responsive Widget Interface

Implement ResponsiveWidget to receive resize events:
class ResponsiveWidget {
public:
    virtual ~ResponsiveWidget() = default;
    
    // Called when window is resized
    virtual void onWindowResize(int newWidth, int newHeight) = 0;
};

Creating Responsive Widgets

class MyResponsiveWidget : public Fern::Widget, 
                          public Fern::ResponsiveWidget {
public:
    MyResponsiveWidget() {
        updateSize(Fern::getWidth(), Fern::getHeight());
    }
    
    void onWindowResize(int newWidth, int newHeight) override {
        updateSize(newWidth, newHeight);
    }
    
    void render() override {
        // Render with responsive dimensions
        Fern::Draw::fillRect(x_, y_, width_, height_, Fern::Colors::Blue);
    }
    
    bool handleInput(const Fern::InputState& input) override {
        return false;
    }
    
private:
    void updateSize(int canvasWidth, int canvasHeight) {
        // Center widget at 50% of screen size
        width_ = canvasWidth / 2;
        height_ = canvasHeight / 2;
        x_ = (canvasWidth - width_) / 2;
        y_ = (canvasHeight - height_) / 2;
    }
};

Built-in Responsive Widgets

Center Widget

CenterWidget automatically recenters on resize:
using namespace Fern;

void setupUI() {
    int width = getWidth();
    int height = getHeight();
    
    auto button = Button(ButtonConfig(0, 0, 150, 50, "Centered"));
    
    // Create center widget
    auto centerWidget = std::make_shared<CenterWidget>(0, 0, width, height);
    centerWidget->add(button);
    
    addWidget(centerWidget);
    
    // Resize automatically handled
    setWindowResizeCallback([](int w, int h) {
        WidgetManager::getInstance().handleWindowResize(w, h);
    });
}

Responsive Layout Patterns

Flexible Layouts with Expanded

Use Expanded to create layouts that adapt to available space:
using namespace Fern;

int width = getWidth();
int height = getHeight();

// Left sidebar (30%) + Main content (70%)
auto sidebar = Container(
    Colors::DarkGray, 0, 0, 0, 0,
    Center(Text(Point(0, 0), "Sidebar", 2, Colors::White))
);

auto content = Container(
    Colors::LightGray, 0, 0, 0, 0,
    Center(Text(Point(0, 0), "Content", 3, Colors::Black))
);

auto layout = Row({
    Expanded(sidebar, 3),   // 30%
    Expanded(content, 7)    // 70%
});

addWidget(layout);

Percentage-Based Sizing

using namespace Fern;

void createResponsiveButton() {
    int width = getWidth();
    int height = getHeight();
    
    // Button takes 40% of screen width, 10% of height
    int btnWidth = width * 0.4;
    int btnHeight = height * 0.1;
    
    // Centered position
    int btnX = (width - btnWidth) / 2;
    int btnY = (height - btnHeight) / 2;
    
    auto button = Button(
        ButtonConfig(btnX, btnY, btnWidth, btnHeight, "40% Width")
    );
}

Maintaining Aspect Ratio

class AspectRatioBox : public Fern::Widget, public Fern::ResponsiveWidget {
public:
    AspectRatioBox(float aspectRatio) : aspectRatio_(aspectRatio) {
        updateSize(Fern::getWidth(), Fern::getHeight());
    }
    
    void onWindowResize(int newWidth, int newHeight) override {
        updateSize(newWidth, newHeight);
    }
    
    void render() override {
        Fern::Draw::fillRect(x_, y_, width_, height_, Fern::Colors::Blue);
    }
    
    bool handleInput(const Fern::InputState& input) override {
        return false;
    }
    
private:
    float aspectRatio_;  // width / height
    
    void updateSize(int canvasWidth, int canvasHeight) {
        // Fit to 80% of screen while maintaining aspect ratio
        int maxWidth = canvasWidth * 0.8;
        int maxHeight = canvasHeight * 0.8;
        
        if (maxWidth / aspectRatio_ <= maxHeight) {
            width_ = maxWidth;
            height_ = maxWidth / aspectRatio_;
        } else {
            height_ = maxHeight;
            width_ = maxHeight * aspectRatio_;
        }
        
        // Center
        x_ = (canvasWidth - width_) / 2;
        y_ = (canvasHeight - height_) / 2;
    }
};

// Usage: 16:9 aspect ratio box
auto box = std::make_shared<AspectRatioBox>(16.0f / 9.0f);

Adaptive UI Patterns

Breakpoint-Based Layout

Change layout based on screen size:
class AdaptiveLayout {
    std::shared_ptr<Fern::Widget> currentLayout;
    
    void setupUI() {
        updateLayout(Fern::getWidth(), Fern::getHeight());
        
        Fern::setWindowResizeCallback([this](int w, int h) {
            updateLayout(w, h);
            Fern::WidgetManager::getInstance().handleWindowResize(w, h);
        });
    }
    
    void updateLayout(int width, int height) {
        using namespace Fern;
        
        // Remove old layout
        if (currentLayout) {
            WidgetManager::getInstance().removeWidget(currentLayout);
        }
        
        // Mobile layout (< 600px)
        if (width < 600) {
            currentLayout = createMobileLayout();
        }
        // Tablet layout (600-1200px)
        else if (width < 1200) {
            currentLayout = createTabletLayout();
        }
        // Desktop layout (>= 1200px)
        else {
            currentLayout = createDesktopLayout();
        }
        
        addWidget(currentLayout);
    }
    
    std::shared_ptr<Fern::Widget> createMobileLayout() {
        // Vertical stack for mobile
        return Column({
            createHeader(),
            Expanded(createContent(), 1),
            createFooter()
        });
    }
    
    std::shared_ptr<Fern::Widget> createTabletLayout() {
        // Two-column for tablet
        return Column({
            createHeader(),
            Expanded(
                Row({
                    Expanded(createSidebar(), 1),
                    Expanded(createContent(), 2)
                }),
                1
            ),
            createFooter()
        });
    }
    
    std::shared_ptr<Fern::Widget> createDesktopLayout() {
        // Three-column for desktop
        return Column({
            createHeader(),
            Expanded(
                Row({
                    Expanded(createSidebar(), 1),
                    Expanded(createContent(), 3),
                    Expanded(createExtras(), 1)
                }),
                1
            ),
            createFooter()
        });
    }
};

Scaling UI Elements

class ScalableUI {
    float scaleFactor = 1.0f;
    
    void updateScale(int width, int height) {
        // Scale based on width (base: 1920px = 1.0x)
        scaleFactor = width / 1920.0f;
        scaleFactor = std::max(0.5f, std::min(2.0f, scaleFactor));  // Clamp
    }
    
    void createScaledButton() {
        int baseWidth = 120;
        int baseHeight = 40;
        int baseFontSize = 2;
        
        int scaledWidth = baseWidth * scaleFactor;
        int scaledHeight = baseHeight * scaleFactor;
        int scaledFontSize = baseFontSize * scaleFactor;
        
        auto button = Fern::Button(
            Fern::ButtonConfig(0, 0, scaledWidth, scaledHeight, "Scaled")
                .style(Fern::ButtonStyle().textScale(scaledFontSize))
        );
    }
};

Dynamic Content Adjustment

Hiding Elements on Small Screens

class ResponsiveToolbar {
    std::vector<std::shared_ptr<Fern::ButtonWidget>> allButtons;
    std::vector<std::shared_ptr<Fern::ButtonWidget>> visibleButtons;
    
    void updateVisibleButtons(int width) {
        visibleButtons.clear();
        
        if (width < 600) {
            // Show only essential buttons on mobile
            visibleButtons.push_back(allButtons[0]);  // Menu
            visibleButtons.push_back(allButtons[1]);  // Search
        } else if (width < 1000) {
            // Show more buttons on tablet
            for (int i = 0; i < 4 && i < allButtons.size(); i++) {
                visibleButtons.push_back(allButtons[i]);
            }
        } else {
            // Show all buttons on desktop
            visibleButtons = allButtons;
        }
        
        rebuildToolbar();
    }
    
    void rebuildToolbar() {
        // Recreate Row with visible buttons
        auto toolbar = Fern::Row({
            visibleButtons.begin(),
            visibleButtons.end()
        }, false, Fern::MainAxisAlignment::SpaceBetween);
    }
};

Text Wrapping and Truncation

void createResponsiveText(int maxWidth) {
    std::string fullText = "This is a long text that might need wrapping";
    
    using namespace Fern;
    
    int fontSize = 2;
    int charWidth = 6 * fontSize;  // Approximate
    int maxChars = maxWidth / charWidth;
    
    std::string displayText = fullText;
    if (fullText.length() > maxChars) {
        displayText = fullText.substr(0, maxChars - 3) + "...";
    }
    
    auto text = Text(
        Point(10, 10),
        displayText,
        fontSize,
        Colors::White
    );
}

Best Practices

1
Use Relative Positioning
2
// Good: Relative to canvas size
int x = getWidth() / 2;
int y = getHeight() / 2;

// Avoid: Hardcoded positions
int x = 400;
int y = 300;
3
Leverage Layout Widgets
4
// Good: Use layouts
auto layout = Column({
    header,
    Expanded(content, 1),
    footer
});

// Avoid: Manual positioning
header->setPosition(0, 0);
content->setPosition(0, 80);
footer->setPosition(0, height - 60);
5
Test Multiple Sizes
6
int main() {
    // Test with different initial sizes
    Fern::initialize(800, 600);   // Tablet
    // Fern::initialize(1920, 1080);  // Desktop
    // Fern::initialize(375, 667);     // Mobile
    
    setupUI();
    Fern::startRenderLoop();
}
7
Handle Minimum Sizes
8
void onWindowResize(int width, int height) {
    // Enforce minimum size
    const int MIN_WIDTH = 320;
    const int MIN_HEIGHT = 240;
    
    if (width < MIN_WIDTH || height < MIN_HEIGHT) {
        // Show warning or scale content
        return;
    }
    
    updateLayout(width, height);
}
Use Expanded widgets with flex factors to create truly responsive layouts that adapt to any screen size.

Complete Example: Responsive App

#include <fern/fern.hpp>

using namespace Fern;

class ResponsiveApp {
    std::shared_ptr<CenterWidget> centerWidget;
    std::shared_ptr<ColumnWidget> mainLayout;
    
public:
    void setup() {
        int width = getWidth();
        int height = getHeight();
        
        createUI();
        
        centerWidget = std::make_shared<CenterWidget>(0, 0, width, height);
        centerWidget->add(mainLayout);
        addWidget(centerWidget);
        
        setWindowResizeCallback([this](int w, int h) {
            handleResize(w, h);
        });
    }
    
    void createUI() {
        auto title = Text(
            TextConfig(0, 0, "Responsive App")
                .style(TextStyle().fontSize(4).color(Colors::White))
        );
        
        auto button1 = Button(
            ButtonPresets::Primary(0, 0, 200, 45, "Button 1")
        );
        
        auto button2 = Button(
            ButtonPresets::Success(0, 0, 200, 45, "Button 2")
        );
        
        mainLayout = Column({
            title,
            SizedBox(0, 30),
            button1,
            SizedBox(0, 15),
            button2
        }, false, MainAxisAlignment::Center, CrossAxisAlignment::Center);
    }
    
    void handleResize(int newWidth, int newHeight) {
        WidgetManager::getInstance().handleWindowResize(newWidth, newHeight);
        
        std::cout << "Resized to: " << newWidth << "x" << newHeight << std::endl;
    }
};

ResponsiveApp app;

void setupUI() {
    app.setup();
}

void draw() {
    Draw::fill(Colors::DarkGray);
}

int main() {
    initialize(1024, 768);
    setupUI();
    setDrawCallback(draw);
    startRenderLoop();
    return 0;
}

Next Steps

Build docs developers (and LLMs) love