Event Guide
SableUI provides a safe event system that allows you to create interactive components with mouse, keyboard, and scroll input. Simple element events can be attached directly in the Layout() phase using inline callbacks; anything more involved — held keys, drag state, hit-testing — goes through the OnUpdate() method instead.
Inline Events
SableUI provides a small set of callbacks that attach directly to an element, for the common cases where you just need "something happened to this specific element."
Everything else — scrolling, all keyboard events, held/dragged state, and hover — is read from the
UIUpdateContextinOnUpdate(), covered below.
onClick
Triggered when the left mouse button is clicked on an element.
Div(onClick([this]() {
count.set(count.get() + 1);
SableUI_Log("Clicked! Count: %d", count.get());
}))
{
Text(SableString::Format("Click me, num clicks: %d", count.get()),
textColour(255, 255, 255));
}
onSecondaryClick
Triggered when the right mouse button is clicked on an element.
Div(onSecondaryClick([this]() {
SableUI_Info("Right clicked");
}))
{
Text("Right-click me", textColour(200, 200, 200));
}
Commonly paired with a context menu shown as a floating panel positioned at the click location.
onDoubleClick
Triggered when an element is clicked twice within a short window.
Note: The double-click timing window is 300ms, and clicks must be within 5 pixels of each other to register as a double-click. These thresholds are constants on the
Windowclass inwindow.h— they aren't currently exposed as something an application can configure per-instance.
[!WARNING] State lambdas can be dangerous and cause problems if used incorrectly — reference-capturing lambdas (
[&]) can be unstable if the referenced variable goes out of scope before the callback fires. The best practice is to capturethisand other arguments by value, for example:onClick([this, otherVar1, otherVar2]() {});.
Hover
There's no onHover/onHoverExit inline callback yet — ElementInfo doesn't carry hover callbacks the way it does onClickFunc. Until that lands, hover has to be computed manually in OnUpdate() by hit-testing the mouse position against an element's rect, the same way ButtonComponent tracks its own pressed state internally:
void OnUpdate(const UIUpdateContext& ctx) override
{
Element* root = GetRootElement();
if (!root) return;
bool hovered = RectBoundingBox(root->rect, ctx.input.mousePos, ctx.input.obscurers, ctx.zIndex);
isHovered.set(hovered);
}
Keyboard Input
Keyboard events are global rather than element-specific, so there's no onKeyPress(...) element callback — instead, override OnUpdate() and read key state off the input context directly. If a keyboard shortcut should only fire while the component is hovered, pair it with the same RectBoundingBox hit-test shown above.
Accessing the Event Context
Override OnUpdate() in your component to reach keyboard, mouse, and timer state:
class MyComponent : public SableUI::BaseComponent {
public:
void Layout() override {
// Your UI layout here
}
void OnUpdate(const UIUpdateContext& ctx) override {
// ctx.input is the UIInputState for this frame
// ctx.zIndex is this component's current z-index
}
};
Key Constants
SableUI provides constexpr constants for all keyboard keys, borrowed from GLFW (the window manager) for easy translation. These constants follow the pattern SABLE_KEY_*:
A list of these keys can be grabbed from
events.h
Key State Queries
ctx.input provides three ways to query key states, each a std::bitset<SABLE_MAX_KEYS>:
isKeyDown
true every frame while the key is held down.
void OnUpdate(const UIUpdateContext& ctx) override {
if (ctx.input.isKeyDown.test(SABLE_KEY_W))
{
// Move forward continuously
posY.set(posY.get() - speed * ctx.input.deltaTime);
}
if (ctx.input.isKeyDown.test(SABLE_KEY_S))
{
// Move backward continuously
posY.set(posY.get() + speed * ctx.input.deltaTime);
}
}
keyPressedEvent
true only on the frame a key is pressed. Use for single actions.
void OnUpdate(const UIUpdateContext& ctx) override {
if (ctx.input.keyPressedEvent.test(SABLE_KEY_SPACE))
{
// Toggle state once per press
isPaused.set(!isPaused.get());
}
}
keyReleasedEvent
true only on the frame a key is released.
void OnUpdate(const UIUpdateContext& ctx) override {
if (ctx.input.keyReleasedEvent.test(SABLE_KEY_LEFT_SHIFT))
{
// Stop running when shift is released
isRunning.set(false);
}
}
Modifier Keys
These tests can be combined to build key combination events.
void OnUpdate(const UIUpdateContext& ctx) override {
bool ctrlPressed = ctx.input.isKeyDown.test(SABLE_KEY_LEFT_CONTROL) ||
ctx.input.isKeyDown.test(SABLE_KEY_RIGHT_CONTROL);
// Ctrl+S for save
if (ctrlPressed && ctx.input.keyPressedEvent.test(SABLE_KEY_S))
{
Save();
}
}
Text input itself — actual typed characters, as opposed to individual key presses — comes through ctx.input.typedCharBuffer, a std::vector<unsigned int> of codepoints typed this frame. See TextFieldComponent in Components for a full example handling typed input, selection, and clipboard together.
Mouse Position and Scrolling
ctx.input also carries mouse position and scroll information:
Mouse Position
void OnUpdate(const UIUpdateContext& ctx) override {
int mouseX = ctx.input.mousePos.x;
int mouseY = ctx.input.mousePos.y;
// Mouse delta since last frame
int deltaX = ctx.input.mouseDelta.x;
int deltaY = ctx.input.mouseDelta.y;
SableUI_Log("Mouse pos: %dx%d, mouse delta: %dx%d",
mouseX, mouseY, deltaX, deltaY);
}
Scrolling
void OnUpdate(const UIUpdateContext& ctx) override {
float scrollX = ctx.input.scrollDelta.x;
float scrollY = ctx.input.scrollDelta.y;
if (scrollY != 0.0f)
{
// Zoom in/out based on scroll
zoomLevel.set(zoomLevel.get() + scrollY * 0.1f);
}
}
Mouse Button State
Mouse buttons are queried the same way as keyboard keys, against SABLE_MOUSE_BUTTON_* constants:
void OnUpdate(const UIUpdateContext& ctx) override {
// Check if left mouse button is held down
if (ctx.input.mouseDown.test(SABLE_MOUSE_BUTTON_LEFT))
{
// Drag operation
dragX.set(dragX.get() + ctx.input.mouseDelta.x);
dragY.set(dragY.get() + ctx.input.mouseDelta.y);
}
// Check for mouse button press
if (ctx.input.mousePressed.test(SABLE_MOUSE_BUTTON_LEFT))
{
isDragging.set(true);
}
// Check for mouse button release
if (ctx.input.mouseReleased.test(SABLE_MOUSE_BUTTON_LEFT))
{
isDragging.set(false);
}
}
There's also mouseDoubleClicked, tested the same way, if onDoubleClick on a specific element isn't granular enough for what you need.
Delta Time
ctx.input.deltaTime gives frame-independent timing for animation and movement:
void OnUpdate(const UIUpdateContext& ctx) override {
if (ctx.input.isKeyDown.test(SABLE_KEY_RIGHT))
{
// Move at constant speed regardless of frame rate
posX.set(posX.get() + speed * ctx.input.deltaTime);
}
}
deltaTime is in seconds, so if speed = 100.0f, the object moves at 100 pixels per second.
Timers
For anything on a schedule rather than tied to a specific input — a blinking cursor, a polling interval, a delayed action — use Timer or Interval instead of checking deltaTime by hand. See State Management for how those work and how they interact with OnUpdate.
State Management
State<T>, Ref<T>, Timer, and Interval for reactive components
Components
Button, Checkbox, TextField, DatePicker, and Calendar
Examples
See event handling in real applications