feat: Add ncurses renderer and example

- **Renderer**: Implemented `clay_renderer_ncurses.c` supporting rectangles, text, borders, and clipping using standard ncurses plotting.
- **Example**: Added `examples/ncurses-example` demonstrating a scrollable "Social Feed" UI with keyboard navigation.
- **Build**: Added `CLAY_INCLUDE_NCURSES_EXAMPLES` option to root `CMakeLists.txt` and integrated the new example.
- **CompConfig**: Updated `.gitignore` to strictly exclude `build/`, `_deps/`, and other standard CMake artifacts.
This commit is contained in:
Seintian 2025-12-28 14:01:41 +01:00
parent 7d099ad870
commit 840606d0c1
5 changed files with 638 additions and 3 deletions

17
.gitignore vendored
View file

@ -1,7 +1,18 @@
cmake-build-debug/
cmake-build-release/
.DS_Store
.idea/
node_modules/
*.dSYM
.vs/
.vs/
# CMake dependencies
_deps/
# CMake build artifacts
build/
cmake-build-debug/
cmake-build-release/
CPack*
Makefile
cmake_install.cmake
CMakeCache.txt
CMakeFiles

View file

@ -12,6 +12,7 @@ option(CLAY_INCLUDE_SDL3_EXAMPLES "Build SDL 3 examples" OFF)
option(CLAY_INCLUDE_WIN32_GDI_EXAMPLES "Build Win32 GDI examples" OFF)
option(CLAY_INCLUDE_SOKOL_EXAMPLES "Build Sokol examples" OFF)
option(CLAY_INCLUDE_PLAYDATE_EXAMPLES "Build Playdate examples" OFF)
option(CLAY_INCLUDE_NCURSES_EXAMPLES "Build Ncurses examples" ON)
message(STATUS "CLAY_INCLUDE_DEMOS: ${CLAY_INCLUDE_DEMOS}")
@ -56,6 +57,10 @@ if(WIN32) # Build only for Win or Wine
endif()
endif()
if(CLAY_INCLUDE_ALL_EXAMPLES OR CLAY_INCLUDE_NCURSES_EXAMPLES)
add_subdirectory("examples/ncurses-example")
endif()
# add_subdirectory("examples/cairo-pdf-rendering") Some issue with github actions populating cairo, disable for now
#add_library(${PROJECT_NAME} INTERFACE)

View file

@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.27)
project(clay-ncurses-example C)
find_package(Curses REQUIRED)
add_executable(clay-ncurses-example main.c)
target_link_libraries(clay-ncurses-example PRIVATE ${CURSES_LIBRARIES})
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} ../../)

View file

@ -0,0 +1,266 @@
#define CLAY_IMPLEMENTATION
#include "../../clay.h"
#include "../../renderers/ncurses/clay_renderer_ncurses.c"
#include <unistd.h> // for usleep
#define DEFAULT_SCROLL_DELTA 3.0f
// State for the example
typedef struct {
bool sidebarOpen;
float scrollDelta;
bool shouldQuit;
} AppState;
static AppState appState = { .sidebarOpen = true, .scrollDelta = 0.0f, .shouldQuit = false };
void HandleInput() {
// Reset delta per frame
appState.scrollDelta = 0.0f;
int ch;
while ((ch = getch()) != ERR) {
if (ch == 'q' || ch == 'Q') {
appState.shouldQuit = true;
}
if (ch == 's' || ch == 'S') {
appState.sidebarOpen = !appState.sidebarOpen;
}
if (ch == KEY_UP) {
appState.scrollDelta += DEFAULT_SCROLL_DELTA;
}
if (ch == KEY_DOWN) {
appState.scrollDelta -= DEFAULT_SCROLL_DELTA;
}
}
}
void RenderSidebar() {
if (!appState.sidebarOpen) return;
CLAY(CLAY_ID("Sidebar"), {
.layout = {
.sizing = { CLAY_SIZING_FIXED(240), CLAY_SIZING_GROW() }, // 30 cells wide
.padding = CLAY_PADDING_ALL(16),
.childGap = 16,
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
.backgroundColor = {30, 30, 30, 255}, // Dark Gray
.border = { .color = {255, 255, 255, 255}, .width = { .right = 2 } } // White Border Right
}) {
CLAY_TEXT(CLAY_STRING("SIDEBAR"), CLAY_TEXT_CONFIG({
.textColor = {255, 255, 0, 255}
}));
CLAY(CLAY_ID("SidebarItem1"), {
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(32) } },
.backgroundColor = {60, 60, 60, 255}
}) {
CLAY_TEXT(CLAY_STRING(" > Item 1"), CLAY_TEXT_CONFIG({ .textColor = {255, 255, 255, 255} }));
}
CLAY(CLAY_ID("SidebarItem2"), {
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(32) } },
.backgroundColor = {60, 60, 60, 255}
}) {
CLAY_TEXT(CLAY_STRING(" > Item 2"), CLAY_TEXT_CONFIG({ .textColor = {255, 255, 255, 255} }));
}
}
}
// Helpers for "Realistic" Data
const char* NAMES[] = {
"Alice",
"Bob",
"Charlie",
"Diana",
"Ethan",
"Fiona",
"George",
"Hannah"
};
const char* TITLES[] = {
"Just released a new library!",
"Thoughts on C programming?",
"Check out this cool algorithm",
"Why I love Ncurses",
"Clay UI is pretty flexible",
"Debugging segfaults all day...",
"Coffee break time ☕",
"Anyone going to the conf?"
};
const char* LOREM[] = {
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
"Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris.",
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum.",
"Excepteur sint occaecat cupidatat non proident, sunt in culpa."
};
void RenderPost(int index) {
CLAY(CLAY_IDI("Post", index), {
.layout = {
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) },
.padding = CLAY_PADDING_ALL(16),
.childGap = 8,
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
.backgroundColor = {25, 25, 25, 255},
.cornerRadius = {8}, // Rounded corners (will render as square in TUI usually unless ACS handled)
.border = { .color = {60, 60, 60, 255}, .width = { .left = 1, .right = 1, .top = 1, .bottom = 1 } }
}) {
// Post Header: Avatar + Name + Time
CLAY(CLAY_IDI("PostHeader", index), {
.layout = {
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) },
.childGap = 12,
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
.layoutDirection = CLAY_LEFT_TO_RIGHT
}
}) {
// Avatar
CLAY(CLAY_IDI("Avatar", index), {
.layout = { .sizing = { CLAY_SIZING_FIXED(32), CLAY_SIZING_FIXED(16) } }, // 2x1 cells approx
.backgroundColor = { (index * 50) % 255, (index * 80) % 255, (index * 30) % 255, 255 },
.cornerRadius = {8}
}) {}
// Name & Title
CLAY(CLAY_IDI("AuthorInfo", index), {
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) }, .layoutDirection = CLAY_TOP_TO_BOTTOM, .childGap = 4 }
}) {
Clay_String name = { .length = strlen(NAMES[index % 8]), .chars = NAMES[index % 8] };
Clay_String title = { .length = strlen(TITLES[index % 8]), .chars = TITLES[index % 8] };
CLAY_TEXT(name, CLAY_TEXT_CONFIG({ .textColor = {255, 255, 255, 255} }));
CLAY_TEXT(title, CLAY_TEXT_CONFIG({ .textColor = {150, 150, 150, 255} }));
}
}
// Post Body
CLAY(CLAY_IDI("PostBody", index), {
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) }, .padding = { .top = 8, .bottom = 8 } }
}) {
Clay_String lorem = { .length = strlen(LOREM[index % 5]), .chars = LOREM[index % 5] };
CLAY_TEXT(lorem, CLAY_TEXT_CONFIG({ .textColor = {200, 200, 200, 255} }));
}
// Post Actions
CLAY(CLAY_IDI("PostActions", index), {
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) }, .childGap = 16, .layoutDirection = CLAY_LEFT_TO_RIGHT }
}) {
CLAY_TEXT(CLAY_STRING("[ Like ]"), CLAY_TEXT_CONFIG({ .textColor = {100, 200, 100, 255} }));
CLAY_TEXT(CLAY_STRING("[ Comment ]"), CLAY_TEXT_CONFIG({ .textColor = {100, 150, 255, 255} }));
CLAY_TEXT(CLAY_STRING("[ Share ]"), CLAY_TEXT_CONFIG({ .textColor = {200, 100, 100, 255} }));
}
}
}
void RenderContent() {
CLAY(CLAY_ID("ContentArea"), {
.layout = {
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() },
.padding = CLAY_PADDING_ALL(16),
.childGap = 16,
.layoutDirection = CLAY_TOP_TO_BOTTOM
},
.backgroundColor = {10, 10, 10, 255}
}) {
// Sticky Header
CLAY(CLAY_ID("Header"), {
.layout = {
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(48) }, // 3 cells high
.padding = { .left = 16, .right=16 },
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER }
},
.backgroundColor = {0, 0, 80, 255},
.border = { .color = {0, 100, 255, 255}, .width = { .bottom = 1 } }
}) {
CLAY_TEXT(CLAY_STRING("Clay Social Feed"), CLAY_TEXT_CONFIG({ .textColor = {255, 255, 255, 255} }));
}
// Scroll Viewport
// We use SIZING_GROW for height so it fills the remaining screen space.
CLAY(CLAY_ID("Viewport"), {
.layout = {
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() },
.padding = { .top = 8, .bottom = 8 }
},
.clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() },
.backgroundColor = {15, 15, 15, 255}
}) {
CLAY(CLAY_ID("FeedList"), {
.layout = {
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) }, // Fit height to content (allows it to be taller than viewport)
.childGap = 16,
.layoutDirection = CLAY_TOP_TO_BOTTOM
}
}) {
// Get first item pos if possible
Clay_ElementData item0 = Clay_GetElementData(CLAY_IDI("Post", 0));
for (int i = 0; i < 50; ++i) { // 50 Posts
RenderPost(i);
}
CLAY_TEXT(CLAY_STRING("--- End of Feed ---"), CLAY_TEXT_CONFIG({ .textColor = {140, 140, 140, 255} }));
}
}
CLAY_TEXT(CLAY_STRING("Controls: ARROW UP/DOWN to Scroll | Q to Quit | S to Toggle Sidebar"), CLAY_TEXT_CONFIG({ .textColor = {120, 120, 120, 255} }));
}
}
void RenderMainLayout() {
CLAY(CLAY_ID("Root"), {
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() }, .layoutDirection = CLAY_LEFT_TO_RIGHT },
.backgroundColor = {0, 0, 0, 255}
}) {
RenderSidebar();
RenderContent();
}
}
int main() {
uint32_t minMemory = Clay_MinMemorySize();
Clay_Arena arena = Clay_CreateArenaWithCapacityAndMemory(minMemory, malloc(minMemory));
Clay_Initialize(arena, (Clay_Dimensions){0,0}, (Clay_ErrorHandler){NULL});
Clay_SetMeasureTextFunction(Clay_Ncurses_MeasureText, NULL);
Clay_Ncurses_Initialize();
// Non-blocking input
timeout(0);
while(!appState.shouldQuit) {
HandleInput();
Clay_Dimensions dims = Clay_Ncurses_GetLayoutDimensions();
Clay_SetLayoutDimensions(dims);
// Handle Scroll Logic
Clay_ElementId viewportId = CLAY_ID("Viewport");
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);
}
Clay_BeginLayout();
RenderMainLayout();
Clay_RenderCommandArray commands = Clay_EndLayout();
Clay_Ncurses_Render(commands);
usleep(32000);
}
Clay_Ncurses_Terminate();
free(arena.memory);
return 0;
}

View file

@ -0,0 +1,339 @@
#include <ncurses.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "../../clay.h"
// -------------------------------------------------------------------------------------------------
// -- Internal State & Context
// -------------------------------------------------------------------------------------------------
#define CLAY_NCURSES_CELL_WIDTH 8.0f
#define CLAY_NCURSES_CELL_HEIGHT 16.0f
static int _clayNcursesScreenWidth = 0;
static int _clayNcursesScreenHeight = 0;
static bool _clayNcursesInitialized = false;
// Scissor / Clipping State
#define MAX_SCISSOR_STACK_DEPTH 16
static Clay_BoundingBox _scissorStack[MAX_SCISSOR_STACK_DEPTH];
static int _scissorStackIndex = 0;
// Color State
// We reserve pair 0. Pairs 1..max are dynamically allocated.
#define MAX_COLOR_PAIRS_CACHE 256
static struct {
short fg;
short bg;
int pairId;
} _colorPairCache[MAX_COLOR_PAIRS_CACHE];
static int _colorPairCacheSize = 0;
// -------------------------------------------------------------------------------------------------
// -- Constants
// -------------------------------------------------------------------------------------------------
// Standard ANSI Colors mapped to easier indices if needed,
// allows extending to 256 colors easily later.
// -------------------------------------------------------------------------------------------------
// -- Forward Declarations
// -------------------------------------------------------------------------------------------------
static short Clay_Ncurses_GetColorId(Clay_Color color);
static int Clay_Ncurses_GetColorPair(short fg, short bg);
static bool Clay_Ncurses_IntersectScissor(int x, int y, int w, int h, int *outX, int *outY, int *outW, int *outH);
// -------------------------------------------------------------------------------------------------
// -- Public API Implementation
// -------------------------------------------------------------------------------------------------
void Clay_Ncurses_Initialize() {
if (_clayNcursesInitialized) return;
initscr();
cbreak(); // Line buffering disabled
noecho(); // Don't echo input
keypad(stdscr, TRUE); // Enable arrow keys
curs_set(0); // Hide cursor
// Enable mouse events if available
mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL);
start_color();
use_default_colors();
// Refresh screen dimensions
getmaxyx(stdscr, _clayNcursesScreenHeight, _clayNcursesScreenWidth);
// Initialize Scissor Stack with full screen
_scissorStack[0] = (Clay_BoundingBox){0, 0, (float)_clayNcursesScreenWidth * CLAY_NCURSES_CELL_WIDTH, (float)_clayNcursesScreenHeight * CLAY_NCURSES_CELL_HEIGHT};
_scissorStackIndex = 0;
_clayNcursesInitialized = true;
}
void Clay_Ncurses_Terminate() {
if (_clayNcursesInitialized) {
clear();
refresh();
endwin();
_clayNcursesInitialized = false;
}
}
Clay_Dimensions Clay_Ncurses_GetLayoutDimensions() {
return (Clay_Dimensions) {
.width = (float)_clayNcursesScreenWidth * CLAY_NCURSES_CELL_WIDTH,
.height = (float)_clayNcursesScreenHeight * CLAY_NCURSES_CELL_HEIGHT
};
}
Clay_Dimensions Clay_Ncurses_MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) {
(void)config;
(void)userData;
// Simple 1-to-1 mapping
return (Clay_Dimensions) {
.width = (float)text.length * CLAY_NCURSES_CELL_WIDTH,
.height = CLAY_NCURSES_CELL_HEIGHT
};
}
void Clay_Ncurses_Render(Clay_RenderCommandArray renderCommands) {
if (!_clayNcursesInitialized) return;
erase(); // Clear buffer
// Update dimensions on render start (handle resize gracefully-ish)
int newW, newH;
getmaxyx(stdscr, newH, newW);
if (newW != _clayNcursesScreenWidth || newH != _clayNcursesScreenHeight) {
_clayNcursesScreenWidth = newW;
_clayNcursesScreenHeight = newH;
}
// Reset Scissor Stack
_scissorStack[0] = (Clay_BoundingBox){0, 0, (float)_clayNcursesScreenWidth * CLAY_NCURSES_CELL_WIDTH, (float)_clayNcursesScreenHeight * CLAY_NCURSES_CELL_HEIGHT};
_scissorStackIndex = 0;
for (int i = 0; i < renderCommands.length; i++) {
Clay_RenderCommand *command = Clay_RenderCommandArray_Get(&renderCommands, i);
Clay_BoundingBox box = command->boundingBox;
switch (command->commandType) {
case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
// Convert to integer coords
int x = (int)(box.x / CLAY_NCURSES_CELL_WIDTH);
int y = (int)(box.y / CLAY_NCURSES_CELL_HEIGHT);
int w = (int)(box.width / CLAY_NCURSES_CELL_WIDTH);
int h = (int)(box.height / CLAY_NCURSES_CELL_HEIGHT);
// Apply Scissor
int dx, dy, dw, dh;
if (!Clay_Ncurses_IntersectScissor(x, y, w, h, &dx, &dy, &dw, &dh)) continue;
// Color
short fg = Clay_Ncurses_GetColorId(command->renderData.rectangle.backgroundColor);
short bg = fg; // Solid block
int pair = Clay_Ncurses_GetColorPair(fg, bg);
attron(COLOR_PAIR(pair));
for (int row = dy; row < dy + dh; row++) {
for (int col = dx; col < dx + dw; col++) {
mvaddch(row, col, ' ');
}
}
attroff(COLOR_PAIR(pair));
break;
}
case CLAY_RENDER_COMMAND_TYPE_TEXT: {
// Text is tricky with clipping.
// We need to clip the string and the position.
int x = (int)(box.x / CLAY_NCURSES_CELL_WIDTH);
int y = (int)(box.y / CLAY_NCURSES_CELL_HEIGHT);
// Text width/height
Clay_StringSlice text = command->renderData.text.stringContents;
int w = text.length;
int h = 1;
int dx, dy, dw, dh;
if (!Clay_Ncurses_IntersectScissor(x, y, w, h, &dx, &dy, &dw, &dh)) continue;
// Color (bg = -1 for transparent/default)
short fg = Clay_Ncurses_GetColorId(command->renderData.text.textColor);
int pair = Clay_Ncurses_GetColorPair(fg, -1);
attron(COLOR_PAIR(pair));
// Calculate substring to print based on clip
// dx is the starting x on screen. x is original start.
// offset in string = dx - x
int offset = dx - x;
int len = dw;
if (offset >= 0 && offset < text.length) {
mvaddnstr(dy, dx, text.chars + offset, len);
}
attroff(COLOR_PAIR(pair));
break;
}
case CLAY_RENDER_COMMAND_TYPE_BORDER: {
int x = (int)(box.x / CLAY_NCURSES_CELL_WIDTH);
int y = (int)(box.y / CLAY_NCURSES_CELL_HEIGHT);
int w = (int)(box.width / CLAY_NCURSES_CELL_WIDTH);
int h = (int)(box.height / CLAY_NCURSES_CELL_HEIGHT);
// TODO: Robust border culling. For now, check if the whole rect intersects AT ALL
int dx, dy, dw, dh;
if (!Clay_Ncurses_IntersectScissor(x, y, w, h, &dx, &dy, &dw, &dh)) continue;
short color = Clay_Ncurses_GetColorId(command->renderData.border.color);
int pair = Clay_Ncurses_GetColorPair(color, -1);
attron(COLOR_PAIR(pair));
// Naive drawing (does not strictly respect scissor for PARTIAL borders, only fully skipped ones if outside)
// Truly correct way handles each line.
// Top
if (y >= dy && y < dy + dh) {
int sx = x + 1, sw = w - 2;
// Intersect line with scissor X
int lx = (sx > dx) ? sx : dx;
int rx = (sx + sw < dx + dw) ? (sx + sw) : (dx + dw);
if (lx < rx) mvhline(y, lx, ACS_HLINE, rx - lx);
}
// Bottom
if (y + h - 1 >= dy && y + h - 1 < dy + dh) {
int sx = x + 1, sw = w - 2;
int lx = (sx > dx) ? sx : dx;
int rx = (sx + sw < dx + dw) ? (sx + sw) : (dx + dw);
if (lx < rx) mvhline(y + h - 1, lx, ACS_HLINE, rx - lx);
}
// Left
if (x >= dx && x < dx + dw) {
int sy = y + 1, sh = h - 2;
int ty = (sy > dy) ? sy : dy;
int by = (sy + sh < dy + dh) ? (sy + sh) : (dy + dh);
if (ty < by) mvvline(ty, x, ACS_VLINE, by - ty);
}
// Right
if (x + w - 1 >= dx && x + w - 1 < dx + dw) {
int sy = y + 1, sh = h - 2;
int ty = (sy > dy) ? sy : dy;
int by = (sy + sh < dy + dh) ? (sy + sh) : (dy + dh);
if (ty < by) mvvline(ty, x + w - 1, ACS_VLINE, by - ty);
}
// Corners (simple visibility check)
if (x >= dx && x < dx + dw && y >= dy && y < dy + dh) mvaddch(y, x, ACS_ULCORNER);
if (x + w - 1 >= dx && x + w - 1 < dx + dw && y >= dy && y < dy + dh) mvaddch(y, x + w - 1, ACS_URCORNER);
if (x >= dx && x < dx + dw && y + h - 1 >= dy && y + h - 1 < dy + dh) mvaddch(y + h - 1, x, ACS_LLCORNER);
if (x + w - 1 >= dx && x + w - 1 < dx + dw && y + h - 1 >= dy && y + h - 1 < dy + dh) mvaddch(y + h - 1, x + w - 1, ACS_LRCORNER);
attroff(COLOR_PAIR(pair));
break;
}
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: {
if (_scissorStackIndex < MAX_SCISSOR_STACK_DEPTH - 1) {
Clay_BoundingBox current = _scissorStack[_scissorStackIndex];
Clay_BoundingBox next = command->boundingBox;
// Intersect next with current
float nX = (next.x > current.x) ? next.x : current.x;
float nY = (next.y > current.y) ? next.y : current.y;
float nR = ((next.x + next.width) < (current.x + current.width)) ? (next.x + next.width) : (current.x + current.width);
float nB = ((next.y + next.height) < (current.y + current.height)) ? (next.y + next.height) : (current.y + current.height);
_scissorStackIndex++;
_scissorStack[_scissorStackIndex] = (Clay_BoundingBox){ nX, nY, nR - nX, nB - nY };
}
break;
}
case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: {
if (_scissorStackIndex > 0) {
_scissorStackIndex--;
}
break;
}
default: break;
}
}
refresh();
}
// -------------------------------------------------------------------------------------------------
// -- Internal Helpers
// -------------------------------------------------------------------------------------------------
static bool Clay_Ncurses_IntersectScissor(int x, int y, int w, int h, int *outX, int *outY, int *outW, int *outH) {
Clay_BoundingBox clip = _scissorStack[_scissorStackIndex];
// Convert clip to cell coords
int cx = (int)(clip.x / CLAY_NCURSES_CELL_WIDTH);
int cy = (int)(clip.y / CLAY_NCURSES_CELL_HEIGHT);
int cw = (int)(clip.width / CLAY_NCURSES_CELL_WIDTH);
int ch = (int)(clip.height / CLAY_NCURSES_CELL_HEIGHT);
// Intersect
int ix = (x > cx) ? x : cx;
int iy = (y > cy) ? y : cy;
int right = (x + w < cx + cw) ? (x + w) : (cx + cw);
int bottom = (y + h < cy + ch) ? (y + h) : (cy + ch);
int iw = right - ix;
int ih = bottom - iy;
if (iw <= 0 || ih <= 0) return false;
*outX = ix;
*outY = iy;
*outW = iw;
*outH = ih;
return true;
}
static short Clay_Ncurses_GetColorId(Clay_Color color) {
// 3-bit Color Mapping (Simple thresholding)
int r = color.r > 128;
int g = color.g > 128;
int b = color.b > 128;
if (r && g && b) return COLOR_WHITE;
if (!r && !g && !b) return COLOR_BLACK;
if (r && g) return COLOR_YELLOW;
if (r && b) return COLOR_MAGENTA;
if (g && b) return COLOR_CYAN;
if (r) return COLOR_RED;
if (g) return COLOR_GREEN;
if (b) return COLOR_BLUE;
return COLOR_WHITE;
}
static int Clay_Ncurses_GetColorPair(short fg, short bg) {
// Check cache
for (int i = 0; i < _colorPairCacheSize; i++) {
if (_colorPairCache[i].fg == fg && _colorPairCache[i].bg == bg) {
return _colorPairCache[i].pairId;
}
}
// Create new
if (_colorPairCacheSize >= MAX_COLOR_PAIRS_CACHE) {
// Full? Just return last one or default.
// Real impl: LRU eviction.
return 0; // Default
}
int newId = _colorPairCacheSize + 1;
init_pair(newId, fg, bg);
_colorPairCache[_colorPairCacheSize].fg = fg;
_colorPairCache[_colorPairCacheSize].bg = bg;
_colorPairCache[_colorPairCacheSize].pairId = newId;
_colorPairCacheSize++;
return newId;
}