mirror of
https://github.com/nicbarker/clay.git
synced 2026-02-06 12:48:49 +00:00
[renderers/ncurses] Refactor Ncurses renderer and example with atomic functions and Doxygen docs
Major refactor of the Ncurses renderer and example application to improve code readability, maintainability, and documentation coverage.
**Ncurses Renderer (`renderers/ncurses/clay_renderer_ncurses.c`):**
* **Atomic Rendering Functions**: Decomposed the monolithic `Clay_Ncurses_Render` function into specialized handlers:
* `Clay_Ncurses_RenderRectangle`
* `Clay_Ncurses_RenderText`
* `Clay_Ncurses_RenderBorder`
* **Scissor Management**: Encapsulated scissor stack operations into `Clay_Ncurses_PushScissor` and `Clay_Ncurses_PopScissor`.
* **Visibility Logic**: Extracted visibility/clipping checks into `Clay_Ncurses_GetVisibleRect` for cleaner reuse.
* **Documentation**: Added comprehensive Doxygen documentation for file headers, internal state, constants, and all functions.
**Ncurses Example (`examples/ncurses-example/main.c`):**
* **Architecture Refactor**: Implementation split into clear "App State", "Input Processing", and "UI Components" sections.
* **UI Componentization**: Renamed and organized UI functions with a consistent `UI_` prefix (e.g., `UI_Sidebar`, `UI_FeedPost`, `UI_HelpModal`).
* **State Management**: Introduced `AppState` struct to centralize application state (sidebar visibility, scroll delta, etc.).
* **Input Handling**: Centralized input logic in `App_ProcessInput` loop.
* **Documentation**: Added full Doxygen documentation for the example source.
This refactor maintains identical runtime behavior while significantly improving the codebase's "spoken english" readability and modularity. loop.
* **Documentation**: Added full Doxygen documentation for the example source.
This refactor maintains identical runtime behavior while significantly improving the codebase's readability and modularity.
This commit is contained in:
parent
de3d63cf61
commit
97c1a797c4
2 changed files with 849 additions and 683 deletions
|
|
@ -1,67 +1,150 @@
|
|||
/**
|
||||
* @file main.c
|
||||
* @author Seintian
|
||||
* @date 2025-12-28
|
||||
* @brief Ncurses Example Application for Clay.
|
||||
*
|
||||
* Demonstrates how to use Clay with the Ncurses renderer to create a terminal-based UI.
|
||||
* Features include:
|
||||
* - A responsive layout with a sidebar and main content area.
|
||||
* - Scrollable content (feed).
|
||||
* - "Floating" modal windows (Help).
|
||||
* - Keyboard user input handling.
|
||||
* - Custom widgets (ProgressBar).
|
||||
*/
|
||||
|
||||
#define CLAY_IMPLEMENTATION
|
||||
#include "../../clay.h"
|
||||
#include "../../renderers/ncurses/clay_renderer_ncurses.c"
|
||||
#include <time.h> // for nanosleep
|
||||
|
||||
#define DEFAULT_SCROLL_DELTA 3.0f
|
||||
#define BLACK_BG_COLOR {20, 20, 20, 255}
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// -- Constants & Configuration
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
// State for the example
|
||||
/** @brief Scroll speed per key press. */
|
||||
#define DEFAULT_SCROLL_SENSITIVITY 3.0f
|
||||
|
||||
/** @brief Accent color: Green. */
|
||||
#define COLOR_ACCENT_GREEN (Clay_Color){0, 200, 0, 255}
|
||||
|
||||
/** @brief Accent color: Orange. */
|
||||
#define COLOR_ACCENT_ORANGE (Clay_Color){200, 150, 0, 255}
|
||||
|
||||
/** @brief Accent color: Red. */
|
||||
#define COLOR_ACCENT_RED (Clay_Color){255, 100, 100, 255}
|
||||
|
||||
/** @brief Accent color: Blue. */
|
||||
#define COLOR_ACCENT_BLUE (Clay_Color){100, 100, 255, 255}
|
||||
|
||||
/** @brief Standard text color: White. */
|
||||
#define COLOR_TEXT_WHITE (Clay_Color){255, 255, 255, 255}
|
||||
|
||||
/** @brief Dimmed text color: Grey. */
|
||||
#define COLOR_TEXT_DIM (Clay_Color){150, 150, 150, 255}
|
||||
|
||||
/** @brief Background color for panels. */
|
||||
#define COLOR_PANEL_BG (Clay_Color){20, 20, 20, 255}
|
||||
|
||||
/** @brief Border color for panels. */
|
||||
#define COLOR_PANEL_BORDER (Clay_Color){100, 100, 100, 255}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// -- Application State
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Global application state.
|
||||
* Stores all mutable state required for the UI logic.
|
||||
*/
|
||||
typedef struct {
|
||||
bool sidebarOpen;
|
||||
float scrollDelta;
|
||||
bool showHelp;
|
||||
bool shouldQuit;
|
||||
bool isSidebarVisible; /**< Toggles the visibility of the sidebar. */
|
||||
bool isHelpModalVisible; /**< Toggles the help overlay. */
|
||||
bool isQuitting; /**< Flag to exit the main loop. */
|
||||
float scrollDelta; /**< Accumulated scroll amount for the current frame. */
|
||||
} AppState;
|
||||
|
||||
static AppState appState = {
|
||||
.sidebarOpen = true,
|
||||
.scrollDelta = 0.0f,
|
||||
.shouldQuit = false,
|
||||
.showHelp = false
|
||||
/** @brief Static instance of application state. */
|
||||
static AppState _appState = {
|
||||
.isSidebarVisible = true,
|
||||
.isHelpModalVisible = false,
|
||||
.isQuitting = false,
|
||||
.scrollDelta = 0.0f
|
||||
};
|
||||
|
||||
void HandleInput() {
|
||||
// Reset delta per frame
|
||||
appState.scrollDelta = 0.0f;
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// -- Input Processing
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
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 == 'h' || ch == 'H') {
|
||||
appState.showHelp = !appState.showHelp;
|
||||
}
|
||||
if (ch == KEY_UP) {
|
||||
appState.scrollDelta += DEFAULT_SCROLL_DELTA;
|
||||
}
|
||||
if (ch == KEY_DOWN) {
|
||||
appState.scrollDelta -= DEFAULT_SCROLL_DELTA;
|
||||
/**
|
||||
* @brief Processes keyboard input for the current frame.
|
||||
* Updates _appState directly based on key presses.
|
||||
* Uses ncurses getch() (non-blocking if timeout is set).
|
||||
*/
|
||||
void App_ProcessInput() {
|
||||
_appState.scrollDelta = 0.0f;
|
||||
|
||||
int key;
|
||||
while ((key = getch()) != ERR) {
|
||||
switch (key) {
|
||||
case 'q':
|
||||
case 'Q':
|
||||
_appState.isQuitting = true;
|
||||
break;
|
||||
case 's':
|
||||
case 'S':
|
||||
_appState.isSidebarVisible = !_appState.isSidebarVisible;
|
||||
break;
|
||||
case 'h':
|
||||
case 'H':
|
||||
_appState.isHelpModalVisible = !_appState.isHelpModalVisible;
|
||||
break;
|
||||
case KEY_UP:
|
||||
_appState.scrollDelta += DEFAULT_SCROLL_SENSITIVITY;
|
||||
break;
|
||||
case KEY_DOWN:
|
||||
_appState.scrollDelta -= DEFAULT_SCROLL_SENSITIVITY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// -- UI Components
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Renders a progress bar widget.
|
||||
* @param label The text label displayed above the bar.
|
||||
* @param percentage The fill percentage (0.0 to 1.0).
|
||||
* @param color The color of the filled portion.
|
||||
*/
|
||||
void UI_ProgressBar(Clay_String label, float percentage, Clay_Color color) {
|
||||
CLAY(CLAY_ID_LOCAL("ProgressBar"), {
|
||||
.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(CLAY_ID_LOCAL("Label"), {
|
||||
.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"), {
|
||||
CLAY(CLAY_ID_LOCAL("Track"), {
|
||||
.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() } },
|
||||
CLAY(CLAY_ID_LOCAL("Fill"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_PERCENT(percentage), CLAY_SIZING_GROW() } },
|
||||
.backgroundColor = color,
|
||||
.cornerRadius = {1}
|
||||
}) {}
|
||||
|
|
@ -69,8 +152,11 @@ void RenderProgressBar(Clay_String label, float percent, Clay_Color color) {
|
|||
}
|
||||
}
|
||||
|
||||
void RenderServerStatus() {
|
||||
CLAY(CLAY_ID("ServerStatus"), {
|
||||
/**
|
||||
* @brief Renders the Server Status widget containing CPU and Memory usage bars.
|
||||
*/
|
||||
void UI_ServerStatusWidget() {
|
||||
CLAY(CLAY_ID("ServerStatusWidget"), {
|
||||
.layout = {
|
||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) },
|
||||
.padding = {16, 16, 16, 16},
|
||||
|
|
@ -80,14 +166,213 @@ void RenderServerStatus() {
|
|||
.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});
|
||||
CLAY_TEXT(CLAY_STRING("SERVER STATUS"), CLAY_TEXT_CONFIG({ .textColor = COLOR_TEXT_WHITE }));
|
||||
UI_ProgressBar(CLAY_STRING("CPU"), 0.45f, COLOR_ACCENT_GREEN);
|
||||
UI_ProgressBar(CLAY_STRING("Mem"), 0.82f, COLOR_ACCENT_ORANGE);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderHelpModal() {
|
||||
if (!appState.showHelp) return;
|
||||
/**
|
||||
* @brief Renders a single item used in the sidebar.
|
||||
* @param label Text to display.
|
||||
* @param textColor Color of the text.
|
||||
*/
|
||||
void UI_SidebarItem(Clay_String label, Clay_Color textColor) {
|
||||
CLAY(CLAY_ID_LOCAL("SidebarItem"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 2) } },
|
||||
.backgroundColor = COLOR_PANEL_BG
|
||||
}) {
|
||||
CLAY_TEXT(label, CLAY_TEXT_CONFIG({ .textColor = textColor }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Renders the application Sidebar.
|
||||
* conditionally rendered based on _appState.isSidebarVisible.
|
||||
*/
|
||||
void UI_Sidebar() {
|
||||
if (!_appState.isSidebarVisible) return;
|
||||
|
||||
CLAY(CLAY_ID("Sidebar"), {
|
||||
.layout = {
|
||||
.sizing = { CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_WIDTH * 30), CLAY_SIZING_GROW() },
|
||||
.padding = CLAY_PADDING_ALL(CLAY_NCURSES_CELL_HEIGHT),
|
||||
.childGap = CLAY_NCURSES_CELL_HEIGHT,
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.backgroundColor = COLOR_PANEL_BG,
|
||||
.border = { .color = COLOR_PANEL_BORDER, .width = { .right = 2 } }
|
||||
}) {
|
||||
CLAY_TEXT(CLAY_STRING("SIDEBAR"), CLAY_TEXT_CONFIG({ .textColor = {255, 255, 0, 255} }));
|
||||
|
||||
UI_ServerStatusWidget();
|
||||
|
||||
UI_SidebarItem(CLAY_STRING(" > Item 1 🌍"), (Clay_Color){0, 255, 255, 255});
|
||||
UI_SidebarItem(CLAY_STRING(" > Item 2 🌐"), COLOR_TEXT_WHITE);
|
||||
|
||||
// Mixed Style Items
|
||||
CLAY(CLAY_ID("SidebarItemMixed1"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 3) }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER } },
|
||||
.backgroundColor = COLOR_PANEL_BG,
|
||||
.cornerRadius = { .topLeft = 8 },
|
||||
.border = { .color = COLOR_ACCENT_RED, .width = {2, 2, 2, 2} }
|
||||
}) {
|
||||
CLAY_TEXT(CLAY_STRING(" > TL Round"), CLAY_TEXT_CONFIG({ .textColor = COLOR_ACCENT_RED }));
|
||||
}
|
||||
|
||||
CLAY(CLAY_ID("SidebarItemMixed2"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 3) }, .childAlignment = { .y = CLAY_ALIGN_Y_CENTER } },
|
||||
.backgroundColor = COLOR_PANEL_BG,
|
||||
.cornerRadius = { .topLeft = 8, .bottomRight = 8 },
|
||||
.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 = COLOR_PANEL_BG,
|
||||
.cornerRadius = { .topLeft = 8, .topRight = 8 },
|
||||
.border = { .color = COLOR_ACCENT_BLUE, .width = {2, 2, 2, 2} }
|
||||
}) {
|
||||
CLAY_TEXT(CLAY_STRING(" > Top Round"), CLAY_TEXT_CONFIG({ .textColor = COLOR_ACCENT_BLUE }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Data for "Realistic" Content
|
||||
|
||||
/** @brief Sample names for feed posts. */
|
||||
const char* NAMES[] = { "Alice", "Bob", "Charlie", "Diana", "Ethan", "Fiona", "George", "Hannah" };
|
||||
|
||||
/** @brief Sample titles for feed posts. */
|
||||
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?" };
|
||||
|
||||
/** @brief Sample body text for feed posts. */
|
||||
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." };
|
||||
|
||||
/**
|
||||
* @brief Renders a single social media feed post.
|
||||
* @param index The index of the post (used to generate deterministic content from sample data).
|
||||
*/
|
||||
void UI_FeedPost(int index) {
|
||||
CLAY(CLAY_IDI("FeedPost", index), {
|
||||
.layout = {
|
||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) },
|
||||
.padding = CLAY_PADDING_ALL(CLAY_NCURSES_CELL_HEIGHT),
|
||||
.childGap = CLAY_NCURSES_CELL_HEIGHT,
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.backgroundColor = COLOR_PANEL_BG,
|
||||
.cornerRadius = {1},
|
||||
.border = { .color = {80, 80, 80, 255}, .width = {2, 2, 2, 2} }
|
||||
}) {
|
||||
// Header: Avatar + Name + Time
|
||||
CLAY(CLAY_IDI("PostHeader", index), {
|
||||
.layout = {
|
||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) },
|
||||
.childGap = CLAY_NCURSES_CELL_WIDTH * 2,
|
||||
.childAlignment = { .y = CLAY_ALIGN_Y_TOP },
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT
|
||||
}
|
||||
}) {
|
||||
CLAY(CLAY_IDI("Avatar", index), {
|
||||
.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 },
|
||||
.cornerRadius = {1}
|
||||
}) {}
|
||||
|
||||
CLAY(CLAY_IDI("AuthorInfo", index), {
|
||||
.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 title = { .length = strlen(TITLES[index % 8]), .chars = TITLES[index % 8] };
|
||||
CLAY_TEXT(name, CLAY_TEXT_CONFIG({ .textColor = COLOR_TEXT_WHITE }));
|
||||
CLAY_TEXT(title, CLAY_TEXT_CONFIG({ .textColor = COLOR_TEXT_DIM }));
|
||||
}
|
||||
}
|
||||
|
||||
// Body
|
||||
CLAY(CLAY_IDI("PostBody", index), {
|
||||
.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_TEXT(lorem, CLAY_TEXT_CONFIG({ .textColor = {200, 200, 200, 255} }));
|
||||
}
|
||||
|
||||
// Actions
|
||||
CLAY(CLAY_IDI("PostActions", index), {
|
||||
.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} }));
|
||||
CLAY_TEXT(CLAY_STRING("[ Comment ]"), CLAY_TEXT_CONFIG({ .textColor = {0, 100, 255, 255} }));
|
||||
CLAY_TEXT(CLAY_STRING("[ Share ]"), CLAY_TEXT_CONFIG({ .textColor = {255, 0, 0, 255} }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Renders the main content area with the scrollable feed.
|
||||
*/
|
||||
void UI_MainContent() {
|
||||
CLAY(CLAY_ID("ContentArea"), {
|
||||
.layout = {
|
||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() },
|
||||
.padding = CLAY_PADDING_ALL(CLAY_NCURSES_CELL_HEIGHT),
|
||||
.childGap = CLAY_NCURSES_CELL_HEIGHT,
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.backgroundColor = COLOR_PANEL_BG
|
||||
}) {
|
||||
// Sticky Header
|
||||
CLAY(CLAY_ID("StickyHeader"), {
|
||||
.layout = {
|
||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 3) },
|
||||
.padding = { .left = CLAY_NCURSES_CELL_WIDTH * 2, .right=CLAY_NCURSES_CELL_WIDTH * 2 },
|
||||
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER }
|
||||
},
|
||||
.backgroundColor = COLOR_PANEL_BG,
|
||||
.border = { .color = {0, 100, 255, 255}, .width = { .bottom = 1 } }
|
||||
}) {
|
||||
CLAY_TEXT(CLAY_STRING("Clay Social Feed"), CLAY_TEXT_CONFIG({ .textColor = COLOR_TEXT_WHITE }));
|
||||
}
|
||||
|
||||
// Scrollable Viewport
|
||||
CLAY(CLAY_ID("Viewport"), {
|
||||
.layout = {
|
||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() },
|
||||
.padding = { .top = 8, .bottom = 8 }
|
||||
},
|
||||
.clip = { .vertical = true, .childOffset = Clay_GetScrollOffset() },
|
||||
.backgroundColor = COLOR_PANEL_BG
|
||||
}) {
|
||||
CLAY(CLAY_ID("FeedList"), {
|
||||
.layout = {
|
||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) },
|
||||
.childGap = 16,
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
}
|
||||
}) {
|
||||
// Determine if we need to initialize scroll position
|
||||
Clay_ElementData item0 = Clay_GetElementData(CLAY_IDI("FeedPost", 0));
|
||||
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
UI_FeedPost(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} }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Renders the Help modal overlay.
|
||||
*/
|
||||
void UI_HelpModal() {
|
||||
if (!_appState.isHelpModalVisible) return;
|
||||
|
||||
CLAY(CLAY_ID("HelpModalOverlay"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() }, .childAlignment = {CLAY_ALIGN_X_CENTER, CLAY_ALIGN_Y_CENTER} },
|
||||
|
|
@ -103,9 +388,9 @@ void RenderHelpModal() {
|
|||
},
|
||||
.backgroundColor = {30, 30, 30, 255},
|
||||
.cornerRadius = {4},
|
||||
.border = { .color = {255, 255, 255, 255}, .width = {2, 2, 2, 2} }
|
||||
.border = { .color = COLOR_TEXT_WHITE, .width = {2, 2, 2, 2} }
|
||||
}) {
|
||||
CLAY_TEXT(CLAY_STRING("Ncurses Example Help"), CLAY_TEXT_CONFIG({ .textColor = {255, 255, 255, 255} }));
|
||||
CLAY_TEXT(CLAY_STRING("Ncurses Example Help"), CLAY_TEXT_CONFIG({ .textColor = COLOR_TEXT_WHITE }));
|
||||
|
||||
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} }));
|
||||
|
|
@ -122,241 +407,42 @@ void RenderHelpModal() {
|
|||
}
|
||||
}
|
||||
|
||||
void RenderSidebar() {
|
||||
if (!appState.sidebarOpen) return;
|
||||
|
||||
CLAY(CLAY_ID("Sidebar"), {
|
||||
.layout = {
|
||||
.sizing = { CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_WIDTH * 30), CLAY_SIZING_GROW() },
|
||||
.padding = CLAY_PADDING_ALL(CLAY_NCURSES_CELL_HEIGHT),
|
||||
.childGap = CLAY_NCURSES_CELL_HEIGHT,
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.backgroundColor = {20, 20, 20, 255},
|
||||
.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
|
||||
}));
|
||||
|
||||
RenderServerStatus();
|
||||
|
||||
CLAY(CLAY_ID("SidebarItem1"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 2) } },
|
||||
.backgroundColor = BLACK_BG_COLOR
|
||||
}) {
|
||||
CLAY_TEXT(CLAY_STRING(" > Item 1 🌍"), CLAY_TEXT_CONFIG({ .textColor = {0, 255, 255, 255} }));
|
||||
}
|
||||
|
||||
CLAY(CLAY_ID("SidebarItem2"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 2) } },
|
||||
.backgroundColor = BLACK_BG_COLOR
|
||||
}) {
|
||||
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} }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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(CLAY_NCURSES_CELL_HEIGHT),
|
||||
.childGap = CLAY_NCURSES_CELL_HEIGHT,
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.backgroundColor = BLACK_BG_COLOR,
|
||||
.cornerRadius = {1},
|
||||
.border = { .color = {80, 80, 80, 255}, .width = {2, 2, 2, 2} }
|
||||
}) {
|
||||
// Post Header: Avatar + Name + Time
|
||||
CLAY(CLAY_IDI("PostHeader", index), {
|
||||
.layout = {
|
||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIT(0) },
|
||||
.childGap = CLAY_NCURSES_CELL_WIDTH * 2,
|
||||
.childAlignment = { .y = CLAY_ALIGN_Y_TOP },
|
||||
.layoutDirection = CLAY_LEFT_TO_RIGHT
|
||||
}
|
||||
}) {
|
||||
// Avatar
|
||||
CLAY(CLAY_IDI("Avatar", index), {
|
||||
.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 },
|
||||
.cornerRadius = {1}
|
||||
}) {}
|
||||
|
||||
// Name & Title
|
||||
CLAY(CLAY_IDI("AuthorInfo", index), {
|
||||
.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 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 = CLAY_NCURSES_CELL_HEIGHT, .bottom = CLAY_NCURSES_CELL_HEIGHT } }
|
||||
}) {
|
||||
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 = 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("[ 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(CLAY_NCURSES_CELL_HEIGHT),
|
||||
.childGap = CLAY_NCURSES_CELL_HEIGHT,
|
||||
.layoutDirection = CLAY_TOP_TO_BOTTOM
|
||||
},
|
||||
.backgroundColor = BLACK_BG_COLOR
|
||||
}) {
|
||||
// Sticky Header
|
||||
CLAY(CLAY_ID("Header"), {
|
||||
.layout = {
|
||||
.sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_FIXED(CLAY_NCURSES_CELL_HEIGHT * 3) }, // 3 cells high
|
||||
.padding = { .left = CLAY_NCURSES_CELL_WIDTH * 2, .right=CLAY_NCURSES_CELL_WIDTH * 2 },
|
||||
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER }
|
||||
},
|
||||
.backgroundColor = BLACK_BG_COLOR,
|
||||
.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 = BLACK_BG_COLOR
|
||||
}) {
|
||||
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() {
|
||||
/**
|
||||
* @brief Renders the root layout of the application.
|
||||
*/
|
||||
void UI_RootLayout() {
|
||||
CLAY(CLAY_ID("Root"), {
|
||||
.layout = { .sizing = { CLAY_SIZING_GROW(), CLAY_SIZING_GROW() }, .layoutDirection = CLAY_LEFT_TO_RIGHT },
|
||||
}) {
|
||||
RenderSidebar();
|
||||
RenderContent();
|
||||
RenderHelpModal();
|
||||
UI_Sidebar();
|
||||
UI_MainContent();
|
||||
UI_HelpModal();
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// -- Main Loop
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @brief Application Entry Point.
|
||||
* Initializes Memory, Clay, Ncurses, and runs the main event loop.
|
||||
*/
|
||||
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);
|
||||
|
||||
// Initialize Ncurses Renderer
|
||||
Clay_Ncurses_Initialize();
|
||||
|
||||
// Non-blocking input
|
||||
// Set non-blocking input for game loop
|
||||
timeout(0);
|
||||
|
||||
while(!appState.shouldQuit) {
|
||||
HandleInput();
|
||||
while(!_appState.isQuitting) {
|
||||
App_ProcessInput();
|
||||
|
||||
Clay_Dimensions dims = Clay_Ncurses_GetLayoutDimensions();
|
||||
Clay_SetLayoutDimensions(dims);
|
||||
|
|
@ -372,15 +458,16 @@ int main() {
|
|||
};
|
||||
Clay_SetPointerState(center, false);
|
||||
|
||||
Clay_UpdateScrollContainers(true, (Clay_Vector2){0, appState.scrollDelta}, 0.016f);
|
||||
Clay_UpdateScrollContainers(true, (Clay_Vector2){0, _appState.scrollDelta}, 0.016f);
|
||||
}
|
||||
|
||||
Clay_BeginLayout();
|
||||
RenderMainLayout();
|
||||
UI_RootLayout();
|
||||
Clay_RenderCommandArray commands = Clay_EndLayout();
|
||||
|
||||
Clay_Ncurses_Render(commands);
|
||||
|
||||
// 60 FPS Target (approx)
|
||||
struct timespec ts = { .tv_sec = 0, .tv_nsec = 16000 * 1000 };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue