mirror of
https://github.com/nicbarker/clay.git
synced 2026-02-06 12:48:49 +00:00
feat(ncurses): optimize rendering, fix memory leaks, and add rounded corners
Significantly improves the stability, performance, and visual quality of the ncurses renderer.
**Renderer Improvements (`clay_renderer_ncurses.c`):**
* **Flicker Reduction**:
* Removed `erase()` call at the start of the frame to enable differential rendering.
* Implemented "Dirty Check" optimizations for Rectangles, Borders, and Text. The renderer now reads the existing screen content (using `mvinch`, `mvin_wch`, `mvin_wchnstr`) and only issues draw commands if the content or color differs.
* Hardened Rectangle dirty check to mask out volatile attributes (comparing only `A_CHARTEXT | A_COLOR`), preventing false-positive redraws caused by internal terminal flags.
* **Memory Safety**:
* Fixed internal ncurses memory leaks by calling `delscreen(set_term(NULL))` in `Clay_Ncurses_Terminate` to properly free the default screen wrapper.
* **Visual Features**:
* Added support for **Rounded Corners**: Borders with `cornerRadius > 0` now render using Unicode arc characters (`╭`, `╮`, `╯`, `╰`).
* Upgraded standard borders to use full Unicode box-drawing characters.
**Example Application Updates (`ncurses-example/main.c`):**
* **Layout Stability**:
* Refactored all layout dimensions and gaps to use `CLAY_NCURSES_CELL_WIDTH` (8) and `CLAY_NCURSES_CELL_HEIGHT` (16) macros, ensuring strict grid alignment.
* Fixed vertical jitter in "Profile Icon" and text headers by enforcing exact height multiples and top-alignment, eliminating sub-pixel rounding errors during scroll.
* **New UI Elements**:
* Added a **Floating Help Modal** (toggled via 'H') to demonstrate Z-ordering and localized input handling.
* Added "Server Status" progress bars to the Sidebar to demonstrate percent-based sizing and colored rectangles.
* Added "Mixed Border" examples to the Sidebar to showcase the new rounded corner capabilities.
* Added "Black" background constant usage for cleaner code.
This commit is contained in:
parent
d4a48a07fc
commit
de3d63cf61
2 changed files with 257 additions and 113 deletions
|
|
@ -4,15 +4,22 @@
|
||||||
#include <time.h> // for nanosleep
|
#include <time.h> // for nanosleep
|
||||||
|
|
||||||
#define DEFAULT_SCROLL_DELTA 3.0f
|
#define DEFAULT_SCROLL_DELTA 3.0f
|
||||||
|
#define BLACK_BG_COLOR {20, 20, 20, 255}
|
||||||
|
|
||||||
// State for the example
|
// State for the example
|
||||||
typedef struct {
|
typedef struct {
|
||||||
bool sidebarOpen;
|
bool sidebarOpen;
|
||||||
float scrollDelta;
|
float scrollDelta;
|
||||||
|
bool showHelp;
|
||||||
bool shouldQuit;
|
bool shouldQuit;
|
||||||
} AppState;
|
} AppState;
|
||||||
|
|
||||||
static AppState appState = { .sidebarOpen = true, .scrollDelta = 0.0f, .shouldQuit = false };
|
static AppState appState = {
|
||||||
|
.sidebarOpen = true,
|
||||||
|
.scrollDelta = 0.0f,
|
||||||
|
.shouldQuit = false,
|
||||||
|
.showHelp = false
|
||||||
|
};
|
||||||
|
|
||||||
void HandleInput() {
|
void HandleInput() {
|
||||||
// Reset delta per frame
|
// Reset delta per frame
|
||||||
|
|
@ -26,6 +33,9 @@ void HandleInput() {
|
||||||
if (ch == 's' || ch == 'S') {
|
if (ch == 's' || ch == 'S') {
|
||||||
appState.sidebarOpen = !appState.sidebarOpen;
|
appState.sidebarOpen = !appState.sidebarOpen;
|
||||||
}
|
}
|
||||||
|
if (ch == 'h' || ch == 'H') {
|
||||||
|
appState.showHelp = !appState.showHelp;
|
||||||
|
}
|
||||||
if (ch == KEY_UP) {
|
if (ch == KEY_UP) {
|
||||||
appState.scrollDelta += DEFAULT_SCROLL_DELTA;
|
appState.scrollDelta += DEFAULT_SCROLL_DELTA;
|
||||||
}
|
}
|
||||||
|
|
@ -35,35 +45,149 @@ void HandleInput() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RenderProgressBar(Clay_String label, float percent, Clay_Color color) {
|
||||||
|
CLAY(CLAY_ID_LOCAL("ProgressBarWrapper"), {
|
||||||
|
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) }, .layoutDirection = CLAY_TOP_TO_BOTTOM, .childGap = CLAY_NCURSES_CELL_HEIGHT }
|
||||||
|
}) {
|
||||||
|
CLAY(CLAY_ID_LOCAL("LabelRow"), {
|
||||||
|
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) }, .layoutDirection = CLAY_LEFT_TO_RIGHT, .childGap = CLAY_NCURSES_CELL_HEIGHT, .childAlignment = {.y = CLAY_ALIGN_Y_CENTER} }
|
||||||
|
}) {
|
||||||
|
CLAY_TEXT(label, CLAY_TEXT_CONFIG({ .textColor = {200, 200, 200, 255}, .fontSize = 16 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAY(CLAY_ID_LOCAL("BarBackground"), {
|
||||||
|
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT) } },
|
||||||
|
.backgroundColor = {40, 40, 40, 255},
|
||||||
|
.cornerRadius = {1}
|
||||||
|
}) {
|
||||||
|
CLAY(CLAY_ID_LOCAL("BarFill"), {
|
||||||
|
.layout = { .sizing = { CLAY_SIZING_PERCENT(percent), CLAY_SIZING_GROW() } },
|
||||||
|
.backgroundColor = color,
|
||||||
|
.cornerRadius = {1}
|
||||||
|
}) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderServerStatus() {
|
||||||
|
CLAY(CLAY_ID("ServerStatus"), {
|
||||||
|
.layout = {
|
||||||
|
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) },
|
||||||
|
.padding = {16, 16, 16, 16},
|
||||||
|
.childGap = 16,
|
||||||
|
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||||
|
},
|
||||||
|
.backgroundColor = {25, 25, 25, 255},
|
||||||
|
.border = { .color = {60, 60, 60, 255}, .width = {2, 2, 2, 2} }
|
||||||
|
}) {
|
||||||
|
CLAY_TEXT(CLAY_STRING("SERVER STATUS"), CLAY_TEXT_CONFIG({ .textColor = {255, 255, 255, 255} }));
|
||||||
|
RenderProgressBar(CLAY_STRING("CPU"), 0.45f, (Clay_Color){0, 200, 0, 255});
|
||||||
|
RenderProgressBar(CLAY_STRING("Mem"), 0.82f, (Clay_Color){200, 150, 0, 255});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderHelpModal() {
|
||||||
|
if (!appState.showHelp) return;
|
||||||
|
|
||||||
|
CLAY(CLAY_ID("HelpModalOverlay"), {
|
||||||
|
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() }, .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER} },
|
||||||
|
.floating = { .zIndex = 100, .attachTo = CLAY_ATTACH_TO_ROOT, .pointerCaptureMode = CLAY_POINTER_CAPTURE_MODE_CAPTURE },
|
||||||
|
.backgroundColor = {0, 0, 0, 150}
|
||||||
|
}) {
|
||||||
|
CLAY(CLAY_ID("HelpModalWindow"), {
|
||||||
|
.layout = {
|
||||||
|
.sizing = { CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_WIDTH * 60), CLAY_SIZING_FIT(0) },
|
||||||
|
.padding = CLAY_PADDING_ALL(CLAY_NCURSES_CELL_HEIGHT),
|
||||||
|
.childGap = CLAY_NCURSES_CELL_WIDTH,
|
||||||
|
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||||
|
},
|
||||||
|
.backgroundColor = {30, 30, 30, 255},
|
||||||
|
.cornerRadius = {4},
|
||||||
|
.border = { .color = {255, 255, 255, 255}, .width = {2, 2, 2, 2} }
|
||||||
|
}) {
|
||||||
|
CLAY_TEXT(CLAY_STRING("Ncurses Example Help"), CLAY_TEXT_CONFIG({ .textColor = {255, 255, 255, 255} }));
|
||||||
|
|
||||||
|
CLAY(CLAY_ID("HelpLine1"), { .layout = { .sizing = {CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0)} } }) {
|
||||||
|
CLAY_TEXT(CLAY_STRING("Keys:"), CLAY_TEXT_CONFIG({ .textColor = {200, 200, 0, 255} }));
|
||||||
|
}
|
||||||
|
CLAY_TEXT(CLAY_STRING("- ARROW KEYS: Scroll Feed"), CLAY_TEXT_CONFIG({ .textColor = {200, 200, 200, 255} }));
|
||||||
|
CLAY_TEXT(CLAY_STRING("- S: Toggle Sidebar"), CLAY_TEXT_CONFIG({ .textColor = {200, 200, 200, 255} }));
|
||||||
|
CLAY_TEXT(CLAY_STRING("- H: Toggle This Help"), CLAY_TEXT_CONFIG({ .textColor = {200, 200, 200, 255} }));
|
||||||
|
CLAY_TEXT(CLAY_STRING("- Q: Quit Application"), CLAY_TEXT_CONFIG({ .textColor = {200, 200, 200, 255} }));
|
||||||
|
|
||||||
|
CLAY(CLAY_ID("HelpCloseTip"), { .layout = { .sizing = {CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0)}, .padding = {.top = 16} } }) {
|
||||||
|
CLAY_TEXT(CLAY_STRING("Press 'H' to close."), CLAY_TEXT_CONFIG({ .textColor = {100, 100, 100, 255} }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RenderSidebar() {
|
void RenderSidebar() {
|
||||||
if (!appState.sidebarOpen) return;
|
if (!appState.sidebarOpen) return;
|
||||||
|
|
||||||
CLAY(CLAY_ID("Sidebar"), {
|
CLAY(CLAY_ID("Sidebar"), {
|
||||||
.layout = {
|
.layout = {
|
||||||
.sizing = { CLAY_SIZING_FIXED(240), CLAY_SIZING_GROW() }, // 30 cells wide
|
.sizing = { CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_WIDTH * 30), CLAY_SIZING_GROW() },
|
||||||
.padding = CLAY_PADDING_ALL(16),
|
.padding = CLAY_PADDING_ALL(CLAY_NCURSES_CELL_HEIGHT),
|
||||||
.childGap = 16,
|
.childGap = CLAY_NCURSES_CELL_HEIGHT,
|
||||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||||
},
|
},
|
||||||
.backgroundColor = {20, 20, 20, 255}, // Uniform Dark BG
|
.backgroundColor = {20, 20, 20, 255},
|
||||||
.border = { .color = {100, 100, 100, 255}, .width = { .right = 2 } } // Lighter Grey Border
|
.border = { .color = {100, 100, 100, 255}, .width = { .right = 2 } } // Lighter Grey Border
|
||||||
}) {
|
}) {
|
||||||
CLAY_TEXT(CLAY_STRING("SIDEBAR"), CLAY_TEXT_CONFIG({
|
CLAY_TEXT(CLAY_STRING("SIDEBAR"), CLAY_TEXT_CONFIG({
|
||||||
.textColor = {255, 255, 0, 255} // Bright Yellow
|
.textColor = {255, 255, 0, 255} // Bright Yellow
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
RenderServerStatus();
|
||||||
|
|
||||||
CLAY(CLAY_ID("SidebarItem1"), {
|
CLAY(CLAY_ID("SidebarItem1"), {
|
||||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(32) } },
|
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 2) } },
|
||||||
.backgroundColor = {20, 20, 20, 255} // Uniform BG
|
.backgroundColor = BLACK_BG_COLOR
|
||||||
}) {
|
}) {
|
||||||
CLAY_TEXT(CLAY_STRING(" > Item 1 (Hello 🌍)"), CLAY_TEXT_CONFIG({ .textColor = {0, 255, 255, 255} })); // Cyan
|
CLAY_TEXT(CLAY_STRING(" > Item 1 🌍"), CLAY_TEXT_CONFIG({ .textColor = {0, 255, 255, 255} }));
|
||||||
}
|
}
|
||||||
|
|
||||||
CLAY(CLAY_ID("SidebarItem2"), {
|
CLAY(CLAY_ID("SidebarItem2"), {
|
||||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(32) } },
|
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 2) } },
|
||||||
.backgroundColor = {20, 20, 20, 255} // Uniform BG
|
.backgroundColor = BLACK_BG_COLOR
|
||||||
}) {
|
}) {
|
||||||
CLAY_TEXT(CLAY_STRING(" > Item 2"), CLAY_TEXT_CONFIG({ .textColor = {255, 255, 255, 255} })); // White
|
CLAY_TEXT(CLAY_STRING(" > Item 2 🌐"), CLAY_TEXT_CONFIG({ .textColor = {255, 255, 255, 255} }));
|
||||||
|
}
|
||||||
|
CLAY(CLAY_ID("SidebarItemMixed1"), {
|
||||||
|
.layout = {
|
||||||
|
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 3) },
|
||||||
|
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER }
|
||||||
|
},
|
||||||
|
.backgroundColor = {20, 20, 20, 255},
|
||||||
|
.cornerRadius = { .topLeft = 8 },
|
||||||
|
.border = { .color = {255, 100, 100, 255}, .width = {2, 2, 2, 2} }
|
||||||
|
}) {
|
||||||
|
CLAY_TEXT(CLAY_STRING(" > TL Round"), CLAY_TEXT_CONFIG({ .textColor = {255, 100, 100, 255} }));
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAY(CLAY_ID("SidebarItemMixed2"), {
|
||||||
|
.layout = {
|
||||||
|
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 3) },
|
||||||
|
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER }
|
||||||
|
},
|
||||||
|
.backgroundColor = {20, 20, 20, 255},
|
||||||
|
.cornerRadius = { .topLeft = 8, .bottomRight = 8 },
|
||||||
|
.border = { .color = {100, 255, 100, 255}, .width = {2, 2, 2, 2} }
|
||||||
|
}) {
|
||||||
|
CLAY_TEXT(CLAY_STRING(" > Diagonal"), CLAY_TEXT_CONFIG({ .textColor = {100, 255, 100, 255} }));
|
||||||
|
}
|
||||||
|
|
||||||
|
CLAY(CLAY_ID("SidebarItemMixed3"), {
|
||||||
|
.layout = {
|
||||||
|
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 3) },
|
||||||
|
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER }
|
||||||
|
},
|
||||||
|
.backgroundColor = {20, 20, 20, 255},
|
||||||
|
.cornerRadius = { .topLeft = 8, .topRight = 8 },
|
||||||
|
.border = { .color = {100, 100, 255, 255}, .width = {2, 2, 2, 2} }
|
||||||
|
}) {
|
||||||
|
CLAY_TEXT(CLAY_STRING(" > Top Round"), CLAY_TEXT_CONFIG({ .textColor = {100, 100, 255, 255} }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -101,33 +225,33 @@ void RenderPost(int index) {
|
||||||
CLAY(CLAY_IDI("Post", index), {
|
CLAY(CLAY_IDI("Post", index), {
|
||||||
.layout = {
|
.layout = {
|
||||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) },
|
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) },
|
||||||
.padding = CLAY_PADDING_ALL(16),
|
.padding = CLAY_PADDING_ALL(CLAY_NCURSES_CELL_HEIGHT),
|
||||||
.childGap = 8,
|
.childGap = CLAY_NCURSES_CELL_HEIGHT,
|
||||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||||
},
|
},
|
||||||
.backgroundColor = {20, 20, 20, 255}, // Uniform BG
|
.backgroundColor = BLACK_BG_COLOR,
|
||||||
.cornerRadius = {8}, // Rounded corners (will render as square in TUI usually unless ACS handled)
|
.cornerRadius = {1},
|
||||||
.border = { .color = {80, 80, 80, 255}, .width = { .left = 1, .right = 1, .top = 1, .bottom = 1 } }
|
.border = { .color = {80, 80, 80, 255}, .width = {2, 2, 2, 2} }
|
||||||
}) {
|
}) {
|
||||||
// Post Header: Avatar + Name + Time
|
// Post Header: Avatar + Name + Time
|
||||||
CLAY(CLAY_IDI("PostHeader", index), {
|
CLAY(CLAY_IDI("PostHeader", index), {
|
||||||
.layout = {
|
.layout = {
|
||||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) },
|
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) },
|
||||||
.childGap = 12,
|
.childGap = CLAY_NCURSES_CELL_WIDTH * 2,
|
||||||
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
|
.childAlignment = { .y = CLAY_ALIGN_Y_TOP },
|
||||||
.layoutDirection = CLAY_LEFT_TO_RIGHT
|
.layoutDirection = CLAY_LEFT_TO_RIGHT
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
// Avatar
|
// Avatar
|
||||||
CLAY(CLAY_IDI("Avatar", index), {
|
CLAY(CLAY_IDI("Avatar", index), {
|
||||||
.layout = { .sizing = { CLAY_SIZING_FIXED(32), CLAY_SIZING_FIXED(16) } }, // 2x1 cells approx
|
.layout = { .sizing = { CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_WIDTH * 4), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 2) } },
|
||||||
.backgroundColor = { (index * 50) % 255, (index * 80) % 255, (index * 30) % 255, 255 },
|
.backgroundColor = { (index * 50) % 255, (index * 80) % 255, (index * 30) % 255, 255 },
|
||||||
.cornerRadius = {8}
|
.cornerRadius = {1}
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
// Name & Title
|
// Name & Title
|
||||||
CLAY(CLAY_IDI("AuthorInfo", index), {
|
CLAY(CLAY_IDI("AuthorInfo", index), {
|
||||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) }, .layoutDirection = CLAY_TOP_TO_BOTTOM, .childGap = 4 }
|
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) }, .layoutDirection = CLAY_TOP_TO_BOTTOM, .childGap = 0 }
|
||||||
}) {
|
}) {
|
||||||
Clay_String name = { .length = strlen(NAMES[index % 8]), .chars = NAMES[index % 8] };
|
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_String title = { .length = strlen(TITLES[index % 8]), .chars = TITLES[index % 8] };
|
||||||
|
|
@ -138,7 +262,7 @@ void RenderPost(int index) {
|
||||||
|
|
||||||
// Post Body
|
// Post Body
|
||||||
CLAY(CLAY_IDI("PostBody", index), {
|
CLAY(CLAY_IDI("PostBody", index), {
|
||||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) }, .padding = { .top = 8, .bottom = 8 } }
|
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) }, .padding = { .top = CLAY_NCURSES_CELL_HEIGHT, .bottom = CLAY_NCURSES_CELL_HEIGHT } }
|
||||||
}) {
|
}) {
|
||||||
Clay_String lorem = { .length = strlen(LOREM[index % 5]), .chars = LOREM[index % 5] };
|
Clay_String lorem = { .length = strlen(LOREM[index % 5]), .chars = LOREM[index % 5] };
|
||||||
CLAY_TEXT(lorem, CLAY_TEXT_CONFIG({ .textColor = {200, 200, 200, 255} }));
|
CLAY_TEXT(lorem, CLAY_TEXT_CONFIG({ .textColor = {200, 200, 200, 255} }));
|
||||||
|
|
@ -146,7 +270,7 @@ void RenderPost(int index) {
|
||||||
|
|
||||||
// Post Actions
|
// Post Actions
|
||||||
CLAY(CLAY_IDI("PostActions", index), {
|
CLAY(CLAY_IDI("PostActions", index), {
|
||||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) }, .childGap = 16, .layoutDirection = CLAY_LEFT_TO_RIGHT }
|
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) }, .childGap = CLAY_NCURSES_CELL_HEIGHT, .layoutDirection = CLAY_LEFT_TO_RIGHT }
|
||||||
}) {
|
}) {
|
||||||
CLAY_TEXT(CLAY_STRING("[ Like ]"), CLAY_TEXT_CONFIG({ .textColor = {0, 255, 0, 255} })); // Bright Green
|
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("[ Comment ]"), CLAY_TEXT_CONFIG({ .textColor = {0, 100, 255, 255} })); // Bright Blue
|
||||||
|
|
@ -159,20 +283,20 @@ void RenderContent() {
|
||||||
CLAY(CLAY_ID("ContentArea"), {
|
CLAY(CLAY_ID("ContentArea"), {
|
||||||
.layout = {
|
.layout = {
|
||||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() },
|
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() },
|
||||||
.padding = CLAY_PADDING_ALL(16),
|
.padding = CLAY_PADDING_ALL(CLAY_NCURSES_CELL_HEIGHT),
|
||||||
.childGap = 16,
|
.childGap = CLAY_NCURSES_CELL_HEIGHT,
|
||||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||||
},
|
},
|
||||||
.backgroundColor = {20, 20, 20, 255} // Uniform BG
|
.backgroundColor = BLACK_BG_COLOR
|
||||||
}) {
|
}) {
|
||||||
// Sticky Header
|
// Sticky Header
|
||||||
CLAY(CLAY_ID("Header"), {
|
CLAY(CLAY_ID("Header"), {
|
||||||
.layout = {
|
.layout = {
|
||||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(48) }, // 3 cells high
|
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 3) }, // 3 cells high
|
||||||
.padding = { .left = 16, .right=16 },
|
.padding = { .left = CLAY_NCURSES_CELL_WIDTH * 2, .right=CLAY_NCURSES_CELL_WIDTH * 2 },
|
||||||
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER }
|
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER }
|
||||||
},
|
},
|
||||||
.backgroundColor = {20, 20, 20, 255}, // Uniform BG
|
.backgroundColor = BLACK_BG_COLOR,
|
||||||
.border = { .color = {0, 100, 255, 255}, .width = { .bottom = 1 } }
|
.border = { .color = {0, 100, 255, 255}, .width = { .bottom = 1 } }
|
||||||
}) {
|
}) {
|
||||||
CLAY_TEXT(CLAY_STRING("Clay Social Feed"), CLAY_TEXT_CONFIG({ .textColor = {255, 255, 255, 255} }));
|
CLAY_TEXT(CLAY_STRING("Clay Social Feed"), CLAY_TEXT_CONFIG({ .textColor = {255, 255, 255, 255} }));
|
||||||
|
|
@ -186,7 +310,7 @@ void RenderContent() {
|
||||||
.padding = { .top = 8, .bottom = 8 }
|
.padding = { .top = 8, .bottom = 8 }
|
||||||
},
|
},
|
||||||
.clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() },
|
.clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() },
|
||||||
.backgroundColor = {20, 20, 20, 255} // Uniform BG
|
.backgroundColor = BLACK_BG_COLOR
|
||||||
}) {
|
}) {
|
||||||
CLAY(CLAY_ID("FeedList"), {
|
CLAY(CLAY_ID("FeedList"), {
|
||||||
.layout = {
|
.layout = {
|
||||||
|
|
@ -213,10 +337,10 @@ void RenderContent() {
|
||||||
void RenderMainLayout() {
|
void RenderMainLayout() {
|
||||||
CLAY(CLAY_ID("Root"), {
|
CLAY(CLAY_ID("Root"), {
|
||||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() }, .layoutDirection = CLAY_LEFT_TO_RIGHT },
|
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() }, .layoutDirection = CLAY_LEFT_TO_RIGHT },
|
||||||
.backgroundColor = {20, 20, 20, 255} // Uniform BG
|
|
||||||
}) {
|
}) {
|
||||||
RenderSidebar();
|
RenderSidebar();
|
||||||
RenderContent();
|
RenderContent();
|
||||||
|
RenderHelpModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,7 +381,7 @@ int main() {
|
||||||
|
|
||||||
Clay_Ncurses_Render(commands);
|
Clay_Ncurses_Render(commands);
|
||||||
|
|
||||||
struct timespec ts = { .tv_sec = 0, .tv_nsec = 32000 * 1000 };
|
struct timespec ts = { .tv_sec = 0, .tv_nsec = 16000 * 1000 };
|
||||||
nanosleep(&ts, NULL);
|
nanosleep(&ts, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,6 @@ static struct {
|
||||||
} _colorPairCache[MAX_COLOR_PAIRS_CACHE];
|
} _colorPairCache[MAX_COLOR_PAIRS_CACHE];
|
||||||
static int _colorPairCacheSize = 0;
|
static int _colorPairCacheSize = 0;
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
|
||||||
// -- Constants
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Standard ANSI Colors mapped to easier indices if needed,
|
|
||||||
// allows extending to 256 colors easily later.
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------------------
|
||||||
// -- Forward Declarations
|
// -- Forward Declarations
|
||||||
// -------------------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------------------
|
||||||
|
|
@ -55,7 +48,6 @@ static int _colorPairCacheSize = 0;
|
||||||
static short Clay_Ncurses_GetColorId(Clay_Color color);
|
static short Clay_Ncurses_GetColorId(Clay_Color color);
|
||||||
static int Clay_Ncurses_GetColorPair(short fg, short bg);
|
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);
|
static bool Clay_Ncurses_IntersectScissor(int x, int y, int w, int h, int *outX, int *outY, int *outW, int *outH);
|
||||||
static bool Clay_Ncurses_IntersectScissor(int x, int y, int w, int h, int *outX, int *outY, int *outW, int *outH);
|
|
||||||
static void Clay_Ncurses_InitLocale(void);
|
static void Clay_Ncurses_InitLocale(void);
|
||||||
static int Clay_Ncurses_MeasureStringWidth(Clay_StringSlice text);
|
static int Clay_Ncurses_MeasureStringWidth(Clay_StringSlice text);
|
||||||
static void Clay_Ncurses_RenderText(Clay_StringSlice text, int x, int y, int renderWidth);
|
static void Clay_Ncurses_RenderText(Clay_StringSlice text, int x, int y, int renderWidth);
|
||||||
|
|
@ -77,21 +69,18 @@ void Clay_Ncurses_Initialize() {
|
||||||
|
|
||||||
Clay_Ncurses_InitLocale();
|
Clay_Ncurses_InitLocale();
|
||||||
initscr();
|
initscr();
|
||||||
cbreak(); // Line buffering disabled
|
cbreak();
|
||||||
noecho(); // Don't echo input
|
noecho();
|
||||||
keypad(stdscr, TRUE); // Enable arrow keys
|
keypad(stdscr, TRUE);
|
||||||
curs_set(0); // Hide cursor
|
curs_set(0);
|
||||||
|
|
||||||
// Enable mouse events if available
|
|
||||||
mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL);
|
mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL);
|
||||||
|
|
||||||
start_color();
|
start_color();
|
||||||
use_default_colors();
|
use_default_colors();
|
||||||
|
|
||||||
// Refresh screen dimensions
|
|
||||||
getmaxyx(stdscr, _clayNcursesScreenHeight, _clayNcursesScreenWidth);
|
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};
|
_scissorStack[0] = (Clay_BoundingBox){0, 0, (float)_clayNcursesScreenWidth * CLAY_NCURSES_CELL_WIDTH, (float)_clayNcursesScreenHeight * CLAY_NCURSES_CELL_HEIGHT};
|
||||||
_scissorStackIndex = 0;
|
_scissorStackIndex = 0;
|
||||||
|
|
||||||
|
|
@ -103,6 +92,12 @@ void Clay_Ncurses_Terminate() {
|
||||||
clear();
|
clear();
|
||||||
refresh();
|
refresh();
|
||||||
endwin();
|
endwin();
|
||||||
|
|
||||||
|
SCREEN *s = set_term(NULL);
|
||||||
|
if (s) {
|
||||||
|
delscreen(s);
|
||||||
|
}
|
||||||
|
|
||||||
_clayNcursesInitialized = false;
|
_clayNcursesInitialized = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -117,7 +112,7 @@ Clay_Dimensions Clay_Ncurses_GetLayoutDimensions() {
|
||||||
Clay_Dimensions Clay_Ncurses_MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) {
|
Clay_Dimensions Clay_Ncurses_MeasureText(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) {
|
||||||
(void)config;
|
(void)config;
|
||||||
(void)userData;
|
(void)userData;
|
||||||
// Measure string width using wcwidth
|
|
||||||
int width = Clay_Ncurses_MeasureStringWidth(text);
|
int width = Clay_Ncurses_MeasureStringWidth(text);
|
||||||
return (Clay_Dimensions) {
|
return (Clay_Dimensions) {
|
||||||
.width = (float)width * CLAY_NCURSES_CELL_WIDTH,
|
.width = (float)width * CLAY_NCURSES_CELL_WIDTH,
|
||||||
|
|
@ -128,9 +123,6 @@ Clay_Dimensions Clay_Ncurses_MeasureText(Clay_StringSlice text, Clay_TextElement
|
||||||
void Clay_Ncurses_Render(Clay_RenderCommandArray renderCommands) {
|
void Clay_Ncurses_Render(Clay_RenderCommandArray renderCommands) {
|
||||||
if (!_clayNcursesInitialized) return;
|
if (!_clayNcursesInitialized) return;
|
||||||
|
|
||||||
erase(); // Clear buffer
|
|
||||||
|
|
||||||
// Update dimensions on render start (handle resize gracefully-ish)
|
|
||||||
int newW, newH;
|
int newW, newH;
|
||||||
getmaxyx(stdscr, newH, newW);
|
getmaxyx(stdscr, newH, newW);
|
||||||
if (newW != _clayNcursesScreenWidth || newH != _clayNcursesScreenHeight) {
|
if (newW != _clayNcursesScreenWidth || newH != _clayNcursesScreenHeight) {
|
||||||
|
|
@ -138,7 +130,6 @@ void Clay_Ncurses_Render(Clay_RenderCommandArray renderCommands) {
|
||||||
_clayNcursesScreenHeight = newH;
|
_clayNcursesScreenHeight = newH;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset Scissor Stack
|
|
||||||
_scissorStack[0] = (Clay_BoundingBox){0, 0, (float)_clayNcursesScreenWidth * CLAY_NCURSES_CELL_WIDTH, (float)_clayNcursesScreenHeight * CLAY_NCURSES_CELL_HEIGHT};
|
_scissorStack[0] = (Clay_BoundingBox){0, 0, (float)_clayNcursesScreenWidth * CLAY_NCURSES_CELL_WIDTH, (float)_clayNcursesScreenHeight * CLAY_NCURSES_CELL_HEIGHT};
|
||||||
_scissorStackIndex = 0;
|
_scissorStackIndex = 0;
|
||||||
|
|
||||||
|
|
@ -148,75 +139,51 @@ void Clay_Ncurses_Render(Clay_RenderCommandArray renderCommands) {
|
||||||
|
|
||||||
switch (command->commandType) {
|
switch (command->commandType) {
|
||||||
case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
|
case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
|
||||||
// Convert to integer coords
|
|
||||||
int x = (int)(box.x / CLAY_NCURSES_CELL_WIDTH);
|
int x = (int)(box.x / CLAY_NCURSES_CELL_WIDTH);
|
||||||
int y = (int)(box.y / CLAY_NCURSES_CELL_HEIGHT);
|
int y = (int)(box.y / CLAY_NCURSES_CELL_HEIGHT);
|
||||||
int w = (int)(box.width / CLAY_NCURSES_CELL_WIDTH);
|
int w = (int)(box.width / CLAY_NCURSES_CELL_WIDTH);
|
||||||
int h = (int)(box.height / CLAY_NCURSES_CELL_HEIGHT);
|
int h = (int)(box.height / CLAY_NCURSES_CELL_HEIGHT);
|
||||||
|
|
||||||
// Apply Scissor
|
|
||||||
int dx, dy, dw, dh;
|
int dx, dy, dw, dh;
|
||||||
if (!Clay_Ncurses_IntersectScissor(x, y, w, h, &dx, &dy, &dw, &dh)) continue;
|
if (!Clay_Ncurses_IntersectScissor(x, y, w, h, &dx, &dy, &dw, &dh)) continue;
|
||||||
|
|
||||||
// Color
|
|
||||||
short fg = Clay_Ncurses_GetColorId(command->renderData.rectangle.backgroundColor);
|
short fg = Clay_Ncurses_GetColorId(command->renderData.rectangle.backgroundColor);
|
||||||
short bg = fg; // Solid block
|
short bg = fg;
|
||||||
int pair = Clay_Ncurses_GetColorPair(fg, bg);
|
int pair = Clay_Ncurses_GetColorPair(fg, bg);
|
||||||
|
|
||||||
attron(COLOR_PAIR(pair));
|
chtype targetChar = ' ' | COLOR_PAIR(pair);
|
||||||
for (int row = dy; row < dy + dh; row++) {
|
for (int row = dy; row < dy + dh; row++) {
|
||||||
for (int col = dx; col < dx + dw; col++) {
|
for (int col = dx; col < dx + dw; col++) {
|
||||||
mvaddch(row, col, ' ');
|
// Robust dirty check: Mask out attributes like A_BOLD which we don't control but might be set by terminal defaults
|
||||||
|
chtype current = mvinch(row, col);
|
||||||
|
if ((current & (A_CHARTEXT | A_COLOR)) != (targetChar & (A_CHARTEXT | A_COLOR))) {
|
||||||
|
mvaddch(row, col, targetChar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
attroff(COLOR_PAIR(pair));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CLAY_RENDER_COMMAND_TYPE_TEXT: {
|
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 x = (int)(box.x / CLAY_NCURSES_CELL_WIDTH);
|
||||||
int y = (int)(box.y / CLAY_NCURSES_CELL_HEIGHT);
|
int y = (int)(box.y / CLAY_NCURSES_CELL_HEIGHT);
|
||||||
// Text width/height
|
|
||||||
Clay_StringSlice text = command->renderData.text.stringContents;
|
Clay_StringSlice text = command->renderData.text.stringContents;
|
||||||
int textWidth = Clay_Ncurses_MeasureStringWidth(text);
|
int textWidth = Clay_Ncurses_MeasureStringWidth(text);
|
||||||
|
|
||||||
int dx, dy, dw, dh;
|
int dx, dy, dw, dh;
|
||||||
if (!Clay_Ncurses_IntersectScissor(x, y, textWidth, 1, &dx, &dy, &dw, &dh)) continue;
|
if (!Clay_Ncurses_IntersectScissor(x, y, textWidth, 1, &dx, &dy, &dw, &dh)) continue;
|
||||||
|
|
||||||
// Color (bg = -1 for transparent/default)
|
|
||||||
short fg = Clay_Ncurses_GetColorId(command->renderData.text.textColor);
|
short fg = Clay_Ncurses_GetColorId(command->renderData.text.textColor);
|
||||||
|
|
||||||
// Inherit background from screen
|
|
||||||
short bg = Clay_Ncurses_GetBackgroundAt(dx, dy);
|
short bg = Clay_Ncurses_GetBackgroundAt(dx, dy);
|
||||||
|
|
||||||
int pair = Clay_Ncurses_GetColorPair(fg, bg);
|
int pair = Clay_Ncurses_GetColorPair(fg, bg);
|
||||||
|
|
||||||
attron(COLOR_PAIR(pair));
|
attron(COLOR_PAIR(pair));
|
||||||
|
|
||||||
// Helper to handle wide char conversion and clipping
|
|
||||||
// We pass the screen coords and expected render width
|
|
||||||
// The helper will handle converting to wchar and printing the slice
|
|
||||||
// But wait, our generic helper accepts 'x' (start) and we need to skip?
|
|
||||||
// For simplicity, let's inline or call a robust helper that takes scissor into account.
|
|
||||||
// Since 'dw' is the width we *can* draw...
|
|
||||||
|
|
||||||
// We need to skip 'dx - x' columns of the string.
|
|
||||||
// This is hard with variable width chars.
|
|
||||||
// Simpler approach: Convert entire string to wchar_t, then skip/take based on wcwidth.
|
|
||||||
|
|
||||||
int skipCols = dx - x;
|
int skipCols = dx - x;
|
||||||
int takeCols = dw;
|
int takeCols = dw;
|
||||||
|
|
||||||
// Temp buffer for wide string
|
|
||||||
// Assuming reasonable max length or malloc
|
|
||||||
int maxLen = text.length + 1;
|
int maxLen = text.length + 1;
|
||||||
wchar_t *wbuf = (wchar_t *)malloc(maxLen * sizeof(wchar_t));
|
wchar_t *wbuf = (wchar_t *)malloc(maxLen * sizeof(wchar_t));
|
||||||
if (wbuf) {
|
if (wbuf) {
|
||||||
// Convert UTF-8 text to wchar
|
|
||||||
// We need a null-terminated string for mbstowcs usually,
|
|
||||||
// or use mbsnrtowcs.
|
|
||||||
// Clay text is not null term.
|
|
||||||
char *tempC = (char *)malloc(text.length + 1);
|
char *tempC = (char *)malloc(text.length + 1);
|
||||||
memcpy(tempC, text.chars, text.length);
|
memcpy(tempC, text.chars, text.length);
|
||||||
tempC[text.length] = '\0';
|
tempC[text.length] = '\0';
|
||||||
|
|
@ -225,15 +192,13 @@ void Clay_Ncurses_Render(Clay_RenderCommandArray renderCommands) {
|
||||||
free(tempC);
|
free(tempC);
|
||||||
|
|
||||||
if (wlen != -1) {
|
if (wlen != -1) {
|
||||||
// Now we have wide chars. We need to find the substring that fits [skipCols ... skipCols+takeCols]
|
|
||||||
int currentCols = 0;
|
int currentCols = 0;
|
||||||
int startIdx = 0;
|
int startIdx = 0;
|
||||||
int endIdx = 0;
|
int endIdx = 0;
|
||||||
|
|
||||||
// Find start
|
|
||||||
for (int k = 0; k < wlen; k++) {
|
for (int k = 0; k < wlen; k++) {
|
||||||
int cw = wcwidth(wbuf[k]);
|
int cw = wcwidth(wbuf[k]);
|
||||||
if (cw < 0) cw = 0; // Unprintable?
|
if (cw < 0) cw = 0;
|
||||||
|
|
||||||
if (currentCols >= skipCols) {
|
if (currentCols >= skipCols) {
|
||||||
startIdx = k;
|
startIdx = k;
|
||||||
|
|
@ -243,12 +208,7 @@ void Clay_Ncurses_Render(Clay_RenderCommandArray renderCommands) {
|
||||||
startIdx = k + 1;
|
startIdx = k + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find end
|
currentCols = 0;
|
||||||
currentCols = 0; // Relative to skipped part?
|
|
||||||
// Re-scan? No, continue?
|
|
||||||
// Better: track cumulative width.
|
|
||||||
|
|
||||||
// Restart logic:
|
|
||||||
int col = 0;
|
int col = 0;
|
||||||
int printStart = -1;
|
int printStart = -1;
|
||||||
int printLen = 0;
|
int printLen = 0;
|
||||||
|
|
@ -257,13 +217,11 @@ void Clay_Ncurses_Render(Clay_RenderCommandArray renderCommands) {
|
||||||
int cw = wcwidth(wbuf[k]);
|
int cw = wcwidth(wbuf[k]);
|
||||||
if (cw < 0) cw = 0;
|
if (cw < 0) cw = 0;
|
||||||
|
|
||||||
// If this char starts within the window
|
|
||||||
if (col >= skipCols && col < skipCols + takeCols) {
|
if (col >= skipCols && col < skipCols + takeCols) {
|
||||||
if (printStart == -1) printStart = k;
|
if (printStart == -1) printStart = k;
|
||||||
printLen++;
|
printLen++;
|
||||||
} else if (col < skipCols && col + cw > skipCols) {
|
} else if (col < skipCols && col + cw > skipCols) {
|
||||||
// Overlap start boundary (e.g. half of a wide char?)
|
// Overlap start boundary (e.g. half of a wide char?)
|
||||||
// ncurses handles this usually? Or we skip it.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
col += cw;
|
col += cw;
|
||||||
|
|
@ -271,7 +229,43 @@ void Clay_Ncurses_Render(Clay_RenderCommandArray renderCommands) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (printStart != -1) {
|
if (printStart != -1) {
|
||||||
mvaddnwstr(dy, dx, wbuf + printStart, printLen);
|
cchar_t *screenChars = (cchar_t *)malloc((printLen + 8) * sizeof(cchar_t));
|
||||||
|
if (screenChars) {
|
||||||
|
int readCount = mvin_wchnstr(dy, dx, screenChars, printLen);
|
||||||
|
if (readCount == ERR) readCount = 0;
|
||||||
|
|
||||||
|
bool dirty = false;
|
||||||
|
if (readCount < printLen) dirty = true;
|
||||||
|
else {
|
||||||
|
for (int i = 0; i < printLen; i++) {
|
||||||
|
wchar_t wch_screen[10] = {0};
|
||||||
|
attr_t attrs;
|
||||||
|
short color_pair;
|
||||||
|
if (getcchar(&screenChars[i], wch_screen, &attrs, &color_pair, NULL) == ERR) {
|
||||||
|
dirty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wch_screen[0] != wbuf[printStart + i]) {
|
||||||
|
dirty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((int)color_pair != pair) {
|
||||||
|
dirty = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(screenChars);
|
||||||
|
|
||||||
|
if (dirty) {
|
||||||
|
mvaddnwstr(dy, dx, wbuf + printStart, printLen);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback if malloc fails
|
||||||
|
mvaddnwstr(dy, dx, wbuf + printStart, printLen);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
free(wbuf);
|
free(wbuf);
|
||||||
|
|
@ -286,54 +280,81 @@ void Clay_Ncurses_Render(Clay_RenderCommandArray renderCommands) {
|
||||||
int w = (int)(box.width / CLAY_NCURSES_CELL_WIDTH);
|
int w = (int)(box.width / CLAY_NCURSES_CELL_WIDTH);
|
||||||
int h = (int)(box.height / CLAY_NCURSES_CELL_HEIGHT);
|
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;
|
int dx, dy, dw, dh;
|
||||||
if (!Clay_Ncurses_IntersectScissor(x, y, w, h, &dx, &dy, &dw, &dh)) continue;
|
if (!Clay_Ncurses_IntersectScissor(x, y, w, h, &dx, &dy, &dw, &dh)) continue;
|
||||||
|
|
||||||
short color = Clay_Ncurses_GetColorId(command->renderData.border.color);
|
short color = Clay_Ncurses_GetColorId(command->renderData.border.color);
|
||||||
|
|
||||||
// Inherit background from the corner of the border (assume uniform)
|
|
||||||
short bg = Clay_Ncurses_GetBackgroundAt(dx, dy);
|
short bg = Clay_Ncurses_GetBackgroundAt(dx, dy);
|
||||||
int pair = Clay_Ncurses_GetColorPair(color, bg);
|
int pair = Clay_Ncurses_GetColorPair(color, bg);
|
||||||
|
|
||||||
attron(COLOR_PAIR(pair));
|
attron(COLOR_PAIR(pair));
|
||||||
|
|
||||||
// Naive drawing (does not strictly respect scissor for PARTIAL borders, only fully skipped ones if outside)
|
cchar_t wc;
|
||||||
// Truly correct way handles each line.
|
wchar_t wstr[2];
|
||||||
|
|
||||||
// Top
|
// Top
|
||||||
if (y >= dy && y < dy + dh) {
|
if (y >= dy && y < dy + dh) {
|
||||||
int sx = x + 1, sw = w - 2;
|
int sx = x + 1, sw = w - 2;
|
||||||
// Intersect line with scissor X
|
|
||||||
int lx = (sx > dx) ? sx : dx;
|
int lx = (sx > dx) ? sx : dx;
|
||||||
int rx = (sx + sw < dx + dw) ? (sx + sw) : (dx + dw);
|
int rx = (sx + sw < dx + dw) ? (sx + sw) : (dx + dw);
|
||||||
if (lx < rx) mvhline(y, lx, ACS_HLINE, rx - lx);
|
mbstowcs(wstr, "─", 2);
|
||||||
|
for (int i = lx; i < rx; i++) {
|
||||||
|
mvin_wch(y, i, &wc);
|
||||||
|
if (wc.chars[0] != wstr[0]) mvprintw(y, i, "─"); // Only print if different
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Bottom
|
// Bottom
|
||||||
if (y + h - 1 >= dy && y + h - 1 < dy + dh) {
|
if (y + h - 1 >= dy && y + h - 1 < dy + dh) {
|
||||||
int sx = x + 1, sw = w - 2;
|
int sx = x + 1, sw = w - 2;
|
||||||
int lx = (sx > dx) ? sx : dx;
|
int lx = (sx > dx) ? sx : dx;
|
||||||
int rx = (sx + sw < dx + dw) ? (sx + sw) : (dx + dw);
|
int rx = (sx + sw < dx + dw) ? (sx + sw) : (dx + dw);
|
||||||
if (lx < rx) mvhline(y + h - 1, lx, ACS_HLINE, rx - lx);
|
mbstowcs(wstr, "─", 2);
|
||||||
|
for (int i = lx; i < rx; i++) {
|
||||||
|
mvin_wch(y + h - 1, i, &wc);
|
||||||
|
if (wc.chars[0] != wstr[0]) mvprintw(y + h - 1, i, "─");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Left
|
// Left
|
||||||
if (x >= dx && x < dx + dw) {
|
if (x >= dx && x < dx + dw) {
|
||||||
int sy = y + 1, sh = h - 2;
|
int sy = y + 1, sh = h - 2;
|
||||||
int ty = (sy > dy) ? sy : dy;
|
int ty = (sy > dy) ? sy : dy;
|
||||||
int by = (sy + sh < dy + dh) ? (sy + sh) : (dy + dh);
|
int by = (sy + sh < dy + dh) ? (sy + sh) : (dy + dh);
|
||||||
if (ty < by) mvvline(ty, x, ACS_VLINE, by - ty);
|
mbstowcs(wstr, "│", 2);
|
||||||
|
for (int i = ty; i < by; i++) {
|
||||||
|
mvin_wch(i, x, &wc);
|
||||||
|
if (wc.chars[0] != wstr[0]) mvprintw(i, x, "│");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Right
|
// Right
|
||||||
if (x + w - 1 >= dx && x + w - 1 < dx + dw) {
|
if (x + w - 1 >= dx && x + w - 1 < dx + dw) {
|
||||||
int sy = y + 1, sh = h - 2;
|
int sy = y + 1, sh = h - 2;
|
||||||
int ty = (sy > dy) ? sy : dy;
|
int ty = (sy > dy) ? sy : dy;
|
||||||
int by = (sy + sh < dy + dh) ? (sy + sh) : (dy + dh);
|
int by = (sy + sh < dy + dh) ? (sy + sh) : (dy + dh);
|
||||||
if (ty < by) mvvline(ty, x + w - 1, ACS_VLINE, by - ty);
|
mbstowcs(wstr, "│", 2);
|
||||||
|
for (int i = ty; i < by; i++) {
|
||||||
|
mvin_wch(i, x + w - 1, &wc);
|
||||||
|
if (wc.chars[0] != wstr[0]) mvprintw(i, x + w - 1, "│");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corners
|
||||||
|
if (x >= dx && x < dx + dw && y >= dy && y < dy + dh) {
|
||||||
|
if (command->renderData.border.cornerRadius.topLeft > 0) mvprintw(y, x, "╭");
|
||||||
|
else mvprintw(y, x, "┌");
|
||||||
|
}
|
||||||
|
if (x + w - 1 >= dx && x + w - 1 < dx + dw && y >= dy && y < dy + dh) {
|
||||||
|
if (command->renderData.border.cornerRadius.topRight > 0) mvprintw(y, x + w - 1, "╮");
|
||||||
|
else mvprintw(y, x + w - 1, "┐");
|
||||||
|
}
|
||||||
|
if (x >= dx && x < dx + dw && y + h - 1 >= dy && y + h - 1 < dy + dh) {
|
||||||
|
if (command->renderData.border.cornerRadius.bottomLeft > 0) mvprintw(y + h - 1, x, "╰");
|
||||||
|
else mvprintw(y + h - 1, x, "└");
|
||||||
|
}
|
||||||
|
if (x + w - 1 >= dx && x + w - 1 < dx + dw && y + h - 1 >= dy && y + h - 1 < dy + dh) {
|
||||||
|
if (command->renderData.border.cornerRadius.bottomRight > 0) mvprintw(y + h - 1, x + w - 1, "╯"); //
|
||||||
|
else mvprintw(y + h - 1, x + w - 1, "┘");
|
||||||
}
|
}
|
||||||
// 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));
|
attroff(COLOR_PAIR(pair));
|
||||||
break;
|
break;
|
||||||
|
|
@ -437,7 +458,6 @@ static short Clay_Ncurses_MatchColor(Clay_Color color) {
|
||||||
// 3. Grayscale (232-255)
|
// 3. Grayscale (232-255)
|
||||||
// If r~=g~=b, check if grayscale provides better match?
|
// If r~=g~=b, check if grayscale provides better match?
|
||||||
// Often cube is fine. Grayscale ramp adds fine detail for darks.
|
// Often cube is fine. Grayscale ramp adds fine detail for darks.
|
||||||
// For now, cube is sufficient for general UI.
|
|
||||||
|
|
||||||
return (short)cubeIndex;
|
return (short)cubeIndex;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue