Skip to main content

Overview

Fern uses a signals and slots pattern for event handling. Signals are emitted when events occur, and slots (callback functions) connected to those signals are called automatically. This provides a type-safe, decoupled way to handle user interactions and widget events.

Signal Basics

The Signal class is a template that accepts argument types:
template <typename... Args>
class Signal {
public:
    using ConnectionID = size_t;
    using SlotFunction = std::function<void(Args...)>;
    
    ConnectionID connect(SlotFunction slot);
    void emit(Args... args) const;
    void disconnect(ConnectionID id);
};

Connecting to Signals

1
Step 1: Create a Widget with Signals
2
Most Fern widgets have built-in signals:
3
using namespace Fern;

auto button = Button(ButtonConfig(100, 50, 120, 40, "Click Me"));

// ButtonWidget has these signals:
// - Signal<> onClick
// - Signal<bool> onHover
// - Signal<bool> onPress
4
Step 2: Connect a Slot (Callback)
5
Connect a lambda or function to the signal:
6
// Connect with lambda
button->onClick.connect([]() {
    std::cout << "Button clicked!" << std::endl;
});

// Connect with function pointer
void handleClick() {
    std::cout << "Clicked!" << std::endl;
}
button->onClick.connect(handleClick);

// Connect with member function
class MyApp {
    void onButtonClick() {
        std::cout << "Member function called!" << std::endl;
    }
    
    void setupUI() {
        auto btn = Button(ButtonConfig(0, 0, 100, 40, "Click"));
        btn->onClick.connect([this]() {
            onButtonClick();
        });
    }
};
7
Step 3: Signals Emit Automatically
8
The widget emits the signal when the event occurs:
9
// Internal to ButtonWidget::handleInput()
if (clicked) {
    onClick.emit();  // All connected slots are called
}

Signal Types

No arguments

Simple event notification without parameters:
Fern::Signal<> onClick;

// Connect
onClick.connect([]() {
    std::cout << "Event triggered!" << std::endl;
});

// Emit
onClick.emit();

Single Argument - Signal<T>

Pass data with the event:
Fern::Signal<bool> onToggle;

// Connect
onToggle.connect([](bool isOn) {
    std::cout << "Toggle: " << (isOn ? "ON" : "OFF") << std::endl;
});

// Emit
onToggle.emit(true);

Multiple arguments

Signals can pass multiple values using template parameters: Pass multiple values:
Fern::Signal<int, std::string> onValueChange;

// Connect
onValueChange.connect([](int value, const std::string& label) {
    std::cout << label << ": " << value << std::endl;
});

// Emit
onValueChange.emit(42, "Score");

Managing Connections

Save Connection ID

Store the connection ID to disconnect later:
auto button = Fern::Button(ButtonConfig(0, 0, 100, 40, "Click"));

// Save connection ID
auto connectionID = button->onClick.connect([]() {
    std::cout << "First handler" << std::endl;
});

// Later: disconnect
button->onClick.disconnect(connectionID);

Multiple Connections

Connect multiple slots to the same signal:
auto button = Fern::Button(ButtonConfig(0, 0, 100, 40, "Click"));

// All of these will be called when button is clicked
button->onClick.connect([]() {
    std::cout << "Handler 1" << std::endl;
});

button->onClick.connect([]() {
    std::cout << "Handler 2" << std::endl;
});

button->onClick.connect([]() {
    std::cout << "Handler 3" << std::endl;
});

// Output when clicked:
// Handler 1
// Handler 2  
// Handler 3

Conditional Disconnection

class ClickCounter {
    int count = 0;
    Fern::Signal<>::ConnectionID connID;
    
    void setup() {
        auto btn = Fern::Button(ButtonConfig(0, 0, 100, 40, "Click"));
        
        connID = btn->onClick.connect([this, btn]() {
            count++;
            std::cout << "Clicks: " << count << std::endl;
            
            if (count >= 10) {
                btn->onClick.disconnect(connID);
                std::cout << "Max clicks reached!" << std::endl;
            }
        });
    }
};

Widget Signals

Button Signals

Signal<> onClick;

// Emitted when button is clicked
button->onClick.connect([]() {
    std::cout << "Clicked!" << std::endl;
});

Custom Widget Signals

Add signals to your custom widgets:
class SliderWidget : public Fern::Widget {
public:
    // Define signals
    Fern::Signal<float> onValueChange;
    Fern::Signal<> onDragStart;
    Fern::Signal<> onDragEnd;
    
    bool handleInput(const Fern::InputState& input) override {
        // Emit signals when appropriate
        if (dragging && valueChanged) {
            onValueChange.emit(currentValue);
        }
        
        if (startedDragging) {
            onDragStart.emit();
        }
        
        if (stoppedDragging) {
            onDragEnd.emit();
        }
        
        return handled;
    }
    
private:
    float currentValue = 0.5f;
    bool dragging = false;
};

Practical Examples

Counter with Multiple Buttons

using namespace Fern;

static int counter = 0;
static std::shared_ptr<TextWidget> display;

void setupUI() {
    display = Text(Point(0, 0), "Count: 0", 3, Colors::White);
    
    auto incrementBtn = Button(ButtonConfig(0, 0, 100, 40, "+"));
    auto decrementBtn = Button(ButtonConfig(0, 0, 100, 40, "-"));
    auto resetBtn = Button(ButtonConfig(0, 0, 100, 40, "Reset"));
    
    // Connect increment
    incrementBtn->onClick.connect([]() {
        counter++;
        display->setText("Count: " + std::to_string(counter));
    });
    
    // Connect decrement
    decrementBtn->onClick.connect([]() {
        counter--;
        display->setText("Count: " + std::to_string(counter));
    });
    
    // Connect reset
    resetBtn->onClick.connect([]() {
        counter = 0;
        display->setText("Count: 0");
    });
    
    auto layout = Column({
        display,
        SizedBox(0, 20),
        Row({decrementBtn, SizedBox(10, 0), incrementBtn}, false),
        SizedBox(0, 10),
        resetBtn
    });
    
    addWidget(Center(layout));
}

Button State Tracking

void setupUI() {
    auto button = Fern::Button(ButtonConfig(100, 50, 120, 40, "Hover Me"));
    
    button->onHover.connect([](bool hovered) {
        if (hovered) {
            std::cout << "Mouse entered button" << std::endl;
        } else {
            std::cout << "Mouse left button" << std::endl;
        }
    });
    
    button->onPress.connect([](bool pressed) {
        if (pressed) {
            std::cout << "Button pressed down" << std::endl;
        } else {
            std::cout << "Button released" << std::endl;
        }
    });
    
    button->onClick.connect([]() {
        std::cout << "Button clicked (full press+release)" << std::endl;
    });
}

Widget Communication

class App {
    std::shared_ptr<TextWidget> statusText;
    std::shared_ptr<ButtonWidget> saveBtn;
    std::shared_ptr<ButtonWidget> loadBtn;
    
    void setupUI() {
        statusText = Text(Point(0, 0), "Ready", 2, Colors::White);
        
        saveBtn = Button(ButtonPresets::Success(0, 0, 100, 40, "Save"));
        loadBtn = Button(ButtonPresets::Primary(0, 0, 100, 40, "Load"));
        
        saveBtn->onClick.connect([this]() {
            statusText->setText("Saving...");
            statusText->setColor(Colors::Warning);
            performSave();
            statusText->setText("Saved!");
            statusText->setColor(Colors::Success);
        });
        
        loadBtn->onClick.connect([this]() {
            statusText->setText("Loading...");
            statusText->setColor(Colors::Info);
            performLoad();
            statusText->setText("Loaded!");
            statusText->setColor(Colors::Success);
        });
    }
    
    void performSave() { /* ... */ }
    void performLoad() { /* ... */ }
};

Custom Signal Example

class ColorPicker : public Fern::Widget {
public:
    Fern::Signal<uint32_t> onColorSelect;  // Emits selected color
    
    void render() override {
        // Draw color swatches
        for (size_t i = 0; i < colors.size(); i++) {
            int swatchX = x_ + (i % 4) * 40;
            int swatchY = y_ + (i / 4) * 40;
            Fern::Draw::fillRect(swatchX, swatchY, 35, 35, colors[i]);
        }
    }
    
    bool handleInput(const Fern::InputState& input) override {
        if (input.mousePressed) {
            int localX = input.mouseX - x_;
            int localY = input.mouseY - y_;
            int index = (localY / 40) * 4 + (localX / 40);
            
            if (index >= 0 && index < colors.size()) {
                onColorSelect.emit(colors[index]);  // Emit selected color
                return true;
            }
        }
        return false;
    }
    
private:
    std::vector<uint32_t> colors = {
        Colors::Red, Colors::Green, Colors::Blue, Colors::Yellow,
        Colors::Cyan, Colors::Magenta, Colors::Orange, Colors::Purple
    };
};

// Usage
auto picker = std::make_shared<ColorPicker>();
picker->onColorSelect.connect([](uint32_t color) {
    std::cout << "Selected color: 0x" << std::hex << color << std::endl;
});

Best Practices

1
Use Lambda Captures Carefully
2
// Good: Capture by value for simple types
int value = 42;
button->onClick.connect([value]() {
    std::cout << value << std::endl;
});

// Good: Capture shared_ptr to keep widget alive
auto text = Text(Point(0, 0), "Hello", 2, Colors::White);
button->onClick.connect([text]() {
    text->setColor(Colors::Red);
});

// Caution: Capturing 'this' - ensure object lifetime
class MyApp {
    void setup() {
        button->onClick.connect([this]() {
            handleClick();  // 'this' must be valid
        });
    }
};
3
Disconnect When Done
4
Avoid memory leaks by disconnecting unused signals:
5
class TemporaryButton {
    Fern::Signal<>::ConnectionID connID;
    
    void enable() {
        connID = button->onClick.connect([]() { /* ... */ });
    }
    
    void disable() {
        button->onClick.disconnect(connID);
    }
};
6
Avoid Circular References
7
// Bad: circular reference
auto widget1 = std::make_shared<MyWidget>();
auto widget2 = std::make_shared<MyWidget>();

widget1->signal.connect([widget2]() { /* uses widget2 */ });
widget2->signal.connect([widget1]() { /* uses widget1 */ });
// Both keep each other alive!

// Good: use weak_ptr for back-references
std::weak_ptr<MyWidget> weakWidget1 = widget1;
widget2->signal.connect([weakWidget1]() {
    if (auto w = weakWidget1.lock()) {
        // Use widget safely
    }
});
8
Keep Slot Functions Fast
9
Signal callbacks run on the main thread:
10
// Good: quick operations
button->onClick.connect([]() {
    counter++;
    updateDisplay();
});

// Avoid: long-running operations
button->onClick.connect([]() {
    // Don't do this in a signal handler:
    loadHugeFile();  // Blocks UI
    complexCalculation();  // Freezes rendering
});
Signals are called in the order they were connected. If order matters, connect signals in the desired sequence.

Next Steps

Build docs developers (and LLMs) love