#ifndef SOKOL_CLAY_INCLUDED #define SOKOL_CLAY_INCLUDED (1) /* sokol_clay.h -- drop-in Clay renderer for sokol_gfx.h Do this: #define SOKOL_CLAY_IMPL before you include this file in *one* C file to create the implementation. Optionally provide the following configuration define both before including the the declaration and implementation: SOKOL_CLAY_NO_SOKOL_APP - don't depend on sokol_app.h (see below for details) Include the following headers before sokol_clay.h (both before including the declaration and implementation): sokol_gl.h sokol_fontstash.h sokol_app.h (except SOKOL_CLAY_NO_SOKOL_APP) clay.h FEATURE OVERVIEW: ================= sokol_clay.h implements the rendering and event-handling code for Clay (https://github.com/nicbarker/clay) on top of sokol_gl.h and (optionally) sokol_app.h. Since sokol_fontstash.h already depends on sokol_gl.h, the rendering is implemented using sokol_gl calls. (TODO: make fontstash optional?) The sokol_app.h dependency is optional and used for input event handling. If you only use sokol_gfx.h but not sokol_app.h in your application, define SOKOL_CLAY_NO_SOKOL_APP before including the implementation of sokol_clay.h, this will remove any dependency to sokol_app.h, but you must call sclay_set_layout_dimensions and handle input yourself. sokol_clay.h is not thread-safe, all calls must be made from the same thread where sokol_gfx.h is running. HOWTO: ====== --- To initialize sokol-clay, call sclay_setup(). This can be done before or after Clay_Initialize. --- Create an array of sclay_font_t and fill it by calling one of: sclay_font_t sclay_add_font(const char *filename); sclay_font_t sclay_add_font_mem(unsigned char *data, int dataLen); The fontId value in Clay corresponds to indices in this array. After calling Clay_Initialize but before calling any layout code, do this: Clay_SetMeasureTextFunction(sclay_measure_text, &fonts); where `fonts` is the abovementioned array. --- At the start of a frame, call sclay_new_frame() if you're using sokol_app.h. If you're not using sokol_app.h, call: void sclay_set_layout_dimensions(Clay_Dimensions size, float dpi_scale); at the start of the frame (or just when the window is resized.) Either way, do some layout, then at the end of the frame call sclay_render: sg_begin_pass(...) // other rendering... sclay_render(renderCommands, &fonts); // other rendering... sgl_draw(); sg_end_pass(); sg_commit(); One caveat: sclay_render assumes the default gl view matrix, and handles scaling automatically. If you've adjusted the view matrix, remember to first call: sgl_matrix_mode_modelview(); sgl_load_identity(); before calling sclay_render. --- if you're using sokol_app.h, from inside the sokol_app.h event callback, call: void sclay_handle_event(const sapp_event* ev); Unfortunately Clay does not currently provide feedback on whether a mouse click was handled or not. --- finally, on application shutdown, call sclay_shutdown() */ #if !defined(SOKOL_CLAY_NO_SOKOL_APP) && !defined(SOKOL_APP_INCLUDED) #error "Please include sokol_app.h before sokol_clay.h (or define SOKOL_CLAY_NO_SOKOL_APP)" #endif typedef int sclay_font_t; void sclay_setup(); void sclay_shutdown(); sclay_font_t sclay_add_font(const char *filename); sclay_font_t sclay_add_font_mem(unsigned char *data, int dataLen); Clay_Dimensions sclay_measure_text(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData); #ifndef SOKOL_CLAY_NO_SOKOL_APP void sclay_new_frame(); void sclay_handle_event(const sapp_event *ev); #endif /* SOKOL_CLAY_NO_SOKOL_APP */ /* Use this if you don't call sclay_new_frame. `size` is the "virtual" size which * your layout is relative to (ie. the actual framebuffer size divided by dpi_scale.) * Set dpi_scale to 1 if you're not using high-dpi support. */ void sclay_set_layout_dimensions(Clay_Dimensions size, float dpi_scale); void sclay_render(Clay_RenderCommandArray renderCommands, sclay_font_t *fonts); #endif /* SOKOL_CLAY_INCLUDED */ #ifdef SOKOL_CLAY_IMPL #define SOKOL_CLAY_IMPL_INCLUDED (1) #ifndef SOKOL_GL_INCLUDED #error "Please include sokol_gl.h before sokol_clay.h" #endif #ifndef SOKOL_FONTSTASH_INCLUDED #error "Please include sokol_fontstash.h before sokol_clay.h" #endif #ifndef CLAY_HEADER #error "Please include clay.h before sokol_clay.h" #endif typedef struct { sgl_pipeline pip; #ifndef SOKOL_CLAY_NO_SOKOL_APP Clay_Vector2 mouse_pos, scroll; bool mouse_down; #endif Clay_Dimensions size; float dpi_scale; FONScontext *fonts; } _sclay_state_t; static _sclay_state_t _sclay; void sclay_setup() { _sclay.pip = sgl_make_pipeline(&(sg_pipeline_desc){ .colors[0] = { .blend = { .enabled = true, .src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA, .dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, }, } }); #ifndef SOKOL_CLAY_NO_SOKOL_APP _sclay.mouse_pos = (Clay_Vector2){0, 0}; _sclay.scroll = (Clay_Vector2){0, 0}; _sclay.mouse_down = false; #endif _sclay.size = (Clay_Dimensions){1, 1}; _sclay.dpi_scale = 1; _sclay.fonts = sfons_create(&(sfons_desc_t){ 0 }); //TODO clay error handler? } void sclay_shutdown() { sgl_destroy_pipeline(_sclay.pip); sfons_destroy(_sclay.fonts); } #ifndef SOKOL_CLAY_NO_SOKOL_APP void sclay_handle_event(const sapp_event* ev) { switch(ev->type){ case SAPP_EVENTTYPE_MOUSE_MOVE: _sclay.mouse_pos.x = ev->mouse_x / _sclay.dpi_scale; _sclay.mouse_pos.y = ev->mouse_y / _sclay.dpi_scale; break; case SAPP_EVENTTYPE_MOUSE_DOWN: _sclay.mouse_down = true; break; case SAPP_EVENTTYPE_MOUSE_UP: _sclay.mouse_down = false; break; case SAPP_EVENTTYPE_MOUSE_SCROLL: _sclay.scroll.x += ev->scroll_x; _sclay.scroll.y += ev->scroll_y; break; default: break; } } void sclay_new_frame() { sclay_set_layout_dimensions((Clay_Dimensions){ (float)sapp_width(), (float)sapp_height() }, sapp_dpi_scale()); Clay_SetPointerState(_sclay.mouse_pos, _sclay.mouse_down); Clay_UpdateScrollContainers(true, _sclay.scroll, sapp_frame_duration()); _sclay.scroll = (Clay_Vector2){0, 0}; } #endif /* SOKOL_CLAY_NO_SOKOL_APP */ void sclay_set_layout_dimensions(Clay_Dimensions size, float dpi_scale) { size.width /= dpi_scale; size.height /= dpi_scale; _sclay.size = size; if(_sclay.dpi_scale != dpi_scale){ _sclay.dpi_scale = dpi_scale; Clay_ResetMeasureTextCache(); } Clay_SetLayoutDimensions(size); } sclay_font_t sclay_add_font(const char *filename) { //TODO log something if we get FONS_INVALID return fonsAddFont(_sclay.fonts, "", filename); } sclay_font_t sclay_add_font_mem(unsigned char *data, int dataLen) { //TODO log something if we get FONS_INVALID return fonsAddFontMem(_sclay.fonts, "", data, dataLen, false); } Clay_Dimensions sclay_measure_text(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData) { sclay_font_t *fonts = (sclay_font_t *)userData; if(!fonts) return (Clay_Dimensions){ 0 }; fonsSetFont(_sclay.fonts, fonts[config->fontId]); fonsSetSize(_sclay.fonts, config->fontSize); fonsSetSpacing(_sclay.fonts, config->letterSpacing); float ascent, descent, lineh; fonsVertMetrics(_sclay.fonts, &ascent, &descent, &lineh); return (Clay_Dimensions) { .width = fonsTextBounds(_sclay.fonts, 0, 0, text.chars, text.chars + text.length, NULL), .height = ascent - descent }; } static void _draw_rect(float x, float y, float w, float h){ sgl_v2f(x, y); sgl_v2f(x, y); sgl_v2f(x+w, y); sgl_v2f(x, y+h); sgl_v2f(x+w, y+h); sgl_v2f(x+w, y+h); } static float _SIN[16] = { 0.000000f, 0.104528f, 0.207912f, 0.309017f, 0.406737f, 0.500000f, 0.587785f, 0.669131f, 0.743145f, 0.809017f, 0.866025f, 0.913545f, 0.951057f, 0.978148f, 0.994522f, 1.000000f, }; /* rx,ry = radius */ static void _draw_corner(float x, float y, float rx, float ry){ x -= rx; y -= ry; sgl_v2f(x, y); for(int i = 0; i < 16; ++i){ sgl_v2f(x, y); sgl_v2f(x+(rx*_SIN[15-i]), y+(ry*_SIN[i])); } sgl_v2f(x+(rx*_SIN[0]), y+(ry*_SIN[15])); } /* rx,ry = radius ix,iy = inner radius */ static void _draw_corner_border(float x, float y, float rx, float ry, float ix, float iy){ x -= rx; y -= ry; sgl_v2f(x+(ix*_SIN[15]), y+(iy*_SIN[0])); for(int i = 0; i < 16; ++i){ sgl_v2f(x+(ix*_SIN[15-i]), y+(iy*_SIN[i])); sgl_v2f(x+(rx*_SIN[15-i]), y+(ry*_SIN[i])); } sgl_v2f(x+(rx*_SIN[0]), y+(ry*_SIN[15])); } void sclay_render(Clay_RenderCommandArray renderCommands, sclay_font_t *fonts) { sgl_matrix_mode_modelview(); sgl_translate(-1.0f, 1.0f, 0.0f); sgl_scale(2.0f/_sclay.size.width, -2.0f/_sclay.size.height, 1.0f); sgl_disable_texture(); sgl_push_pipeline(); sgl_load_pipeline(_sclay.pip); for (uint32_t i = 0; i < renderCommands.length; i++) { Clay_RenderCommand *renderCommand = Clay_RenderCommandArray_Get(&renderCommands, i); Clay_BoundingBox bbox = renderCommand->boundingBox; switch (renderCommand->commandType) { case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: { Clay_RectangleRenderData *config = &renderCommand->renderData.rectangle; sgl_c4f(config->backgroundColor.r / 255.0f, config->backgroundColor.g / 255.0f, config->backgroundColor.b / 255.0f, config->backgroundColor.a / 255.0f); Clay_CornerRadius r = config->cornerRadius; sgl_begin_triangle_strip(); if(r.topLeft > 0 || r.topRight > 0){ _draw_corner(bbox.x, bbox.y, -r.topLeft, -r.topLeft); _draw_corner(bbox.x+bbox.width, bbox.y, r.topRight, -r.topRight); _draw_rect(bbox.x+r.topLeft, bbox.y, bbox.width-r.topLeft-r.topRight, CLAY__MAX(r.topLeft, r.topRight)); } if(r.bottomLeft > 0 || r.bottomRight > 0){ _draw_corner(bbox.x, bbox.y+bbox.height, -r.bottomLeft, r.bottomLeft); _draw_corner(bbox.x+bbox.width, bbox.y+bbox.height, r.bottomRight, r.bottomRight); _draw_rect(bbox.x+r.bottomLeft, bbox.y+bbox.height-CLAY__MAX(r.bottomLeft, r.bottomRight), bbox.width-r.bottomLeft-r.bottomRight, CLAY__MAX(r.bottomLeft, r.bottomRight)); } if(r.topLeft < r.bottomLeft){ if(r.topLeft < r.topRight){ _draw_rect(bbox.x, bbox.y+r.topLeft, r.topLeft, bbox.height-r.topLeft-r.bottomLeft); _draw_rect(bbox.x+r.topLeft, bbox.y+r.topRight, r.bottomLeft-r.topLeft, bbox.height-r.topRight-r.bottomLeft); } else { _draw_rect(bbox.x, bbox.y+r.topLeft, r.bottomLeft, bbox.height-r.topLeft-r.bottomLeft); } } else { if(r.bottomLeft < r.bottomRight){ _draw_rect(bbox.x, bbox.y+r.topLeft, r.bottomLeft, bbox.height-r.topLeft-r.bottomLeft); _draw_rect(bbox.x+r.bottomLeft, bbox.y+r.topLeft, r.topLeft-r.bottomLeft, bbox.height-r.topLeft-r.bottomRight); } else { _draw_rect(bbox.x, bbox.y+r.topLeft, r.topLeft, bbox.height-r.topLeft-r.bottomLeft); } } if(r.topRight < r.bottomRight){ if(r.topRight < r.topLeft){ _draw_rect(bbox.x+bbox.width-r.bottomRight, bbox.y+r.topLeft, r.bottomRight-r.topRight, bbox.height-r.topLeft-r.bottomRight); _draw_rect(bbox.x+bbox.width-r.topRight, bbox.y+r.topRight, r.topRight, bbox.height-r.topRight-r.bottomRight); } else { _draw_rect(bbox.x+bbox.width-r.bottomRight, bbox.y+r.topRight, r.bottomRight, bbox.height-r.topRight-r.bottomRight); } } else { if(r.bottomRight < r.bottomLeft){ _draw_rect(bbox.x+bbox.width-r.topRight, bbox.y+r.topRight, r.topRight-r.bottomRight, bbox.height-r.topRight-r.bottomLeft); _draw_rect(bbox.x+bbox.width-r.bottomRight, bbox.y+r.topRight, r.bottomRight, bbox.height-r.topRight-r.bottomRight); } else { _draw_rect(bbox.x+bbox.width-r.topRight, bbox.y+r.topRight, r.topRight, bbox.height-r.topRight-r.bottomRight); } } _draw_rect(bbox.x+CLAY__MAX(r.topLeft, r.bottomLeft), bbox.y+CLAY__MAX(r.topLeft, r.topRight), bbox.width-CLAY__MAX(r.topLeft, r.bottomLeft)-CLAY__MAX(r.topRight, r.bottomRight), bbox.height-CLAY__MAX(r.topLeft, r.topRight)-CLAY__MAX(r.bottomLeft, r.bottomRight)); sgl_end(); break; } case CLAY_RENDER_COMMAND_TYPE_TEXT: { if(!fonts) break; Clay_TextRenderData *config = &renderCommand->renderData.text; Clay_StringSlice text = config->stringContents; fonsSetFont(_sclay.fonts, fonts[config->fontId]); uint32_t color = sfons_rgba( config->textColor.r, config->textColor.g, config->textColor.b, config->textColor.a); fonsSetColor(_sclay.fonts, color); fonsSetSpacing(_sclay.fonts, config->letterSpacing * _sclay.dpi_scale); fonsSetAlign(_sclay.fonts, FONS_ALIGN_LEFT | FONS_ALIGN_TOP); fonsSetSize(_sclay.fonts, config->fontSize * _sclay.dpi_scale); sgl_matrix_mode_modelview(); sgl_push_matrix(); sgl_scale(1.0f/_sclay.dpi_scale, 1.0f/_sclay.dpi_scale, 1.0f); fonsDrawText(_sclay.fonts, bbox.x*_sclay.dpi_scale, bbox.y*_sclay.dpi_scale, text.chars, text.chars + text.length); sgl_pop_matrix(); break; } case CLAY_RENDER_COMMAND_TYPE_SCISSOR_START: { sgl_scissor_rectf(bbox.x*_sclay.dpi_scale, bbox.y*_sclay.dpi_scale, bbox.width*_sclay.dpi_scale, bbox.height*_sclay.dpi_scale, true); break; } case CLAY_RENDER_COMMAND_TYPE_SCISSOR_END: { sgl_scissor_rectf(0, 0, _sclay.size.width*_sclay.dpi_scale, _sclay.size.height*_sclay.dpi_scale, true); break; } case CLAY_RENDER_COMMAND_TYPE_IMAGE: { //TODO break; } case CLAY_RENDER_COMMAND_TYPE_BORDER: { Clay_BorderRenderData *config = &renderCommand->renderData.border; sgl_c4f(config->color.r / 255.0f, config->color.g / 255.0f, config->color.b / 255.0f, config->color.a / 255.0f); Clay_BorderWidth w = config->width; Clay_CornerRadius r = config->cornerRadius; sgl_begin_triangle_strip(); if(w.left > 0){ _draw_rect(bbox.x, bbox.y + r.topLeft, w.left, bbox.height - r.topLeft - r.bottomLeft); } if(w.right > 0){ _draw_rect(bbox.x + bbox.width - w.right, bbox.y + r.topRight, w.right, bbox.height - r.topRight - r.bottomRight); } if(w.top > 0){ _draw_rect(bbox.x + r.topLeft, bbox.y, bbox.width - r.topLeft - r.topRight, w.top); } if(w.bottom > 0){ _draw_rect(bbox.x + r.bottomLeft, bbox.y + bbox.height - w.bottom, bbox.width - r.bottomLeft - r.bottomRight, w.bottom); } if(r.topLeft > 0 && (w.top > 0 || w.left > 0)){ _draw_corner_border(bbox.x, bbox.y, -r.topLeft, -r.topLeft, -r.topLeft+w.left, -r.topLeft+w.top); } if(r.topRight > 0 && (w.top > 0 || w.right > 0)){ _draw_corner_border(bbox.x+bbox.width, bbox.y, r.topRight, -r.topRight, r.topRight-w.right, -r.topRight+w.top); } if(r.bottomLeft > 0 && (w.bottom > 0 || w.left > 0)){ _draw_corner_border(bbox.x, bbox.y+bbox.height, -r.bottomLeft, r.bottomLeft, -r.bottomLeft+w.left, r.bottomLeft-w.bottom); } if(r.bottomRight > 0 && (w.bottom > 0 || w.right > 0)){ _draw_corner_border(bbox.x+bbox.width, bbox.y+bbox.height, r.bottomRight, r.bottomRight, r.bottomRight-w.right, r.bottomRight-w.bottom); } sgl_end(); break; } default: break; } } sgl_pop_pipeline(); sfons_flush(_sclay.fonts); } #endif /* SOKOL_CLAY_IMPL */