mirror of
https://github.com/nicbarker/clay.git
synced 2026-02-06 12:48:49 +00:00
feat(ncurses): Add interaction callbacks and improve input handling
Introduces Input Processing and Interaction helpers for the Ncurses renderer, ensuring robust mouse support and simplified event handling. **Renderer (`renderers/ncurses`):** - **`Clay_Ncurses_ProcessInput`**: Added a dedicated input processing function that handles both keyboard and mouse events. - Implemented persistent `_isMouseDown` state tracking to fix missed "fast clicks" and preserve button state during drag operations. - Adjusted `mousemask` to `BUTTON1_PRESSED | BUTTON1_RELEASED | REPORT_MOUSE_POSITION` to bypass Ncurses' internal click resolution delay. - **`Clay_Ncurses_OnClick`**: Added a helper function to easily attach click listeners. - Registers the user's callback directly via `Clay_OnHover` (avoiding allocation/proxies). - Matches the standard Clay callback signature pattern. **Example (`examples/ncurses-example`):** - **Input Loop**: Migrated main loop to use `Clay_Ncurses_ProcessInput`. - **Interactions**: - Added a "Toggle Help" button to the sidebar. - Implemented `HandleHelpToggleClick` callback, which explicitly checks for `CLAY_POINTER_DATA_RELEASED_THIS_FRAME` to validate clicks. - Added visual hover effects to sidebar items. **Documentation (`renderers/ncurses/README.md`):** - Updated "Usage" section to demonstrate `Clay_Ncurses_ProcessInput`. - Added "Input & Interaction" section documenting the new helpers.
This commit is contained in:
parent
bc742a190a
commit
c700104760
3 changed files with 203 additions and 17 deletions
|
|
@ -72,20 +72,30 @@ static AppState _appState = {
|
|||
.scrollDelta = 0.0f
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// -- Callback Handlers
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
void HandleHelpToggleClick(Clay_ElementId elementId, Clay_PointerData pointerInfo, void *userData) {
|
||||
if (pointerInfo.state == CLAY_POINTER_DATA_RELEASED_THIS_FRAME) {
|
||||
_appState.isHelpModalVisible = !_appState.isHelpModalVisible;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// -- Input Processing
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Processes keyboard input for the current frame.
|
||||
* @brief Processes input for the current frame.
|
||||
* Updates _appState directly based on key presses.
|
||||
* Uses ncurses getch() (non-blocking if timeout is set).
|
||||
* Uses ncurses input processing (non-blocking if timeout is set).
|
||||
*/
|
||||
void App_ProcessInput() {
|
||||
_appState.scrollDelta = 0.0f;
|
||||
|
||||
int key;
|
||||
while ((key = getch()) != ERR) {
|
||||
int key = Clay_Ncurses_ProcessInput(stdscr);
|
||||
if (key != ERR) {
|
||||
switch (key) {
|
||||
case 'q':
|
||||
case 'Q':
|
||||
|
|
@ -93,6 +103,7 @@ void App_ProcessInput() {
|
|||
break;
|
||||
case 's':
|
||||
case 'S':
|
||||
// Toggle between two states
|
||||
_appState.isSidebarVisible = !_appState.isSidebarVisible;
|
||||
break;
|
||||
case 'h':
|
||||
|
|
@ -180,7 +191,7 @@ void UI_ServerStatusWidget() {
|
|||
void UI_SidebarItem(Clay_String label, Clay_Color textColor) {
|
||||
CLAY(CLAY_ID_LOCAL("SidebarItem"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 2) } },
|
||||
.backgroundColor = COLOR_PANEL_BG
|
||||
.backgroundColor = Clay_Hovered() ? (Clay_Color){60, 60, 60, 255} : COLOR_PANEL_BG
|
||||
}) {
|
||||
CLAY_TEXT(label, CLAY_TEXT_CONFIG({ .textColor = textColor }));
|
||||
}
|
||||
|
|
@ -210,11 +221,20 @@ void UI_Sidebar() {
|
|||
UI_SidebarItem(CLAY_STRING(" > Item 1 🌍"), (Clay_Color){0, 255, 255, 255});
|
||||
UI_SidebarItem(CLAY_STRING(" > Item 2 🌐"), COLOR_TEXT_WHITE);
|
||||
|
||||
CLAY(CLAY_ID("HelpToggleButton"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 2) }, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} },
|
||||
.backgroundColor = Clay_Hovered() ? (Clay_Color){0, 100, 0, 255} : COLOR_PANEL_BG,
|
||||
.cornerRadius = {1}
|
||||
}) {
|
||||
Clay_Ncurses_OnClick(HandleHelpToggleClick, NULL);
|
||||
CLAY_TEXT(CLAY_STRING(" > Toggle Help"), CLAY_TEXT_CONFIG({ .textColor = COLOR_TEXT_WHITE }));
|
||||
}
|
||||
|
||||
// Mixed Style Items
|
||||
CLAY(CLAY_ID("SidebarItemMixed1"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 3) }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER } },
|
||||
.backgroundColor = COLOR_PANEL_BG,
|
||||
.cornerRadius = { .topLeft = 8 },
|
||||
.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 }));
|
||||
|
|
@ -223,7 +243,7 @@ void UI_Sidebar() {
|
|||
CLAY(CLAY_ID("SidebarItemMixed2"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 3) }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER } },
|
||||
.backgroundColor = COLOR_PANEL_BG,
|
||||
.cornerRadius = { .topLeft = 8, .bottomRight = 8 },
|
||||
.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} }));
|
||||
|
|
@ -232,7 +252,7 @@ void UI_Sidebar() {
|
|||
CLAY(CLAY_ID("SidebarItemMixed3"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 3) }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER } },
|
||||
.backgroundColor = COLOR_PANEL_BG,
|
||||
.cornerRadius = { .topLeft = 8, .topRight = 8 },
|
||||
.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 }));
|
||||
|
|
@ -387,7 +407,7 @@ void UI_HelpModal() {
|
|||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.backgroundColor = {30, 30, 30, 255},
|
||||
.cornerRadius = {4},
|
||||
.cornerRadius = {1},
|
||||
.border = { .color = COLOR_TEXT_WHITE, .width = {2, 2, 2, 2} }
|
||||
}) {
|
||||
CLAY_TEXT(CLAY_STRING("Ncurses Example Help"), CLAY_TEXT_CONFIG({ .textColor = COLOR_TEXT_WHITE }));
|
||||
|
|
@ -434,7 +454,7 @@ int main() {
|
|||
|
||||
Clay_Initialize(arena, (Clay_Dimensions){0,0}, (Clay_ErrorHandler){NULL});
|
||||
Clay_SetMeasureTextFunction(Clay_Ncurses_MeasureText, NULL);
|
||||
|
||||
|
||||
// Initialize Ncurses Renderer
|
||||
Clay_Ncurses_Initialize();
|
||||
|
||||
|
|
@ -452,12 +472,6 @@ int main() {
|
|||
Clay_ElementData viewportData = Clay_GetElementData(viewportId);
|
||||
|
||||
if (viewportData.found) {
|
||||
Clay_Vector2 center = {
|
||||
viewportData.boundingBox.x + viewportData.boundingBox.width / 2,
|
||||
viewportData.boundingBox.y + viewportData.boundingBox.height / 2
|
||||
};
|
||||
Clay_SetPointerState(center, false);
|
||||
|
||||
Clay_UpdateScrollContainers(true, (Clay_Vector2){0, _appState.scrollDelta}, 0.016f);
|
||||
}
|
||||
|
||||
|
|
|
|||
114
renderers/ncurses/README.md
Normal file
114
renderers/ncurses/README.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# Clay Ncurses Renderer
|
||||
|
||||
This directory contains a backend renderer for [Clay](https://github.com/nicbarker/clay) that targets the terminal using the `ncurses` library. It allows you to build text-based user interfaces (TUI) using the same Clay layout engine used for graphical applications.
|
||||
|
||||
## Features
|
||||
|
||||
- **Responsive Layouts in the Terminal**: Use flex-box like layout rules to organize text and panels in a terminal window.
|
||||
- **Color Support**:
|
||||
- Automatically matches Clay's `RGB` colors to the nearest available terminal color.
|
||||
- Supports **256-color** terminals (xterm-256color) for richer palettes.
|
||||
- Graceful fallback to standard 8 ANSI colors for older terminals.
|
||||
- **UTF-8 Support**: Correctly measures and renders multibyte characters (assuming the terminal is configured for UTF-8).
|
||||
- **Primitives Supported**:
|
||||
- `Rectangle`: Renders as solid blocks of color.
|
||||
- `Text`: Renders colored text.
|
||||
- `Border`: Renders lines using ACS (Alternate Character Set) box-drawing characters, supporting rounded corners and different line styles where possible.
|
||||
- `Scissor/Clipping`: Fully supports nested clipping rectangles (e.g., for scroll containers).
|
||||
- **Input Handling**:
|
||||
- sets up standard ncurses input modes (cbreak, noecho, keypad).
|
||||
- Enables mouse event reporting.
|
||||
|
||||
## Usage
|
||||
|
||||
To use the ncurses renderer in your Clay application:
|
||||
|
||||
### 1. Include the Renderer
|
||||
|
||||
```c
|
||||
#define CLAY_IMPLEMENTATION
|
||||
#include "clay.h"
|
||||
#include "renderers/ncurses/clay_renderer_ncurses.c"
|
||||
```
|
||||
|
||||
### 2. Initialization
|
||||
|
||||
Initialize the renderer before your main loop. This sets up the terminal screen, colors, and input modes.
|
||||
|
||||
```c
|
||||
Clay_Initialize(arena, (Clay_Dimensions){0,0}, (Clay_ErrorHandler){NULL});
|
||||
Clay_SetMeasureTextFunction(Clay_Ncurses_MeasureText, NULL);
|
||||
Clay_Ncurses_Initialize();
|
||||
```
|
||||
|
||||
### 3. Rendering Loop
|
||||
|
||||
In your main loop, update the layout dimensions based on the terminal size, run the layout, and then pass the render commands to the ncurses renderer.
|
||||
|
||||
```c
|
||||
while (!shouldQuit) {
|
||||
// 1. Get current terminal size
|
||||
Clay_Dimensions dims = Clay_Ncurses_GetLayoutDimensions();
|
||||
Clay_SetLayoutDimensions(dims);
|
||||
|
||||
// 2. Handle Input
|
||||
int key = Clay_Ncurses_ProcessInput(stdscr);
|
||||
if (key == 'q') break;
|
||||
|
||||
// 3. Define Layout
|
||||
Clay_BeginLayout();
|
||||
|
||||
// Example: Clickable Element
|
||||
CLAY(CLAY_ID("Clickable"), {0}) {
|
||||
Clay_Ncurses_OnClick(MyCallback, myData);
|
||||
CLAY_TEXT(CLAY_STRING("Click Me"), CLAY_TEXT_CONFIG({0}));
|
||||
}
|
||||
|
||||
Clay_RenderCommandArray commands = Clay_EndLayout();
|
||||
|
||||
// 4. Render
|
||||
Clay_Ncurses_Render(commands);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Input & Interaction
|
||||
|
||||
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.
|
||||
|
||||
### 5. Cleanup
|
||||
|
||||
Restore the terminal to its normal state before exiting.
|
||||
|
||||
```c
|
||||
Clay_Ncurses_Terminate();
|
||||
```
|
||||
|
||||
## Compilation
|
||||
|
||||
You must link against the `ncurses` (and potentially `tinfo`) library.
|
||||
|
||||
```bash
|
||||
gcc main.c -lncurses -o my_app
|
||||
```
|
||||
|
||||
On some systems attempting to use wide characters/UTF-8 might require linking `ncursesw` instead:
|
||||
|
||||
```bash
|
||||
gcc main.c -lncursesw -o my_app
|
||||
```
|
||||
|
||||
## How it Works
|
||||
|
||||
The renderer maps Clay's floating-point coordinate system to the integer grid of the terminal.
|
||||
- **Cell Size**: It assumes a logical "pixel" size for each character cell (defaults to 8x16 internally) to map Clay's high-precision layout to character columns and rows.
|
||||
- **Double Buffering**: It uses ncurses' standard buffering mechanisms (`refresh()`) to prevent flickering during updates.
|
||||
- **Clipping**: It uses a software scissor stack to determine visibility, as terminals do not natively support arbitrary clipping regions for drawing commands.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Images**: Rendering images is not currently supported.
|
||||
- **Fonts**: Text size is fixed to the terminal's cell size. `fontSize` configs are ignored for layout measurement, though they affect the logical ID generation.
|
||||
- **Pixel Precision**: Since the output is quantized to character cells, fine-grained pixel alignment (e.g., a 1px shift) will snap to the nearest cell boundary.
|
||||
|
|
@ -425,7 +425,10 @@ void Clay_Ncurses_Initialize() {
|
|||
keypad(stdscr, TRUE);
|
||||
curs_set(0);
|
||||
|
||||
mousemask(ALL_MOUSE_EVENTS | 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(BUTTON1_PRESSED | BUTTON1_RELEASED | REPORT_MOUSE_POSITION, NULL);
|
||||
|
||||
start_color();
|
||||
use_default_colors();
|
||||
|
|
@ -613,3 +616,58 @@ static int Clay_Ncurses_MeasureStringWidth(Clay_StringSlice text) {
|
|||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Handles Ncurses input and updates Clay's internal pointer state.
|
||||
* Use this instead of standard getch() in your main loop to enable mouse interaction.
|
||||
*
|
||||
* @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;
|
||||
|
||||
// Handle Mouse
|
||||
if (key == KEY_MOUSE) {
|
||||
MEVENT event;
|
||||
if (getmouse(&event) == OK) {
|
||||
// Convert Cell Coordinates -> Clay Logical Coordinates
|
||||
Clay_Vector2 mousePos = {
|
||||
(float)event.x * CLAY_NCURSES_CELL_WIDTH,
|
||||
(float)event.y * CLAY_NCURSES_CELL_HEIGHT
|
||||
};
|
||||
|
||||
// Persistent state to handle drag/move events where button state might be absent in the event mask
|
||||
static bool _isMouseDown = false;
|
||||
|
||||
if (event.bstate & (BUTTON1_PRESSED | BUTTON1_DOUBLE_CLICKED | BUTTON1_TRIPLE_CLICKED)) {
|
||||
_isMouseDown = true;
|
||||
}
|
||||
|
||||
if (event.bstate & BUTTON1_RELEASED) {
|
||||
_isMouseDown = false;
|
||||
}
|
||||
|
||||
// Update Clay State
|
||||
Clay_SetPointerState(mousePos, _isMouseDown);
|
||||
}
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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`.
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue