diff --git a/renderers/termbox2/clay_renderer_termbox2.c b/renderers/termbox2/clay_renderer_termbox2.c new file mode 100644 index 0000000..6760f89 --- /dev/null +++ b/renderers/termbox2/clay_renderer_termbox2.c @@ -0,0 +1,1190 @@ +/* + zlib/libpng license + + Copyright (c) 2025 Mivirl + + This software is provided 'as-is', without any express or implied warranty. + In no event will the authors be held liable for any damages arising from the + use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software in a + product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ + +#include "../../clay.h" + +#define TB_OPT_ATTR_W 32 // Required for truecolor support +#include "termbox2.h" + + +// ------------------------------------------------------------------------------------------------- +// -- Data structures + +typedef struct { + int width; + int height; +} clay_tb_dimensions; + +typedef struct { + float width; + float height; +} clay_tb_pixel_dimensions; + +typedef struct { + int x, y, width, height; +} cell_bounding_box; + +typedef struct { + Clay_Color clay; + uintattr_t termbox; +} color_pair; + +enum border_mode { + CLAY_TB_BORDER_MODE_DEFAULT, + CLAY_TB_BORDER_MODE_ROUND, + CLAY_TB_BORDER_MODE_MINIMUM, +}; + +enum border_chars { + CLAY_TB_BORDER_CHARS_DEFAULT, + CLAY_TB_BORDER_CHARS_ASCII, + CLAY_TB_BORDER_CHARS_UNICODE, + CLAY_TB_BORDER_CHARS_NONE, +}; + +// Truecolor is only enabled if TB_OPT_ATTR_W is set to 32 or 64. The default is 16, so it must be +// defined to reference the constant +#ifndef TB_OUTPUT_TRUECOLOR +#define TB_OUTPUT_TRUECOLOR (TB_OUTPUT_GRAYSCALE + 1) +#endif + +// Constant that doesn't collide with termbox2's existing output modes +#define CLAY_TB_OUTPUT_NOCOLOR 0 + +#if !(defined NDEBUG || defined CLAY_TB_NDEBUG) +#define clay_tb_assert(condition, ...) \ + if (!(condition)) { \ + Clay_Termbox_Close(); \ + fprintf(stderr, "%s %d (%s): Assertion failure: ", __FILE__, __LINE__, __func__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + exit(1); \ + } +#else +#define clay_tb_assert(condition, ...) +#endif // NDEBUG || CLAY_TB_NDEBUG + + +// ------------------------------------------------------------------------------------------------- +// -- Public API + +/** + Set the equivalent size for a terminal cell in pixels. + + This size is used to convert Clay's pixel measurements to terminal cells, and + affects scaling. + + Default dimensions were measured on Debian 12: (9, 21) + + \param width Width of a terminal cell in pixels + \param height Height of a terminal cell in pixels +*/ +void Clay_Termbox_Set_Cell_Pixel_Size(float width, float height); + +/** + Sets the color rendering mode for the terminal + + \param color_mode Termbox output mode as defined in termbox2.h, excluding truecolor + - TB_OUTPUT_NORMAL - Use default ANSI colors + - TB_OUTPUT_256 - Use 256 terminal colors + - TB_OUTPUT_216 - Use 216 terminal colors from 256 color mode + - TB_OUTPUT_GRAYSCALE - Use 24 gray colors from 256 color mode + - CLAY_TB_OUTPUT_NOCOLOR - Don't use ANSI colors at all + */ +void Clay_Termbox_Set_Color_Mode(int color_mode); + +/** + Sets the method for converting the width of borders to terminal cells + + \param border_mode Method for adjusting border sizes to fit terminal cells + - CLAY_TB_BORDER_MODE_DEFAULT - same as CLAY_TB_BORDER_MODE_MINIMUM + - CLAY_TB_BORDER_MODE_ROUND - borders will be rounded to nearest cell + size + - CLAY_TB_BORDER_MODE_MINIMUM - borders will have a minimum width of one + cell + */ +void Clay_Termbox_Set_Border_Mode(enum border_mode border_mode); + +/** + Sets the character style to use for rendering borders + + \param border_chars Characters used for rendering borders + - CLAY_TB_BORDER_CHARS_DEFAULT - same as BORDER_UNICODE + - CLAY_TB_BORDER_CHARS_ASCII - Uses ascii characters: '+', '|', '-' + - CLAY_TB_BORDER_CHARS_UNICODE - Uses unicode box drawing characters + - CLAY_TB_BORDER_CHARS_NONE - Don't draw borders + */ +void Clay_Termbox_Set_Border_Chars(enum border_chars border_chars); + +/** + Enables or disables emulated transparency + + If the color mode is TB_OUTPUT_NORMAL or CLAY_TB_OUTPUT_NOCOLOR, transparency will not be enabled + + \param transparency Transparency value to set + */ +void Clay_Termbox_Set_Transparency(bool transparency); + +/** + Current width of the terminal in pixels +*/ +float Clay_Termbox_Width(void); + +/** + Current height of the terminal in pixels +*/ +float Clay_Termbox_Height(void); + +/** + Current width of a terminal cell in pixels +*/ +float Clay_Termbox_Cell_Width(void); + +/** + Current height of a terminal cell in pixels +*/ +float Clay_Termbox_Cell_Height(void); + +/** + Callback function used to measure the dimensions in pixels of a text string + + \param text Text to measure + \param config Ignored + \param userData Ignored + */ +static inline Clay_Dimensions Clay_Termbox_MeasureText( + Clay_StringSlice text, Clay_TextElementConfig *config, void *userData); + +/** + Set up configuration, start termbox2, and allocate internal structures. + + Configuration can be overriden by environment variables: + - CLAY_TB_COLOR_MODE + - NORMAL + - 256 + - 216 + - GRAYSCALE + - TRUECOLOR + - NOCOLOR + - CLAY_TB_BORDER_CHARS + - DEFAULT + - ASCII + - UNICODE + - NONE + - CLAY_TB_TRANSPARENCY + - 1 + - 0 + - CLAY_TB_CELL_PIXELS + - 10x20 + + Must be run before using this renderer. + + \param color_mode Termbox output mode as defined in termbox2.h, excluding truecolor + \param border_mode Method for adjusting border sizes to fit terminal cells + \param border_chars Characters used for rendering borders + \param transparency Emulate transparency using background colors + */ +void Clay_Termbox_Initialize(int color_mode, enum border_mode border_mode, + enum border_chars border_chars, bool fake_transparency); + +/** + Stop termbox2 and release internal structures + */ +void Clay_Termbox_Close(void); + +/** + Render a set of commands to the terminal + + \param commands Array of render commands from Clay's CreateLayout() function + */ +void Clay_Termbox_Render(Clay_RenderCommandArray commands); + + +// ------------------------------------------------------------------------------------------------- +// -- Internal state + +// Settings/options +static bool clay_tb_initialized = false; +static int clay_tb_color_mode = TB_OUTPUT_NORMAL; +static bool clay_tb_transparency = false; +static enum border_mode clay_tb_border_mode = CLAY_TB_BORDER_MODE_DEFAULT; +static enum border_chars clay_tb_border_chars = CLAY_TB_BORDER_CHARS_DEFAULT; + +// Dimensions of a cell are specified in pixels +// Default dimensions were measured from the default terminal on Debian 12: +// Terminal: gnome-terminal +// Font: "Monospace Regular" +// Font size: 11 +static clay_tb_pixel_dimensions clay_tb_cell_size = { .width = 9, .height = 21 }; + +// Scissor mode prevents drawing outside of the specified bounding box +static bool clay_tb_scissor_enabled = false; +cell_bounding_box clay_tb_scissor_box; + + +// ----------------------------------------------- +// -- Color buffers + +// Buffers storing background colors from previously drawn items. Used to emulate transparency and +// set the background color for text. + +// Termbox's tb_get_buffer function would allow using it's internal buffer without needing to make +// a duplicate buffer here, but the function is deprecated. A new function for accessing the buffer +// might be merged in the future: https://github.com/termbox/termbox2/pull/65 + +// Buffer storing +static uintattr_t *clay_tb_color_buffer_termbox = NULL; +static Clay_Color *clay_tb_color_buffer_clay = NULL; +// Dimensions are specified in cells +static clay_tb_dimensions clay_tb_color_buffer_dimensions = { 0, 0 }; +static clay_tb_dimensions clay_tb_color_buffer_max_dimensions = { 0, 0 }; + + +// ------------------------------------------------------------------------------------------------- +// -- Internal utility functions + +static inline bool clay_tb_valid_color(Clay_Color color) +{ + return ( + 0x00 <= color.r && color.r <= 0xff && + 0x00 <= color.g && color.g <= 0xff && + 0x00 <= color.b && color.b <= 0xff && + 0x00 <= color.a && color.a <= 0xff + ); +} + +/** + In 256-color mode, there are 216 colors (excluding default terminal colors and gray colors), + with 6 different magnitudes for each of r, g, b. + + This function clamps to the nearest intensity (represented by 0-5) that can be output in this mode + + Possible intensities per component: 0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff + + Examples: + - 0x20 -> 0 + - 0x2f -> 1 + - 0x85 -> 2 + - 0xff -> 5 + + \param color 8-bit intensity of one RGB component +*/ +static int clay_tb_rgb_intensity_to_index(int color) +{ + clay_tb_assert(0x00 <= color && color <= 0xff, "Invalid intensity (allowed range 0x00-0xff)"); + return (color < 0x2f) ? 0 + : (color < 0x73) ? 1 + : 2 + ((color - 0x73) / 0x28); +} + +/** + Convert an RGB color from Clay's representation to the nearest representable color in the current + termbox2 output mode + + \param color Color to convert + */ +static uintattr_t clay_tb_color_convert(Clay_Color color) +{ + clay_tb_assert(clay_tb_valid_color(color), "Invalid Clay color"); + + uintattr_t tb_color = TB_DEFAULT; + + switch (clay_tb_color_mode) { + default: { + clay_tb_assert(false, "Invalid or unimplemented Termbox color output mode (%d)", + clay_tb_color_mode); + break; + } + case TB_OUTPUT_NORMAL: { + const int color_lut_count = 16; + const uintattr_t color_lut[][4] = { + { TB_BLACK, 0x00, 0x00, 0x00 }, + { TB_RED, 0xaa, 0x00, 0x00 }, + { TB_GREEN, 0x00, 0xaa, 0x00 }, + { TB_YELLOW, 0xaa, 0x55, 0x00 }, + { TB_BLUE, 0x00, 0x00, 0xaa }, + { TB_MAGENTA, 0xaa, 0x00, 0xaa }, + { TB_CYAN, 0x00, 0xaa, 0xaa }, + { TB_WHITE, 0xaa, 0xaa, 0xaa }, + { TB_BLACK | TB_BRIGHT, 0x55, 0x55, 0x55 }, + { TB_RED | TB_BRIGHT, 0xff, 0x55, 0x55 }, + { TB_GREEN | TB_BRIGHT, 0x55, 0xff, 0x55 }, + { TB_YELLOW | TB_BRIGHT, 0xff, 0xff, 0x55 }, + { TB_BLUE | TB_BRIGHT, 0x55, 0x55, 0xff }, + { TB_MAGENTA | TB_BRIGHT, 0xff, 0x55, 0xff }, + { TB_CYAN | TB_BRIGHT, 0x55, 0xff, 0xff }, + { TB_WHITE | TB_BRIGHT, 0xff, 0xff, 0xff } + }; + + // Find nearest color on the lookup table + int color_index = 0; + float min_distance_squared = 0xff * 0xff * 3; + for (int i = 0; i < color_lut_count; ++i) { + float r_distance = color.r - (float)color_lut[i][1]; + float g_distance = color.g - (float)color_lut[i][2]; + float b_distance = color.b - (float)color_lut[i][3]; + + float distance_squared = + (r_distance * r_distance) + + (g_distance * g_distance) + + (b_distance * b_distance); + + // Penalize pure black and white to display faded colors more often + if (TB_BLACK == color_lut[i][0] || TB_WHITE == color_lut[i][0] + || (TB_BLACK | TB_BRIGHT) == color_lut[i][0] + || (TB_WHITE | TB_BRIGHT) == color_lut[i][0]) { + distance_squared *= 2; + } + + if (distance_squared < min_distance_squared) { + min_distance_squared = distance_squared; + color_index = i; + } + } + tb_color = color_lut[color_index][0]; + break; + } + case TB_OUTPUT_216: { + int r_index = clay_tb_rgb_intensity_to_index((int)color.r); + int g_index = clay_tb_rgb_intensity_to_index((int)color.g); + int b_index = clay_tb_rgb_intensity_to_index((int)color.b); + + tb_color = 0x01 + (36 * r_index) + (6 * g_index) + (b_index); + break; + } + case TB_OUTPUT_256: { + const int index_lut_count = 6; + const uintattr_t index_lut[] = { 0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff }; + + int r_index = clay_tb_rgb_intensity_to_index((int)color.r); + int g_index = clay_tb_rgb_intensity_to_index((int)color.g); + int b_index = clay_tb_rgb_intensity_to_index((int)color.b); + + int rgb_color = 0x10 + (36 * r_index) + (6 * g_index) + (b_index); + + float rgb_r_distance = color.r - (float)index_lut[r_index]; + float rgb_g_distance = color.g - (float)index_lut[g_index]; + float rgb_b_distance = color.b - (float)index_lut[b_index]; + + float rgb_distance_squared = + (rgb_r_distance * rgb_r_distance) + + (rgb_g_distance * rgb_g_distance) + + (rgb_b_distance * rgb_b_distance); + + int avg_color = (int)((color.r + color.g + color.b) / 3); + int gray_avg_color = (avg_color * 24 / 0x100); + int gray_color = 0xe8 + gray_avg_color; + + float gray_r_distance = color.r - (float)gray_avg_color; + float gray_g_distance = color.g - (float)gray_avg_color; + float gray_b_distance = color.b - (float)gray_avg_color; + + float gray_distance_squared = + (gray_r_distance * gray_r_distance) + + (gray_g_distance * gray_g_distance) + + (gray_b_distance * gray_b_distance); + + tb_color = (rgb_distance_squared < gray_distance_squared) ? rgb_color : gray_color; + + break; + } + case TB_OUTPUT_GRAYSCALE: { + // 24 shades of gray + float avg_color = ((color.r + color.g + color.b) / 3); + tb_color = 0x01 + (int)(avg_color * 24 / 0x100); + break; + } + case TB_OUTPUT_TRUECOLOR: { + clay_tb_assert(32 <= TB_OPT_ATTR_W, "Truecolor requires TB_OPT_ATTR_W to be 32 or 64"); + tb_color = ((uintattr_t)color.r << 4 * 4) + ((uintattr_t)color.g << 2 * 4) + + ((uintattr_t)color.b); + if (0x000000 == tb_color) { + tb_color = TB_HI_BLACK; + } + break; + } + case CLAY_TB_OUTPUT_NOCOLOR: { + // Uses default terminal colors + tb_color = TB_DEFAULT; + break; + } + } + return tb_color; +} + +/** + Round float to nearest integer value + + Used instead of roundf() so math.h doesn't need to be linked + + \param f Float to round + */ +static inline int clay_tb_roundf(float f) +{ + int i = f; + return (f - i > 0.5f) ? i + 1 : i; +} + +/** + Snap pixel values from Clay to nearest cell values + + Width/height accounts for offset from x/y, so a box at x=(1.2 * cell_width) and + width=(1.4 * cell_width) is snapped to x=1 and width=2. + + \param box Bounding box with pixel measurements to convert + */ +static inline cell_bounding_box cell_snap_bounding_box(Clay_BoundingBox box) +{ + return (cell_bounding_box) { + .x = clay_tb_roundf(box.x / clay_tb_cell_size.width), + .y = clay_tb_roundf(box.y / clay_tb_cell_size.height), + .width = clay_tb_roundf((box.x + box.width) / clay_tb_cell_size.width) + - clay_tb_roundf(box.x / clay_tb_cell_size.width), + .height = clay_tb_roundf((box.y + box.height) / clay_tb_cell_size.height) + - clay_tb_roundf(box.y / clay_tb_cell_size.height), + }; +} + +/** + Get stored termbox color for a position from the internal color buffer + + \param x X position of cell + \param y Y position of cell + */ +static inline uintattr_t clay_tb_color_buffer_termbox_get(int x, int y) +{ + clay_tb_assert(0 <= x && x < clay_tb_color_buffer_dimensions.width, + "Cell buffer x position (%d) offscreen (range 0-%d)", x, + clay_tb_color_buffer_dimensions.width); + clay_tb_assert(0 <= y && y < clay_tb_color_buffer_dimensions.height, + "Cell buffer y position (%d) offscreen (range 0-%d)", y, + clay_tb_color_buffer_dimensions.height); + return clay_tb_color_buffer_termbox[x + (y * clay_tb_color_buffer_dimensions.width)]; +} + +/** + Set stored termbox color for a position in the internal color buffer + + \param x X position of cell + \param y Y position of cell + \param color Color to store + */ +static inline void clay_tb_color_buffer_termbox_set(int x, int y, uintattr_t color) +{ + clay_tb_assert(0 <= x && x < clay_tb_color_buffer_dimensions.width, + "Cell buffer x position (%d) offscreen (range 0-%d)", x, + clay_tb_color_buffer_dimensions.width); + clay_tb_assert(0 <= y && y < clay_tb_color_buffer_dimensions.height, + "Cell buffer y position (%d) offscreen (range 0-%d)", y, + clay_tb_color_buffer_dimensions.height); + clay_tb_color_buffer_termbox[x + (y * clay_tb_color_buffer_dimensions.width)] = color; +} + +/** + Get stored clay color for a position from the internal color buffer + + \param x X position of cell + \param y Y position of cell + */ +static inline Clay_Color clay_tb_color_buffer_clay_get(int x, int y) +{ + clay_tb_assert( + clay_tb_transparency, "Clay color buffer is only available if transparency is enabled\n"); + clay_tb_assert(0 <= x && x < clay_tb_color_buffer_dimensions.width, + "Cell buffer x position (%d) offscreen (range 0-%d)", x, + clay_tb_color_buffer_dimensions.width); + clay_tb_assert(0 <= y && y < clay_tb_color_buffer_dimensions.height, + "Cell buffer y position (%d) offscreen (range 0-%d)", y, + clay_tb_color_buffer_dimensions.height); + return clay_tb_color_buffer_clay[x + (y * clay_tb_color_buffer_dimensions.width)]; +} + +/** + Set stored clay color for a position in the internal color buffer + + \param x X position of cell + \param y Y position of cell + \param color Color to store + */ +static inline void clay_tb_color_buffer_clay_set(int x, int y, Clay_Color color) +{ + clay_tb_assert( + clay_tb_transparency, "Clay color buffer is only available if transparency is enabled\n"); + clay_tb_assert(0 <= x && x < clay_tb_color_buffer_dimensions.width, + "Cell buffer x position (%d) offscreen (range 0-%d)", x, + clay_tb_color_buffer_dimensions.width); + clay_tb_assert(0 <= y && y < clay_tb_color_buffer_dimensions.height, + "Cell buffer y position (%d) offscreen (range 0-%d)", y, + clay_tb_color_buffer_dimensions.height); + clay_tb_color_buffer_clay[x + (y * clay_tb_color_buffer_dimensions.width)] = color; +} + +/** + Resize internal color buffer to the current terminal size + */ +static void clay_tb_resize_buffer(void) +{ + int current_width = tb_width(); + int current_height = tb_height(); + + // Reallocate if the new size is larger than the maximum size of the buffer + size_t max_size = (size_t)clay_tb_color_buffer_max_dimensions.width + * clay_tb_color_buffer_max_dimensions.height; + size_t new_size = (size_t)current_width * current_height; + if (max_size < new_size) { + + uintattr_t *tmp_termbox + = realloc(clay_tb_color_buffer_termbox, sizeof(uintattr_t) * new_size); + if (NULL == tmp_termbox) { + clay_tb_assert(false, "Reallocation failure for internal termbox color buffer"); + } + clay_tb_color_buffer_termbox = tmp_termbox; + for (size_t i = max_size; i < new_size; ++i) { + clay_tb_color_buffer_termbox[i] = TB_DEFAULT; + } + + Clay_Color *tmp_clay = realloc(clay_tb_color_buffer_clay, sizeof(Clay_Color) * new_size); + if (NULL == tmp_clay) { + clay_tb_assert(false, "Reallocation failure for internal clay color buffer"); + } + clay_tb_color_buffer_clay = tmp_clay; + for (size_t i = max_size; i < new_size; ++i) { + clay_tb_color_buffer_clay[i] = (Clay_Color) { 0 }; + } + + clay_tb_color_buffer_max_dimensions.width = current_width; + clay_tb_color_buffer_max_dimensions.height = current_height; + } + clay_tb_color_buffer_dimensions.width = current_width; + clay_tb_color_buffer_dimensions.height = current_height; +} + +/** + Calculate color at a given position after emulating transparency. + + This isn't true transparency, just the background colors changing to emulate it + + \param color Pair of termbox and clay color representations to overlay with the background color + \param x X position of cell + \param y Y position of cell + */ +static inline color_pair clay_tb_get_transparency_color(int x, int y, color_pair color) +{ + if (!clay_tb_transparency) { + return color; + } + + Clay_Color color_bg = clay_tb_color_buffer_clay_get(x, y); + Clay_Color new_color = { + .r = color_bg.r + (color.clay.a / 255) * (color.clay.r - color_bg.r), + .g = color_bg.g + (color.clay.a / 255) * (color.clay.g - color_bg.g), + .b = color_bg.b + (color.clay.a / 255) * (color.clay.b - color_bg.b), + .a = 255 + }; + + return (color_pair) { + .clay = new_color, + .termbox = clay_tb_color_convert(new_color) + }; +} + +/** + Draw a character cell at a position on screen. + + Accounts for scissor mode and stores the cell to the internal color buffer for transparency and + text backgrounds. + + \param x X position of cell + \param y Y position of cell + \param ch Utf32 representation of character to draw + \param tb_fg Foreground color in termbox representation + \param tb_bg Background color in termbox representation + \param fg Foreground color in clay representation + \param bg Background color in clay representation + */ +static int clay_tb_set_cell( + int x, int y, uint32_t ch, uintattr_t tb_fg, uintattr_t tb_bg, Clay_Color bg) +{ + clay_tb_assert(0 <= x && x < tb_width(), "Cell buffer x position (%d) offscreen (range 0-%d)", + x, tb_width()); + clay_tb_assert(0 <= y && y < tb_height(), "Cell buffer y position (%d) offscreen (range 0-%d)", + y, tb_height()); + + if (!clay_tb_scissor_enabled + || (clay_tb_scissor_enabled + && (clay_tb_scissor_box.x <= x + && x < clay_tb_scissor_box.x + clay_tb_scissor_box.width) + && (clay_tb_scissor_box.y <= y + && y < clay_tb_scissor_box.y + clay_tb_scissor_box.height))) { + int codepoint_width = tb_wcwidth(ch); + if (-1 == codepoint_width) { + // Nonprintable character, use REPLACEMENT CHARACTER (U+FFFD) + ch = U'\ufffd'; + codepoint_width = tb_wcwidth(ch); + } + + int err; + int max_x = CLAY__MIN(x + codepoint_width, tb_width()); + for (int i = x; i < max_x; ++i) { + clay_tb_color_buffer_termbox_set(i, y, tb_bg); + if (clay_tb_transparency) { + clay_tb_color_buffer_clay_set(i, y, bg); + } + err = tb_set_cell(i, y, ch, tb_fg, tb_bg); + if (TB_OK != err) { + break; + } + } + + return err; + } + return -1; +} + + +// ------------------------------------------------------------------------------------------------- +// -- Public API implementation + +void Clay_Termbox_Set_Cell_Pixel_Size(float width, float height) +{ + clay_tb_assert(0 <= width, "Cell pixel width must be > 0"); + clay_tb_assert(0 <= height, "Cell pixel height must be > 0"); + clay_tb_cell_size = (clay_tb_pixel_dimensions) { .width = width, .height = height }; +} + +void Clay_Termbox_Set_Color_Mode(int color_mode) +{ + clay_tb_assert(clay_tb_initialized, "Clay_Termbox_Initialize must be run first"); + clay_tb_assert(CLAY_TB_OUTPUT_NOCOLOR <= color_mode && color_mode <= TB_OUTPUT_TRUECOLOR, + "Color mode invalid (%d)", color_mode); + + if (CLAY_TB_OUTPUT_NOCOLOR == color_mode) { + tb_set_output_mode(TB_OUTPUT_NORMAL); + } else { + tb_set_output_mode(color_mode); + } + + // Force complete re-render to ensure all colors are redrawn + tb_invalidate(); + + clay_tb_color_mode = color_mode; + + // Re-set transparency value. It will be toggled off if the new output mode doesn't support it + Clay_Termbox_Set_Transparency(clay_tb_transparency); +} + +void Clay_Termbox_Set_Border_Mode(enum border_mode border_mode) +{ + clay_tb_assert(CLAY_TB_BORDER_MODE_DEFAULT <= border_mode + && border_mode <= CLAY_TB_BORDER_MODE_MINIMUM, + "Border mode invalid (%d)", border_mode); + if (CLAY_TB_BORDER_MODE_DEFAULT == border_mode) { + clay_tb_border_mode = CLAY_TB_BORDER_MODE_MINIMUM; + } else { + clay_tb_border_mode = border_mode; + } +} + +void Clay_Termbox_Set_Border_Chars(enum border_chars border_chars) +{ + clay_tb_assert( + CLAY_TB_BORDER_CHARS_DEFAULT <= border_chars && border_chars <= CLAY_TB_BORDER_CHARS_NONE, + "Border mode invalid (%d)", border_chars); + if (CLAY_TB_BORDER_CHARS_DEFAULT == border_chars) { + clay_tb_border_chars = CLAY_TB_BORDER_CHARS_UNICODE; + } else { + clay_tb_border_chars = border_chars; + } +} + +void Clay_Termbox_Set_Transparency(bool transparency) +{ + clay_tb_transparency = transparency; + if (TB_OUTPUT_NORMAL == clay_tb_color_mode || CLAY_TB_OUTPUT_NOCOLOR == clay_tb_color_mode) { + clay_tb_transparency = false; + } + + // Clay color buffer is only needed if emulating transparency + if (clay_tb_transparency && NULL == clay_tb_color_buffer_clay) { + size_t size = (size_t)tb_width() * tb_height(); + clay_tb_color_buffer_clay = malloc(sizeof(Clay_Color) * size); + for (int i = 0; i < size; ++i) { + clay_tb_color_buffer_clay[i] = (Clay_Color) { 0, 0, 0, 0 }; + } + } + +} + +float Clay_Termbox_Width(void) +{ + clay_tb_assert(clay_tb_initialized, "Clay_Termbox_Initialize must be run first"); + + return (float)tb_width() * clay_tb_cell_size.width; +} + +float Clay_Termbox_Height(void) +{ + clay_tb_assert(clay_tb_initialized, "Clay_Termbox_Initialize must be run first"); + + return (float)tb_height() * clay_tb_cell_size.height; +} + +float Clay_Termbox_Cell_Width(void) +{ + return clay_tb_cell_size.width; +} + +float Clay_Termbox_Cell_Height(void) +{ + return clay_tb_cell_size.height; +} + +static inline Clay_Dimensions Clay_Termbox_MeasureText( + Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) +{ + clay_tb_assert(clay_tb_initialized, "Clay_Termbox_Initialize must be run first"); + + int width = 0; + int height = 1; + + // Convert to utf32 so termbox2's internal wcwidth function can get the printed width of each + // codepoint + for (int32_t i = 0; i < text.length;) { + uint32_t ch; + int codepoint_bytes = tb_utf8_char_to_unicode(&ch, text.chars + i); + if (0 > codepoint_bytes) { + clay_tb_assert(false, "Invalid utf8"); + } + i += codepoint_bytes; + + int codepoint_width = tb_wcwidth(ch); + if (-1 == codepoint_width) { + // Nonprintable character, use width of REPLACEMENT CHARACTER (U+FFFD) + codepoint_width = tb_wcwidth(0xfffd); + } + width += codepoint_width; + } + return (Clay_Dimensions) { + (float)width * clay_tb_cell_size.width, + (float)height * clay_tb_cell_size.height + }; +} + +void Clay_Termbox_Initialize( + int color_mode, enum border_mode border_mode, enum border_chars border_chars, bool transparency) +{ + int new_color_mode = color_mode; + int new_border_mode = border_mode; + int new_border_chars = border_chars; + int new_transparency = transparency; + clay_tb_pixel_dimensions new_pixel_size = clay_tb_cell_size; + + // Check for environment variables that override settings + + const char *env_color_mode = getenv("CLAY_TB_COLOR_MODE"); + if (NULL != env_color_mode) { + if (0 == strcmp("NORMAL", env_color_mode)) { + new_color_mode = TB_OUTPUT_NORMAL; + } else if (0 == strcmp("256", env_color_mode)) { + new_color_mode = TB_OUTPUT_256; + } else if (0 == strcmp("216", env_color_mode)) { + new_color_mode = TB_OUTPUT_216; + } else if (0 == strcmp("GRAYSCALE", env_color_mode)) { + new_color_mode = TB_OUTPUT_GRAYSCALE; + } else if (0 == strcmp("TRUECOLOR", env_color_mode)) { + new_color_mode = TB_OUTPUT_TRUECOLOR; + } else if (0 == strcmp("NOCOLOR", env_color_mode)) { + new_color_mode = CLAY_TB_OUTPUT_NOCOLOR; + } + } + + const char *env_border_chars = getenv("CLAY_TB_BORDER_CHARS"); + if (NULL != env_border_chars) { + if (0 == strcmp("DEFAULT", env_border_chars)) { + new_border_chars = CLAY_TB_BORDER_CHARS_DEFAULT; + } else if (0 == strcmp("ASCII", env_border_chars)) { + new_border_chars = CLAY_TB_BORDER_CHARS_ASCII; + } else if (0 == strcmp("UNICODE", env_border_chars)) { + new_border_chars = CLAY_TB_BORDER_CHARS_UNICODE; + } else if (0 == strcmp("NONE", env_border_chars)) { + new_border_chars = CLAY_TB_BORDER_CHARS_NONE; + } + } + + const char *env_transparency = getenv("CLAY_TB_TRANSPARENCY"); + if (NULL != env_transparency) { + if (0 == strcmp("1", env_transparency)) { + new_transparency = true; + } else if (0 == strcmp("0", env_transparency)) { + new_transparency = false; + } + } + + const char *env_cell_pixels = getenv("CLAY_TB_CELL_PIXELS"); + if (NULL != env_cell_pixels) { + const char *str_width = env_cell_pixels; + const char *str_height = strstr(env_cell_pixels, "x") + 1; + if (NULL + 1 != str_height) { + bool missing_value = false; + + errno = 0; + float cell_width = strtof(str_width, NULL); + if (0 != errno || 0 > cell_width) { + missing_value = true; + } + float cell_height = strtof(str_height, NULL); + if (0 != errno || 0 >= cell_height) { + missing_value = true; + } + + if (!missing_value) { + new_pixel_size = (clay_tb_pixel_dimensions) { cell_width, cell_height }; + } + } + } + + // NO_COLOR indicates that ANSI colors shouldn't be used: https://no-color.org/ + const char *env_nocolor = getenv("NO_COLOR"); + if (NULL != env_nocolor && '\0' != env_nocolor[0]) { + new_color_mode = CLAY_TB_OUTPUT_NOCOLOR; + } + + tb_init(); + tb_set_input_mode(TB_INPUT_MOUSE); + + // Enable mouse hover support + // - see https://github.com/termbox/termbox2/issues/71#issuecomment-2179581609 + // - 1003 "Any-event tracking" mode + // - 1006 SGR extended coordinates (already enabled with TB_INPUT_MOUSE) + tb_sendf("\x1b[?%d;%dh", 1003, 1006); + + clay_tb_initialized = true; + + Clay_Termbox_Set_Color_Mode(new_color_mode); + Clay_Termbox_Set_Border_Mode(new_border_mode); + Clay_Termbox_Set_Border_Chars(new_border_chars); + Clay_Termbox_Set_Transparency(new_transparency); + Clay_Termbox_Set_Cell_Pixel_Size(new_pixel_size.width, new_pixel_size.height); + + size_t size = (size_t)tb_width() * tb_height(); + clay_tb_color_buffer_termbox = malloc(sizeof(uintattr_t) * size); + for (int i = 0; i < size; ++i) { + clay_tb_color_buffer_termbox[i] = TB_DEFAULT; + } +} + +void Clay_Termbox_Close(void) +{ + if (clay_tb_initialized) { + // Disable mouse hover support + tb_sendf("\x1b[?%d;%dl", 1003, 1006); + + free(clay_tb_color_buffer_termbox); + free(clay_tb_color_buffer_clay); + tb_shutdown(); + clay_tb_initialized = false; + } +} + +void Clay_Termbox_Render(Clay_RenderCommandArray commands) +{ + clay_tb_assert(clay_tb_initialized, "Clay_Termbox_Initialize must be run first"); + + clay_tb_resize_buffer(); + for (int32_t i = 0; i < commands.length; ++i) { + const Clay_RenderCommand *command = Clay_RenderCommandArray_Get(&commands, i); + const cell_bounding_box cell_box = cell_snap_bounding_box(command->boundingBox); + + int box_begin_x = CLAY__MAX(cell_box.x, 0); + int box_end_x = CLAY__MIN(cell_box.x + cell_box.width, tb_width()); + int box_begin_y = CLAY__MAX(cell_box.y, 0); + int box_end_y = CLAY__MIN(cell_box.y + cell_box.height, tb_height()); + + if (box_end_x < 0 || box_end_y < 0 || tb_width() < box_begin_x + || tb_height() < box_begin_y) { + continue; + } + + switch (command->commandType) { + default: { + clay_tb_assert(false, "Unhandled command: %d\n", command->commandType); + } + case CLAY_RENDER_COMMAND_TYPE_NONE: { + break; + } + case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: { + Clay_RectangleRenderData render_data = command->renderData.rectangle; + Clay_Color color_fg = { 0, 0, 0, 0 }; + Clay_Color color_bg = render_data.backgroundColor; + uintattr_t color_tb_fg = TB_DEFAULT; + uintattr_t color_tb_bg = clay_tb_color_convert(color_bg); + + for (int y = box_begin_y; y < box_end_y; ++y) { + for (int x = box_begin_x; x < box_end_x; ++x) { + color_pair color_bg_new = clay_tb_get_transparency_color( + x, y, (color_pair) { color_bg, color_tb_bg }); + clay_tb_set_cell( + x, y, ' ', color_tb_fg, color_bg_new.termbox, color_bg_new.clay); + } + } + break; + } + case CLAY_RENDER_COMMAND_TYPE_BORDER: { + if (CLAY_TB_BORDER_CHARS_NONE == clay_tb_border_chars) { + break; + } + Clay_BorderRenderData render_data = command->renderData.border; + Clay_Color color_fg = { 0, 0, 0, 1 }; + Clay_Color color_bg = render_data.color; + uintattr_t color_tb_fg = TB_DEFAULT; + uintattr_t color_tb_bg = clay_tb_color_convert(color_bg); + + int border_skip_begin_x = box_begin_x; + int border_skip_end_x = box_end_x; + int border_skip_begin_y = box_begin_y; + int border_skip_end_y = box_end_y; + + switch (clay_tb_border_mode) { + default: { + clay_tb_assert(false, "Invalid or unimplemented border mode (%d)", + clay_tb_border_mode); + break; + } + case CLAY_TB_BORDER_MODE_MINIMUM: { + // Borders will be at least one cell wide if width is nonzero + // and the bounding box is large enough to not be all borders + if (0 < cell_box.width) { + if (0 < render_data.width.left) { + border_skip_begin_x = box_begin_x + + (int)CLAY__MAX( + 1, (render_data.width.left / clay_tb_cell_size.width)); + } + if (0 < render_data.width.right) { + border_skip_end_x = box_end_x + - (int)CLAY__MAX( + 1, (render_data.width.right / clay_tb_cell_size.width)); + } + } + if (0 < cell_box.height) { + if (0 < render_data.width.top) { + border_skip_begin_y = box_begin_y + + (int)CLAY__MAX( + 1, (render_data.width.top / clay_tb_cell_size.width)); + } + if (0 < render_data.width.bottom) { + border_skip_end_y = box_end_y + - (int)CLAY__MAX( + 1, (render_data.width.bottom / clay_tb_cell_size.width)); + } + } + break; + } + case CLAY_TB_BORDER_MODE_ROUND: { + int halfwidth = clay_tb_roundf(clay_tb_cell_size.width / 2); + int halfheight = clay_tb_roundf(clay_tb_cell_size.height / 2); + + if (halfwidth < render_data.width.left) { + border_skip_begin_x = box_begin_x + + (int)CLAY__MAX( + 1, (render_data.width.left / clay_tb_cell_size.width)); + } + if (halfwidth < render_data.width.right) { + border_skip_end_x = box_end_x + - (int)CLAY__MAX( + 1, (render_data.width.right / clay_tb_cell_size.width)); + } + if (halfheight < render_data.width.top) { + border_skip_begin_y = box_begin_y + + (int)CLAY__MAX( + 1, (render_data.width.top / clay_tb_cell_size.width)); + } + if (halfheight < render_data.width.bottom) { + border_skip_end_y = box_end_y + - (int)CLAY__MAX( + 1, (render_data.width.bottom / clay_tb_cell_size.width)); + } + break; + } + } + + // Draw border, skipping over the center of the bounding box + for (int y = box_begin_y; y < box_end_y; ++y) { + for (int x = box_begin_x; x < box_end_x; ++x) { + if ((border_skip_begin_x <= x && x < border_skip_end_x) + && (border_skip_begin_y <= y && y < border_skip_end_y)) { + x = border_skip_end_x - 1; + continue; + } + + uint32_t ch; + switch (clay_tb_border_chars) { + default: { + clay_tb_assert(false, + "Invalid or unimplemented border character mode (%d)", + clay_tb_border_chars); + } + case CLAY_TB_BORDER_CHARS_UNICODE: { + if ((x < border_skip_begin_x) + && (y < border_skip_begin_y)) { // Top left + ch = U'\u250c'; + } else if ((x >= border_skip_end_x) + && (y < border_skip_begin_y)) { // Top right + ch = U'\u2510'; + } else if ((x < border_skip_begin_x) + && (y >= border_skip_end_y)) { // Bottom left + ch = U'\u2514'; + } else if ((x >= border_skip_end_x) + && (y >= border_skip_end_y)) { // Bottom right + ch = U'\u2518'; + } else if (x < border_skip_begin_x || x >= border_skip_end_x) { + ch = U'\u2502'; + } else if (y < border_skip_begin_y || y >= border_skip_end_y) { + ch = U'\u2500'; + } + break; + } + case CLAY_TB_BORDER_CHARS_DEFAULT: + case CLAY_TB_BORDER_CHARS_ASCII: { + if ((x < border_skip_begin_x || x >= border_skip_end_x) + && (y < border_skip_begin_y || y >= border_skip_end_y)) { + ch = '+'; + } else if (x < border_skip_begin_x || x >= border_skip_end_x) { + ch = '|'; + } else if (y < border_skip_begin_y || y >= border_skip_end_y) { + ch = '-'; + } + break; + } + } + + color_pair color_bg_new = clay_tb_get_transparency_color( + x, y, (color_pair) { color_bg, color_tb_bg }); + clay_tb_set_cell( + x, y, ch, color_tb_fg, color_bg_new.termbox, color_bg_new.clay); + } + } + break; + } + case CLAY_RENDER_COMMAND_TYPE_TEXT: { + Clay_TextRenderData render_data = command->renderData.text; + Clay_Color color_fg = render_data.textColor; + uintattr_t color_tb_fg = clay_tb_color_convert(color_fg); + + Clay_StringSlice *text = &render_data.stringContents; + int32_t i = 0; + for (int y = box_begin_y; y < box_end_y; ++y) { + for (int x = box_begin_x; x < box_end_x;) { + uint32_t ch = ' '; + if (i < text->length) { + int codepoint_length = tb_utf8_char_to_unicode(&ch, text->chars + i); + if (0 > codepoint_length) { + clay_tb_assert(false, "Invalid utf8"); + } + i += codepoint_length; + + uintattr_t color_tb_bg = clay_tb_color_buffer_termbox_get(x, y); + Clay_Color color_bg = { 0 }; + color_pair color_bg_new = clay_tb_get_transparency_color( + x, y, (color_pair) { color_bg, color_tb_bg }); + clay_tb_set_cell( + x, y, ch, color_tb_fg, color_bg_new.termbox, color_bg_new.clay); + } + + int codepoint_width = tb_wcwidth(ch); + if (-1 == codepoint_width) { + // Nonprintable character, use REPLACEMENT CHARACTER (U+FFFD) + ch = U'\ufffd'; + codepoint_width = tb_wcwidth(ch); + } + + x += codepoint_width; + } + } + break; + } + case CLAY_RENDER_COMMAND_TYPE_IMAGE: { + Clay_ImageRenderData render_data = command->renderData.image; + Clay_Color color_fg = { 0, 0, 0, 0 }; + Clay_Color color_bg = render_data.backgroundColor; + uintattr_t color_tb_fg = clay_tb_color_convert(color_fg); + uintattr_t color_tb_bg; + + // Only set background to the provided color if it's non-default + bool color_specified + = !(color_bg.r == 0 && color_bg.g == 0 && color_bg.b == 0 && color_bg.a == 0); + if (color_specified) { + color_tb_bg = clay_tb_color_convert(color_bg); + } + + const char *placeholder_text = "[Image]"; + + int i = 0; + unsigned long len = strlen(placeholder_text); + for (int y = box_begin_y; y < box_end_y; ++y) { + float percent_y = (float)(y - box_begin_y) / (float)cell_box.height; + + for (int x = box_begin_x; x < box_end_x; ++x) { + char ch = ' '; + if (i < len) { + ch = placeholder_text[i++]; + } + + if (!color_specified) { + // Use a placeholder pattern for the image + float percent_x = (float)(cell_box.width - (x - box_begin_x)) + / (float)cell_box.width; + if (percent_x > percent_y) { + color_bg = (Clay_Color) { 0x94, 0xb4, 0xff, 0xff }; + color_tb_bg = clay_tb_color_convert(color_bg); + } else { + color_bg = (Clay_Color) { 0x3f, 0xcc, 0x45, 0xff }; + color_tb_bg = clay_tb_color_convert(color_bg); + } + } + clay_tb_set_cell(x, y, ch, color_tb_fg, color_tb_bg, color_bg); + } + } + break; + } + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: { + clay_tb_scissor_box = (cell_bounding_box) { + .x = box_begin_x, + .y = box_begin_y, + .width = box_end_x - box_begin_x, + .height = box_end_y - box_begin_y, + }; + clay_tb_scissor_enabled = true; + break; + } + case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: { + clay_tb_scissor_enabled = false; + break; + } + case CLAY_RENDER_COMMAND_TYPE_CUSTOM: { + break; + } + } + } +}