// Copyright (c) 2024 Justin Andreas Lacoste (@27justin) // // 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. // // SPDX-License-Identifier: Zlib #include #include #include #include // TODO: Regarding image support, currently this renderer only // supports PNG images, this is due to cairo having just PNG as it's // main file format. We maybe should introduce stb_image to load them // as bitmaps and feed cairo that way. #define CLAY_EXTEND_CONFIG_IMAGE Clay_String path; // Filesystem path // TODO: We should use the given `uint16_t fontId` instead of doing this. #define CLAY_EXTEND_CONFIG_TEXT Clay_String fontFamily; // Font family #define CLAY_IMPLEMENTATION #include "../../clay.h" #include //////////////////////////////// // // Public API // // Initialize the internal cairo pointer with the user provided instance. // This is REQUIRED before calling Clay_Cairo_Render. void Clay_Cairo_Initialize(cairo_t *cairo); // Render the command queue to the `cairo_t*` instance you called // `Clay_Cairo_Initialize` on. void Clay_Cairo_Render(Clay_RenderCommandArray commands); //////////////////////////////// //////////////////////////////// // Convencience macros // #define CLAY_TO_CAIRO(color) color.r / 255.0, color.g / 255.0, color.b / 255.0, color.a / 255.0 #define DEG2RAD(degrees) (degrees * ( M_PI / 180.0 ) ) //////////////////////////////// //////////////////////////////// // Implementation // // Cairo instance static cairo_t *Clay__Cairo = NULL; // Return a null-terminated copy of Clay_String `str`. // Callee is required to free. static inline char *Clay_Cairo__NullTerminate(Clay_String *str) { char *copy = (char*) malloc(str->length + 1); if (!copy) { fprintf(stderr, "Memory allocation failed\n"); return NULL; } memcpy(copy, str->chars, str->length); copy[str->length] = '\0'; return copy; } // Measure text using cairo's *toy* text API. static inline Clay_Dimensions Clay_Cairo_MeasureText(Clay_String *str, Clay_TextElementConfig *config) { // Edge case: Clay computes the width of a whitespace character // once. Cairo does not factor in whitespaces when computing text // extents, this edge-case serves as a short-circuit to introduce // (somewhat) sensible values into Clay. if(str->length == 1 && str->chars[0] == ' ') { cairo_text_extents_t te; cairo_text_extents(Clay__Cairo, " ", &te); return (Clay_Dimensions) { // The multiplication here follows no real logic, just // brute-forcing it until the text boundaries look // okay-ish. You should probably rather use a proper text // shaping engine like HarfBuzz or Pango. .width = ((float) te.x_advance) * 1.9f, .height = (float) config->fontSize }; } // Ensure string is null-terminated for Cairo char *text = Clay_Cairo__NullTerminate(str); char *font_family = Clay_Cairo__NullTerminate(&config->fontFamily); // Save and reset the Cairo context to avoid unwanted transformations cairo_save(Clay__Cairo); cairo_identity_matrix(Clay__Cairo); // Set font properties cairo_select_font_face(Clay__Cairo, font_family, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); cairo_set_font_size(Clay__Cairo, config->fontSize); // Use glyph extents for better precision cairo_scaled_font_t *scaled_font = cairo_get_scaled_font(Clay__Cairo); if (!scaled_font) { fprintf(stderr, "Failed to get scaled font\n"); cairo_restore(Clay__Cairo); free(text); free(font_family); return (Clay_Dimensions){0, 0}; } cairo_glyph_t *glyphs = NULL; int num_glyphs = 0; cairo_status_t status = cairo_scaled_font_text_to_glyphs( scaled_font, 0, 0, text, -1, &glyphs, &num_glyphs, NULL, NULL, NULL ); if (status != CAIRO_STATUS_SUCCESS || !glyphs || num_glyphs == 0) { fprintf(stderr, "Failed to generate glyphs: %s\n", cairo_status_to_string(status)); cairo_restore(Clay__Cairo); free(text); free(font_family); return (Clay_Dimensions){0, 0}; } // Measure the glyph extents cairo_text_extents_t glyph_extents; cairo_glyph_extents(Clay__Cairo, glyphs, num_glyphs, &glyph_extents); // Clean up glyphs cairo_glyph_free(glyphs); // Restore the Cairo context cairo_restore(Clay__Cairo); // Free temporary strings free(text); free(font_family); // Return dimensions return (Clay_Dimensions){ .width = (float) glyph_extents.width, .height = (float) glyph_extents.height }; } void Clay_Cairo_Initialize(cairo_t *cairo) { Clay__Cairo = cairo; } // Internally used to copy images onto our document/active workspace. void Clay_Cairo__Blit_Surface(cairo_surface_t *src_surface, cairo_surface_t *dest_surface, double x, double y, double scale_x, double scale_y) { // Create a cairo context for the destination surface cairo_t *cr = cairo_create(dest_surface); // Save the context's state cairo_save(cr); // Apply translation to position the source at (x, y) cairo_translate(cr, x, y); // Apply scaling to the context cairo_scale(cr, scale_x, scale_y); // Set the source surface at (0, 0) after applying transformations cairo_set_source_surface(cr, src_surface, 0, 0); // Paint the scaled source surface onto the destination surface cairo_paint(cr); // Restore the context's state to remove transformations cairo_restore(cr); // Clean up cairo_destroy(cr); } void Clay_Cairo_Render(Clay_RenderCommandArray commands) { cairo_t *cr = Clay__Cairo; for(size_t i = 0; i < commands.length; i++) { Clay_RenderCommand *command = Clay_RenderCommandArray_Get(&commands, i); switch(command->commandType) { case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: { Clay_RectangleElementConfig *config = command->config.rectangleElementConfig; Clay_Color color = config->color; Clay_BoundingBox bb = command->boundingBox; cairo_set_source_rgba(cr, CLAY_TO_CAIRO(color)); cairo_new_sub_path(cr); cairo_arc(cr, bb.x + config->cornerRadius.topLeft, bb.y + config->cornerRadius.topLeft, config->cornerRadius.topLeft, M_PI, 3 * M_PI / 2); // 180° to 270° cairo_arc(cr, bb.x + bb.width - config->cornerRadius.topRight, bb.y + config->cornerRadius.topRight, config->cornerRadius.topRight, 3 * M_PI / 2, 2 * M_PI); // 270° to 360° cairo_arc(cr, bb.x + bb.width - config->cornerRadius.bottomRight, bb.y + bb.height - config->cornerRadius.bottomRight, config->cornerRadius.bottomRight, 0, M_PI / 2); // 0° to 90° cairo_arc(cr, bb.x + config->cornerRadius.bottomLeft, bb.y + bb.height - config->cornerRadius.bottomLeft, config->cornerRadius.bottomLeft, M_PI / 2, M_PI); // 90° to 180° cairo_close_path(cr); cairo_fill(cr); break; } case CLAY_RENDER_COMMAND_TYPE_TEXT: { // Cairo expects null terminated strings, we need to clone // to temporarily introduce one. char *text = Clay_Cairo__NullTerminate(&command->text); char *font_family = Clay_Cairo__NullTerminate(&command->config.textElementConfig->fontFamily); Clay_BoundingBox bb = command->boundingBox; Clay_Color color = command->config.textElementConfig->textColor; cairo_select_font_face(Clay__Cairo, font_family, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); cairo_set_font_size(cr, command->config.textElementConfig->fontSize); cairo_move_to(cr, bb.x, bb.y + bb.height); cairo_set_source_rgba(cr, CLAY_TO_CAIRO(color)); cairo_show_text(cr, text); cairo_close_path(cr); free(text); free(font_family); break; } case CLAY_RENDER_COMMAND_TYPE_BORDER: { Clay_BorderElementConfig *config = command->config.borderElementConfig; Clay_BoundingBox bb = command->boundingBox; double top_left_radius = config->cornerRadius.topLeft / 2.0; double top_right_radius = config->cornerRadius.topRight / 2.0; double bottom_right_radius = config->cornerRadius.bottomRight / 2.0; double bottom_left_radius = config->cornerRadius.bottomLeft / 2.0; // Draw the top border if (config->top.width > 0) { cairo_set_line_width(cr, config->top.width); cairo_set_source_rgba(cr, CLAY_TO_CAIRO(config->top.color)); cairo_new_sub_path(cr); // Left half-arc for top-left corner cairo_arc(cr, bb.x + top_left_radius, bb.y + top_left_radius, top_left_radius, DEG2RAD(225), DEG2RAD(270)); // Line to right half-arc cairo_line_to(cr, bb.x + bb.width - top_right_radius, bb.y); // Right half-arc for top-right corner cairo_arc(cr, bb.x + bb.width - top_right_radius, bb.y + top_right_radius, top_right_radius, DEG2RAD(270), DEG2RAD(305)); cairo_stroke(cr); } // Draw the right border if (config->right.width > 0) { cairo_set_line_width(cr, config->right.width); cairo_set_source_rgba(cr, CLAY_TO_CAIRO(config->right.color)); cairo_new_sub_path(cr); // Top half-arc for top-right corner cairo_arc(cr, bb.x + bb.width - top_right_radius, bb.y + top_right_radius, top_right_radius, DEG2RAD(305), DEG2RAD(350)); // Line to bottom half-arc cairo_line_to(cr, bb.x + bb.width, bb.y + bb.height - bottom_right_radius); // Bottom half-arc for bottom-right corner cairo_arc(cr, bb.x + bb.width - bottom_right_radius, bb.y + bb.height - bottom_right_radius, bottom_right_radius, DEG2RAD(0), DEG2RAD(45)); cairo_stroke(cr); } // Draw the bottom border if (config->bottom.width > 0) { cairo_set_line_width(cr, config->bottom.width); cairo_set_source_rgba(cr, CLAY_TO_CAIRO(config->bottom.color)); cairo_new_sub_path(cr); // Right half-arc for bottom-right corner cairo_arc(cr, bb.x + bb.width - bottom_right_radius, bb.y + bb.height - bottom_right_radius, bottom_right_radius, DEG2RAD(45), DEG2RAD(90)); // Line to left half-arc cairo_line_to(cr, bb.x + bottom_left_radius, bb.y + bb.height); // Left half-arc for bottom-left corner cairo_arc(cr, bb.x + bottom_left_radius, bb.y + bb.height - bottom_left_radius, bottom_left_radius, DEG2RAD(90), DEG2RAD(135)); cairo_stroke(cr); } // Draw the left border if (config->left.width > 0) { cairo_set_line_width(cr, config->left.width); cairo_set_source_rgba(cr, CLAY_TO_CAIRO(config->left.color)); cairo_new_sub_path(cr); // Bottom half-arc for bottom-left corner cairo_arc(cr, bb.x + bottom_left_radius, bb.y + bb.height - bottom_left_radius, bottom_left_radius, DEG2RAD(135), DEG2RAD(180)); // Line to top half-arc cairo_line_to(cr, bb.x, bb.y + top_left_radius); // Top half-arc for top-left corner cairo_arc(cr, bb.x + top_left_radius, bb.y + top_left_radius, top_left_radius, DEG2RAD(180), DEG2RAD(225)); cairo_stroke(cr); } break; } case CLAY_RENDER_COMMAND_TYPE_IMAGE: { Clay_ImageElementConfig *config = command->config.imageElementConfig; Clay_BoundingBox bb = command->boundingBox; char *path = Clay_Cairo__NullTerminate(&config->path); cairo_surface_t *surf = cairo_image_surface_create_from_png(path), *origin = cairo_get_target(cr); // Calculate the original image dimensions double image_w = cairo_image_surface_get_width(surf), image_h = cairo_image_surface_get_height(surf); // Calculate the scaling factor to fit within the bounding box while preserving aspect ratio double scale_w = bb.width / image_w; double scale_h = bb.height / image_h; double scale = (scale_w < scale_h) ? scale_w : scale_h; // Use the smaller scaling factor // Apply the same scale to both dimensions to preserve aspect ratio double scale_x = scale; double scale_y = scale; // Calculate the scaled image dimensions double scaled_w = image_w * scale_x; double scaled_h = image_h * scale_y; // Adjust the x and y coordinates to center the scaled image within the bounding box double centered_x = bb.x + (bb.width - scaled_w) / 2.0; double centered_y = bb.y + (bb.height - scaled_h) / 2.0; // Blit the scaled and centered image Clay_Cairo__Blit_Surface(surf, origin, centered_x, centered_y, scale_x, scale_y); // Clean up the source surface cairo_surface_destroy(surf); free(path); break; } case CLAY_RENDER_COMMAND_TYPE_CUSTOM: { // Slot your custom elements in here. } default: { fprintf(stderr, "Unknown command type %d\n", (int) command->commandType); } } } }