mirror of
https://github.com/nicbarker/clay.git
synced 2026-02-06 12:48:49 +00:00
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:
parent
7d099ad870
commit
840606d0c1
5 changed files with 638 additions and 3 deletions
17
.gitignore
vendored
17
.gitignore
vendored
|
|
@ -1,7 +1,18 @@
|
||||||
cmake-build-debug/
|
|
||||||
cmake-build-release/
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.idea/
|
.idea/
|
||||||
node_modules/
|
node_modules/
|
||||||
*.dSYM
|
*.dSYM
|
||||||
.vs/
|
.vs/
|
||||||
|
|
||||||
|
# CMake dependencies
|
||||||
|
_deps/
|
||||||
|
|
||||||
|
# CMake build artifacts
|
||||||
|
build/
|
||||||
|
cmake-build-debug/
|
||||||
|
cmake-build-release/
|
||||||
|
CPack*
|
||||||
|
Makefile
|
||||||
|
cmake_install.cmake
|
||||||
|
CMakeCache.txt
|
||||||
|
CMakeFiles
|
||||||
|
|
|
||||||
|
|
@ -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_WIN32_GDI_EXAMPLES "Build Win32 GDI examples" OFF)
|
||||||
option(CLAY_INCLUDE_SOKOL_EXAMPLES "Build Sokol examples" OFF)
|
option(CLAY_INCLUDE_SOKOL_EXAMPLES "Build Sokol examples" OFF)
|
||||||
option(CLAY_INCLUDE_PLAYDATE_EXAMPLES "Build Playdate 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}")
|
message(STATUS "CLAY_INCLUDE_DEMOS: ${CLAY_INCLUDE_DEMOS}")
|
||||||
|
|
||||||
|
|
@ -56,6 +57,10 @@ if(WIN32) # Build only for Win or Wine
|
||||||
endif()
|
endif()
|
||||||
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_subdirectory("examples/cairo-pdf-rendering") Some issue with github actions populating cairo, disable for now
|
||||||
|
|
||||||
#add_library(${PROJECT_NAME} INTERFACE)
|
#add_library(${PROJECT_NAME} INTERFACE)
|
||||||
|
|
|
||||||
14
examples/ncurses-example/CMakeLists.txt
Normal file
14
examples/ncurses-example/CMakeLists.txt
Normal 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} ../../)
|
||||||
266
examples/ncurses-example/main.c
Normal file
266
examples/ncurses-example/main.c
Normal 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;
|
||||||
|
}
|
||||||
339
renderers/ncurses/clay_renderer_ncurses.c
Normal file
339
renderers/ncurses/clay_renderer_ncurses.c
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue