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();
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;
};
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;
}
};
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:
Horizontal Split
Grid Layout
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);
using namespace Fern;
// 2x2 grid that adapts to window size
auto topRow = Row({
Expanded(Container(Colors::Red, 0, 0, 0, 0,
Center(Text(Point(0, 0), "1", 3, Colors::White))), 1),
Expanded(Container(Colors::Green, 0, 0, 0, 0,
Center(Text(Point(0, 0), "2", 3, Colors::White))), 1)
});
auto bottomRow = Row({
Expanded(Container(Colors::Blue, 0, 0, 0, 0,
Center(Text(Point(0, 0), "3", 3, Colors::White))), 1),
Expanded(Container(Colors::Yellow, 0, 0, 0, 0,
Center(Text(Point(0, 0), "4", 3, Colors::Black))), 1)
});
auto grid = Column({
Expanded(topRow, 1),
Expanded(bottomRow, 1)
});
addWidget(grid);
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
// Good: Relative to canvas size
int x = getWidth() / 2;
int y = getHeight() / 2;
// Avoid: Hardcoded positions
int x = 400;
int y = 300;
// 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);
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();
}
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