clay/renderers/termbox2/clay_renderer_termbox2.c

1777 lines
71 KiB
C

/*
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"
#include "image_character_masks.h"
#define TB_OPT_ATTR_W 32 // Required for truecolor support
#include "termbox2.h"
#include "stb_image.h"
#include "stb_image_resize2.h"
// -------------------------------------------------------------------------------------------------
// -- Data structures
typedef struct {
int width, height;
} clay_tb_dimensions;
typedef struct {
float width, height;
} clay_tb_pixel_dimensions;
typedef struct {
int x, y;
int width, height;
} clay_tb_cell_bounding_box;
typedef struct {
Clay_Color clay;
uintattr_t termbox;
} clay_tb_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_BLANK,
CLAY_TB_BORDER_CHARS_NONE,
};
enum image_mode {
CLAY_TB_IMAGE_MODE_DEFAULT,
CLAY_TB_IMAGE_MODE_PLACEHOLDER,
CLAY_TB_IMAGE_MODE_BG,
CLAY_TB_IMAGE_MODE_ASCII_FG,
CLAY_TB_IMAGE_MODE_ASCII_FG_FAST,
CLAY_TB_IMAGE_MODE_ASCII,
CLAY_TB_IMAGE_MODE_ASCII_FAST,
CLAY_TB_IMAGE_MODE_UNICODE,
CLAY_TB_IMAGE_MODE_UNICODE_FAST,
};
typedef struct {
// Stores information about image loaded from stb
int pixel_width, pixel_height;
unsigned char *pixel_data;
// Internal cached data from previous renders
struct {
enum image_mode last_image_mode;
int width, height;
size_t size_max;
uint32_t *characters;
Clay_Color *foreground;
Clay_Color *background;
// Data storing progress of partially complete image conversions that take multiple renders
struct clay_tb_partial_render {
bool in_progress;
unsigned char *resized_pixel_data;
int cursor_x, cursor_y;
int cursor_mask;
int min_difference_squared_sum;
int best_mask;
Clay_Color best_foreground, best_background;
} partial_render;
} internal;
} clay_tb_image;
// 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_BLANK - Draws background colors only
- CLAY_TB_BORDER_CHARS_NONE - Don't draw borders
*/
void Clay_Termbox_Set_Border_Chars(enum border_chars border_chars);
/**
Sets the method for drawing images
\param image_mode Method for adjusting border sizes to fit terminal cells
- CLAY_TB_IMAGE_MODE_DEFAULT - same as CLAY_TB_IMAGE_MODE_UNICODE
- CLAY_TB_IMAGE_MODE_PLACEHOLDER - Draw a placeholder pattern in place of
images
- CLAY_TB_IMAGE_MODE_BG - Draw image by setting the background color
for space characters
- CLAY_TB_IMAGE_MODE_ASCII_FG - Draw image by setting the foreground color
for ascii characters
- CLAY_TB_IMAGE_MODE_ASCII - Draw image by setting the foreground and
background colors for ascii characters
- CLAY_TB_IMAGE_MODE_UNICODE - Draw image by setting the foreground and
background colors for unicode characters
- CLAY_TB_IMAGE_MODE_ASCII_FG_FAST - Draw image by setting the foreground color
for ascii characters. Checks fewer
characters to draw faster
- CLAY_TB_IMAGE_MODE_ASCII_FAST - Draw image by setting the foreground and
background colors for ascii characters.
Checks fewer characters to draw faster
- CLAY_TB_IMAGE_MODE_UNICODE_FAST - Draw image by setting the foreground and
background colors for unicode characters.
Checks fewer characters to draw faster
*/
void Clay_Termbox_Set_Image_Mode(enum image_mode image_mode);
/**
Fuel corresponds to the amount of time spent per render on drawing images. Increasing this has
the image render faster, but the program will be less responsive until it finishes
Cost to draw one cell (lengths of arrays in image_character_masks.h):
- 1 : CLAY_TB_IMAGE_MODE_BG
- 15 : CLAY_TB_IMAGE_MODE_UNICODE_FAST, CLAY_TB_IMAGE_MODE_ASCII_FAST,
CLAY_TB_IMAGE_MODE_ASCII_FG_FAST
- 52 : CLAY_TB_IMAGE_MODE_UNICODE
- 95 : CLAY_TB_IMAGE_MODE_ASCII, CLAY_TB_IMAGE_MODE_ASCII_FG
\param fuel_max Maximum amount of fuel used per render (shared between all images)
\param fuel_per_image Maximum amount of fuel used per render per image
*/
void Clay_Termbox_Set_Image_Fuel(int fuel_max, int fuel_per_image);
/**
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);
/**
Load an image from a file into a format usable with this renderer
Supports image formats from stb_image (JPG, PNG, TGA, BMP, PSD, GIF, HDR, PIC)
Note that rendered characters are cached in the returned `clay_tb_image`. If the same image is
used in multiple places, load it a separate time for each use to reduce unecessary reprocessing
every render.
\param filename File to load image from
*/
clay_tb_image Clay_Termbox_Image_Load_File(const char *filename);
/**
Load an image from memory into a format usable with this renderer
Supports image formats from stb_image (JPG, PNG, TGA, BMP, PSD, GIF, HDR, PIC)
Note that rendered characters are cached in the returned `clay_tb_image`. If the same image is
used in multiple places, load it a separate time for each use to reduce unecessary reprocessing
every render.
\param image Image to load. Should be the whole file copied into memory
\param size Size of the image in bytes
*/
clay_tb_image Clay_Termbox_Image_Load_Memory(const void *image, int size);
/**
Free an image
\param image Image to free
*/
void Clay_Termbox_Image_Free(clay_tb_image *image);
/**
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
- BLANK
- NONE
- CLAY_TB_IMAGE_MODE
- DEFAULT
- PLACEHOLDER
- BG
- ASCII_FG
- ASCII
- UNICODE
- ASCII_FG_FAST
- ASCII_FAST
- UNICODE_FAST
- 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 image_mode Method for drawing images
\param transparency Emulate transparency using background colors
*/
void Clay_Termbox_Initialize(int color_mode, enum border_mode border_mode,
enum border_chars border_chars, enum image_mode image_mode, bool 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);
/**
Convenience function to block until an event is received from termbox. If an image is only
partially rendered, this returns immediately.
*/
void Clay_Termbox_Waitfor_Event(void);
// -------------------------------------------------------------------------------------------------
// -- 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;
static enum image_mode clay_tb_image_mode = CLAY_TB_IMAGE_MODE_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;
clay_tb_cell_bounding_box clay_tb_scissor_box;
// Images may be drawn across multiple renders to improve responsiveness. The initial draw will be
// approximate, then further partial draws will replace characters with more accurate ones
static bool clay_tb_partial_image_drawn = false;
// Maximum fuel used per render across all images
static int clay_tb_image_fuel_max = 200 * 1024;
// Maximum fuel used per render per image
static int clay_tb_image_fuel_per_image = 100 * 1024;
// Fuel used this render
static int clay_tb_image_fuel_used = 0;
// -----------------------------------------------
// -- Color buffer
// Buffer storing background colors from previously drawn items. Used to emulate transparency and
// set the background color for text.
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: (%f, %f, %f, %f)", color.r,
color.g, color.b, color.a);
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 clay_tb_cell_bounding_box cell_snap_bounding_box(Clay_BoundingBox box)
{
return (clay_tb_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),
};
}
/**
Snap pixel values from Clay to nearest cell values without considering x and y position when
calculating width/height.
Width/height ignores 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=1.
\param box Bounding box with pixel measurements to convert
*/
static inline clay_tb_cell_bounding_box cell_snap_pos_ind_bounding_box(Clay_BoundingBox box)
{
return (clay_tb_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.width / clay_tb_cell_size.width),
.height = clay_tb_roundf(box.height / clay_tb_cell_size.height),
};
}
/**
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(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(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) {
Clay_Color *tmp_clay = tb_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 clay_tb_color_pair clay_tb_get_transparency_color(
int x, int y, clay_tb_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 (clay_tb_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_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;
}
/**
Convert a pixel-based image to a cell-based image of the specified width and height. Stores the
converted/resized result in the cache of the input image.
If the image has not changed size or image mode since the last convert it is returned unchanged
\param image Image to convert/resize
\param width Target width in cells for the converted image
\param height Target height in cells for the converted image
*/
bool clay_tb_image_convert(clay_tb_image *image, int width, int height)
{
clay_tb_assert(NULL != image->pixel_data, "Image must be loaded");
bool image_unchanged = (width == image->internal.width && height == image->internal.height
&& (clay_tb_image_mode == image->internal.last_image_mode));
if (image_unchanged && !image->internal.partial_render.in_progress) {
return true;
}
if (!image_unchanged) {
free(image->internal.partial_render.resized_pixel_data);
image->internal.partial_render = (struct clay_tb_partial_render) {
.in_progress = false,
.resized_pixel_data = NULL,
.cursor_x = 0,
.cursor_y = 0,
.cursor_mask = 0,
.min_difference_squared_sum = INT_MAX,
.best_mask = 0,
.best_foreground = { 0, 0, 0, 0 },
.best_background = { 0, 0, 0, 0 }
};
}
const size_t size = (size_t)width * height;
// Allocate/resize internal cache data
if (size > image->internal.size_max) {
uint32_t *tmp_characters = realloc(image->internal.characters, size * sizeof(uint32_t));
Clay_Color *tmp_foreground = realloc(image->internal.foreground, size * sizeof(Clay_Color));
Clay_Color *tmp_background = realloc(image->internal.background, size * sizeof(Clay_Color));
if (NULL == tmp_characters || NULL == tmp_foreground || NULL == tmp_background) {
image->internal.size_max = 0;
free(tmp_characters);
free(tmp_foreground);
free(tmp_background);
image->internal.characters = NULL;
image->internal.foreground = NULL;
image->internal.background = NULL;
return false;
}
image->internal.characters = tmp_characters;
image->internal.foreground = tmp_foreground;
image->internal.background = tmp_background;
image->internal.size_max = size;
}
image->internal.width = width;
image->internal.height = height;
// Resize image using the same width/height in cells, but with the pixel sizes of the character
// masks instead of the cell size. The pixel data for each character mask will be compared to
// the pixel data of a small section of the image under the mask. The closest mask to the image
// data is chosen as the character to draw.
const int character_mask_pixel_width = 6;
const int character_mask_pixel_height = 12;
const int pixel_width = width * character_mask_pixel_width;
const int pixel_height = height * character_mask_pixel_height;
unsigned char *resized_pixel_data;
if (image->internal.partial_render.in_progress) {
resized_pixel_data = image->internal.partial_render.resized_pixel_data;
} else {
resized_pixel_data = stbir_resize_uint8_linear(image->pixel_data, image->pixel_width,
image->pixel_height, 0, NULL, pixel_width, pixel_height, 0, STBIR_RGB);
image->internal.partial_render.resized_pixel_data = resized_pixel_data;
}
int num_character_masks = 1;
const clay_tb_character_mask *character_masks = NULL;
switch (clay_tb_image_mode) {
case CLAY_TB_IMAGE_MODE_BG: {
num_character_masks = 1;
character_masks = &clay_tb_image_shapes_ascii_fast[0];
break;
}
case CLAY_TB_IMAGE_MODE_ASCII:
case CLAY_TB_IMAGE_MODE_ASCII_FG: {
num_character_masks = CLAY_TB_IMAGE_SHAPES_ASCII_BEST_COUNT;
character_masks = &clay_tb_image_shapes_ascii_best[0];
break;
}
case CLAY_TB_IMAGE_MODE_UNICODE: {
num_character_masks = CLAY_TB_IMAGE_SHAPES_UNICODE_BEST_COUNT;
character_masks = &clay_tb_image_shapes_unicode_best[0];
break;
}
case CLAY_TB_IMAGE_MODE_ASCII_FAST:
case CLAY_TB_IMAGE_MODE_ASCII_FG_FAST: {
num_character_masks = CLAY_TB_IMAGE_SHAPES_ASCII_FAST_COUNT;
character_masks = &clay_tb_image_shapes_ascii_fast[0];
break;
}
case CLAY_TB_IMAGE_MODE_UNICODE_FAST: {
num_character_masks = CLAY_TB_IMAGE_SHAPES_UNICODE_FAST_COUNT;
character_masks = &clay_tb_image_shapes_unicode_fast[0];
break;
}
};
// The number of character masks to check before exiting the render for this step
// Used to improve responsiveness by splitting renders across multiple frames
const int fuel_amount_initial
= CLAY__MIN(clay_tb_image_fuel_per_image, clay_tb_image_fuel_max - clay_tb_image_fuel_used);
int fuel_remaining = fuel_amount_initial;
bool partial_character_render = false;
// Do a quick initial render to set the background
if (!image->internal.partial_render.in_progress) {
image->internal.last_image_mode = clay_tb_image_mode;
for (int y = image->internal.partial_render.cursor_y; y < height; ++y) {
for (int x = image->internal.partial_render.cursor_x; x < width; ++x) {
const int cell_top_left_pixel_x = x * character_mask_pixel_width;
const int cell_top_left_pixel_y = y * character_mask_pixel_height;
const int image_index = 3
* (((cell_top_left_pixel_y + character_mask_pixel_height / 2) * pixel_width)
+ (cell_top_left_pixel_x + character_mask_pixel_width / 2));
Clay_Color pixel_color = {
(float)resized_pixel_data[image_index],
(float)resized_pixel_data[image_index + 1],
(float)resized_pixel_data[image_index + 2],
};
const int cell_index = y * width + x;
image->internal.characters[cell_index] = '.';
image->internal.foreground[cell_index] = pixel_color;
image->internal.background[cell_index] = pixel_color;
fuel_remaining = CLAY__MAX(0, fuel_remaining - 1);
}
}
}
if (0 == fuel_remaining) {
image->internal.partial_render.in_progress = true;
clay_tb_partial_image_drawn = true;
goto done;
}
for (int y = image->internal.partial_render.cursor_y; y < height; ++y) {
for (int x = image->internal.partial_render.cursor_x; x < width; ++x) {
const int cell_top_left_pixel_x = x * character_mask_pixel_width;
const int cell_top_left_pixel_y = y * character_mask_pixel_height;
// For each possible cell character, use the mask to find the average color for the
// foreground ('1's) and background ('0's).
int min_difference_squared_sum
= image->internal.partial_render.min_difference_squared_sum;
int best_mask = image->internal.partial_render.best_mask;
Clay_Color best_foreground = image->internal.partial_render.best_foreground;
Clay_Color best_background = image->internal.partial_render.best_background;
for (int i = image->internal.partial_render.cursor_mask; i < num_character_masks; ++i) {
int color_avg_background_r = 0;
int color_avg_background_g = 0;
int color_avg_background_b = 0;
int color_avg_foreground_r = 0;
int color_avg_foreground_g = 0;
int color_avg_foreground_b = 0;
int foreground_count = 0;
int background_count = 0;
for (int cell_pixel_y = 0; cell_pixel_y < character_mask_pixel_height;
++cell_pixel_y) {
for (int cell_pixel_x = 0; cell_pixel_x < character_mask_pixel_width;
++cell_pixel_x) {
const int index = 3
* (((cell_top_left_pixel_y + cell_pixel_y) * pixel_width)
+ (cell_top_left_pixel_x + cell_pixel_x));
const int mask_index
= (cell_pixel_y * character_mask_pixel_width) + cell_pixel_x;
if (0 == character_masks[i].data[mask_index]) {
if (CLAY_TB_IMAGE_MODE_ASCII_FG != clay_tb_image_mode
&& CLAY_TB_IMAGE_MODE_ASCII_FG_FAST != clay_tb_image_mode) {
color_avg_background_r += resized_pixel_data[index];
color_avg_background_g += resized_pixel_data[index + 1];
color_avg_background_b += resized_pixel_data[index + 2];
background_count += 1;
}
} else {
color_avg_foreground_r += resized_pixel_data[index];
color_avg_foreground_g += resized_pixel_data[index + 1];
color_avg_foreground_b += resized_pixel_data[index + 2];
foreground_count += 1;
}
}
}
if (CLAY_TB_IMAGE_MODE_ASCII_FG != clay_tb_image_mode
&& CLAY_TB_IMAGE_MODE_ASCII_FG_FAST != clay_tb_image_mode) {
color_avg_background_r /= CLAY__MAX(1, background_count);
color_avg_background_g /= CLAY__MAX(1, background_count);
color_avg_background_b /= CLAY__MAX(1, background_count);
} else {
color_avg_background_r = 0;
color_avg_background_g = 0;
color_avg_background_b = 0;
}
color_avg_foreground_r /= CLAY__MAX(1, foreground_count);
color_avg_foreground_g /= CLAY__MAX(1, foreground_count);
color_avg_foreground_b /= CLAY__MAX(1, foreground_count);
// Determine the difference between the mask with colors and the actual pixel data
int difference_squared_sum = 0;
for (int cell_pixel_y = 0; cell_pixel_y < character_mask_pixel_height;
++cell_pixel_y) {
for (int cell_pixel_x = 0; cell_pixel_x < character_mask_pixel_width;
++cell_pixel_x) {
const int index = 3
* (((cell_top_left_pixel_y + cell_pixel_y) * pixel_width)
+ (cell_top_left_pixel_x + cell_pixel_x));
int rdiff, gdiff, bdiff, adiff;
const int mask_index
= (cell_pixel_y * character_mask_pixel_width) + cell_pixel_x;
if (0 == character_masks[i].data[mask_index]) {
rdiff = (color_avg_background_r - resized_pixel_data[index]);
gdiff = (color_avg_background_g - resized_pixel_data[index + 1]);
bdiff = (color_avg_background_b - resized_pixel_data[index + 2]);
} else {
rdiff = (color_avg_foreground_r - resized_pixel_data[index]);
gdiff = (color_avg_foreground_g - resized_pixel_data[index + 1]);
bdiff = (color_avg_foreground_b - resized_pixel_data[index + 2]);
}
difference_squared_sum += (
(rdiff * rdiff) +
(gdiff * gdiff) +
(bdiff * bdiff));
}
}
// Choose the closest character mask to the image data
if (difference_squared_sum < min_difference_squared_sum) {
min_difference_squared_sum = difference_squared_sum;
best_mask = i;
best_background = (Clay_Color) {
.r = (float)color_avg_background_r,
.g = (float)color_avg_background_g,
.b = (float)color_avg_background_b,
.a = 255
};
best_foreground = (Clay_Color) {
.r = (float)color_avg_foreground_r,
.g = (float)color_avg_foreground_g,
.b = (float)color_avg_foreground_b,
.a = 255
};
}
fuel_remaining -= 1;
if (0 == fuel_remaining) {
// Set progress for partial render
image->internal.partial_render = (struct clay_tb_partial_render) {
.in_progress = true,
.resized_pixel_data = resized_pixel_data,
.cursor_x = x,
.cursor_y = y,
.cursor_mask = i + 1,
.min_difference_squared_sum = min_difference_squared_sum,
.best_mask = best_mask,
.best_foreground = best_foreground,
.best_background = best_background
};
partial_character_render = true;
clay_tb_partial_image_drawn = true;
goto done;
}
}
image->internal.partial_render.cursor_mask = 0;
// Set data in cache for this character
const int index = y * width + x;
image->internal.characters[index] = character_masks[best_mask].character;
image->internal.foreground[index] = best_foreground;
image->internal.background[index] = best_background;
image->internal.partial_render = (struct clay_tb_partial_render) {
.in_progress = true,
.resized_pixel_data = resized_pixel_data,
.cursor_x = x + 1,
.cursor_y = y,
.cursor_mask = 0,
.min_difference_squared_sum = INT_MAX,
.best_mask = 0,
.best_foreground = { 0, 0, 0, 0 },
.best_background = { 0, 0, 0, 0 },
};
if (0 == fuel_remaining) {
clay_tb_partial_image_drawn = true;
goto done;
}
}
image->internal.partial_render.cursor_x = 0;
}
image->internal.partial_render.cursor_y = 0;
image->internal.partial_render.in_progress = false;
free(resized_pixel_data);
image->internal.partial_render.resized_pixel_data = NULL;
done:
clay_tb_image_fuel_used += fuel_amount_initial - fuel_remaining;
return true;
}
// -------------------------------------------------------------------------------------------------
// -- 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_Image_Mode(enum image_mode image_mode)
{
clay_tb_assert(CLAY_TB_IMAGE_MODE_DEFAULT <= image_mode
&& image_mode <= CLAY_TB_IMAGE_MODE_UNICODE_FAST,
"Image mode invalid (%d)", image_mode);
if (CLAY_TB_IMAGE_MODE_DEFAULT == image_mode) {
clay_tb_image_mode = CLAY_TB_IMAGE_MODE_UNICODE;
} else {
clay_tb_image_mode = image_mode;
}
}
void Clay_Termbox_Set_Image_Fuel(int fuel_max, int fuel_per_image)
{
clay_tb_assert(0 < fuel_max && 0 < fuel_per_image,
"Fuel must be positive (%d, %d)", fuel_max, fuel_per_image);
clay_tb_image_fuel_max = fuel_max;
clay_tb_image_fuel_per_image = fuel_per_image;
}
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;
}
}
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
};
}
clay_tb_image Clay_Termbox_Image_Load_File(const char *filename)
{
clay_tb_assert(NULL != filename, "Filename cannot be null");
clay_tb_image rv = { 0 };
FILE *image_file = NULL;
image_file = fopen(filename, "r");
if (NULL == image_file) {
fprintf(stderr, "Failed to open image %s: %s\n", filename, strerror(errno));
return rv;
}
int channels_in_file;
const int desired_color_channels = 3;
rv.pixel_data = stbi_load_from_file(
image_file, &rv.pixel_width, &rv.pixel_height, &channels_in_file, desired_color_channels);
fclose(image_file);
return rv;
}
clay_tb_image Clay_Termbox_Image_Load_Memory(const void *image, int size)
{
clay_tb_assert(NULL != image, "Image cannot be null");
clay_tb_assert(0 < size, "Image size must be > 0");
clay_tb_image rv = { 0 };
int channels_in_file;
const int desired_color_channels = 3;
rv.pixel_data = stbi_load_from_memory(
image, size, &rv.pixel_width, &rv.pixel_height, &channels_in_file, desired_color_channels);
return rv;
}
void Clay_Termbox_Image_Free(clay_tb_image *image)
{
free(image->pixel_data);
free(image->internal.partial_render.resized_pixel_data);
free(image->internal.characters);
free(image->internal.foreground);
free(image->internal.background);
*image = (clay_tb_image) { 0 };
}
void Clay_Termbox_Initialize(int color_mode, enum border_mode border_mode,
enum border_chars border_chars, enum image_mode image_mode, bool transparency)
{
int new_color_mode = color_mode;
int new_border_mode = border_mode;
int new_border_chars = border_chars;
int new_image_mode = image_mode;
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("BLANK", env_border_chars)) {
new_border_chars = CLAY_TB_BORDER_CHARS_BLANK;
} else if (0 == strcmp("NONE", env_border_chars)) {
new_border_chars = CLAY_TB_BORDER_CHARS_NONE;
}
}
const char *env_image_mode = getenv("CLAY_TB_IMAGE_MODE");
if (NULL != env_image_mode) {
if (0 == strcmp("DEFAULT", env_image_mode)) {
new_image_mode = CLAY_TB_IMAGE_MODE_DEFAULT;
} else if (0 == strcmp("PLACEHOLDER", env_image_mode)) {
new_image_mode = CLAY_TB_IMAGE_MODE_PLACEHOLDER;
} else if (0 == strcmp("BG", env_image_mode)) {
new_image_mode = CLAY_TB_IMAGE_MODE_BG;
} else if (0 == strcmp("ASCII_FG", env_image_mode)) {
new_image_mode = CLAY_TB_IMAGE_MODE_ASCII_FG;
} else if (0 == strcmp("ASCII", env_image_mode)) {
new_image_mode = CLAY_TB_IMAGE_MODE_ASCII;
} else if (0 == strcmp("UNICODE", env_image_mode)) {
new_image_mode = CLAY_TB_IMAGE_MODE_UNICODE;
} else if (0 == strcmp("ASCII_FG_FAST", env_image_mode)) {
new_image_mode = CLAY_TB_IMAGE_MODE_ASCII_FG_FAST;
} else if (0 == strcmp("ASCII_FAST", env_image_mode)) {
new_image_mode = CLAY_TB_IMAGE_MODE_ASCII_FAST;
} else if (0 == strcmp("UNICODE_FAST", env_image_mode)) {
new_image_mode = CLAY_TB_IMAGE_MODE_UNICODE_FAST;
}
}
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_Image_Mode(new_image_mode);
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_clay = tb_malloc(sizeof(Clay_Color) * size);
for (int i = 0; i < size; ++i) {
clay_tb_color_buffer_clay[i] = (Clay_Color) { 0, 0, 0, 0 };
}
}
void Clay_Termbox_Close(void)
{
if (clay_tb_initialized) {
// Disable mouse hover support
tb_sendf("\x1b[?%d;%dl", 1003, 1006);
tb_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();
clay_tb_partial_image_drawn = false;
clay_tb_image_fuel_used = 0;
for (int32_t i = 0; i < commands.length; ++i) {
const Clay_RenderCommand *command = Clay_RenderCommandArray_Get(&commands, i);
const clay_tb_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) {
clay_tb_color_pair color_bg_new = clay_tb_get_transparency_color(
x, y, (clay_tb_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;
}
case CLAY_TB_BORDER_CHARS_BLANK: {
ch = ' ';
break;
}
}
clay_tb_color_pair color_bg_new = clay_tb_get_transparency_color(
x, y, (clay_tb_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_transparency)
? TB_DEFAULT
: clay_tb_color_convert(clay_tb_color_buffer_clay_get(x, y));
Clay_Color color_bg = { 0 };
clay_tb_color_pair color_bg_new = clay_tb_get_transparency_color(
x, y, (clay_tb_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);
}
bool use_placeholder = true;
clay_tb_image *image = (clay_tb_image *)render_data.imageData;
if (!(CLAY_TB_IMAGE_MODE_PLACEHOLDER == clay_tb_image_mode
|| CLAY_TB_OUTPUT_NOCOLOR == clay_tb_color_mode)) {
bool convert_success = (NULL != image)
? clay_tb_image_convert(image, cell_box.width, cell_box.height)
: false;
if (convert_success) {
use_placeholder = false;
}
}
if (!use_placeholder) {
// Render image
for (int y = box_begin_y; y < box_end_y; ++y) {
int y_offset = y - cell_box.y;
for (int x = box_begin_x; x < box_end_x; ++x) {
int x_offset = x - cell_box.x;
// Fetch cells from the image's cache
if (!color_specified) {
if (CLAY_TB_IMAGE_MODE_ASCII_FG == clay_tb_image_mode
|| CLAY_TB_IMAGE_MODE_ASCII_FG_FAST == clay_tb_image_mode) {
color_bg = (Clay_Color) { 0, 0, 0, 0 };
color_tb_bg = TB_DEFAULT;
} else {
color_bg
= image->internal
.background[y_offset * cell_box.width + x_offset];
color_tb_bg = clay_tb_color_convert(color_bg);
}
}
color_tb_fg = clay_tb_color_convert(
image->internal.foreground[y_offset * cell_box.width + x_offset]);
uint32_t ch
= image->internal.characters[y_offset * cell_box.width + x_offset];
if (CLAY_TB_IMAGE_MODE_BG == clay_tb_image_mode) {
ch = ' ';
}
clay_tb_set_cell(x, y, ch, color_tb_fg, color_tb_bg, color_bg);
}
}
} else {
// Render a placeholder pattern
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 = (clay_tb_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;
}
}
}
}
void Clay_Termbox_Waitfor_Event(void)
{
if (clay_tb_partial_image_drawn) {
return;
}
int termbox_ttyfd, termbox_resizefd;
tb_get_fds(&termbox_ttyfd, &termbox_resizefd);
int nfds = CLAY__MAX(termbox_ttyfd, termbox_resizefd) + 1;
fd_set monitor_set;
FD_ZERO(&monitor_set);
FD_SET(termbox_ttyfd, &monitor_set);
FD_SET(termbox_resizefd, &monitor_set);
select(nfds, &monitor_set, NULL, NULL, NULL);
}