mirror of
https://github.com/nicbarker/clay.git
synced 2026-02-06 12:48:49 +00:00
Significantly enhances the Ncurses renderer capabilities and updates the example application.
Renderer Changes:
- Unicode Support:
- Implemented automatic UTF-8 locale detection and initialization.
- Switched to wide-character handling (`wchar_t`, `mvaddnwstr`) for correct rendering of multi-byte characters (e.g., Emojis).
- Used `wcwidth` for accurate string width measurement.
- Color Support:
- Upgraded from 3-bit (8 colors) to 256-color support (xterm-256color).
- Added `Clay_Ncurses_MatchColor` to map arbitrary RGB values to the nearest color in the standard 6x6x6 color cube.
- Added capability detection to fallback gracefully on simpler terminals.
- Visual Fidelity:
- Implemented background color inheritance (`Clay_Ncurses_GetBackgroundAt`) to simulate transparency.
- Text and borders now render on top of existing background colors instead of resetting to the terminal default.
- Build & POSIX:
- Added `_XOPEN_SOURCE_EXTENDED` and `_XOPEN_SOURCE=700` definitions for standard compliance.
Example Application (clay-ncurses-example):
- Theme:
- Updated to a modern dark theme (Uniform `{20, 20, 20}` background).
- Switched to saturated/bright foreground colors for better contrast.
- Fixes:
- Replaced obsolete `usleep` with POSIX-compliant `nanosleep`.
- Build:
- Updated CMakeLists.txt to enforce linking against `ncursesw` (wide version).
Verified with `clay-ncurses-example` on Linux (xterm-256color).
267 lines
9.8 KiB
C
267 lines
9.8 KiB
C
#define CLAY_IMPLEMENTATION
|
|
#include "../../clay.h"
|
|
#include "../../renderers/ncurses/clay_renderer_ncurses.c"
|
|
#include <time.h> // for nanosleep
|
|
|
|
#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 = {20, 20, 20, 255}, // Uniform Dark BG
|
|
.border = { .color = {100, 100, 100, 255}, .width = { .right = 2 } } // Lighter Grey Border
|
|
}) {
|
|
CLAY_TEXT(CLAY_STRING("SIDEBAR"), CLAY_TEXT_CONFIG({
|
|
.textColor = {255, 255, 0, 255} // Bright Yellow
|
|
}));
|
|
|
|
CLAY(CLAY_ID("SidebarItem1"), {
|
|
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(32) } },
|
|
.backgroundColor = {20, 20, 20, 255} // Uniform BG
|
|
}) {
|
|
CLAY_TEXT(CLAY_STRING(" > Item 1 (Hello 🌍)"), CLAY_TEXT_CONFIG({ .textColor = {0, 255, 255, 255} })); // Cyan
|
|
}
|
|
|
|
CLAY(CLAY_ID("SidebarItem2"), {
|
|
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(32) } },
|
|
.backgroundColor = {20, 20, 20, 255} // Uniform BG
|
|
}) {
|
|
CLAY_TEXT(CLAY_STRING(" > Item 2"), CLAY_TEXT_CONFIG({ .textColor = {255, 255, 255, 255} })); // White
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = {20, 20, 20, 255}, // Uniform BG
|
|
.cornerRadius = {8}, // Rounded corners (will render as square in TUI usually unless ACS handled)
|
|
.border = { .color = {80, 80, 80, 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 = {0, 255, 0, 255} })); // Bright Green
|
|
CLAY_TEXT(CLAY_STRING("[ Comment ]"), CLAY_TEXT_CONFIG({ .textColor = {0, 100, 255, 255} })); // Bright Blue
|
|
CLAY_TEXT(CLAY_STRING("[ Share ]"), CLAY_TEXT_CONFIG({ .textColor = {255, 0, 0, 255} })); // Bright Red
|
|
}
|
|
}
|
|
}
|
|
|
|
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 = {20, 20, 20, 255} // Uniform BG
|
|
}) {
|
|
// 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 = {20, 20, 20, 255}, // Uniform BG
|
|
.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 = {20, 20, 20, 255} // Uniform BG
|
|
}) {
|
|
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 = {20, 20, 20, 255} // Uniform BG
|
|
}) {
|
|
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);
|
|
|
|
struct timespec ts = { .tv_sec = 0, .tv_nsec = 32000 * 1000 };
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
|
|
Clay_Ncurses_Terminate();
|
|
free(arena.memory);
|
|
return 0;
|
|
}
|