From e89f3d15e91a05dcaf6cffb0069ff17a0be45efb Mon Sep 17 00:00:00 2001 From: Seintian Date: Mon, 29 Dec 2025 20:33:20 +0100 Subject: [PATCH] feat(ncurses): event-driven scrolling & font styling - Update `renderers/ncurses/clay_renderer_ncurses.c`: - Export `CLAY_NCURSES_KEY_SCROLL_UP` and `CLAY_NCURSES_KEY_SCROLL_DOWN` key codes. - Modify `Clay_Ncurses_ProcessInput` to map mouse wheel events (`BUTTON4`, `BUTTON5`) to these key codes. - Update `Clay_Ncurses_OnClick` to trigger on `CLAY_POINTER_DATA_PRESSED_THIS_FRAME` for immediate feedback. - Update `examples/ncurses-example/main.c`: - Handle `CLAY_NCURSES_KEY_SCROLL_UP/DOWN` in `App_ProcessInput` to drive `_appState.scrollDelta`. - Simplify `HandleHelpToggleClick` to toggle visibility directly. - Apply bold and underline font styles to sidebar items. - Convert input processing to a `while` loop to process all pending events per frame. --- examples/ncurses-example/main.c | 16 +++---- renderers/ncurses/README.md | 17 +++++-- renderers/ncurses/clay_renderer_ncurses.c | 54 +++++++++++++++++++---- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/examples/ncurses-example/main.c b/examples/ncurses-example/main.c index 58b6b9a..b2f6e00 100644 --- a/examples/ncurses-example/main.c +++ b/examples/ncurses-example/main.c @@ -77,9 +77,7 @@ static AppState _appState = { // ------------------------------------------------------------------------------------------------- void HandleHelpToggleClick(Clay_ElementId elementId, Clay_PointerData pointerInfo, void *userData) { - if (pointerInfo.state == CLAY_POINTER_DATA_RELEASED_THIS_FRAME) { - _appState.isHelpModalVisible = !_appState.isHelpModalVisible; - } + _appState.isHelpModalVisible = !_appState.isHelpModalVisible; } // ------------------------------------------------------------------------------------------------- @@ -94,8 +92,8 @@ void HandleHelpToggleClick(Clay_ElementId elementId, Clay_PointerData pointerInf void App_ProcessInput() { _appState.scrollDelta = 0.0f; - int key = Clay_Ncurses_ProcessInput(stdscr); - if (key != ERR) { + int key; + while ((key = Clay_Ncurses_ProcessInput(stdscr)) != ERR) { switch (key) { case 'q': case 'Q': @@ -111,9 +109,11 @@ void App_ProcessInput() { _appState.isHelpModalVisible = !_appState.isHelpModalVisible; break; case KEY_UP: + case CLAY_NCURSES_KEY_SCROLL_UP: _appState.scrollDelta += DEFAULT_SCROLL_SENSITIVITY; break; case KEY_DOWN: + case CLAY_NCURSES_KEY_SCROLL_DOWN: _appState.scrollDelta -= DEFAULT_SCROLL_SENSITIVITY; break; } @@ -237,7 +237,7 @@ void UI_Sidebar() { .cornerRadius = { .topLeft = 1 }, .border = { .color = COLOR_ACCENT_RED, .width = {2, 2, 2, 2} } }) { - CLAY_TEXT(CLAY_STRING(" > TL Round"), CLAY_TEXT_CONFIG({ .textColor = COLOR_ACCENT_RED })); + CLAY_TEXT(CLAY_STRING(" > TL BOLD"), CLAY_TEXT_CONFIG({ .textColor = COLOR_ACCENT_RED, .fontId = CLAY_NCURSES_FONT_BOLD })); } CLAY(CLAY_ID("SidebarItemMixed2"), { @@ -246,7 +246,7 @@ void UI_Sidebar() { .cornerRadius = { .topLeft = 1, .bottomRight = 1 }, .border = { .color = {100, 255, 100, 255}, .width = {2, 2, 2, 2} } }) { - CLAY_TEXT(CLAY_STRING(" > Diagonal"), CLAY_TEXT_CONFIG({ .textColor = {100, 255, 100, 255} })); + CLAY_TEXT(CLAY_STRING(" > Diag Under"), CLAY_TEXT_CONFIG({ .textColor = {100, 255, 100, 255}, .fontId = CLAY_NCURSES_FONT_UNDERLINE })); } CLAY(CLAY_ID("SidebarItemMixed3"), { @@ -255,7 +255,7 @@ void UI_Sidebar() { .cornerRadius = { .topLeft = 1, .topRight = 1 }, .border = { .color = COLOR_ACCENT_BLUE, .width = {2, 2, 2, 2} } }) { - CLAY_TEXT(CLAY_STRING(" > Top Round"), CLAY_TEXT_CONFIG({ .textColor = COLOR_ACCENT_BLUE })); + CLAY_TEXT(CLAY_STRING(" > Top Bold Und"), CLAY_TEXT_CONFIG({ .textColor = COLOR_ACCENT_BLUE, .fontId = CLAY_NCURSES_FONT_BOLD | CLAY_NCURSES_FONT_UNDERLINE })); } } } diff --git a/renderers/ncurses/README.md b/renderers/ncurses/README.md index 690bd7a..2c073ec 100644 --- a/renderers/ncurses/README.md +++ b/renderers/ncurses/README.md @@ -75,10 +75,21 @@ while (!shouldQuit) { The renderer provides helper functions to easy integration of mouse interactions: -- **`Clay_Ncurses_ProcessInput(WINDOW *window)`**: Call this instead of `getch` or `wgetch`. It handles mouse events, updates the internal Clay pointer state, and returns the key code for your application to handle (e.g., keyboard shortcuts). -- **`Clay_Ncurses_OnClick(void (*userData)(...), void *userData)`**: A helper to attach a click listener to the current element. It uses `Clay_OnHover` internally. Your callback function should check if `pointerInfo.state == CLAY_POINTER_DATA_RELEASED_THIS_FRAME` to detect a valid click. +- **`Clay_Ncurses_ProcessInput(WINDOW *window)`**: Call this instead of `getch` or `wgetch`. It handles mouse events (including scroll wheel mapping to `Clay_UpdateScrollContainers`), updates the internal Clay pointer state, and returns the key code for your application to handle. +- **`Clay_Ncurses_OnClick(void (*userData)(...), void *userData)`**: A helper to attach a click listener to the current element. It uses `Clay_OnHover` internally. Your callback function should check if `pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME` for instant click feedback. -### 5. Cleanup +### 5. Font Styling + +You can apply **Bold** and **Underline** styles using the `fontId` configuration in `CLAY_TEXT`. +Use the provided macros: + +```c +CLAY_TEXT(CLAY_STRING("Bold Text"), CLAY_TEXT_CONFIG({ .fontId = CLAY_NCURSES_FONT_BOLD })); +CLAY_TEXT(CLAY_STRING("Underline"), CLAY_TEXT_CONFIG({ .fontId = CLAY_NCURSES_FONT_UNDERLINE })); +CLAY_TEXT(CLAY_STRING("Both"), CLAY_TEXT_CONFIG({ .fontId = CLAY_NCURSES_FONT_BOLD | CLAY_NCURSES_FONT_UNDERLINE })); +``` + +### 6. Cleanup Restore the terminal to its normal state before exiting. diff --git a/renderers/ncurses/clay_renderer_ncurses.c b/renderers/ncurses/clay_renderer_ncurses.c index 1cb288f..61c144f 100644 --- a/renderers/ncurses/clay_renderer_ncurses.c +++ b/renderers/ncurses/clay_renderer_ncurses.c @@ -26,6 +26,13 @@ #include #include "../../clay.h" +#define CLAY_NCURSES_FONT_BOLD 1 +#define CLAY_NCURSES_FONT_UNDERLINE 2 + +// Custom Key Codes for Mouse Scrolling +#define CLAY_NCURSES_KEY_SCROLL_UP 123456 +#define CLAY_NCURSES_KEY_SCROLL_DOWN 123457 + // ------------------------------------------------------------------------------------------------- // -- Internal State & Constants // ------------------------------------------------------------------------------------------------- @@ -51,6 +58,10 @@ static int _screenHeight = 0; /** @brief Flag indicating if the ncurses subsystem has been successfully initialized. */ static bool _isInitialized = false; +// Input State +static bool _pointerReleasedThisFrame = false; +static bool _pointerPressedThisFrame = false; + // Scissor / Clipping State /** @brief Maximum depth of the scissor/clipping stack. */ @@ -234,6 +245,9 @@ static void Clay_Ncurses_RenderText(Clay_RenderCommand *command) { int pair = Clay_Ncurses_GetColorPair(fg, bg); attron(COLOR_PAIR(pair)); + attron(COLOR_PAIR(pair)); + if (command->renderData.text.fontId & CLAY_NCURSES_FONT_BOLD) attron(A_BOLD); + if (command->renderData.text.fontId & CLAY_NCURSES_FONT_UNDERLINE) attron(A_UNDERLINE); // Complex multibyte string handling // We render to a temporary buffer first to handle wide characters @@ -277,6 +291,8 @@ static void Clay_Ncurses_RenderText(Clay_RenderCommand *command) { } free(wbuf); + if (command->renderData.text.fontId & CLAY_NCURSES_FONT_BOLD) attroff(A_BOLD); + if (command->renderData.text.fontId & CLAY_NCURSES_FONT_UNDERLINE) attroff(A_UNDERLINE); attroff(COLOR_PAIR(pair)); } @@ -428,7 +444,12 @@ void Clay_Ncurses_Initialize() { // We only ask for PRESS and RELEASE events. // If we ask for CLICK events, ncurses waits to see if a release happens quickly, // which delays the report of the PRESS event or swallows it, causing Clay to miss the "Down" state. - mousemask(BUTTON1_PRESSED | BUTTON1_RELEASED | REPORT_MOUSE_POSITION, NULL); + // We only ask for PRESS and RELEASE events. + // If we ask for CLICK events, ncurses waits to see if a release happens quickly, + // which delays the report of the PRESS event or swallows it, causing Clay to miss the "Down" state. + mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL); + // Disable strict click resolution to avoid input lag. + mouseinterval(0); start_color(); use_default_colors(); @@ -496,6 +517,9 @@ Clay_Dimensions Clay_Ncurses_MeasureText(Clay_StringSlice text, Clay_TextElement void Clay_Ncurses_Render(Clay_RenderCommandArray renderCommands) { if (!_isInitialized) return; + // Reset input state for next frame + _pointerPressedThisFrame = false; + // Update screen dimensions if terminal successfully resized int newW, newH; getmaxyx(stdscr, newH, newW); @@ -624,8 +648,6 @@ static int Clay_Ncurses_MeasureStringWidth(Clay_StringSlice text) { * @param window The Ncurses window to read input from (e.g. stdscr). * @return The key code pressed, or ERR if no input. */ -static bool _pointerReleasedThisFrame = false; - int Clay_Ncurses_ProcessInput(WINDOW *window) { int key = wgetch(window); _pointerReleasedThisFrame = false; @@ -643,16 +665,31 @@ int Clay_Ncurses_ProcessInput(WINDOW *window) { // Persistent state to handle drag/move events where button state might be absent in the event mask static bool _isMouseDown = false; + // Update Clay State FIRST so scroll/interaction logic knows where the mouse is + Clay_SetPointerState(mousePos, _isMouseDown); + if (event.bstate & (BUTTON1_PRESSED | BUTTON1_DOUBLE_CLICKED | BUTTON1_TRIPLE_CLICKED)) { _isMouseDown = true; + if (event.bstate & BUTTON1_PRESSED) { + _pointerPressedThisFrame = true; + } } if (event.bstate & BUTTON1_RELEASED) { _isMouseDown = false; } - // Update Clay State - Clay_SetPointerState(mousePos, _isMouseDown); + // Handle Scroll Wheel + #ifdef BUTTON4_PRESSED + if (event.bstate & BUTTON4_PRESSED) { + return CLAY_NCURSES_KEY_SCROLL_UP; + } + #endif + #ifdef BUTTON5_PRESSED + if (event.bstate & BUTTON5_PRESSED) { + return CLAY_NCURSES_KEY_SCROLL_DOWN; + } + #endif } } @@ -661,13 +698,14 @@ int Clay_Ncurses_ProcessInput(WINDOW *window) { /** * @brief Helper to attach an OnClick listener to the current element. - * Registers a hover callback. The user's function must check `pointerData.state == CLAY_POINTER_DATA_RELEASED_THIS_FRAME`. + * Registers a hover callback. The user's function must check `pointerData.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME`. * * @param onClickFunc Function pointer to call. * @param userData User data passed to the callback. */ void Clay_Ncurses_OnClick(void (*onClickFunc)(Clay_ElementId elementId, Clay_PointerData pointerData, void *userData), void *userData) { - if (onClickFunc) { - Clay_OnHover(onClickFunc, userData); + if (onClickFunc && Clay_Hovered() && _pointerPressedThisFrame) { + Clay_PointerData pointerData = (Clay_PointerData){ .state = CLAY_POINTER_DATA_PRESSED_THIS_FRAME }; + onClickFunc((Clay_ElementId){0}, pointerData, userData); } }