Fix Ncurses click handling and restore ncursesw build

This commit addresses issues with mouse interaction reliability and build configuration in the Ncurses renderer and example.

Renderer Changes:
- Enable explicit xterm 1003 mouse tracking (Any Event) via escape sequences to ensure reliable hover sensing from the first frame (though not so portable).
- Refactor `Clay_Ncurses_ProcessInput` to:
  - Correctly order `Clay_SetPointerState` calls to ensure the "Pressed" state is registered before the function returns.
  - Detect and return a new `CLAY_NCURSES_KEY_MOUSE_CLICK` event code for single, double, and triple clicks.
- Make `Clay_Ncurses_OnClick` as a semantic wrapper around `Clay_OnHover`.
- Add empty switch cases for `CLAY_RENDER_COMMAND_TYPE_IMAGE` and `CLAY_RENDER_COMMAND_TYPE_CUSTOM` to prevent unhandled enumeration warnings.

Example Application Changes:
- Update `main.c` to break the input processing loop immediately upon receiving `CLAY_NCURSES_KEY_MOUSE_CLICK`, ensuring the "Pressed" state is processed by the layout engine for that frame.

Build System:
- Update `CMakeLists.txt` to replace `FindCurses` with `find_library` and `find_path` for `ncursesw`.
This commit is contained in:
Seintian 2025-12-29 21:57:50 +01:00
parent e89f3d15e9
commit 8643a5d2d9
3 changed files with 49 additions and 38 deletions

View file

@ -1,17 +1,19 @@
cmake_minimum_required(VERSION 3.27)
project(clay-ncurses-example C)
set(CURSES_NEED_WIDE TRUE)
find_package(Curses REQUIRED)
# Find ncursesw explicitly
find_library(NCURSESW_LIB NAMES ncursesw REQUIRED)
find_path(NCURSESW_INCLUDE_DIR NAMES ncurses.h PATH_SUFFIXES ncursesw)
add_compile_definitions(_XOPEN_SOURCE_EXTENDED _XOPEN_SOURCE=700)
add_executable(clay-ncurses-example main.c)
target_link_libraries(clay-ncurses-example PRIVATE ${CURSES_LIBRARIES})
target_link_libraries(clay-ncurses-example PRIVATE ${NCURSESW_LIB})
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
target_link_libraries(clay-ncurses-example PRIVATE m)
endif()
target_include_directories(clay-ncurses-example PRIVATE ${CURSES_INCLUDE_DIRS} ../../)
target_include_directories(clay-ncurses-example PRIVATE ${NCURSESW_INCLUDE_DIR} ../../)

View file

@ -77,7 +77,9 @@ static AppState _appState = {
// -------------------------------------------------------------------------------------------------
void HandleHelpToggleClick(Clay_ElementId elementId, Clay_PointerData pointerInfo, void *userData) {
_appState.isHelpModalVisible = !_appState.isHelpModalVisible;
if (pointerInfo.state == CLAY_POINTER_DATA_PRESSED_THIS_FRAME) {
_appState.isHelpModalVisible = !_appState.isHelpModalVisible;
}
}
// -------------------------------------------------------------------------------------------------
@ -94,6 +96,11 @@ void App_ProcessInput() {
int key;
while ((key = Clay_Ncurses_ProcessInput(stdscr)) != ERR) {
if (key == CLAY_NCURSES_KEY_MOUSE_CLICK) {
// Stop processing input this frame to ensure Clay sees the "Pressed" state
// before a subsequent "Released" event might overwrite it.
break;
}
switch (key) {
case 'q':
case 'Q':

View file

@ -1,7 +1,7 @@
/**
* @file clay_renderer_ncurses.c
* @author Seintian
* @date 2025-12-28
* @date 2025-12-29
* @brief Ncurses renderer implementation for the Clay UI library.
*
* This file provides a backend for rendering Clay UI layouts using the Ncurses library.
@ -29,9 +29,9 @@
#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
#define CLAY_NCURSES_KEY_MOUSE_CLICK 123458
// -------------------------------------------------------------------------------------------------
// -- Internal State & Constants
@ -58,10 +58,6 @@ 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. */
@ -327,7 +323,7 @@ static void Clay_Ncurses_RenderBorder(Clay_RenderCommand *command) {
// Simplification: Check intersection with range for horizontal lines
int h_sx = x + 1;
int h_ex = x + w - 1;
int draw_sx = (h_sx > dx) ? h_sx : dx;
int draw_ex = (h_ex < dx + dw) ? h_ex : dx + dw;
@ -342,7 +338,7 @@ static void Clay_Ncurses_RenderBorder(Clay_RenderCommand *command) {
int h_ex = x + w - 1;
int draw_sx = (h_sx > dx) ? h_sx : dx;
int draw_ex = (h_ex < dx + dw) ? h_ex : dx + dw;
if (draw_ex > draw_sx) {
mvwhline(stdscr, y + h - 1, draw_sx, ACS_HLINE, draw_ex - draw_sx);
}
@ -354,7 +350,7 @@ static void Clay_Ncurses_RenderBorder(Clay_RenderCommand *command) {
int v_ey = y + h - 1;
int draw_sy = (v_sy > dy) ? v_sy : dy;
int draw_ey = (v_ey < dy + dh) ? v_ey : dy + dh;
if (draw_ey > draw_sy) {
mvwvline(stdscr, draw_sy, x, ACS_VLINE, draw_ey - draw_sy);
}
@ -441,16 +437,13 @@ void Clay_Ncurses_Initialize() {
keypad(stdscr, TRUE);
curs_set(0);
// 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.
// 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);
// Force xterm mouse tracking (Any Event) to ensure we get position updates
// even when buttons are not pressed. This fixes hover detection.
puts("\033[?1003h");
start_color();
use_default_colors();
@ -468,6 +461,9 @@ void Clay_Ncurses_Initialize() {
*/
void Clay_Ncurses_Terminate() {
if (_isInitialized) {
// Restore mouse tracking state
puts("\033[?1003l");
clear();
refresh();
endwin();
@ -517,9 +513,6 @@ 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);
@ -551,6 +544,8 @@ void Clay_Ncurses_Render(Clay_RenderCommandArray renderCommands) {
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END:
Clay_Ncurses_PopScissor();
break;
case CLAY_RENDER_COMMAND_TYPE_IMAGE:
case CLAY_RENDER_COMMAND_TYPE_CUSTOM:
default:
break;
}
@ -631,13 +626,17 @@ static int Clay_Ncurses_MeasureStringWidth(Clay_StringSlice text) {
wchar_t wc;
int bytes = mbtowc(&wc, ptr, len);
if (bytes <= 0) {
ptr++; len--; continue;
ptr++;
len--;
continue;
}
int w = wcwidth(wc);
if (w > 0) width += w;
ptr += bytes;
len -= bytes;
}
return width;
}
@ -650,7 +649,6 @@ static int Clay_Ncurses_MeasureStringWidth(Clay_StringSlice text) {
*/
int Clay_Ncurses_ProcessInput(WINDOW *window) {
int key = wgetch(window);
_pointerReleasedThisFrame = false;
// Handle Mouse
if (key == KEY_MOUSE) {
@ -664,19 +662,21 @@ 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);
bool shouldReturnClick = false;
if (event.bstate & (BUTTON1_PRESSED | BUTTON1_DOUBLE_CLICKED | BUTTON1_TRIPLE_CLICKED)) {
_isMouseDown = true;
if (event.bstate & BUTTON1_PRESSED) {
_pointerPressedThisFrame = true;
}
shouldReturnClick = true;
}
else if (event.bstate & BUTTON1_RELEASED) {
_isMouseDown = false;
}
if (event.bstate & BUTTON1_RELEASED) {
_isMouseDown = false;
// Update Clay State with the final determined state for this event
Clay_SetPointerState(mousePos, _isMouseDown);
if (shouldReturnClick) {
return CLAY_NCURSES_KEY_MOUSE_CLICK;
}
// Handle Scroll Wheel
@ -703,9 +703,11 @@ int Clay_Ncurses_ProcessInput(WINDOW *window) {
* @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_Hovered() && _pointerPressedThisFrame) {
Clay_PointerData pointerData = (Clay_PointerData){ .state = CLAY_POINTER_DATA_PRESSED_THIS_FRAME };
onClickFunc((Clay_ElementId){0}, pointerData, userData);
void Clay_Ncurses_OnClick(
void (*onClickFunc)(Clay_ElementId elementId, Clay_PointerData pointerData, void *userData),
void *userData
) {
if (onClickFunc) {
Clay_OnHover(onClickFunc, userData);
}
}