diff --git a/build.bat b/build.bat index e11b7d3..9d1c053 100644 --- a/build.bat +++ b/build.bat @@ -1,11 +1,11 @@ - -if not exist bin mkdir bin - -set glsl_shaders=src\glsl_shaders\common.glsl src\glsl_shaders\blit_vertex.glsl src\glsl_shaders\blit_fragment.glsl src\glsl_shaders\clear_counters.glsl src\glsl_shaders\tile.glsl src\glsl_shaders\sort.glsl src\glsl_shaders\draw.glsl - -call python3 scripts\embed_text.py %glsl_shaders% --prefix=glsl_ --output src\glsl_shaders.h - -set INCLUDES=/I src /I src/util /I src/platform /I ext /I ext/angle_headers -set LIBS=user32.lib opengl32.lib gdi32.lib shcore.lib delayimp.lib dwmapi.lib /LIBPATH:./bin libEGL.dll.lib libGLESv2.dll.lib /DELAYLOAD:libEGL.dll /DELAYLOAD:libGLESv2.dll - -cl /we4013 /Zi /Zc:preprocessor /DMP_BUILD_DLL /std:c11 %INCLUDES% src/milepost.c /Fo:bin/milepost.o /LD /link %LIBS% /OUT:bin/milepost.dll /IMPLIB:bin/milepost.dll.lib + +if not exist bin mkdir bin + +set glsl_shaders=src\glsl_shaders\common.glsl src\glsl_shaders\blit_vertex.glsl src\glsl_shaders\blit_fragment.glsl src\glsl_shaders\clear_counters.glsl src\glsl_shaders\tile.glsl src\glsl_shaders\sort.glsl src\glsl_shaders\draw.glsl + +call python3 scripts\embed_text.py %glsl_shaders% --prefix=glsl_ --output src\glsl_shaders.h + +set INCLUDES=/I src /I src/util /I src/platform /I ext /I ext/angle_headers +set LIBS=user32.lib opengl32.lib gdi32.lib shcore.lib delayimp.lib dwmapi.lib comctl32.lib ole32.lib shell32.lib /LIBPATH:./bin libEGL.dll.lib libGLESv2.dll.lib /DELAYLOAD:libEGL.dll /DELAYLOAD:libGLESv2.dll + +cl /we4013 /Zi /Zc:preprocessor /DMP_BUILD_DLL /std:c11 %INCLUDES% src/milepost.c /Fo:bin/milepost.o /LD /link /MANIFEST:EMBED /MANIFESTINPUT:src/win32_manifest.xml %LIBS% /OUT:bin/milepost.dll /IMPLIB:bin/milepost.dll.lib diff --git a/examples/canvas/main.c b/examples/canvas/main.c index 9cb0d8b..ddc8a4e 100644 --- a/examples/canvas/main.c +++ b/examples/canvas/main.c @@ -1,209 +1,210 @@ -/************************************************************//** -* -* @file: main.cpp -* @author: Martin Fouilleul -* @date: 30/07/2022 -* @revision: -* -*****************************************************************/ -#include -#include -#include - -#define _USE_MATH_DEFINES //NOTE: necessary for MSVC -#include - -#include"milepost.h" - -#define LOG_SUBSYSTEM "Main" - - -mg_font create_font() -{ - //NOTE(martin): create font - str8 fontPath = mp_app_get_resource_path(mem_scratch(), "../resources/OpenSansLatinSubset.ttf"); - char* fontPathCString = str8_to_cstring(mem_scratch(), fontPath); - - FILE* fontFile = fopen(fontPathCString, "r"); - if(!fontFile) - { - log_error("Could not load font file '%s': %s\n", fontPathCString, strerror(errno)); - return(mg_font_nil()); - } - unsigned char* fontData = 0; - fseek(fontFile, 0, SEEK_END); - u32 fontDataSize = ftell(fontFile); - rewind(fontFile); - fontData = (unsigned char*)malloc(fontDataSize); - fread(fontData, 1, fontDataSize, fontFile); - fclose(fontFile); - - unicode_range ranges[5] = {UNICODE_RANGE_BASIC_LATIN, - UNICODE_RANGE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, - UNICODE_RANGE_LATIN_EXTENDED_A, - UNICODE_RANGE_LATIN_EXTENDED_B, - UNICODE_RANGE_SPECIALS}; - - mg_font font = mg_font_create_from_memory(fontDataSize, fontData, 5, ranges); - free(fontData); - - return(font); -} - -int main() -{ - mp_init(); - mp_clock_init(); //TODO put that in mp_init()? - - mp_rect windowRect = {.x = 100, .y = 100, .w = 810, .h = 610}; - mp_window window = mp_window_create(windowRect, "test", 0); - - mp_rect contentRect = mp_window_get_content_rect(window); - - //NOTE: create surface - mg_surface surface = mg_surface_create_for_window(window, MG_CANVAS); - mg_surface_swap_interval(surface, 0); - - mg_canvas canvas = mg_canvas_create(); - - if(mg_canvas_is_nil(canvas)) - { - printf("Error: couldn't create canvas\n"); - return(-1); - } - - mg_font font = create_font(); - - // start app - mp_window_bring_to_front(window); - mp_window_focus(window); - - f32 x = 400, y = 300; - f32 speed = 0; - f32 dx = speed, dy = speed; - f64 frameTime = 0; - - while(!mp_should_quit()) - { - f64 startTime = mp_get_time(MP_CLOCK_MONOTONIC); - - mp_pump_events(0); - mp_event* event = 0; - while((event = mp_next_event(mem_scratch())) != 0) - { - switch(event->type) - { - case MP_EVENT_WINDOW_CLOSE: - { - mp_request_quit(); - } break; - - case MP_EVENT_KEYBOARD_KEY: - { - if(event->key.action == MP_KEY_PRESS || event->key.action == MP_KEY_REPEAT) - { - f32 factor = (event->key.mods & MP_KEYMOD_SHIFT) ? 10 : 1; - - if(event->key.code == MP_KEY_LEFT) - { - x-=0.3*factor; - } - else if(event->key.code == MP_KEY_RIGHT) - { - x+=0.3*factor; - } - else if(event->key.code == MP_KEY_UP) - { - y-=0.3*factor; - } - else if(event->key.code == MP_KEY_DOWN) - { - y+=0.3*factor; - } - } - } break; - - default: - break; - } - } - - if(x-200 < 0) - { - x = 200; - dx = speed; - } - if(x+200 > contentRect.w) - { - x = contentRect.w - 200; - dx = -speed; - } - if(y-200 < 0) - { - y = 200; - dy = speed; - } - if(y+200 > contentRect.h) - { - y = contentRect.h - 200; - dy = -speed; - } - x += dx; - y += dy; - - // background - mg_set_color_rgba(0, 1, 1, 1); - mg_clear(); - - // head - mg_set_color_rgba(1, 1, 0, 1); - - mg_circle_fill(x, y, 200); - - // smile - f32 frown = frameTime > 0.033 ? -100 : 0; - - mg_set_color_rgba(0, 0, 0, 1); - mg_set_width(20); - mg_move_to(x-100, y+100); - mg_cubic_to(x-50, y+150+frown, x+50, y+150+frown, x+100, y+100); - mg_stroke(); - - // eyes - mg_ellipse_fill(x-70, y-50, 30, 50); - mg_ellipse_fill(x+70, y-50, 30, 50); - - // text - mg_set_color_rgba(0, 0, 1, 1); - mg_set_font(font); - mg_set_font_size(12); - mg_move_to(50, 600-50); - - str8 text = str8_pushf(mem_scratch(), - "Milepost vector graphics test program (frame time = %fs, fps = %f)...", - frameTime, - 1./frameTime); - mg_text_outlines(text); - mg_fill(); - - printf("Milepost vector graphics test program (frame time = %fs, fps = %f)...\n", - frameTime, - 1./frameTime); - - mg_surface_prepare(surface); - mg_render(surface, canvas); - mg_surface_present(surface); - - mem_arena_clear(mem_scratch()); - frameTime = mp_get_time(MP_CLOCK_MONOTONIC) - startTime; - } - - mg_font_destroy(font); - mg_canvas_destroy(canvas); - mg_surface_destroy(surface); - mp_window_destroy(window); - - mp_terminate(); - - return(0); -} +/************************************************************//** +* +* @file: main.cpp +* @author: Martin Fouilleul +* @date: 30/07/2022 +* @revision: +* +*****************************************************************/ +#include +#include +#include +#include + +#define _USE_MATH_DEFINES //NOTE: necessary for MSVC +#include + +#include"milepost.h" + +#define LOG_SUBSYSTEM "Main" + + +mg_font create_font() +{ + //NOTE(martin): create font + str8 fontPath = mp_app_get_resource_path(mem_scratch(), "../resources/OpenSansLatinSubset.ttf"); + char* fontPathCString = str8_to_cstring(mem_scratch(), fontPath); + + FILE* fontFile = fopen(fontPathCString, "r"); + if(!fontFile) + { + log_error("Could not load font file '%s': %s\n", fontPathCString, strerror(errno)); + return(mg_font_nil()); + } + unsigned char* fontData = 0; + fseek(fontFile, 0, SEEK_END); + u32 fontDataSize = ftell(fontFile); + rewind(fontFile); + fontData = (unsigned char*)malloc(fontDataSize); + fread(fontData, 1, fontDataSize, fontFile); + fclose(fontFile); + + unicode_range ranges[5] = {UNICODE_RANGE_BASIC_LATIN, + UNICODE_RANGE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, + UNICODE_RANGE_LATIN_EXTENDED_A, + UNICODE_RANGE_LATIN_EXTENDED_B, + UNICODE_RANGE_SPECIALS}; + + mg_font font = mg_font_create_from_memory(fontDataSize, fontData, 5, ranges); + free(fontData); + + return(font); +} + +int main() +{ + mp_init(); + mp_clock_init(); //TODO put that in mp_init()? + + mp_rect windowRect = {.x = 100, .y = 100, .w = 810, .h = 610}; + mp_window window = mp_window_create(windowRect, "test", 0); + + mp_rect contentRect = mp_window_get_content_rect(window); + + //NOTE: create surface + mg_surface surface = mg_surface_create_for_window(window, MG_CANVAS); + mg_surface_swap_interval(surface, 0); + + mg_canvas canvas = mg_canvas_create(); + + if(mg_canvas_is_nil(canvas)) + { + printf("Error: couldn't create canvas\n"); + return(-1); + } + + mg_font font = create_font(); + + // start app + mp_window_bring_to_front(window); + mp_window_focus(window); + + f32 x = 400, y = 300; + f32 speed = 0; + f32 dx = speed, dy = speed; + f64 frameTime = 0; + + while(!mp_should_quit()) + { + f64 startTime = mp_get_time(MP_CLOCK_MONOTONIC); + + mp_pump_events(0); + mp_event* event = 0; + while((event = mp_next_event(mem_scratch())) != 0) + { + switch(event->type) + { + case MP_EVENT_WINDOW_CLOSE: + { + mp_request_quit(); + } break; + + case MP_EVENT_KEYBOARD_KEY: + { + if(event->key.action == MP_KEY_PRESS || event->key.action == MP_KEY_REPEAT) + { + f32 factor = (event->key.mods & MP_KEYMOD_SHIFT) ? 10 : 1; + + if(event->key.code == MP_KEY_LEFT) + { + x-=0.3*factor; + } + else if(event->key.code == MP_KEY_RIGHT) + { + x+=0.3*factor; + } + else if(event->key.code == MP_KEY_UP) + { + y-=0.3*factor; + } + else if(event->key.code == MP_KEY_DOWN) + { + y+=0.3*factor; + } + } + } break; + + default: + break; + } + } + + if(x-200 < 0) + { + x = 200; + dx = speed; + } + if(x+200 > contentRect.w) + { + x = contentRect.w - 200; + dx = -speed; + } + if(y-200 < 0) + { + y = 200; + dy = speed; + } + if(y+200 > contentRect.h) + { + y = contentRect.h - 200; + dy = -speed; + } + x += dx; + y += dy; + + // background + mg_set_color_rgba(0, 1, 1, 1); + mg_clear(); + + // head + mg_set_color_rgba(1, 1, 0, 1); + + mg_circle_fill(x, y, 200); + + // smile + f32 frown = frameTime > 0.033 ? -100 : 0; + + mg_set_color_rgba(0, 0, 0, 1); + mg_set_width(20); + mg_move_to(x-100, y+100); + mg_cubic_to(x-50, y+150+frown, x+50, y+150+frown, x+100, y+100); + mg_stroke(); + + // eyes + mg_ellipse_fill(x-70, y-50, 30, 50); + mg_ellipse_fill(x+70, y-50, 30, 50); + + // text + mg_set_color_rgba(0, 0, 1, 1); + mg_set_font(font); + mg_set_font_size(12); + mg_move_to(50, 600-50); + + str8 text = str8_pushf(mem_scratch(), + "Milepost vector graphics test program (frame time = %fs, fps = %f)...", + frameTime, + 1./frameTime); + mg_text_outlines(text); + mg_fill(); + + printf("Milepost vector graphics test program (frame time = %fs, fps = %f)...\n", + frameTime, + 1./frameTime); + + mg_surface_prepare(surface); + mg_render(surface, canvas); + mg_surface_present(surface); + + mem_arena_clear(mem_scratch()); + frameTime = mp_get_time(MP_CLOCK_MONOTONIC) - startTime; + } + + mg_font_destroy(font); + mg_canvas_destroy(canvas); + mg_surface_destroy(surface); + mp_window_destroy(window); + + mp_terminate(); + + return(0); +} diff --git a/examples/perf_text/main.c b/examples/perf_text/main.c index f5e76ba..7e69264 100644 --- a/examples/perf_text/main.c +++ b/examples/perf_text/main.c @@ -1,326 +1,327 @@ - -#include -#include - -#define LOG_DEFAULT_LEVEL LOG_LEVEL_MESSAGE -#define LOG_COMPILE_DEBUG - -#include"milepost.h" - -#define LOG_SUBSYSTEM "Main" - -static const char* TEST_STRING = -"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla quam enim, aliquam in placerat luctus, rutrum in quam. " -"Cras urna elit, pellentesque ac ipsum at, lobortis scelerisque eros. Aenean et turpis nibh. Maecenas lectus augue, eleifend " -"nec efficitur eu, faucibus eget turpis. Suspendisse vel nulla mi. Duis imperdiet neque orci, ac ultrices orci molestie a. " -"Etiam malesuada vulputate hendrerit. Cras ultricies diam in lectus finibus, eu laoreet diam rutrum.\n" -"\n" -"Etiam dictum orci arcu, ac fermentum leo dapibus lacinia. Integer vitae elementum ex. Vestibulum tempor nunc eu hendrerit " -"ornare. Nunc pretium ligula sit amet massa pulvinar, vitae imperdiet justo bibendum. Maecenas consectetur elementum mi, sed " -"vehicula neque pulvinar sit amet. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tortor erat, accumsan in laoreet " -"quis, placerat nec enim. Nulla facilisi. Morbi vitae nibh ligula. Suspendisse in molestie magna, eget aliquet mauris. Sed " -"aliquam faucibus magna.\n" -"\n" -"Sed metus odio, imperdiet et consequat non, faucibus nec risus. Suspendisse facilisis sem neque, id scelerisque dui mattis sit " -"amet. Nullam tincidunt nisl nec dui dignissim mattis. Proin fermentum ornare ipsum. Proin eleifend, mi vitae porttitor placerat, " -"neque magna elementum turpis, eu aliquet mi urna et leo. Pellentesque interdum est mauris, sed pellentesque risus blandit in. " -"Phasellus dignissim consequat eros, at aliquam elit finibus posuere. Proin suscipit tortor leo, id vulputate odio lobortis in. " -"Vestibulum et orci ligula. Sed scelerisque nunc non nisi aliquam, vel eleifend felis suscipit. Integer posuere sapien elit, " -"lacinia ultricies nibh sodales nec.\n" -"\n" -"Etiam aliquam purus sit amet purus ultricies tristique. Nunc maximus nunc quis magna ornare, vel interdum urna fermentum. " -"Vestibulum cursus nisl ut nulla egestas, quis mattis elit venenatis. Praesent malesuada mi non magna aliquam fringilla eget eu " -"turpis. Integer suscipit elit vel consectetur vulputate. Integer euismod, erat eget elementum tempus, magna metus consectetur " -"elit, sed feugiat urna sapien sodales sapien. Sed sit amet varius nunc. Curabitur sodales nunc justo, ac scelerisque ipsum semper " -"eget. Integer ornare, velit ut hendrerit dapibus, erat mauris commodo justo, vel semper urna justo non mauris. Proin blandit, " -"enim ut posuere placerat, leo nibh tristique eros, ut pulvinar sapien elit eget enim. Pellentesque et mauris lectus. Curabitur " -"quis lobortis leo, sit amet egestas dui. Nullam ut sapien eu justo lacinia ultrices. Ut tincidunt, sem non luctus tempus, felis " -"purus imperdiet nisi, non ultricies libero ipsum eu augue. Mauris at luctus enim.\n" -"\n" -"Aliquam sed tortor a justo pulvinar dictum consectetur eu felis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices " -"posuere cubilia curae; Etiam vehicula porttitor volutpat. Morbi fringilla tortor nec accumsan aliquet. Aliquam in commodo neque. " -"Sed laoreet tellus in consectetur aliquet. Nullam nibh eros, feugiat sit amet aliquam non, malesuada vel urna. Ut vel egestas nunc. " -"Pellentesque vitae ante quis ante pharetra pretium. Nam quis eros commodo, mattis enim sed, finibus ante. Quisque lacinia tortor ut " -"odio laoreet, vel viverra libero porttitor. Vestibulum vitae dapibus ex. Phasellus varius lorem sed justo sollicitudin faucibus. " -"Etiam aliquam lacinia consectetur. Phasellus nulla ipsum, viverra non nulla in, rhoncus posuere nunc.\n" -"\n" -"Phasellus efficitur commodo tellus, eget lobortis erat porta quis. Aenean condimentum tortor ut neque dapibus, vitae vulputate quam " -"condimentum. Aliquam elementum vitae nulla vitae tristique. Suspendisse feugiat turpis ac magna dapibus, ut blandit diam tincidunt. " -"Integer id dui id enim ullamcorper dictum. Maecenas malesuada vitae ex pharetra iaculis. Curabitur eu dolor consectetur, tempus augue " -"sed, finibus est. Nulla facilisi. Vivamus sed lacinia turpis, in gravida dolor. Aenean interdum consectetur enim a malesuada. Sed turpis " -"nisi, lacinia et fermentum nec, pharetra id dui. Vivamus neque ligula, iaculis sed tempor eget, vehicula blandit quam. Morbi rhoncus quam " -"semper magna mollis luctus. Donec eu dolor ut ante ullamcorper porta. Mauris et est tristique libero pharetra faucibus.\n" -"\n" -"Duis ut elementum sem. Praesent commodo erat nec sem ultricies sollicitudin. Suspendisse a pellentesque sapien. Nunc ac magna a dui " -"elementum luctus non a mi. Cras elementum nunc sed nunc gravida, sit amet accumsan tortor pulvinar. Etiam elit arcu, pellentesque non ex " -"id, vestibulum pellentesque velit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque habitant morbi tristique senectus " -"et netus et malesuada fames ac turpis egestas. Proin sit amet velit eget tellus vulputate sagittis eget non massa. Cras accumsan tempor " -"tortor, quis rutrum neque placerat id. Nullam a egestas eros, eu porta nisi. Aenean rutrum, sapien quis fermentum tempus, dolor orci " -"faucibus eros, vel luctus justo leo vitae ante. Curabitur aliquam condimentum ipsum sit amet ultrices. Nullam ac velit semper, dapibus urna " -"sit amet, malesuada enim. Mauris ultricies nibh orci."; - - -mg_font create_font(const char* path) -{ - //NOTE(martin): create font - str8 fontPath = mp_app_get_resource_path(mem_scratch(), path); - char* fontPathCString = str8_to_cstring(mem_scratch(), fontPath); - - FILE* fontFile = fopen(fontPathCString, "r"); - if(!fontFile) - { - log_error("Could not load font file '%s'\n", fontPathCString); - return(mg_font_nil()); - } - unsigned char* fontData = 0; - fseek(fontFile, 0, SEEK_END); - u32 fontDataSize = ftell(fontFile); - rewind(fontFile); - fontData = (unsigned char*)malloc(fontDataSize); - fread(fontData, 1, fontDataSize, fontFile); - fclose(fontFile); - - unicode_range ranges[5] = {UNICODE_RANGE_BASIC_LATIN, - UNICODE_RANGE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, - UNICODE_RANGE_LATIN_EXTENDED_A, - UNICODE_RANGE_LATIN_EXTENDED_B, - UNICODE_RANGE_SPECIALS}; - - mg_font font = mg_font_create_from_memory(fontDataSize, fontData, 5, ranges); - free(fontData); - - return(font); -} - -int main() -{ - mp_init(); - mp_clock_init(); - - mp_rect rect = {.x = 100, .y = 100, .w = 980, .h = 600}; - mp_window window = mp_window_create(rect, "test", 0); - - mp_rect contentRect = mp_window_get_content_rect(window); - - //NOTE: create surface, canvas and font - - mg_surface surface = mg_surface_create_for_window(window, MG_CANVAS); - mg_surface_swap_interval(surface, 0); - - mg_canvas canvas = mg_canvas_create(); - - const int fontCount = 3; - int fontIndex = 0; - mg_font fonts[fontCount] = {create_font("../resources/OpenSansLatinSubset.ttf"), - create_font("../resources/CMUSerif-Roman.ttf"), - create_font("../resources/courier.ttf")}; - - mg_font_extents extents[fontCount]; - f32 fontScales[fontCount]; - f32 lineHeights[fontCount]; - - for(int i=0; itype) - { - case MP_EVENT_WINDOW_CLOSE: - { - mp_request_quit(); - } break; - - case MP_EVENT_MOUSE_BUTTON: - { - if(event->key.code == MP_MOUSE_LEFT) - { - if(event->key.action == MP_KEY_PRESS) - { - tracked = true; - vec2 mousePos = mp_mouse_position(&inputState); - trackPoint.x = mousePos.x/zoom - startX; - trackPoint.y = mousePos.y/zoom - startY; - } - else - { - tracked = false; - } - } - } break; - - case MP_EVENT_MOUSE_WHEEL: - { - vec2 mousePos = mp_mouse_position(&inputState); - f32 trackX = mousePos.x/zoom - startX; - f32 trackY = mousePos.y/zoom - startY; - - zoom *= 1 + event->move.deltaY * 0.01; - zoom = Clamp(zoom, 0.2, 10); - - startX = mousePos.x/zoom - trackX; - startY = mousePos.y/zoom - trackY; - } break; - - case MP_EVENT_KEYBOARD_KEY: - { - if(event->key.code == MP_KEY_SPACE && event->key.action == MP_KEY_PRESS) - { - fontIndex = (fontIndex+1)%fontCount; - } - } break; - - default: - break; - } - } - - if(tracked) - { - vec2 mousePos = mp_mouse_position(&inputState); - startX = mousePos.x/zoom - trackPoint.x; - startY = mousePos.y/zoom - trackPoint.y; - } - - f32 textX = startX; - f32 textY = startY; - -/* - mg_set_color_rgba(1, 1, 1, 1); - mg_clear(); - mg_set_color_rgba(1, 0, 0, 1); - for(int i=0; i<1000; i++) - { - mg_rectangle_fill(0, 0, 100, 100); - } -*/ - - mg_matrix_push((mg_mat2x3){zoom, 0, 0, - 0, zoom, 0}); - - mg_set_color_rgba(1, 1, 1, 1); - mg_clear(); - - mg_set_font(fonts[fontIndex]); - mg_set_font_size(14); - mg_set_color_rgba(0, 0, 0, 1); - - mg_move_to(textX, textY); - - int startIndex = 0; - while(startIndex < codePointCount) - { - bool lineBreak = false; - int subIndex = 0; - for(; (startIndex+subIndex) < codePointCount && subIndex < 120; subIndex++) - { - if(codePoints[startIndex + subIndex] == '\n') - { - break; - } - } - - u32 glyphs[512]; - mg_font_get_glyph_indices(fonts[fontIndex], (str32){subIndex, codePoints+startIndex}, (str32){512, glyphs}); - - mg_glyph_outlines((str32){subIndex, glyphs}); - mg_fill(); - - textY += lineHeights[fontIndex]; - mg_move_to(textX, textY); - startIndex++; - - startIndex += subIndex; - } - - mg_matrix_pop(); - - mg_set_color_rgba(0, 0, 1, 1); - mg_set_font(fonts[fontIndex]); - mg_set_font_size(14); - mg_move_to(10, contentRect.h - 10 - lineHeights[fontIndex]); - - str8 text = str8_pushf(mem_scratch(), - "Test program: %i glyphs, frame time = %fs, fps = %f", - glyphCount, - frameTime, - 1./frameTime); - mg_text_outlines(text); - mg_fill(); - - - f64 startFlushTime = mp_get_time(MP_CLOCK_MONOTONIC); - - mg_surface_prepare(surface); - mg_render(surface, canvas); - - f64 startPresentTime = mp_get_time(MP_CLOCK_MONOTONIC); - mg_surface_present(surface); - - f64 endFrameTime = mp_get_time(MP_CLOCK_MONOTONIC); - - frameTime = (endFrameTime - startFrameTime); - - printf("frame time: %.2fms (%.2fFPS), draw = %f.2ms, flush = %.2fms, present = %.2fms\n", - frameTime*1000, - 1./frameTime, - (startFlushTime - startFrameTime)*1000, - (startPresentTime - startFlushTime)*1000, - (endFrameTime - startPresentTime)*1000); - - mp_input_next_frame(&inputState); - mem_arena_clear(mem_scratch()); - } - - - for(int i=0; i +#include + +#define LOG_DEFAULT_LEVEL LOG_LEVEL_MESSAGE +#define LOG_COMPILE_DEBUG + +#include"milepost.h" + +#define LOG_SUBSYSTEM "Main" + +static const char* TEST_STRING = +"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla quam enim, aliquam in placerat luctus, rutrum in quam. " +"Cras urna elit, pellentesque ac ipsum at, lobortis scelerisque eros. Aenean et turpis nibh. Maecenas lectus augue, eleifend " +"nec efficitur eu, faucibus eget turpis. Suspendisse vel nulla mi. Duis imperdiet neque orci, ac ultrices orci molestie a. " +"Etiam malesuada vulputate hendrerit. Cras ultricies diam in lectus finibus, eu laoreet diam rutrum.\n" +"\n" +"Etiam dictum orci arcu, ac fermentum leo dapibus lacinia. Integer vitae elementum ex. Vestibulum tempor nunc eu hendrerit " +"ornare. Nunc pretium ligula sit amet massa pulvinar, vitae imperdiet justo bibendum. Maecenas consectetur elementum mi, sed " +"vehicula neque pulvinar sit amet. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tortor erat, accumsan in laoreet " +"quis, placerat nec enim. Nulla facilisi. Morbi vitae nibh ligula. Suspendisse in molestie magna, eget aliquet mauris. Sed " +"aliquam faucibus magna.\n" +"\n" +"Sed metus odio, imperdiet et consequat non, faucibus nec risus. Suspendisse facilisis sem neque, id scelerisque dui mattis sit " +"amet. Nullam tincidunt nisl nec dui dignissim mattis. Proin fermentum ornare ipsum. Proin eleifend, mi vitae porttitor placerat, " +"neque magna elementum turpis, eu aliquet mi urna et leo. Pellentesque interdum est mauris, sed pellentesque risus blandit in. " +"Phasellus dignissim consequat eros, at aliquam elit finibus posuere. Proin suscipit tortor leo, id vulputate odio lobortis in. " +"Vestibulum et orci ligula. Sed scelerisque nunc non nisi aliquam, vel eleifend felis suscipit. Integer posuere sapien elit, " +"lacinia ultricies nibh sodales nec.\n" +"\n" +"Etiam aliquam purus sit amet purus ultricies tristique. Nunc maximus nunc quis magna ornare, vel interdum urna fermentum. " +"Vestibulum cursus nisl ut nulla egestas, quis mattis elit venenatis. Praesent malesuada mi non magna aliquam fringilla eget eu " +"turpis. Integer suscipit elit vel consectetur vulputate. Integer euismod, erat eget elementum tempus, magna metus consectetur " +"elit, sed feugiat urna sapien sodales sapien. Sed sit amet varius nunc. Curabitur sodales nunc justo, ac scelerisque ipsum semper " +"eget. Integer ornare, velit ut hendrerit dapibus, erat mauris commodo justo, vel semper urna justo non mauris. Proin blandit, " +"enim ut posuere placerat, leo nibh tristique eros, ut pulvinar sapien elit eget enim. Pellentesque et mauris lectus. Curabitur " +"quis lobortis leo, sit amet egestas dui. Nullam ut sapien eu justo lacinia ultrices. Ut tincidunt, sem non luctus tempus, felis " +"purus imperdiet nisi, non ultricies libero ipsum eu augue. Mauris at luctus enim.\n" +"\n" +"Aliquam sed tortor a justo pulvinar dictum consectetur eu felis. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices " +"posuere cubilia curae; Etiam vehicula porttitor volutpat. Morbi fringilla tortor nec accumsan aliquet. Aliquam in commodo neque. " +"Sed laoreet tellus in consectetur aliquet. Nullam nibh eros, feugiat sit amet aliquam non, malesuada vel urna. Ut vel egestas nunc. " +"Pellentesque vitae ante quis ante pharetra pretium. Nam quis eros commodo, mattis enim sed, finibus ante. Quisque lacinia tortor ut " +"odio laoreet, vel viverra libero porttitor. Vestibulum vitae dapibus ex. Phasellus varius lorem sed justo sollicitudin faucibus. " +"Etiam aliquam lacinia consectetur. Phasellus nulla ipsum, viverra non nulla in, rhoncus posuere nunc.\n" +"\n" +"Phasellus efficitur commodo tellus, eget lobortis erat porta quis. Aenean condimentum tortor ut neque dapibus, vitae vulputate quam " +"condimentum. Aliquam elementum vitae nulla vitae tristique. Suspendisse feugiat turpis ac magna dapibus, ut blandit diam tincidunt. " +"Integer id dui id enim ullamcorper dictum. Maecenas malesuada vitae ex pharetra iaculis. Curabitur eu dolor consectetur, tempus augue " +"sed, finibus est. Nulla facilisi. Vivamus sed lacinia turpis, in gravida dolor. Aenean interdum consectetur enim a malesuada. Sed turpis " +"nisi, lacinia et fermentum nec, pharetra id dui. Vivamus neque ligula, iaculis sed tempor eget, vehicula blandit quam. Morbi rhoncus quam " +"semper magna mollis luctus. Donec eu dolor ut ante ullamcorper porta. Mauris et est tristique libero pharetra faucibus.\n" +"\n" +"Duis ut elementum sem. Praesent commodo erat nec sem ultricies sollicitudin. Suspendisse a pellentesque sapien. Nunc ac magna a dui " +"elementum luctus non a mi. Cras elementum nunc sed nunc gravida, sit amet accumsan tortor pulvinar. Etiam elit arcu, pellentesque non ex " +"id, vestibulum pellentesque velit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque habitant morbi tristique senectus " +"et netus et malesuada fames ac turpis egestas. Proin sit amet velit eget tellus vulputate sagittis eget non massa. Cras accumsan tempor " +"tortor, quis rutrum neque placerat id. Nullam a egestas eros, eu porta nisi. Aenean rutrum, sapien quis fermentum tempus, dolor orci " +"faucibus eros, vel luctus justo leo vitae ante. Curabitur aliquam condimentum ipsum sit amet ultrices. Nullam ac velit semper, dapibus urna " +"sit amet, malesuada enim. Mauris ultricies nibh orci."; + + +mg_font create_font(const char* path) +{ + //NOTE(martin): create font + str8 fontPath = mp_app_get_resource_path(mem_scratch(), path); + char* fontPathCString = str8_to_cstring(mem_scratch(), fontPath); + + FILE* fontFile = fopen(fontPathCString, "r"); + if(!fontFile) + { + log_error("Could not load font file '%s'\n", fontPathCString); + return(mg_font_nil()); + } + unsigned char* fontData = 0; + fseek(fontFile, 0, SEEK_END); + u32 fontDataSize = ftell(fontFile); + rewind(fontFile); + fontData = (unsigned char*)malloc(fontDataSize); + fread(fontData, 1, fontDataSize, fontFile); + fclose(fontFile); + + unicode_range ranges[5] = {UNICODE_RANGE_BASIC_LATIN, + UNICODE_RANGE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, + UNICODE_RANGE_LATIN_EXTENDED_A, + UNICODE_RANGE_LATIN_EXTENDED_B, + UNICODE_RANGE_SPECIALS}; + + mg_font font = mg_font_create_from_memory(fontDataSize, fontData, 5, ranges); + free(fontData); + + return(font); +} + +enum { FONT_COUNT = 3 }; + +int main() +{ + mp_init(); + mp_clock_init(); + + mp_rect rect = {.x = 100, .y = 100, .w = 980, .h = 600}; + mp_window window = mp_window_create(rect, "test", 0); + + mp_rect contentRect = mp_window_get_content_rect(window); + + //NOTE: create surface, canvas and font + + mg_surface surface = mg_surface_create_for_window(window, MG_CANVAS); + mg_surface_swap_interval(surface, 0); + + mg_canvas canvas = mg_canvas_create(); + + int fontIndex = 0; + mg_font fonts[FONT_COUNT] = {create_font("../resources/OpenSansLatinSubset.ttf"), + create_font("../resources/CMUSerif-Roman.ttf"), + create_font("../resources/courier.ttf")}; + + mg_font_extents extents[FONT_COUNT]; + f32 fontScales[FONT_COUNT]; + f32 lineHeights[FONT_COUNT]; + + for(int i=0; itype) + { + case MP_EVENT_WINDOW_CLOSE: + { + mp_request_quit(); + } break; + + case MP_EVENT_MOUSE_BUTTON: + { + if(event->key.code == MP_MOUSE_LEFT) + { + if(event->key.action == MP_KEY_PRESS) + { + tracked = true; + vec2 mousePos = mp_mouse_position(&inputState); + trackPoint.x = mousePos.x/zoom - startX; + trackPoint.y = mousePos.y/zoom - startY; + } + else + { + tracked = false; + } + } + } break; + + case MP_EVENT_MOUSE_WHEEL: + { + vec2 mousePos = mp_mouse_position(&inputState); + f32 trackX = mousePos.x/zoom - startX; + f32 trackY = mousePos.y/zoom - startY; + + zoom *= 1 + event->move.deltaY * 0.01; + zoom = Clamp(zoom, 0.2, 10); + + startX = mousePos.x/zoom - trackX; + startY = mousePos.y/zoom - trackY; + } break; + + case MP_EVENT_KEYBOARD_KEY: + { + if(event->key.code == MP_KEY_SPACE && event->key.action == MP_KEY_PRESS) + { + fontIndex = (fontIndex+1)%FONT_COUNT; + } + } break; + + default: + break; + } + } + + if(tracked) + { + vec2 mousePos = mp_mouse_position(&inputState); + startX = mousePos.x/zoom - trackPoint.x; + startY = mousePos.y/zoom - trackPoint.y; + } + + f32 textX = startX; + f32 textY = startY; + +/* + mg_set_color_rgba(1, 1, 1, 1); + mg_clear(); + mg_set_color_rgba(1, 0, 0, 1); + for(int i=0; i<1000; i++) + { + mg_rectangle_fill(0, 0, 100, 100); + } +*/ + + mg_matrix_push((mg_mat2x3){zoom, 0, 0, + 0, zoom, 0}); + + mg_set_color_rgba(1, 1, 1, 1); + mg_clear(); + + mg_set_font(fonts[fontIndex]); + mg_set_font_size(14); + mg_set_color_rgba(0, 0, 0, 1); + + mg_move_to(textX, textY); + + int startIndex = 0; + while(startIndex < codePointCount) + { + bool lineBreak = false; + int subIndex = 0; + for(; (startIndex+subIndex) < codePointCount && subIndex < 120; subIndex++) + { + if(codePoints[startIndex + subIndex] == '\n') + { + break; + } + } + + u32 glyphs[512]; + mg_font_get_glyph_indices(fonts[fontIndex], (str32){subIndex, codePoints+startIndex}, (str32){512, glyphs}); + + mg_glyph_outlines((str32){subIndex, glyphs}); + mg_fill(); + + textY += lineHeights[fontIndex]; + mg_move_to(textX, textY); + startIndex++; + + startIndex += subIndex; + } + + mg_matrix_pop(); + + mg_set_color_rgba(0, 0, 1, 1); + mg_set_font(fonts[fontIndex]); + mg_set_font_size(14); + mg_move_to(10, contentRect.h - 10 - lineHeights[fontIndex]); + + str8 text = str8_pushf(mem_scratch(), + "Test program: %i glyphs, frame time = %fs, fps = %f", + glyphCount, + frameTime, + 1./frameTime); + mg_text_outlines(text); + mg_fill(); + + + f64 startFlushTime = mp_get_time(MP_CLOCK_MONOTONIC); + + mg_surface_prepare(surface); + mg_render(surface, canvas); + + f64 startPresentTime = mp_get_time(MP_CLOCK_MONOTONIC); + mg_surface_present(surface); + + f64 endFrameTime = mp_get_time(MP_CLOCK_MONOTONIC); + + frameTime = (endFrameTime - startFrameTime); + + printf("frame time: %.2fms (%.2fFPS), draw = %f.2ms, flush = %.2fms, present = %.2fms\n", + frameTime*1000, + 1./frameTime, + (startFlushTime - startFrameTime)*1000, + (startPresentTime - startFlushTime)*1000, + (endFrameTime - startPresentTime)*1000); + + mp_input_next_frame(&inputState); + mem_arena_clear(mem_scratch()); + } + + + for(int i=0; i -#include - -#include"milepost.h" - -#define LOG_SUBSYSTEM "Main" - -int main() -{ - LogLevel(LOG_LEVEL_DEBUG); - - mp_init(); - - mp_rect rect = {.x = 100, .y = 100, .w = 800, .h = 600}; - mp_window window = mp_window_create(rect, "test", 0); - - mp_window_bring_to_front(window); - mp_window_focus(window); - - while(!mp_should_quit()) - { - mp_pump_events(0); - mp_event event = {0}; - while(mp_next_event(&event)) - { - switch(event.type) - { - case MP_EVENT_WINDOW_CLOSE: - { - mp_request_quit(); - } break; - - case MP_EVENT_WINDOW_RESIZE: - { - printf("resized, rect = {%f, %f, %f, %f}\n", - event.frame.rect.x, - event.frame.rect.y, - event.frame.rect.w, - event.frame.rect.h); - } break; - - case MP_EVENT_WINDOW_MOVE: - { - printf("moved, rect = {%f, %f, %f, %f}\n", - event.frame.rect.x, - event.frame.rect.y, - event.frame.rect.w, - event.frame.rect.h); - } break; - - case MP_EVENT_MOUSE_MOVE: - { - printf("mouse moved, pos = {%f, %f}, delta = {%f, %f}\n", - event.move.x, - event.move.y, - event.move.deltaX, - event.move.deltaY); - } break; - - case MP_EVENT_MOUSE_WHEEL: - { - printf("mouse wheel, delta = {%f, %f}\n", - event.move.deltaX, - event.move.deltaY); - } break; - - case MP_EVENT_MOUSE_ENTER: - { - printf("mouse enter\n"); - } break; - - case MP_EVENT_MOUSE_LEAVE: - { - printf("mouse leave\n"); - } break; - - case MP_EVENT_MOUSE_BUTTON: - { - printf("mouse button %i: %i\n", - event.key.code, - event.key.action == MP_KEY_PRESS ? 1 : 0); - } break; - - case MP_EVENT_KEYBOARD_KEY: - { - printf("key %i: %s\n", - event.key.code, - event.key.action == MP_KEY_PRESS ? "press" : (event.key.action == MP_KEY_RELEASE ? "release" : "repeat")); - } break; - - case MP_EVENT_KEYBOARD_CHAR: - { - printf("entered char %s\n", event.character.sequence); - } break; - - default: - break; - } - } - } - - mp_terminate(); - - return(0); -} +/************************************************************//** +* +* @file: main.cpp +* @author: Martin Fouilleul +* @date: 30/07/2022 +* @revision: +* +*****************************************************************/ +#include +#include +#include + +#include"milepost.h" + +int main() +{ + mp_init(); + + mp_rect rect = {.x = 100, .y = 100, .w = 800, .h = 600}; + mp_window window = mp_window_create(rect, "test", 0); + + mp_window_bring_to_front(window); + mp_window_focus(window); + + while(!mp_should_quit()) + { + mp_pump_events(0); + mp_event *event = 0; + while((event = mp_next_event(mem_scratch())) != 0) + { + switch(event->type) + { + case MP_EVENT_WINDOW_CLOSE: + { + mp_request_quit(); + } break; + + case MP_EVENT_WINDOW_RESIZE: + { + printf("resized, rect = {%f, %f, %f, %f}\n", + event->frame.rect.x, + event->frame.rect.y, + event->frame.rect.w, + event->frame.rect.h); + } break; + + case MP_EVENT_WINDOW_MOVE: + { + printf("moved, rect = {%f, %f, %f, %f}\n", + event->frame.rect.x, + event->frame.rect.y, + event->frame.rect.w, + event->frame.rect.h); + } break; + + case MP_EVENT_MOUSE_MOVE: + { + printf("mouse moved, pos = {%f, %f}, delta = {%f, %f}\n", + event->move.x, + event->move.y, + event->move.deltaX, + event->move.deltaY); + } break; + + case MP_EVENT_MOUSE_WHEEL: + { + printf("mouse wheel, delta = {%f, %f}\n", + event->move.deltaX, + event->move.deltaY); + } break; + + case MP_EVENT_MOUSE_ENTER: + { + printf("mouse enter\n"); + } break; + + case MP_EVENT_MOUSE_LEAVE: + { + printf("mouse leave\n"); + } break; + + case MP_EVENT_MOUSE_BUTTON: + { + printf("mouse button %i: %i\n", + event->key.code, + event->key.action == MP_KEY_PRESS ? 1 : 0); + } break; + + case MP_EVENT_KEYBOARD_KEY: + { + printf("key %i: %s\n", + event->key.code, + event->key.action == MP_KEY_PRESS ? "press" : (event->key.action == MP_KEY_RELEASE ? "release" : "repeat")); + } break; + + case MP_EVENT_KEYBOARD_CHAR: + { + printf("entered char %s\n", event->character.sequence); + } break; + + default: + break; + } + } + mem_arena_clear(mem_scratch()); + } + + mp_terminate(); + + return(0); +} diff --git a/examples/tiger/build.bat b/examples/tiger/build.bat index 004a30f..b4a4bdb 100644 --- a/examples/tiger/build.bat +++ b/examples/tiger/build.bat @@ -1,4 +1,4 @@ - -set INCLUDES=/I ..\..\src /I ..\..\src\util /I ..\..\src\platform /I ../../ext /I ../../ext/angle_headers - -cl /we4013 /Zi /Zc:preprocessor /std:c11 %INCLUDES% main.c /link /LIBPATH:../../bin milepost.dll.lib /out:../../bin/example_canvas.exe + +set INCLUDES=/I ..\..\src /I ..\..\src\util /I ..\..\src\platform /I ../../ext /I ../../ext/angle_headers + +cl /we4013 /Zi /Zc:preprocessor /std:c11 %INCLUDES% main.c /link /LIBPATH:../../bin milepost.dll.lib /out:../../bin/example_tiger.exe diff --git a/examples/tiger/main.c b/examples/tiger/main.c index c103a41..6ec1ffe 100644 --- a/examples/tiger/main.c +++ b/examples/tiger/main.c @@ -1,247 +1,248 @@ -/************************************************************//** -* -* @file: main.cpp -* @author: Martin Fouilleul -* @date: 30/07/2022 -* @revision: -* -*****************************************************************/ -#include -#include -#include - -#define _USE_MATH_DEFINES //NOTE: necessary for MSVC -#include - -#include"milepost.h" - -#include"tiger.c" - -mg_font create_font() -{ - //NOTE(martin): create font - str8 fontPath = mp_app_get_resource_path(mem_scratch(), "../resources/OpenSansLatinSubset.ttf"); - char* fontPathCString = str8_to_cstring(mem_scratch(), fontPath); - - FILE* fontFile = fopen(fontPathCString, "r"); - if(!fontFile) - { - log_error("Could not load font file '%s': %s\n", fontPathCString, strerror(errno)); - return(mg_font_nil()); - } - unsigned char* fontData = 0; - fseek(fontFile, 0, SEEK_END); - u32 fontDataSize = ftell(fontFile); - rewind(fontFile); - fontData = (unsigned char*)malloc(fontDataSize); - fread(fontData, 1, fontDataSize, fontFile); - fclose(fontFile); - - unicode_range ranges[5] = {UNICODE_RANGE_BASIC_LATIN, - UNICODE_RANGE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, - UNICODE_RANGE_LATIN_EXTENDED_A, - UNICODE_RANGE_LATIN_EXTENDED_B, - UNICODE_RANGE_SPECIALS}; - - mg_font font = mg_font_create_from_memory(fontDataSize, fontData, 5, ranges); - free(fontData); - - return(font); -} - -int main() -{ - mp_init(); - mp_clock_init(); //TODO put that in mp_init()? - - mp_rect windowRect = {.x = 100, .y = 100, .w = 810, .h = 610}; - mp_window window = mp_window_create(windowRect, "test", 0); - - mp_rect contentRect = mp_window_get_content_rect(window); - - //NOTE: create surface - mg_surface surface = mg_surface_create_for_window(window, MG_CANVAS); - mg_surface_swap_interval(surface, 0); - - //TODO: create canvas - mg_canvas canvas = mg_canvas_create(); - - if(mg_canvas_is_nil(canvas)) - { - printf("Error: couldn't create canvas\n"); - return(-1); - } - - mg_font font = create_font(); - - // start app - mp_window_bring_to_front(window); - mp_window_focus(window); - - bool tracked = false; - vec2 trackPoint = {0}; - - f32 zoom = 1; - f32 startX = 300, startY = 200; - bool singlePath = false; - int singlePathIndex = 0; - - f64 frameTime = 0; - - mp_input_state inputState = {0}; - - while(!mp_should_quit()) - { - f64 startTime = mp_get_time(MP_CLOCK_MONOTONIC); - - mp_pump_events(0); - mp_event* event = 0; - while((event = mp_next_event(mem_scratch())) != 0) - { - mp_input_process_event(&inputState, event); - - switch(event->type) - { - case MP_EVENT_WINDOW_CLOSE: - { - mp_request_quit(); - } break; - - case MP_EVENT_WINDOW_RESIZE: - { - mp_rect frame = {0, 0, event->frame.rect.w, event->frame.rect.h}; - mg_surface_set_frame(surface, frame); - } break; - - case MP_EVENT_MOUSE_BUTTON: - { - if(event->key.code == MP_MOUSE_LEFT) - { - if(event->key.action == MP_KEY_PRESS) - { - tracked = true; - vec2 mousePos = mp_mouse_position(&inputState); - trackPoint.x = (mousePos.x - startX)/zoom; - trackPoint.y = (mousePos.y - startY)/zoom; - } - else - { - tracked = false; - } - } - } break; - - case MP_EVENT_MOUSE_WHEEL: - { - vec2 mousePos = mp_mouse_position(&inputState); - f32 pinX = (mousePos.x - startX)/zoom; - f32 pinY = (mousePos.y - startY)/zoom; - - zoom *= 1 + event->move.deltaY * 0.01; - zoom = Clamp(zoom, 0.5, 5); - - startX = mousePos.x - pinX*zoom; - startY = mousePos.y - pinY*zoom; - } break; - - case MP_EVENT_KEYBOARD_KEY: - { - if(event->key.action == MP_KEY_PRESS || event->key.action == MP_KEY_REPEAT) - { - switch(event->key.code) - { - case MP_KEY_SPACE: - singlePath = !singlePath; - break; - - case MP_KEY_UP: - { - if(event->key.mods & MP_KEYMOD_SHIFT) - { - singlePathIndex++; - } - else - { - zoom += 0.001; - } - } break; - - case MP_KEY_DOWN: - { - if(event->key.mods & MP_KEYMOD_SHIFT) - { - singlePathIndex--; - } - else - { - zoom -= 0.001; - } - } break; - } - } - } break; - - default: - break; - } - } - - if(tracked) - { - vec2 mousePos = mp_mouse_position(&inputState); - startX = mousePos.x - trackPoint.x*zoom; - startY = mousePos.y - trackPoint.y*zoom; - } - - mg_surface_prepare(surface); - - mg_set_color_rgba(1, 0, 1, 1); - mg_clear(); - - mg_matrix_push((mg_mat2x3){zoom, 0, startX, - 0, zoom, startY}); - - draw_tiger(singlePath, singlePathIndex); - - if(singlePath) - { - printf("display single path %i\n", singlePathIndex); - printf("viewpos = (%f, %f), zoom = %f\n", startX, startY, zoom); - } - - mg_matrix_pop(); - - // text - mg_set_color_rgba(0, 0, 1, 1); - mg_set_font(font); - mg_set_font_size(12); - mg_move_to(50, 600-50); - - str8 text = str8_pushf(mem_scratch(), - "Milepost vector graphics test program (frame time = %fs, fps = %f)...", - frameTime, - 1./frameTime); - mg_text_outlines(text); - mg_fill(); - - printf("Milepost vector graphics test program (frame time = %fs, fps = %f)...\n", - frameTime, - 1./frameTime); - - mg_render(surface, canvas); - mg_surface_present(surface); - - mp_input_next_frame(&inputState); - mem_arena_clear(mem_scratch()); - frameTime = mp_get_time(MP_CLOCK_MONOTONIC) - startTime; - } - - mg_font_destroy(font); - mg_canvas_destroy(canvas); - mg_surface_destroy(surface); - mp_window_destroy(window); - - mp_terminate(); - - return(0); -} +/************************************************************//** +* +* @file: main.cpp +* @author: Martin Fouilleul +* @date: 30/07/2022 +* @revision: +* +*****************************************************************/ +#include +#include +#include +#include + +#define _USE_MATH_DEFINES //NOTE: necessary for MSVC +#include + +#include"milepost.h" + +#include"tiger.c" + +mg_font create_font() +{ + //NOTE(martin): create font + str8 fontPath = mp_app_get_resource_path(mem_scratch(), "../resources/OpenSansLatinSubset.ttf"); + char* fontPathCString = str8_to_cstring(mem_scratch(), fontPath); + + FILE* fontFile = fopen(fontPathCString, "r"); + if(!fontFile) + { + log_error("Could not load font file '%s': %s\n", fontPathCString, strerror(errno)); + return(mg_font_nil()); + } + unsigned char* fontData = 0; + fseek(fontFile, 0, SEEK_END); + u32 fontDataSize = ftell(fontFile); + rewind(fontFile); + fontData = (unsigned char*)malloc(fontDataSize); + fread(fontData, 1, fontDataSize, fontFile); + fclose(fontFile); + + unicode_range ranges[5] = {UNICODE_RANGE_BASIC_LATIN, + UNICODE_RANGE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, + UNICODE_RANGE_LATIN_EXTENDED_A, + UNICODE_RANGE_LATIN_EXTENDED_B, + UNICODE_RANGE_SPECIALS}; + + mg_font font = mg_font_create_from_memory(fontDataSize, fontData, 5, ranges); + free(fontData); + + return(font); +} + +int main() +{ + mp_init(); + mp_clock_init(); //TODO put that in mp_init()? + + mp_rect windowRect = {.x = 100, .y = 100, .w = 810, .h = 610}; + mp_window window = mp_window_create(windowRect, "test", 0); + + mp_rect contentRect = mp_window_get_content_rect(window); + + //NOTE: create surface + mg_surface surface = mg_surface_create_for_window(window, MG_CANVAS); + mg_surface_swap_interval(surface, 0); + + //TODO: create canvas + mg_canvas canvas = mg_canvas_create(); + + if(mg_canvas_is_nil(canvas)) + { + printf("Error: couldn't create canvas\n"); + return(-1); + } + + mg_font font = create_font(); + + // start app + mp_window_bring_to_front(window); + mp_window_focus(window); + + bool tracked = false; + vec2 trackPoint = {0}; + + f32 zoom = 1; + f32 startX = 300, startY = 200; + bool singlePath = false; + int singlePathIndex = 0; + + f64 frameTime = 0; + + mp_input_state inputState = {0}; + + while(!mp_should_quit()) + { + f64 startTime = mp_get_time(MP_CLOCK_MONOTONIC); + + mp_pump_events(0); + mp_event* event = 0; + while((event = mp_next_event(mem_scratch())) != 0) + { + mp_input_process_event(&inputState, event); + + switch(event->type) + { + case MP_EVENT_WINDOW_CLOSE: + { + mp_request_quit(); + } break; + + case MP_EVENT_WINDOW_RESIZE: + { + mp_rect frame = {0, 0, event->frame.rect.w, event->frame.rect.h}; + mg_surface_set_frame(surface, frame); + } break; + + case MP_EVENT_MOUSE_BUTTON: + { + if(event->key.code == MP_MOUSE_LEFT) + { + if(event->key.action == MP_KEY_PRESS) + { + tracked = true; + vec2 mousePos = mp_mouse_position(&inputState); + trackPoint.x = (mousePos.x - startX)/zoom; + trackPoint.y = (mousePos.y - startY)/zoom; + } + else + { + tracked = false; + } + } + } break; + + case MP_EVENT_MOUSE_WHEEL: + { + vec2 mousePos = mp_mouse_position(&inputState); + f32 pinX = (mousePos.x - startX)/zoom; + f32 pinY = (mousePos.y - startY)/zoom; + + zoom *= 1 + event->move.deltaY * 0.01; + zoom = Clamp(zoom, 0.5, 5); + + startX = mousePos.x - pinX*zoom; + startY = mousePos.y - pinY*zoom; + } break; + + case MP_EVENT_KEYBOARD_KEY: + { + if(event->key.action == MP_KEY_PRESS || event->key.action == MP_KEY_REPEAT) + { + switch(event->key.code) + { + case MP_KEY_SPACE: + singlePath = !singlePath; + break; + + case MP_KEY_UP: + { + if(event->key.mods & MP_KEYMOD_SHIFT) + { + singlePathIndex++; + } + else + { + zoom += 0.001; + } + } break; + + case MP_KEY_DOWN: + { + if(event->key.mods & MP_KEYMOD_SHIFT) + { + singlePathIndex--; + } + else + { + zoom -= 0.001; + } + } break; + } + } + } break; + + default: + break; + } + } + + if(tracked) + { + vec2 mousePos = mp_mouse_position(&inputState); + startX = mousePos.x - trackPoint.x*zoom; + startY = mousePos.y - trackPoint.y*zoom; + } + + mg_surface_prepare(surface); + + mg_set_color_rgba(1, 0, 1, 1); + mg_clear(); + + mg_matrix_push((mg_mat2x3){zoom, 0, startX, + 0, zoom, startY}); + + draw_tiger(singlePath, singlePathIndex); + + if(singlePath) + { + printf("display single path %i\n", singlePathIndex); + printf("viewpos = (%f, %f), zoom = %f\n", startX, startY, zoom); + } + + mg_matrix_pop(); + + // text + mg_set_color_rgba(0, 0, 1, 1); + mg_set_font(font); + mg_set_font_size(12); + mg_move_to(50, 600-50); + + str8 text = str8_pushf(mem_scratch(), + "Milepost vector graphics test program (frame time = %fs, fps = %f)...", + frameTime, + 1./frameTime); + mg_text_outlines(text); + mg_fill(); + + printf("Milepost vector graphics test program (frame time = %fs, fps = %f)...\n", + frameTime, + 1./frameTime); + + mg_render(surface, canvas); + mg_surface_present(surface); + + mp_input_next_frame(&inputState); + mem_arena_clear(mem_scratch()); + frameTime = mp_get_time(MP_CLOCK_MONOTONIC) - startTime; + } + + mg_font_destroy(font); + mg_canvas_destroy(canvas); + mg_surface_destroy(surface); + mp_window_destroy(window); + + mp_terminate(); + + return(0); +} diff --git a/examples/triangleGL/main.c b/examples/triangleGL/main.c index f187db4..07ee863 100644 --- a/examples/triangleGL/main.c +++ b/examples/triangleGL/main.c @@ -1,165 +1,163 @@ -/************************************************************//** -* -* @file: main.cpp -* @author: Martin Fouilleul -* @date: 30/07/2022 -* @revision: -* -*****************************************************************/ -#include -#include - -#define _USE_MATH_DEFINES //NOTE: necessary for MSVC -#include - -#define MG_INCLUDE_GL_API -#include"milepost.h" - -#define LOG_SUBSYSTEM "Main" - -unsigned int program; - -const char* vshaderSource = - "#version 430\n" - "attribute vec4 vPosition;\n" - "uniform mat4 transform;\n" - "void main()\n" - "{\n" - " gl_Position = transform*vPosition;\n" - "}\n"; - -const char* fshaderSource = - "#version 430\n" - "precision mediump float;\n" - "void main()\n" - "{\n" - " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n" - "}\n"; - -void compile_shader(GLuint shader, const char* source) -{ - glShaderSource(shader, 1, &source, 0); - glCompileShader(shader); - - int err = glGetError(); - if(err) - { - printf("gl error: %i\n", err); - } - - int status = 0; - glGetShaderiv(shader, GL_COMPILE_STATUS, &status); - if(!status) - { - char buffer[256]; - int size = 0; - glGetShaderInfoLog(shader, 256, &size, buffer); - printf("shader error: %.*s\n", size, buffer); - } -} - -int main() -{ - LogLevel(LOG_LEVEL_DEBUG); - - mp_init(); - - mp_rect rect = {.x = 100, .y = 100, .w = 800, .h = 600}; - mp_window window = mp_window_create(rect, "test", 0); - - //NOTE: create surface - mg_surface surface = mg_surface_create_for_window(window, MG_BACKEND_GL); - - //NOTE: init shader and gl state - mg_surface_prepare(surface); - - GLuint vao; - glGenVertexArrays(1, &vao); - glBindVertexArray(vao); - - GLuint vertexBuffer; - glGenBuffers(1, &vertexBuffer); - - GLfloat vertices[] = { - -0.866/2, -0.5/2, 0, 0.866/2, -0.5/2, 0, 0, 0.5, 0}; - - glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - - unsigned int vshader = glCreateShader(GL_VERTEX_SHADER); - unsigned int fshader = glCreateShader(GL_FRAGMENT_SHADER); - program = glCreateProgram(); - - compile_shader(vshader, vshaderSource); - compile_shader(fshader, fshaderSource); - - glAttachShader(program, vshader); - glAttachShader(program, fshader); - glLinkProgram(program); - - int status = 0; - glGetProgramiv(program, GL_LINK_STATUS, &status); - if(!status) - { - char buffer[256]; - int size = 0; - glGetProgramInfoLog(program, 256, &size, buffer); - printf("link error: %.*s\n", size, buffer); - } - - glUseProgram(program); - - mp_window_bring_to_front(window); -// mp_window_focus(window); - - while(!mp_should_quit()) - { - mp_pump_events(0); - mp_event event = {0}; - while(mp_next_event(&event)) - { - switch(event.type) - { - case MP_EVENT_WINDOW_CLOSE: - { - mp_request_quit(); - } break; - - default: - break; - } - } - - mg_surface_prepare(surface); - - glClearColor(0.3, 0.3, 1, 1); - glClear(GL_COLOR_BUFFER_BIT); - - static float alpha = 0; - //f32 aspect = frameSize.x/frameSize.y; - f32 aspect = 800/(f32)600; - - GLfloat matrix[] = {cosf(alpha)/aspect, sinf(alpha), 0, 0, - -sinf(alpha)/aspect, cosf(alpha), 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1}; - - alpha += 2*M_PI/120; - - glUniformMatrix4fv(0, 1, false, matrix); - - - glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); - glEnableVertexAttribArray(0); - - glDrawArrays(GL_TRIANGLES, 0, 3); - - mg_surface_present(surface); - } - - mp_terminate(); - - return(0); -} +/************************************************************//** +* +* @file: main.cpp +* @author: Martin Fouilleul +* @date: 30/07/2022 +* @revision: +* +*****************************************************************/ +#include +#include + +#define _USE_MATH_DEFINES //NOTE: necessary for MSVC +#include + +#define MG_INCLUDE_GL_API +#include"milepost.h" + +unsigned int program; + +const char* vshaderSource = + "#version 430\n" + "attribute vec4 vPosition;\n" + "uniform mat4 transform;\n" + "void main()\n" + "{\n" + " gl_Position = transform*vPosition;\n" + "}\n"; + +const char* fshaderSource = + "#version 430\n" + "precision mediump float;\n" + "void main()\n" + "{\n" + " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n" + "}\n"; + +void compile_shader(GLuint shader, const char* source) +{ + glShaderSource(shader, 1, &source, 0); + glCompileShader(shader); + + int err = glGetError(); + if(err) + { + printf("gl error: %i\n", err); + } + + int status = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + if(!status) + { + char buffer[256]; + int size = 0; + glGetShaderInfoLog(shader, 256, &size, buffer); + printf("shader error: %.*s\n", size, buffer); + } +} + +int main() +{ + mp_init(); + + mp_rect rect = {.x = 100, .y = 100, .w = 800, .h = 600}; + mp_window window = mp_window_create(rect, "test", 0); + + //NOTE: create surface + mg_surface surface = mg_surface_create_for_window(window, MG_GL); + + //NOTE: init shader and gl state + mg_surface_prepare(surface); + + GLuint vao; + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + GLuint vertexBuffer; + glGenBuffers(1, &vertexBuffer); + + GLfloat vertices[] = { + -0.866/2, -0.5/2, 0, 0.866/2, -0.5/2, 0, 0, 0.5, 0}; + + glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + + unsigned int vshader = glCreateShader(GL_VERTEX_SHADER); + unsigned int fshader = glCreateShader(GL_FRAGMENT_SHADER); + program = glCreateProgram(); + + compile_shader(vshader, vshaderSource); + compile_shader(fshader, fshaderSource); + + glAttachShader(program, vshader); + glAttachShader(program, fshader); + glLinkProgram(program); + + int status = 0; + glGetProgramiv(program, GL_LINK_STATUS, &status); + if(!status) + { + char buffer[256]; + int size = 0; + glGetProgramInfoLog(program, 256, &size, buffer); + printf("link error: %.*s\n", size, buffer); + } + + glUseProgram(program); + + mp_window_bring_to_front(window); +// mp_window_focus(window); + + while(!mp_should_quit()) + { + mp_pump_events(0); + mp_event* event = 0; + while((event = mp_next_event(mem_scratch())) != 0) + { + switch(event->type) + { + case MP_EVENT_WINDOW_CLOSE: + { + mp_request_quit(); + } break; + + default: + break; + } + } + + mg_surface_prepare(surface); + + glClearColor(0.3, 0.3, 1, 1); + glClear(GL_COLOR_BUFFER_BIT); + + static float alpha = 0; + //f32 aspect = frameSize.x/frameSize.y; + f32 aspect = 800/(f32)600; + + GLfloat matrix[] = {cosf(alpha)/aspect, sinf(alpha), 0, 0, + -sinf(alpha)/aspect, cosf(alpha), 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1}; + + alpha += 2*M_PI/120; + + glUniformMatrix4fv(0, 1, false, matrix); + + + glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); + glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); + glEnableVertexAttribArray(0); + + glDrawArrays(GL_TRIANGLES, 0, 3); + + mg_surface_present(surface); + + mem_arena_clear(mem_scratch()); + } + + mp_terminate(); + + return(0); +} diff --git a/examples/ui/main.c b/examples/ui/main.c index 161af0a..fa63e87 100644 --- a/examples/ui/main.c +++ b/examples/ui/main.c @@ -1,605 +1,633 @@ -/************************************************************//** -* -* @file: main.cpp -* @author: Martin Fouilleul -* @date: 30/07/2022 -* @revision: -* -*****************************************************************/ -#include -#include -#include - -#define _USE_MATH_DEFINES //NOTE: necessary for MSVC -#include - -#include"milepost.h" - -#define LOG_SUBSYSTEM "Main" - -void debug_print_indent(int indent) -{ - for(int i=0; ipattern.l, selector, ui_selector, listElt) - { - switch(selector->kind) - { - case UI_SEL_ANY: - printf("any: "); - break; - - case UI_SEL_OWNER: - printf("owner: "); - break; - - case UI_SEL_TEXT: - printf("text='%.*s': ", (int)selector->text.len, selector->text.ptr); - break; - - case UI_SEL_TAG: - printf("tag=0x%llx: ", selector->tag.hash); - break; - - case UI_SEL_STATUS: - { - if(selector->status & UI_HOVER) - { - printf("hover: "); - } - if(selector->status & UI_ACTIVE) - { - printf("active: "); - } - if(selector->status & UI_DRAGGING) - { - printf("dragging: "); - } - } break; - - case UI_SEL_KEY: - printf("key=0x%llx: ", selector->key.hash); - break; - - default: - printf("unknown: "); - break; - } - } - printf("=> font size = %f\n", rule->style->fontSize); -} -void debug_print_size(ui_box* box, ui_axis axis, int indent) -{ - debug_print_indent(indent); - printf("size %s: ", axis == UI_AXIS_X ? "x" : "y"); - f32 value = box->targetStyle->size.c[axis].value; - switch(box->targetStyle->size.c[axis].kind) - { - case UI_SIZE_TEXT: - printf("text\n"); - break; - - case UI_SIZE_CHILDREN: - printf("children\n"); - break; - - case UI_SIZE_PARENT: - printf("parent: %f\n", value); - break; - - case UI_SIZE_PARENT_MINUS_PIXELS: - printf("parent minus pixels: %f\n", value); - break; - - case UI_SIZE_PIXELS: - printf("pixels: %f\n", value); - break; - } - -} - -void debug_print_styles(ui_box* box, int indent) -{ - debug_print_indent(indent); - printf("### box '%.*s'\n", (int)box->string.len, box->string.ptr); - indent++; - - debug_print_indent(indent); - printf("font size: %f\n", box->targetStyle->fontSize); - - debug_print_size(box, UI_AXIS_X, indent); - debug_print_size(box, UI_AXIS_Y, indent); - - if(!list_empty(&box->beforeRules)) - { - debug_print_indent(indent); - printf("before rules:\n"); - for_list(&box->beforeRules, rule, ui_style_rule, boxElt) - { - debug_print_indent(indent+1); - debug_print_rule(rule); - } - } - - if(!list_empty(&box->afterRules)) - { - debug_print_indent(indent); - printf("after rules:\n"); - for_list(&box->afterRules, rule, ui_style_rule, boxElt) - { - debug_print_indent(indent+1); - debug_print_rule(rule); - } - } - - if(!list_empty(&box->children)) - { - debug_print_indent(indent); - printf("children:\n"); - indent++; - for_list(&box->children, child, ui_box, listElt) - { - debug_print_styles(child, indent); - } - } -} - -mg_font create_font() -{ - //NOTE(martin): create font - str8 fontPath = mp_app_get_resource_path(mem_scratch(), "../resources/OpenSansLatinSubset.ttf"); - char* fontPathCString = str8_to_cstring(mem_scratch(), fontPath); - - FILE* fontFile = fopen(fontPathCString, "r"); - if(!fontFile) - { - log_error("Could not load font file '%s': %s\n", fontPathCString, strerror(errno)); - return(mg_font_nil()); - } - unsigned char* fontData = 0; - fseek(fontFile, 0, SEEK_END); - u32 fontDataSize = ftell(fontFile); - rewind(fontFile); - fontData = (unsigned char*)malloc(fontDataSize); - fread(fontData, 1, fontDataSize, fontFile); - fclose(fontFile); - - unicode_range ranges[5] = {UNICODE_RANGE_BASIC_LATIN, - UNICODE_RANGE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, - UNICODE_RANGE_LATIN_EXTENDED_A, - UNICODE_RANGE_LATIN_EXTENDED_B, - UNICODE_RANGE_SPECIALS}; - - mg_font font = mg_font_create_from_memory(fontDataSize, fontData, 5, ranges); - free(fontData); - - return(font); -} - -void widget_begin_view(char* str) -{ - ui_style_next(&(ui_style){.layout.axis = UI_AXIS_Y, - .layout.spacing = 10, - .layout.margin.x = 10, - .layout.margin.y = 10, - .layout.align.x = UI_ALIGN_CENTER, - .layout.align.y = UI_ALIGN_START}, - UI_STYLE_LAYOUT); - - ui_box_begin(str, UI_FLAG_DRAW_BORDER); - ui_label(str); - -} - -void widget_end_view(void) -{ - ui_box_end(); -} - -#define widget_view(s) defer_loop(widget_begin_view(s), widget_end_view()) - -int main() -{ - LogLevel(LOG_LEVEL_WARNING); - - mp_init(); - mp_clock_init(); //TODO put that in mp_init()? - - ui_init(); - - mp_rect windowRect = {.x = 100, .y = 100, .w = 810, .h = 610}; - mp_window window = mp_window_create(windowRect, "test", 0); - - mp_rect contentRect = mp_window_get_content_rect(window); - - //NOTE: create surface - mg_surface surface = mg_surface_create_for_window(window, MG_BACKEND_DEFAULT); - mg_surface_swap_interval(surface, 0); - - //TODO: create canvas - mg_canvas canvas = mg_canvas_create(surface); - - if(mg_canvas_is_nil(canvas)) - { - printf("Error: couldn't create canvas\n"); - return(-1); - } - - mg_font font = create_font(); - - mem_arena textArena = {0}; - mem_arena_init(&textArena); - - // start app - mp_window_bring_to_front(window); - mp_window_focus(window); - - while(!mp_should_quit()) - { - bool printDebugStyle = false; - - f64 startTime = mp_get_time(MP_CLOCK_MONOTONIC); - - mp_pump_events(0); - mp_event event = {0}; - while(mp_next_event(&event)) - { - switch(event.type) - { - case MP_EVENT_WINDOW_CLOSE: - { - mp_request_quit(); - } break; - - - case MP_EVENT_KEYBOARD_KEY: - { - if(event.key.action == MP_KEY_PRESS && event.key.code == MP_KEY_P) - { - printDebugStyle = true; - } - } break; - - default: - break; - } - } - - //TEST UI - ui_style defaultStyle = {.bgColor = {0}, - .color = {1, 1, 1, 1}, - .font = font, - .fontSize = 16, - .borderColor = {1, 0, 0, 1}, - .borderSize = 2}; - - ui_style_mask defaultMask = UI_STYLE_BG_COLOR - | UI_STYLE_COLOR - | UI_STYLE_BORDER_COLOR - | UI_STYLE_BORDER_SIZE - | UI_STYLE_FONT - | UI_STYLE_FONT_SIZE; - - ui_flags debugFlags = UI_FLAG_DRAW_BORDER; - - ui_box* root = 0; - ui_frame(&defaultStyle, defaultMask) - { - root = ui_box_top(); - ui_style_match_before(ui_pattern_all(), &defaultStyle, defaultMask); - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, - .size.height = {UI_SIZE_PARENT, 1}, - .layout.axis = UI_AXIS_Y, - .layout.align.x = UI_ALIGN_CENTER, - .layout.align.y = UI_ALIGN_START, - .layout.spacing = 10, - .layout.margin.x = 10, - .layout.margin.y = 10, - .bgColor = {0.11, 0.11, 0.11, 1}}, - UI_STYLE_SIZE - | UI_STYLE_LAYOUT - | UI_STYLE_BG_COLOR); - - ui_container("background", UI_FLAG_DRAW_BACKGROUND) - { - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, - .size.height = {UI_SIZE_CHILDREN}, - .layout.align.x = UI_ALIGN_CENTER}, - UI_STYLE_SIZE - |UI_STYLE_LAYOUT_ALIGN_X); - ui_container("title", debugFlags) - { - ui_style_next(&(ui_style){.fontSize = 26}, UI_STYLE_FONT_SIZE); - ui_label("Milepost UI Demo"); - - if(ui_box_sig(ui_box_top()).hovering) - { - ui_tooltip("tooltip") - { - ui_style_next(&(ui_style){.bgColor = {1, 0.99, 0.82, 1}}, - UI_STYLE_BG_COLOR); - - ui_container("background", UI_FLAG_DRAW_BACKGROUND) - { - ui_style_next(&(ui_style){.color = {0, 0, 0, 1}}, - UI_STYLE_COLOR); - - ui_label("That is a tooltip!"); - } - } - } - } - - ui_menu_bar("Menu bar") - { - ui_menu("Menu 1") - { - if(ui_menu_button("Option 1.1").pressed) - { - printf("Pressed option 1.1\n"); - } - ui_menu_button("Option 1.2"); - ui_menu_button("Option 1.3"); - ui_menu_button("Option 1.4"); - } - - ui_menu("Menu 2") - { - ui_menu_button("Option 2.1"); - ui_menu_button("Option 2.2"); - ui_menu_button("Option 2.3"); - ui_menu_button("Option 2.4"); - } - - ui_menu("Menu 3") - { - ui_menu_button("Option 3.1"); - ui_menu_button("Option 3.2"); - ui_menu_button("Option 3.3"); - ui_menu_button("Option 3.4"); - } - } - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, - .size.height = {UI_SIZE_PARENT, 1, 1}}, - UI_STYLE_SIZE); - - ui_style_next(&(ui_style){.layout.axis = UI_AXIS_X}, UI_STYLE_LAYOUT_AXIS); - ui_container("contents", debugFlags) - { - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 0.5}, - .size.height = {UI_SIZE_PARENT, 1}, - .borderColor = {0, 0, 1, 1}}, - UI_STYLE_SIZE - |UI_STYLE_BORDER_COLOR); - - ui_container("left", debugFlags) - { - ui_style_next(&(ui_style){.layout.axis = UI_AXIS_X, - .layout.spacing = 10, - .layout.margin.x = 10, - .layout.margin.y = 10, - .size.width = {UI_SIZE_PARENT, 1}, - .size.height = {UI_SIZE_PARENT, 0.5}}, - UI_STYLE_LAYOUT_AXIS - |UI_STYLE_LAYOUT_SPACING - |UI_STYLE_LAYOUT_MARGIN_X - |UI_STYLE_LAYOUT_MARGIN_Y - |UI_STYLE_SIZE); - - ui_container("up", debugFlags) - { - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 0.5}, - .size.height = {UI_SIZE_PARENT, 1}}, - UI_STYLE_SIZE); - widget_view("Buttons") - { - ui_button("Button 1"); - ui_button("Button 2"); - ui_button("Button 3"); - } - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 0.5}, - .size.height = {UI_SIZE_PARENT, 1}}, - UI_STYLE_SIZE); - - - ui_pattern pattern = {0}; - ui_pattern_push(mem_scratch(), &pattern, (ui_selector){.kind = UI_SEL_TAG, .tag = ui_tag_make("checkbox")}); - ui_style_match_after(pattern, - &(ui_style){.bgColor = {0, 1, 0, 1}, - .color = {1, 1, 1, 1}}, - UI_STYLE_COLOR | UI_STYLE_BG_COLOR); - - widget_view("checkboxes") - { - static bool check1 = true; - static bool check2 = false; - static bool check3 = false; - - ui_checkbox("check1", &check1); - ui_checkbox("check2", &check2); - ui_checkbox("check3", &check3); - } - } - - ui_style_next(&(ui_style){.layout.axis = UI_AXIS_X, - .size.width = {UI_SIZE_PARENT, 1}, - .size.height = {UI_SIZE_PARENT, 0.5}}, - UI_STYLE_LAYOUT_AXIS - |UI_STYLE_SIZE); - - ui_container("down", debugFlags) - { - widget_view("Vertical Sliders") - { - ui_style_next(&(ui_style){.layout.axis = UI_AXIS_X, - .layout.spacing = 10}, - UI_STYLE_LAYOUT_AXIS - |UI_STYLE_LAYOUT_SPACING); - ui_container("contents", 0) - { - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 20}, - .size.height = {UI_SIZE_PIXELS, 200}}, - UI_STYLE_SIZE); - static f32 slider1 = 0; - ui_slider("slider1", 0.2, &slider1); - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 20}, - .size.height = {UI_SIZE_PIXELS, 200}}, - UI_STYLE_SIZE); - static f32 slider2 = 0; - ui_slider("slider2", 0.2, &slider2); - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 20}, - .size.height = {UI_SIZE_PIXELS, 200}}, - UI_STYLE_SIZE); - static f32 slider3 = 0; - ui_slider("slider3", 0.2, &slider3); - } - } - - widget_view("Horizontal Sliders") - { - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 200}, - .size.height = {UI_SIZE_PIXELS, 20}}, - UI_STYLE_SIZE); - static f32 slider1 = 0; - ui_slider("slider1", 0.2, &slider1); - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 200}, - .size.height = {UI_SIZE_PIXELS, 20}}, - UI_STYLE_SIZE); - static f32 slider2 = 0; - ui_slider("slider2", 0.2, &slider2); - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 200}, - .size.height = {UI_SIZE_PIXELS, 20}}, - UI_STYLE_SIZE); - static f32 slider3 = 0; - ui_slider("slider3", 0.2, &slider3); - } - } - } - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 0.5}, - .size.height = {UI_SIZE_PARENT, 1}}, - UI_STYLE_SIZE); - - ui_container("right", debugFlags) - { - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, - .size.height = {UI_SIZE_PARENT, 0.33}}, - UI_STYLE_SIZE); - widget_view("Text box") - { - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 300}, - .size.height = {UI_SIZE_TEXT}}, - UI_STYLE_SIZE); - static str8 text = {0}; - ui_text_box_result res = ui_text_box("textbox", mem_scratch(), text); - if(res.changed) - { - mem_arena_clear(&textArena); - text = str8_push_copy(&textArena, res.text); - } - } - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, - .size.height = {UI_SIZE_PARENT, 0.33}}, - UI_STYLE_SIZE); - widget_view("Test") - { - ui_pattern pattern = {}; - ui_pattern_push(mem_scratch(), &pattern, (ui_selector){.kind = UI_SEL_TEXT, .text = STR8("panel")}); - ui_style_match_after(pattern, &(ui_style){.bgColor = {0.3, 0.3, 1, 1}}, UI_STYLE_BG_COLOR); - - static int selected = 0; - str8 options[] = {STR8("option 1"), - STR8("option 2"), - STR8("long option 3"), - STR8("option 4"), - STR8("option 5")}; - ui_select_popup_info info = {.selectedIndex = selected, - .optionCount = 5, - .options = options}; - - ui_select_popup_info result = ui_select_popup("popup", &info); - selected = result.selectedIndex; - } - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, - .size.height = {UI_SIZE_PARENT, 0.33}}, - UI_STYLE_SIZE); - widget_view("Color") - { - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, - .size.height = {UI_SIZE_PARENT, 0.7}, - .layout.axis = UI_AXIS_X}, - UI_STYLE_SIZE - |UI_STYLE_LAYOUT_AXIS); - - ui_panel("Panel", UI_FLAG_DRAW_BORDER) - { - ui_style_next(&(ui_style){.layout.axis = UI_AXIS_X}, - UI_STYLE_LAYOUT_AXIS); - ui_container("contents", 0) - { - ui_style_next(&(ui_style){.layout.spacing = 20}, - UI_STYLE_LAYOUT_SPACING); - ui_container("buttons", 0) - { - ui_button("Button A"); - ui_button("Button B"); - ui_button("Button C"); - ui_button("Button D"); - } - - ui_style_next(&(ui_style){.layout.axis = UI_AXIS_X, - .layout.spacing = 20}, - UI_STYLE_LAYOUT_SPACING - |UI_STYLE_LAYOUT_AXIS); - - ui_container("buttons2", 0) - { - ui_button("Button A"); - ui_button("Button B"); - ui_button("Button C"); - ui_button("Button D"); - } - } - } - } - - } - } - } - } - if(printDebugStyle) - { - debug_print_styles(root, 0); - } - - mg_surface_prepare(surface); - - ui_draw(); - - mg_flush(); - mg_surface_present(surface); - - mem_arena_clear(mem_scratch()); - } - - mg_surface_destroy(surface); - mp_terminate(); - - return(0); -} +/************************************************************//** +* +* @file: main.cpp +* @author: Martin Fouilleul +* @date: 30/07/2022 +* @revision: +* +*****************************************************************/ +#include +#include +#include +#include + +#define _USE_MATH_DEFINES //NOTE: necessary for MSVC +#include + +#include"milepost.h" + +void debug_print_indent(int indent) +{ + for(int i=0; ipattern.l, selector, ui_selector, listElt) + { + switch(selector->kind) + { + case UI_SEL_ANY: + printf("any: "); + break; + + case UI_SEL_OWNER: + printf("owner: "); + break; + + case UI_SEL_TEXT: + printf("text='%.*s': ", (int)selector->text.len, selector->text.ptr); + break; + + case UI_SEL_TAG: + printf("tag=0x%llx: ", selector->tag.hash); + break; + + case UI_SEL_STATUS: + { + if(selector->status & UI_HOVER) + { + printf("hover: "); + } + if(selector->status & UI_ACTIVE) + { + printf("active: "); + } + if(selector->status & UI_DRAGGING) + { + printf("dragging: "); + } + } break; + + case UI_SEL_KEY: + printf("key=0x%llx: ", selector->key.hash); + break; + + default: + printf("unknown: "); + break; + } + } + printf("=> font size = %f\n", rule->style->fontSize); +} +void debug_print_size(ui_box* box, ui_axis axis, int indent) +{ + debug_print_indent(indent); + printf("size %s: ", axis == UI_AXIS_X ? "x" : "y"); + f32 value = box->targetStyle->size.c[axis].value; + switch(box->targetStyle->size.c[axis].kind) + { + case UI_SIZE_TEXT: + printf("text\n"); + break; + + case UI_SIZE_CHILDREN: + printf("children\n"); + break; + + case UI_SIZE_PARENT: + printf("parent: %f\n", value); + break; + + case UI_SIZE_PARENT_MINUS_PIXELS: + printf("parent minus pixels: %f\n", value); + break; + + case UI_SIZE_PIXELS: + printf("pixels: %f\n", value); + break; + } + +} + +void debug_print_styles(ui_box* box, int indent) +{ + debug_print_indent(indent); + printf("### box '%.*s'\n", (int)box->string.len, box->string.ptr); + indent++; + + debug_print_indent(indent); + printf("font size: %f\n", box->targetStyle->fontSize); + + debug_print_size(box, UI_AXIS_X, indent); + debug_print_size(box, UI_AXIS_Y, indent); + + if(!list_empty(&box->beforeRules)) + { + debug_print_indent(indent); + printf("before rules:\n"); + for_list(&box->beforeRules, rule, ui_style_rule, boxElt) + { + debug_print_indent(indent+1); + debug_print_rule(rule); + } + } + + if(!list_empty(&box->afterRules)) + { + debug_print_indent(indent); + printf("after rules:\n"); + for_list(&box->afterRules, rule, ui_style_rule, boxElt) + { + debug_print_indent(indent+1); + debug_print_rule(rule); + } + } + + if(!list_empty(&box->children)) + { + debug_print_indent(indent); + printf("children:\n"); + indent++; + for_list(&box->children, child, ui_box, listElt) + { + debug_print_styles(child, indent); + } + } +} + +mg_font create_font() +{ + //NOTE(martin): create font + str8 fontPath = mp_app_get_resource_path(mem_scratch(), "../resources/OpenSansLatinSubset.ttf"); + char* fontPathCString = str8_to_cstring(mem_scratch(), fontPath); + + FILE* fontFile = fopen(fontPathCString, "r"); + if(!fontFile) + { + log_error("Could not load font file '%s': %s\n", fontPathCString, strerror(errno)); + return(mg_font_nil()); + } + unsigned char* fontData = 0; + fseek(fontFile, 0, SEEK_END); + u32 fontDataSize = ftell(fontFile); + rewind(fontFile); + fontData = (unsigned char*)malloc(fontDataSize); + fread(fontData, 1, fontDataSize, fontFile); + fclose(fontFile); + + unicode_range ranges[5] = {UNICODE_RANGE_BASIC_LATIN, + UNICODE_RANGE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, + UNICODE_RANGE_LATIN_EXTENDED_A, + UNICODE_RANGE_LATIN_EXTENDED_B, + UNICODE_RANGE_SPECIALS}; + + mg_font font = mg_font_create_from_memory(fontDataSize, fontData, 5, ranges); + free(fontData); + + return(font); +} + +void widget_begin_view(char* str) +{ + ui_style_next(&(ui_style){.layout.axis = UI_AXIS_Y, + .layout.spacing = 10, + .layout.margin.x = 10, + .layout.margin.y = 10, + .layout.align.x = UI_ALIGN_CENTER, + .layout.align.y = UI_ALIGN_START}, + UI_STYLE_LAYOUT); + + ui_box_begin(str, UI_FLAG_DRAW_BORDER); + ui_label(str); + +} + +void widget_end_view(void) +{ + ui_box_end(); +} + +#define widget_view(s) defer_loop(widget_begin_view(s), widget_end_view()) + +int main() +{ + mp_init(); + mp_clock_init(); //TODO put that in mp_init()? + + ui_context context; + ui_init(&context); + ui_set_context(&context); + + mp_rect windowRect = {.x = 100, .y = 100, .w = 810, .h = 610}; + mp_window window = mp_window_create(windowRect, "test", 0); + + mp_rect contentRect = mp_window_get_content_rect(window); + + //NOTE: create surface + mg_surface surface = mg_surface_create_for_window(window, MG_CANVAS); + mg_surface_swap_interval(surface, 0); + + //TODO: create canvas + mg_canvas canvas = mg_canvas_create(); + + if(mg_canvas_is_nil(canvas)) + { + printf("Error: couldn't create canvas\n"); + return(-1); + } + + mg_font font = create_font(); + + mem_arena textArena = {0}; + mem_arena_init(&textArena); + + // start app + mp_window_bring_to_front(window); + mp_window_focus(window); + + while(!mp_should_quit()) + { + bool printDebugStyle = false; + + f64 startTime = mp_get_time(MP_CLOCK_MONOTONIC); + + mp_pump_events(0); + mp_event* event = 0; + while((event = mp_next_event(mem_scratch())) != 0) + { + ui_process_event(event); + + switch(event->type) + { + case MP_EVENT_WINDOW_CLOSE: + { + mp_request_quit(); + } break; + + + case MP_EVENT_KEYBOARD_KEY: + { + if(event->key.action == MP_KEY_PRESS && event->key.code == MP_KEY_P) + { + printDebugStyle = true; + } + } break; + + default: + break; + } + } + + //TEST UI + ui_style defaultStyle = {.bgColor = {0}, + .color = {1, 1, 1, 1}, + .font = font, + .fontSize = 16, + .borderColor = {1, 0, 0, 1}, + .borderSize = 2}; + + ui_style_mask defaultMask = UI_STYLE_BG_COLOR + | UI_STYLE_COLOR + | UI_STYLE_BORDER_COLOR + | UI_STYLE_BORDER_SIZE + | UI_STYLE_FONT + | UI_STYLE_FONT_SIZE; + + ui_flags debugFlags = UI_FLAG_DRAW_BORDER; + + ui_box* root = 0; + + mp_rect frameRect = mg_surface_get_frame(surface); + vec2 frameSize = {frameRect.w, frameRect.h}; + + ui_frame(frameSize, &defaultStyle, defaultMask) + { + root = ui_box_top(); + ui_style_match_before(ui_pattern_all(), &defaultStyle, defaultMask); + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, + .size.height = {UI_SIZE_PARENT, 1}, + .layout.axis = UI_AXIS_Y, + .layout.align.x = UI_ALIGN_CENTER, + .layout.align.y = UI_ALIGN_START, + .layout.spacing = 10, + .layout.margin.x = 10, + .layout.margin.y = 10, + .bgColor = {0.11, 0.11, 0.11, 1}}, + UI_STYLE_SIZE + | UI_STYLE_LAYOUT + | UI_STYLE_BG_COLOR); + + ui_container("background", UI_FLAG_DRAW_BACKGROUND) + { + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, + .size.height = {UI_SIZE_CHILDREN}, + .layout.align.x = UI_ALIGN_CENTER}, + UI_STYLE_SIZE + |UI_STYLE_LAYOUT_ALIGN_X); + ui_container("title", debugFlags) + { + ui_style_next(&(ui_style){.fontSize = 26}, UI_STYLE_FONT_SIZE); + ui_label("Milepost UI Demo"); + + if(ui_box_sig(ui_box_top()).hovering) + { + ui_tooltip("tooltip") + { + ui_style_next(&(ui_style){.bgColor = {1, 0.99, 0.82, 1}}, + UI_STYLE_BG_COLOR); + + ui_container("background", UI_FLAG_DRAW_BACKGROUND) + { + ui_style_next(&(ui_style){.color = {0, 0, 0, 1}}, + UI_STYLE_COLOR); + + ui_label("That is a tooltip!"); + } + } + } + } + + ui_menu_bar("Menu bar") + { + ui_menu("Menu 1") + { + if(ui_menu_button("Option 1.1").pressed) + { + printf("Pressed option 1.1\n"); + } + ui_menu_button("Option 1.2"); + ui_menu_button("Option 1.3"); + ui_menu_button("Option 1.4"); + } + + ui_menu("Menu 2") + { + ui_menu_button("Option 2.1"); + ui_menu_button("Option 2.2"); + ui_menu_button("Option 2.3"); + ui_menu_button("Option 2.4"); + } + + ui_menu("Menu 3") + { + ui_menu_button("Option 3.1"); + ui_menu_button("Option 3.2"); + ui_menu_button("Option 3.3"); + ui_menu_button("Option 3.4"); + } + } + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, + .size.height = {UI_SIZE_PARENT, 1, 1}}, + UI_STYLE_SIZE); + + ui_style_next(&(ui_style){.layout.axis = UI_AXIS_X}, UI_STYLE_LAYOUT_AXIS); + ui_container("contents", debugFlags) + { + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 0.5}, + .size.height = {UI_SIZE_PARENT, 1}, + .borderColor = {0, 0, 1, 1}}, + UI_STYLE_SIZE + |UI_STYLE_BORDER_COLOR); + + ui_container("left", debugFlags) + { + ui_style_next(&(ui_style){.layout.axis = UI_AXIS_X, + .layout.spacing = 10, + .layout.margin.x = 10, + .layout.margin.y = 10, + .size.width = {UI_SIZE_PARENT, 1}, + .size.height = {UI_SIZE_PARENT, 0.5}}, + UI_STYLE_LAYOUT_AXIS + |UI_STYLE_LAYOUT_SPACING + |UI_STYLE_LAYOUT_MARGIN_X + |UI_STYLE_LAYOUT_MARGIN_Y + |UI_STYLE_SIZE); + + ui_container("up", debugFlags) + { + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 0.5}, + .size.height = {UI_SIZE_PARENT, 1}}, + UI_STYLE_SIZE); + widget_view("Buttons") + { + if(ui_button("Test Dialog").clicked) + { + char* options[] = {"Accept", "Reject"}; + int res = mp_alert_popup("test dialog", "dialog message", 2, options); + if(res >= 0) + { + printf("selected options %i: %s\n", res, options[res]); + } + else + { + printf("no options selected\n"); + } + } + + if(ui_button("Open").clicked) + { + char* filters[] = {"md"}; + str8 file = mp_open_dialog(mem_scratch(), "Open File", "C:\\Users", 1, filters, false); + printf("selected file %.*s\n", (int)file.len, file.ptr); + } + + if(ui_button("Save").clicked) + { + str8 file = mp_save_dialog(mem_scratch(), "Save File", "C:\\Users", 0, 0); + printf("selected file %.*s\n", (int)file.len, file.ptr); + } + } + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 0.5}, + .size.height = {UI_SIZE_PARENT, 1}}, + UI_STYLE_SIZE); + + + ui_pattern pattern = {0}; + ui_pattern_push(mem_scratch(), &pattern, (ui_selector){.kind = UI_SEL_TAG, .tag = ui_tag_make("checkbox")}); + ui_style_match_after(pattern, + &(ui_style){.bgColor = {0, 1, 0, 1}, + .color = {1, 1, 1, 1}}, + UI_STYLE_COLOR | UI_STYLE_BG_COLOR); + + widget_view("checkboxes") + { + static bool check1 = true; + static bool check2 = false; + static bool check3 = false; + + ui_checkbox("check1", &check1); + ui_checkbox("check2", &check2); + ui_checkbox("check3", &check3); + } + } + + ui_style_next(&(ui_style){.layout.axis = UI_AXIS_X, + .size.width = {UI_SIZE_PARENT, 1}, + .size.height = {UI_SIZE_PARENT, 0.5}}, + UI_STYLE_LAYOUT_AXIS + |UI_STYLE_SIZE); + + ui_container("down", debugFlags) + { + widget_view("Vertical Sliders") + { + ui_style_next(&(ui_style){.layout.axis = UI_AXIS_X, + .layout.spacing = 10}, + UI_STYLE_LAYOUT_AXIS + |UI_STYLE_LAYOUT_SPACING); + ui_container("contents", 0) + { + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 20}, + .size.height = {UI_SIZE_PIXELS, 200}}, + UI_STYLE_SIZE); + static f32 slider1 = 0; + ui_slider("slider1", 0.2, &slider1); + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 20}, + .size.height = {UI_SIZE_PIXELS, 200}}, + UI_STYLE_SIZE); + static f32 slider2 = 0; + ui_slider("slider2", 0.2, &slider2); + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 20}, + .size.height = {UI_SIZE_PIXELS, 200}}, + UI_STYLE_SIZE); + static f32 slider3 = 0; + ui_slider("slider3", 0.2, &slider3); + } + } + + widget_view("Horizontal Sliders") + { + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 200}, + .size.height = {UI_SIZE_PIXELS, 20}}, + UI_STYLE_SIZE); + static f32 slider1 = 0; + ui_slider("slider1", 0.2, &slider1); + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 200}, + .size.height = {UI_SIZE_PIXELS, 20}}, + UI_STYLE_SIZE); + static f32 slider2 = 0; + ui_slider("slider2", 0.2, &slider2); + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 200}, + .size.height = {UI_SIZE_PIXELS, 20}}, + UI_STYLE_SIZE); + static f32 slider3 = 0; + ui_slider("slider3", 0.2, &slider3); + } + } + } + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 0.5}, + .size.height = {UI_SIZE_PARENT, 1}}, + UI_STYLE_SIZE); + + ui_container("right", debugFlags) + { + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, + .size.height = {UI_SIZE_PARENT, 0.33}}, + UI_STYLE_SIZE); + widget_view("Text box") + { + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 300}, + .size.height = {UI_SIZE_TEXT}}, + UI_STYLE_SIZE); + static str8 text = {0}; + ui_text_box_result res = ui_text_box("textbox", mem_scratch(), text); + if(res.changed) + { + mem_arena_clear(&textArena); + text = str8_push_copy(&textArena, res.text); + } + } + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, + .size.height = {UI_SIZE_PARENT, 0.33}}, + UI_STYLE_SIZE); + widget_view("Test") + { + ui_pattern pattern = {0}; + ui_pattern_push(mem_scratch(), &pattern, (ui_selector){.kind = UI_SEL_TEXT, .text = STR8("panel")}); + ui_style_match_after(pattern, &(ui_style){.bgColor = {0.3, 0.3, 1, 1}}, UI_STYLE_BG_COLOR); + + static int selected = 0; + str8 options[] = {STR8("option 1"), + STR8("option 2"), + STR8("long option 3"), + STR8("option 4"), + STR8("option 5")}; + ui_select_popup_info info = {.selectedIndex = selected, + .optionCount = 5, + .options = options}; + + ui_select_popup_info result = ui_select_popup("popup", &info); + selected = result.selectedIndex; + } + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, + .size.height = {UI_SIZE_PARENT, 0.33}}, + UI_STYLE_SIZE); + widget_view("Color") + { + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, + .size.height = {UI_SIZE_PARENT, 0.7}, + .layout.axis = UI_AXIS_X}, + UI_STYLE_SIZE + |UI_STYLE_LAYOUT_AXIS); + + ui_panel("Panel", UI_FLAG_DRAW_BORDER) + { + ui_style_next(&(ui_style){.layout.axis = UI_AXIS_X}, + UI_STYLE_LAYOUT_AXIS); + ui_container("contents", 0) + { + ui_style_next(&(ui_style){.layout.spacing = 20}, + UI_STYLE_LAYOUT_SPACING); + ui_container("buttons", 0) + { + ui_button("Button A"); + ui_button("Button B"); + ui_button("Button C"); + ui_button("Button D"); + } + + ui_style_next(&(ui_style){.layout.axis = UI_AXIS_X, + .layout.spacing = 20}, + UI_STYLE_LAYOUT_SPACING + |UI_STYLE_LAYOUT_AXIS); + + ui_container("buttons2", 0) + { + ui_button("Button A"); + ui_button("Button B"); + ui_button("Button C"); + ui_button("Button D"); + } + } + } + } + + } + } + } + } + if(printDebugStyle) + { + debug_print_styles(root, 0); + } + + mg_surface_prepare(surface); + + ui_draw(); + + mg_render(surface, canvas); + mg_surface_present(surface); + + mem_arena_clear(mem_scratch()); + } + + mg_surface_destroy(surface); + mp_terminate(); + + return(0); +} diff --git a/ext/angle_install_notes.md b/ext/angle_install_notes.md index fae4821..c49a07c 100644 --- a/ext/angle_install_notes.md +++ b/ext/angle_install_notes.md @@ -1,77 +1,84 @@ -## Angle install on macOS - -* Get ninja if needed: `brew install ninja` -* Get the `depot_tools`repo: `git clone https://chromium.googlesource.com/* chromium/tools/depot_tools.git` -* Set path: `export PATH=/path/to/depot_tools:$PATH` - -* Maybe necessary to fiddle with certificates here, otherwise `fetch angle` fails in the subsequent steps. - -``` -cd /Applications/Python\ 3.6 -sudo ./Install\ Certificates.command -``` -* Fetch angle: - -``` -mkdir angle -cd angle -fetch angle -``` -* Generate build config: `gn gen out/Debug` - - * To see available arguments: `gn args out/Debug --list` - * To change arguments: `gn args out/Debug` - -For example, to generate dwarf dsyms files, set: - -``` -enable_dsyms=true -use_debug_fission=true -symbol_level=2 -``` - -We also need to set `is_component_build=false` in order to have self-contained librarries. - -Then, build with `autoninja -C out/Debug`and wait until you pass out. - -## Angle install on windows - -* need Python3 (can install through win app store) -* need Windows SDK -* clone `depot_tools`: `git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git` -or download and unzip bundle at [https://storage.googleapis.com/chrome-infra/depot_tools.zip](https://storage.googleapis.com/chrome-infra/depot_tools.zip) -* set `depot_tools` in path env variable through control panel>System and security>system>advanced system settings -* run `gclient` in a cmd shell -* set `DEPOT_TOOLS_WIN_TOOLCHAIN=0` -* `mkdir angle` -* `cd angle` -* `fetch angle` -* wait a million years - -* if it fails when running `python3 third_party/depot_tools/download_from_google_storage.py ...` - -> open `DEPS` and change `third_party/depot_tools` to `../depot/tools` -* run `gclient sync` to complete previous step - -* `gn gen out/Debug` -* `gn args out/Debug` and edit arguments: - * `angle_enable_vulkan = false` - * `angle_build_tests = false` - * `is_component_build = false` - -* link with `libEGL.dll.lib` and `libGLESv2.dll.lib` -* put `libEGL.dll` and `libGLESv2.dll` in same directory as executable - -## To get debugging kinda working with renderdoc: - -Run `gn args out/Debug` and set - * `angle_enable_trace = true` - * `angle_enable_annotator_run_time_checks = true` - -* `autoninja -C out/Debug` -* wait a while - -In renderdoc, set env variables -`RENDERDOC_HOOK_EGL 0` (if you want to trace underlying native API) -`RENDERDOC_HOOK_EGL 1` (if you want to trace EGL calls. You also need to put `libEGL` in the renderdoc folder so it's found when capturing stuff. Unfortunately though, that seems to provoke crashes...) - -`ANGLE_ENABLE_DEBUG_MARKERS 1` (to turn on debug markers) +## Angle install on macOS + +* Get ninja if needed: `brew install ninja` +* Get the `depot_tools`repo: `git clone https://chromium.googlesource.com/* chromium/tools/depot_tools.git` +* Set path: `export PATH=/path/to/depot_tools:$PATH` + +* Maybe necessary to fiddle with certificates here, otherwise `fetch angle` fails in the subsequent steps. + +``` +cd /Applications/Python\ 3.6 +sudo ./Install\ Certificates.command +``` +* Fetch angle: + +``` +mkdir angle +cd angle +fetch angle +``` +* Generate build config: `gn gen out/Debug` + + * To see available arguments: `gn args out/Debug --list` + * To change arguments: `gn args out/Debug` + +For example, to generate dwarf dsyms files, set: + +``` +enable_dsyms=true +use_debug_fission=true +symbol_level=2 +``` + +We also need to set `is_component_build=false` in order to have self-contained librarries. + +Then, build with `autoninja -C out/Debug`and wait until you pass out. + +## Angle install on windows + +* need Python3 (can install through win app store) +* need Windows SDK +* clone `depot_tools`: `git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git` +or download and unzip bundle at [https://storage.googleapis.com/chrome-infra/depot_tools.zip](https://storage.googleapis.com/chrome-infra/depot_tools.zip) +* set `depot_tools` in path env variable through control panel>System and security>system>advanced system settings +* run `gclient` in a cmd shell +* set `DEPOT_TOOLS_WIN_TOOLCHAIN=0` +* `mkdir angle` +* `cd angle` +* `fetch angle` +* wait a million years + +* if it fails when running `python3 third_party/depot_tools/download_from_google_storage.py ...` + -> open `DEPS` and change `third_party/depot_tools` to `../depot_tools` +* run `gclient sync` to complete previous step + +* `gn gen out/Debug` +* `gn args out/Debug` and edit arguments: +``` +is_component_build = false +angle_build_tests = false +angle_enable_metal = false +angle_enable_d3d9 = false +angle_enable_gl = false +angle_enable_vulkan = false +``` + + +* `ninja -C out/Debug` +* link with `libEGL.dll.lib` and `libGLESv2.dll.lib` +* put `libEGL.dll` and `libGLESv2.dll` in same directory as executable + +## To get debugging kinda working with renderdoc: + +Run `gn args out/Debug` and set + * `angle_enable_trace = true` + * `angle_enable_annotator_run_time_checks = true` + +* `autoninja -C out/Debug` +* wait a while + +In renderdoc, set env variables +`RENDERDOC_HOOK_EGL 0` (if you want to trace underlying native API) +`RENDERDOC_HOOK_EGL 1` (if you want to trace EGL calls. You also need to put `libEGL` in the renderdoc folder so it's found when capturing stuff. Unfortunately though, that seems to provoke crashes...) + +`ANGLE_ENABLE_DEBUG_MARKERS 1` (to turn on debug markers) diff --git a/src/gl_canvas.c b/src/gl_canvas.c index 8f5d985..7cba07c 100644 --- a/src/gl_canvas.c +++ b/src/gl_canvas.c @@ -1,498 +1,2065 @@ -/************************************************************//** -* -* @file: gl_canvas.c -* @author: Martin Fouilleul -* @date: 29/01/2023 -* @revision: -* -*****************************************************************/ -#include"graphics_surface.h" -#include"macro_helpers.h" -#include"glsl_shaders.h" -#include"gl_api.h" - -typedef struct mg_gl_canvas_backend -{ - mg_canvas_backend interface; - mg_surface surface; - - GLuint vao; - GLuint dummyVertexBuffer; - GLuint vertexBuffer; - GLuint shapeBuffer; - GLuint indexBuffer; - GLuint tileCounterBuffer; - GLuint tileArrayBuffer; - GLuint clearCounterProgram; - GLuint tileProgram; - GLuint sortProgram; - GLuint drawProgram; - GLuint blitProgram; - - GLuint outTexture; - - char* indexMapping; - char* vertexMapping; - char* shapeMapping; - -} mg_gl_canvas_backend; - -typedef struct mg_gl_image -{ - mg_image_data interface; - - GLuint textureID; -} mg_gl_image; - -//NOTE: debugger -typedef struct debug_vertex -{ - vec4 cubic; - vec2 pos; - int shapeIndex; - u8 pad[4]; -} debug_vertex; - -typedef struct debug_shape -{ - vec4 color; - vec4 clip; - vec2 uv; - u8 pad[8]; -} debug_shape; - -#define LayoutNext(prevName, prevType, nextType) \ - AlignUpOnPow2(_cat3_(LAYOUT_, prevName, _OFFSET)+_cat3_(LAYOUT_, prevType, _SIZE), _cat3_(LAYOUT_, nextType, _ALIGN)) - -enum { - LAYOUT_VEC2_SIZE = 8, - LAYOUT_VEC2_ALIGN = 8, - LAYOUT_VEC4_SIZE = 16, - LAYOUT_VEC4_ALIGN = 16, - LAYOUT_INT_SIZE = 4, - LAYOUT_INT_ALIGN = 4, - LAYOUT_MAT2x3_SIZE = sizeof(float)*6, - LAYOUT_MAT2x3_ALIGN = 4, - - LAYOUT_CUBIC_OFFSET = 0, - LAYOUT_POS_OFFSET = LayoutNext(CUBIC, VEC4, VEC2), - LAYOUT_ZINDEX_OFFSET = LayoutNext(POS, VEC2, INT), - LAYOUT_VERTEX_ALIGN = 16, - LAYOUT_VERTEX_SIZE = LayoutNext(ZINDEX, INT, VERTEX), - - LAYOUT_COLOR_OFFSET = 0, - LAYOUT_CLIP_OFFSET = LayoutNext(COLOR, VEC4, VEC4), - LAYOUT_UV_TRANSFORM_OFFSET = LayoutNext(CLIP, VEC4, MAT2x3), - LAYOUT_SHAPE_ALIGN = 16, - LAYOUT_SHAPE_SIZE = LayoutNext(UV_TRANSFORM, MAT2x3, SHAPE), - - MG_GL_CANVAS_MAX_BUFFER_LENGTH = 1<<20, - MG_GL_CANVAS_MAX_SHAPE_BUFFER_SIZE = LAYOUT_SHAPE_SIZE * MG_GL_CANVAS_MAX_BUFFER_LENGTH, - MG_GL_CANVAS_MAX_VERTEX_BUFFER_SIZE = LAYOUT_VERTEX_SIZE * MG_GL_CANVAS_MAX_BUFFER_LENGTH, - MG_GL_CANVAS_MAX_INDEX_BUFFER_SIZE = LAYOUT_INT_SIZE * MG_GL_CANVAS_MAX_BUFFER_LENGTH, - - //TODO: actually size this dynamically - MG_GL_CANVAS_MAX_TILE_COUNT = 65536, //NOTE: this allows for 256*256 tiles (e.g. 4096*4096 pixels) - MG_GL_CANVAS_TILE_COUNTER_BUFFER_SIZE = LAYOUT_INT_SIZE * MG_GL_CANVAS_MAX_TILE_COUNT, - - MG_GL_CANVAS_TILE_ARRAY_LENGTH = 1<<10, // max overlapping triangles per tiles - MG_GL_CANVAS_TILE_ARRAY_BUFFER_SIZE = LAYOUT_INT_SIZE * MG_GL_CANVAS_MAX_TILE_COUNT * MG_GL_CANVAS_TILE_ARRAY_LENGTH, -}; - -void mg_gl_canvas_update_vertex_layout(mg_gl_canvas_backend* backend) -{ - backend->interface.vertexLayout = (mg_vertex_layout){ - .maxVertexCount = MG_GL_CANVAS_MAX_BUFFER_LENGTH, - .maxIndexCount = MG_GL_CANVAS_MAX_BUFFER_LENGTH, - .posBuffer = backend->vertexMapping + LAYOUT_POS_OFFSET, - .posStride = LAYOUT_VERTEX_SIZE, - .cubicBuffer = backend->vertexMapping + LAYOUT_CUBIC_OFFSET, - .cubicStride = LAYOUT_VERTEX_SIZE, - .shapeIndexBuffer = backend->vertexMapping + LAYOUT_ZINDEX_OFFSET, - .shapeIndexStride = LAYOUT_VERTEX_SIZE, - - .colorBuffer = backend->shapeMapping + LAYOUT_COLOR_OFFSET, - .colorStride = LAYOUT_SHAPE_SIZE, - .clipBuffer = backend->shapeMapping + LAYOUT_CLIP_OFFSET, - .clipStride = LAYOUT_SHAPE_SIZE, - .uvTransformBuffer = backend->shapeMapping + LAYOUT_UV_TRANSFORM_OFFSET, - .uvTransformStride = LAYOUT_SHAPE_SIZE, - - .indexBuffer = backend->indexMapping, - .indexStride = LAYOUT_INT_SIZE}; -} - -void mg_gl_send_buffers(mg_gl_canvas_backend* backend, int shapeCount, int vertexCount, int indexCount) -{ - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->vertexBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, LAYOUT_VERTEX_SIZE*vertexCount, backend->vertexMapping, GL_STREAM_DRAW); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->shapeBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, LAYOUT_SHAPE_SIZE*shapeCount, backend->shapeMapping, GL_STREAM_DRAW); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->indexBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, LAYOUT_INT_SIZE*indexCount, backend->indexMapping, GL_STREAM_DRAW); -} - -void mg_gl_canvas_begin(mg_canvas_backend* interface) -{ - mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; - glEnable(GL_BLEND); - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); -} - -void mg_gl_canvas_end(mg_canvas_backend* interface) -{ - //NOTE: nothing to do here... -} - -void mg_gl_canvas_clear(mg_canvas_backend* interface, mg_color clearColor) -{ - mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; - - glClearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a); - glClear(GL_COLOR_BUFFER_BIT); -} - -void mg_gl_canvas_draw_batch(mg_canvas_backend* interface, mg_image_data* imageInterface, u32 shapeCount, u32 vertexCount, u32 indexCount) -{ - mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; - -/*NOTE: if we want debug_vertex while debugging, the following ensures the struct def doesn't get stripped away - debug_vertex vertex; - debug_shape shape; - printf("foo %p, bar %p\n", &vertex, &shape); -//*/ - mg_gl_send_buffers(backend, shapeCount, vertexCount, indexCount); - - mp_rect frame = mg_surface_get_frame(backend->surface); - vec2 contentsScaling = mg_surface_contents_scaling(backend->surface); - - const int tileSize = 16; - const int tileCountX = (frame.w*contentsScaling.x + tileSize - 1)/tileSize; - const int tileCountY = (frame.h*contentsScaling.y + tileSize - 1)/tileSize; - const int tileArrayLength = MG_GL_CANVAS_TILE_ARRAY_LENGTH; - - //TODO: ensure there's enough space in tile buffer - - //NOTE: first clear counters - glUseProgram(backend->clearCounterProgram); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, backend->tileCounterBuffer); - glDispatchCompute(tileCountX*tileCountY, 1, 1); - - //NOTE: we first distribute triangles into tiles: - - glUseProgram(backend->tileProgram); - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, backend->vertexBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->shapeBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, backend->indexBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, backend->tileCounterBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, backend->tileArrayBuffer); - - glUniform1ui(0, indexCount); - glUniform2ui(1, tileCountX, tileCountY); - glUniform1ui(2, tileSize); - glUniform1ui(3, tileArrayLength); - glUniform2f(4, contentsScaling.x, contentsScaling.y); - - u32 threadCount = indexCount/3; - glDispatchCompute((threadCount + 255)/256, 1, 1); - - //NOTE: next we sort triangles in each tile - glUseProgram(backend->sortProgram); - - glUniform1ui(0, indexCount); - glUniform2ui(1, tileCountX, tileCountY); - glUniform1ui(2, tileSize); - glUniform1ui(3, tileArrayLength); - - glDispatchCompute(tileCountX * tileCountY, 1, 1); - - //NOTE: then we fire the drawing shader that will select only triangles in its tile - glUseProgram(backend->drawProgram); - - glBindImageTexture(0, backend->outTexture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8); - - glUniform1ui(0, indexCount); - glUniform2ui(1, tileCountX, tileCountY); - glUniform1ui(2, tileSize); - glUniform1ui(3, tileArrayLength); - glUniform2f(4, contentsScaling.x, contentsScaling.y); - - if(imageInterface) - { - //TODO: make sure this image belongs to that context - mg_gl_image* image = (mg_gl_image*)imageInterface; - glActiveTexture(GL_TEXTURE1); - glBindTexture(GL_TEXTURE_2D, image->textureID); - glUniform1ui(5, 1); - } - else - { - glUniform1ui(5, 0); - } - - glDispatchCompute(tileCountX, tileCountY, 1); - - //NOTE: now blit out texture to surface - glUseProgram(backend->blitProgram); - glBindBuffer(GL_ARRAY_BUFFER, backend->dummyVertexBuffer); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, backend->outTexture); - glUniform1i(0, 0); - - glDrawArrays(GL_TRIANGLES, 0, 6); - - mg_gl_canvas_update_vertex_layout(backend); -} - -void mg_gl_canvas_destroy(mg_canvas_backend* interface) -{ - mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; - - glDeleteTextures(1, &backend->outTexture); - - glDeleteBuffers(1, &backend->dummyVertexBuffer); - glDeleteBuffers(1, &backend->vertexBuffer); - glDeleteBuffers(1, &backend->shapeBuffer); - glDeleteBuffers(1, &backend->indexBuffer); - glDeleteBuffers(1, &backend->tileCounterBuffer); - glDeleteBuffers(1, &backend->tileArrayBuffer); - - glDeleteVertexArrays(1, &backend->vao); - - free(backend->shapeMapping); - free(backend->vertexMapping); - free(backend->indexMapping); - free(backend); -} - -mg_image_data* mg_gl_canvas_image_create(mg_canvas_backend* interface, vec2 size) -{ - mg_gl_image* image = 0; - - image = malloc_type(mg_gl_image); - if(image) - { - glGenTextures(1, &image->textureID); - glBindTexture(GL_TEXTURE_2D, image->textureID); -// glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, size.x, size.y); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - image->interface.size = size; - } - return((mg_image_data*)image); -} - -void mg_gl_canvas_image_destroy(mg_canvas_backend* interface, mg_image_data* imageInterface) -{ - //TODO: check that this image belongs to this context - mg_gl_image* image = (mg_gl_image*)imageInterface; - glDeleteTextures(1, &image->textureID); - free(image); -} - -void mg_gl_canvas_image_upload_region(mg_canvas_backend* interface, - mg_image_data* imageInterface, - mp_rect region, - u8* pixels) -{ - //TODO: check that this image belongs to this context - mg_gl_image* image = (mg_gl_image*)imageInterface; - glBindTexture(GL_TEXTURE_2D, image->textureID); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, region.w, region.h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); -} - -static int mg_gl_compile_shader(const char* name, GLuint shader, const char* source) -{ - int res = 0; - - const char* sources[3] = {"#version 430", glsl_common, source}; - - glShaderSource(shader, 3, sources, 0); - glCompileShader(shader); - - int status = 0; - glGetShaderiv(shader, GL_COMPILE_STATUS, &status); - if(!status) - { - char buffer[256]; - int size = 0; - glGetShaderInfoLog(shader, 256, &size, buffer); - printf("Shader compile error (%s): %.*s\n", name, size, buffer); - res = -1; - } - return(res); -} - -static int mg_gl_canvas_compile_compute_program_named(const char* name, const char* source, GLuint* outProgram) -{ - int res = 0; - *outProgram = 0; - - GLuint shader = glCreateShader(GL_COMPUTE_SHADER); - GLuint program = glCreateProgram(); - - res |= mg_gl_compile_shader(name, shader, source); - - if(!res) - { - glAttachShader(program, shader); - glLinkProgram(program); - - int status = 0; - glGetProgramiv(program, GL_LINK_STATUS, &status); - if(!status) - { - char buffer[256]; - int size = 0; - glGetProgramInfoLog(program, 256, &size, buffer); - log_error("Shader link error (%s): %.*s\n", name, size, buffer); - - res = -1; - } - else - { - *outProgram = program; - } - } - return(res); -} - -int mg_gl_canvas_compile_render_program_named(const char* progName, - const char* vertexName, - const char* fragmentName, - const char* vertexSrc, - const char* fragmentSrc, - GLuint* outProgram) -{ - int res = 0; - *outProgram = 0; - - GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); - GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); - GLuint program = glCreateProgram(); - - res |= mg_gl_compile_shader(vertexName, vertexShader, vertexSrc); - res |= mg_gl_compile_shader(fragmentName, fragmentShader, fragmentSrc); - - if(!res) - { - glAttachShader(program, vertexShader); - glAttachShader(program, fragmentShader); - glLinkProgram(program); - - int status = 0; - glGetProgramiv(program, GL_LINK_STATUS, &status); - if(!status) - { - char buffer[256]; - int size = 0; - glGetProgramInfoLog(program, 256, &size, buffer); - log_error("Shader link error (%s): %.*s\n", progName, size, buffer); - res = -1; - } - else - { - *outProgram = program; - } - } - return(res); -} - -#define mg_gl_canvas_compile_compute_program(src, out) \ - mg_gl_canvas_compile_compute_program_named(#src, src, out) - -#define mg_gl_canvas_compile_render_program(progName, shaderSrc, vertexSrc, out) \ - mg_gl_canvas_compile_render_program_named(progName, #shaderSrc, #vertexSrc, shaderSrc, vertexSrc, out) - -mg_canvas_backend* mg_gl_canvas_create(mg_surface surface) -{ - mg_gl_canvas_backend* backend = 0; - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - - int err = 0; - - if(surfaceData && surfaceData->backend == MG_BACKEND_GL) - { - backend = malloc_type(mg_gl_canvas_backend); - memset(backend, 0, sizeof(mg_gl_canvas_backend)); - backend->surface = surface; - - //NOTE(martin): setup interface functions - backend->interface.destroy = mg_gl_canvas_destroy; - backend->interface.begin = mg_gl_canvas_begin; - backend->interface.end = mg_gl_canvas_end; - backend->interface.clear = mg_gl_canvas_clear; - backend->interface.drawBatch = mg_gl_canvas_draw_batch; - backend->interface.imageCreate = mg_gl_canvas_image_create; - backend->interface.imageDestroy = mg_gl_canvas_image_destroy; - backend->interface.imageUploadRegion = mg_gl_canvas_image_upload_region; - - mg_surface_prepare(surface); - - glGenVertexArrays(1, &backend->vao); - glBindVertexArray(backend->vao); - - glGenBuffers(1, &backend->dummyVertexBuffer); - glBindBuffer(GL_ARRAY_BUFFER, backend->dummyVertexBuffer); - - glGenBuffers(1, &backend->vertexBuffer); - glGenBuffers(1, &backend->shapeBuffer); - glGenBuffers(1, &backend->indexBuffer); - - glGenBuffers(1, &backend->tileCounterBuffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileCounterBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_CANVAS_TILE_COUNTER_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); - - glGenBuffers(1, &backend->tileArrayBuffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileArrayBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_CANVAS_TILE_ARRAY_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); - - mp_rect frame = mg_surface_get_frame(backend->surface); - vec2 contentsScaling = mg_surface_contents_scaling(backend->surface); - - glGenTextures(1, &backend->outTexture); - glBindTexture(GL_TEXTURE_2D, backend->outTexture); - glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, frame.w*contentsScaling.x, frame.h*contentsScaling.y); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - //NOTE: create programs - err |= mg_gl_canvas_compile_compute_program(glsl_clear_counters, &backend->clearCounterProgram); - err |= mg_gl_canvas_compile_compute_program(glsl_tile, &backend->tileProgram); - err |= mg_gl_canvas_compile_compute_program(glsl_sort, &backend->sortProgram); - err |= mg_gl_canvas_compile_compute_program(glsl_draw, &backend->drawProgram); - err |= mg_gl_canvas_compile_render_program("blit", glsl_blit_vertex, glsl_blit_fragment, &backend->blitProgram); - - if(glGetError() != GL_NO_ERROR) - { - err |= -1; - } - - backend->shapeMapping = malloc_array(char, MG_GL_CANVAS_MAX_SHAPE_BUFFER_SIZE); - backend->vertexMapping = malloc_array(char, MG_GL_CANVAS_MAX_VERTEX_BUFFER_SIZE); - backend->indexMapping = malloc_array(char, MG_GL_CANVAS_MAX_INDEX_BUFFER_SIZE); - - if( !backend->shapeMapping - || !backend->shapeMapping - || !backend->shapeMapping) - { - err |= -1; - } - - if(err) - { - mg_gl_canvas_destroy((mg_canvas_backend*)backend); - backend = 0; - } - else - { - mg_gl_canvas_update_vertex_layout(backend); - } - } - - return((mg_canvas_backend*)backend); -} +/************************************************************//** +* +* @file: gl_canvas.c +* @author: Martin Fouilleul +* @date: 29/01/2023 +* @revision: +* +*****************************************************************/ +#include"graphics_surface.h" +#include"macro_helpers.h" +#include"glsl_shaders.h" +#include"gl_api.h" + +typedef struct mg_gl_canvas_backend +{ + mg_canvas_backend interface; + mg_wgl_surface* surface; + + mp_rect clip; + mg_mat2x3 transform; + mg_image image; + mp_rect srcRegion; + mg_color clearColor; + + u32 nextShapeIndex; + u32 vertexCount; + u32 indexCount; + + vec4 shapeExtents; + vec4 shapeScreenExtents; + + mg_vertex_layout vertexLayout; + + GLuint vao; + GLuint dummyVertexBuffer; + GLuint vertexBuffer; + GLuint shapeBuffer; + GLuint indexBuffer; + GLuint tileCounterBuffer; + GLuint tileArrayBuffer; + GLuint clearCounterProgram; + GLuint tileProgram; + GLuint sortProgram; + GLuint drawProgram; + GLuint blitProgram; + + GLuint outTexture; + + char* indexMapping; + char* vertexMapping; + char* shapeMapping; + +} mg_gl_canvas_backend; + +typedef struct mg_gl_image +{ + mg_image_data interface; + + GLuint textureID; +} mg_gl_image; + +//NOTE: debugger +typedef struct debug_vertex +{ + vec4 cubic; + vec2 pos; + int shapeIndex; + u8 pad[4]; +} debug_vertex; + +typedef struct debug_shape +{ + vec4 color; + vec4 clip; + vec2 uv; + u8 pad[8]; +} debug_shape; + + +//-------------------------------------------------------------------- +// Primitives encoding +//-------------------------------------------------------------------- +void mg_reset_shape_index(mg_gl_canvas_backend* backend) +{ + backend->nextShapeIndex = 0; + backend->shapeExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; +} + +void mg_finalize_shape(mg_gl_canvas_backend* backend) +{ + if(backend->nextShapeIndex) + { + //NOTE: set shape's uv transform for the _current_ shape + vec2 texSize = mg_image_size(backend->image); + + mp_rect srcRegion = backend->srcRegion; + + mp_rect destRegion = {backend->shapeExtents.x, + backend->shapeExtents.y, + backend->shapeExtents.z - backend->shapeExtents.x, + backend->shapeExtents.w - backend->shapeExtents.y}; + + mg_mat2x3 srcRegionToImage = {1/texSize.x, 0, srcRegion.x/texSize.x, + 0, 1/texSize.y, srcRegion.y/texSize.y}; + mg_mat2x3 destRegionToSrcRegion = {srcRegion.w/destRegion.w, 0, 0, + 0, srcRegion.h/destRegion.h, 0}; + mg_mat2x3 userToDestRegion = {1, 0, -destRegion.x, + 0, 1, -destRegion.y}; + + mg_mat2x3 screenToUser = mg_mat2x3_inv(backend->transform); + + mg_mat2x3 uvTransform = srcRegionToImage; + uvTransform = mg_mat2x3_mul_m(uvTransform, destRegionToSrcRegion); + uvTransform = mg_mat2x3_mul_m(uvTransform, userToDestRegion); + uvTransform = mg_mat2x3_mul_m(uvTransform, screenToUser); + + int index = backend->nextShapeIndex-1; + mg_vertex_layout* layout = &backend->vertexLayout; + *(mg_mat2x3*)(layout->uvTransformBuffer + index*layout->uvTransformStride) = uvTransform; + + //TODO: transform extents before clipping + mp_rect clip = {maximum(backend->clip.x, backend->shapeScreenExtents.x), + maximum(backend->clip.y, backend->shapeScreenExtents.y), + minimum(backend->clip.x + backend->clip.w, backend->shapeScreenExtents.z), + minimum(backend->clip.y + backend->clip.h, backend->shapeScreenExtents.w)}; + + *(mp_rect*)(((char*)layout->clipBuffer) + index*layout->clipStride) = clip; + } +} + +u32 mg_next_shape(mg_gl_canvas_backend* backend, mg_attributes* attributes) +{ + mg_finalize_shape(backend); + + backend->clip = attributes->clip; + backend->transform = attributes->transform; + backend->srcRegion = attributes->srcRegion; + backend->shapeExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; + backend->shapeScreenExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; + + mg_vertex_layout* layout = &backend->vertexLayout; + int index = backend->nextShapeIndex; + backend->nextShapeIndex++; + + *(mg_color*)(((char*)layout->colorBuffer) + index*layout->colorStride) = attributes->color; + + return(index); +} + +//TODO(martin): rename with something more explicit +u32 mg_vertices_base_index(mg_gl_canvas_backend* backend) +{ + return(backend->vertexCount); +} + +int* mg_reserve_indices(mg_gl_canvas_backend* backend, u32 indexCount) +{ + mg_vertex_layout* layout = &backend->vertexLayout; + + //TODO: do something here... + ASSERT(backend->indexCount + indexCount < layout->maxIndexCount); + + int* base = ((int*)layout->indexBuffer) + backend->indexCount; + backend->indexCount += indexCount; + return(base); +} + +void mg_push_vertex_cubic(mg_gl_canvas_backend* backend, vec2 pos, vec4 cubic) +{ + backend->shapeExtents.x = minimum(backend->shapeExtents.x, pos.x); + backend->shapeExtents.y = minimum(backend->shapeExtents.y, pos.y); + backend->shapeExtents.z = maximum(backend->shapeExtents.z, pos.x); + backend->shapeExtents.w = maximum(backend->shapeExtents.w, pos.y); + + vec2 screenPos = mg_mat2x3_mul(backend->transform, pos); + + backend->shapeScreenExtents.x = minimum(backend->shapeScreenExtents.x, screenPos.x); + backend->shapeScreenExtents.y = minimum(backend->shapeScreenExtents.y, screenPos.y); + backend->shapeScreenExtents.z = maximum(backend->shapeScreenExtents.z, screenPos.x); + backend->shapeScreenExtents.w = maximum(backend->shapeScreenExtents.w, screenPos.y); + + mg_vertex_layout* layout = &backend->vertexLayout; + ASSERT(backend->vertexCount < layout->maxVertexCount); + ASSERT(backend->nextShapeIndex > 0); + + int shapeIndex = maximum(0, backend->nextShapeIndex-1); + u32 index = backend->vertexCount; + backend->vertexCount++; + + *(vec2*)(((char*)layout->posBuffer) + index*layout->posStride) = screenPos; + *(vec4*)(((char*)layout->cubicBuffer) + index*layout->cubicStride) = cubic; + *(u32*)(((char*)layout->shapeIndexBuffer) + index*layout->shapeIndexStride) = shapeIndex; +} + +void mg_push_vertex(mg_gl_canvas_backend* backend, vec2 pos) +{ + mg_push_vertex_cubic(backend, pos, (vec4){1, 1, 1, 1}); +} +//----------------------------------------------------------------------------------------------------------- +// Path Filling +//----------------------------------------------------------------------------------------------------------- +//NOTE(martin): forward declarations +void mg_render_fill_cubic(mg_gl_canvas_backend* backend, vec2 p[4]); + +//NOTE(martin): quadratics filling + +void mg_render_fill_quadratic(mg_gl_canvas_backend* backend, vec2 p[3]) +{ + u32 baseIndex = mg_vertices_base_index(backend); + + i32* indices = mg_reserve_indices(backend, 3); + + mg_push_vertex_cubic(backend, (vec2){p[0].x, p[0].y}, (vec4){0, 0, 0, 1}); + mg_push_vertex_cubic(backend, (vec2){p[1].x, p[1].y}, (vec4){0.5, 0, 0.5, 1}); + mg_push_vertex_cubic(backend, (vec2){p[2].x, p[2].y}, (vec4){1, 1, 1, 1}); + + indices[0] = baseIndex + 0; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; +} + +//NOTE(martin): cubic filling + +void mg_split_and_fill_cubic(mg_gl_canvas_backend* backend, vec2 p[4], f32 tSplit) +{ + int subVertexCount = 0; + int subIndexCount = 0; + + f32 OneMinusTSplit = 1-tSplit; + + vec2 q0 = {OneMinusTSplit*p[0].x + tSplit*p[1].x, + OneMinusTSplit*p[0].y + tSplit*p[1].y}; + + vec2 q1 = {OneMinusTSplit*p[1].x + tSplit*p[2].x, + OneMinusTSplit*p[1].y + tSplit*p[2].y}; + + vec2 q2 = {OneMinusTSplit*p[2].x + tSplit*p[3].x, + OneMinusTSplit*p[2].y + tSplit*p[3].y}; + + vec2 r0 = {OneMinusTSplit*q0.x + tSplit*q1.x, + OneMinusTSplit*q0.y + tSplit*q1.y}; + + vec2 r1 = {OneMinusTSplit*q1.x + tSplit*q2.x, + OneMinusTSplit*q1.y + tSplit*q2.y}; + + vec2 split = {OneMinusTSplit*r0.x + tSplit*r1.x, + OneMinusTSplit*r0.y + tSplit*r1.y};; + + vec2 subPointsLow[4] = {p[0], q0, r0, split}; + vec2 subPointsHigh[4] = {split, r1, q2, p[3]}; + + //NOTE(martin): add base triangle + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 3); + + mg_push_vertex(backend, (vec2){p[0].x, p[0].y}); + mg_push_vertex(backend, (vec2){split.x, split.y}); + mg_push_vertex(backend, (vec2){p[3].x, p[3].y}); + + indices[0] = baseIndex + 0; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + + mg_render_fill_cubic(backend, subPointsLow); + mg_render_fill_cubic(backend, subPointsHigh); +} + +int mg_cubic_outside_test(vec4 c) +{ + int res = (c.x*c.x*c.x - c.y*c.z < 0) ? -1 : 1; + return(res); +} + +void mg_render_fill_cubic(mg_gl_canvas_backend* backend, vec2 p[4]) +{ + vec4 testCoords[4]; + + /*NOTE(martin): first convert the control points to power basis, multiplying by M3 + + | 1 0 0 0| + M3 = |-3 3 0 0| + | 3 -6 3 0| + |-1 3 -3 1| + ie: + c0 = p0 + c1 = -3*p0 + 3*p1 + c2 = 3*p0 - 6*p1 + 3*p2 + c3 = -p0 + 3*p1 - 3*p2 + p3 + */ + f32 c1x = 3.0*p[1].x - 3.0*p[0].x; + f32 c1y = 3.0*p[1].y - 3.0*p[0].y; + + f32 c2x = 3.0*p[0].x + 3.0*p[2].x - 6.0*p[1].x; + f32 c2y = 3.0*p[0].y + 3.0*p[2].y - 6.0*p[1].y; + + f32 c3x = 3.0*p[1].x - 3.0*p[2].x + p[3].x - p[0].x; + f32 c3y = 3.0*p[1].y - 3.0*p[2].y + p[3].y - p[0].y; + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + //TODO(martin): we shouldn't need scaling here since now we're doing our shader math in fixed point? + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + c1x /= 10; + c1y /= 10; + c2x /= 10; + c2y /= 10; + c3x /= 10; + c3y /= 10; + + /*NOTE(martin): + now, compute determinants d0, d1, d2, d3, which gives the coefficients of the + inflection points polynomial: + + I(t, s) = d0*t^3 - 3*d1*t^2*s + 3*d2*t*s^2 - d3*s^3 + + The roots of this polynomial are the inflection points of the parametric curve, in homogeneous + coordinates (ie we can have an inflection point at inifinity with s=0). + + |x3 y3 w3| |x3 y3 w3| |x3 y3 w3| |x2 y2 w2| + d0 = det |x2 y2 w2| d1 = -det |x2 y2 w2| d2 = det |x1 y1 w1| d3 = -det |x1 y1 w1| + |x1 y1 w1| |x0 y0 w0| |x0 y0 w0| |x0 y0 w0| + + In our case, the pi.w equal 1 (no point at infinity), so _in_the_power_basis_, w1 = w2 = w3 = 0 and w0 = 1 + (which also means d0 = 0) + */ + + f32 d1 = c3y*c2x - c3x*c2y; + f32 d2 = c3x*c1y - c3y*c1x; + f32 d3 = c2y*c1x - c2x*c1y; + + //NOTE(martin): compute the second factor of the discriminant discr(I) = d1^2*(3*d2^2 - 4*d3*d1) + f32 discrFactor2 = 3.0*Square(d2) - 4.0*d3*d1; + + //NOTE(martin): each following case gives the number of roots, hence the category of the parametric curve + if(fabs(d1) < 0.1 && fabs(d2) < 0.1 && d3 != 0) + { + //NOTE(martin): quadratic degenerate case + //NOTE(martin): compute quadratic curve control point, which is at p0 + 1.5*(p1-p0) = 1.5*p1 - 0.5*p0 + vec2 quadControlPoints[3] = { p[0], + {1.5*p[1].x - 0.5*p[0].x, 1.5*p[1].y - 0.5*p[0].y}, + p[3]}; + + mg_render_fill_quadratic(backend, quadControlPoints); + return; + } + else if( (discrFactor2 > 0 && d1 != 0) + ||(discrFactor2 == 0 && d1 != 0)) + { + //NOTE(martin): serpentine curve or cusp with inflection at infinity + // (these two cases are handled the same way). + //NOTE(martin): compute the solutions (tl, sl), (tm, sm), and (tn, sn) of the inflection point equation + f32 tl = d2 + sqrt(discrFactor2/3); + f32 sl = 2*d1; + f32 tm = d2 - sqrt(discrFactor2/3); + f32 sm = sl; + + /*NOTE(martin): + the power basis coefficients of points k,l,m,n are collected into the rows of the 4x4 matrix F: + + | tl*tm tl^3 tm^3 1 | + | -sm*tl - sl*tm -3sl*tl^2 -3*sm*tm^2 0 | + | sl*sm 3*sl^2*tl 3*sm^2*tm 0 | + | 0 -sl^3 -sm^3 0 | + + This matrix is then multiplied by M3^(-1) on the left which yelds the bezier coefficients of k, l, m, n + which are assigned as a 4D image coordinates to control points. + + + | 1 0 0 0 | + M3^(-1) = | 1 1/3 0 0 | + | 1 2/3 1/3 0 | + | 1 1 1 1 | + */ + testCoords[0].x = tl*tm; + testCoords[0].y = Cube(tl); + testCoords[0].z = Cube(tm); + + testCoords[1].x = tl*tm - (sm*tl + sl*tm)/3; + testCoords[1].y = Cube(tl) - sl*Square(tl); + testCoords[1].z = Cube(tm) - sm*Square(tm); + + testCoords[2].x = tl*tm - (sm*tl + sl*tm)*2/3 + sl*sm/3; + testCoords[2].y = Cube(tl) - 2*sl*Square(tl) + Square(sl)*tl; + testCoords[2].z = Cube(tm) - 2*sm*Square(tm) + Square(sm)*tm; + + testCoords[3].x = tl*tm - (sm*tl + sl*tm) + sl*sm; + testCoords[3].y = Cube(tl) - 3*sl*Square(tl) + 3*Square(sl)*tl - Cube(sl); + testCoords[3].z = Cube(tm) - 3*sm*Square(tm) + 3*Square(sm)*tm - Cube(sm); + } + else if(discrFactor2 < 0 && d1 != 0) + { + //NOTE(martin): loop curve + f32 td = d2 + sqrt(-discrFactor2); + f32 sd = 2*d1; + f32 te = d2 - sqrt(-discrFactor2); + f32 se = sd; + + //NOTE(martin): if one of the parameters (td/sd) or (te/se) is in the interval [0,1], the double point + // is inside the control points convex hull and would cause a shading anomaly. If this is + // the case, subdivide the curve at that point + + //TODO: study edge case where td/sd ~ 1 or 0 (which causes an infinite recursion in split and fill). + // quick fix for now is adding a little slop in the check... + + if(sd != 0 && td/sd < 0.99 && td/sd > 0.01) + { + mg_split_and_fill_cubic(backend, p, td/sd); + return; + } + if(se != 0 && te/se < 0.99 && te/se > 0.01) + { + mg_split_and_fill_cubic(backend, p, te/se); + return; + } + + /*NOTE(martin): + the power basis coefficients of points k,l,m,n are collected into the rows of the 4x4 matrix F: + + | td*te td^2*te td*te^2 1 | + | -se*td - sd*te -se*td^2 - 2sd*te*td -sd*te^2 - 2*se*td*te 0 | + | sd*se te*sd^2 + 2*se*td*sd td*se^2 + 2*sd*te*se 0 | + | 0 -sd^2*se -sd*se^2 0 | + + This matrix is then multiplied by M3^(-1) on the left which yelds the bezier coefficients of k, l, m, n + which are assigned as a 4D image coordinates to control points. + + + | 1 0 0 0 | + M3^(-1) = | 1 1/3 0 0 | + | 1 2/3 1/3 0 | + | 1 1 1 1 | + */ + testCoords[0].x = td*te; + testCoords[0].y = Square(td)*te; + testCoords[0].z = td*Square(te); + + testCoords[1].x = td*te - (se*td + sd*te)/3.0; + testCoords[1].y = Square(td)*te - (se*Square(td) + 2.*sd*te*td)/3.0; + testCoords[1].z = td*Square(te) - (sd*Square(te) + 2*se*td*te)/3.0; + + testCoords[2].x = td*te - 2.0*(se*td + sd*te)/3.0 + sd*se/3.0; + testCoords[2].y = Square(td)*te - 2.0*(se*Square(td) + 2.0*sd*te*td)/3.0 + (te*Square(sd) + 2.0*se*td*sd)/3.0; + testCoords[2].z = td*Square(te) - 2.0*(sd*Square(te) + 2.0*se*td*te)/3.0 + (td*Square(se) + 2.0*sd*te*se)/3.0; + + testCoords[3].x = td*te - (se*td + sd*te) + sd*se; + testCoords[3].y = Square(td)*te - (se*Square(td) + 2.0*sd*te*td) + (te*Square(sd) + 2.0*se*td*sd) - Square(sd)*se; + testCoords[3].z = td*Square(te) - (sd*Square(te) + 2.0*se*td*te) + (td*Square(se) + 2.0*sd*te*se) - sd*Square(se); + } + else if(d1 == 0 && d2 != 0) + { + //NOTE(martin): cusp with cusp at infinity + + f32 tl = d3; + f32 sl = 3*d2; + + /*NOTE(martin): + the power basis coefficients of points k,l,m,n are collected into the rows of the 4x4 matrix F: + + | tl tl^3 1 1 | + | -sl -3sl*tl^2 0 0 | + | 0 3*sl^2*tl 0 0 | + | 0 -sl^3 0 0 | + + This matrix is then multiplied by M3^(-1) on the left which yelds the bezier coefficients of k, l, m, n + which are assigned as a 4D image coordinates to control points. + + + | 1 0 0 0 | + M3^(-1) = | 1 1/3 0 0 | + | 1 2/3 1/3 0 | + | 1 1 1 1 | + */ + + testCoords[0].x = tl; + testCoords[0].y = Cube(tl); + testCoords[0].z = 1; + + testCoords[1].x = tl - sl/3; + testCoords[1].y = Cube(tl) - sl*Square(tl); + testCoords[1].z = 1; + + testCoords[2].x = tl - sl*2/3; + testCoords[2].y = Cube(tl) - 2*sl*Square(tl) + Square(sl)*tl; + testCoords[2].z = 1; + + testCoords[3].x = tl - sl; + testCoords[3].y = Cube(tl) - 3*sl*Square(tl) + 3*Square(sl)*tl - Cube(sl); + testCoords[3].z = 1; + } + else if(d1 == 0 && d2 == 0 && d3 == 0) + { + //NOTE(martin): line or point degenerate case, ignored + return; + } + else + { + //TODO(martin): handle error ? put some epsilon slack on the conditions ? + ASSERT(0, "not implemented yet !"); + return; + } + + //NOTE(martin): compute convex hull indices using Gift wrapping / Jarvis' march algorithm + int convexHullIndices[4]; + int leftMostPointIndex = 0; + + for(int i=0; i<4; i++) + { + if(p[i].x < p[leftMostPointIndex].x) + { + leftMostPointIndex = i; + } + } + int currentPointIndex = leftMostPointIndex; + int i=0; + int convexHullCount = 0; + + do + { + convexHullIndices[i] = currentPointIndex; + convexHullCount++; + int bestGuessIndex = 0; + + for(int j=0; j<4; j++) + { + vec2 bestGuessEdge = {.x = p[bestGuessIndex].x - p[currentPointIndex].x, + .y = p[bestGuessIndex].y - p[currentPointIndex].y}; + + vec2 nextGuessEdge = {.x = p[j].x - p[currentPointIndex].x, + .y = p[j].y - p[currentPointIndex].y}; + + //NOTE(martin): if control point j is on the right of current edge, it is a best guess + // (in case of colinearity we choose the point which is farthest from the current point) + + f32 crossProduct = bestGuessEdge.x*nextGuessEdge.y - bestGuessEdge.y*nextGuessEdge.x; + + if( bestGuessIndex == currentPointIndex + || crossProduct < 0) + { + bestGuessIndex = j; + } + else if(crossProduct == 0) + { + + //NOTE(martin): if vectors v1, v2 are colinear and distinct, and ||v1|| > ||v2||, + // either abs(v1.x) > abs(v2.x) or abs(v1.y) > abs(v2.y) + // so we don't actually need to compute their norm to select the greatest + // (and if v1 and v2 are equal we don't have to update our best guess.) + + //TODO(martin): in case of colinearity we should rather select the edge that has the greatest dot product with last edge ?? + + if(fabs(nextGuessEdge.x) > fabs(bestGuessEdge.x) + || fabs(nextGuessEdge.y) > fabs(bestGuessEdge.y)) + { + bestGuessIndex = j; + } + } + } + i++; + currentPointIndex = bestGuessIndex; + + } while(currentPointIndex != leftMostPointIndex && i<4); + + //NOTE(martin): triangulation and inside/outside tests. In the shader, the outside is defined by s*(k^3 - lm) > 0 + // ie the 4th coordinate s flips the inside/outside test. + // We affect s such that the covered are is between the curve and the line joining p0 and p3. + + //TODO: quick fix, maybe later cull degenerate hulls beforehand + if(convexHullCount <= 2) + { + //NOTE(martin): if convex hull has only two point, we have a degenerate cubic that displays nothing. + return; + } + else if(convexHullCount == 3) + { + /*NOTE(martin): + We have 3 case here: + 1) Endpoints are coincidents. We push on triangle, and test an intermediate point for orientation. + 2) The point not on the hull is an endpoint. We push two triangle (p0, p3, p1) and (p0, p3, p2). We test the intermediate + points to know if we must flip the orientation of the curve. + 3) The point not on the hull is an intermediate point: we emit one triangle. We test the intermediate point on the hull + to know if we must flip the orientation of the curve. + */ + if( p[0].x == p[3].x + && p[0].y == p[3].y) + { + //NOTE: case 1: endpoints are coincidents + int outsideTest = mg_cubic_outside_test(testCoords[1]); + + //NOTE: push triangle + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 3); + + mg_push_vertex_cubic(backend, p[0], (vec4){vec4_expand_xyz(testCoords[0]), outsideTest}); + mg_push_vertex_cubic(backend, p[1], (vec4){vec4_expand_xyz(testCoords[1]), outsideTest}); + mg_push_vertex_cubic(backend, p[2], (vec4){vec4_expand_xyz(testCoords[2]), outsideTest}); + + for(int i=0; i<3; i++) + { + indices[i] = baseIndex + i; + } + } + else + { + //NOTE: find point not on the hull + int insidePointIndex = -1; + { + bool present[4] = {0}; + for(int i=0; i<3; i++) + { + present[convexHullIndices[i]] = true; + } + for(int i=0; i<4; i++) + { + if(!present[i]) + { + insidePointIndex = i; + break; + } + } + } + DEBUG_ASSERT(insidePointIndex >= 0 && insidePointIndex < 4); + + if(insidePointIndex == 0 || insidePointIndex == 3) + { + //NOTE: case 2: the point inside the hull is an endpoint + + int outsideTest0 = mg_cubic_outside_test(testCoords[1]); + int outsideTest1 = mg_cubic_outside_test(testCoords[2]); + + //NOTE: push triangles + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + mg_push_vertex_cubic(backend, p[0], (vec4){vec4_expand_xyz(testCoords[0]), outsideTest0}); + mg_push_vertex_cubic(backend, p[3], (vec4){vec4_expand_xyz(testCoords[3]), outsideTest0}); + mg_push_vertex_cubic(backend, p[1], (vec4){vec4_expand_xyz(testCoords[1]), outsideTest0}); + mg_push_vertex_cubic(backend, p[0], (vec4){vec4_expand_xyz(testCoords[0]), outsideTest1}); + mg_push_vertex_cubic(backend, p[3], (vec4){vec4_expand_xyz(testCoords[3]), outsideTest1}); + mg_push_vertex_cubic(backend, p[2], (vec4){vec4_expand_xyz(testCoords[2]), outsideTest1}); + + for(int i=0; i<6; i++) + { + indices[i] = baseIndex + i; + } + } + else + { + int testIndex = (insidePointIndex == 1) ? 2 : 1; + int outsideTest = mg_cubic_outside_test(testCoords[testIndex]); + + //NOTE: push triangle + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 3); + + for(int i=0; i<3; i++) + { + mg_push_vertex_cubic(backend, + p[convexHullIndices[i]], + (vec4){vec4_expand_xyz(testCoords[convexHullIndices[i]]), + outsideTest}); + } + + for(int i=0; i<3; i++) + { + indices[i] = baseIndex + i; + } + } + } + } + else + { + DEBUG_ASSERT(convexHullCount == 4); + /*NOTE(martin): + We build a fan from the hull, starting from an endpoint. For each triangle, we test the vertex that is an intermediate + control point for orientation + */ + int endPointIndex = -1; + for(int i=0; i<4; i++) + { + if(convexHullIndices[i] == 0 || convexHullIndices[i] == 3) + { + endPointIndex = i; + break; + } + } + ASSERT(endPointIndex >= 0); + + int fanIndices[6] = {convexHullIndices[endPointIndex], + convexHullIndices[(endPointIndex + 1)%4], + convexHullIndices[(endPointIndex + 2)%4], + convexHullIndices[endPointIndex], + convexHullIndices[(endPointIndex + 2)%4], + convexHullIndices[(endPointIndex + 3)%4]}; + + //NOTE: fan indices on the hull are (0,1,2)(0,2,3). So if the 3rd vertex of the hull is an intermediate point it works + // as a test vertex for both triangles. Otherwise, the test vertices on the fan are 1 and 5. + int outsideTest0 = 1; + int outsideTest1 = 1; + + if( fanIndices[2] == 1 + ||fanIndices[2] == 2) + { + outsideTest0 = outsideTest1 = mg_cubic_outside_test(testCoords[fanIndices[2]]); + } + else + { + DEBUG_ASSERT(fanIndices[1] == 1 || fanIndices[1] == 2); + DEBUG_ASSERT(fanIndices[5] == 1 || fanIndices[5] == 2); + + outsideTest0 = mg_cubic_outside_test(testCoords[fanIndices[1]]); + outsideTest1 = mg_cubic_outside_test(testCoords[fanIndices[5]]); + } + + //NOTE: push triangles + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + for(int i=0; i<3; i++) + { + mg_push_vertex_cubic(backend, p[fanIndices[i]], (vec4){vec4_expand_xyz(testCoords[fanIndices[i]]), outsideTest0}); + } + for(int i=0; i<3; i++) + { + mg_push_vertex_cubic(backend, p[fanIndices[i+3]], (vec4){vec4_expand_xyz(testCoords[fanIndices[i+3]]), outsideTest1}); + } + + for(int i=0; i<6; i++) + { + indices[i] = baseIndex + i; + } + } +} + +//NOTE(martin): global path fill + +void mg_render_fill(mg_gl_canvas_backend* backend, mg_path_elt* elements, mg_path_descriptor* path) +{ + u32 eltCount = path->count; + vec2 startPoint = path->startPoint; + vec2 endPoint = path->startPoint; + vec2 currentPoint = path->startPoint; + + for(int eltIndex=0; eltIndexp[0], elt->p[1], elt->p[2]}; + + switch(elt->type) + { + case MG_PATH_MOVE: + { + startPoint = elt->p[0]; + endPoint = elt->p[0]; + currentPoint = endPoint; + continue; + } break; + + case MG_PATH_LINE: + { + endPoint = controlPoints[1]; + } break; + + case MG_PATH_QUADRATIC: + { + mg_render_fill_quadratic(backend, controlPoints); + endPoint = controlPoints[2]; + + } break; + + case MG_PATH_CUBIC: + { + mg_render_fill_cubic(backend, controlPoints); + endPoint = controlPoints[3]; + } break; + } + + //NOTE(martin): now fill interior triangle + u32 baseIndex = mg_vertices_base_index(backend); + int* indices = mg_reserve_indices(backend, 3); + + mg_push_vertex(backend, startPoint); + mg_push_vertex(backend, currentPoint); + mg_push_vertex(backend, endPoint); + + indices[0] = baseIndex; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + + currentPoint = endPoint; + } +} + +//----------------------------------------------------------------------------------------------------------- +// Path Stroking +//----------------------------------------------------------------------------------------------------------- + +void mg_render_stroke_line(mg_gl_canvas_backend* backend, vec2 p[2], mg_attributes* attributes) +{ + //NOTE(martin): get normals multiplied by halfWidth + f32 halfW = attributes->width/2; + + vec2 n0 = {p[0].y - p[1].y, + p[1].x - p[0].x}; + f32 norm0 = sqrt(n0.x*n0.x + n0.y*n0.y); + n0.x *= halfW/norm0; + n0.y *= halfW/norm0; + + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + mg_push_vertex(backend, (vec2){p[0].x + n0.x, p[0].y + n0.y}); + mg_push_vertex(backend, (vec2){p[1].x + n0.x, p[1].y + n0.y}); + mg_push_vertex(backend, (vec2){p[1].x - n0.x, p[1].y - n0.y}); + mg_push_vertex(backend, (vec2){p[0].x - n0.x, p[0].y - n0.y}); + + indices[0] = baseIndex; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + indices[3] = baseIndex; + indices[4] = baseIndex + 2; + indices[5] = baseIndex + 3; +} + +bool mg_intersect_hull_legs(vec2 p0, vec2 p1, vec2 p2, vec2 p3, vec2* intersection) +{ + /*NOTE: check intersection of lines (p0-p1) and (p2-p3) + + P = p0 + u(p1-p0) + P = p2 + w(p3-p2) + */ + bool found = false; + + f32 den = (p0.x - p1.x)*(p2.y - p3.y) - (p0.y - p1.y)*(p2.x - p3.x); + if(fabs(den) > 0.0001) + { + f32 u = ((p0.x - p2.x)*(p2.y - p3.y) - (p0.y - p2.y)*(p2.x - p3.x))/den; + f32 w = ((p0.x - p2.x)*(p0.y - p1.y) - (p0.y - p2.y)*(p0.x - p1.x))/den; + + intersection->x = p0.x + u*(p1.x - p0.x); + intersection->y = p0.y + u*(p1.y - p0.y); + found = true; + } + return(found); +} + +bool mg_offset_hull(int count, vec2* p, vec2* result, f32 offset) +{ + //NOTE: we should have no more than two coincident points here. This means the leg between + // those two points can't be offset, but we can set a double point at the start of first leg, + // end of first leg, or we can join the first and last leg to create a missing middle one + + vec2 legs[3][2] = {0}; + bool valid[3] = {0}; + + for(int i=0; i= 1e-6) + { + n = vec2_mul(offset/norm, n); + legs[i][0] = vec2_add(p[i], n); + legs[i][1] = vec2_add(p[i+1], n); + valid[i] = true; + } + } + + //NOTE: now we find intersections + + // first point is either the start of the first or second leg + if(valid[0]) + { + result[0] = legs[0][0]; + } + else + { + ASSERT(valid[1]); + result[0] = legs[1][0]; + } + + for(int i=1; iwidth) + ||!mg_offset_hull(3, p, negativeOffsetHull, -0.5 * attributes->width)) + { + //NOTE: offsetting the hull failed, split the curve + vec2 splitLeft[3]; + vec2 splitRight[3]; + mg_quadratic_split(p, 0.5, splitLeft, splitRight); + mg_render_stroke_quadratic(backend, splitLeft, attributes); + mg_render_stroke_quadratic(backend, splitRight, attributes); + } + else + { + //NOTE(martin): the distance d between the offset curve and the path must be between w/2-tolerance and w/2+tolerance + // thus, by constraining tolerance to be at most, 0.5*width, we can rewrite this condition like this: + // + // (w/2-tolerance)^2 < d^2 < (w/2+tolerance)^2 + // + // we compute the maximum overshoot outside these bounds and split the curve at the corresponding parameter + + //TODO: maybe refactor by using tolerance in the _check_, not in the computation of the overshoot + f32 tolerance = minimum(attributes->tolerance, 0.5 * attributes->width); + f32 d2LowBound = Square(0.5 * attributes->width - attributes->tolerance); + f32 d2HighBound = Square(0.5 * attributes->width + attributes->tolerance); + + f32 maxOvershoot = 0; + f32 maxOvershootParameter = 0; + + for(int i=0; i maxOvershoot) + { + maxOvershoot = overshoot; + maxOvershootParameter = t; + } + } + + if(maxOvershoot > 0) + { + vec2 splitLeft[3]; + vec2 splitRight[3]; + mg_quadratic_split(p, maxOvershootParameter, splitLeft, splitRight); + mg_render_stroke_quadratic(backend, splitLeft, attributes); + mg_render_stroke_quadratic(backend, splitRight, attributes); + } + else + { + //NOTE(martin): push the actual fill commands for the offset contour + + mg_next_shape(backend, attributes); + + mg_render_fill_quadratic(backend, positiveOffsetHull); + mg_render_fill_quadratic(backend, negativeOffsetHull); + + //NOTE(martin): add base triangles + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + mg_push_vertex(backend, positiveOffsetHull[0]); + mg_push_vertex(backend, positiveOffsetHull[2]); + mg_push_vertex(backend, negativeOffsetHull[2]); + mg_push_vertex(backend, negativeOffsetHull[0]); + + indices[0] = baseIndex + 0; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + indices[3] = baseIndex + 0; + indices[4] = baseIndex + 2; + indices[5] = baseIndex + 3; + } + } + #undef CHECK_SAMPLE_COUNT +} + +vec2 mg_cubic_get_point(vec2 p[4], f32 t) +{ + vec2 r; + + f32 oneMt = 1-t; + f32 oneMt2 = Square(oneMt); + f32 oneMt3 = oneMt2*oneMt; + f32 t2 = Square(t); + f32 t3 = t2*t; + + r.x = oneMt3*p[0].x + 3*oneMt2*t*p[1].x + 3*oneMt*t2*p[2].x + t3*p[3].x; + r.y = oneMt3*p[0].y + 3*oneMt2*t*p[1].y + 3*oneMt*t2*p[2].y + t3*p[3].y; + + return(r); +} + +void mg_cubic_split(vec2 p[4], f32 t, vec2 outLeft[4], vec2 outRight[4]) +{ + //NOTE(martin): split bezier curve p at parameter t, using De Casteljau's algorithm + // the q_n are the points along the hull's segments at parameter t + // the r_n are the points along the (q_n, q_n+1) segments at parameter t + // s is the split point. + + vec2 q0 = {(1-t)*p[0].x + t*p[1].x, + (1-t)*p[0].y + t*p[1].y}; + + vec2 q1 = {(1-t)*p[1].x + t*p[2].x, + (1-t)*p[1].y + t*p[2].y}; + + vec2 q2 = {(1-t)*p[2].x + t*p[3].x, + (1-t)*p[2].y + t*p[3].y}; + + vec2 r0 = {(1-t)*q0.x + t*q1.x, + (1-t)*q0.y + t*q1.y}; + + vec2 r1 = {(1-t)*q1.x + t*q2.x, + (1-t)*q1.y + t*q2.y}; + + vec2 s = {(1-t)*r0.x + t*r1.x, + (1-t)*r0.y + t*r1.y};; + + outLeft[0] = p[0]; + outLeft[1] = q0; + outLeft[2] = r0; + outLeft[3] = s; + + outRight[0] = s; + outRight[1] = r1; + outRight[2] = q2; + outRight[3] = p[3]; +} + +void mg_render_stroke_cubic(mg_gl_canvas_backend* backend, vec2 p[4], mg_attributes* attributes) +{ + //NOTE: check degenerate line cases + f32 equalEps = 1e-3; + + if( (vec2_close(p[0], p[1], equalEps) && vec2_close(p[2], p[3], equalEps)) + ||(vec2_close(p[0], p[1], equalEps) && vec2_close(p[1], p[2], equalEps)) + ||(vec2_close(p[1], p[2], equalEps) && vec2_close(p[2], p[3], equalEps))) + { + vec2 line[2] = {p[0], p[3]}; + mg_render_stroke_line(backend, line, attributes); + return; + } + else if(vec2_close(p[0], p[1], equalEps) && vec2_close(p[1], p[3], equalEps)) + { + vec2 line[2] = {p[0], vec2_add(vec2_mul(5./9, p[0]), vec2_mul(4./9, p[2]))}; + mg_render_stroke_line(backend, line, attributes); + return; + } + else if(vec2_close(p[0], p[2], equalEps) && vec2_close(p[2], p[3], equalEps)) + { + vec2 line[2] = {p[0], vec2_add(vec2_mul(5./9, p[0]), vec2_mul(4./9, p[1]))}; + mg_render_stroke_line(backend, line, attributes); + return; + } + + #define CHECK_SAMPLE_COUNT 5 + f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; + + vec2 positiveOffsetHull[4]; + vec2 negativeOffsetHull[4]; + + if( !mg_offset_hull(4, p, positiveOffsetHull, 0.5 * attributes->width) + || !mg_offset_hull(4, p, negativeOffsetHull, -0.5 * attributes->width)) + { + vec2 splitLeft[4]; + vec2 splitRight[4]; + mg_cubic_split(p, 0.5, splitLeft, splitRight); + mg_render_stroke_cubic(backend, splitLeft, attributes); + mg_render_stroke_cubic(backend, splitRight, attributes); + return; + } + + //NOTE(martin): the distance d between the offset curve and the path must be between w/2-tolerance and w/2+tolerance + // thus, by constraining tolerance to be at most, 0.5*width, we can rewrite this condition like this: + // + // (w/2-tolerance)^2 < d^2 < (w/2+tolerance)^2 + // + // we compute the maximum overshoot outside these bounds and split the curve at the corresponding parameter + + //TODO: maybe refactor by using tolerance in the _check_, not in the computation of the overshoot + f32 tolerance = minimum(attributes->tolerance, 0.5 * attributes->width); + f32 d2LowBound = Square(0.5 * attributes->width - attributes->tolerance); + f32 d2HighBound = Square(0.5 * attributes->width + attributes->tolerance); + + f32 maxOvershoot = 0; + f32 maxOvershootParameter = 0; + + for(int i=0; i maxOvershoot) + { + maxOvershoot = overshoot; + maxOvershootParameter = t; + } + } + + if(maxOvershoot > 0) + { + vec2 splitLeft[4]; + vec2 splitRight[4]; + mg_cubic_split(p, maxOvershootParameter, splitLeft, splitRight); + mg_render_stroke_cubic(backend, splitLeft, attributes); + mg_render_stroke_cubic(backend, splitRight, attributes); + + //TODO: render joint between the split curves + } + else + { + //NOTE(martin): push the actual fill commands for the offset contour + mg_next_shape(backend, attributes); + + mg_render_fill_cubic(backend, positiveOffsetHull); + mg_render_fill_cubic(backend, negativeOffsetHull); + + //NOTE(martin): add base triangles + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + mg_push_vertex(backend, positiveOffsetHull[0]); + mg_push_vertex(backend, positiveOffsetHull[3]); + mg_push_vertex(backend, negativeOffsetHull[3]); + mg_push_vertex(backend, negativeOffsetHull[0]); + + indices[0] = baseIndex + 0; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + indices[3] = baseIndex + 0; + indices[4] = baseIndex + 2; + indices[5] = baseIndex + 3; + } + #undef CHECK_SAMPLE_COUNT +} + +void mg_stroke_cap(mg_gl_canvas_backend* backend, vec2 p0, vec2 direction, mg_attributes* attributes) +{ + //NOTE(martin): compute the tangent and normal vectors (multiplied by half width) at the cap point + + f32 dn = sqrt(Square(direction.x) + Square(direction.y)); + f32 alpha = 0.5 * attributes->width/dn; + + vec2 n0 = {-alpha*direction.y, + alpha*direction.x}; + + vec2 m0 = {alpha*direction.x, + alpha*direction.y}; + + mg_next_shape(backend, attributes); + + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + mg_push_vertex(backend, (vec2){p0.x + n0.x, p0.y + n0.y}); + mg_push_vertex(backend, (vec2){p0.x + n0.x + m0.x, p0.y + n0.y + m0.y}); + mg_push_vertex(backend, (vec2){p0.x - n0.x + m0.x, p0.y - n0.y + m0.y}); + mg_push_vertex(backend, (vec2){p0.x - n0.x, p0.y - n0.y}); + + indices[0] = baseIndex; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + indices[3] = baseIndex; + indices[4] = baseIndex + 2; + indices[5] = baseIndex + 3; +} + +void mg_stroke_joint(mg_gl_canvas_backend* backend, + vec2 p0, + vec2 t0, + vec2 t1, + mg_attributes* attributes) +{ + //NOTE(martin): compute the normals at the joint point + f32 norm_t0 = sqrt(Square(t0.x) + Square(t0.y)); + f32 norm_t1 = sqrt(Square(t1.x) + Square(t1.y)); + + vec2 n0 = {-t0.y, t0.x}; + n0.x /= norm_t0; + n0.y /= norm_t0; + + vec2 n1 = {-t1.y, t1.x}; + n1.x /= norm_t1; + n1.y /= norm_t1; + + //NOTE(martin): the sign of the cross product determines if the normals are facing outwards or inwards the angle. + // we flip them to face outwards if needed + f32 crossZ = n0.x*n1.y - n0.y*n1.x; + if(crossZ > 0) + { + n0.x *= -1; + n0.y *= -1; + n1.x *= -1; + n1.y *= -1; + } + + mg_next_shape(backend, attributes); + + //NOTE(martin): use the same code as hull offset to find mitter point... + /*NOTE(martin): let vector u = (n0+n1) and vector v = pIntersect - p1 + then v = u * (2*offset / norm(u)^2) + (this can be derived from writing the pythagoras theorems in the triangles of the joint) + */ + f32 halfW = 0.5 * attributes->width; + vec2 u = {n0.x + n1.x, n0.y + n1.y}; + f32 uNormSquare = u.x*u.x + u.y*u.y; + f32 alpha = attributes->width / uNormSquare; + vec2 v = {u.x * alpha, u.y * alpha}; + + f32 excursionSquare = uNormSquare * Square(alpha - attributes->width/4); + + if( attributes->joint == MG_JOINT_MITER + && excursionSquare <= Square(attributes->maxJointExcursion)) + { + vec2 mitterPoint = {p0.x + v.x, p0.y + v.y}; + + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 6); + + mg_push_vertex(backend, p0); + mg_push_vertex(backend, (vec2){p0.x + n0.x*halfW, p0.y + n0.y*halfW}); + mg_push_vertex(backend, mitterPoint); + mg_push_vertex(backend, (vec2){p0.x + n1.x*halfW, p0.y + n1.y*halfW}); + + indices[0] = baseIndex; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + indices[3] = baseIndex; + indices[4] = baseIndex + 2; + indices[5] = baseIndex + 3; + } + else + { + //NOTE(martin): add a bevel joint + u32 baseIndex = mg_vertices_base_index(backend); + i32* indices = mg_reserve_indices(backend, 3); + + mg_push_vertex(backend, p0); + mg_push_vertex(backend, (vec2){p0.x + n0.x*halfW, p0.y + n0.y*halfW}); + mg_push_vertex(backend, (vec2){p0.x + n1.x*halfW, p0.y + n1.y*halfW}); + + DEBUG_ASSERT(!isnan(n0.x) && !isnan(n0.y) && !isnan(n1.x) && !isnan(n1.y)); + + indices[0] = baseIndex; + indices[1] = baseIndex + 1; + indices[2] = baseIndex + 2; + } +} + +void mg_render_stroke_element(mg_gl_canvas_backend* backend, + mg_path_elt* element, + mg_attributes* attributes, + vec2 currentPoint, + vec2* startTangent, + vec2* endTangent, + vec2* endPoint) +{ + vec2 controlPoints[4] = {currentPoint, element->p[0], element->p[1], element->p[2]}; + int endPointIndex = 0; + mg_next_shape(backend, attributes); + + switch(element->type) + { + case MG_PATH_LINE: + mg_render_stroke_line(backend, controlPoints, attributes); + endPointIndex = 1; + break; + + case MG_PATH_QUADRATIC: + mg_render_stroke_quadratic(backend, controlPoints, attributes); + endPointIndex = 2; + break; + + case MG_PATH_CUBIC: + mg_render_stroke_cubic(backend, controlPoints, attributes); + endPointIndex = 3; + break; + + case MG_PATH_MOVE: + ASSERT(0, "should be unreachable"); + break; + } + + //NOTE: ensure tangents are properly computed even in presence of coincident points + //TODO: see if we can do this in a less hacky way + + for(int i=1; i<4; i++) + { + if( controlPoints[i].x != controlPoints[0].x + || controlPoints[i].y != controlPoints[0].y) + { + *startTangent = (vec2){.x = controlPoints[i].x - controlPoints[0].x, + .y = controlPoints[i].y - controlPoints[0].y}; + break; + } + } + *endPoint = controlPoints[endPointIndex]; + + for(int i=endPointIndex-1; i>=0; i++) + { + if( controlPoints[i].x != endPoint->x + || controlPoints[i].y != endPoint->y) + { + *endTangent = (vec2){.x = endPoint->x - controlPoints[i].x, + .y = endPoint->y - controlPoints[i].y}; + break; + } + } + + DEBUG_ASSERT(startTangent->x != 0 || startTangent->y != 0); +} + +u32 mg_render_stroke_subpath(mg_gl_canvas_backend* backend, + mg_path_elt* elements, + mg_path_descriptor* path, + mg_attributes* attributes, + u32 startIndex, + vec2 startPoint) +{ + u32 eltCount = path->count; + DEBUG_ASSERT(startIndex < eltCount); + + vec2 currentPoint = startPoint; + vec2 endPoint = {0, 0}; + vec2 previousEndTangent = {0, 0}; + vec2 firstTangent = {0, 0}; + vec2 startTangent = {0, 0}; + vec2 endTangent = {0, 0}; + + //NOTE(martin): render first element and compute first tangent + mg_render_stroke_element(backend, elements + startIndex, attributes, currentPoint, &startTangent, &endTangent, &endPoint); + + firstTangent = startTangent; + previousEndTangent = endTangent; + currentPoint = endPoint; + + //NOTE(martin): render subsequent elements along with their joints + u32 eltIndex = startIndex + 1; + for(; + eltIndexjoint != MG_JOINT_NONE) + { + mg_stroke_joint(backend, currentPoint, previousEndTangent, startTangent, attributes); + } + previousEndTangent = endTangent; + currentPoint = endPoint; + } + u32 subPathEltCount = eltIndex - (startIndex+1); + + //NOTE(martin): draw end cap / joint. We ensure there's at least two segments to draw a closing joint + if( subPathEltCount > 1 + && startPoint.x == endPoint.x + && startPoint.y == endPoint.y) + { + if(attributes->joint != MG_JOINT_NONE) + { + //NOTE(martin): add a closing joint if the path is closed + mg_stroke_joint(backend, endPoint, endTangent, firstTangent, attributes); + } + } + else if(attributes->cap == MG_CAP_SQUARE) + { + //NOTE(martin): add start and end cap + mg_stroke_cap(backend, startPoint, (vec2){-startTangent.x, -startTangent.y}, attributes); + mg_stroke_cap(backend, endPoint, startTangent, attributes); + } + + return(eltIndex); +} + + +void mg_render_stroke(mg_gl_canvas_backend* backend, + mg_path_elt* elements, + mg_path_descriptor* path, + mg_attributes* attributes) +{ + u32 eltCount = path->count; + DEBUG_ASSERT(eltCount); + + vec2 startPoint = path->startPoint; + u32 startIndex = 0; + + while(startIndex < eltCount) + { + //NOTE(martin): eliminate leading moves + while(startIndex < eltCount && elements[startIndex].type == MG_PATH_MOVE) + { + startPoint = elements[startIndex].p[0]; + startIndex++; + } + + if(startIndex < eltCount) + { + startIndex = mg_render_stroke_subpath(backend, elements, path, attributes, startIndex, startPoint); + } + } +} + +//-------------------------------------------------------------------- +// GL dispatch +//-------------------------------------------------------------------- +#define LayoutNext(prevName, prevType, nextType) \ + AlignUpOnPow2(_cat3_(LAYOUT_, prevName, _OFFSET)+_cat3_(LAYOUT_, prevType, _SIZE), _cat3_(LAYOUT_, nextType, _ALIGN)) + +enum { + LAYOUT_VEC2_SIZE = 8, + LAYOUT_VEC2_ALIGN = 8, + LAYOUT_VEC4_SIZE = 16, + LAYOUT_VEC4_ALIGN = 16, + LAYOUT_INT_SIZE = 4, + LAYOUT_INT_ALIGN = 4, + LAYOUT_MAT2x3_SIZE = sizeof(float)*6, + LAYOUT_MAT2x3_ALIGN = 4, + + LAYOUT_CUBIC_OFFSET = 0, + LAYOUT_POS_OFFSET = LayoutNext(CUBIC, VEC4, VEC2), + LAYOUT_ZINDEX_OFFSET = LayoutNext(POS, VEC2, INT), + LAYOUT_VERTEX_ALIGN = 16, + LAYOUT_VERTEX_SIZE = LayoutNext(ZINDEX, INT, VERTEX), + + LAYOUT_COLOR_OFFSET = 0, + LAYOUT_CLIP_OFFSET = LayoutNext(COLOR, VEC4, VEC4), + LAYOUT_UV_TRANSFORM_OFFSET = LayoutNext(CLIP, VEC4, MAT2x3), + LAYOUT_SHAPE_ALIGN = 16, + LAYOUT_SHAPE_SIZE = LayoutNext(UV_TRANSFORM, MAT2x3, SHAPE), + + MG_GL_CANVAS_MAX_BUFFER_LENGTH = 1<<20, + MG_GL_CANVAS_MAX_SHAPE_BUFFER_SIZE = LAYOUT_SHAPE_SIZE * MG_GL_CANVAS_MAX_BUFFER_LENGTH, + MG_GL_CANVAS_MAX_VERTEX_BUFFER_SIZE = LAYOUT_VERTEX_SIZE * MG_GL_CANVAS_MAX_BUFFER_LENGTH, + MG_GL_CANVAS_MAX_INDEX_BUFFER_SIZE = LAYOUT_INT_SIZE * MG_GL_CANVAS_MAX_BUFFER_LENGTH, + + //TODO: actually size this dynamically + MG_GL_CANVAS_MAX_TILE_COUNT = 65536, //NOTE: this allows for 256*256 tiles (e.g. 4096*4096 pixels) + MG_GL_CANVAS_TILE_COUNTER_BUFFER_SIZE = LAYOUT_INT_SIZE * MG_GL_CANVAS_MAX_TILE_COUNT, + + MG_GL_CANVAS_TILE_ARRAY_LENGTH = 1<<10, // max overlapping triangles per tiles + MG_GL_CANVAS_TILE_ARRAY_BUFFER_SIZE = LAYOUT_INT_SIZE * MG_GL_CANVAS_MAX_TILE_COUNT * MG_GL_CANVAS_TILE_ARRAY_LENGTH, +}; + +void mg_gl_canvas_update_vertex_layout(mg_gl_canvas_backend* backend) +{ + backend->vertexLayout = (mg_vertex_layout){ + .maxVertexCount = MG_GL_CANVAS_MAX_BUFFER_LENGTH, + .maxIndexCount = MG_GL_CANVAS_MAX_BUFFER_LENGTH, + .posBuffer = backend->vertexMapping + LAYOUT_POS_OFFSET, + .posStride = LAYOUT_VERTEX_SIZE, + .cubicBuffer = backend->vertexMapping + LAYOUT_CUBIC_OFFSET, + .cubicStride = LAYOUT_VERTEX_SIZE, + .shapeIndexBuffer = backend->vertexMapping + LAYOUT_ZINDEX_OFFSET, + .shapeIndexStride = LAYOUT_VERTEX_SIZE, + + .colorBuffer = backend->shapeMapping + LAYOUT_COLOR_OFFSET, + .colorStride = LAYOUT_SHAPE_SIZE, + .clipBuffer = backend->shapeMapping + LAYOUT_CLIP_OFFSET, + .clipStride = LAYOUT_SHAPE_SIZE, + .uvTransformBuffer = backend->shapeMapping + LAYOUT_UV_TRANSFORM_OFFSET, + .uvTransformStride = LAYOUT_SHAPE_SIZE, + + .indexBuffer = backend->indexMapping, + .indexStride = LAYOUT_INT_SIZE}; +} + +void mg_gl_send_buffers(mg_gl_canvas_backend* backend, int shapeCount, int vertexCount, int indexCount) +{ + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->vertexBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, LAYOUT_VERTEX_SIZE*vertexCount, backend->vertexMapping, GL_STREAM_DRAW); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->shapeBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, LAYOUT_SHAPE_SIZE*shapeCount, backend->shapeMapping, GL_STREAM_DRAW); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->indexBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, LAYOUT_INT_SIZE*indexCount, backend->indexMapping, GL_STREAM_DRAW); +} + +void mg_gl_canvas_begin(mg_gl_canvas_backend* backend) +{ + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + glClearColor(backend->clearColor.r, backend->clearColor.g, backend->clearColor.b, backend->clearColor.a); + glClear(GL_COLOR_BUFFER_BIT); +} + +void mg_gl_canvas_end(mg_gl_canvas_backend* backend) +{ + //NOTE: nothing to do here... +} + +void mg_gl_canvas_clear(mg_canvas_backend* interface, mg_color clearColor) +{ + mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; + + glClearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a); + glClear(GL_COLOR_BUFFER_BIT); +} + +void mg_gl_canvas_draw_batch(mg_gl_canvas_backend* backend, mg_image_data* imageInterface, u32 shapeCount, u32 vertexCount, u32 indexCount) +{ + mg_finalize_shape(backend); + +/*NOTE: if we want debug_vertex while debugging, the following ensures the struct def doesn't get stripped away + debug_vertex vertex; + debug_shape shape; + printf("foo %p, bar %p\n", &vertex, &shape); +//*/ + mg_gl_send_buffers(backend, shapeCount, vertexCount, indexCount); + + mg_wgl_surface* surface = backend->surface; + + mp_rect frame = surface->interface.getFrame((mg_surface_data*)surface); + vec2 contentsScaling = surface->interface.contentsScaling((mg_surface_data*)surface); + + const int tileSize = 16; + const int tileCountX = (frame.w*contentsScaling.x + tileSize - 1)/tileSize; + const int tileCountY = (frame.h*contentsScaling.y + tileSize - 1)/tileSize; + const int tileArrayLength = MG_GL_CANVAS_TILE_ARRAY_LENGTH; + + //TODO: ensure there's enough space in tile buffer + + //NOTE: first clear counters + glUseProgram(backend->clearCounterProgram); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, backend->tileCounterBuffer); + glDispatchCompute(tileCountX*tileCountY, 1, 1); + + //NOTE: we first distribute triangles into tiles: + + glUseProgram(backend->tileProgram); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, backend->vertexBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->shapeBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, backend->indexBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, backend->tileCounterBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, backend->tileArrayBuffer); + + glUniform1ui(0, indexCount); + glUniform2ui(1, tileCountX, tileCountY); + glUniform1ui(2, tileSize); + glUniform1ui(3, tileArrayLength); + glUniform2f(4, contentsScaling.x, contentsScaling.y); + + u32 threadCount = indexCount/3; + glDispatchCompute((threadCount + 255)/256, 1, 1); + + //NOTE: next we sort triangles in each tile + glUseProgram(backend->sortProgram); + + glUniform1ui(0, indexCount); + glUniform2ui(1, tileCountX, tileCountY); + glUniform1ui(2, tileSize); + glUniform1ui(3, tileArrayLength); + + glDispatchCompute(tileCountX * tileCountY, 1, 1); + + //NOTE: then we fire the drawing shader that will select only triangles in its tile + glUseProgram(backend->drawProgram); + + glBindImageTexture(0, backend->outTexture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8); + + glUniform1ui(0, indexCount); + glUniform2ui(1, tileCountX, tileCountY); + glUniform1ui(2, tileSize); + glUniform1ui(3, tileArrayLength); + glUniform2f(4, contentsScaling.x, contentsScaling.y); + + if(imageInterface) + { + //TODO: make sure this image belongs to that context + mg_gl_image* image = (mg_gl_image*)imageInterface; + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, image->textureID); + glUniform1ui(5, 1); + } + else + { + glUniform1ui(5, 0); + } + + glDispatchCompute(tileCountX, tileCountY, 1); + + //NOTE: now blit out texture to surface + glUseProgram(backend->blitProgram); + glBindBuffer(GL_ARRAY_BUFFER, backend->dummyVertexBuffer); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, backend->outTexture); + glUniform1i(0, 0); + + glDrawArrays(GL_TRIANGLES, 0, 6); + + mg_gl_canvas_update_vertex_layout(backend); + + mg_reset_shape_index(backend); + + backend->vertexCount = 0; + backend->indexCount = 0; +} + + +void mg_gl_canvas_render(mg_canvas_backend* interface, + mg_color clearColor, + u32 primitiveCount, + mg_primitive* primitives, + u32 eltCount, + mg_path_elt* pathElements) +{ + mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; + + u32 nextIndex = 0; + + mg_reset_shape_index(backend); + + backend->clip = (mp_rect){-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX}; + backend->image = mg_image_nil(); + backend->clearColor = clearColor; + mg_gl_canvas_begin(backend); + + for(int i=0; i= primitiveCount) + { + log_error("invalid location '%i' in graphics command buffer would cause an overrun\n", nextIndex); + break; + } + mg_primitive* primitive = &(primitives[nextIndex]); + nextIndex++; + + if(i && primitive->attributes.image.h != backend->image.h) + { + mg_image_data* imageData = mg_image_data_from_handle(backend->image); + mg_gl_canvas_draw_batch(backend, imageData, backend->nextShapeIndex, backend->vertexCount, backend->indexCount); + backend->image = primitive->attributes.image; + } + + switch(primitive->cmd) + { + case MG_CMD_FILL: + { + mg_next_shape(backend, &primitive->attributes); + mg_render_fill(backend, + pathElements + primitive->path.startIndex, + &primitive->path); + } break; + + case MG_CMD_STROKE: + { + mg_render_stroke(backend, + pathElements + primitive->path.startIndex, + &primitive->path, + &primitive->attributes); + } break; + + case MG_CMD_JUMP: + { + if(primitive->jump == ~0) + { + //NOTE(martin): normal end of stream marker + goto exit_command_loop; + } + else if(primitive->jump >= primitiveCount) + { + log_error("invalid jump location '%i' in graphics command buffer\n", primitive->jump); + goto exit_command_loop; + } + else + { + nextIndex = primitive->jump; + } + } break; + } + } + exit_command_loop: ; + + mg_image_data* imageData = mg_image_data_from_handle(backend->image); + mg_gl_canvas_draw_batch(backend, imageData, backend->nextShapeIndex, backend->vertexCount, backend->indexCount); + + mg_gl_canvas_end(backend); + + //NOTE(martin): clear buffers + backend->vertexCount = 0; + backend->indexCount = 0; +} +//-------------------------------------------------------------------- +// Image API +//-------------------------------------------------------------------- +mg_image_data* mg_gl_canvas_image_create(mg_canvas_backend* interface, vec2 size) +{ + mg_gl_image* image = 0; + + image = malloc_type(mg_gl_image); + if(image) + { + glGenTextures(1, &image->textureID); + glBindTexture(GL_TEXTURE_2D, image->textureID); +// glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, size.x, size.y); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + image->interface.size = size; + } + return((mg_image_data*)image); +} + +void mg_gl_canvas_image_destroy(mg_canvas_backend* interface, mg_image_data* imageInterface) +{ + //TODO: check that this image belongs to this context + mg_gl_image* image = (mg_gl_image*)imageInterface; + glDeleteTextures(1, &image->textureID); + free(image); +} + +void mg_gl_canvas_image_upload_region(mg_canvas_backend* interface, + mg_image_data* imageInterface, + mp_rect region, + u8* pixels) +{ + //TODO: check that this image belongs to this context + mg_gl_image* image = (mg_gl_image*)imageInterface; + glBindTexture(GL_TEXTURE_2D, image->textureID); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, region.w, region.h, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); +} + +//-------------------------------------------------------------------- +// Canvas setup / destroy +//-------------------------------------------------------------------- + +void mg_gl_canvas_destroy(mg_canvas_backend* interface) +{ + mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; + + glDeleteTextures(1, &backend->outTexture); + + glDeleteBuffers(1, &backend->dummyVertexBuffer); + glDeleteBuffers(1, &backend->vertexBuffer); + glDeleteBuffers(1, &backend->shapeBuffer); + glDeleteBuffers(1, &backend->indexBuffer); + glDeleteBuffers(1, &backend->tileCounterBuffer); + glDeleteBuffers(1, &backend->tileArrayBuffer); + + glDeleteVertexArrays(1, &backend->vao); + + free(backend->shapeMapping); + free(backend->vertexMapping); + free(backend->indexMapping); + free(backend); +} + +static int mg_gl_compile_shader(const char* name, GLuint shader, const char* source) +{ + int res = 0; + + const char* sources[3] = {"#version 430", glsl_common, source}; + + glShaderSource(shader, 3, sources, 0); + glCompileShader(shader); + + int status = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + if(!status) + { + char buffer[256]; + int size = 0; + glGetShaderInfoLog(shader, 256, &size, buffer); + printf("Shader compile error (%s): %.*s\n", name, size, buffer); + res = -1; + } + return(res); +} + +static int mg_gl_canvas_compile_compute_program_named(const char* name, const char* source, GLuint* outProgram) +{ + int res = 0; + *outProgram = 0; + + GLuint shader = glCreateShader(GL_COMPUTE_SHADER); + GLuint program = glCreateProgram(); + + res |= mg_gl_compile_shader(name, shader, source); + + if(!res) + { + glAttachShader(program, shader); + glLinkProgram(program); + + int status = 0; + glGetProgramiv(program, GL_LINK_STATUS, &status); + if(!status) + { + char buffer[256]; + int size = 0; + glGetProgramInfoLog(program, 256, &size, buffer); + log_error("Shader link error (%s): %.*s\n", name, size, buffer); + + res = -1; + } + else + { + *outProgram = program; + } + } + return(res); +} + +int mg_gl_canvas_compile_render_program_named(const char* progName, + const char* vertexName, + const char* fragmentName, + const char* vertexSrc, + const char* fragmentSrc, + GLuint* outProgram) +{ + int res = 0; + *outProgram = 0; + + GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); + GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); + GLuint program = glCreateProgram(); + + res |= mg_gl_compile_shader(vertexName, vertexShader, vertexSrc); + res |= mg_gl_compile_shader(fragmentName, fragmentShader, fragmentSrc); + + if(!res) + { + glAttachShader(program, vertexShader); + glAttachShader(program, fragmentShader); + glLinkProgram(program); + + int status = 0; + glGetProgramiv(program, GL_LINK_STATUS, &status); + if(!status) + { + char buffer[256]; + int size = 0; + glGetProgramInfoLog(program, 256, &size, buffer); + log_error("Shader link error (%s): %.*s\n", progName, size, buffer); + res = -1; + } + else + { + *outProgram = program; + } + } + return(res); +} + +#define mg_gl_canvas_compile_compute_program(src, out) \ + mg_gl_canvas_compile_compute_program_named(#src, src, out) + +#define mg_gl_canvas_compile_render_program(progName, shaderSrc, vertexSrc, out) \ + mg_gl_canvas_compile_render_program_named(progName, #shaderSrc, #vertexSrc, shaderSrc, vertexSrc, out) + +mg_canvas_backend* gl_canvas_backend_create(mg_wgl_surface* surface) +{ + mg_gl_canvas_backend* backend = malloc_type(mg_gl_canvas_backend); + if(backend) + { + ////////////////////////////////////////////////////// + //TODO + ////////////////////////////////////////////////////// + memset(backend, 0, sizeof(mg_gl_canvas_backend)); + backend->surface = surface; + + //NOTE(martin): setup interface functions + backend->interface.destroy = mg_gl_canvas_destroy; + backend->interface.render = mg_gl_canvas_render; + backend->interface.imageCreate = mg_gl_canvas_image_create; + backend->interface.imageDestroy = mg_gl_canvas_image_destroy; + backend->interface.imageUploadRegion = mg_gl_canvas_image_upload_region; + + /* + backend->interface.destroy = mg_gl_canvas_destroy; + backend->interface.begin = mg_gl_canvas_begin; + backend->interface.end = mg_gl_canvas_end; + backend->interface.clear = mg_gl_canvas_clear; + backend->interface.drawBatch = mg_gl_canvas_draw_batch; + */ + + surface->interface.prepare((mg_surface_data*)surface); + + glGenVertexArrays(1, &backend->vao); + glBindVertexArray(backend->vao); + + glGenBuffers(1, &backend->dummyVertexBuffer); + glBindBuffer(GL_ARRAY_BUFFER, backend->dummyVertexBuffer); + + glGenBuffers(1, &backend->vertexBuffer); + glGenBuffers(1, &backend->shapeBuffer); + glGenBuffers(1, &backend->indexBuffer); + + glGenBuffers(1, &backend->tileCounterBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileCounterBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_CANVAS_TILE_COUNTER_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); + + glGenBuffers(1, &backend->tileArrayBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileArrayBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_CANVAS_TILE_ARRAY_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); + + mp_rect frame = surface->interface.getFrame((mg_surface_data*)surface); + vec2 contentsScaling = surface->interface.contentsScaling((mg_surface_data*)surface); + + glGenTextures(1, &backend->outTexture); + glBindTexture(GL_TEXTURE_2D, backend->outTexture); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, frame.w*contentsScaling.x, frame.h*contentsScaling.y); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + //NOTE: create programs + int err = 0; + err |= mg_gl_canvas_compile_compute_program(glsl_clear_counters, &backend->clearCounterProgram); + err |= mg_gl_canvas_compile_compute_program(glsl_tile, &backend->tileProgram); + err |= mg_gl_canvas_compile_compute_program(glsl_sort, &backend->sortProgram); + err |= mg_gl_canvas_compile_compute_program(glsl_draw, &backend->drawProgram); + err |= mg_gl_canvas_compile_render_program("blit", glsl_blit_vertex, glsl_blit_fragment, &backend->blitProgram); + + if(glGetError() != GL_NO_ERROR) + { + err |= -1; + } + + backend->shapeMapping = malloc_array(char, MG_GL_CANVAS_MAX_SHAPE_BUFFER_SIZE); + backend->vertexMapping = malloc_array(char, MG_GL_CANVAS_MAX_VERTEX_BUFFER_SIZE); + backend->indexMapping = malloc_array(char, MG_GL_CANVAS_MAX_INDEX_BUFFER_SIZE); + + if( !backend->shapeMapping + || !backend->shapeMapping + || !backend->shapeMapping) + { + err |= -1; + } + + if(err) + { + mg_gl_canvas_destroy((mg_canvas_backend*)backend); + backend = 0; + } + else + { + mg_gl_canvas_update_vertex_layout(backend); + } + } + return((mg_canvas_backend*)backend); +} + +mg_surface_data* gl_canvas_surface_create_for_window(mp_window window) +{ + mg_wgl_surface* surface = (mg_wgl_surface*)mg_wgl_surface_create_for_window(window); + + if(surface) + { + surface->interface.backend = gl_canvas_backend_create(surface); + if(surface->interface.backend) + { + surface->interface.api = MG_CANVAS; + } + else + { + surface->interface.destroy((mg_surface_data*)surface); + surface = 0; + } + } + return((mg_surface_data*)surface); +} diff --git a/src/glsl_shaders.h b/src/glsl_shaders.h deleted file mode 100644 index 6745a33..0000000 --- a/src/glsl_shaders.h +++ /dev/null @@ -1,469 +0,0 @@ -/********************************************************************* -* -* file: glsl_shaders.h -* note: string literals auto-generated by embed_text.py -* date: 09/032023 -* -**********************************************************************/ -#ifndef __GLSL_SHADERS_H__ -#define __GLSL_SHADERS_H__ - - -//NOTE: string imported from src\glsl_shaders\common.glsl -const char* glsl_common = -"\n" -"layout(std430) buffer;\n" -"\n" -"struct vertex {\n" -" vec4 cubic;\n" -" vec2 pos;\n" -" int shapeIndex;\n" -"};\n" -"\n" -"struct shape {\n" -" vec4 color;\n" -" vec4 clip;\n" -" float uvTransform[6];\n" -"};\n"; - -//NOTE: string imported from src\glsl_shaders\blit_vertex.glsl -const char* glsl_blit_vertex = -"\n" -"precision mediump float;\n" -"\n" -"out vec2 uv;\n" -"\n" -"void main()\n" -"{\n" -" /* generate (0, 0) (1, 0) (1, 1) (1, 1) (0, 1) (0, 0)*/\n" -"\n" -" float x = float(((uint(gl_VertexID) + 2u) / 3u)%2u);\n" -" float y = float(((uint(gl_VertexID) + 1u) / 3u)%2u);\n" -"\n" -" gl_Position = vec4(-1.0f + x*2.0f, -1.0f+y*2.0f, 0.0f, 1.0f);\n" -" uv = vec2(x, 1-y);\n" -"}\n"; - -//NOTE: string imported from src\glsl_shaders\blit_fragment.glsl -const char* glsl_blit_fragment = -"\n" -"precision mediump float;\n" -"\n" -"in vec2 uv;\n" -"out vec4 fragColor;\n" -"\n" -"layout(location=0) uniform sampler2D tex;\n" -"\n" -"void main()\n" -"{\n" -" fragColor = texture(tex, uv);\n" -"}\n"; - -//NOTE: string imported from src\glsl_shaders\clear_counters.glsl -const char* glsl_clear_counters = -"\n" -"layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;\n" -"\n" -"precision mediump float;\n" -"layout(std430) buffer;\n" -"\n" -"layout(binding = 0) coherent restrict writeonly buffer tileCounterBufferSSBO {\n" -" uint elements[];\n" -"} tileCounterBuffer ;\n" -"\n" -"void main()\n" -"{\n" -" uint tileIndex = gl_WorkGroupID.x;\n" -" tileCounterBuffer.elements[tileIndex] = 0u;\n" -"}\n"; - -//NOTE: string imported from src\glsl_shaders\tile.glsl -const char* glsl_tile = -"\n" -"layout(local_size_x = 512, local_size_y = 1, local_size_z = 1) in;\n" -"\n" -"precision mediump float;\n" -"\n" -"layout(binding = 0) restrict readonly buffer vertexBufferSSBO {\n" -" vertex elements[];\n" -"} vertexBuffer ;\n" -"\n" -"layout(binding = 1) restrict readonly buffer shapeBufferSSBO {\n" -" shape elements[];\n" -"} shapeBuffer ;\n" -"\n" -"layout(binding = 2) restrict readonly buffer indexBufferSSBO {\n" -" uint elements[];\n" -"} indexBuffer ;\n" -"\n" -"layout(binding = 3) coherent restrict buffer tileCounterBufferSSBO {\n" -" uint elements[];\n" -"} tileCounterBuffer ;\n" -"\n" -"layout(binding = 4) coherent restrict writeonly buffer tileArrayBufferSSBO {\n" -" uint elements[];\n" -"} tileArrayBuffer ;\n" -"\n" -"layout(location = 0) uniform uint indexCount;\n" -"layout(location = 1) uniform uvec2 tileCount;\n" -"layout(location = 2) uniform uint tileSize;\n" -"layout(location = 3) uniform uint tileArraySize;\n" -"layout(location = 4) uniform vec2 scaling;\n" -"\n" -"void main()\n" -"{\n" -" uint triangleIndex = (gl_WorkGroupID.x*gl_WorkGroupSize.x + gl_LocalInvocationIndex) * 3u;\n" -" if(triangleIndex >= indexCount)\n" -" {\n" -" return;\n" -" }\n" -"\n" -" uint i0 = indexBuffer.elements[triangleIndex];\n" -" uint i1 = indexBuffer.elements[triangleIndex+1u];\n" -" uint i2 = indexBuffer.elements[triangleIndex+2u];\n" -"\n" -" vec2 p0 = vertexBuffer.elements[i0].pos * scaling;\n" -" vec2 p1 = vertexBuffer.elements[i1].pos * scaling;\n" -" vec2 p2 = vertexBuffer.elements[i2].pos * scaling;\n" -"\n" -" int shapeIndex = vertexBuffer.elements[i0].shapeIndex;\n" -" vec4 clip = shapeBuffer.elements[shapeIndex].clip * vec4(scaling, scaling);\n" -"\n" -" vec4 fbox = vec4(max(min(min(p0.x, p1.x), p2.x), clip.x),\n" -" max(min(min(p0.y, p1.y), p2.y), clip.y),\n" -" min(max(max(p0.x, p1.x), p2.x), clip.z),\n" -" min(max(max(p0.y, p1.y), p2.y), clip.w));\n" -"\n" -" ivec4 box = ivec4(floor(fbox))/int(tileSize);\n" -"\n" -" //NOTE(martin): it's importat to do the computation with signed int, so that we can have negative xMax/yMax\n" -" // otherwise all triangles on the left or below the x/y axis are attributed to tiles on row/column 0.\n" -" int xMin = max(0, box.x);\n" -" int yMin = max(0, box.y);\n" -" int xMax = min(box.z, int(tileCount.x) - 1);\n" -" int yMax = min(box.w, int(tileCount.y) - 1);\n" -"\n" -" for(int y = yMin; y <= yMax; y++)\n" -" {\n" -" for(int x = xMin ; x <= xMax; x++)\n" -" {\n" -" uint tileIndex = uint(y)*tileCount.x + uint(x);\n" -" uint tileCounter = atomicAdd(tileCounterBuffer.elements[tileIndex], 1u);\n" -" if(tileCounter < tileArraySize)\n" -" {\n" -" tileArrayBuffer.elements[tileArraySize*tileIndex + tileCounter] = triangleIndex;\n" -" }\n" -" }\n" -" }\n" -"}\n"; - -//NOTE: string imported from src\glsl_shaders\sort.glsl -const char* glsl_sort = -"\n" -"layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in;\n" -"\n" -"precision mediump float;\n" -"\n" -"layout(binding = 0) restrict readonly buffer vertexBufferSSBO {\n" -" vertex elements[];\n" -"} vertexBuffer ;\n" -"\n" -"layout(binding = 1) restrict readonly buffer shapeBufferSSBO {\n" -" shape elements[];\n" -"} shapeBuffer ;\n" -"\n" -"layout(binding = 2) restrict readonly buffer indexBufferSSBO {\n" -" uint elements[];\n" -"} indexBuffer ;\n" -"\n" -"layout(binding = 3) coherent readonly restrict buffer tileCounterBufferSSBO {\n" -" uint elements[];\n" -"} tileCounterBuffer ;\n" -"\n" -"layout(binding = 4) coherent restrict buffer tileArrayBufferSSBO {\n" -" uint elements[];\n" -"} tileArrayBuffer ;\n" -"\n" -"layout(location = 0) uniform uint indexCount;\n" -"layout(location = 1) uniform uvec2 tileCount;\n" -"layout(location = 2) uniform uint tileSize;\n" -"layout(location = 3) uniform uint tileArraySize;\n" -"\n" -"int get_shape_index(uint tileArrayOffset, uint tileArrayIndex)\n" -"{\n" -" uint triangleIndex = tileArrayBuffer.elements[tileArrayOffset + tileArrayIndex];\n" -" uint i0 = indexBuffer.elements[triangleIndex];\n" -" int shapeIndex = vertexBuffer.elements[i0].shapeIndex;\n" -" return(shapeIndex);\n" -"}\n" -"\n" -"void main()\n" -"{\n" -" uint tileIndex = gl_WorkGroupID.x;\n" -" uint tileArrayOffset = tileArraySize * tileIndex;\n" -" uint tileArrayCount = min(tileCounterBuffer.elements[tileIndex], tileArraySize);\n" -"\n" -" for(uint tileArrayIndex=1u; tileArrayIndex < tileArrayCount; tileArrayIndex++)\n" -" {\n" -" for(uint sortIndex = tileArrayIndex; sortIndex > 0u; sortIndex--)\n" -" {\n" -" int shapeIndex = get_shape_index(tileArrayOffset, sortIndex);\n" -" int prevShapeIndex = get_shape_index(tileArrayOffset, sortIndex-1u);\n" -"\n" -" if(shapeIndex >= prevShapeIndex)\n" -" {\n" -" break;\n" -" }\n" -" uint tmp = tileArrayBuffer.elements[tileArrayOffset + sortIndex];\n" -" tileArrayBuffer.elements[tileArrayOffset + sortIndex] = tileArrayBuffer.elements[tileArrayOffset + sortIndex - 1u];\n" -" tileArrayBuffer.elements[tileArrayOffset + sortIndex - 1u] = tmp;\n" -" }\n" -" }\n" -"}\n"; - -//NOTE: string imported from src\glsl_shaders\draw.glsl -const char* glsl_draw = -"\n" -"#extension GL_ARB_gpu_shader_int64 : require\n" -"layout(local_size_x = 16, local_size_y = 16, local_size_z = 1) in;\n" -"\n" -"precision mediump float;\n" -"//precision mediump image2D;\n" -"\n" -"layout(binding = 0) restrict readonly buffer vertexBufferSSBO {\n" -" vertex elements[];\n" -"} vertexBuffer ;\n" -"\n" -"layout(binding = 1) restrict readonly buffer shapeBufferSSBO {\n" -" shape elements[];\n" -"} shapeBuffer ;\n" -"\n" -"layout(binding = 2) restrict readonly buffer indexBufferSSBO {\n" -" uint elements[];\n" -"} indexBuffer ;\n" -"\n" -"layout(binding = 3) restrict readonly buffer tileCounterBufferSSBO {\n" -" uint elements[];\n" -"} tileCounterBuffer ;\n" -"\n" -"layout(binding = 4) restrict readonly buffer tileArrayBufferSSBO {\n" -" uint elements[];\n" -"} tileArrayBuffer ;\n" -"\n" -"layout(location = 0) uniform uint indexCount;\n" -"layout(location = 1) uniform uvec2 tileCount;\n" -"layout(location = 2) uniform uint tileSize;\n" -"layout(location = 3) uniform uint tileArraySize;\n" -"layout(location = 4) uniform vec2 scaling;\n" -"layout(location = 5) uniform uint useTexture;\n" -"\n" -"layout(rgba8, binding = 0) uniform restrict writeonly image2D outTexture;\n" -"\n" -"layout(binding = 1) uniform sampler2D srcTexture;\n" -"\n" -"\n" -"bool is_top_left(ivec2 a, ivec2 b)\n" -"{\n" -" return( (a.y == b.y && b.x < a.x)\n" -" ||(b.y < a.y));\n" -"}\n" -"\n" -"//////////////////////////////////////////////////////////////////////////////\n" -"//TODO: we should do these computations on 64bits, because otherwise\n" -"// we might overflow for values > 2048.\n" -"// Unfortunately this is costly.\n" -"// Another way is to precompute triangle edges (b - a) in full precision\n" -"// once to avoid doing it all the time...\n" -"//////////////////////////////////////////////////////////////////////////////\n" -"int orient2d(ivec2 a, ivec2 b, ivec2 p)\n" -"{\n" -" return((b.x-a.x)*(p.y-a.y) - (b.y-a.y)*(p.x-a.x));\n" -"}\n" -"\n" -"int is_clockwise(ivec2 p0, ivec2 p1, ivec2 p2)\n" -"{\n" -" return((p1 - p0).x*(p2 - p0).y - (p1 - p0).y*(p2 - p0).x);\n" -"}\n" -"\n" -"void main()\n" -"{\n" -" ivec2 pixelCoord = ivec2(gl_WorkGroupID.xy*uvec2(16, 16) + gl_LocalInvocationID.xy);\n" -" uvec2 tileCoord = uvec2(pixelCoord) / tileSize;\n" -" uint tileIndex = tileCoord.y * tileCount.x + tileCoord.x;\n" -" uint tileCounter = min(tileCounterBuffer.elements[tileIndex], tileArraySize);\n" -"\n" -" const float subPixelFactor = 16.;\n" -" ivec2 centerPoint = ivec2((vec2(pixelCoord) + vec2(0.5, 0.5)) * subPixelFactor);\n" -"\n" -"//*\n" -" const int sampleCount = 8;\n" -" ivec2 samplePoints[sampleCount] = ivec2[sampleCount](centerPoint + ivec2(1, 3),\n" -" centerPoint + ivec2(-1, -3),\n" -" centerPoint + ivec2(5, -1),\n" -" centerPoint + ivec2(-3, 5),\n" -" centerPoint + ivec2(-5, -5),\n" -" centerPoint + ivec2(-7, 1),\n" -" centerPoint + ivec2(3, -7),\n" -" centerPoint + ivec2(7, 7));\n" -"/*/\n" -" const int sampleCount = 4;\n" -" ivec2 samplePoints[sampleCount] = ivec2[sampleCount](centerPoint + ivec2(-2, 6),\n" -" centerPoint + ivec2(6, 2),\n" -" centerPoint + ivec2(-6, -2),\n" -" centerPoint + ivec2(2, -6));\n" -"//*/\n" -" //DEBUG\n" -"/*\n" -" {\n" -" vec4 fragColor = vec4(0);\n" -"\n" -" if( pixelCoord.x % 16 == 0\n" -" ||pixelCoord.y % 16 == 0)\n" -" {\n" -" fragColor = vec4(0, 0, 0, 1);\n" -" }\n" -" else if(tileCounterBuffer.elements[tileIndex] == 0xffffu)\n" -" {\n" -" fragColor = vec4(1, 0, 1, 1);\n" -" }\n" -" else if(tileCounter != 0u)\n" -" {\n" -" fragColor = vec4(0, 1, 0, 1);\n" -" }\n" -" else\n" -" {\n" -" fragColor = vec4(1, 0, 0, 1);\n" -" }\n" -" imageStore(outTexture, pixelCoord, fragColor);\n" -" return;\n" -" }\n" -"//*/\n" -" //----\n" -"\n" -" vec4 sampleColor[sampleCount];\n" -" vec4 currentColor[sampleCount];\n" -" int currentShapeIndex[sampleCount];\n" -" int flipCount[sampleCount];\n" -"\n" -" for(int i=0; i clip.z\n" -" || samplePoint.y < clip.y\n" -" || samplePoint.y > clip.w)\n" -" {\n" -" continue;\n" -" }\n" -"\n" -" int w0 = orient2d(p1, p2, samplePoint);\n" -" int w1 = orient2d(p2, p0, samplePoint);\n" -" int w2 = orient2d(p0, p1, samplePoint);\n" -"\n" -" if((w0+bias0) >= 0 && (w1+bias1) >= 0 && (w2+bias2) >= 0)\n" -" {\n" -" vec4 cubic = (cubic0*float(w0) + cubic1*float(w1) + cubic2*float(w2))/(float(w0)+float(w1)+float(w2));\n" -"\n" -" float eps = 0.0001;\n" -" if(cubic.w*(cubic.x*cubic.x*cubic.x - cubic.y*cubic.z) <= eps)\n" -" {\n" -" if(shapeIndex == currentShapeIndex[sampleIndex])\n" -" {\n" -" flipCount[sampleIndex]++;\n" -" }\n" -" else\n" -" {\n" -" if((flipCount[sampleIndex] & 0x01) != 0)\n" -" {\n" -" sampleColor[sampleIndex] = currentColor[sampleIndex];\n" -" }\n" -"\n" -" vec4 nextColor = color;\n" -" if(useTexture)\n" -" {\n" -" vec3 sampleFP = vec3(vec2(samplePoint).xy/(subPixelFactor*2.), 1);\n" -" vec2 uv = (uvTransform * sampleFP).xy;\n" -" vec4 texColor = texture(srcTexture, uv);\n" -" texColor.rgb *= texColor.a;\n" -" nextColor *= texColor;\n" -" }\n" -" currentColor[sampleIndex] = sampleColor[sampleIndex]*(1.-nextColor.a) + nextColor;\n" -" currentShapeIndex[sampleIndex] = shapeIndex;\n" -" flipCount[sampleIndex] = 1;\n" -" }\n" -" }\n" -" }\n" -" }\n" -" }\n" -" vec4 pixelColor = vec4(0);\n" -" for(int sampleIndex = 0; sampleIndex < sampleCount; sampleIndex++)\n" -" {\n" -" if((flipCount[sampleIndex] & 0x01) != 0)\n" -" {\n" -" sampleColor[sampleIndex] = currentColor[sampleIndex];\n" -" }\n" -" pixelColor += sampleColor[sampleIndex];\n" -" }\n" -"\n" -" imageStore(outTexture, pixelCoord, pixelColor/float(sampleCount));\n" -"}\n"; - -#endif // __GLSL_SHADERS_H__ diff --git a/src/graphics_surface.c b/src/graphics_surface.c index 3c39420..20eec15 100644 --- a/src/graphics_surface.c +++ b/src/graphics_surface.c @@ -1,407 +1,407 @@ -/************************************************************//** -* -* @file: graphics_surface.c -* @author: Martin Fouilleul -* @date: 25/04/2023 -* -*****************************************************************/ - -#include"graphics_surface.h" - -//--------------------------------------------------------------- -// typed handles functions -//--------------------------------------------------------------- - -mg_surface mg_surface_handle_alloc(mg_surface_data* surface) -{ - mg_surface handle = {.h = mg_handle_alloc(MG_HANDLE_SURFACE, (void*)surface) }; - return(handle); -} - -mg_surface_data* mg_surface_data_from_handle(mg_surface handle) -{ - mg_surface_data* data = mg_data_from_handle(MG_HANDLE_SURFACE, handle.h); - return(data); -} - -mg_image mg_image_handle_alloc(mg_image_data* image) -{ - mg_image handle = {.h = mg_handle_alloc(MG_HANDLE_IMAGE, (void*)image) }; - return(handle); -} - -mg_image_data* mg_image_data_from_handle(mg_image handle) -{ - mg_image_data* data = mg_data_from_handle(MG_HANDLE_IMAGE, handle.h); - return(data); -} - -//--------------------------------------------------------------- -// surface API -//--------------------------------------------------------------- - -#if MG_COMPILE_GL - #if PLATFORM_WINDOWS - #include"wgl_surface.h" - #define gl_surface_create_for_window mg_wgl_surface_create_for_window - #endif -#endif - -#if MG_COMPILE_GLES - #include"egl_surface.h" -#endif - -#if MG_COMPILE_METAL - #include"mtl_surface.h" -#endif - -#if MG_COMPILE_CANVAS - #if PLATFORM_MACOS - mg_surface_data* mtl_canvas_surface_create_for_window(mp_window window); - #elif PLATFORM_WINDOWS - //TODO - #endif -#endif - -bool mg_is_surface_backend_available(mg_surface_api api) -{ - bool result = false; - switch(api) - { - #if MG_COMPILE_METAL - case MG_METAL: - #endif - - #if MG_COMPILE_GL - case MG_GL: - #endif - - #if MG_COMPILE_GLES - case MG_GLES: - #endif - - #if MG_COMPILE_CANVAS - case MG_CANVAS: - #endif - result = true; - break; - - default: - break; - } - return(result); -} - -mg_surface mg_surface_nil() { return((mg_surface){.h = 0}); } -bool mg_surface_is_nil(mg_surface surface) { return(surface.h == 0); } - -mg_surface mg_surface_create_for_window(mp_window window, mg_surface_api api) -{ - if(__mgData.init) - { - mg_init(); - } - mg_surface surfaceHandle = mg_surface_nil(); - mg_surface_data* surface = 0; - - switch(api) - { - #if MG_COMPILE_GL - case MG_GL: - surface = gl_surface_create_for_window(window); - break; - #endif - - #if MG_COMPILE_GLES - case MG_GLES: - surface = mg_egl_surface_create_for_window(window); - break; - #endif - - #if MG_COMPILE_METAL - case MG_METAL: - surface = mg_mtl_surface_create_for_window(window); - break; - #endif - - #if MG_COMPILE_CANVAS - case MG_CANVAS: - - #if PLATFORM_MACOS - surface = mtl_canvas_surface_create_for_window(window); - #elif PLATFORM_WINDOWS - surface = gl_canvas_surface_create_for_window(window); - #endif - break; - #endif - - default: - break; - } - if(surface) - { - surfaceHandle = mg_surface_handle_alloc(surface); - } - return(surfaceHandle); -} - -mg_surface mg_surface_create_remote(u32 width, u32 height, mg_surface_api api) -{ - if(__mgData.init) - { - mg_init(); - } - mg_surface surfaceHandle = mg_surface_nil(); - mg_surface_data* surface = 0; - - switch(api) - { - #if MG_COMPILE_GLES - case MG_GLES: - surface = mg_egl_surface_create_remote(width, height); - break; - #endif - - default: - break; - } - if(surface) - { - surfaceHandle = mg_surface_handle_alloc(surface); - } - return(surfaceHandle); -} - -mg_surface mg_surface_create_host(mp_window window) -{ - if(__mgData.init) - { - mg_init(); - } - mg_surface handle = mg_surface_nil(); - mg_surface_data* surface = 0; - #if PLATFORM_MACOS - surface = mg_osx_surface_create_host(window); - #elif PLATFORM_WINDOWS - surface = mg_win32_surface_create_host(window); - #endif - - if(surface) - { - handle = mg_surface_handle_alloc(surface); - } - return(handle); -} - -void mg_surface_destroy(mg_surface handle) -{ - DEBUG_ASSERT(__mgData.init); - mg_surface_data* surface = mg_surface_data_from_handle(handle); - if(surface) - { - if(surface->backend && surface->backend->destroy) - { - surface->backend->destroy(surface->backend); - } - surface->destroy(surface); - mg_handle_recycle(handle.h); - } -} - -void mg_surface_prepare(mg_surface surface) -{ - DEBUG_ASSERT(__mgData.init); - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->prepare) - { - surfaceData->prepare(surfaceData); - } -} - -void mg_surface_present(mg_surface surface) -{ - DEBUG_ASSERT(__mgData.init); - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->present) - { - surfaceData->present(surfaceData); - } -} - -void mg_surface_swap_interval(mg_surface surface, int swap) -{ - DEBUG_ASSERT(__mgData.init); - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->swapInterval) - { - surfaceData->swapInterval(surfaceData, swap); - } -} - -vec2 mg_surface_contents_scaling(mg_surface surface) -{ - DEBUG_ASSERT(__mgData.init); - vec2 scaling = {1, 1}; - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->contentsScaling) - { - scaling = surfaceData->contentsScaling(surfaceData); - } - return(scaling); -} - - -void mg_surface_set_frame(mg_surface surface, mp_rect frame) -{ - DEBUG_ASSERT(__mgData.init); - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->setFrame) - { - surfaceData->setFrame(surfaceData, frame); - } -} - -mp_rect mg_surface_get_frame(mg_surface surface) -{ - DEBUG_ASSERT(__mgData.init); - mp_rect res = {0}; - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->getFrame) - { - res = surfaceData->getFrame(surfaceData); - } - return(res); -} - -void mg_surface_set_hidden(mg_surface surface, bool hidden) -{ - DEBUG_ASSERT(__mgData.init); - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->setHidden) - { - surfaceData->setHidden(surfaceData, hidden); - } -} - -bool mg_surface_get_hidden(mg_surface surface) -{ - DEBUG_ASSERT(__mgData.init); - bool res = false; - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->getHidden) - { - res = surfaceData->getHidden(surfaceData); - } - return(res); -} - -void* mg_surface_native_layer(mg_surface surface) -{ - void* res = 0; - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->nativeLayer) - { - res = surfaceData->nativeLayer(surfaceData); - } - return(res); -} - -mg_surface_id mg_surface_remote_id(mg_surface handle) -{ - mg_surface_id remoteId = 0; - mg_surface_data* surface = mg_surface_data_from_handle(handle); - if(surface && surface->remoteID) - { - remoteId = surface->remoteID(surface); - } - return(remoteId); -} - -void mg_surface_host_connect(mg_surface handle, mg_surface_id remoteID) -{ - mg_surface_data* surface = mg_surface_data_from_handle(handle); - if(surface && surface->hostConnect) - { - surface->hostConnect(surface, remoteID); - } -} - -void mg_surface_render_commands(mg_surface surface, - mg_color clearColor, - u32 primitiveCount, - mg_primitive* primitives, - u32 eltCount, - mg_path_elt* elements) -{ - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->backend) - { - surfaceData->backend->render(surfaceData->backend, - clearColor, - primitiveCount, - primitives, - eltCount, - elements); - } -} - -//------------------------------------------------------------------------------------------ -//NOTE(martin): images -//------------------------------------------------------------------------------------------ - -vec2 mg_image_size(mg_image image) -{ - vec2 res = {0}; - mg_image_data* imageData = mg_image_data_from_handle(image); - if(imageData) - { - res = imageData->size; - } - return(res); -} - -mg_image mg_image_create(mg_surface surface, u32 width, u32 height) -{ - mg_image image = mg_image_nil(); - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->backend) - { - DEBUG_ASSERT(surfaceData->api == MG_CANVAS); - - mg_image_data* imageData = surfaceData->backend->imageCreate(surfaceData->backend, (vec2){width, height}); - if(imageData) - { - imageData->surface = surface; - image = mg_image_handle_alloc(imageData); - } - } - return(image); -} - -void mg_image_destroy(mg_image image) -{ - mg_image_data* imageData = mg_image_data_from_handle(image); - if(imageData) - { - mg_surface_data* surface = mg_surface_data_from_handle(imageData->surface); - if(surface && surface->backend) - { - surface->backend->imageDestroy(surface->backend, imageData); - mg_handle_recycle(image.h); - } - } -} - -void mg_image_upload_region_rgba8(mg_image image, mp_rect region, u8* pixels) -{ - mg_image_data* imageData = mg_image_data_from_handle(image); - if(imageData) - { - mg_surface_data* surfaceData = mg_surface_data_from_handle(imageData->surface); - if(surfaceData) - { - DEBUG_ASSERT(surfaceData->backend); - surfaceData->backend->imageUploadRegion(surfaceData->backend, imageData, region, pixels); - } - } -} +/************************************************************//** +* +* @file: graphics_surface.c +* @author: Martin Fouilleul +* @date: 25/04/2023 +* +*****************************************************************/ + +#include"graphics_surface.h" + +//--------------------------------------------------------------- +// typed handles functions +//--------------------------------------------------------------- + +mg_surface mg_surface_handle_alloc(mg_surface_data* surface) +{ + mg_surface handle = {.h = mg_handle_alloc(MG_HANDLE_SURFACE, (void*)surface) }; + return(handle); +} + +mg_surface_data* mg_surface_data_from_handle(mg_surface handle) +{ + mg_surface_data* data = mg_data_from_handle(MG_HANDLE_SURFACE, handle.h); + return(data); +} + +mg_image mg_image_handle_alloc(mg_image_data* image) +{ + mg_image handle = {.h = mg_handle_alloc(MG_HANDLE_IMAGE, (void*)image) }; + return(handle); +} + +mg_image_data* mg_image_data_from_handle(mg_image handle) +{ + mg_image_data* data = mg_data_from_handle(MG_HANDLE_IMAGE, handle.h); + return(data); +} + +//--------------------------------------------------------------- +// surface API +//--------------------------------------------------------------- + +#if MG_COMPILE_GL + #if PLATFORM_WINDOWS + #include"wgl_surface.h" + #define gl_surface_create_for_window mg_wgl_surface_create_for_window + #endif +#endif + +#if MG_COMPILE_GLES + #include"egl_surface.h" +#endif + +#if MG_COMPILE_METAL + #include"mtl_surface.h" +#endif + +#if MG_COMPILE_CANVAS + #if PLATFORM_MACOS + mg_surface_data* mtl_canvas_surface_create_for_window(mp_window window); + #elif PLATFORM_WINDOWS + mg_surface_data* gl_canvas_surface_create_for_window(mp_window window); + #endif +#endif + +bool mg_is_surface_backend_available(mg_surface_api api) +{ + bool result = false; + switch(api) + { + #if MG_COMPILE_METAL + case MG_METAL: + #endif + + #if MG_COMPILE_GL + case MG_GL: + #endif + + #if MG_COMPILE_GLES + case MG_GLES: + #endif + + #if MG_COMPILE_CANVAS + case MG_CANVAS: + #endif + result = true; + break; + + default: + break; + } + return(result); +} + +mg_surface mg_surface_nil() { return((mg_surface){.h = 0}); } +bool mg_surface_is_nil(mg_surface surface) { return(surface.h == 0); } + +mg_surface mg_surface_create_for_window(mp_window window, mg_surface_api api) +{ + if(__mgData.init) + { + mg_init(); + } + mg_surface surfaceHandle = mg_surface_nil(); + mg_surface_data* surface = 0; + + switch(api) + { + #if MG_COMPILE_GL + case MG_GL: + surface = gl_surface_create_for_window(window); + break; + #endif + + #if MG_COMPILE_GLES + case MG_GLES: + surface = mg_egl_surface_create_for_window(window); + break; + #endif + + #if MG_COMPILE_METAL + case MG_METAL: + surface = mg_mtl_surface_create_for_window(window); + break; + #endif + + #if MG_COMPILE_CANVAS + case MG_CANVAS: + + #if PLATFORM_MACOS + surface = mtl_canvas_surface_create_for_window(window); + #elif PLATFORM_WINDOWS + surface = gl_canvas_surface_create_for_window(window); + #endif + break; + #endif + + default: + break; + } + if(surface) + { + surfaceHandle = mg_surface_handle_alloc(surface); + } + return(surfaceHandle); +} + +mg_surface mg_surface_create_remote(u32 width, u32 height, mg_surface_api api) +{ + if(__mgData.init) + { + mg_init(); + } + mg_surface surfaceHandle = mg_surface_nil(); + mg_surface_data* surface = 0; + + switch(api) + { + #if MG_COMPILE_GLES + case MG_GLES: + surface = mg_egl_surface_create_remote(width, height); + break; + #endif + + default: + break; + } + if(surface) + { + surfaceHandle = mg_surface_handle_alloc(surface); + } + return(surfaceHandle); +} + +mg_surface mg_surface_create_host(mp_window window) +{ + if(__mgData.init) + { + mg_init(); + } + mg_surface handle = mg_surface_nil(); + mg_surface_data* surface = 0; + #if PLATFORM_MACOS + surface = mg_osx_surface_create_host(window); + #elif PLATFORM_WINDOWS + surface = mg_win32_surface_create_host(window); + #endif + + if(surface) + { + handle = mg_surface_handle_alloc(surface); + } + return(handle); +} + +void mg_surface_destroy(mg_surface handle) +{ + DEBUG_ASSERT(__mgData.init); + mg_surface_data* surface = mg_surface_data_from_handle(handle); + if(surface) + { + if(surface->backend && surface->backend->destroy) + { + surface->backend->destroy(surface->backend); + } + surface->destroy(surface); + mg_handle_recycle(handle.h); + } +} + +void mg_surface_prepare(mg_surface surface) +{ + DEBUG_ASSERT(__mgData.init); + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->prepare) + { + surfaceData->prepare(surfaceData); + } +} + +void mg_surface_present(mg_surface surface) +{ + DEBUG_ASSERT(__mgData.init); + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->present) + { + surfaceData->present(surfaceData); + } +} + +void mg_surface_swap_interval(mg_surface surface, int swap) +{ + DEBUG_ASSERT(__mgData.init); + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->swapInterval) + { + surfaceData->swapInterval(surfaceData, swap); + } +} + +vec2 mg_surface_contents_scaling(mg_surface surface) +{ + DEBUG_ASSERT(__mgData.init); + vec2 scaling = {1, 1}; + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->contentsScaling) + { + scaling = surfaceData->contentsScaling(surfaceData); + } + return(scaling); +} + + +void mg_surface_set_frame(mg_surface surface, mp_rect frame) +{ + DEBUG_ASSERT(__mgData.init); + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->setFrame) + { + surfaceData->setFrame(surfaceData, frame); + } +} + +mp_rect mg_surface_get_frame(mg_surface surface) +{ + DEBUG_ASSERT(__mgData.init); + mp_rect res = {0}; + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->getFrame) + { + res = surfaceData->getFrame(surfaceData); + } + return(res); +} + +void mg_surface_set_hidden(mg_surface surface, bool hidden) +{ + DEBUG_ASSERT(__mgData.init); + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->setHidden) + { + surfaceData->setHidden(surfaceData, hidden); + } +} + +bool mg_surface_get_hidden(mg_surface surface) +{ + DEBUG_ASSERT(__mgData.init); + bool res = false; + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->getHidden) + { + res = surfaceData->getHidden(surfaceData); + } + return(res); +} + +void* mg_surface_native_layer(mg_surface surface) +{ + void* res = 0; + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->nativeLayer) + { + res = surfaceData->nativeLayer(surfaceData); + } + return(res); +} + +mg_surface_id mg_surface_remote_id(mg_surface handle) +{ + mg_surface_id remoteId = 0; + mg_surface_data* surface = mg_surface_data_from_handle(handle); + if(surface && surface->remoteID) + { + remoteId = surface->remoteID(surface); + } + return(remoteId); +} + +void mg_surface_host_connect(mg_surface handle, mg_surface_id remoteID) +{ + mg_surface_data* surface = mg_surface_data_from_handle(handle); + if(surface && surface->hostConnect) + { + surface->hostConnect(surface, remoteID); + } +} + +void mg_surface_render_commands(mg_surface surface, + mg_color clearColor, + u32 primitiveCount, + mg_primitive* primitives, + u32 eltCount, + mg_path_elt* elements) +{ + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->backend) + { + surfaceData->backend->render(surfaceData->backend, + clearColor, + primitiveCount, + primitives, + eltCount, + elements); + } +} + +//------------------------------------------------------------------------------------------ +//NOTE(martin): images +//------------------------------------------------------------------------------------------ + +vec2 mg_image_size(mg_image image) +{ + vec2 res = {0}; + mg_image_data* imageData = mg_image_data_from_handle(image); + if(imageData) + { + res = imageData->size; + } + return(res); +} + +mg_image mg_image_create(mg_surface surface, u32 width, u32 height) +{ + mg_image image = mg_image_nil(); + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->backend) + { + DEBUG_ASSERT(surfaceData->api == MG_CANVAS); + + mg_image_data* imageData = surfaceData->backend->imageCreate(surfaceData->backend, (vec2){width, height}); + if(imageData) + { + imageData->surface = surface; + image = mg_image_handle_alloc(imageData); + } + } + return(image); +} + +void mg_image_destroy(mg_image image) +{ + mg_image_data* imageData = mg_image_data_from_handle(image); + if(imageData) + { + mg_surface_data* surface = mg_surface_data_from_handle(imageData->surface); + if(surface && surface->backend) + { + surface->backend->imageDestroy(surface->backend, imageData); + mg_handle_recycle(image.h); + } + } +} + +void mg_image_upload_region_rgba8(mg_image image, mp_rect region, u8* pixels) +{ + mg_image_data* imageData = mg_image_data_from_handle(image); + if(imageData) + { + mg_surface_data* surfaceData = mg_surface_data_from_handle(imageData->surface); + if(surfaceData) + { + DEBUG_ASSERT(surfaceData->backend); + surfaceData->backend->imageUploadRegion(surfaceData->backend, imageData, region, pixels); + } + } +} diff --git a/src/graphics_surface.h b/src/graphics_surface.h index 22218cc..89e1e5c 100644 --- a/src/graphics_surface.h +++ b/src/graphics_surface.h @@ -1,158 +1,154 @@ -/************************************************************//** -* -* @file: graphics_surface.h -* @author: Martin Fouilleul -* @date: 26/04/2023 -* -*****************************************************************/ -#ifndef __GRAPHICS_SURFACE_H_ -#define __GRAPHICS_SURFACE_H_ - -#include"graphics_common.h" -#include"mp_app_internal.h" - -#ifdef __cplusplus -extern "C" { -#endif - -//--------------------------------------------------------------- -// surface interface -//--------------------------------------------------------------- -typedef struct mg_surface_data mg_surface_data; -typedef struct mg_canvas_backend mg_canvas_backend; - -typedef void (*mg_surface_destroy_proc)(mg_surface_data* surface); -typedef void (*mg_surface_prepare_proc)(mg_surface_data* surface); -typedef void (*mg_surface_present_proc)(mg_surface_data* surface); -typedef void (*mg_surface_swap_interval_proc)(mg_surface_data* surface, int swap); -typedef vec2 (*mg_surface_contents_scaling_proc)(mg_surface_data* surface); -typedef mp_rect (*mg_surface_get_frame_proc)(mg_surface_data* surface); -typedef void (*mg_surface_set_frame_proc)(mg_surface_data* surface, mp_rect frame); -typedef bool (*mg_surface_get_hidden_proc)(mg_surface_data* surface); -typedef void (*mg_surface_set_hidden_proc)(mg_surface_data* surface, bool hidden); -typedef void* (*mg_surface_native_layer_proc)(mg_surface_data* surface); -typedef mg_surface_id (*mg_surface_remote_id_proc)(mg_surface_data* surface); -typedef void (*mg_surface_host_connect_proc)(mg_surface_data* surface, mg_surface_id remoteId); - -typedef struct mg_surface_data -{ - mg_surface_api api; - mp_layer layer; - - mg_surface_destroy_proc destroy; - mg_surface_prepare_proc prepare; - mg_surface_present_proc present; - mg_surface_swap_interval_proc swapInterval; - mg_surface_contents_scaling_proc contentsScaling; - mg_surface_get_frame_proc getFrame; - mg_surface_set_frame_proc setFrame; - mg_surface_get_hidden_proc getHidden; - mg_surface_set_hidden_proc setHidden; - mg_surface_native_layer_proc nativeLayer; - mg_surface_remote_id_proc remoteID; - mg_surface_host_connect_proc hostConnect; - - mg_canvas_backend* backend; - -} mg_surface_data; - -mg_surface mg_surface_alloc_handle(mg_surface_data* surface); -mg_surface_data* mg_surface_data_from_handle(mg_surface handle); - -void mg_surface_init_for_window(mg_surface_data* surface, mp_window_data* window); -void mg_surface_init_remote(mg_surface_data* surface, u32 width, u32 height); -void mg_surface_init_host(mg_surface_data* surface, mp_window_data* window); -void mg_surface_cleanup(mg_surface_data* surface); -void* mg_surface_native_layer(mg_surface surface); - -//--------------------------------------------------------------- -// canvas backend interface -//--------------------------------------------------------------- -typedef struct mg_image_data -{ - list_elt listElt; - u32 generation; - mg_surface surface; - vec2 size; - -} mg_image_data; - -typedef struct mg_vertex_layout -{ - u32 maxVertexCount; - u32 maxIndexCount; - - char* posBuffer; - u32 posStride; - - char* cubicBuffer; - u32 cubicStride; - - char* uvTransformBuffer; - u32 uvTransformStride; - - char* colorBuffer; - u32 colorStride; - - char* texturedBuffer; - u32 texturedStride; - - char* shapeIndexBuffer; - u32 shapeIndexStride; - - char* clipBuffer; - u32 clipStride; - - char* indexBuffer; - u32 indexStride; - -} mg_vertex_layout; - - -typedef void (*mg_canvas_backend_destroy_proc)(mg_canvas_backend* backend); -typedef void (*mg_canvas_backend_begin_proc)(mg_canvas_backend* backend, mg_color clearColor); -typedef void (*mg_canvas_backend_end_proc)(mg_canvas_backend* backend); -typedef void (*mg_canvas_backend_draw_batch_proc)(mg_canvas_backend* backend, - mg_image_data* imageData, - u32 vertexCount, - u32 shapeCount, - u32 indexCount); - - -typedef mg_image_data* (*mg_canvas_backend_image_create_proc)(mg_canvas_backend* backend, vec2 size); -typedef void (*mg_canvas_backend_image_destroy_proc)(mg_canvas_backend* backend, mg_image_data* image); -typedef void (*mg_canvas_backend_image_upload_region_proc)(mg_canvas_backend* backend, - mg_image_data* image, - mp_rect region, - u8* pixels); - -typedef void (*mg_canvas_backend_render_proc)(mg_canvas_backend* backend, - mg_color clearColor, - u32 primitiveCount, - mg_primitive* primitives, - u32 eltCount, - mg_path_elt* pathElements); - -typedef struct mg_canvas_backend -{ - mg_vertex_layout vertexLayout; - - mg_canvas_backend_destroy_proc destroy; - mg_canvas_backend_begin_proc begin; - mg_canvas_backend_end_proc end; - mg_canvas_backend_draw_batch_proc drawBatch; - - mg_canvas_backend_image_create_proc imageCreate; - mg_canvas_backend_image_destroy_proc imageDestroy; - mg_canvas_backend_image_upload_region_proc imageUploadRegion; - - - mg_canvas_backend_render_proc render; - -} mg_canvas_backend; - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif //__GRAPHICS_SURFACE_H_ +/************************************************************//** +* +* @file: graphics_surface.h +* @author: Martin Fouilleul +* @date: 26/04/2023 +* +*****************************************************************/ +#ifndef __GRAPHICS_SURFACE_H_ +#define __GRAPHICS_SURFACE_H_ + +#include"graphics_common.h" +#include"mp_app_internal.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------- +// surface interface +//--------------------------------------------------------------- +typedef struct mg_surface_data mg_surface_data; +typedef struct mg_canvas_backend mg_canvas_backend; + +typedef void (*mg_surface_destroy_proc)(mg_surface_data* surface); +typedef void (*mg_surface_prepare_proc)(mg_surface_data* surface); +typedef void (*mg_surface_present_proc)(mg_surface_data* surface); +typedef void (*mg_surface_swap_interval_proc)(mg_surface_data* surface, int swap); +typedef vec2 (*mg_surface_contents_scaling_proc)(mg_surface_data* surface); +typedef mp_rect (*mg_surface_get_frame_proc)(mg_surface_data* surface); +typedef void (*mg_surface_set_frame_proc)(mg_surface_data* surface, mp_rect frame); +typedef bool (*mg_surface_get_hidden_proc)(mg_surface_data* surface); +typedef void (*mg_surface_set_hidden_proc)(mg_surface_data* surface, bool hidden); +typedef void* (*mg_surface_native_layer_proc)(mg_surface_data* surface); +typedef mg_surface_id (*mg_surface_remote_id_proc)(mg_surface_data* surface); +typedef void (*mg_surface_host_connect_proc)(mg_surface_data* surface, mg_surface_id remoteId); + +typedef struct mg_surface_data +{ + mg_surface_api api; + mp_layer layer; + + mg_surface_destroy_proc destroy; + mg_surface_prepare_proc prepare; + mg_surface_present_proc present; + mg_surface_swap_interval_proc swapInterval; + mg_surface_contents_scaling_proc contentsScaling; + mg_surface_get_frame_proc getFrame; + mg_surface_set_frame_proc setFrame; + mg_surface_get_hidden_proc getHidden; + mg_surface_set_hidden_proc setHidden; + mg_surface_native_layer_proc nativeLayer; + mg_surface_remote_id_proc remoteID; + mg_surface_host_connect_proc hostConnect; + + mg_canvas_backend* backend; + +} mg_surface_data; + +mg_surface mg_surface_alloc_handle(mg_surface_data* surface); +mg_surface_data* mg_surface_data_from_handle(mg_surface handle); + +void mg_surface_init_for_window(mg_surface_data* surface, mp_window_data* window); +void mg_surface_init_remote(mg_surface_data* surface, u32 width, u32 height); +void mg_surface_init_host(mg_surface_data* surface, mp_window_data* window); +void mg_surface_cleanup(mg_surface_data* surface); +void* mg_surface_native_layer(mg_surface surface); + +//--------------------------------------------------------------- +// canvas backend interface +//--------------------------------------------------------------- +typedef struct mg_image_data +{ + list_elt listElt; + u32 generation; + mg_surface surface; + vec2 size; + +} mg_image_data; + +typedef struct mg_vertex_layout +{ + u32 maxVertexCount; + u32 maxIndexCount; + + char* posBuffer; + u32 posStride; + + char* cubicBuffer; + u32 cubicStride; + + char* shapeIndexBuffer; + u32 shapeIndexStride; + + char* colorBuffer; + u32 colorStride; + + char* clipBuffer; + u32 clipStride; + + char* uvTransformBuffer; + u32 uvTransformStride; + + char* indexBuffer; + u32 indexStride; + +} mg_vertex_layout; + +typedef void (*mg_canvas_backend_destroy_proc)(mg_canvas_backend* backend); +typedef void (*mg_canvas_backend_begin_proc)(mg_canvas_backend* backend, mg_color clearColor); +typedef void (*mg_canvas_backend_end_proc)(mg_canvas_backend* backend); +typedef void (*mg_canvas_backend_draw_batch_proc)(mg_canvas_backend* backend, + mg_image_data* imageData, + u32 vertexCount, + u32 shapeCount, + u32 indexCount); + + +typedef mg_image_data* (*mg_canvas_backend_image_create_proc)(mg_canvas_backend* backend, vec2 size); +typedef void (*mg_canvas_backend_image_destroy_proc)(mg_canvas_backend* backend, mg_image_data* image); +typedef void (*mg_canvas_backend_image_upload_region_proc)(mg_canvas_backend* backend, + mg_image_data* image, + mp_rect region, + u8* pixels); + +typedef void (*mg_canvas_backend_render_proc)(mg_canvas_backend* backend, + mg_color clearColor, + u32 primitiveCount, + mg_primitive* primitives, + u32 eltCount, + mg_path_elt* pathElements); + +typedef struct mg_canvas_backend +{ +// mg_vertex_layout vertexLayout; + + mg_canvas_backend_destroy_proc destroy; + mg_canvas_backend_begin_proc begin; + mg_canvas_backend_end_proc end; + mg_canvas_backend_draw_batch_proc drawBatch; + + mg_canvas_backend_image_create_proc imageCreate; + mg_canvas_backend_image_destroy_proc imageDestroy; + mg_canvas_backend_image_upload_region_proc imageUploadRegion; + + + mg_canvas_backend_render_proc render; + +} mg_canvas_backend; + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif //__GRAPHICS_SURFACE_H_ diff --git a/src/mtl_renderer.m b/src/mtl_renderer.m index f17de0b..3c45b73 100644 --- a/src/mtl_renderer.m +++ b/src/mtl_renderer.m @@ -1,1475 +1,1475 @@ -/************************************************************//** -* -* @file: mtl_canvas.m -* @author: Martin Fouilleul -* @date: 12/07/2020 -* @revision: 24/01/2023 -* -*****************************************************************/ -#import -#import -#include - -#include"graphics_surface.h" -#include"macro_helpers.h" -#include"osx_app.h" - -#include"mtl_renderer.h" - -const int MG_MTL_INPUT_BUFFERS_COUNT = 3, - MG_MTL_TILE_SIZE = 16, - MG_MTL_MSAA_COUNT = 8; - -typedef struct mg_mtl_canvas_backend -{ - mg_canvas_backend interface; - mg_mtl_surface* surface; - - id pathPipeline; - id segmentPipeline; - id backpropPipeline; - id mergePipeline; - id rasterPipeline; - id blitPipeline; - - id outTexture; - - int pathBufferOffset; - int elementBufferOffset; - int bufferIndex; - dispatch_semaphore_t bufferSemaphore; - - id pathBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; - id elementBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; - id logBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; - id logOffsetBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; - - id segmentCountBuffer; - id segmentBuffer; - id pathQueueBuffer; - id tileQueueBuffer; - id tileQueueCountBuffer; - id tileOpBuffer; - id tileOpCountBuffer; - id screenTilesBuffer; - - int msaaCount; - vec2 frameSize; - -} mg_mtl_canvas_backend; - -typedef struct mg_mtl_image_data -{ - mg_image_data interface; - id texture; -} mg_mtl_image_data; - -void mg_mtl_print_log(int bufferIndex, id logBuffer, id logOffsetBuffer) -{ - char* log = [logBuffer contents]; - int size = *(int*)[logOffsetBuffer contents]; - - if(size) - { - log_info("Log from buffer %i:\n", bufferIndex); - - int index = 0; - while(index < size) - { - int len = strlen(log+index); - printf("%s", log+index); - index += (len+1); - } - } -} - - -typedef struct mg_mtl_encoding_context -{ - int mtlEltCount; - mg_mtl_path* pathBufferData; - mg_mtl_path_elt* elementBufferData; - int pathIndex; - int localEltIndex; - mg_primitive* primitive; - vec4 pathScreenExtents; - vec4 pathUserExtents; - -} mg_mtl_encoding_context; - -static void mg_update_path_extents(vec4* extents, vec2 p) -{ - extents->x = minimum(extents->x, p.x); - extents->y = minimum(extents->y, p.y); - extents->z = maximum(extents->z, p.x); - extents->w = maximum(extents->w, p.y); -} - -void mg_mtl_canvas_encode_element(mg_mtl_encoding_context* context, mg_path_elt_type kind, vec2* p) -{ - mg_mtl_path_elt* mtlElt = &context->elementBufferData[context->mtlEltCount]; - context->mtlEltCount++; - - mtlElt->pathIndex = context->pathIndex; - int count = 0; - switch(kind) - { - case MG_PATH_LINE: - mtlElt->kind = MG_MTL_LINE; - count = 2; - break; - - case MG_PATH_QUADRATIC: - mtlElt->kind = MG_MTL_QUADRATIC; - count = 3; - break; - - case MG_PATH_CUBIC: - mtlElt->kind = MG_MTL_CUBIC; - count = 4; - break; - - default: - break; - } - - mtlElt->localEltIndex = context->localEltIndex; - - for(int i=0; ipathUserExtents, p[i]); - - vec2 screenP = mg_mat2x3_mul(context->primitive->attributes.transform, p[i]); - mtlElt->p[i] = (vector_float2){screenP.x, screenP.y}; - - mg_update_path_extents(&context->pathScreenExtents, screenP); - } -} - - -bool mg_intersect_hull_legs(vec2 p0, vec2 p1, vec2 p2, vec2 p3, vec2* intersection) -{ - /*NOTE: check intersection of lines (p0-p1) and (p2-p3) - - P = p0 + u(p1-p0) - P = p2 + w(p3-p2) - */ - bool found = false; - - f32 den = (p0.x - p1.x)*(p2.y - p3.y) - (p0.y - p1.y)*(p2.x - p3.x); - if(fabs(den) > 0.0001) - { - f32 u = ((p0.x - p2.x)*(p2.y - p3.y) - (p0.y - p2.y)*(p2.x - p3.x))/den; - f32 w = ((p0.x - p2.x)*(p0.y - p1.y) - (p0.y - p2.y)*(p0.x - p1.x))/den; - - intersection->x = p0.x + u*(p1.x - p0.x); - intersection->y = p0.y + u*(p1.y - p0.y); - found = true; - } - return(found); -} - -bool mg_offset_hull(int count, vec2* p, vec2* result, f32 offset) -{ - //NOTE: we should have no more than two coincident points here. This means the leg between - // those two points can't be offset, but we can set a double point at the start of first leg, - // end of first leg, or we can join the first and last leg to create a missing middle one - - vec2 legs[3][2] = {0}; - bool valid[3] = {0}; - - for(int i=0; i= 1e-6) - { - n = vec2_mul(offset/norm, n); - legs[i][0] = vec2_add(p[i], n); - legs[i][1] = vec2_add(p[i+1], n); - valid[i] = true; - } - } - - //NOTE: now we find intersections - - // first point is either the start of the first or second leg - if(valid[0]) - { - result[0] = legs[0][0]; - } - else - { - ASSERT(valid[1]); - result[0] = legs[1][0]; - } - - for(int i=1; iprimitive->attributes.width; - - vec2 v = {p[1].x-p[0].x, p[1].y-p[0].y}; - vec2 n = {v.y, -v.x}; - f32 norm = sqrt(n.x*n.x + n.y*n.y); - vec2 offset = vec2_mul(0.5*width/norm, n); - - vec2 left[2] = {vec2_add(p[0], offset), vec2_add(p[1], offset)}; - vec2 right[2] = {vec2_add(p[1], vec2_mul(-1, offset)), vec2_add(p[0], vec2_mul(-1, offset))}; - vec2 joint0[2] = {vec2_add(p[0], vec2_mul(-1, offset)), vec2_add(p[0], offset)}; - vec2 joint1[2] = {vec2_add(p[1], offset), vec2_add(p[1], vec2_mul(-1, offset))}; - - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, right); - - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, left); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint0); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint1); -} - -void mg_mtl_render_stroke_quadratic(mg_mtl_encoding_context* context, vec2* p) -{ - f32 width = context->primitive->attributes.width; - f32 tolerance = minimum(context->primitive->attributes.tolerance, 0.5 * width); - - //NOTE: check for degenerate line case - const f32 equalEps = 1e-3; - if(vec2_close(p[0], p[1], equalEps)) - { - mg_mtl_render_stroke_line(context, p+1); - return; - } - else if(vec2_close(p[1], p[2], equalEps)) - { - mg_mtl_render_stroke_line(context, p); - return; - } - - vec2 leftHull[3]; - vec2 rightHull[3]; - - if( !mg_offset_hull(3, p, leftHull, width/2) - || !mg_offset_hull(3, p, rightHull, -width/2)) - { - //TODO split and recurse - //NOTE: offsetting the hull failed, split the curve - vec2 splitLeft[3]; - vec2 splitRight[3]; - mg_quadratic_split(p, 0.5, splitLeft, splitRight); - mg_mtl_render_stroke_quadratic(context, splitLeft); - mg_mtl_render_stroke_quadratic(context, splitRight); - } - else - { - const int CHECK_SAMPLE_COUNT = 5; - f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; - - f32 d2LowBound = Square(0.5 * width - tolerance); - f32 d2HighBound = Square(0.5 * width + tolerance); - - f32 maxOvershoot = 0; - f32 maxOvershootParameter = 0; - - for(int i=0; i maxOvershoot) - { - maxOvershoot = overshoot; - maxOvershootParameter = t; - } - } - - if(maxOvershoot > 0) - { - vec2 splitLeft[3]; - vec2 splitRight[3]; - mg_quadratic_split(p, maxOvershootParameter, splitLeft, splitRight); - mg_mtl_render_stroke_quadratic(context, splitLeft); - mg_mtl_render_stroke_quadratic(context, splitRight); - } - else - { - vec2 tmp = leftHull[0]; - leftHull[0] = leftHull[2]; - leftHull[2] = tmp; - - mg_mtl_canvas_encode_element(context, MG_PATH_QUADRATIC, rightHull); - mg_mtl_canvas_encode_element(context, MG_PATH_QUADRATIC, leftHull); - - vec2 joint0[2] = {rightHull[2], leftHull[0]}; - vec2 joint1[2] = {leftHull[2], rightHull[0]}; - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint0); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint1); - } - } -} - -void mg_mtl_render_stroke_cubic(mg_mtl_encoding_context* context, vec2* p) -{ - f32 width = context->primitive->attributes.width; - f32 tolerance = minimum(context->primitive->attributes.tolerance, 0.5 * width); - - //NOTE: check degenerate line cases - f32 equalEps = 1e-3; - - if( (vec2_close(p[0], p[1], equalEps) && vec2_close(p[2], p[3], equalEps)) - ||(vec2_close(p[0], p[1], equalEps) && vec2_close(p[1], p[2], equalEps)) - ||(vec2_close(p[1], p[2], equalEps) && vec2_close(p[2], p[3], equalEps))) - { - vec2 line[2] = {p[0], p[3]}; - mg_mtl_render_stroke_line(context, line); - return; - } - else if(vec2_close(p[0], p[1], equalEps) && vec2_close(p[1], p[3], equalEps)) - { - vec2 line[2] = {p[0], vec2_add(vec2_mul(5./9, p[0]), vec2_mul(4./9, p[2]))}; - mg_mtl_render_stroke_line(context, line); - return; - } - else if(vec2_close(p[0], p[2], equalEps) && vec2_close(p[2], p[3], equalEps)) - { - vec2 line[2] = {p[0], vec2_add(vec2_mul(5./9, p[0]), vec2_mul(4./9, p[1]))}; - mg_mtl_render_stroke_line(context, line); - return; - } - - vec2 leftHull[4]; - vec2 rightHull[4]; - - if( !mg_offset_hull(4, p, leftHull, width/2) - || !mg_offset_hull(4, p, rightHull, -width/2)) - { - //TODO split and recurse - //NOTE: offsetting the hull failed, split the curve - vec2 splitLeft[4]; - vec2 splitRight[4]; - mg_cubic_split(p, 0.5, splitLeft, splitRight); - mg_mtl_render_stroke_cubic(context, splitLeft); - mg_mtl_render_stroke_cubic(context, splitRight); - } - else - { - const int CHECK_SAMPLE_COUNT = 5; - f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; - - f32 d2LowBound = Square(0.5 * width - tolerance); - f32 d2HighBound = Square(0.5 * width + tolerance); - - f32 maxOvershoot = 0; - f32 maxOvershootParameter = 0; - - for(int i=0; i maxOvershoot) - { - maxOvershoot = overshoot; - maxOvershootParameter = t; - } - } - - if(maxOvershoot > 0) - { - vec2 splitLeft[4]; - vec2 splitRight[4]; - mg_cubic_split(p, maxOvershootParameter, splitLeft, splitRight); - mg_mtl_render_stroke_cubic(context, splitLeft); - mg_mtl_render_stroke_cubic(context, splitRight); - } - else - { - vec2 tmp = leftHull[0]; - leftHull[0] = leftHull[3]; - leftHull[3] = tmp; - tmp = leftHull[1]; - leftHull[1] = leftHull[2]; - leftHull[2] = tmp; - - mg_mtl_canvas_encode_element(context, MG_PATH_CUBIC, rightHull); - mg_mtl_canvas_encode_element(context, MG_PATH_CUBIC, leftHull); - - vec2 joint0[2] = {rightHull[3], leftHull[0]}; - vec2 joint1[2] = {leftHull[3], rightHull[0]}; - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint0); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint1); - } - } -} - -void mg_mtl_render_stroke_element(mg_mtl_encoding_context* context, - mg_path_elt* element, - vec2 currentPoint, - vec2* startTangent, - vec2* endTangent, - vec2* endPoint) -{ - vec2 controlPoints[4] = {currentPoint, element->p[0], element->p[1], element->p[2]}; - int endPointIndex = 0; - - switch(element->type) - { - case MG_PATH_LINE: - mg_mtl_render_stroke_line(context, controlPoints); - endPointIndex = 1; - break; - - case MG_PATH_QUADRATIC: - mg_mtl_render_stroke_quadratic(context, controlPoints); - endPointIndex = 2; - break; - - case MG_PATH_CUBIC: - mg_mtl_render_stroke_cubic(context, controlPoints); - endPointIndex = 3; - break; - - case MG_PATH_MOVE: - ASSERT(0, "should be unreachable"); - break; - } - - //NOTE: ensure tangents are properly computed even in presence of coincident points - //TODO: see if we can do this in a less hacky way - - for(int i=1; i<4; i++) - { - if( controlPoints[i].x != controlPoints[0].x - || controlPoints[i].y != controlPoints[0].y) - { - *startTangent = (vec2){.x = controlPoints[i].x - controlPoints[0].x, - .y = controlPoints[i].y - controlPoints[0].y}; - break; - } - } - *endPoint = controlPoints[endPointIndex]; - - for(int i=endPointIndex-1; i>=0; i++) - { - if( controlPoints[i].x != endPoint->x - || controlPoints[i].y != endPoint->y) - { - *endTangent = (vec2){.x = endPoint->x - controlPoints[i].x, - .y = endPoint->y - controlPoints[i].y}; - break; - } - } - DEBUG_ASSERT(startTangent->x != 0 || startTangent->y != 0); -} - -void mg_mtl_stroke_cap(mg_mtl_encoding_context* context, - vec2 p0, - vec2 direction) -{ - mg_attributes* attributes = &context->primitive->attributes; - - //NOTE(martin): compute the tangent and normal vectors (multiplied by half width) at the cap point - f32 dn = sqrt(Square(direction.x) + Square(direction.y)); - f32 alpha = 0.5 * attributes->width/dn; - - vec2 n0 = {-alpha*direction.y, - alpha*direction.x}; - - vec2 m0 = {alpha*direction.x, - alpha*direction.y}; - - vec2 points[] = {{p0.x + n0.x, p0.y + n0.y}, - {p0.x + n0.x + m0.x, p0.y + n0.y + m0.y}, - {p0.x - n0.x + m0.x, p0.y - n0.y + m0.y}, - {p0.x - n0.x, p0.y - n0.y}, - {p0.x + n0.x, p0.y + n0.y}}; - - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+1); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+2); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+3); -} - -void mg_mtl_stroke_joint(mg_mtl_encoding_context* context, - vec2 p0, - vec2 t0, - vec2 t1) -{ - mg_attributes* attributes = &context->primitive->attributes; - - //NOTE(martin): compute the normals at the joint point - f32 norm_t0 = sqrt(Square(t0.x) + Square(t0.y)); - f32 norm_t1 = sqrt(Square(t1.x) + Square(t1.y)); - - vec2 n0 = {-t0.y, t0.x}; - n0.x /= norm_t0; - n0.y /= norm_t0; - - vec2 n1 = {-t1.y, t1.x}; - n1.x /= norm_t1; - n1.y /= norm_t1; - - //NOTE(martin): the sign of the cross product determines if the normals are facing outwards or inwards the angle. - // we flip them to face outwards if needed - f32 crossZ = n0.x*n1.y - n0.y*n1.x; - if(crossZ > 0) - { - n0.x *= -1; - n0.y *= -1; - n1.x *= -1; - n1.y *= -1; - } - - //NOTE(martin): use the same code as hull offset to find mitter point... - /*NOTE(martin): let vector u = (n0+n1) and vector v = pIntersect - p1 - then v = u * (2*offset / norm(u)^2) - (this can be derived from writing the pythagoras theorems in the triangles of the joint) - */ - f32 halfW = 0.5 * attributes->width; - vec2 u = {n0.x + n1.x, n0.y + n1.y}; - f32 uNormSquare = u.x*u.x + u.y*u.y; - f32 alpha = attributes->width / uNormSquare; - vec2 v = {u.x * alpha, u.y * alpha}; - - f32 excursionSquare = uNormSquare * Square(alpha - attributes->width/4); - - if( attributes->joint == MG_JOINT_MITER - && excursionSquare <= Square(attributes->maxJointExcursion)) - { - //NOTE(martin): add a mitter joint - vec2 points[] = {p0, - {p0.x + n0.x*halfW, p0.y + n0.y*halfW}, - {p0.x + v.x, p0.y + v.y}, - {p0.x + n1.x*halfW, p0.y + n1.y*halfW}, - p0}; - - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+1); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+2); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+3); - } - else - { - //NOTE(martin): add a bevel joint - vec2 points[] = {p0, - {p0.x + n0.x*halfW, p0.y + n0.y*halfW}, - {p0.x + n1.x*halfW, p0.y + n1.y*halfW}, - p0}; - - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+1); - mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+2); - } -} - -u32 mg_mtl_render_stroke_subpath(mg_mtl_encoding_context* context, - mg_path_elt* elements, - mg_path_descriptor* path, - u32 startIndex, - vec2 startPoint) -{ - u32 eltCount = path->count; - DEBUG_ASSERT(startIndex < eltCount); - - vec2 currentPoint = startPoint; - vec2 endPoint = {0, 0}; - vec2 previousEndTangent = {0, 0}; - vec2 firstTangent = {0, 0}; - vec2 startTangent = {0, 0}; - vec2 endTangent = {0, 0}; - - //NOTE(martin): render first element and compute first tangent - mg_mtl_render_stroke_element(context, elements + startIndex, currentPoint, &startTangent, &endTangent, &endPoint); - - firstTangent = startTangent; - previousEndTangent = endTangent; - currentPoint = endPoint; - - //NOTE(martin): render subsequent elements along with their joints - - mg_attributes* attributes = &context->primitive->attributes; - - u32 eltIndex = startIndex + 1; - for(; - eltIndexjoint != MG_JOINT_NONE) - { - mg_mtl_stroke_joint(context, currentPoint, previousEndTangent, startTangent); - } - previousEndTangent = endTangent; - currentPoint = endPoint; - } - u32 subPathEltCount = eltIndex - startIndex; - - //NOTE(martin): draw end cap / joint. We ensure there's at least two segments to draw a closing joint - if( subPathEltCount > 1 - && startPoint.x == endPoint.x - && startPoint.y == endPoint.y) - { - if(attributes->joint != MG_JOINT_NONE) - { - //NOTE(martin): add a closing joint if the path is closed - mg_mtl_stroke_joint(context, endPoint, endTangent, firstTangent); - } - } - else if(attributes->cap == MG_CAP_SQUARE) - { - //NOTE(martin): add start and end cap - mg_mtl_stroke_cap(context, startPoint, (vec2){-startTangent.x, -startTangent.y}); - mg_mtl_stroke_cap(context, endPoint, endTangent); - } - return(eltIndex); -} - -void mg_mtl_render_stroke(mg_mtl_encoding_context* context, - mg_path_elt* elements, - mg_path_descriptor* path) -{ - u32 eltCount = path->count; - DEBUG_ASSERT(eltCount); - - vec2 startPoint = path->startPoint; - u32 startIndex = 0; - - while(startIndex < eltCount) - { - //NOTE(martin): eliminate leading moves - while(startIndex < eltCount && elements[startIndex].type == MG_PATH_MOVE) - { - startPoint = elements[startIndex].p[0]; - startIndex++; - } - if(startIndex < eltCount) - { - startIndex = mg_mtl_render_stroke_subpath(context, elements, path, startIndex, startPoint); - } - } -} - - -void mg_mtl_render_batch(mg_mtl_canvas_backend* backend, - mg_mtl_surface* surface, - int pathCount, - int eltCount, - mg_image_data* image, - int tileSize, - int nTilesX, - int nTilesY, - vec2 viewportSize, - f32 scale) -{ - //NOTE: encode GPU commands - @autoreleasepool - { - //NOTE: clear counters - id blitEncoder = [surface->commandBuffer blitCommandEncoder]; - blitEncoder.label = @"clear counters"; - [blitEncoder fillBuffer: backend->segmentCountBuffer range: NSMakeRange(0, sizeof(int)) value: 0]; - [blitEncoder fillBuffer: backend->tileQueueCountBuffer range: NSMakeRange(0, sizeof(int)) value: 0]; - [blitEncoder fillBuffer: backend->tileOpCountBuffer range: NSMakeRange(0, sizeof(int)) value: 0]; - [blitEncoder endEncoding]; - - //NOTE: path setup pass - id pathEncoder = [surface->commandBuffer computeCommandEncoder]; - pathEncoder.label = @"path pass"; - [pathEncoder setComputePipelineState: backend->pathPipeline]; - - [pathEncoder setBytes:&pathCount length:sizeof(int) atIndex:0]; - [pathEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset:backend->pathBufferOffset atIndex:1]; - [pathEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:2]; - [pathEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:3]; - [pathEncoder setBuffer:backend->tileQueueCountBuffer offset:0 atIndex:4]; - [pathEncoder setBytes:&tileSize length:sizeof(int) atIndex:5]; - [pathEncoder setBytes:&scale length:sizeof(int) atIndex:6]; - - MTLSize pathGridSize = MTLSizeMake(pathCount, 1, 1); - MTLSize pathGroupSize = MTLSizeMake([backend->pathPipeline maxTotalThreadsPerThreadgroup], 1, 1); - - [pathEncoder dispatchThreads: pathGridSize threadsPerThreadgroup: pathGroupSize]; - [pathEncoder endEncoding]; - - //NOTE: segment setup pass - id segmentEncoder = [surface->commandBuffer computeCommandEncoder]; - segmentEncoder.label = @"segment pass"; - [segmentEncoder setComputePipelineState: backend->segmentPipeline]; - - [segmentEncoder setBytes:&eltCount length:sizeof(int) atIndex:0]; - [segmentEncoder setBuffer:backend->elementBuffer[backend->bufferIndex] offset:backend->elementBufferOffset atIndex:1]; - [segmentEncoder setBuffer:backend->segmentCountBuffer offset:0 atIndex:2]; - [segmentEncoder setBuffer:backend->segmentBuffer offset:0 atIndex:3]; - [segmentEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:4]; - [segmentEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:5]; - [segmentEncoder setBuffer:backend->tileOpBuffer offset:0 atIndex:6]; - [segmentEncoder setBuffer:backend->tileOpCountBuffer offset:0 atIndex:7]; - [segmentEncoder setBytes:&tileSize length:sizeof(int) atIndex:8]; - [segmentEncoder setBytes:&scale length:sizeof(int) atIndex:9]; - [segmentEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:10]; - [segmentEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:11]; - - MTLSize segmentGridSize = MTLSizeMake(eltCount, 1, 1); - MTLSize segmentGroupSize = MTLSizeMake([backend->segmentPipeline maxTotalThreadsPerThreadgroup], 1, 1); - - [segmentEncoder dispatchThreads: segmentGridSize threadsPerThreadgroup: segmentGroupSize]; - [segmentEncoder endEncoding]; - - //NOTE: backprop pass - id backpropEncoder = [surface->commandBuffer computeCommandEncoder]; - backpropEncoder.label = @"backprop pass"; - [backpropEncoder setComputePipelineState: backend->backpropPipeline]; - - [backpropEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:0]; - [backpropEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:1]; - [backpropEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:2]; - [backpropEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:3]; - - MTLSize backpropGroupSize = MTLSizeMake([backend->backpropPipeline maxTotalThreadsPerThreadgroup], 1, 1); - MTLSize backpropGridSize = MTLSizeMake(pathCount*backpropGroupSize.width, 1, 1); - - [backpropEncoder dispatchThreads: backpropGridSize threadsPerThreadgroup: backpropGroupSize]; - [backpropEncoder endEncoding]; - - //NOTE: merge pass - id mergeEncoder = [surface->commandBuffer computeCommandEncoder]; - mergeEncoder.label = @"merge pass"; - [mergeEncoder setComputePipelineState: backend->mergePipeline]; - - [mergeEncoder setBytes:&pathCount length:sizeof(int) atIndex:0]; - [mergeEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset:backend->pathBufferOffset atIndex:1]; - [mergeEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:2]; - [mergeEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:3]; - [mergeEncoder setBuffer:backend->tileOpBuffer offset:0 atIndex:4]; - [mergeEncoder setBuffer:backend->tileOpCountBuffer offset:0 atIndex:5]; - [mergeEncoder setBuffer:backend->screenTilesBuffer offset:0 atIndex:6]; - [mergeEncoder setBytes:&tileSize length:sizeof(int) atIndex:7]; - [mergeEncoder setBytes:&scale length:sizeof(float) atIndex:8]; - [mergeEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:9]; - [mergeEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:10]; - - MTLSize mergeGridSize = MTLSizeMake(nTilesX, nTilesY, 1); - MTLSize mergeGroupSize = MTLSizeMake(16, 16, 1); - - [mergeEncoder dispatchThreads: mergeGridSize threadsPerThreadgroup: mergeGroupSize]; - [mergeEncoder endEncoding]; - - //NOTE: raster pass - id rasterEncoder = [surface->commandBuffer computeCommandEncoder]; - rasterEncoder.label = @"raster pass"; - [rasterEncoder setComputePipelineState: backend->rasterPipeline]; - - [rasterEncoder setBuffer:backend->screenTilesBuffer offset:0 atIndex:0]; - [rasterEncoder setBuffer:backend->tileOpBuffer offset:0 atIndex:1]; - [rasterEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset:backend->pathBufferOffset atIndex:2]; - [rasterEncoder setBuffer:backend->segmentBuffer offset:0 atIndex:3]; - [rasterEncoder setBytes:&tileSize length:sizeof(int) atIndex:4]; - [rasterEncoder setBytes:&scale length:sizeof(float) atIndex:5]; - [rasterEncoder setBytes:&backend->msaaCount length:sizeof(int) atIndex:6]; - [rasterEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:7]; - [rasterEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:8]; - - [rasterEncoder setTexture:backend->outTexture atIndex:0]; - - int useTexture = 0; - if(image) - { - mg_mtl_image_data* mtlImage = (mg_mtl_image_data*)image; - [rasterEncoder setTexture: mtlImage->texture atIndex: 1]; - useTexture = 1; - } - [rasterEncoder setBytes: &useTexture length:sizeof(int) atIndex: 9]; - - MTLSize rasterGridSize = MTLSizeMake(viewportSize.x, viewportSize.y, 1); - MTLSize rasterGroupSize = MTLSizeMake(16, 16, 1); - [rasterEncoder dispatchThreads: rasterGridSize threadsPerThreadgroup: rasterGroupSize]; - - [rasterEncoder endEncoding]; - - //NOTE: blit pass - MTLViewport viewport = {0, 0, viewportSize.x, viewportSize.y, 0, 1}; - - MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; - renderPassDescriptor.colorAttachments[0].texture = surface->drawable.texture; - renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionLoad; - renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; - - id renderEncoder = [surface->commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; - renderEncoder.label = @"blit pass"; - [renderEncoder setViewport: viewport]; - [renderEncoder setRenderPipelineState: backend->blitPipeline]; - [renderEncoder setFragmentTexture: backend->outTexture atIndex: 0]; - [renderEncoder drawPrimitives: MTLPrimitiveTypeTriangle - vertexStart: 0 - vertexCount: 3 ]; - [renderEncoder endEncoding]; - } -} - -void mg_mtl_canvas_resize(mg_mtl_canvas_backend* backend, vec2 size) -{ - @autoreleasepool - { - if(backend->screenTilesBuffer) - { - [backend->screenTilesBuffer release]; - backend->screenTilesBuffer = nil; - } - int tileSize = MG_MTL_TILE_SIZE; - int nTilesX = (int)(size.x + tileSize - 1)/tileSize; - int nTilesY = (int)(size.y + tileSize - 1)/tileSize; - MTLResourceOptions bufferOptions = MTLResourceStorageModePrivate; - backend->screenTilesBuffer = [backend->surface->device newBufferWithLength: nTilesX*nTilesY*sizeof(int) - options: bufferOptions]; - - if(backend->outTexture) - { - [backend->outTexture release]; - backend->outTexture = nil; - } - MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; - texDesc.textureType = MTLTextureType2D; - texDesc.storageMode = MTLStorageModePrivate; - texDesc.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite; - texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; - texDesc.width = size.x; - texDesc.height = size.y; - - backend->outTexture = [backend->surface->device newTextureWithDescriptor:texDesc]; - - backend->frameSize = size; - } -} - -void mg_mtl_canvas_render(mg_canvas_backend* interface, - mg_color clearColor, - u32 primitiveCount, - mg_primitive* primitives, - u32 eltCount, - mg_path_elt* pathElements) -{ - mg_mtl_canvas_backend* backend = (mg_mtl_canvas_backend*)interface; - - //NOTE: update rolling buffers - dispatch_semaphore_wait(backend->bufferSemaphore, DISPATCH_TIME_FOREVER); - backend->bufferIndex = (backend->bufferIndex + 1) % MG_MTL_INPUT_BUFFERS_COUNT; - - mg_mtl_path_elt* elementBufferData = (mg_mtl_path_elt*)[backend->elementBuffer[backend->bufferIndex] contents]; - mg_mtl_path* pathBufferData = (mg_mtl_path*)[backend->pathBuffer[backend->bufferIndex] contents]; - - ///////////////////////////////////////////////////////////////////////////////////// - //TODO: ensure screen tiles buffer is correct size - ///////////////////////////////////////////////////////////////////////////////////// - - //NOTE: prepare rendering - mg_mtl_surface* surface = backend->surface; - - mp_rect frame = surface->interface.getFrame((mg_surface_data*)surface); - - f32 scale = surface->mtlLayer.contentsScale; - vec2 viewportSize = {frame.w * scale, frame.h * scale}; - int tileSize = MG_MTL_TILE_SIZE; - int nTilesX = (int)(frame.w * scale + tileSize - 1)/tileSize; - int nTilesY = (int)(frame.h * scale + tileSize - 1)/tileSize; - - if(viewportSize.x != backend->frameSize.x || viewportSize.y != backend->frameSize.y) - { - mg_mtl_canvas_resize(backend, viewportSize); - } - - mg_mtl_surface_acquire_command_buffer(surface); - mg_mtl_surface_acquire_drawable(surface); - - @autoreleasepool - { - //NOTE: clear log counter - id blitEncoder = [surface->commandBuffer blitCommandEncoder]; - blitEncoder.label = @"clear log counter"; - [blitEncoder fillBuffer: backend->logOffsetBuffer[backend->bufferIndex] range: NSMakeRange(0, sizeof(int)) value: 0]; - [blitEncoder endEncoding]; - - //NOTE: clear screen - MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; - renderPassDescriptor.colorAttachments[0].texture = surface->drawable.texture; - renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; - renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(clearColor.r, clearColor.g, clearColor.b, clearColor.a); - renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; - - id renderEncoder = [surface->commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; - renderEncoder.label = @"clear pass"; - [renderEncoder endEncoding]; - } - backend->pathBufferOffset = 0; - backend->elementBufferOffset = 0; - - //NOTE: encode and render batches - int pathCount = 0; - vec2 currentPos = {0}; - - mg_image currentImage = mg_image_nil(); - mg_mtl_encoding_context context = {.mtlEltCount = 0, - .elementBufferData = elementBufferData, - .pathBufferData = pathBufferData}; - - for(int primitiveIndex = 0; primitiveIndex < primitiveCount; primitiveIndex++) - { - mg_primitive* primitive = &primitives[primitiveIndex]; - - if(primitiveIndex && (primitive->attributes.image.h != currentImage.h)) - { - mg_image_data* imageData = mg_image_data_from_handle(currentImage); - - mg_mtl_render_batch(backend, - surface, - pathCount, - context.mtlEltCount, - imageData, - tileSize, - nTilesX, - nTilesY, - viewportSize, - scale); - - backend->pathBufferOffset += pathCount * sizeof(mg_mtl_path); - backend->elementBufferOffset += context.mtlEltCount * sizeof(mg_mtl_path_elt); - pathCount = 0; - context.mtlEltCount = 0; - context.elementBufferData = (mg_mtl_path_elt*)((char*)elementBufferData + backend->elementBufferOffset); - context.pathBufferData = (mg_mtl_path*)((char*)pathBufferData + backend->pathBufferOffset); - } - currentImage = primitive->attributes.image; - - if(primitive->path.count) - { - context.primitive = primitive; - context.pathIndex = pathCount; - context.pathScreenExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; - context.pathUserExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; - - if(primitive->cmd == MG_CMD_STROKE) - { - mg_mtl_render_stroke(&context, pathElements + primitive->path.startIndex, &primitive->path); - } - else - { - int segCount = 0; - for(int eltIndex = 0; - (eltIndex < primitive->path.count) && (primitive->path.startIndex + eltIndex < eltCount); - eltIndex++) - { - context.localEltIndex = segCount; - - mg_path_elt* elt = &pathElements[primitive->path.startIndex + eltIndex]; - - if(elt->type != MG_PATH_MOVE) - { - vec2 p[4] = {currentPos, elt->p[0], elt->p[1], elt->p[2]}; - mg_mtl_canvas_encode_element(&context, elt->type, p); - segCount++; - } - switch(elt->type) - { - case MG_PATH_MOVE: - currentPos = elt->p[0]; - break; - - case MG_PATH_LINE: - currentPos = elt->p[0]; - break; - - case MG_PATH_QUADRATIC: - currentPos = elt->p[1]; - break; - - case MG_PATH_CUBIC: - currentPos = elt->p[2]; - break; - } - } - } - //NOTE: push path - mg_mtl_path* path = &context.pathBufferData[pathCount]; - pathCount++; - - path->cmd = (mg_mtl_cmd)primitive->cmd; - - path->box = (vector_float4){context.pathScreenExtents.x, - context.pathScreenExtents.y, - context.pathScreenExtents.z, - context.pathScreenExtents.w}; - - path->clip = (vector_float4){primitive->attributes.clip.x, - primitive->attributes.clip.y, - primitive->attributes.clip.x + primitive->attributes.clip.w, - primitive->attributes.clip.y + primitive->attributes.clip.h}; - - path->color = (vector_float4){primitive->attributes.color.r, - primitive->attributes.color.g, - primitive->attributes.color.b, - primitive->attributes.color.a}; - - mp_rect srcRegion = primitive->attributes.srcRegion; - - mp_rect destRegion = {context.pathUserExtents.x, - context.pathUserExtents.y, - context.pathUserExtents.z - context.pathUserExtents.x, - context.pathUserExtents.w - context.pathUserExtents.y}; - - if(!mg_image_is_nil(primitive->attributes.image)) - { - vec2 texSize = mg_image_size(primitive->attributes.image); - - mg_mat2x3 srcRegionToImage = {1/texSize.x, 0, srcRegion.x/texSize.x, - 0, 1/texSize.y, srcRegion.y/texSize.y}; - - mg_mat2x3 destRegionToSrcRegion = {srcRegion.w/destRegion.w, 0, 0, - 0, srcRegion.h/destRegion.h, 0}; - - mg_mat2x3 userToDestRegion = {1, 0, -destRegion.x, - 0, 1, -destRegion.y}; - - mg_mat2x3 screenToUser = mg_mat2x3_inv(primitive->attributes.transform); - - mg_mat2x3 uvTransform = srcRegionToImage; - uvTransform = mg_mat2x3_mul_m(uvTransform, destRegionToSrcRegion); - uvTransform = mg_mat2x3_mul_m(uvTransform, userToDestRegion); - uvTransform = mg_mat2x3_mul_m(uvTransform, screenToUser); - - path->uvTransform = simd_matrix(simd_make_float3(uvTransform.m[0]/scale, uvTransform.m[3]/scale, 0), - simd_make_float3(uvTransform.m[1]/scale, uvTransform.m[4]/scale, 0), - simd_make_float3(uvTransform.m[2], uvTransform.m[5], 1)); - } - } - } - - mg_image_data* imageData = mg_image_data_from_handle(currentImage); - mg_mtl_render_batch(backend, - surface, - pathCount, - context.mtlEltCount, - imageData, - tileSize, - nTilesX, - nTilesY, - viewportSize, - scale); - - @autoreleasepool - { - //NOTE: finalize - [surface->commandBuffer addCompletedHandler:^(id commandBuffer) - { - mg_mtl_print_log(backend->bufferIndex, backend->logBuffer[backend->bufferIndex], backend->logOffsetBuffer[backend->bufferIndex]); - dispatch_semaphore_signal(backend->bufferSemaphore); - }]; - } -} - -void mg_mtl_canvas_destroy(mg_canvas_backend* interface) -{ - mg_mtl_canvas_backend* backend = (mg_mtl_canvas_backend*)interface; - - @autoreleasepool - { - [backend->pathPipeline release]; - [backend->segmentPipeline release]; - [backend->backpropPipeline release]; - [backend->mergePipeline release]; - [backend->rasterPipeline release]; - [backend->blitPipeline release]; - - for(int i=0; ipathBuffer[i] release]; - [backend->elementBuffer[i] release]; - [backend->logBuffer[i] release]; - [backend->logOffsetBuffer[i] release]; - } - [backend->segmentCountBuffer release]; - [backend->segmentBuffer release]; - [backend->tileQueueBuffer release]; - [backend->tileQueueCountBuffer release]; - [backend->tileOpBuffer release]; - [backend->tileOpCountBuffer release]; - [backend->screenTilesBuffer release]; - } - - free(backend); -} - -mg_image_data* mg_mtl_canvas_image_create(mg_canvas_backend* interface, vec2 size) -{ - mg_mtl_image_data* image = 0; - mg_mtl_canvas_backend* backend = (mg_mtl_canvas_backend*)interface; - mg_mtl_surface* surface = backend->surface; - - @autoreleasepool - { - image = malloc_type(mg_mtl_image_data); - if(image) - { - MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; - texDesc.textureType = MTLTextureType2D; - texDesc.storageMode = MTLStorageModeManaged; - texDesc.usage = MTLTextureUsageShaderRead; - texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; - texDesc.width = size.x; - texDesc.height = size.y; - - image->texture = [surface->device newTextureWithDescriptor:texDesc]; - if(image->texture != nil) - { - [image->texture retain]; - image->interface.size = size; - } - else - { - free(image); - image = 0; - } - } - } - return((mg_image_data*)image); -} - -void mg_mtl_canvas_image_destroy(mg_canvas_backend* backendInterface, mg_image_data* imageInterface) -{ - mg_mtl_image_data* image = (mg_mtl_image_data*)imageInterface; - @autoreleasepool - { - [image->texture release]; - free(image); - } -} - -void mg_mtl_canvas_image_upload_region(mg_canvas_backend* backendInterface, mg_image_data* imageInterface, mp_rect region, u8* pixels) -{@autoreleasepool{ - mg_mtl_image_data* image = (mg_mtl_image_data*)imageInterface; - MTLRegion mtlRegion = MTLRegionMake2D(region.x, region.y, region.w, region.h); - [image->texture replaceRegion:mtlRegion - mipmapLevel:0 - withBytes:(void*)pixels - bytesPerRow: 4 * region.w]; -}} - -const u32 MG_MTL_PATH_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_path), - MG_MTL_ELEMENT_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_path_elt), - MG_MTL_SEGMENT_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_segment), - MG_MTL_PATH_QUEUE_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_path_queue), - MG_MTL_TILE_QUEUE_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_tile_queue), - MG_MTL_TILE_OP_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_tile_op); - -mg_canvas_backend* mtl_canvas_backend_create(mg_mtl_surface* surface) -{ - mg_mtl_canvas_backend* backend = 0; - - backend = malloc_type(mg_mtl_canvas_backend); - memset(backend, 0, sizeof(mg_mtl_canvas_backend)); - - backend->msaaCount = MG_MTL_MSAA_COUNT; - backend->surface = surface; - - //NOTE(martin): setup interface functions - backend->interface.destroy = mg_mtl_canvas_destroy; - backend->interface.render = mg_mtl_canvas_render; - backend->interface.imageCreate = mg_mtl_canvas_image_create; - backend->interface.imageDestroy = mg_mtl_canvas_image_destroy; - backend->interface.imageUploadRegion = mg_mtl_canvas_image_upload_region; - - @autoreleasepool{ - //NOTE: load metal library - str8 shaderPath = mp_app_get_resource_path(mem_scratch(), "mtl_renderer.metallib"); - NSString* metalFileName = [[NSString alloc] initWithBytes: shaderPath.ptr length:shaderPath.len encoding: NSUTF8StringEncoding]; - NSError* err = 0; - id library = [surface->device newLibraryWithFile: metalFileName error:&err]; - if(err != nil) - { - const char* errStr = [[err localizedDescription] UTF8String]; - log_error("error : %s\n", errStr); - return(0); - } - id pathFunction = [library newFunctionWithName:@"mtl_path_setup"]; - id segmentFunction = [library newFunctionWithName:@"mtl_segment_setup"]; - id backpropFunction = [library newFunctionWithName:@"mtl_backprop"]; - id mergeFunction = [library newFunctionWithName:@"mtl_merge"]; - id rasterFunction = [library newFunctionWithName:@"mtl_raster"]; - id vertexFunction = [library newFunctionWithName:@"mtl_vertex_shader"]; - id fragmentFunction = [library newFunctionWithName:@"mtl_fragment_shader"]; - - //NOTE: create pipelines - NSError* error = NULL; - - backend->pathPipeline = [surface->device newComputePipelineStateWithFunction: pathFunction - error:&error]; - - backend->segmentPipeline = [surface->device newComputePipelineStateWithFunction: segmentFunction - error:&error]; - - backend->backpropPipeline = [surface->device newComputePipelineStateWithFunction: backpropFunction - error:&error]; - - backend->mergePipeline = [surface->device newComputePipelineStateWithFunction: mergeFunction - error:&error]; - - backend->rasterPipeline = [surface->device newComputePipelineStateWithFunction: rasterFunction - error:&error]; - - MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; - pipelineStateDescriptor.label = @"blit pipeline"; - pipelineStateDescriptor.vertexFunction = vertexFunction; - pipelineStateDescriptor.fragmentFunction = fragmentFunction; - pipelineStateDescriptor.colorAttachments[0].pixelFormat = surface->mtlLayer.pixelFormat; - pipelineStateDescriptor.colorAttachments[0].blendingEnabled = YES; - pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; - pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne; - pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; - pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; - pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne; - pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; - - backend->blitPipeline = [surface->device newRenderPipelineStateWithDescriptor: pipelineStateDescriptor error:&err]; - - //NOTE: create textures - mp_rect frame = surface->interface.getFrame((mg_surface_data*)surface); - f32 scale = surface->mtlLayer.contentsScale; - - backend->frameSize = (vec2){frame.w*scale, frame.h*scale}; - - MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; - texDesc.textureType = MTLTextureType2D; - texDesc.storageMode = MTLStorageModePrivate; - texDesc.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite; - texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; - texDesc.width = backend->frameSize.x; - texDesc.height = backend->frameSize.y; - - backend->outTexture = [surface->device newTextureWithDescriptor:texDesc]; - - //NOTE: create buffers - - backend->bufferSemaphore = dispatch_semaphore_create(MG_MTL_INPUT_BUFFERS_COUNT); - backend->bufferIndex = 0; - - MTLResourceOptions bufferOptions = MTLResourceCPUCacheModeWriteCombined - | MTLResourceStorageModeShared; - - for(int i=0; ipathBuffer[i] = [surface->device newBufferWithLength: MG_MTL_PATH_BUFFER_SIZE - options: bufferOptions]; - - backend->elementBuffer[i] = [surface->device newBufferWithLength: MG_MTL_ELEMENT_BUFFER_SIZE - options: bufferOptions]; - } - - bufferOptions = MTLResourceStorageModePrivate; - backend->segmentBuffer = [surface->device newBufferWithLength: MG_MTL_SEGMENT_BUFFER_SIZE - options: bufferOptions]; - - backend->segmentCountBuffer = [surface->device newBufferWithLength: sizeof(int) - options: bufferOptions]; - - backend->pathQueueBuffer = [surface->device newBufferWithLength: MG_MTL_PATH_QUEUE_BUFFER_SIZE - options: bufferOptions]; - - backend->tileQueueBuffer = [surface->device newBufferWithLength: MG_MTL_TILE_QUEUE_BUFFER_SIZE - options: bufferOptions]; - - backend->tileQueueCountBuffer = [surface->device newBufferWithLength: sizeof(int) - options: bufferOptions]; - - backend->tileOpBuffer = [surface->device newBufferWithLength: MG_MTL_TILE_OP_BUFFER_SIZE - options: bufferOptions]; - - backend->tileOpCountBuffer = [surface->device newBufferWithLength: sizeof(int) - options: bufferOptions]; - - int tileSize = MG_MTL_TILE_SIZE; - int nTilesX = (int)(frame.w * scale + tileSize - 1)/tileSize; - int nTilesY = (int)(frame.h * scale + tileSize - 1)/tileSize; - backend->screenTilesBuffer = [surface->device newBufferWithLength: nTilesX*nTilesY*sizeof(int) - options: bufferOptions]; - - bufferOptions = MTLResourceStorageModeShared; - for(int i=0; ilogBuffer[i] = [surface->device newBufferWithLength: 1<<20 - options: bufferOptions]; - - backend->logOffsetBuffer[i] = [surface->device newBufferWithLength: sizeof(int) - options: bufferOptions]; - } - } - return((mg_canvas_backend*)backend); -} - -mg_surface_data* mtl_canvas_surface_create_for_window(mp_window window) -{ - mg_mtl_surface* surface = (mg_mtl_surface*)mg_mtl_surface_create_for_window(window); - - if(surface) - { - surface->interface.backend = mtl_canvas_backend_create(surface); - if(surface->interface.backend) - { - surface->interface.api = MG_CANVAS; - } - else - { - surface->interface.destroy((mg_surface_data*)surface); - surface = 0; - } - } - return((mg_surface_data*)surface); -} +/************************************************************//** +* +* @file: mtl_canvas.m +* @author: Martin Fouilleul +* @date: 12/07/2020 +* @revision: 24/01/2023 +* +*****************************************************************/ +#import +#import +#include + +#include"graphics_surface.h" +#include"macro_helpers.h" +#include"osx_app.h" + +#include"mtl_renderer.h" + +const int MG_MTL_INPUT_BUFFERS_COUNT = 3, + MG_MTL_TILE_SIZE = 16, + MG_MTL_MSAA_COUNT = 8; + +typedef struct mg_mtl_canvas_backend +{ + mg_canvas_backend interface; + mg_mtl_surface* surface; + + id pathPipeline; + id segmentPipeline; + id backpropPipeline; + id mergePipeline; + id rasterPipeline; + id blitPipeline; + + id outTexture; + + int pathBufferOffset; + int elementBufferOffset; + int bufferIndex; + dispatch_semaphore_t bufferSemaphore; + + id pathBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; + id elementBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; + id logBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; + id logOffsetBuffer[MG_MTL_INPUT_BUFFERS_COUNT]; + + id segmentCountBuffer; + id segmentBuffer; + id pathQueueBuffer; + id tileQueueBuffer; + id tileQueueCountBuffer; + id tileOpBuffer; + id tileOpCountBuffer; + id screenTilesBuffer; + + int msaaCount; + vec2 frameSize; + +} mg_mtl_canvas_backend; + +typedef struct mg_mtl_image_data +{ + mg_image_data interface; + id texture; +} mg_mtl_image_data; + +void mg_mtl_print_log(int bufferIndex, id logBuffer, id logOffsetBuffer) +{ + char* log = [logBuffer contents]; + int size = *(int*)[logOffsetBuffer contents]; + + if(size) + { + log_info("Log from buffer %i:\n", bufferIndex); + + int index = 0; + while(index < size) + { + int len = strlen(log+index); + printf("%s", log+index); + index += (len+1); + } + } +} + + +typedef struct mg_mtl_encoding_context +{ + int mtlEltCount; + mg_mtl_path* pathBufferData; + mg_mtl_path_elt* elementBufferData; + int pathIndex; + int localEltIndex; + mg_primitive* primitive; + vec4 pathScreenExtents; + vec4 pathUserExtents; + +} mg_mtl_encoding_context; + +static void mg_update_path_extents(vec4* extents, vec2 p) +{ + extents->x = minimum(extents->x, p.x); + extents->y = minimum(extents->y, p.y); + extents->z = maximum(extents->z, p.x); + extents->w = maximum(extents->w, p.y); +} + +void mg_mtl_canvas_encode_element(mg_mtl_encoding_context* context, mg_path_elt_type kind, vec2* p) +{ + mg_mtl_path_elt* mtlElt = &context->elementBufferData[context->mtlEltCount]; + context->mtlEltCount++; + + mtlElt->pathIndex = context->pathIndex; + int count = 0; + switch(kind) + { + case MG_PATH_LINE: + mtlElt->kind = MG_MTL_LINE; + count = 2; + break; + + case MG_PATH_QUADRATIC: + mtlElt->kind = MG_MTL_QUADRATIC; + count = 3; + break; + + case MG_PATH_CUBIC: + mtlElt->kind = MG_MTL_CUBIC; + count = 4; + break; + + default: + break; + } + + mtlElt->localEltIndex = context->localEltIndex; + + for(int i=0; ipathUserExtents, p[i]); + + vec2 screenP = mg_mat2x3_mul(context->primitive->attributes.transform, p[i]); + mtlElt->p[i] = (vector_float2){screenP.x, screenP.y}; + + mg_update_path_extents(&context->pathScreenExtents, screenP); + } +} + + +bool mg_intersect_hull_legs(vec2 p0, vec2 p1, vec2 p2, vec2 p3, vec2* intersection) +{ + /*NOTE: check intersection of lines (p0-p1) and (p2-p3) + + P = p0 + u(p1-p0) + P = p2 + w(p3-p2) + */ + bool found = false; + + f32 den = (p0.x - p1.x)*(p2.y - p3.y) - (p0.y - p1.y)*(p2.x - p3.x); + if(fabs(den) > 0.0001) + { + f32 u = ((p0.x - p2.x)*(p2.y - p3.y) - (p0.y - p2.y)*(p2.x - p3.x))/den; + f32 w = ((p0.x - p2.x)*(p0.y - p1.y) - (p0.y - p2.y)*(p0.x - p1.x))/den; + + intersection->x = p0.x + u*(p1.x - p0.x); + intersection->y = p0.y + u*(p1.y - p0.y); + found = true; + } + return(found); +} + +bool mg_offset_hull(int count, vec2* p, vec2* result, f32 offset) +{ + //NOTE: we should have no more than two coincident points here. This means the leg between + // those two points can't be offset, but we can set a double point at the start of first leg, + // end of first leg, or we can join the first and last leg to create a missing middle one + + vec2 legs[3][2] = {0}; + bool valid[3] = {0}; + + for(int i=0; i= 1e-6) + { + n = vec2_mul(offset/norm, n); + legs[i][0] = vec2_add(p[i], n); + legs[i][1] = vec2_add(p[i+1], n); + valid[i] = true; + } + } + + //NOTE: now we find intersections + + // first point is either the start of the first or second leg + if(valid[0]) + { + result[0] = legs[0][0]; + } + else + { + ASSERT(valid[1]); + result[0] = legs[1][0]; + } + + for(int i=1; iprimitive->attributes.width; + + vec2 v = {p[1].x-p[0].x, p[1].y-p[0].y}; + vec2 n = {v.y, -v.x}; + f32 norm = sqrt(n.x*n.x + n.y*n.y); + vec2 offset = vec2_mul(0.5*width/norm, n); + + vec2 left[2] = {vec2_add(p[0], offset), vec2_add(p[1], offset)}; + vec2 right[2] = {vec2_add(p[1], vec2_mul(-1, offset)), vec2_add(p[0], vec2_mul(-1, offset))}; + vec2 joint0[2] = {vec2_add(p[0], vec2_mul(-1, offset)), vec2_add(p[0], offset)}; + vec2 joint1[2] = {vec2_add(p[1], offset), vec2_add(p[1], vec2_mul(-1, offset))}; + + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, right); + + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, left); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint0); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint1); +} + +void mg_mtl_render_stroke_quadratic(mg_mtl_encoding_context* context, vec2* p) +{ + f32 width = context->primitive->attributes.width; + f32 tolerance = minimum(context->primitive->attributes.tolerance, 0.5 * width); + + //NOTE: check for degenerate line case + const f32 equalEps = 1e-3; + if(vec2_close(p[0], p[1], equalEps)) + { + mg_mtl_render_stroke_line(context, p+1); + return; + } + else if(vec2_close(p[1], p[2], equalEps)) + { + mg_mtl_render_stroke_line(context, p); + return; + } + + vec2 leftHull[3]; + vec2 rightHull[3]; + + if( !mg_offset_hull(3, p, leftHull, width/2) + || !mg_offset_hull(3, p, rightHull, -width/2)) + { + //TODO split and recurse + //NOTE: offsetting the hull failed, split the curve + vec2 splitLeft[3]; + vec2 splitRight[3]; + mg_quadratic_split(p, 0.5, splitLeft, splitRight); + mg_mtl_render_stroke_quadratic(context, splitLeft); + mg_mtl_render_stroke_quadratic(context, splitRight); + } + else + { + const int CHECK_SAMPLE_COUNT = 5; + f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; + + f32 d2LowBound = Square(0.5 * width - tolerance); + f32 d2HighBound = Square(0.5 * width + tolerance); + + f32 maxOvershoot = 0; + f32 maxOvershootParameter = 0; + + for(int i=0; i maxOvershoot) + { + maxOvershoot = overshoot; + maxOvershootParameter = t; + } + } + + if(maxOvershoot > 0) + { + vec2 splitLeft[3]; + vec2 splitRight[3]; + mg_quadratic_split(p, maxOvershootParameter, splitLeft, splitRight); + mg_mtl_render_stroke_quadratic(context, splitLeft); + mg_mtl_render_stroke_quadratic(context, splitRight); + } + else + { + vec2 tmp = leftHull[0]; + leftHull[0] = leftHull[2]; + leftHull[2] = tmp; + + mg_mtl_canvas_encode_element(context, MG_PATH_QUADRATIC, rightHull); + mg_mtl_canvas_encode_element(context, MG_PATH_QUADRATIC, leftHull); + + vec2 joint0[2] = {rightHull[2], leftHull[0]}; + vec2 joint1[2] = {leftHull[2], rightHull[0]}; + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint0); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint1); + } + } +} + +void mg_mtl_render_stroke_cubic(mg_mtl_encoding_context* context, vec2* p) +{ + f32 width = context->primitive->attributes.width; + f32 tolerance = minimum(context->primitive->attributes.tolerance, 0.5 * width); + + //NOTE: check degenerate line cases + f32 equalEps = 1e-3; + + if( (vec2_close(p[0], p[1], equalEps) && vec2_close(p[2], p[3], equalEps)) + ||(vec2_close(p[0], p[1], equalEps) && vec2_close(p[1], p[2], equalEps)) + ||(vec2_close(p[1], p[2], equalEps) && vec2_close(p[2], p[3], equalEps))) + { + vec2 line[2] = {p[0], p[3]}; + mg_mtl_render_stroke_line(context, line); + return; + } + else if(vec2_close(p[0], p[1], equalEps) && vec2_close(p[1], p[3], equalEps)) + { + vec2 line[2] = {p[0], vec2_add(vec2_mul(5./9, p[0]), vec2_mul(4./9, p[2]))}; + mg_mtl_render_stroke_line(context, line); + return; + } + else if(vec2_close(p[0], p[2], equalEps) && vec2_close(p[2], p[3], equalEps)) + { + vec2 line[2] = {p[0], vec2_add(vec2_mul(5./9, p[0]), vec2_mul(4./9, p[1]))}; + mg_mtl_render_stroke_line(context, line); + return; + } + + vec2 leftHull[4]; + vec2 rightHull[4]; + + if( !mg_offset_hull(4, p, leftHull, width/2) + || !mg_offset_hull(4, p, rightHull, -width/2)) + { + //TODO split and recurse + //NOTE: offsetting the hull failed, split the curve + vec2 splitLeft[4]; + vec2 splitRight[4]; + mg_cubic_split(p, 0.5, splitLeft, splitRight); + mg_mtl_render_stroke_cubic(context, splitLeft); + mg_mtl_render_stroke_cubic(context, splitRight); + } + else + { + const int CHECK_SAMPLE_COUNT = 5; + f32 checkSamples[CHECK_SAMPLE_COUNT] = {1./6, 2./6, 3./6, 4./6, 5./6}; + + f32 d2LowBound = Square(0.5 * width - tolerance); + f32 d2HighBound = Square(0.5 * width + tolerance); + + f32 maxOvershoot = 0; + f32 maxOvershootParameter = 0; + + for(int i=0; i maxOvershoot) + { + maxOvershoot = overshoot; + maxOvershootParameter = t; + } + } + + if(maxOvershoot > 0) + { + vec2 splitLeft[4]; + vec2 splitRight[4]; + mg_cubic_split(p, maxOvershootParameter, splitLeft, splitRight); + mg_mtl_render_stroke_cubic(context, splitLeft); + mg_mtl_render_stroke_cubic(context, splitRight); + } + else + { + vec2 tmp = leftHull[0]; + leftHull[0] = leftHull[3]; + leftHull[3] = tmp; + tmp = leftHull[1]; + leftHull[1] = leftHull[2]; + leftHull[2] = tmp; + + mg_mtl_canvas_encode_element(context, MG_PATH_CUBIC, rightHull); + mg_mtl_canvas_encode_element(context, MG_PATH_CUBIC, leftHull); + + vec2 joint0[2] = {rightHull[3], leftHull[0]}; + vec2 joint1[2] = {leftHull[3], rightHull[0]}; + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint0); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, joint1); + } + } +} + +void mg_mtl_render_stroke_element(mg_mtl_encoding_context* context, + mg_path_elt* element, + vec2 currentPoint, + vec2* startTangent, + vec2* endTangent, + vec2* endPoint) +{ + vec2 controlPoints[4] = {currentPoint, element->p[0], element->p[1], element->p[2]}; + int endPointIndex = 0; + + switch(element->type) + { + case MG_PATH_LINE: + mg_mtl_render_stroke_line(context, controlPoints); + endPointIndex = 1; + break; + + case MG_PATH_QUADRATIC: + mg_mtl_render_stroke_quadratic(context, controlPoints); + endPointIndex = 2; + break; + + case MG_PATH_CUBIC: + mg_mtl_render_stroke_cubic(context, controlPoints); + endPointIndex = 3; + break; + + case MG_PATH_MOVE: + ASSERT(0, "should be unreachable"); + break; + } + + //NOTE: ensure tangents are properly computed even in presence of coincident points + //TODO: see if we can do this in a less hacky way + + for(int i=1; i<4; i++) + { + if( controlPoints[i].x != controlPoints[0].x + || controlPoints[i].y != controlPoints[0].y) + { + *startTangent = (vec2){.x = controlPoints[i].x - controlPoints[0].x, + .y = controlPoints[i].y - controlPoints[0].y}; + break; + } + } + *endPoint = controlPoints[endPointIndex]; + + for(int i=endPointIndex-1; i>=0; i++) + { + if( controlPoints[i].x != endPoint->x + || controlPoints[i].y != endPoint->y) + { + *endTangent = (vec2){.x = endPoint->x - controlPoints[i].x, + .y = endPoint->y - controlPoints[i].y}; + break; + } + } + DEBUG_ASSERT(startTangent->x != 0 || startTangent->y != 0); +} + +void mg_mtl_stroke_cap(mg_mtl_encoding_context* context, + vec2 p0, + vec2 direction) +{ + mg_attributes* attributes = &context->primitive->attributes; + + //NOTE(martin): compute the tangent and normal vectors (multiplied by half width) at the cap point + f32 dn = sqrt(Square(direction.x) + Square(direction.y)); + f32 alpha = 0.5 * attributes->width/dn; + + vec2 n0 = {-alpha*direction.y, + alpha*direction.x}; + + vec2 m0 = {alpha*direction.x, + alpha*direction.y}; + + vec2 points[] = {{p0.x + n0.x, p0.y + n0.y}, + {p0.x + n0.x + m0.x, p0.y + n0.y + m0.y}, + {p0.x - n0.x + m0.x, p0.y - n0.y + m0.y}, + {p0.x - n0.x, p0.y - n0.y}, + {p0.x + n0.x, p0.y + n0.y}}; + + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+1); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+2); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+3); +} + +void mg_mtl_stroke_joint(mg_mtl_encoding_context* context, + vec2 p0, + vec2 t0, + vec2 t1) +{ + mg_attributes* attributes = &context->primitive->attributes; + + //NOTE(martin): compute the normals at the joint point + f32 norm_t0 = sqrt(Square(t0.x) + Square(t0.y)); + f32 norm_t1 = sqrt(Square(t1.x) + Square(t1.y)); + + vec2 n0 = {-t0.y, t0.x}; + n0.x /= norm_t0; + n0.y /= norm_t0; + + vec2 n1 = {-t1.y, t1.x}; + n1.x /= norm_t1; + n1.y /= norm_t1; + + //NOTE(martin): the sign of the cross product determines if the normals are facing outwards or inwards the angle. + // we flip them to face outwards if needed + f32 crossZ = n0.x*n1.y - n0.y*n1.x; + if(crossZ > 0) + { + n0.x *= -1; + n0.y *= -1; + n1.x *= -1; + n1.y *= -1; + } + + //NOTE(martin): use the same code as hull offset to find mitter point... + /*NOTE(martin): let vector u = (n0+n1) and vector v = pIntersect - p1 + then v = u * (2*offset / norm(u)^2) + (this can be derived from writing the pythagoras theorems in the triangles of the joint) + */ + f32 halfW = 0.5 * attributes->width; + vec2 u = {n0.x + n1.x, n0.y + n1.y}; + f32 uNormSquare = u.x*u.x + u.y*u.y; + f32 alpha = attributes->width / uNormSquare; + vec2 v = {u.x * alpha, u.y * alpha}; + + f32 excursionSquare = uNormSquare * Square(alpha - attributes->width/4); + + if( attributes->joint == MG_JOINT_MITER + && excursionSquare <= Square(attributes->maxJointExcursion)) + { + //NOTE(martin): add a mitter joint + vec2 points[] = {p0, + {p0.x + n0.x*halfW, p0.y + n0.y*halfW}, + {p0.x + v.x, p0.y + v.y}, + {p0.x + n1.x*halfW, p0.y + n1.y*halfW}, + p0}; + + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+1); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+2); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+3); + } + else + { + //NOTE(martin): add a bevel joint + vec2 points[] = {p0, + {p0.x + n0.x*halfW, p0.y + n0.y*halfW}, + {p0.x + n1.x*halfW, p0.y + n1.y*halfW}, + p0}; + + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+1); + mg_mtl_canvas_encode_element(context, MG_PATH_LINE, points+2); + } +} + +u32 mg_mtl_render_stroke_subpath(mg_mtl_encoding_context* context, + mg_path_elt* elements, + mg_path_descriptor* path, + u32 startIndex, + vec2 startPoint) +{ + u32 eltCount = path->count; + DEBUG_ASSERT(startIndex < eltCount); + + vec2 currentPoint = startPoint; + vec2 endPoint = {0, 0}; + vec2 previousEndTangent = {0, 0}; + vec2 firstTangent = {0, 0}; + vec2 startTangent = {0, 0}; + vec2 endTangent = {0, 0}; + + //NOTE(martin): render first element and compute first tangent + mg_mtl_render_stroke_element(context, elements + startIndex, currentPoint, &startTangent, &endTangent, &endPoint); + + firstTangent = startTangent; + previousEndTangent = endTangent; + currentPoint = endPoint; + + //NOTE(martin): render subsequent elements along with their joints + + mg_attributes* attributes = &context->primitive->attributes; + + u32 eltIndex = startIndex + 1; + for(; + eltIndexjoint != MG_JOINT_NONE) + { + mg_mtl_stroke_joint(context, currentPoint, previousEndTangent, startTangent); + } + previousEndTangent = endTangent; + currentPoint = endPoint; + } + u32 subPathEltCount = eltIndex - startIndex; + + //NOTE(martin): draw end cap / joint. We ensure there's at least two segments to draw a closing joint + if( subPathEltCount > 1 + && startPoint.x == endPoint.x + && startPoint.y == endPoint.y) + { + if(attributes->joint != MG_JOINT_NONE) + { + //NOTE(martin): add a closing joint if the path is closed + mg_mtl_stroke_joint(context, endPoint, endTangent, firstTangent); + } + } + else if(attributes->cap == MG_CAP_SQUARE) + { + //NOTE(martin): add start and end cap + mg_mtl_stroke_cap(context, startPoint, (vec2){-startTangent.x, -startTangent.y}); + mg_mtl_stroke_cap(context, endPoint, endTangent); + } + return(eltIndex); +} + +void mg_mtl_render_stroke(mg_mtl_encoding_context* context, + mg_path_elt* elements, + mg_path_descriptor* path) +{ + u32 eltCount = path->count; + DEBUG_ASSERT(eltCount); + + vec2 startPoint = path->startPoint; + u32 startIndex = 0; + + while(startIndex < eltCount) + { + //NOTE(martin): eliminate leading moves + while(startIndex < eltCount && elements[startIndex].type == MG_PATH_MOVE) + { + startPoint = elements[startIndex].p[0]; + startIndex++; + } + if(startIndex < eltCount) + { + startIndex = mg_mtl_render_stroke_subpath(context, elements, path, startIndex, startPoint); + } + } +} + + +void mg_mtl_render_batch(mg_mtl_canvas_backend* backend, + mg_mtl_surface* surface, + int pathCount, + int eltCount, + mg_image_data* image, + int tileSize, + int nTilesX, + int nTilesY, + vec2 viewportSize, + f32 scale) +{ + //NOTE: encode GPU commands + @autoreleasepool + { + //NOTE: clear counters + id blitEncoder = [surface->commandBuffer blitCommandEncoder]; + blitEncoder.label = @"clear counters"; + [blitEncoder fillBuffer: backend->segmentCountBuffer range: NSMakeRange(0, sizeof(int)) value: 0]; + [blitEncoder fillBuffer: backend->tileQueueCountBuffer range: NSMakeRange(0, sizeof(int)) value: 0]; + [blitEncoder fillBuffer: backend->tileOpCountBuffer range: NSMakeRange(0, sizeof(int)) value: 0]; + [blitEncoder endEncoding]; + + //NOTE: path setup pass + id pathEncoder = [surface->commandBuffer computeCommandEncoder]; + pathEncoder.label = @"path pass"; + [pathEncoder setComputePipelineState: backend->pathPipeline]; + + [pathEncoder setBytes:&pathCount length:sizeof(int) atIndex:0]; + [pathEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset:backend->pathBufferOffset atIndex:1]; + [pathEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:2]; + [pathEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:3]; + [pathEncoder setBuffer:backend->tileQueueCountBuffer offset:0 atIndex:4]; + [pathEncoder setBytes:&tileSize length:sizeof(int) atIndex:5]; + [pathEncoder setBytes:&scale length:sizeof(int) atIndex:6]; + + MTLSize pathGridSize = MTLSizeMake(pathCount, 1, 1); + MTLSize pathGroupSize = MTLSizeMake([backend->pathPipeline maxTotalThreadsPerThreadgroup], 1, 1); + + [pathEncoder dispatchThreads: pathGridSize threadsPerThreadgroup: pathGroupSize]; + [pathEncoder endEncoding]; + + //NOTE: segment setup pass + id segmentEncoder = [surface->commandBuffer computeCommandEncoder]; + segmentEncoder.label = @"segment pass"; + [segmentEncoder setComputePipelineState: backend->segmentPipeline]; + + [segmentEncoder setBytes:&eltCount length:sizeof(int) atIndex:0]; + [segmentEncoder setBuffer:backend->elementBuffer[backend->bufferIndex] offset:backend->elementBufferOffset atIndex:1]; + [segmentEncoder setBuffer:backend->segmentCountBuffer offset:0 atIndex:2]; + [segmentEncoder setBuffer:backend->segmentBuffer offset:0 atIndex:3]; + [segmentEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:4]; + [segmentEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:5]; + [segmentEncoder setBuffer:backend->tileOpBuffer offset:0 atIndex:6]; + [segmentEncoder setBuffer:backend->tileOpCountBuffer offset:0 atIndex:7]; + [segmentEncoder setBytes:&tileSize length:sizeof(int) atIndex:8]; + [segmentEncoder setBytes:&scale length:sizeof(int) atIndex:9]; + [segmentEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:10]; + [segmentEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:11]; + + MTLSize segmentGridSize = MTLSizeMake(eltCount, 1, 1); + MTLSize segmentGroupSize = MTLSizeMake([backend->segmentPipeline maxTotalThreadsPerThreadgroup], 1, 1); + + [segmentEncoder dispatchThreads: segmentGridSize threadsPerThreadgroup: segmentGroupSize]; + [segmentEncoder endEncoding]; + + //NOTE: backprop pass + id backpropEncoder = [surface->commandBuffer computeCommandEncoder]; + backpropEncoder.label = @"backprop pass"; + [backpropEncoder setComputePipelineState: backend->backpropPipeline]; + + [backpropEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:0]; + [backpropEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:1]; + [backpropEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:2]; + [backpropEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:3]; + + MTLSize backpropGroupSize = MTLSizeMake([backend->backpropPipeline maxTotalThreadsPerThreadgroup], 1, 1); + MTLSize backpropGridSize = MTLSizeMake(pathCount*backpropGroupSize.width, 1, 1); + + [backpropEncoder dispatchThreads: backpropGridSize threadsPerThreadgroup: backpropGroupSize]; + [backpropEncoder endEncoding]; + + //NOTE: merge pass + id mergeEncoder = [surface->commandBuffer computeCommandEncoder]; + mergeEncoder.label = @"merge pass"; + [mergeEncoder setComputePipelineState: backend->mergePipeline]; + + [mergeEncoder setBytes:&pathCount length:sizeof(int) atIndex:0]; + [mergeEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset:backend->pathBufferOffset atIndex:1]; + [mergeEncoder setBuffer:backend->pathQueueBuffer offset:0 atIndex:2]; + [mergeEncoder setBuffer:backend->tileQueueBuffer offset:0 atIndex:3]; + [mergeEncoder setBuffer:backend->tileOpBuffer offset:0 atIndex:4]; + [mergeEncoder setBuffer:backend->tileOpCountBuffer offset:0 atIndex:5]; + [mergeEncoder setBuffer:backend->screenTilesBuffer offset:0 atIndex:6]; + [mergeEncoder setBytes:&tileSize length:sizeof(int) atIndex:7]; + [mergeEncoder setBytes:&scale length:sizeof(float) atIndex:8]; + [mergeEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:9]; + [mergeEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:10]; + + MTLSize mergeGridSize = MTLSizeMake(nTilesX, nTilesY, 1); + MTLSize mergeGroupSize = MTLSizeMake(16, 16, 1); + + [mergeEncoder dispatchThreads: mergeGridSize threadsPerThreadgroup: mergeGroupSize]; + [mergeEncoder endEncoding]; + + //NOTE: raster pass + id rasterEncoder = [surface->commandBuffer computeCommandEncoder]; + rasterEncoder.label = @"raster pass"; + [rasterEncoder setComputePipelineState: backend->rasterPipeline]; + + [rasterEncoder setBuffer:backend->screenTilesBuffer offset:0 atIndex:0]; + [rasterEncoder setBuffer:backend->tileOpBuffer offset:0 atIndex:1]; + [rasterEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset:backend->pathBufferOffset atIndex:2]; + [rasterEncoder setBuffer:backend->segmentBuffer offset:0 atIndex:3]; + [rasterEncoder setBytes:&tileSize length:sizeof(int) atIndex:4]; + [rasterEncoder setBytes:&scale length:sizeof(float) atIndex:5]; + [rasterEncoder setBytes:&backend->msaaCount length:sizeof(int) atIndex:6]; + [rasterEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:7]; + [rasterEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:8]; + + [rasterEncoder setTexture:backend->outTexture atIndex:0]; + + int useTexture = 0; + if(image) + { + mg_mtl_image_data* mtlImage = (mg_mtl_image_data*)image; + [rasterEncoder setTexture: mtlImage->texture atIndex: 1]; + useTexture = 1; + } + [rasterEncoder setBytes: &useTexture length:sizeof(int) atIndex: 9]; + + MTLSize rasterGridSize = MTLSizeMake(viewportSize.x, viewportSize.y, 1); + MTLSize rasterGroupSize = MTLSizeMake(16, 16, 1); + [rasterEncoder dispatchThreads: rasterGridSize threadsPerThreadgroup: rasterGroupSize]; + + [rasterEncoder endEncoding]; + + //NOTE: blit pass + MTLViewport viewport = {0, 0, viewportSize.x, viewportSize.y, 0, 1}; + + MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; + renderPassDescriptor.colorAttachments[0].texture = surface->drawable.texture; + renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionLoad; + renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; + + id renderEncoder = [surface->commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; + renderEncoder.label = @"blit pass"; + [renderEncoder setViewport: viewport]; + [renderEncoder setRenderPipelineState: backend->blitPipeline]; + [renderEncoder setFragmentTexture: backend->outTexture atIndex: 0]; + [renderEncoder drawPrimitives: MTLPrimitiveTypeTriangle + vertexStart: 0 + vertexCount: 3 ]; + [renderEncoder endEncoding]; + } +} + +void mg_mtl_canvas_resize(mg_mtl_canvas_backend* backend, vec2 size) +{ + @autoreleasepool + { + if(backend->screenTilesBuffer) + { + [backend->screenTilesBuffer release]; + backend->screenTilesBuffer = nil; + } + int tileSize = MG_MTL_TILE_SIZE; + int nTilesX = (int)(size.x + tileSize - 1)/tileSize; + int nTilesY = (int)(size.y + tileSize - 1)/tileSize; + MTLResourceOptions bufferOptions = MTLResourceStorageModePrivate; + backend->screenTilesBuffer = [backend->surface->device newBufferWithLength: nTilesX*nTilesY*sizeof(int) + options: bufferOptions]; + + if(backend->outTexture) + { + [backend->outTexture release]; + backend->outTexture = nil; + } + MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; + texDesc.textureType = MTLTextureType2D; + texDesc.storageMode = MTLStorageModePrivate; + texDesc.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite; + texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; + texDesc.width = size.x; + texDesc.height = size.y; + + backend->outTexture = [backend->surface->device newTextureWithDescriptor:texDesc]; + + backend->frameSize = size; + } +} + +void mg_mtl_canvas_render(mg_canvas_backend* interface, + mg_color clearColor, + u32 primitiveCount, + mg_primitive* primitives, + u32 eltCount, + mg_path_elt* pathElements) +{ + mg_mtl_canvas_backend* backend = (mg_mtl_canvas_backend*)interface; + + //NOTE: update rolling buffers + dispatch_semaphore_wait(backend->bufferSemaphore, DISPATCH_TIME_FOREVER); + backend->bufferIndex = (backend->bufferIndex + 1) % MG_MTL_INPUT_BUFFERS_COUNT; + + mg_mtl_path_elt* elementBufferData = (mg_mtl_path_elt*)[backend->elementBuffer[backend->bufferIndex] contents]; + mg_mtl_path* pathBufferData = (mg_mtl_path*)[backend->pathBuffer[backend->bufferIndex] contents]; + + ///////////////////////////////////////////////////////////////////////////////////// + //TODO: ensure screen tiles buffer is correct size + ///////////////////////////////////////////////////////////////////////////////////// + + //NOTE: prepare rendering + mg_mtl_surface* surface = backend->surface; + + mp_rect frame = surface->interface.getFrame((mg_surface_data*)surface); + + f32 scale = surface->mtlLayer.contentsScale; + vec2 viewportSize = {frame.w * scale, frame.h * scale}; + int tileSize = MG_MTL_TILE_SIZE; + int nTilesX = (int)(frame.w * scale + tileSize - 1)/tileSize; + int nTilesY = (int)(frame.h * scale + tileSize - 1)/tileSize; + + if(viewportSize.x != backend->frameSize.x || viewportSize.y != backend->frameSize.y) + { + mg_mtl_canvas_resize(backend, viewportSize); + } + + mg_mtl_surface_acquire_command_buffer(surface); + mg_mtl_surface_acquire_drawable(surface); + + @autoreleasepool + { + //NOTE: clear log counter + id blitEncoder = [surface->commandBuffer blitCommandEncoder]; + blitEncoder.label = @"clear log counter"; + [blitEncoder fillBuffer: backend->logOffsetBuffer[backend->bufferIndex] range: NSMakeRange(0, sizeof(int)) value: 0]; + [blitEncoder endEncoding]; + + //NOTE: clear screen + MTLRenderPassDescriptor* renderPassDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; + renderPassDescriptor.colorAttachments[0].texture = surface->drawable.texture; + renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(clearColor.r, clearColor.g, clearColor.b, clearColor.a); + renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; + + id renderEncoder = [surface->commandBuffer renderCommandEncoderWithDescriptor:renderPassDescriptor]; + renderEncoder.label = @"clear pass"; + [renderEncoder endEncoding]; + } + backend->pathBufferOffset = 0; + backend->elementBufferOffset = 0; + + //NOTE: encode and render batches + int pathCount = 0; + vec2 currentPos = {0}; + + mg_image currentImage = mg_image_nil(); + mg_mtl_encoding_context context = {.mtlEltCount = 0, + .elementBufferData = elementBufferData, + .pathBufferData = pathBufferData}; + + for(int primitiveIndex = 0; primitiveIndex < primitiveCount; primitiveIndex++) + { + mg_primitive* primitive = &primitives[primitiveIndex]; + + if(primitiveIndex && (primitive->attributes.image.h != currentImage.h)) + { + mg_image_data* imageData = mg_image_data_from_handle(currentImage); + + mg_mtl_render_batch(backend, + surface, + pathCount, + context.mtlEltCount, + imageData, + tileSize, + nTilesX, + nTilesY, + viewportSize, + scale); + + backend->pathBufferOffset += pathCount * sizeof(mg_mtl_path); + backend->elementBufferOffset += context.mtlEltCount * sizeof(mg_mtl_path_elt); + pathCount = 0; + context.mtlEltCount = 0; + context.elementBufferData = (mg_mtl_path_elt*)((char*)elementBufferData + backend->elementBufferOffset); + context.pathBufferData = (mg_mtl_path*)((char*)pathBufferData + backend->pathBufferOffset); + } + currentImage = primitive->attributes.image; + + if(primitive->path.count) + { + context.primitive = primitive; + context.pathIndex = pathCount; + context.pathScreenExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; + context.pathUserExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; + + if(primitive->cmd == MG_CMD_STROKE) + { + mg_mtl_render_stroke(&context, pathElements + primitive->path.startIndex, &primitive->path); + } + else + { + int segCount = 0; + for(int eltIndex = 0; + (eltIndex < primitive->path.count) && (primitive->path.startIndex + eltIndex < eltCount); + eltIndex++) + { + context.localEltIndex = segCount; + + mg_path_elt* elt = &pathElements[primitive->path.startIndex + eltIndex]; + + if(elt->type != MG_PATH_MOVE) + { + vec2 p[4] = {currentPos, elt->p[0], elt->p[1], elt->p[2]}; + mg_mtl_canvas_encode_element(&context, elt->type, p); + segCount++; + } + switch(elt->type) + { + case MG_PATH_MOVE: + currentPos = elt->p[0]; + break; + + case MG_PATH_LINE: + currentPos = elt->p[0]; + break; + + case MG_PATH_QUADRATIC: + currentPos = elt->p[1]; + break; + + case MG_PATH_CUBIC: + currentPos = elt->p[2]; + break; + } + } + } + //NOTE: push path + mg_mtl_path* path = &context.pathBufferData[pathCount]; + pathCount++; + + path->cmd = (mg_mtl_cmd)primitive->cmd; + + path->box = (vector_float4){context.pathScreenExtents.x, + context.pathScreenExtents.y, + context.pathScreenExtents.z, + context.pathScreenExtents.w}; + + path->clip = (vector_float4){primitive->attributes.clip.x, + primitive->attributes.clip.y, + primitive->attributes.clip.x + primitive->attributes.clip.w, + primitive->attributes.clip.y + primitive->attributes.clip.h}; + + path->color = (vector_float4){primitive->attributes.color.r, + primitive->attributes.color.g, + primitive->attributes.color.b, + primitive->attributes.color.a}; + + mp_rect srcRegion = primitive->attributes.srcRegion; + + mp_rect destRegion = {context.pathUserExtents.x, + context.pathUserExtents.y, + context.pathUserExtents.z - context.pathUserExtents.x, + context.pathUserExtents.w - context.pathUserExtents.y}; + + if(!mg_image_is_nil(primitive->attributes.image)) + { + vec2 texSize = mg_image_size(primitive->attributes.image); + + mg_mat2x3 srcRegionToImage = {1/texSize.x, 0, srcRegion.x/texSize.x, + 0, 1/texSize.y, srcRegion.y/texSize.y}; + + mg_mat2x3 destRegionToSrcRegion = {srcRegion.w/destRegion.w, 0, 0, + 0, srcRegion.h/destRegion.h, 0}; + + mg_mat2x3 userToDestRegion = {1, 0, -destRegion.x, + 0, 1, -destRegion.y}; + + mg_mat2x3 screenToUser = mg_mat2x3_inv(primitive->attributes.transform); + + mg_mat2x3 uvTransform = srcRegionToImage; + uvTransform = mg_mat2x3_mul_m(uvTransform, destRegionToSrcRegion); + uvTransform = mg_mat2x3_mul_m(uvTransform, userToDestRegion); + uvTransform = mg_mat2x3_mul_m(uvTransform, screenToUser); + + path->uvTransform = simd_matrix(simd_make_float3(uvTransform.m[0]/scale, uvTransform.m[3]/scale, 0), + simd_make_float3(uvTransform.m[1]/scale, uvTransform.m[4]/scale, 0), + simd_make_float3(uvTransform.m[2], uvTransform.m[5], 1)); + } + } + } + + mg_image_data* imageData = mg_image_data_from_handle(currentImage); + mg_mtl_render_batch(backend, + surface, + pathCount, + context.mtlEltCount, + imageData, + tileSize, + nTilesX, + nTilesY, + viewportSize, + scale); + + @autoreleasepool + { + //NOTE: finalize + [surface->commandBuffer addCompletedHandler:^(id commandBuffer) + { + mg_mtl_print_log(backend->bufferIndex, backend->logBuffer[backend->bufferIndex], backend->logOffsetBuffer[backend->bufferIndex]); + dispatch_semaphore_signal(backend->bufferSemaphore); + }]; + } +} + +void mg_mtl_canvas_destroy(mg_canvas_backend* interface) +{ + mg_mtl_canvas_backend* backend = (mg_mtl_canvas_backend*)interface; + + @autoreleasepool + { + [backend->pathPipeline release]; + [backend->segmentPipeline release]; + [backend->backpropPipeline release]; + [backend->mergePipeline release]; + [backend->rasterPipeline release]; + [backend->blitPipeline release]; + + for(int i=0; ipathBuffer[i] release]; + [backend->elementBuffer[i] release]; + [backend->logBuffer[i] release]; + [backend->logOffsetBuffer[i] release]; + } + [backend->segmentCountBuffer release]; + [backend->segmentBuffer release]; + [backend->tileQueueBuffer release]; + [backend->tileQueueCountBuffer release]; + [backend->tileOpBuffer release]; + [backend->tileOpCountBuffer release]; + [backend->screenTilesBuffer release]; + } + + free(backend); +} + +mg_image_data* mg_mtl_canvas_image_create(mg_canvas_backend* interface, vec2 size) +{ + mg_mtl_image_data* image = 0; + mg_mtl_canvas_backend* backend = (mg_mtl_canvas_backend*)interface; + mg_mtl_surface* surface = backend->surface; + + @autoreleasepool + { + image = malloc_type(mg_mtl_image_data); + if(image) + { + MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; + texDesc.textureType = MTLTextureType2D; + texDesc.storageMode = MTLStorageModeManaged; + texDesc.usage = MTLTextureUsageShaderRead; + texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; + texDesc.width = size.x; + texDesc.height = size.y; + + image->texture = [surface->device newTextureWithDescriptor:texDesc]; + if(image->texture != nil) + { + [image->texture retain]; + image->interface.size = size; + } + else + { + free(image); + image = 0; + } + } + } + return((mg_image_data*)image); +} + +void mg_mtl_canvas_image_destroy(mg_canvas_backend* backendInterface, mg_image_data* imageInterface) +{ + mg_mtl_image_data* image = (mg_mtl_image_data*)imageInterface; + @autoreleasepool + { + [image->texture release]; + free(image); + } +} + +void mg_mtl_canvas_image_upload_region(mg_canvas_backend* backendInterface, mg_image_data* imageInterface, mp_rect region, u8* pixels) +{@autoreleasepool{ + mg_mtl_image_data* image = (mg_mtl_image_data*)imageInterface; + MTLRegion mtlRegion = MTLRegionMake2D(region.x, region.y, region.w, region.h); + [image->texture replaceRegion:mtlRegion + mipmapLevel:0 + withBytes:(void*)pixels + bytesPerRow: 4 * region.w]; +}} + +const u32 MG_MTL_PATH_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_path), + MG_MTL_ELEMENT_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_path_elt), + MG_MTL_SEGMENT_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_segment), + MG_MTL_PATH_QUEUE_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_path_queue), + MG_MTL_TILE_QUEUE_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_tile_queue), + MG_MTL_TILE_OP_BUFFER_SIZE = (4<<20)*sizeof(mg_mtl_tile_op); + +mg_canvas_backend* mtl_canvas_backend_create(mg_mtl_surface* surface) +{ + mg_mtl_canvas_backend* backend = 0; + + backend = malloc_type(mg_mtl_canvas_backend); + memset(backend, 0, sizeof(mg_mtl_canvas_backend)); + + backend->msaaCount = MG_MTL_MSAA_COUNT; + backend->surface = surface; + + //NOTE(martin): setup interface functions + backend->interface.destroy = mg_mtl_canvas_destroy; + backend->interface.render = mg_mtl_canvas_render; + backend->interface.imageCreate = mg_mtl_canvas_image_create; + backend->interface.imageDestroy = mg_mtl_canvas_image_destroy; + backend->interface.imageUploadRegion = mg_mtl_canvas_image_upload_region; + + @autoreleasepool{ + //NOTE: load metal library + str8 shaderPath = mp_app_get_resource_path(mem_scratch(), "mtl_renderer.metallib"); + NSString* metalFileName = [[NSString alloc] initWithBytes: shaderPath.ptr length:shaderPath.len encoding: NSUTF8StringEncoding]; + NSError* err = 0; + id library = [surface->device newLibraryWithFile: metalFileName error:&err]; + if(err != nil) + { + const char* errStr = [[err localizedDescription] UTF8String]; + log_error("error : %s\n", errStr); + return(0); + } + id pathFunction = [library newFunctionWithName:@"mtl_path_setup"]; + id segmentFunction = [library newFunctionWithName:@"mtl_segment_setup"]; + id backpropFunction = [library newFunctionWithName:@"mtl_backprop"]; + id mergeFunction = [library newFunctionWithName:@"mtl_merge"]; + id rasterFunction = [library newFunctionWithName:@"mtl_raster"]; + id vertexFunction = [library newFunctionWithName:@"mtl_vertex_shader"]; + id fragmentFunction = [library newFunctionWithName:@"mtl_fragment_shader"]; + + //NOTE: create pipelines + NSError* error = NULL; + + backend->pathPipeline = [surface->device newComputePipelineStateWithFunction: pathFunction + error:&error]; + + backend->segmentPipeline = [surface->device newComputePipelineStateWithFunction: segmentFunction + error:&error]; + + backend->backpropPipeline = [surface->device newComputePipelineStateWithFunction: backpropFunction + error:&error]; + + backend->mergePipeline = [surface->device newComputePipelineStateWithFunction: mergeFunction + error:&error]; + + backend->rasterPipeline = [surface->device newComputePipelineStateWithFunction: rasterFunction + error:&error]; + + MTLRenderPipelineDescriptor *pipelineStateDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipelineStateDescriptor.label = @"blit pipeline"; + pipelineStateDescriptor.vertexFunction = vertexFunction; + pipelineStateDescriptor.fragmentFunction = fragmentFunction; + pipelineStateDescriptor.colorAttachments[0].pixelFormat = surface->mtlLayer.pixelFormat; + pipelineStateDescriptor.colorAttachments[0].blendingEnabled = YES; + pipelineStateDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; + pipelineStateDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorOne; + pipelineStateDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + pipelineStateDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; + pipelineStateDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne; + pipelineStateDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + + backend->blitPipeline = [surface->device newRenderPipelineStateWithDescriptor: pipelineStateDescriptor error:&err]; + + //NOTE: create textures + mp_rect frame = surface->interface.getFrame((mg_surface_data*)surface); + f32 scale = surface->mtlLayer.contentsScale; + + backend->frameSize = (vec2){frame.w*scale, frame.h*scale}; + + MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; + texDesc.textureType = MTLTextureType2D; + texDesc.storageMode = MTLStorageModePrivate; + texDesc.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite; + texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; + texDesc.width = backend->frameSize.x; + texDesc.height = backend->frameSize.y; + + backend->outTexture = [surface->device newTextureWithDescriptor:texDesc]; + + //NOTE: create buffers + + backend->bufferSemaphore = dispatch_semaphore_create(MG_MTL_INPUT_BUFFERS_COUNT); + backend->bufferIndex = 0; + + MTLResourceOptions bufferOptions = MTLResourceCPUCacheModeWriteCombined + | MTLResourceStorageModeShared; + + for(int i=0; ipathBuffer[i] = [surface->device newBufferWithLength: MG_MTL_PATH_BUFFER_SIZE + options: bufferOptions]; + + backend->elementBuffer[i] = [surface->device newBufferWithLength: MG_MTL_ELEMENT_BUFFER_SIZE + options: bufferOptions]; + } + + bufferOptions = MTLResourceStorageModePrivate; + backend->segmentBuffer = [surface->device newBufferWithLength: MG_MTL_SEGMENT_BUFFER_SIZE + options: bufferOptions]; + + backend->segmentCountBuffer = [surface->device newBufferWithLength: sizeof(int) + options: bufferOptions]; + + backend->pathQueueBuffer = [surface->device newBufferWithLength: MG_MTL_PATH_QUEUE_BUFFER_SIZE + options: bufferOptions]; + + backend->tileQueueBuffer = [surface->device newBufferWithLength: MG_MTL_TILE_QUEUE_BUFFER_SIZE + options: bufferOptions]; + + backend->tileQueueCountBuffer = [surface->device newBufferWithLength: sizeof(int) + options: bufferOptions]; + + backend->tileOpBuffer = [surface->device newBufferWithLength: MG_MTL_TILE_OP_BUFFER_SIZE + options: bufferOptions]; + + backend->tileOpCountBuffer = [surface->device newBufferWithLength: sizeof(int) + options: bufferOptions]; + + int tileSize = MG_MTL_TILE_SIZE; + int nTilesX = (int)(frame.w * scale + tileSize - 1)/tileSize; + int nTilesY = (int)(frame.h * scale + tileSize - 1)/tileSize; + backend->screenTilesBuffer = [surface->device newBufferWithLength: nTilesX*nTilesY*sizeof(int) + options: bufferOptions]; + + bufferOptions = MTLResourceStorageModeShared; + for(int i=0; ilogBuffer[i] = [surface->device newBufferWithLength: 1<<20 + options: bufferOptions]; + + backend->logOffsetBuffer[i] = [surface->device newBufferWithLength: sizeof(int) + options: bufferOptions]; + } + } + return((mg_canvas_backend*)backend); +} + +mg_surface_data* mtl_canvas_surface_create_for_window(mp_window window) +{ + mg_mtl_surface* surface = (mg_mtl_surface*)mg_mtl_surface_create_for_window(window); + + if(surface) + { + surface->interface.backend = mtl_canvas_backend_create(surface); + if(surface->interface.backend) + { + surface->interface.api = MG_CANVAS; + } + else + { + surface->interface.destroy((mg_surface_data*)surface); + surface = 0; + } + } + return((mg_surface_data*)surface); +} diff --git a/src/platform/platform_math.h b/src/platform/platform_math.h index f54d02c..442fd48 100644 --- a/src/platform/platform_math.h +++ b/src/platform/platform_math.h @@ -1,26 +1,27 @@ -/************************************************************//** -* -* @file: platform_math.h -* @author: Martin Fouilleul -* @date: 26/04/2023 -* -*****************************************************************/ -#ifndef __PLATFORM_MATH_H_ -#define __PLATFORM_MATH_H_ - -#include"platform.h" - -#if !PLATFORM_ORCA - #include -#else - -#define M_PI 3.14159265358979323846 - -double fabs(double x); -double sqrt(double sqrt); -double cos(double x); -double sin(double x); - -#endif - -#endif //__PLATFORM_MATH_H_ +/************************************************************//** +* +* @file: platform_math.h +* @author: Martin Fouilleul +* @date: 26/04/2023 +* +*****************************************************************/ +#ifndef __PLATFORM_MATH_H_ +#define __PLATFORM_MATH_H_ + +#include"platform.h" + +#if !PLATFORM_ORCA + #define _USE_MATH_DEFINES //NOTE: necessary for MSVC + #include +#else + +#define M_PI 3.14159265358979323846 + +double fabs(double x); +double sqrt(double sqrt); +double cos(double x); +double sin(double x); + +#endif + +#endif //__PLATFORM_MATH_H_ diff --git a/src/ui.c b/src/ui.c index 70d671f..fd13c1a 100644 --- a/src/ui.c +++ b/src/ui.c @@ -1,2814 +1,2818 @@ -/************************************************************//** -* -* @file: ui.c -* @author: Martin Fouilleul -* @date: 08/08/2022 -* @revision: -* -*****************************************************************/ -#include"platform.h" -#include"platform_assert.h" -#include"memory.h" -#include"hash.h" -#include"platform_clock.h" -#include"ui.h" - -static ui_style UI_STYLE_DEFAULTS = -{ - .size.width = {.kind = UI_SIZE_CHILDREN, - .value = 0, - .relax = 0}, - .size.height = {.kind = UI_SIZE_CHILDREN, - .value = 0, - .relax = 0}, - - .layout = {.axis = UI_AXIS_Y, - .align = {UI_ALIGN_START, - UI_ALIGN_START}}, - .color = {0, 0, 0, 1}, - .fontSize = 16, -}; - -mp_thread_local ui_context __uiThreadContext = {0}; -mp_thread_local ui_context* __uiCurrentContext = 0; - -ui_context* ui_get_context(void) -{ - return(__uiCurrentContext); -} - -void ui_set_context(ui_context* context) -{ - __uiCurrentContext = context; -} - -//----------------------------------------------------------------------------- -// stacks -//----------------------------------------------------------------------------- -ui_stack_elt* ui_stack_push(ui_context* ui, ui_stack_elt** stack) -{ - ui_stack_elt* elt = mem_arena_alloc_type(&ui->frameArena, ui_stack_elt); - memset(elt, 0, sizeof(ui_stack_elt)); - elt->parent = *stack; - *stack = elt; - return(elt); -} - -void ui_stack_pop(ui_stack_elt** stack) -{ - if(*stack) - { - *stack = (*stack)->parent; - } - else - { - log_error("ui stack underflow\n"); - } -} - -mp_rect ui_intersect_rects(mp_rect lhs, mp_rect rhs) -{ - //NOTE(martin): intersect with current clip - f32 x0 = maximum(lhs.x, rhs.x); - f32 y0 = maximum(lhs.y, rhs.y); - f32 x1 = minimum(lhs.x + lhs.w, rhs.x + rhs.w); - f32 y1 = minimum(lhs.y + lhs.h, rhs.y + rhs.h); - mp_rect r = {x0, y0, maximum(0, x1-x0), maximum(0, y1-y0)}; - return(r); -} - -mp_rect ui_clip_top(void) -{ - mp_rect r = {-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX}; - ui_context* ui = ui_get_context(); - ui_stack_elt* elt = ui->clipStack; - if(elt) - { - r = elt->clip; - } - return(r); -} - -void ui_clip_push(mp_rect clip) -{ - ui_context* ui = ui_get_context(); - mp_rect current = ui_clip_top(); - ui_stack_elt* elt = ui_stack_push(ui, &ui->clipStack); - elt->clip = ui_intersect_rects(current, clip); -} - -void ui_clip_pop(void) -{ - ui_context* ui = ui_get_context(); - ui_stack_pop(&ui->clipStack); -} - -ui_box* ui_box_top(void) -{ - ui_context* ui = ui_get_context(); - ui_stack_elt* elt = ui->boxStack; - ui_box* box = elt ? elt->box : 0; - return(box); -} - -void ui_box_push(ui_box* box) -{ - ui_context* ui = ui_get_context(); - ui_stack_elt* elt = ui_stack_push(ui, &ui->boxStack); - elt->box = box; - if(box->flags & UI_FLAG_CLIP) - { - ui_clip_push(box->rect); - } -} - -void ui_box_pop(void) -{ - ui_context* ui = ui_get_context(); - ui_box* box = ui_box_top(); - if(box) - { - if(box->flags & UI_FLAG_CLIP) - { - ui_clip_pop(); - } - ui_stack_pop(&ui->boxStack); - } -} - -//----------------------------------------------------------------------------- -// tagging -//----------------------------------------------------------------------------- - -ui_tag ui_tag_make_str8(str8 string) -{ - ui_tag tag = {.hash = mp_hash_xx64_string(string)}; - return(tag); -} - -void ui_tag_box_str8(ui_box* box, str8 string) -{ - ui_context* ui = ui_get_context(); - ui_tag_elt* elt = mem_arena_alloc_type(&ui->frameArena, ui_tag_elt); - elt->tag = ui_tag_make_str8(string); - list_append(&box->tags, &elt->listElt); -} - -void ui_tag_next_str8(str8 string) -{ - ui_context* ui = ui_get_context(); - ui_tag_elt* elt = mem_arena_alloc_type(&ui->frameArena, ui_tag_elt); - elt->tag = ui_tag_make_str8(string); - list_append(&ui->nextBoxTags, &elt->listElt); -} - -//----------------------------------------------------------------------------- -// key hashing and caching -//----------------------------------------------------------------------------- -ui_key ui_key_make_str8(str8 string) -{ - ui_context* ui = ui_get_context(); - u64 seed = 0; - ui_box* parent = ui_box_top(); - if(parent) - { - seed = parent->key.hash; - } - - ui_key key = {0}; - key.hash = mp_hash_xx64_string_seed(string, seed); - return(key); -} - -ui_key ui_key_make_path(str8_list path) -{ - ui_context* ui = ui_get_context(); - u64 seed = 0; - ui_box* parent = ui_box_top(); - if(parent) - { - seed = parent->key.hash; - } - for_list(&path.list, elt, str8_elt, listElt) - { - seed = mp_hash_xx64_string_seed(elt->string, seed); - } - ui_key key = {seed}; - return(key); -} - -bool ui_key_equal(ui_key a, ui_key b) -{ - return(a.hash == b.hash); -} - -void ui_box_cache(ui_context* ui, ui_box* box) -{ - u64 index = box->key.hash & (UI_BOX_MAP_BUCKET_COUNT-1); - list_append(&(ui->boxMap[index]), &box->bucketElt); -} - -ui_box* ui_box_lookup_key(ui_key key) -{ - ui_context* ui = ui_get_context(); - u64 index = key.hash & (UI_BOX_MAP_BUCKET_COUNT-1); - - for_list(&ui->boxMap[index], box, ui_box, bucketElt) - { - if(ui_key_equal(key, box->key)) - { - return(box); - } - } - return(0); -} - -ui_box* ui_box_lookup_str8(str8 string) -{ - ui_key key = ui_key_make_str8(string); - return(ui_box_lookup_key(key)); -} - -//----------------------------------------------------------------------------- -// styling -//----------------------------------------------------------------------------- - -void ui_pattern_push(mem_arena* arena, ui_pattern* pattern, ui_selector selector) -{ - ui_selector* copy = mem_arena_alloc_type(arena, ui_selector); - *copy = selector; - list_append(&pattern->l, ©->listElt); -} - -ui_pattern ui_pattern_all(void) -{ - ui_context* ui = ui_get_context(); - ui_pattern pattern = {0}; - ui_pattern_push(&ui->frameArena, &pattern, (ui_selector){.kind = UI_SEL_ANY}); - return(pattern); -} - -ui_pattern ui_pattern_owner(void) -{ - ui_context* ui = ui_get_context(); - ui_pattern pattern = {0}; - ui_pattern_push(&ui->frameArena, &pattern, (ui_selector){.kind = UI_SEL_OWNER}); - return(pattern); -} - -void ui_style_match_before(ui_pattern pattern, ui_style* style, ui_style_mask mask) -{ - ui_context* ui = ui_get_context(); - if(ui) - { - ui_style_rule* rule = mem_arena_alloc_type(&ui->frameArena, ui_style_rule); - rule->pattern = pattern; - rule->mask = mask; - rule->style = mem_arena_alloc_type(&ui->frameArena, ui_style); - *rule->style = *style; - - list_append(&ui->nextBoxBeforeRules, &rule->boxElt); - } -} - -void ui_style_match_after(ui_pattern pattern, ui_style* style, ui_style_mask mask) -{ - ui_context* ui = ui_get_context(); - if(ui) - { - ui_style_rule* rule = mem_arena_alloc_type(&ui->frameArena, ui_style_rule); - rule->pattern = pattern; - rule->mask = mask; - rule->style = mem_arena_alloc_type(&ui->frameArena, ui_style); - *rule->style = *style; - - list_append(&ui->nextBoxAfterRules, &rule->boxElt); - } -} - -void ui_style_next(ui_style* style, ui_style_mask mask) -{ - ui_style_match_before(ui_pattern_owner(), style, mask); -} - -void ui_style_box_before(ui_box* box, ui_pattern pattern, ui_style* style, ui_style_mask mask) -{ - ui_context* ui = ui_get_context(); - if(ui) - { - ui_style_rule* rule = mem_arena_alloc_type(&ui->frameArena, ui_style_rule); - rule->pattern = pattern; - rule->mask = mask; - rule->style = mem_arena_alloc_type(&ui->frameArena, ui_style); - *rule->style = *style; - - list_append(&box->beforeRules, &rule->boxElt); - rule->owner = box; - } -} - -void ui_style_box_after(ui_box* box, ui_pattern pattern, ui_style* style, ui_style_mask mask) -{ - ui_context* ui = ui_get_context(); - if(ui) - { - ui_style_rule* rule = mem_arena_alloc_type(&ui->frameArena, ui_style_rule); - rule->pattern = pattern; - rule->mask = mask; - rule->style = mem_arena_alloc_type(&ui->frameArena, ui_style); - *rule->style = *style; - - list_append(&box->afterRules, &rule->boxElt); - rule->owner = box; - } -} - -//----------------------------------------------------------------------------- -// input -//----------------------------------------------------------------------------- - -void ui_process_event(mp_event* event) -{ - ui_context* ui = ui_get_context(); - mp_input_process_event(&ui->input, event); -} - -vec2 ui_mouse_position(void) -{ - ui_context* ui = ui_get_context(); - vec2 mousePos = mp_mouse_position(&ui->input); - return(mousePos); -} - -vec2 ui_mouse_delta(void) -{ - ui_context* ui = ui_get_context(); - vec2 delta = mp_mouse_delta(&ui->input); - return(delta); -} - -vec2 ui_mouse_wheel(void) -{ - ui_context* ui = ui_get_context(); - vec2 delta = mp_mouse_wheel(&ui->input); - return(delta); -} - -//----------------------------------------------------------------------------- -// ui boxes -//----------------------------------------------------------------------------- - -bool ui_rect_hit(mp_rect r, vec2 p) -{ - return( (p.x > r.x) - &&(p.x < r.x + r.w) - &&(p.y > r.y) - &&(p.y < r.y + r.h)); -} - -bool ui_box_hovering(ui_box* box, vec2 p) -{ - ui_context* ui = ui_get_context(); - - mp_rect clip = ui_clip_top(); - mp_rect rect = ui_intersect_rects(clip, box->rect); - bool hit = ui_rect_hit(rect, p); - bool result = hit && (!ui->hovered || box->z >= ui->hovered->z); - return(result); -} - -ui_box* ui_box_make_str8(str8 string, ui_flags flags) -{ - ui_context* ui = ui_get_context(); - - ui_key key = ui_key_make_str8(string); - ui_box* box = ui_box_lookup_key(key); - - if(!box) - { - box = mem_pool_alloc_type(&ui->boxPool, ui_box); - memset(box, 0, sizeof(ui_box)); - - box->key = key; - box->fresh = true; - ui_box_cache(ui, box); - } - else - { - box->fresh = false; - } - - //NOTE: setup hierarchy - if(box->frameCounter != ui->frameCounter) - { - list_init(&box->children); - box->parent = ui_box_top(); - if(box->parent) - { - list_append(&box->parent->children, &box->listElt); - box->parentClosed = box->parent->closed || box->parent->parentClosed; - } - - if(box->flags & UI_FLAG_OVERLAY) - { - list_append(&ui->overlayList, &box->overlayElt); - } - } - else - { - //maybe this should be a warning that we're trying to make the box twice in the same frame? - log_warning("trying to make ui box '%.*s' multiple times in the same frame\n", (int)box->string.len, box->string.ptr); - } - - //NOTE: setup per-frame state - box->frameCounter = ui->frameCounter; - box->string = str8_push_copy(&ui->frameArena, string); - box->flags = flags; - - //NOTE: create style and setup non-inherited attributes to default values - box->targetStyle = mem_arena_alloc_type(&ui->frameArena, ui_style); - ui_apply_style_with_mask(box->targetStyle, &UI_STYLE_DEFAULTS, ~0ULL); - - //NOTE: set tags, before rules and last box - box->tags = ui->nextBoxTags; - ui->nextBoxTags = (list_info){0}; - - box->beforeRules = ui->nextBoxBeforeRules; - for_list(&box->beforeRules, rule, ui_style_rule, boxElt) - { - rule->owner = box; - } - ui->nextBoxBeforeRules = (list_info){0}; - - box->afterRules = ui->nextBoxAfterRules; - for_list(&box->afterRules, rule, ui_style_rule, boxElt) - { - rule->owner = box; - } - ui->nextBoxAfterRules = (list_info){0}; - - - //NOTE: set scroll - vec2 wheel = ui_mouse_wheel(); - if(box->flags & UI_FLAG_SCROLL_WHEEL_X) - { - box->scroll.x += wheel.x; - } - if(box->flags & UI_FLAG_SCROLL_WHEEL_Y) - { - box->scroll.y += wheel.y; - } - - return(box); -} - -ui_box* ui_box_begin_str8(str8 string, ui_flags flags) -{ - ui_context* ui = ui_get_context(); - ui_box* box = ui_box_make_str8(string, flags); - ui_box_push(box); - return(box); -} - -ui_box* ui_box_end(void) -{ - ui_context* ui = ui_get_context(); - ui_box* box = ui_box_top(); - DEBUG_ASSERT(box, "box stack underflow"); - - ui_box_pop(); - - return(box); -} - -void ui_box_set_draw_proc(ui_box* box, ui_box_draw_proc proc, void* data) -{ - box->drawProc = proc; - box->drawData = data; -} - -void ui_box_set_closed(ui_box* box, bool closed) -{ - box->closed = closed; -} - -bool ui_box_closed(ui_box* box) -{ - return(box->closed); -} - -void ui_box_activate(ui_box* box) -{ - box->active = true; -} - -void ui_box_deactivate(ui_box* box) -{ - box->active = false; -} - -bool ui_box_active(ui_box* box) -{ - return(box->active); -} - -void ui_box_set_hot(ui_box* box, bool hot) -{ - box->hot = hot; -} - -bool ui_box_hot(ui_box* box) -{ - return(box->hot); -} - -ui_sig ui_box_sig(ui_box* box) -{ - //NOTE: compute input signals - ui_sig sig = {0}; - - ui_context* ui = ui_get_context(); - mp_input_state* input = &ui->input; - - sig.box = box; - - if(!box->closed && !box->parentClosed) - { - vec2 mousePos = ui_mouse_position(); - - sig.hovering = ui_box_hovering(box, mousePos); - - if(box->flags & UI_FLAG_CLICKABLE) - { - if(sig.hovering) - { - sig.pressed = mp_mouse_pressed(input, MP_MOUSE_LEFT); - if(sig.pressed) - { - box->dragging = true; - } - sig.doubleClicked = mp_mouse_double_clicked(input, MP_MOUSE_LEFT); - sig.rightPressed = mp_mouse_pressed(input, MP_MOUSE_RIGHT); - } - - sig.released = mp_mouse_released(input, MP_MOUSE_LEFT); - if(sig.released) - { - if(box->dragging && sig.hovering) - { - sig.clicked = true; - } - } - - if(!mp_mouse_down(input, MP_MOUSE_LEFT)) - { - box->dragging = false; - } - - sig.dragging = box->dragging; - } - - sig.mouse = (vec2){mousePos.x - box->rect.x, mousePos.y - box->rect.y}; - sig.delta = ui_mouse_delta(); - sig.wheel = ui_mouse_wheel(); - } - return(sig); -} - -bool ui_box_hidden(ui_box* box) -{ - return(box->closed || box->parentClosed); -} - -//----------------------------------------------------------------------------- -// Auto-layout -//----------------------------------------------------------------------------- - -void ui_animate_f32(ui_context* ui, f32* value, f32 target, f32 animationTime) -{ - if( animationTime < 1e-6 - || fabs(*value - target) < 0.001) - { - *value = target; - } - else - { - - /*NOTE: - we use the euler approximation for df/dt = alpha(target - f) - the implicit form is f(t) = target*(1-e^(-alpha*t)) for the rising front, - and f(t) = e^(-alpha*t) for the falling front (e.g. classic RC circuit charge/discharge) - - Here we bake alpha = 1/tau = -ln(0.05)/tr, with tr the rise time to 95% of target - */ - f32 alpha = 3/animationTime; - f32 dt = ui->lastFrameDuration; - - *value += (target - *value)*alpha*dt; - } -} - -void ui_animate_color(ui_context* ui, mg_color* color, mg_color target, f32 animationTime) -{ - for(int i=0; i<4; i++) - { - ui_animate_f32(ui, &color->c[i], target.c[i], animationTime); - } -} - -void ui_animate_ui_size(ui_context* ui, ui_size* size, ui_size target, f32 animationTime) -{ - size->kind = target.kind; - ui_animate_f32(ui, &size->value, target.value, animationTime); - ui_animate_f32(ui, &size->relax, target.relax, animationTime); -} - -void ui_box_animate_style(ui_context* ui, ui_box* box) -{ - ui_style* targetStyle = box->targetStyle; - DEBUG_ASSERT(targetStyle); - - f32 animationTime = targetStyle->animationTime; - - //NOTE: interpolate based on transition values - ui_style_mask mask = box->targetStyle->animationMask; - - if(box->fresh) - { - box->style = *targetStyle; - } - else - { - if(mask & UI_STYLE_SIZE_WIDTH) - { - ui_animate_ui_size(ui, &box->style.size.c[UI_AXIS_X], targetStyle->size.c[UI_AXIS_X], animationTime); - } - else - { - box->style.size.c[UI_AXIS_X] = targetStyle->size.c[UI_AXIS_X]; - } - - if(mask & UI_STYLE_SIZE_HEIGHT) - { - ui_animate_ui_size(ui, &box->style.size.c[UI_AXIS_Y], targetStyle->size.c[UI_AXIS_Y], animationTime); - } - else - { - box->style.size.c[UI_AXIS_Y] = targetStyle->size.c[UI_AXIS_Y]; - } - - if(mask & UI_STYLE_COLOR) - { - ui_animate_color(ui, &box->style.color, targetStyle->color, animationTime); - } - else - { - box->style.color = targetStyle->color; - } - - - if(mask & UI_STYLE_BG_COLOR) - { - ui_animate_color(ui, &box->style.bgColor, targetStyle->bgColor, animationTime); - } - else - { - box->style.bgColor = targetStyle->bgColor; - } - - if(mask & UI_STYLE_BORDER_COLOR) - { - ui_animate_color(ui, &box->style.borderColor, targetStyle->borderColor, animationTime); - } - else - { - box->style.borderColor = targetStyle->borderColor; - } - - if(mask & UI_STYLE_FONT_SIZE) - { - ui_animate_f32(ui, &box->style.fontSize, targetStyle->fontSize, animationTime); - } - else - { - box->style.fontSize = targetStyle->fontSize; - } - - if(mask & UI_STYLE_BORDER_SIZE) - { - ui_animate_f32(ui, &box->style.borderSize, targetStyle->borderSize, animationTime); - } - else - { - box->style.borderSize = targetStyle->borderSize; - } - - if(mask & UI_STYLE_ROUNDNESS) - { - ui_animate_f32(ui, &box->style.roundness, targetStyle->roundness, animationTime); - } - else - { - box->style.roundness = targetStyle->roundness; - } - - //NOTE: float target is animated in compute rect - box->style.floatTarget = targetStyle->floatTarget; - - //TODO: non animatable attributes. use mask - box->style.layout = targetStyle->layout; - box->style.font = targetStyle->font; - } -} - -void ui_apply_style_with_mask(ui_style* dst, ui_style* src, ui_style_mask mask) -{ - if(mask & UI_STYLE_SIZE_WIDTH) - { - dst->size.c[UI_AXIS_X] = src->size.c[UI_AXIS_X]; - } - if(mask & UI_STYLE_SIZE_HEIGHT) - { - dst->size.c[UI_AXIS_Y] = src->size.c[UI_AXIS_Y]; - } - if(mask & UI_STYLE_LAYOUT_AXIS) - { - dst->layout.axis = src->layout.axis; - } - if(mask & UI_STYLE_LAYOUT_ALIGN_X) - { - dst->layout.align.x = src->layout.align.x; - } - if(mask & UI_STYLE_LAYOUT_ALIGN_Y) - { - dst->layout.align.y = src->layout.align.y; - } - if(mask & UI_STYLE_LAYOUT_SPACING) - { - dst->layout.spacing = src->layout.spacing; - } - if(mask & UI_STYLE_LAYOUT_MARGIN_X) - { - dst->layout.margin.x = src->layout.margin.x; - } - if(mask & UI_STYLE_LAYOUT_MARGIN_Y) - { - dst->layout.margin.y = src->layout.margin.y; - } - if(mask & UI_STYLE_FLOAT_X) - { - dst->floating.c[UI_AXIS_X] = src->floating.c[UI_AXIS_X]; - dst->floatTarget.x = src->floatTarget.x; - } - if(mask & UI_STYLE_FLOAT_Y) - { - dst->floating.c[UI_AXIS_Y] = src->floating.c[UI_AXIS_Y]; - dst->floatTarget.y = src->floatTarget.y; - } - if(mask & UI_STYLE_COLOR) - { - dst->color = src->color; - } - if(mask & UI_STYLE_BG_COLOR) - { - dst->bgColor = src->bgColor; - } - if(mask & UI_STYLE_BORDER_COLOR) - { - dst->borderColor = src->borderColor; - } - if(mask & UI_STYLE_BORDER_SIZE) - { - dst->borderSize = src->borderSize; - } - if(mask & UI_STYLE_ROUNDNESS) - { - dst->roundness = src->roundness; - } - if(mask & UI_STYLE_FONT) - { - dst->font = src->font; - } - if(mask & UI_STYLE_FONT_SIZE) - { - dst->fontSize = src->fontSize; - } - if(mask & UI_STYLE_ANIMATION_TIME) - { - dst->animationTime = src->animationTime; - } - if(mask & UI_STYLE_ANIMATION_MASK) - { - dst->animationMask = src->animationMask; - } -} - - -bool ui_style_selector_match(ui_box* box, ui_style_rule* rule, ui_selector* selector) -{ - bool res = false; - switch(selector->kind) - { - case UI_SEL_ANY: - res = true; - break; - - case UI_SEL_OWNER: - res = (box == rule->owner); - break; - - case UI_SEL_TEXT: - res = !str8_cmp(box->string, selector->text); - break; - - case UI_SEL_TAG: - { - for_list(&box->tags, elt, ui_tag_elt, listElt) - { - if(elt->tag.hash == selector->tag.hash) - { - res = true; - break; - } - } - } break; - - case UI_SEL_STATUS: - { - res = true; - if(selector->status & UI_HOVER) - { - res = res && ui_box_hovering(box, ui_mouse_position()); - } - if(selector->status & UI_ACTIVE) - { - res = res && box->active; - } - if(selector->status & UI_DRAGGING) - { - res = res && box->dragging; - } - } break; - - case UI_SEL_KEY: - res = ui_key_equal(box->key, selector->key); - default: - break; - } - return(res); -} - -void ui_style_rule_match(ui_context* ui, ui_box* box, ui_style_rule* rule, list_info* buildList, list_info* tmpList) -{ - ui_selector* selector = list_first_entry(&rule->pattern.l, ui_selector, listElt); - bool match = ui_style_selector_match(box, rule, selector); - - selector = list_next_entry(&rule->pattern.l, selector, ui_selector, listElt); - while(match && selector && selector->op == UI_SEL_AND) - { - match = match && ui_style_selector_match(box, rule, selector); - selector = list_next_entry(&rule->pattern.l, selector, ui_selector, listElt); - } - - if(match) - { - if(!selector) - { - ui_apply_style_with_mask(box->targetStyle, rule->style, rule->mask); - } - else - { - //NOTE create derived rule if there's more than one selector - ui_style_rule* derived = mem_arena_alloc_type(&ui->frameArena, ui_style_rule); - derived->mask = rule->mask; - derived->style = rule->style; - derived->pattern.l = (list_info){&selector->listElt, rule->pattern.l.last}; - - list_append(buildList, &derived->buildElt); - list_append(tmpList, &derived->tmpElt); - } - } -} - -void ui_styling_prepass(ui_context* ui, ui_box* box, list_info* before, list_info* after) -{ - //NOTE: inherit style from parent - if(box->parent) - { - ui_apply_style_with_mask(box->targetStyle, - box->parent->targetStyle, - UI_STYLE_MASK_INHERITED); - } - - - //NOTE: append box before rules to before and tmp - list_info tmpBefore = {0}; - for_list(&box->beforeRules, rule, ui_style_rule, boxElt) - { - list_append(before, &rule->buildElt); - list_append(&tmpBefore, &rule->tmpElt); - } - //NOTE: match before rules - for_list(before, rule, ui_style_rule, buildElt) - { - ui_style_rule_match(ui, box, rule, before, &tmpBefore); - } - - //NOTE: prepend box after rules to after and append them to tmp - list_info tmpAfter = {0}; - for_list_reverse(&box->afterRules, rule, ui_style_rule, boxElt) - { - list_push(after, &rule->buildElt); - list_append(&tmpAfter, &rule->tmpElt); - } - - //NOTE: match after rules - for_list(after, rule, ui_style_rule, buildElt) - { - ui_style_rule_match(ui, box, rule, after, &tmpAfter); - } - - //NOTE: compute static sizes - ui_box_animate_style(ui, box); - - if(ui_box_hidden(box)) - { - return; - } - - ui_style* style = &box->style; - - mp_rect textBox = {0}; - ui_size desiredSize[2] = {box->style.size.c[UI_AXIS_X], - box->style.size.c[UI_AXIS_Y]}; - - if( desiredSize[UI_AXIS_X].kind == UI_SIZE_TEXT - ||desiredSize[UI_AXIS_Y].kind == UI_SIZE_TEXT) - { - textBox = mg_text_bounding_box(style->font, style->fontSize, box->string); - } - - for(int i=0; ilayout.margin.c[i]; - box->rect.c[2+i] = textBox.c[2+i] + margin*2; - } - else if(size.kind == UI_SIZE_PIXELS) - { - box->rect.c[2+i] = size.value; - } - } - - //NOTE: descend in children - for_list(&box->children, child, ui_box, listElt) - { - ui_styling_prepass(ui, child, before, after); - } - - //NOTE: remove temporary rules - for_list(&tmpBefore, rule, ui_style_rule, tmpElt) - { - list_remove(before, &rule->buildElt); - } - for_list(&tmpAfter, rule, ui_style_rule, tmpElt) - { - list_remove(after, &rule->buildElt); - } -} - -bool ui_layout_downward_dependency(ui_box* child, int axis) -{ - return( !ui_box_hidden(child) - && !child->style.floating.c[axis] - && child->style.size.c[axis].kind != UI_SIZE_PARENT - && child->style.size.c[axis].kind != UI_SIZE_PARENT_MINUS_PIXELS); -} - -void ui_layout_downward_dependent_size(ui_context* ui, ui_box* box, int axis) -{ - //NOTE: layout children and compute spacing - f32 count = 0; - for_list(&box->children, child, ui_box, listElt) - { - if(!ui_box_hidden(child)) - { - ui_layout_downward_dependent_size(ui, child, axis); - - if( box->style.layout.axis == axis - && !child->style.floating.c[axis]) - { - count++; - } - } - } - box->spacing[axis] = maximum(0, count-1)*box->style.layout.spacing; - - ui_size* size = &box->style.size.c[axis]; - if(size->kind == UI_SIZE_CHILDREN) - { - //NOTE: if box is dependent on children, compute children's size. If we're in the layout - // axis this is the sum of each child size, otherwise it is the maximum child size - f32 sum = 0; - - if(box->style.layout.axis == axis) - { - for_list(&box->children, child, ui_box, listElt) - { - if(ui_layout_downward_dependency(child, axis)) - { - sum += child->rect.c[2+axis]; - } - } - } - else - { - for_list(&box->children, child, ui_box, listElt) - { - if(ui_layout_downward_dependency(child, axis)) - { - sum = maximum(sum, child->rect.c[2+axis]); - } - } - } - f32 margin = box->style.layout.margin.c[axis]; - box->rect.c[2+axis] = sum + box->spacing[axis] + 2*margin; - } -} - -void ui_layout_upward_dependent_size(ui_context* ui, ui_box* box, int axis) -{ - //NOTE: re-compute/set size of children that depend on box's size - - f32 margin = box->style.layout.margin.c[axis]; - f32 availableSize = maximum(0, box->rect.c[2+axis] - box->spacing[axis] - 2*margin); - - for_list(&box->children, child, ui_box, listElt) - { - ui_size* size = &child->style.size.c[axis]; - if(size->kind == UI_SIZE_PARENT) - { - child->rect.c[2+axis] = availableSize * size->value; - } - else if(size->kind == UI_SIZE_PARENT_MINUS_PIXELS) - { - child->rect.c[2+axis] = maximum(0, availableSize - size->value); - } - } - - //NOTE: solve downard conflicts - int overflowFlag = (UI_FLAG_ALLOW_OVERFLOW_X << axis); - f32 sum = 0; - - if(box->style.layout.axis == axis) - { - //NOTE: if we're solving in the layout axis, first compute total sum of children and - // total slack available - f32 slack = 0; - - for_list(&box->children, child, ui_box, listElt) - { - if( !ui_box_hidden(child) - && !child->style.floating.c[axis]) - { - sum += child->rect.c[2+axis]; - slack += child->rect.c[2+axis] * child->style.size.c[axis].relax; - } - } - - if(!(box->flags & overflowFlag)) - { - //NOTE: then remove excess proportionally to each box slack, and recompute children sum. - f32 totalContents = sum + box->spacing[axis] + 2*box->style.layout.margin.c[axis]; - f32 excess = ClampLowBound(totalContents - box->rect.c[2+axis], 0); - f32 alpha = Clamp(excess / slack, 0, 1); - - sum = 0; - for_list(&box->children, child, ui_box, listElt) - { - f32 relax = child->style.size.c[axis].relax; - child->rect.c[2+axis] -= alpha * child->rect.c[2+axis] * relax; - sum += child->rect.c[2+axis]; - } - } - } - else - { - //NOTE: if we're solving on the secondary axis, we remove excess to each box individually - // according to its own slack. Children sum is the maximum child size. - - for_list(&box->children, child, ui_box, listElt) - { - if(!ui_box_hidden(child) && !child->style.floating.c[axis]) - { - if(!(box->flags & overflowFlag)) - { - f32 totalContents = child->rect.c[2+axis] + 2*box->style.layout.margin.c[axis]; - f32 excess = ClampLowBound(totalContents - box->rect.c[2+axis], 0); - f32 relax = child->style.size.c[axis].relax; - child->rect.c[2+axis] -= minimum(excess, child->rect.c[2+axis]*relax); - } - sum = maximum(sum, child->rect.c[2+axis]); - } - } - } - - box->childrenSum[axis] = sum; - - //NOTE: recurse in children - for_list(&box->children, child, ui_box, listElt) - { - ui_layout_upward_dependent_size(ui, child, axis); - } -} - -void ui_layout_compute_rect(ui_context* ui, ui_box* box, vec2 pos) -{ - if(ui_box_hidden(box)) - { - return; - } - - box->rect.x = pos.x; - box->rect.y = pos.y; - box->z = ui->z; - ui->z++; - - ui_axis layoutAxis = box->style.layout.axis; - ui_axis secondAxis = (layoutAxis == UI_AXIS_X) ? UI_AXIS_Y : UI_AXIS_X; - f32 spacing = box->style.layout.spacing; - - ui_align* align = box->style.layout.align.c; - - vec2 origin = {box->rect.x, - box->rect.y}; - vec2 currentPos = origin; - - vec2 margin = {box->style.layout.margin.x, - box->style.layout.margin.y}; - - currentPos.x += margin.x; - currentPos.y += margin.y; - - for(int i=0; irect.c[2+i] - (box->childrenSum[i] + box->spacing[i] + margin.c[i]); - } - } - if(align[layoutAxis] == UI_ALIGN_CENTER) - { - currentPos.c[layoutAxis] = origin.c[layoutAxis] - + 0.5*(box->rect.c[2+layoutAxis] - - (box->childrenSum[layoutAxis] + box->spacing[layoutAxis])); - } - - currentPos.x -= box->scroll.x; - currentPos.y -= box->scroll.y; - - for_list(&box->children, child, ui_box, listElt) - { - if(align[secondAxis] == UI_ALIGN_CENTER) - { - currentPos.c[secondAxis] = origin.c[secondAxis] + 0.5*(box->rect.c[2+secondAxis] - child->rect.c[2+secondAxis]); - } - - vec2 childPos = currentPos; - for(int i=0; istyle.floating.c[i]) - { - ui_style* style = child->targetStyle; - if((child->targetStyle->animationMask & (UI_STYLE_FLOAT_X << i)) - && !child->fresh) - { - ui_animate_f32(ui, &child->floatPos.c[i], child->style.floatTarget.c[i], style->animationTime); - } - else - { - child->floatPos.c[i] = child->style.floatTarget.c[i]; - } - childPos.c[i] = origin.c[i] + child->floatPos.c[i]; - } - } - - ui_layout_compute_rect(ui, child, childPos); - - if(!child->style.floating.c[layoutAxis]) - { - currentPos.c[layoutAxis] += child->rect.c[2+layoutAxis] + spacing; - } - } -} - -void ui_layout_find_next_hovered_recursive(ui_context* ui, ui_box* box, vec2 p) -{ - if(ui_box_hidden(box)) - { - return; - } - - bool hit = ui_rect_hit(box->rect, p); - if(hit && (box->flags & UI_FLAG_BLOCK_MOUSE)) - { - ui->hovered = box; - } - if(hit || !(box->flags & UI_FLAG_CLIP)) - { - for_list(&box->children, child, ui_box, listElt) - { - ui_layout_find_next_hovered_recursive(ui, child, p); - } - } -} - -void ui_layout_find_next_hovered(ui_context* ui, vec2 p) -{ - ui->hovered = 0; - ui_layout_find_next_hovered_recursive(ui, ui->root, p); -} - -void ui_solve_layout(ui_context* ui) -{ - list_info beforeRules = {0}; - list_info afterRules = {0}; - - //NOTE: style and compute static sizes - ui_styling_prepass(ui, ui->root, &beforeRules, &afterRules); - - //NOTE: reparent overlay boxes - for_list(&ui->overlayList, box, ui_box, overlayElt) - { - if(box->parent) - { - list_remove(&box->parent->children, &box->listElt); - list_append(&ui->overlay->children, &box->listElt); - } - } - - //NOTE: compute layout - for(int axis=0; axisroot, axis); - ui_layout_upward_dependent_size(ui, ui->root, axis); - } - ui_layout_compute_rect(ui, ui->root, (vec2){0, 0}); - - vec2 p = ui_mouse_position(); - ui_layout_find_next_hovered(ui, p); -} - -//----------------------------------------------------------------------------- -// Drawing -//----------------------------------------------------------------------------- - -void ui_rectangle_fill(mp_rect rect, f32 roundness) -{ - if(roundness) - { - mg_rounded_rectangle_fill(rect.x, rect.y, rect.w, rect.h, roundness); - } - else - { - mg_rectangle_fill(rect.x, rect.y, rect.w, rect.h); - } -} - -void ui_rectangle_stroke(mp_rect rect, f32 roundness) -{ - if(roundness) - { - mg_rounded_rectangle_stroke(rect.x, rect.y, rect.w, rect.h, roundness); - } - else - { - mg_rectangle_stroke(rect.x, rect.y, rect.w, rect.h); - } -} - -void ui_draw_box(ui_box* box) -{ - if(ui_box_hidden(box)) - { - return; - } - - ui_style* style = &box->style; - - if(box->flags & UI_FLAG_CLIP) - { - mg_clip_push(box->rect.x, box->rect.y, box->rect.w, box->rect.h); - } - - if(box->flags & UI_FLAG_DRAW_BACKGROUND) - { - mg_set_color(style->bgColor); - ui_rectangle_fill(box->rect, style->roundness); - } - - if((box->flags & UI_FLAG_DRAW_PROC) && box->drawProc) - { - box->drawProc(box, box->drawData); - } - - for_list(&box->children, child, ui_box, listElt) - { - ui_draw_box(child); - } - - if(box->flags & UI_FLAG_DRAW_TEXT) - { - mp_rect textBox = mg_text_bounding_box(style->font, style->fontSize, box->string); - - f32 x = 0; - f32 y = 0; - switch(style->layout.align.x) - { - case UI_ALIGN_START: - x = box->rect.x + style->layout.margin.x; - break; - - case UI_ALIGN_END: - x = box->rect.x + box->rect.w - style->layout.margin.x - textBox.w; - break; - - case UI_ALIGN_CENTER: - x = box->rect.x + 0.5*(box->rect.w - textBox.w); - break; - } - - switch(style->layout.align.y) - { - case UI_ALIGN_START: - y = box->rect.y + style->layout.margin.y - textBox.y; - break; - - case UI_ALIGN_END: - y = box->rect.y + box->rect.h - style->layout.margin.y - textBox.h + textBox.y; - break; - - case UI_ALIGN_CENTER: - y = box->rect.y + 0.5*(box->rect.h - textBox.h) - textBox.y; - break; - } - - mg_set_font(style->font); - mg_set_font_size(style->fontSize); - mg_set_color(style->color); - - mg_move_to(x, y); - mg_text_outlines(box->string); - mg_fill(); - } - - if(box->flags & UI_FLAG_CLIP) - { - mg_clip_pop(); - } - - if(box->flags & UI_FLAG_DRAW_BORDER) - { - mg_set_width(style->borderSize); - mg_set_color(style->borderColor); - ui_rectangle_stroke(box->rect, style->roundness); - } -} - -void ui_draw() -{ - ui_context* ui = ui_get_context(); - - //NOTE: draw - bool oldTextFlip = mg_get_text_flip(); - mg_set_text_flip(false); - - ui_draw_box(ui->root); - - mg_set_text_flip(oldTextFlip); -} - -//----------------------------------------------------------------------------- -// frame begin/end -//----------------------------------------------------------------------------- - -void ui_begin_frame(vec2 size, ui_style* defaultStyle, ui_style_mask defaultMask) -{ - ui_context* ui = ui_get_context(); - - mem_arena_clear(&ui->frameArena); - - ui->frameCounter++; - f64 time = mp_get_time(MP_CLOCK_MONOTONIC); - ui->lastFrameDuration = time - ui->frameTime; - ui->frameTime = time; - - ui->clipStack = 0; - ui->z = 0; - - defaultMask &= UI_STYLE_COLOR - | UI_STYLE_BG_COLOR - | UI_STYLE_BORDER_COLOR - | UI_STYLE_FONT - | UI_STYLE_FONT_SIZE; - - ui_style_match_before(ui_pattern_all(), defaultStyle, defaultMask); - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, size.x}, - .size.height = {UI_SIZE_PIXELS, size.y}}, - UI_STYLE_SIZE); - - ui->root = ui_box_begin("_root_", 0); - - ui_style_mask contentStyleMask = UI_STYLE_SIZE - | UI_STYLE_LAYOUT - | UI_STYLE_FLOAT; - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, - .size.height = {UI_SIZE_PARENT, 1}, - .layout = {UI_AXIS_Y, UI_ALIGN_START, UI_ALIGN_START}, - .floating = {true, true}, - .floatTarget = {0, 0}}, - contentStyleMask); - - ui_box* contents = ui_box_make("_contents_", 0); - - ui_style_next(&(ui_style){.layout = {UI_AXIS_Y, UI_ALIGN_START, UI_ALIGN_START}, - .floating = {true, true}, - .floatTarget = {0, 0}}, - UI_STYLE_LAYOUT | UI_STYLE_FLOAT_X | UI_STYLE_FLOAT_Y); - - ui->overlay = ui_box_make("_overlay_", 0); - ui->overlayList = (list_info){0}; - - ui->nextBoxBeforeRules = (list_info){0}; - ui->nextBoxAfterRules = (list_info){0}; - ui->nextBoxTags = (list_info){0}; - - ui_box_push(contents); -} - -void ui_end_frame(void) -{ - ui_context* ui = ui_get_context(); - - ui_box_pop(); - - ui_box* box = ui_box_end(); - DEBUG_ASSERT(box == ui->root, "unbalanced box stack"); - - //TODO: check balancing of style stacks - - //NOTE: layout - ui_solve_layout(ui); - - //NOTE: prune unused boxes - for(int i=0; iboxMap[i], box, ui_box, bucketElt) - { - if(box->frameCounter < ui->frameCounter) - { - list_remove(&ui->boxMap[i], &box->bucketElt); - } - } - } - - mp_input_next_frame(&ui->input); -} - -//----------------------------------------------------------------------------- -// Init / cleanup -//----------------------------------------------------------------------------- -void ui_init(ui_context* ui) -{ - __uiCurrentContext = &__uiThreadContext; - - memset(ui, 0, sizeof(ui_context)); - mem_arena_init(&ui->frameArena); - mem_pool_init(&ui->boxPool, sizeof(ui_box)); - ui->init = true; - - ui_set_context(ui); -} - -void ui_cleanup(void) -{ - ui_context* ui = ui_get_context(); - mem_arena_release(&ui->frameArena); - mem_pool_release(&ui->boxPool); - ui->init = false; -} - - -//----------------------------------------------------------------------------- -// label -//----------------------------------------------------------------------------- - -ui_sig ui_label_str8(str8 label) -{ - ui_style_next(&(ui_style){.size.width = {UI_SIZE_TEXT, 0, 0}, - .size.height = {UI_SIZE_TEXT, 0, 0}}, - UI_STYLE_SIZE_WIDTH | UI_STYLE_SIZE_HEIGHT); - - ui_flags flags = UI_FLAG_CLIP - | UI_FLAG_DRAW_TEXT; - ui_box* box = ui_box_make_str8(label, flags); - - ui_sig sig = ui_box_sig(box); - return(sig); -} - -ui_sig ui_label(const char* label) -{ - return(ui_label_str8(STR8((char*)label))); -} - -//------------------------------------------------------------------------------ -// button -//------------------------------------------------------------------------------ - -ui_sig ui_button_behavior(ui_box* box) -{ - ui_sig sig = ui_box_sig(box); - - if(sig.hovering) - { - ui_box_set_hot(box, true); - if(sig.dragging) - { - ui_box_activate(box); - } - } - else - { - ui_box_set_hot(box, false); - } - if(!sig.dragging) - { - ui_box_deactivate(box); - } - return(sig); -} - -ui_sig ui_button_str8(str8 label) -{ - ui_context* ui = ui_get_context(); - - ui_style defaultStyle = {.size.width = {UI_SIZE_TEXT}, - .size.height = {UI_SIZE_TEXT}, - .layout.align.x = UI_ALIGN_CENTER, - .layout.align.y = UI_ALIGN_CENTER, - .layout.margin.x = 5, - .layout.margin.y = 5, - .bgColor = {0.5, 0.5, 0.5, 1}, - .borderColor = {0.2, 0.2, 0.2, 1}, - .borderSize = 1, - .roundness = 10}; - - ui_style_mask defaultMask = UI_STYLE_SIZE_WIDTH - | UI_STYLE_SIZE_HEIGHT - | UI_STYLE_LAYOUT_MARGIN_X - | UI_STYLE_LAYOUT_MARGIN_Y - | UI_STYLE_LAYOUT_ALIGN_X - | UI_STYLE_LAYOUT_ALIGN_Y - | UI_STYLE_BG_COLOR - | UI_STYLE_BORDER_COLOR - | UI_STYLE_BORDER_SIZE - | UI_STYLE_ROUNDNESS; - - ui_style_next(&defaultStyle, defaultMask); - - ui_style activeStyle = {.bgColor = {0.3, 0.3, 0.3, 1}, - .borderColor = {0.2, 0.2, 0.2, 1}, - .borderSize = 2}; - ui_style_mask activeMask = UI_STYLE_BG_COLOR - | UI_STYLE_BORDER_COLOR - | UI_STYLE_BORDER_SIZE; - ui_pattern activePattern = {0}; - ui_pattern_push(&ui->frameArena, - &activePattern, - (ui_selector){.kind = UI_SEL_STATUS, - .status = UI_ACTIVE|UI_HOVER}); - ui_style_match_before(activePattern, &activeStyle, activeMask); - - ui_flags flags = UI_FLAG_CLICKABLE - | UI_FLAG_CLIP - | UI_FLAG_DRAW_BACKGROUND - | UI_FLAG_DRAW_BORDER - | UI_FLAG_DRAW_TEXT - | UI_FLAG_HOT_ANIMATION - | UI_FLAG_ACTIVE_ANIMATION; - - ui_box* box = ui_box_make_str8(label, flags); - ui_tag_box(box, "button"); - - ui_sig sig = ui_button_behavior(box); - return(sig); -} - -ui_sig ui_button(const char* label) -{ - return(ui_button_str8(STR8((char*)label))); -} - -void ui_checkbox_draw(ui_box* box, void* data) -{ - bool checked = *(bool*)data; - if(checked) - { - mg_move_to(box->rect.x + 0.2*box->rect.w, box->rect.y + 0.5*box->rect.h); - mg_line_to(box->rect.x + 0.4*box->rect.w, box->rect.y + 0.75*box->rect.h); - mg_line_to(box->rect.x + 0.8*box->rect.w, box->rect.y + 0.2*box->rect.h); - - mg_set_color(box->style.color); - mg_set_width(0.2*box->rect.w); - mg_set_joint(MG_JOINT_MITER); - mg_set_max_joint_excursion(0.2 * box->rect.h); - mg_stroke(); - } -} - -ui_sig ui_checkbox(const char* name, bool* checked) -{ - ui_context* ui = ui_get_context(); - - ui_style defaultStyle = {.size.width = {UI_SIZE_PIXELS, 20}, - .size.height = {UI_SIZE_PIXELS, 20}, - .bgColor = {1, 1, 1, 1}, - .color = {0, 0, 0, 1}, - .borderColor = {0.2, 0.2, 0.2, 1}, - .borderSize = 1, - .roundness = 5}; - - ui_style_mask defaultMask = UI_STYLE_SIZE_WIDTH - | UI_STYLE_SIZE_HEIGHT - | UI_STYLE_BG_COLOR - | UI_STYLE_COLOR - | UI_STYLE_BORDER_COLOR - | UI_STYLE_BORDER_SIZE - | UI_STYLE_ROUNDNESS; - - ui_style_next(&defaultStyle, defaultMask); - - ui_style activeStyle = {.bgColor = {0.5, 0.5, 0.5, 1}, - .borderColor = {0.2, 0.2, 0.2, 1}, - .borderSize = 2}; - ui_style_mask activeMask = UI_STYLE_BG_COLOR - | UI_STYLE_BORDER_COLOR - | UI_STYLE_BORDER_SIZE; - ui_pattern activePattern = {0}; - ui_pattern_push(&ui->frameArena, - &activePattern, - (ui_selector){.kind = UI_SEL_STATUS, - .status = UI_ACTIVE|UI_HOVER}); - ui_style_match_before(activePattern, &activeStyle, activeMask); - - ui_flags flags = UI_FLAG_CLICKABLE - | UI_FLAG_CLIP - | UI_FLAG_DRAW_BACKGROUND - | UI_FLAG_DRAW_PROC - | UI_FLAG_DRAW_BORDER - | UI_FLAG_HOT_ANIMATION - | UI_FLAG_ACTIVE_ANIMATION; - - ui_box* box = ui_box_make(name, flags); - ui_tag_box(box, "checkbox"); - - ui_sig sig = ui_button_behavior(box); - if(sig.clicked) - { - *checked = !*checked; - } - ui_box_set_draw_proc(box, ui_checkbox_draw, checked); - - return(sig); -} - -//------------------------------------------------------------------------------ -// slider / scrollbar -//------------------------------------------------------------------------------ -ui_box* ui_slider(const char* label, f32 thumbRatio, f32* scrollValue) -{ - ui_style_match_before(ui_pattern_all(), &(ui_style){0}, UI_STYLE_LAYOUT); - ui_box* frame = ui_box_begin(label, 0); - { - f32 beforeRatio = (*scrollValue) * (1. - thumbRatio); - f32 afterRatio = (1. - *scrollValue) * (1. - thumbRatio); - - ui_axis trackAxis = (frame->rect.w > frame->rect.h) ? UI_AXIS_X : UI_AXIS_Y; - ui_axis secondAxis = (trackAxis == UI_AXIS_Y) ? UI_AXIS_X : UI_AXIS_Y; - f32 roundness = 0.5*frame->rect.c[2+secondAxis]; - f32 animationTime = 0.5; - - ui_style trackStyle = {.size.width = {UI_SIZE_PARENT, 1}, - .size.height = {UI_SIZE_PARENT, 1}, - .layout.axis = trackAxis, - .layout.align.x = UI_ALIGN_START, - .layout.align.y = UI_ALIGN_START, - .bgColor = {0.5, 0.5, 0.5, 1}, - .roundness = roundness}; - - ui_style beforeStyle = trackStyle; - beforeStyle.size.c[trackAxis] = (ui_size){UI_SIZE_PARENT, beforeRatio}; - - ui_style afterStyle = trackStyle; - afterStyle.size.c[trackAxis] = (ui_size){UI_SIZE_PARENT, afterRatio}; - - ui_style thumbStyle = trackStyle; - thumbStyle.size.c[trackAxis] = (ui_size){UI_SIZE_PARENT, thumbRatio}; - thumbStyle.bgColor = (mg_color){0.3, 0.3, 0.3, 1}; - - ui_style_mask styleMask = UI_STYLE_SIZE_WIDTH - | UI_STYLE_SIZE_HEIGHT - | UI_STYLE_LAYOUT - | UI_STYLE_BG_COLOR - | UI_STYLE_ROUNDNESS; - - ui_flags trackFlags = UI_FLAG_CLIP - | UI_FLAG_DRAW_BACKGROUND - | UI_FLAG_HOT_ANIMATION - | UI_FLAG_ACTIVE_ANIMATION; - - ui_style_next(&trackStyle, styleMask); - ui_box* track = ui_box_begin("track", trackFlags); - - ui_style_next(&beforeStyle, UI_STYLE_SIZE_WIDTH|UI_STYLE_SIZE_HEIGHT); - ui_box* beforeSpacer = ui_box_make("before", 0); - - - ui_flags thumbFlags = UI_FLAG_CLICKABLE - | UI_FLAG_DRAW_BACKGROUND - | UI_FLAG_HOT_ANIMATION - | UI_FLAG_ACTIVE_ANIMATION; - - ui_style_next(&thumbStyle, styleMask); - ui_box* thumb = ui_box_make("thumb", thumbFlags); - - - ui_style_next(&afterStyle, UI_STYLE_SIZE_WIDTH|UI_STYLE_SIZE_HEIGHT); - ui_box* afterSpacer = ui_box_make("after", 0); - - ui_box_end(); - - //NOTE: interaction - ui_sig thumbSig = ui_box_sig(thumb); - if(thumbSig.dragging) - { - f32 trackExtents = track->rect.c[2+trackAxis] - thumb->rect.c[2+trackAxis]; - f32 delta = thumbSig.delta.c[trackAxis]/trackExtents; - f32 oldValue = *scrollValue; - - *scrollValue += delta; - *scrollValue = Clamp(*scrollValue, 0, 1); - } - - ui_sig trackSig = ui_box_sig(track); - - if(ui_box_active(frame)) - { - //NOTE: activated from outside - ui_box_set_hot(track, true); - ui_box_set_hot(thumb, true); - ui_box_activate(track); - ui_box_activate(thumb); - } - - if(trackSig.hovering) - { - ui_box_set_hot(track, true); - ui_box_set_hot(thumb, true); - } - else if(thumbSig.wheel.c[trackAxis] == 0) - { - ui_box_set_hot(track, false); - ui_box_set_hot(thumb, false); - } - - if(thumbSig.dragging) - { - ui_box_activate(track); - ui_box_activate(thumb); - } - else if(thumbSig.wheel.c[trackAxis] == 0) - { - ui_box_deactivate(track); - ui_box_deactivate(thumb); - ui_box_deactivate(frame); - } - - } ui_box_end(); - - return(frame); -} - -//------------------------------------------------------------------------------ -// panels -//------------------------------------------------------------------------------ -void ui_panel_begin(const char* str, ui_flags flags) -{ - flags = flags - | UI_FLAG_CLIP - | UI_FLAG_BLOCK_MOUSE - | UI_FLAG_ALLOW_OVERFLOW_X - | UI_FLAG_ALLOW_OVERFLOW_Y; - - ui_box_begin(str, flags); -} - -void ui_panel_end(void) -{ - ui_box* panel = ui_box_top(); - ui_sig sig = ui_box_sig(panel); - - f32 contentsW = ClampLowBound(panel->childrenSum[0], panel->rect.w); - f32 contentsH = ClampLowBound(panel->childrenSum[1], panel->rect.h); - - contentsW = ClampLowBound(contentsW, 1); - contentsH = ClampLowBound(contentsH, 1); - - ui_box* scrollBarX = 0; - ui_box* scrollBarY = 0; - - bool needsScrollX = contentsW > panel->rect.w; - bool needsScrollY = contentsH > panel->rect.h; - - if(needsScrollX) - { - f32 thumbRatioX = panel->rect.w / contentsW; - f32 sliderX = panel->scroll.x /(contentsW - panel->rect.w); - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1., 0}, - .size.height = {UI_SIZE_PIXELS, 10, 0}, - .floating.x = true, - .floating.y = true, - .floatTarget = {0, panel->rect.h - 10}}, - UI_STYLE_SIZE - |UI_STYLE_FLOAT); - - scrollBarX = ui_slider("scrollerX", thumbRatioX, &sliderX); - - panel->scroll.x = sliderX * (contentsW - panel->rect.w); - if(sig.hovering) - { - ui_box_activate(scrollBarX); - } - } - - if(needsScrollY) - { - f32 thumbRatioY = panel->rect.h / contentsH; - f32 sliderY = panel->scroll.y /(contentsH - panel->rect.h); - - f32 spacerSize = needsScrollX ? 10 : 0; - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 10, 0}, - .size.height = {UI_SIZE_PARENT_MINUS_PIXELS, spacerSize, 0}, - .floating.x = true, - .floating.y = true, - .floatTarget = {panel->rect.w - 10, 0}}, - UI_STYLE_SIZE - |UI_STYLE_FLOAT); - - scrollBarY = ui_slider("scrollerY", thumbRatioY, &sliderY); - - panel->scroll.y = sliderY * (contentsH - panel->rect.h); - if(sig.hovering) - { - ui_box_activate(scrollBarY); - } - } - panel->scroll.x = Clamp(panel->scroll.x, 0, contentsW - panel->rect.w); - panel->scroll.y = Clamp(panel->scroll.y, 0, contentsH - panel->rect.h); - - ui_box_end(); -} - -//------------------------------------------------------------------------------ -// tooltips -//------------------------------------------------------------------------------ - -ui_sig ui_tooltip_begin(const char* name) -{ - ui_context* ui = ui_get_context(); - - vec2 p = ui_mouse_position(); - - ui_style style = {.size.width = {UI_SIZE_CHILDREN}, - .size.height = {UI_SIZE_CHILDREN}, - .floating.x = true, - .floating.y = true, - .floatTarget = {p.x, p.y}}; - ui_style_mask mask = UI_STYLE_SIZE | UI_STYLE_FLOAT; - - ui_style_next(&style, mask); - - ui_flags flags = UI_FLAG_OVERLAY - | UI_FLAG_DRAW_BACKGROUND - | UI_FLAG_DRAW_BORDER; - - ui_box* tooltip = ui_box_make(name, flags); - ui_box_push(tooltip); - - return(ui_box_sig(tooltip)); -} - -void ui_tooltip_end(void) -{ - ui_box_pop(); // tooltip -} - -//------------------------------------------------------------------------------ -// Menus -//------------------------------------------------------------------------------ - -void ui_menu_bar_begin(const char* name) -{ - ui_style style = {.size.width = {UI_SIZE_PARENT, 1, 0}, - .size.height = {UI_SIZE_CHILDREN}, - .layout.axis = UI_AXIS_X, - .layout.spacing = 20,}; - ui_style_mask mask = UI_STYLE_SIZE - | UI_STYLE_LAYOUT_AXIS - | UI_STYLE_LAYOUT_SPACING; - - ui_style_next(&style, mask); - ui_box* bar = ui_box_begin(name, UI_FLAG_DRAW_BACKGROUND); - - ui_sig sig = ui_box_sig(bar); - ui_context* ui = ui_get_context(); - if(!sig.hovering && mp_mouse_released(&ui->input, MP_MOUSE_LEFT)) - { - ui_box_deactivate(bar); - } -} - -void ui_menu_bar_end(void) -{ - ui_box_end(); // menu bar -} - -void ui_menu_begin(const char* label) -{ - ui_box* container = ui_box_make(label, 0); - ui_box_push(container); - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_TEXT}, - .size.height = {UI_SIZE_TEXT}}, - UI_STYLE_SIZE); - - ui_box* button = ui_box_make(label, UI_FLAG_CLICKABLE | UI_FLAG_DRAW_TEXT); - ui_box* bar = container->parent; - - ui_sig sig = ui_box_sig(button); - ui_sig barSig = ui_box_sig(bar); - - ui_context* ui = ui_get_context(); - - ui_style style = {.size.width = {UI_SIZE_CHILDREN}, - .size.height = {UI_SIZE_CHILDREN}, - .floating.x = true, - .floating.y = true, - .floatTarget = {button->rect.x, - button->rect.y + button->rect.h}, - .layout.axis = UI_AXIS_Y, - .layout.spacing = 5, - .layout.margin.x = 0, - .layout.margin.y = 5, - .bgColor = {0.2, 0.2, 0.2, 1}}; - - ui_style_mask mask = UI_STYLE_SIZE - | UI_STYLE_FLOAT - | UI_STYLE_LAYOUT - | UI_STYLE_BG_COLOR; - - ui_flags flags = UI_FLAG_OVERLAY - | UI_FLAG_DRAW_BACKGROUND - | UI_FLAG_DRAW_BORDER; - - ui_style_next(&style, mask); - ui_box* menu = ui_box_make("panel", flags); - - if(ui_box_active(bar)) - { - if(sig.hovering) - { - ui_box_activate(button); - } - else if(barSig.hovering) - { - ui_box_deactivate(button); - } - } - else - { - ui_box_deactivate(button); - if(sig.pressed) - { - ui_box_activate(bar); - ui_box_activate(button); - } - } - - ui_box_set_closed(menu, !ui_box_active(button)); - ui_box_push(menu); -} - -void ui_menu_end(void) -{ - ui_box_pop(); // menu - ui_box_pop(); // container -} - -ui_sig ui_menu_button(const char* name) -{ - ui_context* ui = ui_get_context(); - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_TEXT}, - .size.height = {UI_SIZE_TEXT}, - .layout.margin.x = 5, - .bgColor = {0, 0, 0, 0}}, - UI_STYLE_SIZE - |UI_STYLE_LAYOUT_MARGIN_X - |UI_STYLE_BG_COLOR); - - ui_pattern pattern = {0}; - ui_pattern_push(&ui->frameArena, &pattern, (ui_selector){.kind = UI_SEL_STATUS, .status = UI_HOVER}); - - ui_style style = {.bgColor = {0, 0, 1, 1}}; - ui_style_mask mask = UI_STYLE_BG_COLOR; - ui_style_match_before(pattern, &style, mask); - - ui_flags flags = UI_FLAG_CLICKABLE - | UI_FLAG_CLIP - | UI_FLAG_DRAW_TEXT - | UI_FLAG_DRAW_BACKGROUND; - - ui_box* box = ui_box_make(name, flags); - ui_sig sig = ui_box_sig(box); - return(sig); -} - -void ui_select_popup_draw_arrow(ui_box* box, void* data) -{ - f32 r = minimum(box->parent->style.roundness, box->rect.w); - f32 cr = r*4*(sqrt(2)-1)/3; - - mg_move_to(box->rect.x, box->rect.y); - mg_line_to(box->rect.x + box->rect.w - r, box->rect.y); - mg_cubic_to(box->rect.x + box->rect.w - cr, box->rect.y, - box->rect.x + box->rect.w, box->rect.y + cr, - box->rect.x + box->rect.w, box->rect.y + r); - mg_line_to(box->rect.x + box->rect.w, box->rect.y + box->rect.h - r); - mg_cubic_to(box->rect.x + box->rect.w, box->rect.y + box->rect.h - cr, - box->rect.x + box->rect.w - cr, box->rect.y + box->rect.h, - box->rect.x + box->rect.w - r, box->rect.y + box->rect.h); - mg_line_to(box->rect.x, box->rect.y + box->rect.h); - - mg_set_color(box->style.bgColor); - mg_fill(); - - mg_move_to(box->rect.x + 0.25*box->rect.w, box->rect.y + 0.45*box->rect.h); - mg_line_to(box->rect.x + 0.5*box->rect.w, box->rect.y + 0.75*box->rect.h); - mg_line_to(box->rect.x + 0.75*box->rect.w, box->rect.y + 0.45*box->rect.h); - mg_close_path(); - - mg_set_color(box->style.color); - mg_fill(); -} - -ui_select_popup_info ui_select_popup(const char* name, ui_select_popup_info* info) -{ - ui_select_popup_info result = *info; - - ui_context* ui = ui_get_context(); - - ui_container(name, 0) - { - ui_box* button = ui_box_make("button", - UI_FLAG_CLICKABLE - |UI_FLAG_DRAW_BACKGROUND - |UI_FLAG_DRAW_BORDER - |UI_FLAG_ALLOW_OVERFLOW_X - |UI_FLAG_CLIP); - - f32 maxOptionWidth = 0; - f32 lineHeight = 0; - mp_rect bbox = {0}; - for(int i=0; ioptionCount; i++) - { - bbox = mg_text_bounding_box(button->style.font, button->style.fontSize, info->options[i]); - maxOptionWidth = maximum(maxOptionWidth, bbox.w); - } - f32 buttonWidth = maxOptionWidth + 2*button->style.layout.margin.x + button->rect.h; - - ui_style_box_before(button, - ui_pattern_owner(), - &(ui_style){.size.width = {UI_SIZE_PIXELS, buttonWidth}, - .size.height = {UI_SIZE_CHILDREN}, - .layout.margin.x = 5, - .layout.margin.y = 1, - .roundness = 5, - .borderSize = 1, - .borderColor = {0.3, 0.3, 0.3, 1}}, - UI_STYLE_SIZE - |UI_STYLE_LAYOUT_MARGIN_X - |UI_STYLE_LAYOUT_MARGIN_Y - |UI_STYLE_ROUNDNESS - |UI_STYLE_BORDER_SIZE - |UI_STYLE_BORDER_COLOR); - ui_box_push(button); - { - ui_label_str8(info->options[info->selectedIndex]); - - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, button->rect.h}, - .size.height = {UI_SIZE_PIXELS, button->rect.h}, - .floating.x = true, - .floating.y = true, - .floatTarget = {button->rect.w - button->rect.h, 0}, - .color = {0, 0, 0, 1}, - .bgColor = {0.7, 0.7, 0.7, 1}}, - UI_STYLE_SIZE - |UI_STYLE_FLOAT - |UI_STYLE_COLOR - |UI_STYLE_BG_COLOR); - - ui_box* arrow = ui_box_make("arrow", UI_FLAG_DRAW_PROC); - ui_box_set_draw_proc(arrow, ui_select_popup_draw_arrow, 0); - - } ui_box_pop(); - - //panel - ui_box* panel = ui_box_make("panel", - UI_FLAG_DRAW_BACKGROUND - |UI_FLAG_BLOCK_MOUSE - |UI_FLAG_OVERLAY); - - //TODO: set width to max(button.w, max child...) - f32 containerWidth = maximum(maxOptionWidth + 2*panel->style.layout.margin.x, - button->rect.w); - - ui_style_box_before(panel, - ui_pattern_owner(), - &(ui_style){.size.width = {UI_SIZE_PIXELS, containerWidth}, - .size.height = {UI_SIZE_CHILDREN}, - .floating.x = true, - .floating.y = true, - .floatTarget = {button->rect.x, - button->rect.y + button->rect.h}, - .layout.axis = UI_AXIS_Y, - .layout.margin.x = 0, - .layout.margin.y = 5, - .bgColor = {0.2, 0.2, 0.2, 1}}, - UI_STYLE_SIZE - |UI_STYLE_FLOAT - |UI_STYLE_LAYOUT - |UI_STYLE_BG_COLOR); - - ui_box_push(panel); - { - for(int i=0; ioptionCount; i++) - { - ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, - .size.height = {UI_SIZE_TEXT}, - .layout.axis = UI_AXIS_Y, - .layout.align.x = UI_ALIGN_START, - .layout.margin.x = 5, - .layout.margin.y = 2.5}, - UI_STYLE_SIZE - |UI_STYLE_LAYOUT_AXIS - |UI_STYLE_LAYOUT_ALIGN_X - |UI_STYLE_LAYOUT_MARGIN_X - |UI_STYLE_LAYOUT_MARGIN_Y); - - - ui_pattern pattern = {0}; - ui_pattern_push(&ui->frameArena, &pattern, (ui_selector){.kind = UI_SEL_STATUS, .status = UI_HOVER}); - ui_style_match_before(pattern, &(ui_style){.bgColor = {0, 0, 1, 1}}, UI_STYLE_BG_COLOR); - - ui_box* box = ui_box_make_str8(info->options[i], - UI_FLAG_DRAW_TEXT - |UI_FLAG_CLICKABLE - |UI_FLAG_DRAW_BACKGROUND); - ui_sig sig = ui_box_sig(box); - if(sig.pressed) - { - result.selectedIndex = i; - } - } - } - ui_box_pop(); - - ui_context* ui = ui_get_context(); - if(ui_box_active(panel) && mp_mouse_pressed(&ui->input, MP_MOUSE_LEFT)) - { - ui_box_deactivate(panel); - } - else if(ui_box_sig(button).pressed) - { - ui_box_activate(panel); - } - ui_box_set_closed(panel, !ui_box_active(panel)); - } - return(result); -} - -//------------------------------------------------------------------------------ -// text box -//------------------------------------------------------------------------------ -str32 ui_edit_replace_selection_with_codepoints(ui_context* ui, str32 codepoints, str32 input) -{ - u32 start = minimum(ui->editCursor, ui->editMark); - u32 end = maximum(ui->editCursor, ui->editMark); - - str32 before = str32_slice(codepoints, 0, start); - str32 after = str32_slice(codepoints, end, codepoints.len); - - str32_list list = {0}; - str32_list_push(&ui->frameArena, &list, before); - str32_list_push(&ui->frameArena, &list, input); - str32_list_push(&ui->frameArena, &list, after); - - codepoints = str32_list_join(&ui->frameArena, list); - - ui->editCursor = start + input.len; - ui->editMark = ui->editCursor; - return(codepoints); -} - -str32 ui_edit_delete_selection(ui_context* ui, str32 codepoints) -{ - return(ui_edit_replace_selection_with_codepoints(ui, codepoints, (str32){0})); -} - -void ui_edit_copy_selection_to_clipboard(ui_context* ui, str32 codepoints) -{ - if(ui->editCursor == ui->editMark) - { - return; - } - u32 start = minimum(ui->editCursor, ui->editMark); - u32 end = maximum(ui->editCursor, ui->editMark); - str32 selection = str32_slice(codepoints, start, end); - str8 string = utf8_push_from_codepoints(&ui->frameArena, selection); - - mp_clipboard_clear(); - mp_clipboard_set_string(string); -} - -str32 ui_edit_replace_selection_with_clipboard(ui_context* ui, str32 codepoints) -{ - str8 string = mp_clipboard_get_string(&ui->frameArena); - str32 input = utf8_push_to_codepoints(&ui->frameArena, string); - str32 result = ui_edit_replace_selection_with_codepoints(ui, codepoints, input); - return(result); -} - -typedef enum { - UI_EDIT_MOVE, - UI_EDIT_SELECT, - UI_EDIT_SELECT_EXTEND, - UI_EDIT_DELETE, - UI_EDIT_CUT, - UI_EDIT_COPY, - UI_EDIT_PASTE, - UI_EDIT_SELECT_ALL } ui_edit_op; - -typedef enum { - UI_EDIT_MOVE_NONE = 0, - UI_EDIT_MOVE_ONE, - UI_EDIT_MOVE_WORD, - UI_EDIT_MOVE_LINE } ui_edit_move; - -typedef struct ui_edit_command -{ - mp_key_code key; - mp_keymod_flags mods; - - ui_edit_op operation; - ui_edit_move move; - int direction; - -} ui_edit_command; - -#if PLATFORM_WINDOWS - #define OS_COPY_PASTE_MOD MP_KEYMOD_CTRL -#elif PLATFORM_MACOS - #define OS_COPY_PASTE_MOD MP_KEYMOD_CMD -#endif - -const ui_edit_command UI_EDIT_COMMANDS[] = { - //NOTE(martin): move one left - { - .key = MP_KEY_LEFT, - .operation = UI_EDIT_MOVE, - .move = UI_EDIT_MOVE_ONE, - .direction = -1 - }, - //NOTE(martin): move one right - { - .key = MP_KEY_RIGHT, - .operation = UI_EDIT_MOVE, - .move = UI_EDIT_MOVE_ONE, - .direction = 1 - }, - //NOTE(martin): move start - { - .key = MP_KEY_Q, - .mods = MP_KEYMOD_CTRL, - .operation = UI_EDIT_MOVE, - .move = UI_EDIT_MOVE_LINE, - .direction = -1 - }, - { - .key = MP_KEY_UP, - .operation = UI_EDIT_MOVE, - .move = UI_EDIT_MOVE_LINE, - .direction = -1 - }, - //NOTE(martin): move end - { - .key = MP_KEY_E, - .mods = MP_KEYMOD_CTRL, - .operation = UI_EDIT_MOVE, - .move = UI_EDIT_MOVE_LINE, - .direction = 1 - }, - { - .key = MP_KEY_DOWN, - .operation = UI_EDIT_MOVE, - .move = UI_EDIT_MOVE_LINE, - .direction = 1 - }, - //NOTE(martin): select one left - { - .key = MP_KEY_LEFT, - .mods = MP_KEYMOD_SHIFT, - .operation = UI_EDIT_SELECT, - .move = UI_EDIT_MOVE_ONE, - .direction = -1 - }, - //NOTE(martin): select one right - { - .key = MP_KEY_RIGHT, - .mods = MP_KEYMOD_SHIFT, - .operation = UI_EDIT_SELECT, - .move = UI_EDIT_MOVE_ONE, - .direction = 1 - }, - //NOTE(martin): extend select to start - { - .key = MP_KEY_Q, - .mods = MP_KEYMOD_CTRL | MP_KEYMOD_SHIFT, - .operation = UI_EDIT_SELECT_EXTEND, - .move = UI_EDIT_MOVE_LINE, - .direction = -1 - }, - { - .key = MP_KEY_UP, - .mods = MP_KEYMOD_SHIFT, - .operation = UI_EDIT_SELECT_EXTEND, - .move = UI_EDIT_MOVE_LINE, - .direction = -1 - }, - //NOTE(martin): extend select to end - { - .key = MP_KEY_E, - .mods = MP_KEYMOD_CTRL | MP_KEYMOD_SHIFT, - .operation = UI_EDIT_SELECT_EXTEND, - .move = UI_EDIT_MOVE_LINE, - .direction = 1 - }, - { - .key = MP_KEY_DOWN, - .mods = MP_KEYMOD_SHIFT, - .operation = UI_EDIT_SELECT_EXTEND, - .move = UI_EDIT_MOVE_LINE, - .direction = 1 - }, - //NOTE(martin): select all - { - .key = MP_KEY_Q, - .mods = OS_COPY_PASTE_MOD, - .operation = UI_EDIT_SELECT_ALL, - .move = UI_EDIT_MOVE_NONE - }, - //NOTE(martin): delete - { - .key = MP_KEY_DELETE, - .operation = UI_EDIT_DELETE, - .move = UI_EDIT_MOVE_ONE, - .direction = 1 - }, - //NOTE(martin): backspace - { - .key = MP_KEY_BACKSPACE, - .operation = UI_EDIT_DELETE, - .move = UI_EDIT_MOVE_ONE, - .direction = -1 - }, - //NOTE(martin): cut - { - .key = MP_KEY_X, - .mods = OS_COPY_PASTE_MOD, - .operation = UI_EDIT_CUT, - .move = UI_EDIT_MOVE_NONE - }, - //NOTE(martin): copy - { - .key = MP_KEY_C, - .mods = OS_COPY_PASTE_MOD, - .operation = UI_EDIT_COPY, - .move = UI_EDIT_MOVE_NONE - }, - //NOTE(martin): paste - { - .key = MP_KEY_V, - .mods = OS_COPY_PASTE_MOD, - .operation = UI_EDIT_PASTE, - .move = UI_EDIT_MOVE_NONE - } -}; - -const u32 UI_EDIT_COMMAND_COUNT = sizeof(UI_EDIT_COMMANDS)/sizeof(ui_edit_command); - -void ui_edit_perform_move(ui_context* ui, ui_edit_move move, int direction, u32 textLen) -{ - switch(move) - { - case UI_EDIT_MOVE_NONE: - break; - - case UI_EDIT_MOVE_ONE: - { - if(direction < 0 && ui->editCursor > 0) - { - ui->editCursor--; - } - else if(direction > 0 && ui->editCursor < textLen) - { - ui->editCursor++; - } - } break; - - case UI_EDIT_MOVE_LINE: - { - if(direction < 0) - { - ui->editCursor = 0; - } - else if(direction > 0) - { - ui->editCursor = textLen; - } - } break; - - case UI_EDIT_MOVE_WORD: - DEBUG_ASSERT(0, "not implemented yet"); - break; - } -} - -str32 ui_edit_perform_operation(ui_context* ui, ui_edit_op operation, ui_edit_move move, int direction, str32 codepoints) -{ - switch(operation) - { - case UI_EDIT_MOVE: - { - //NOTE(martin): we place the cursor on the direction-most side of the selection - // before performing the move - u32 cursor = direction < 0 ? - minimum(ui->editCursor, ui->editMark) : - maximum(ui->editCursor, ui->editMark); - ui->editCursor = cursor; - - if(ui->editCursor == ui->editMark || move != UI_EDIT_MOVE_ONE) - { - //NOTE: we special case move-one when there is a selection - // (just place the cursor at begining/end of selection) - ui_edit_perform_move(ui, move, direction, codepoints.len); - } - ui->editMark = ui->editCursor; - } break; - - case UI_EDIT_SELECT: - { - ui_edit_perform_move(ui, move, direction, codepoints.len); - } break; - - case UI_EDIT_SELECT_EXTEND: - { - if((direction > 0) != (ui->editCursor > ui->editMark)) - { - u32 tmp = ui->editCursor; - ui->editCursor = ui->editMark; - ui->editMark = tmp; - } - ui_edit_perform_move(ui, move, direction, codepoints.len); - } break; - - case UI_EDIT_DELETE: - { - if(ui->editCursor == ui->editMark) - { - ui_edit_perform_move(ui, move, direction, codepoints.len); - } - codepoints = ui_edit_delete_selection(ui, codepoints); - ui->editMark = ui->editCursor; - } break; - - case UI_EDIT_CUT: - { - ui_edit_copy_selection_to_clipboard(ui, codepoints); - codepoints = ui_edit_delete_selection(ui, codepoints); - } break; - - case UI_EDIT_COPY: - { - ui_edit_copy_selection_to_clipboard(ui, codepoints); - } break; - - case UI_EDIT_PASTE: - { - codepoints = ui_edit_replace_selection_with_clipboard(ui, codepoints); - } break; - - case UI_EDIT_SELECT_ALL: - { - ui->editCursor = 0; - ui->editMark = codepoints.len; - } break; - } - ui->editCursorBlinkStart = ui->frameTime; - - return(codepoints); -} - -void ui_text_box_render(ui_box* box, void* data) -{ - str32 codepoints = *(str32*)data; - ui_context* ui = ui_get_context(); - - u32 firstDisplayedChar = 0; - if(ui_box_active(box)) - { - firstDisplayedChar = ui->editFirstDisplayedChar; - } - - ui_style* style = &box->style; - mg_font_extents extents = mg_font_get_scaled_extents(style->font, style->fontSize); - f32 lineHeight = extents.ascent + extents.descent; - - str32 before = str32_slice(codepoints, 0, firstDisplayedChar); - mp_rect beforeBox = mg_text_bounding_box_utf32(style->font, style->fontSize, before); - - f32 textMargin = 5; //TODO: make that configurable - - f32 textX = textMargin + box->rect.x - beforeBox.w; - f32 textTop = box->rect.y + 0.5*(box->rect.h - lineHeight); - f32 textY = textTop + extents.ascent ; - - if(box->active) - { - u32 selectStart = minimum(ui->editCursor, ui->editMark); - u32 selectEnd = maximum(ui->editCursor, ui->editMark); - - str32 beforeSelect = str32_slice(codepoints, 0, selectStart); - mp_rect beforeSelectBox = mg_text_bounding_box_utf32(style->font, style->fontSize, beforeSelect); - beforeSelectBox.x += textX; - beforeSelectBox.y += textY; - - if(selectStart != selectEnd) - { - str32 select = str32_slice(codepoints, selectStart, selectEnd); - str32 afterSelect = str32_slice(codepoints, selectEnd, codepoints.len); - mp_rect selectBox = mg_text_bounding_box_utf32(style->font, style->fontSize, select); - mp_rect afterSelectBox = mg_text_bounding_box_utf32(style->font, style->fontSize, afterSelect); - - selectBox.x += beforeSelectBox.x + beforeSelectBox.w; - selectBox.y += textY; - - mg_set_color_rgba(0, 0, 1, 1); - mg_rectangle_fill(selectBox.x, selectBox.y, selectBox.w, lineHeight); - - mg_set_font(style->font); - mg_set_font_size(style->fontSize); - mg_set_color(style->color); - - mg_move_to(textX, textY); - mg_codepoints_outlines(beforeSelect); - mg_fill(); - - mg_set_color_rgba(1, 1, 1, 1); - mg_codepoints_outlines(select); - mg_fill(); - - mg_set_color(style->color); - mg_codepoints_outlines(afterSelect); - mg_fill(); - } - else - { - if(!((u64)(2*(ui->frameTime - ui->editCursorBlinkStart)) & 1)) - { - f32 caretX = box->rect.x + textMargin - beforeBox.w + beforeSelectBox.w; - f32 caretY = textTop; - mg_set_color(style->color); - mg_rectangle_fill(caretX, caretY, 1, lineHeight); - } - mg_set_font(style->font); - mg_set_font_size(style->fontSize); - mg_set_color(style->color); - - mg_move_to(textX, textY); - mg_codepoints_outlines(codepoints); - mg_fill(); - } - } - else - { - mg_set_font(style->font); - mg_set_font_size(style->fontSize); - mg_set_color(style->color); - - mg_move_to(textX, textY); - mg_codepoints_outlines(codepoints); - mg_fill(); - } -} - -ui_text_box_result ui_text_box(const char* name, mem_arena* arena, str8 text) -{ - ui_context* ui = ui_get_context(); - - ui_text_box_result result = {.text = text}; - - ui_flags frameFlags = UI_FLAG_CLICKABLE - | UI_FLAG_DRAW_BACKGROUND - | UI_FLAG_DRAW_BORDER - | UI_FLAG_CLIP - | UI_FLAG_DRAW_PROC; - - ui_box* frame = ui_box_make(name, frameFlags); - ui_style* style = &frame->style; - f32 textMargin = 5; //TODO parameterize this margin! must be the same as in ui_text_box_render - - mg_font_extents extents = mg_font_get_scaled_extents(style->font, style->fontSize); - - ui_sig sig = ui_box_sig(frame); - - if(sig.hovering) - { - ui_box_set_hot(frame, true); - - if(sig.pressed) - { - if(!ui_box_active(frame)) - { - ui_box_activate(frame); - - //NOTE: focus - ui->focus = frame; - ui->editFirstDisplayedChar = 0; - ui->editCursor = 0; - ui->editMark = 0; - } - ui->editCursorBlinkStart = ui->frameTime; - } - - if(sig.pressed || sig.dragging) - { - //NOTE: set cursor/extend selection on mouse press or drag - vec2 pos = ui_mouse_position(); - f32 cursorX = pos.x - frame->rect.x - textMargin; - - str32 codepoints = utf8_push_to_codepoints(&ui->frameArena, text); - i32 newCursor = codepoints.len; - f32 x = 0; - for(int i = ui->editFirstDisplayedChar; ifont, style->fontSize, str32_slice(codepoints, i, i+1)); - if(x + 0.5*bbox.w > cursorX) - { - newCursor = i; - break; - } - x += bbox.w; - } - //NOTE: put cursor the closest to new cursor (this maximizes the resulting selection, - // and seems to be the standard behaviour across a number of text editor) - if(abs(newCursor - ui->editCursor) > abs(newCursor - ui->editMark)) - { - i32 tmp = ui->editCursor; - ui->editCursor = ui->editMark; - ui->editMark = tmp; - } - //NOTE: set the new cursor, and set or leave the mark depending on mode - ui->editCursor = newCursor; - if(sig.pressed && !(mp_key_mods(&ui->input) & MP_KEYMOD_SHIFT)) - { - ui->editMark = ui->editCursor; - } - } - } - else - { - ui_box_set_hot(frame, false); - - if(sig.pressed) - { - if(ui_box_active(frame)) - { - ui_box_deactivate(frame); - - //NOTE loose focus - ui->focus = 0; - } - } - } - - if(ui_box_active(frame)) - { - str32 oldCodepoints = utf8_push_to_codepoints(&ui->frameArena, text); - str32 codepoints = oldCodepoints; - ui->editCursor = Clamp(ui->editCursor, 0, codepoints.len); - ui->editMark = Clamp(ui->editMark, 0, codepoints.len); - - //NOTE replace selection with input codepoints - str32 input = mp_input_text_utf32(&ui->input, &ui->frameArena); - if(input.len) - { - codepoints = ui_edit_replace_selection_with_codepoints(ui, codepoints, input); - ui->editCursorBlinkStart = ui->frameTime; - } - - //NOTE handle shortcuts - mp_keymod_flags mods = mp_key_mods(&ui->input); - - for(int i=0; iinput, command->key) || mp_key_repeated(&ui->input, command->key)) - && mods == command->mods) - { - codepoints = ui_edit_perform_operation(ui, command->operation, command->move, command->direction, codepoints); - break; - } - } - - //NOTE(martin): check changed/accepted - if(oldCodepoints.ptr != codepoints.ptr) - { - result.changed = true; - result.text = utf8_push_from_codepoints(arena, codepoints); - } - - if(mp_key_pressed(&ui->input, MP_KEY_ENTER)) - { - //TODO(martin): extract in gui_edit_complete() (and use below) - result.accepted = true; - ui_box_deactivate(frame); - ui->focus = 0; - } - - //NOTE slide contents - { - if(ui->editCursor < ui->editFirstDisplayedChar) - { - ui->editFirstDisplayedChar = ui->editCursor; - } - else - { - i32 firstDisplayedChar = ui->editFirstDisplayedChar; - str32 firstToCursor = str32_slice(codepoints, firstDisplayedChar, ui->editCursor); - mp_rect firstToCursorBox = mg_text_bounding_box_utf32(style->font, style->fontSize, firstToCursor); - - while(firstToCursorBox.w > (frame->rect.w - 2*textMargin)) - { - firstDisplayedChar++; - firstToCursor = str32_slice(codepoints, firstDisplayedChar, ui->editCursor); - firstToCursorBox = mg_text_bounding_box_utf32(style->font, style->fontSize, firstToCursor); - } - - ui->editFirstDisplayedChar = firstDisplayedChar; - } - } - - //NOTE: set renderer - str32* renderCodepoints = mem_arena_alloc_type(&ui->frameArena, str32); - *renderCodepoints = str32_push_copy(&ui->frameArena, codepoints); - ui_box_set_draw_proc(frame, ui_text_box_render, renderCodepoints); - } - else - { - //NOTE: set renderer - str32* renderCodepoints = mem_arena_alloc_type(&ui->frameArena, str32); - *renderCodepoints = utf8_push_to_codepoints(&ui->frameArena, text); - ui_box_set_draw_proc(frame, ui_text_box_render, renderCodepoints); - } - - return(result); -} +/************************************************************//** +* +* @file: ui.c +* @author: Martin Fouilleul +* @date: 08/08/2022 +* @revision: +* +*****************************************************************/ +#include"platform.h" +#include"platform_assert.h" +#include"memory.h" +#include"hash.h" +#include"platform_clock.h" +#include"ui.h" + +static ui_style UI_STYLE_DEFAULTS = +{ + .size.width = {.kind = UI_SIZE_CHILDREN, + .value = 0, + .relax = 0}, + .size.height = {.kind = UI_SIZE_CHILDREN, + .value = 0, + .relax = 0}, + + .layout = {.axis = UI_AXIS_Y, + .align = {UI_ALIGN_START, + UI_ALIGN_START}}, + .color = {0, 0, 0, 1}, + .fontSize = 16, +}; + +mp_thread_local ui_context __uiThreadContext = {0}; +mp_thread_local ui_context* __uiCurrentContext = 0; + +ui_context* ui_get_context(void) +{ + return(__uiCurrentContext); +} + +void ui_set_context(ui_context* context) +{ + __uiCurrentContext = context; +} + +//----------------------------------------------------------------------------- +// stacks +//----------------------------------------------------------------------------- +ui_stack_elt* ui_stack_push(ui_context* ui, ui_stack_elt** stack) +{ + ui_stack_elt* elt = mem_arena_alloc_type(&ui->frameArena, ui_stack_elt); + memset(elt, 0, sizeof(ui_stack_elt)); + elt->parent = *stack; + *stack = elt; + return(elt); +} + +void ui_stack_pop(ui_stack_elt** stack) +{ + if(*stack) + { + *stack = (*stack)->parent; + } + else + { + log_error("ui stack underflow\n"); + } +} + +mp_rect ui_intersect_rects(mp_rect lhs, mp_rect rhs) +{ + //NOTE(martin): intersect with current clip + f32 x0 = maximum(lhs.x, rhs.x); + f32 y0 = maximum(lhs.y, rhs.y); + f32 x1 = minimum(lhs.x + lhs.w, rhs.x + rhs.w); + f32 y1 = minimum(lhs.y + lhs.h, rhs.y + rhs.h); + mp_rect r = {x0, y0, maximum(0, x1-x0), maximum(0, y1-y0)}; + return(r); +} + +mp_rect ui_clip_top(void) +{ + mp_rect r = {-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX}; + ui_context* ui = ui_get_context(); + ui_stack_elt* elt = ui->clipStack; + if(elt) + { + r = elt->clip; + } + return(r); +} + +void ui_clip_push(mp_rect clip) +{ + ui_context* ui = ui_get_context(); + mp_rect current = ui_clip_top(); + ui_stack_elt* elt = ui_stack_push(ui, &ui->clipStack); + elt->clip = ui_intersect_rects(current, clip); +} + +void ui_clip_pop(void) +{ + ui_context* ui = ui_get_context(); + ui_stack_pop(&ui->clipStack); +} + +ui_box* ui_box_top(void) +{ + ui_context* ui = ui_get_context(); + ui_stack_elt* elt = ui->boxStack; + ui_box* box = elt ? elt->box : 0; + return(box); +} + +void ui_box_push(ui_box* box) +{ + ui_context* ui = ui_get_context(); + ui_stack_elt* elt = ui_stack_push(ui, &ui->boxStack); + elt->box = box; + if(box->flags & UI_FLAG_CLIP) + { + ui_clip_push(box->rect); + } +} + +void ui_box_pop(void) +{ + ui_context* ui = ui_get_context(); + ui_box* box = ui_box_top(); + if(box) + { + if(box->flags & UI_FLAG_CLIP) + { + ui_clip_pop(); + } + ui_stack_pop(&ui->boxStack); + } +} + +//----------------------------------------------------------------------------- +// tagging +//----------------------------------------------------------------------------- + +ui_tag ui_tag_make_str8(str8 string) +{ + ui_tag tag = {.hash = mp_hash_xx64_string(string)}; + return(tag); +} + +void ui_tag_box_str8(ui_box* box, str8 string) +{ + ui_context* ui = ui_get_context(); + ui_tag_elt* elt = mem_arena_alloc_type(&ui->frameArena, ui_tag_elt); + elt->tag = ui_tag_make_str8(string); + list_append(&box->tags, &elt->listElt); +} + +void ui_tag_next_str8(str8 string) +{ + ui_context* ui = ui_get_context(); + ui_tag_elt* elt = mem_arena_alloc_type(&ui->frameArena, ui_tag_elt); + elt->tag = ui_tag_make_str8(string); + list_append(&ui->nextBoxTags, &elt->listElt); +} + +//----------------------------------------------------------------------------- +// key hashing and caching +//----------------------------------------------------------------------------- +ui_key ui_key_make_str8(str8 string) +{ + ui_context* ui = ui_get_context(); + u64 seed = 0; + ui_box* parent = ui_box_top(); + if(parent) + { + seed = parent->key.hash; + } + + ui_key key = {0}; + key.hash = mp_hash_xx64_string_seed(string, seed); + return(key); +} + +ui_key ui_key_make_path(str8_list path) +{ + ui_context* ui = ui_get_context(); + u64 seed = 0; + ui_box* parent = ui_box_top(); + if(parent) + { + seed = parent->key.hash; + } + for_list(&path.list, elt, str8_elt, listElt) + { + seed = mp_hash_xx64_string_seed(elt->string, seed); + } + ui_key key = {seed}; + return(key); +} + +bool ui_key_equal(ui_key a, ui_key b) +{ + return(a.hash == b.hash); +} + +void ui_box_cache(ui_context* ui, ui_box* box) +{ + u64 index = box->key.hash & (UI_BOX_MAP_BUCKET_COUNT-1); + list_append(&(ui->boxMap[index]), &box->bucketElt); +} + +ui_box* ui_box_lookup_key(ui_key key) +{ + ui_context* ui = ui_get_context(); + u64 index = key.hash & (UI_BOX_MAP_BUCKET_COUNT-1); + + for_list(&ui->boxMap[index], box, ui_box, bucketElt) + { + if(ui_key_equal(key, box->key)) + { + return(box); + } + } + return(0); +} + +ui_box* ui_box_lookup_str8(str8 string) +{ + ui_key key = ui_key_make_str8(string); + return(ui_box_lookup_key(key)); +} + +//----------------------------------------------------------------------------- +// styling +//----------------------------------------------------------------------------- + +void ui_pattern_push(mem_arena* arena, ui_pattern* pattern, ui_selector selector) +{ + ui_selector* copy = mem_arena_alloc_type(arena, ui_selector); + *copy = selector; + list_append(&pattern->l, ©->listElt); +} + +ui_pattern ui_pattern_all(void) +{ + ui_context* ui = ui_get_context(); + ui_pattern pattern = {0}; + ui_pattern_push(&ui->frameArena, &pattern, (ui_selector){.kind = UI_SEL_ANY}); + return(pattern); +} + +ui_pattern ui_pattern_owner(void) +{ + ui_context* ui = ui_get_context(); + ui_pattern pattern = {0}; + ui_pattern_push(&ui->frameArena, &pattern, (ui_selector){.kind = UI_SEL_OWNER}); + return(pattern); +} + +void ui_style_match_before(ui_pattern pattern, ui_style* style, ui_style_mask mask) +{ + ui_context* ui = ui_get_context(); + if(ui) + { + ui_style_rule* rule = mem_arena_alloc_type(&ui->frameArena, ui_style_rule); + rule->pattern = pattern; + rule->mask = mask; + rule->style = mem_arena_alloc_type(&ui->frameArena, ui_style); + *rule->style = *style; + + list_append(&ui->nextBoxBeforeRules, &rule->boxElt); + } +} + +void ui_style_match_after(ui_pattern pattern, ui_style* style, ui_style_mask mask) +{ + ui_context* ui = ui_get_context(); + if(ui) + { + ui_style_rule* rule = mem_arena_alloc_type(&ui->frameArena, ui_style_rule); + rule->pattern = pattern; + rule->mask = mask; + rule->style = mem_arena_alloc_type(&ui->frameArena, ui_style); + *rule->style = *style; + + list_append(&ui->nextBoxAfterRules, &rule->boxElt); + } +} + +void ui_style_next(ui_style* style, ui_style_mask mask) +{ + ui_style_match_before(ui_pattern_owner(), style, mask); +} + +void ui_style_box_before(ui_box* box, ui_pattern pattern, ui_style* style, ui_style_mask mask) +{ + ui_context* ui = ui_get_context(); + if(ui) + { + ui_style_rule* rule = mem_arena_alloc_type(&ui->frameArena, ui_style_rule); + rule->pattern = pattern; + rule->mask = mask; + rule->style = mem_arena_alloc_type(&ui->frameArena, ui_style); + *rule->style = *style; + + list_append(&box->beforeRules, &rule->boxElt); + rule->owner = box; + } +} + +void ui_style_box_after(ui_box* box, ui_pattern pattern, ui_style* style, ui_style_mask mask) +{ + ui_context* ui = ui_get_context(); + if(ui) + { + ui_style_rule* rule = mem_arena_alloc_type(&ui->frameArena, ui_style_rule); + rule->pattern = pattern; + rule->mask = mask; + rule->style = mem_arena_alloc_type(&ui->frameArena, ui_style); + *rule->style = *style; + + list_append(&box->afterRules, &rule->boxElt); + rule->owner = box; + } +} + +//----------------------------------------------------------------------------- +// input +//----------------------------------------------------------------------------- + +void ui_process_event(mp_event* event) +{ + ui_context* ui = ui_get_context(); + mp_input_process_event(&ui->input, event); +} + +vec2 ui_mouse_position(void) +{ + ui_context* ui = ui_get_context(); + vec2 mousePos = mp_mouse_position(&ui->input); + return(mousePos); +} + +vec2 ui_mouse_delta(void) +{ + ui_context* ui = ui_get_context(); + vec2 delta = mp_mouse_delta(&ui->input); + return(delta); +} + +vec2 ui_mouse_wheel(void) +{ + ui_context* ui = ui_get_context(); + vec2 delta = mp_mouse_wheel(&ui->input); + return(delta); +} + +//----------------------------------------------------------------------------- +// ui boxes +//----------------------------------------------------------------------------- + +bool ui_rect_hit(mp_rect r, vec2 p) +{ + return( (p.x > r.x) + &&(p.x < r.x + r.w) + &&(p.y > r.y) + &&(p.y < r.y + r.h)); +} + +bool ui_box_hovering(ui_box* box, vec2 p) +{ + ui_context* ui = ui_get_context(); + + mp_rect clip = ui_clip_top(); + mp_rect rect = ui_intersect_rects(clip, box->rect); + bool hit = ui_rect_hit(rect, p); + bool result = hit && (!ui->hovered || box->z >= ui->hovered->z); + return(result); +} + +ui_box* ui_box_make_str8(str8 string, ui_flags flags) +{ + ui_context* ui = ui_get_context(); + + ui_key key = ui_key_make_str8(string); + ui_box* box = ui_box_lookup_key(key); + + if(!box) + { + box = mem_pool_alloc_type(&ui->boxPool, ui_box); + memset(box, 0, sizeof(ui_box)); + + box->key = key; + box->fresh = true; + ui_box_cache(ui, box); + } + else + { + box->fresh = false; + } + + //NOTE: setup hierarchy + if(box->frameCounter != ui->frameCounter) + { + list_init(&box->children); + box->parent = ui_box_top(); + if(box->parent) + { + list_append(&box->parent->children, &box->listElt); + box->parentClosed = box->parent->closed || box->parent->parentClosed; + } + + if(box->flags & UI_FLAG_OVERLAY) + { + list_append(&ui->overlayList, &box->overlayElt); + } + } + else + { + //maybe this should be a warning that we're trying to make the box twice in the same frame? + log_warning("trying to make ui box '%.*s' multiple times in the same frame\n", (int)box->string.len, box->string.ptr); + } + + //NOTE: setup per-frame state + box->frameCounter = ui->frameCounter; + box->string = str8_push_copy(&ui->frameArena, string); + box->flags = flags; + + //NOTE: create style and setup non-inherited attributes to default values + box->targetStyle = mem_arena_alloc_type(&ui->frameArena, ui_style); + ui_apply_style_with_mask(box->targetStyle, &UI_STYLE_DEFAULTS, ~0ULL); + + //NOTE: set tags, before rules and last box + box->tags = ui->nextBoxTags; + ui->nextBoxTags = (list_info){0}; + + box->beforeRules = ui->nextBoxBeforeRules; + for_list(&box->beforeRules, rule, ui_style_rule, boxElt) + { + rule->owner = box; + } + ui->nextBoxBeforeRules = (list_info){0}; + + box->afterRules = ui->nextBoxAfterRules; + for_list(&box->afterRules, rule, ui_style_rule, boxElt) + { + rule->owner = box; + } + ui->nextBoxAfterRules = (list_info){0}; + + + //NOTE: set scroll + if(ui_box_hovering(box, ui_mouse_position())) + { + vec2 wheel = ui_mouse_wheel(); + if(box->flags & UI_FLAG_SCROLL_WHEEL_X) + { + box->scroll.x += wheel.x; + } + if(box->flags & UI_FLAG_SCROLL_WHEEL_Y) + { + box->scroll.y += wheel.y; + } + } + return(box); +} + +ui_box* ui_box_begin_str8(str8 string, ui_flags flags) +{ + ui_context* ui = ui_get_context(); + ui_box* box = ui_box_make_str8(string, flags); + ui_box_push(box); + return(box); +} + +ui_box* ui_box_end(void) +{ + ui_context* ui = ui_get_context(); + ui_box* box = ui_box_top(); + DEBUG_ASSERT(box, "box stack underflow"); + + ui_box_pop(); + + return(box); +} + +void ui_box_set_draw_proc(ui_box* box, ui_box_draw_proc proc, void* data) +{ + box->drawProc = proc; + box->drawData = data; +} + +void ui_box_set_closed(ui_box* box, bool closed) +{ + box->closed = closed; +} + +bool ui_box_closed(ui_box* box) +{ + return(box->closed); +} + +void ui_box_activate(ui_box* box) +{ + box->active = true; +} + +void ui_box_deactivate(ui_box* box) +{ + box->active = false; +} + +bool ui_box_active(ui_box* box) +{ + return(box->active); +} + +void ui_box_set_hot(ui_box* box, bool hot) +{ + box->hot = hot; +} + +bool ui_box_hot(ui_box* box) +{ + return(box->hot); +} + +ui_sig ui_box_sig(ui_box* box) +{ + //NOTE: compute input signals + ui_sig sig = {0}; + + ui_context* ui = ui_get_context(); + mp_input_state* input = &ui->input; + + sig.box = box; + + if(!box->closed && !box->parentClosed) + { + vec2 mousePos = ui_mouse_position(); + + sig.hovering = ui_box_hovering(box, mousePos); + + if(box->flags & UI_FLAG_CLICKABLE) + { + if(sig.hovering) + { + sig.pressed = mp_mouse_pressed(input, MP_MOUSE_LEFT); + if(sig.pressed) + { + box->dragging = true; + } + sig.doubleClicked = mp_mouse_double_clicked(input, MP_MOUSE_LEFT); + sig.rightPressed = mp_mouse_pressed(input, MP_MOUSE_RIGHT); + } + + sig.released = mp_mouse_released(input, MP_MOUSE_LEFT); + if(sig.released) + { + if(box->dragging && sig.hovering) + { + sig.clicked = true; + } + } + + if(!mp_mouse_down(input, MP_MOUSE_LEFT)) + { + box->dragging = false; + } + + sig.dragging = box->dragging; + } + + sig.mouse = (vec2){mousePos.x - box->rect.x, mousePos.y - box->rect.y}; + sig.delta = ui_mouse_delta(); + sig.wheel = ui_mouse_wheel(); + } + return(sig); +} + +bool ui_box_hidden(ui_box* box) +{ + return(box->closed || box->parentClosed); +} + +//----------------------------------------------------------------------------- +// Auto-layout +//----------------------------------------------------------------------------- + +void ui_animate_f32(ui_context* ui, f32* value, f32 target, f32 animationTime) +{ + if( animationTime < 1e-6 + || fabs(*value - target) < 0.001) + { + *value = target; + } + else + { + + /*NOTE: + we use the euler approximation for df/dt = alpha(target - f) + the implicit form is f(t) = target*(1-e^(-alpha*t)) for the rising front, + and f(t) = e^(-alpha*t) for the falling front (e.g. classic RC circuit charge/discharge) + + Here we bake alpha = 1/tau = -ln(0.05)/tr, with tr the rise time to 95% of target + */ + f32 alpha = 3/animationTime; + f32 dt = ui->lastFrameDuration; + + *value += (target - *value)*alpha*dt; + } +} + +void ui_animate_color(ui_context* ui, mg_color* color, mg_color target, f32 animationTime) +{ + for(int i=0; i<4; i++) + { + ui_animate_f32(ui, &color->c[i], target.c[i], animationTime); + } +} + +void ui_animate_ui_size(ui_context* ui, ui_size* size, ui_size target, f32 animationTime) +{ + size->kind = target.kind; + ui_animate_f32(ui, &size->value, target.value, animationTime); + ui_animate_f32(ui, &size->relax, target.relax, animationTime); +} + +void ui_box_animate_style(ui_context* ui, ui_box* box) +{ + ui_style* targetStyle = box->targetStyle; + DEBUG_ASSERT(targetStyle); + + f32 animationTime = targetStyle->animationTime; + + //NOTE: interpolate based on transition values + ui_style_mask mask = box->targetStyle->animationMask; + + if(box->fresh) + { + box->style = *targetStyle; + } + else + { + if(mask & UI_STYLE_SIZE_WIDTH) + { + ui_animate_ui_size(ui, &box->style.size.c[UI_AXIS_X], targetStyle->size.c[UI_AXIS_X], animationTime); + } + else + { + box->style.size.c[UI_AXIS_X] = targetStyle->size.c[UI_AXIS_X]; + } + + if(mask & UI_STYLE_SIZE_HEIGHT) + { + ui_animate_ui_size(ui, &box->style.size.c[UI_AXIS_Y], targetStyle->size.c[UI_AXIS_Y], animationTime); + } + else + { + box->style.size.c[UI_AXIS_Y] = targetStyle->size.c[UI_AXIS_Y]; + } + + if(mask & UI_STYLE_COLOR) + { + ui_animate_color(ui, &box->style.color, targetStyle->color, animationTime); + } + else + { + box->style.color = targetStyle->color; + } + + + if(mask & UI_STYLE_BG_COLOR) + { + ui_animate_color(ui, &box->style.bgColor, targetStyle->bgColor, animationTime); + } + else + { + box->style.bgColor = targetStyle->bgColor; + } + + if(mask & UI_STYLE_BORDER_COLOR) + { + ui_animate_color(ui, &box->style.borderColor, targetStyle->borderColor, animationTime); + } + else + { + box->style.borderColor = targetStyle->borderColor; + } + + if(mask & UI_STYLE_FONT_SIZE) + { + ui_animate_f32(ui, &box->style.fontSize, targetStyle->fontSize, animationTime); + } + else + { + box->style.fontSize = targetStyle->fontSize; + } + + if(mask & UI_STYLE_BORDER_SIZE) + { + ui_animate_f32(ui, &box->style.borderSize, targetStyle->borderSize, animationTime); + } + else + { + box->style.borderSize = targetStyle->borderSize; + } + + if(mask & UI_STYLE_ROUNDNESS) + { + ui_animate_f32(ui, &box->style.roundness, targetStyle->roundness, animationTime); + } + else + { + box->style.roundness = targetStyle->roundness; + } + + //NOTE: float target is animated in compute rect + box->style.floatTarget = targetStyle->floatTarget; + + //TODO: non animatable attributes. use mask + box->style.layout = targetStyle->layout; + box->style.font = targetStyle->font; + } +} + +void ui_apply_style_with_mask(ui_style* dst, ui_style* src, ui_style_mask mask) +{ + if(mask & UI_STYLE_SIZE_WIDTH) + { + dst->size.c[UI_AXIS_X] = src->size.c[UI_AXIS_X]; + } + if(mask & UI_STYLE_SIZE_HEIGHT) + { + dst->size.c[UI_AXIS_Y] = src->size.c[UI_AXIS_Y]; + } + if(mask & UI_STYLE_LAYOUT_AXIS) + { + dst->layout.axis = src->layout.axis; + } + if(mask & UI_STYLE_LAYOUT_ALIGN_X) + { + dst->layout.align.x = src->layout.align.x; + } + if(mask & UI_STYLE_LAYOUT_ALIGN_Y) + { + dst->layout.align.y = src->layout.align.y; + } + if(mask & UI_STYLE_LAYOUT_SPACING) + { + dst->layout.spacing = src->layout.spacing; + } + if(mask & UI_STYLE_LAYOUT_MARGIN_X) + { + dst->layout.margin.x = src->layout.margin.x; + } + if(mask & UI_STYLE_LAYOUT_MARGIN_Y) + { + dst->layout.margin.y = src->layout.margin.y; + } + if(mask & UI_STYLE_FLOAT_X) + { + dst->floating.c[UI_AXIS_X] = src->floating.c[UI_AXIS_X]; + dst->floatTarget.x = src->floatTarget.x; + } + if(mask & UI_STYLE_FLOAT_Y) + { + dst->floating.c[UI_AXIS_Y] = src->floating.c[UI_AXIS_Y]; + dst->floatTarget.y = src->floatTarget.y; + } + if(mask & UI_STYLE_COLOR) + { + dst->color = src->color; + } + if(mask & UI_STYLE_BG_COLOR) + { + dst->bgColor = src->bgColor; + } + if(mask & UI_STYLE_BORDER_COLOR) + { + dst->borderColor = src->borderColor; + } + if(mask & UI_STYLE_BORDER_SIZE) + { + dst->borderSize = src->borderSize; + } + if(mask & UI_STYLE_ROUNDNESS) + { + dst->roundness = src->roundness; + } + if(mask & UI_STYLE_FONT) + { + dst->font = src->font; + } + if(mask & UI_STYLE_FONT_SIZE) + { + dst->fontSize = src->fontSize; + } + if(mask & UI_STYLE_ANIMATION_TIME) + { + dst->animationTime = src->animationTime; + } + if(mask & UI_STYLE_ANIMATION_MASK) + { + dst->animationMask = src->animationMask; + } +} + + +bool ui_style_selector_match(ui_box* box, ui_style_rule* rule, ui_selector* selector) +{ + bool res = false; + switch(selector->kind) + { + case UI_SEL_ANY: + res = true; + break; + + case UI_SEL_OWNER: + res = (box == rule->owner); + break; + + case UI_SEL_TEXT: + res = !str8_cmp(box->string, selector->text); + break; + + case UI_SEL_TAG: + { + for_list(&box->tags, elt, ui_tag_elt, listElt) + { + if(elt->tag.hash == selector->tag.hash) + { + res = true; + break; + } + } + } break; + + case UI_SEL_STATUS: + { + res = true; + if(selector->status & UI_HOVER) + { + res = res && ui_box_hovering(box, ui_mouse_position()); + } + if(selector->status & UI_ACTIVE) + { + res = res && box->active; + } + if(selector->status & UI_DRAGGING) + { + res = res && box->dragging; + } + } break; + + case UI_SEL_KEY: + res = ui_key_equal(box->key, selector->key); + default: + break; + } + return(res); +} + +void ui_style_rule_match(ui_context* ui, ui_box* box, ui_style_rule* rule, list_info* buildList, list_info* tmpList) +{ + ui_selector* selector = list_first_entry(&rule->pattern.l, ui_selector, listElt); + bool match = ui_style_selector_match(box, rule, selector); + + selector = list_next_entry(&rule->pattern.l, selector, ui_selector, listElt); + while(match && selector && selector->op == UI_SEL_AND) + { + match = match && ui_style_selector_match(box, rule, selector); + selector = list_next_entry(&rule->pattern.l, selector, ui_selector, listElt); + } + + if(match) + { + if(!selector) + { + ui_apply_style_with_mask(box->targetStyle, rule->style, rule->mask); + } + else + { + //NOTE create derived rule if there's more than one selector + ui_style_rule* derived = mem_arena_alloc_type(&ui->frameArena, ui_style_rule); + derived->mask = rule->mask; + derived->style = rule->style; + derived->pattern.l = (list_info){&selector->listElt, rule->pattern.l.last}; + + list_append(buildList, &derived->buildElt); + list_append(tmpList, &derived->tmpElt); + } + } +} + +void ui_styling_prepass(ui_context* ui, ui_box* box, list_info* before, list_info* after) +{ + //NOTE: inherit style from parent + if(box->parent) + { + ui_apply_style_with_mask(box->targetStyle, + box->parent->targetStyle, + UI_STYLE_MASK_INHERITED); + } + + + //NOTE: append box before rules to before and tmp + list_info tmpBefore = {0}; + for_list(&box->beforeRules, rule, ui_style_rule, boxElt) + { + list_append(before, &rule->buildElt); + list_append(&tmpBefore, &rule->tmpElt); + } + //NOTE: match before rules + for_list(before, rule, ui_style_rule, buildElt) + { + ui_style_rule_match(ui, box, rule, before, &tmpBefore); + } + + //NOTE: prepend box after rules to after and append them to tmp + list_info tmpAfter = {0}; + for_list_reverse(&box->afterRules, rule, ui_style_rule, boxElt) + { + list_push(after, &rule->buildElt); + list_append(&tmpAfter, &rule->tmpElt); + } + + //NOTE: match after rules + for_list(after, rule, ui_style_rule, buildElt) + { + ui_style_rule_match(ui, box, rule, after, &tmpAfter); + } + + //NOTE: compute static sizes + ui_box_animate_style(ui, box); + + if(ui_box_hidden(box)) + { + return; + } + + ui_style* style = &box->style; + + mp_rect textBox = {0}; + ui_size desiredSize[2] = {box->style.size.c[UI_AXIS_X], + box->style.size.c[UI_AXIS_Y]}; + + if( desiredSize[UI_AXIS_X].kind == UI_SIZE_TEXT + ||desiredSize[UI_AXIS_Y].kind == UI_SIZE_TEXT) + { + textBox = mg_text_bounding_box(style->font, style->fontSize, box->string); + } + + for(int i=0; ilayout.margin.c[i]; + box->rect.c[2+i] = textBox.c[2+i] + margin*2; + } + else if(size.kind == UI_SIZE_PIXELS) + { + box->rect.c[2+i] = size.value; + } + } + + //NOTE: descend in children + for_list(&box->children, child, ui_box, listElt) + { + ui_styling_prepass(ui, child, before, after); + } + + //NOTE: remove temporary rules + for_list(&tmpBefore, rule, ui_style_rule, tmpElt) + { + list_remove(before, &rule->buildElt); + } + for_list(&tmpAfter, rule, ui_style_rule, tmpElt) + { + list_remove(after, &rule->buildElt); + } +} + +bool ui_layout_downward_dependency(ui_box* child, int axis) +{ + return( !ui_box_hidden(child) + && !child->style.floating.c[axis] + && child->style.size.c[axis].kind != UI_SIZE_PARENT + && child->style.size.c[axis].kind != UI_SIZE_PARENT_MINUS_PIXELS); +} + +void ui_layout_downward_dependent_size(ui_context* ui, ui_box* box, int axis) +{ + //NOTE: layout children and compute spacing + f32 count = 0; + for_list(&box->children, child, ui_box, listElt) + { + if(!ui_box_hidden(child)) + { + ui_layout_downward_dependent_size(ui, child, axis); + + if( box->style.layout.axis == axis + && !child->style.floating.c[axis]) + { + count++; + } + } + } + box->spacing[axis] = maximum(0, count-1)*box->style.layout.spacing; + + ui_size* size = &box->style.size.c[axis]; + if(size->kind == UI_SIZE_CHILDREN) + { + //NOTE: if box is dependent on children, compute children's size. If we're in the layout + // axis this is the sum of each child size, otherwise it is the maximum child size + f32 sum = 0; + + if(box->style.layout.axis == axis) + { + for_list(&box->children, child, ui_box, listElt) + { + if(ui_layout_downward_dependency(child, axis)) + { + sum += child->rect.c[2+axis]; + } + } + } + else + { + for_list(&box->children, child, ui_box, listElt) + { + if(ui_layout_downward_dependency(child, axis)) + { + sum = maximum(sum, child->rect.c[2+axis]); + } + } + } + f32 margin = box->style.layout.margin.c[axis]; + box->rect.c[2+axis] = sum + box->spacing[axis] + 2*margin; + } +} + +void ui_layout_upward_dependent_size(ui_context* ui, ui_box* box, int axis) +{ + //NOTE: re-compute/set size of children that depend on box's size + + f32 margin = box->style.layout.margin.c[axis]; + f32 availableSize = maximum(0, box->rect.c[2+axis] - box->spacing[axis] - 2*margin); + + for_list(&box->children, child, ui_box, listElt) + { + ui_size* size = &child->style.size.c[axis]; + if(size->kind == UI_SIZE_PARENT) + { + child->rect.c[2+axis] = availableSize * size->value; + } + else if(size->kind == UI_SIZE_PARENT_MINUS_PIXELS) + { + child->rect.c[2+axis] = maximum(0, availableSize - size->value); + } + } + + //NOTE: solve downard conflicts + int overflowFlag = (UI_FLAG_ALLOW_OVERFLOW_X << axis); + f32 sum = 0; + + if(box->style.layout.axis == axis) + { + //NOTE: if we're solving in the layout axis, first compute total sum of children and + // total slack available + f32 slack = 0; + + for_list(&box->children, child, ui_box, listElt) + { + if( !ui_box_hidden(child) + && !child->style.floating.c[axis]) + { + sum += child->rect.c[2+axis]; + slack += child->rect.c[2+axis] * child->style.size.c[axis].relax; + } + } + + if(!(box->flags & overflowFlag)) + { + //NOTE: then remove excess proportionally to each box slack, and recompute children sum. + f32 totalContents = sum + box->spacing[axis] + 2*box->style.layout.margin.c[axis]; + f32 excess = ClampLowBound(totalContents - box->rect.c[2+axis], 0); + f32 alpha = Clamp(excess / slack, 0, 1); + + sum = 0; + for_list(&box->children, child, ui_box, listElt) + { + f32 relax = child->style.size.c[axis].relax; + child->rect.c[2+axis] -= alpha * child->rect.c[2+axis] * relax; + sum += child->rect.c[2+axis]; + } + } + } + else + { + //NOTE: if we're solving on the secondary axis, we remove excess to each box individually + // according to its own slack. Children sum is the maximum child size. + + for_list(&box->children, child, ui_box, listElt) + { + if(!ui_box_hidden(child) && !child->style.floating.c[axis]) + { + if(!(box->flags & overflowFlag)) + { + f32 totalContents = child->rect.c[2+axis] + 2*box->style.layout.margin.c[axis]; + f32 excess = ClampLowBound(totalContents - box->rect.c[2+axis], 0); + f32 relax = child->style.size.c[axis].relax; + child->rect.c[2+axis] -= minimum(excess, child->rect.c[2+axis]*relax); + } + sum = maximum(sum, child->rect.c[2+axis]); + } + } + } + + box->childrenSum[axis] = sum; + + //NOTE: recurse in children + for_list(&box->children, child, ui_box, listElt) + { + ui_layout_upward_dependent_size(ui, child, axis); + } +} + +void ui_layout_compute_rect(ui_context* ui, ui_box* box, vec2 pos) +{ + if(ui_box_hidden(box)) + { + return; + } + + box->rect.x = pos.x; + box->rect.y = pos.y; + box->z = ui->z; + ui->z++; + + ui_axis layoutAxis = box->style.layout.axis; + ui_axis secondAxis = (layoutAxis == UI_AXIS_X) ? UI_AXIS_Y : UI_AXIS_X; + f32 spacing = box->style.layout.spacing; + + ui_align* align = box->style.layout.align.c; + + vec2 origin = {box->rect.x, + box->rect.y}; + vec2 currentPos = origin; + + vec2 margin = {box->style.layout.margin.x, + box->style.layout.margin.y}; + + currentPos.x += margin.x; + currentPos.y += margin.y; + + for(int i=0; irect.c[2+i] - (box->childrenSum[i] + box->spacing[i] + margin.c[i]); + } + } + if(align[layoutAxis] == UI_ALIGN_CENTER) + { + currentPos.c[layoutAxis] = origin.c[layoutAxis] + + 0.5*(box->rect.c[2+layoutAxis] + - (box->childrenSum[layoutAxis] + box->spacing[layoutAxis])); + } + + currentPos.x -= box->scroll.x; + currentPos.y -= box->scroll.y; + + for_list(&box->children, child, ui_box, listElt) + { + if(align[secondAxis] == UI_ALIGN_CENTER) + { + currentPos.c[secondAxis] = origin.c[secondAxis] + 0.5*(box->rect.c[2+secondAxis] - child->rect.c[2+secondAxis]); + } + + vec2 childPos = currentPos; + for(int i=0; istyle.floating.c[i]) + { + ui_style* style = child->targetStyle; + if((child->targetStyle->animationMask & (UI_STYLE_FLOAT_X << i)) + && !child->fresh) + { + ui_animate_f32(ui, &child->floatPos.c[i], child->style.floatTarget.c[i], style->animationTime); + } + else + { + child->floatPos.c[i] = child->style.floatTarget.c[i]; + } + childPos.c[i] = origin.c[i] + child->floatPos.c[i]; + } + } + + ui_layout_compute_rect(ui, child, childPos); + + if(!child->style.floating.c[layoutAxis]) + { + currentPos.c[layoutAxis] += child->rect.c[2+layoutAxis] + spacing; + } + } +} + +void ui_layout_find_next_hovered_recursive(ui_context* ui, ui_box* box, vec2 p) +{ + if(ui_box_hidden(box)) + { + return; + } + + bool hit = ui_rect_hit(box->rect, p); + if(hit && (box->flags & UI_FLAG_BLOCK_MOUSE)) + { + ui->hovered = box; + } + if(hit || !(box->flags & UI_FLAG_CLIP)) + { + for_list(&box->children, child, ui_box, listElt) + { + ui_layout_find_next_hovered_recursive(ui, child, p); + } + } +} + +void ui_layout_find_next_hovered(ui_context* ui, vec2 p) +{ + ui->hovered = 0; + ui_layout_find_next_hovered_recursive(ui, ui->root, p); +} + +void ui_solve_layout(ui_context* ui) +{ + list_info beforeRules = {0}; + list_info afterRules = {0}; + + //NOTE: style and compute static sizes + ui_styling_prepass(ui, ui->root, &beforeRules, &afterRules); + + //NOTE: reparent overlay boxes + for_list(&ui->overlayList, box, ui_box, overlayElt) + { + if(box->parent) + { + list_remove(&box->parent->children, &box->listElt); + list_append(&ui->overlay->children, &box->listElt); + } + } + + //NOTE: compute layout + for(int axis=0; axisroot, axis); + ui_layout_upward_dependent_size(ui, ui->root, axis); + } + ui_layout_compute_rect(ui, ui->root, (vec2){0, 0}); + + vec2 p = ui_mouse_position(); + ui_layout_find_next_hovered(ui, p); +} + +//----------------------------------------------------------------------------- +// Drawing +//----------------------------------------------------------------------------- + +void ui_rectangle_fill(mp_rect rect, f32 roundness) +{ + if(roundness) + { + mg_rounded_rectangle_fill(rect.x, rect.y, rect.w, rect.h, roundness); + } + else + { + mg_rectangle_fill(rect.x, rect.y, rect.w, rect.h); + } +} + +void ui_rectangle_stroke(mp_rect rect, f32 roundness) +{ + if(roundness) + { + mg_rounded_rectangle_stroke(rect.x, rect.y, rect.w, rect.h, roundness); + } + else + { + mg_rectangle_stroke(rect.x, rect.y, rect.w, rect.h); + } +} + +void ui_draw_box(ui_box* box) +{ + if(ui_box_hidden(box)) + { + return; + } + + ui_style* style = &box->style; + + if(box->flags & UI_FLAG_CLIP) + { + mg_clip_push(box->rect.x, box->rect.y, box->rect.w, box->rect.h); + } + + if(box->flags & UI_FLAG_DRAW_BACKGROUND) + { + mg_set_color(style->bgColor); + ui_rectangle_fill(box->rect, style->roundness); + } + + if((box->flags & UI_FLAG_DRAW_PROC) && box->drawProc) + { + box->drawProc(box, box->drawData); + } + + for_list(&box->children, child, ui_box, listElt) + { + ui_draw_box(child); + } + + if(box->flags & UI_FLAG_DRAW_TEXT) + { + mp_rect textBox = mg_text_bounding_box(style->font, style->fontSize, box->string); + + f32 x = 0; + f32 y = 0; + switch(style->layout.align.x) + { + case UI_ALIGN_START: + x = box->rect.x + style->layout.margin.x; + break; + + case UI_ALIGN_END: + x = box->rect.x + box->rect.w - style->layout.margin.x - textBox.w; + break; + + case UI_ALIGN_CENTER: + x = box->rect.x + 0.5*(box->rect.w - textBox.w); + break; + } + + switch(style->layout.align.y) + { + case UI_ALIGN_START: + y = box->rect.y + style->layout.margin.y - textBox.y; + break; + + case UI_ALIGN_END: + y = box->rect.y + box->rect.h - style->layout.margin.y - textBox.h + textBox.y; + break; + + case UI_ALIGN_CENTER: + y = box->rect.y + 0.5*(box->rect.h - textBox.h) - textBox.y; + break; + } + + mg_set_font(style->font); + mg_set_font_size(style->fontSize); + mg_set_color(style->color); + + mg_move_to(x, y); + mg_text_outlines(box->string); + mg_fill(); + } + + if(box->flags & UI_FLAG_CLIP) + { + mg_clip_pop(); + } + + if(box->flags & UI_FLAG_DRAW_BORDER) + { + mg_set_width(style->borderSize); + mg_set_color(style->borderColor); + ui_rectangle_stroke(box->rect, style->roundness); + } +} + +void ui_draw() +{ + ui_context* ui = ui_get_context(); + + //NOTE: draw + bool oldTextFlip = mg_get_text_flip(); + mg_set_text_flip(false); + + ui_draw_box(ui->root); + + mg_set_text_flip(oldTextFlip); +} + +//----------------------------------------------------------------------------- +// frame begin/end +//----------------------------------------------------------------------------- + +void ui_begin_frame(vec2 size, ui_style* defaultStyle, ui_style_mask defaultMask) +{ + ui_context* ui = ui_get_context(); + + mem_arena_clear(&ui->frameArena); + + ui->frameCounter++; + f64 time = mp_get_time(MP_CLOCK_MONOTONIC); + ui->lastFrameDuration = time - ui->frameTime; + ui->frameTime = time; + + ui->clipStack = 0; + ui->z = 0; + + defaultMask &= UI_STYLE_COLOR + | UI_STYLE_BG_COLOR + | UI_STYLE_BORDER_COLOR + | UI_STYLE_FONT + | UI_STYLE_FONT_SIZE; + + ui_style_match_before(ui_pattern_all(), defaultStyle, defaultMask); + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, size.x}, + .size.height = {UI_SIZE_PIXELS, size.y}}, + UI_STYLE_SIZE); + + ui->root = ui_box_begin("_root_", 0); + + ui_style_mask contentStyleMask = UI_STYLE_SIZE + | UI_STYLE_LAYOUT + | UI_STYLE_FLOAT; + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, + .size.height = {UI_SIZE_PARENT, 1}, + .layout = {UI_AXIS_Y, UI_ALIGN_START, UI_ALIGN_START}, + .floating = {true, true}, + .floatTarget = {0, 0}}, + contentStyleMask); + + ui_box* contents = ui_box_make("_contents_", 0); + + ui_style_next(&(ui_style){.layout = {UI_AXIS_Y, UI_ALIGN_START, UI_ALIGN_START}, + .floating = {true, true}, + .floatTarget = {0, 0}}, + UI_STYLE_LAYOUT | UI_STYLE_FLOAT_X | UI_STYLE_FLOAT_Y); + + ui->overlay = ui_box_make("_overlay_", 0); + ui->overlayList = (list_info){0}; + + ui->nextBoxBeforeRules = (list_info){0}; + ui->nextBoxAfterRules = (list_info){0}; + ui->nextBoxTags = (list_info){0}; + + ui_box_push(contents); +} + +void ui_end_frame(void) +{ + ui_context* ui = ui_get_context(); + + ui_box_pop(); + + ui_box* box = ui_box_end(); + DEBUG_ASSERT(box == ui->root, "unbalanced box stack"); + + //TODO: check balancing of style stacks + + //NOTE: layout + ui_solve_layout(ui); + + //NOTE: prune unused boxes + for(int i=0; iboxMap[i], box, ui_box, bucketElt) + { + if(box->frameCounter < ui->frameCounter) + { + list_remove(&ui->boxMap[i], &box->bucketElt); + } + } + } + + mp_input_next_frame(&ui->input); +} + +//----------------------------------------------------------------------------- +// Init / cleanup +//----------------------------------------------------------------------------- +void ui_init(ui_context* ui) +{ + __uiCurrentContext = &__uiThreadContext; + + memset(ui, 0, sizeof(ui_context)); + mem_arena_init(&ui->frameArena); + mem_pool_init(&ui->boxPool, sizeof(ui_box)); + ui->init = true; + + ui_set_context(ui); +} + +void ui_cleanup(void) +{ + ui_context* ui = ui_get_context(); + mem_arena_release(&ui->frameArena); + mem_pool_release(&ui->boxPool); + ui->init = false; +} + + +//----------------------------------------------------------------------------- +// label +//----------------------------------------------------------------------------- + +ui_sig ui_label_str8(str8 label) +{ + ui_style_next(&(ui_style){.size.width = {UI_SIZE_TEXT, 0, 0}, + .size.height = {UI_SIZE_TEXT, 0, 0}}, + UI_STYLE_SIZE_WIDTH | UI_STYLE_SIZE_HEIGHT); + + ui_flags flags = UI_FLAG_CLIP + | UI_FLAG_DRAW_TEXT; + ui_box* box = ui_box_make_str8(label, flags); + + ui_sig sig = ui_box_sig(box); + return(sig); +} + +ui_sig ui_label(const char* label) +{ + return(ui_label_str8(STR8((char*)label))); +} + +//------------------------------------------------------------------------------ +// button +//------------------------------------------------------------------------------ + +ui_sig ui_button_behavior(ui_box* box) +{ + ui_sig sig = ui_box_sig(box); + + if(sig.hovering) + { + ui_box_set_hot(box, true); + if(sig.dragging) + { + ui_box_activate(box); + } + } + else + { + ui_box_set_hot(box, false); + } + if(!sig.dragging) + { + ui_box_deactivate(box); + } + return(sig); +} + +ui_sig ui_button_str8(str8 label) +{ + ui_context* ui = ui_get_context(); + + ui_style defaultStyle = {.size.width = {UI_SIZE_TEXT}, + .size.height = {UI_SIZE_TEXT}, + .layout.align.x = UI_ALIGN_CENTER, + .layout.align.y = UI_ALIGN_CENTER, + .layout.margin.x = 5, + .layout.margin.y = 5, + .bgColor = {0.5, 0.5, 0.5, 1}, + .borderColor = {0.2, 0.2, 0.2, 1}, + .borderSize = 1, + .roundness = 10}; + + ui_style_mask defaultMask = UI_STYLE_SIZE_WIDTH + | UI_STYLE_SIZE_HEIGHT + | UI_STYLE_LAYOUT_MARGIN_X + | UI_STYLE_LAYOUT_MARGIN_Y + | UI_STYLE_LAYOUT_ALIGN_X + | UI_STYLE_LAYOUT_ALIGN_Y + | UI_STYLE_BG_COLOR + | UI_STYLE_BORDER_COLOR + | UI_STYLE_BORDER_SIZE + | UI_STYLE_ROUNDNESS; + + ui_style_next(&defaultStyle, defaultMask); + + ui_style activeStyle = {.bgColor = {0.3, 0.3, 0.3, 1}, + .borderColor = {0.2, 0.2, 0.2, 1}, + .borderSize = 2}; + ui_style_mask activeMask = UI_STYLE_BG_COLOR + | UI_STYLE_BORDER_COLOR + | UI_STYLE_BORDER_SIZE; + ui_pattern activePattern = {0}; + ui_pattern_push(&ui->frameArena, + &activePattern, + (ui_selector){.kind = UI_SEL_STATUS, + .status = UI_ACTIVE|UI_HOVER}); + ui_style_match_before(activePattern, &activeStyle, activeMask); + + ui_flags flags = UI_FLAG_CLICKABLE + | UI_FLAG_CLIP + | UI_FLAG_DRAW_BACKGROUND + | UI_FLAG_DRAW_BORDER + | UI_FLAG_DRAW_TEXT + | UI_FLAG_HOT_ANIMATION + | UI_FLAG_ACTIVE_ANIMATION; + + ui_box* box = ui_box_make_str8(label, flags); + ui_tag_box(box, "button"); + + ui_sig sig = ui_button_behavior(box); + return(sig); +} + +ui_sig ui_button(const char* label) +{ + return(ui_button_str8(STR8((char*)label))); +} + +void ui_checkbox_draw(ui_box* box, void* data) +{ + bool checked = *(bool*)data; + if(checked) + { + mg_move_to(box->rect.x + 0.2*box->rect.w, box->rect.y + 0.5*box->rect.h); + mg_line_to(box->rect.x + 0.4*box->rect.w, box->rect.y + 0.75*box->rect.h); + mg_line_to(box->rect.x + 0.8*box->rect.w, box->rect.y + 0.2*box->rect.h); + + mg_set_color(box->style.color); + mg_set_width(0.2*box->rect.w); + mg_set_joint(MG_JOINT_MITER); + mg_set_max_joint_excursion(0.2 * box->rect.h); + mg_stroke(); + } +} + +ui_sig ui_checkbox(const char* name, bool* checked) +{ + ui_context* ui = ui_get_context(); + + ui_style defaultStyle = {.size.width = {UI_SIZE_PIXELS, 20}, + .size.height = {UI_SIZE_PIXELS, 20}, + .bgColor = {1, 1, 1, 1}, + .color = {0, 0, 0, 1}, + .borderColor = {0.2, 0.2, 0.2, 1}, + .borderSize = 1, + .roundness = 5}; + + ui_style_mask defaultMask = UI_STYLE_SIZE_WIDTH + | UI_STYLE_SIZE_HEIGHT + | UI_STYLE_BG_COLOR + | UI_STYLE_COLOR + | UI_STYLE_BORDER_COLOR + | UI_STYLE_BORDER_SIZE + | UI_STYLE_ROUNDNESS; + + ui_style_next(&defaultStyle, defaultMask); + + ui_style activeStyle = {.bgColor = {0.5, 0.5, 0.5, 1}, + .borderColor = {0.2, 0.2, 0.2, 1}, + .borderSize = 2}; + ui_style_mask activeMask = UI_STYLE_BG_COLOR + | UI_STYLE_BORDER_COLOR + | UI_STYLE_BORDER_SIZE; + ui_pattern activePattern = {0}; + ui_pattern_push(&ui->frameArena, + &activePattern, + (ui_selector){.kind = UI_SEL_STATUS, + .status = UI_ACTIVE|UI_HOVER}); + ui_style_match_before(activePattern, &activeStyle, activeMask); + + ui_flags flags = UI_FLAG_CLICKABLE + | UI_FLAG_CLIP + | UI_FLAG_DRAW_BACKGROUND + | UI_FLAG_DRAW_PROC + | UI_FLAG_DRAW_BORDER + | UI_FLAG_HOT_ANIMATION + | UI_FLAG_ACTIVE_ANIMATION; + + ui_box* box = ui_box_make(name, flags); + ui_tag_box(box, "checkbox"); + + ui_sig sig = ui_button_behavior(box); + if(sig.clicked) + { + *checked = !*checked; + } + ui_box_set_draw_proc(box, ui_checkbox_draw, checked); + + return(sig); +} + +//------------------------------------------------------------------------------ +// slider / scrollbar +//------------------------------------------------------------------------------ +ui_box* ui_slider(const char* label, f32 thumbRatio, f32* scrollValue) +{ + ui_style_match_before(ui_pattern_all(), &(ui_style){0}, UI_STYLE_LAYOUT); + ui_box* frame = ui_box_begin(label, 0); + { + f32 beforeRatio = (*scrollValue) * (1. - thumbRatio); + f32 afterRatio = (1. - *scrollValue) * (1. - thumbRatio); + + ui_axis trackAxis = (frame->rect.w > frame->rect.h) ? UI_AXIS_X : UI_AXIS_Y; + ui_axis secondAxis = (trackAxis == UI_AXIS_Y) ? UI_AXIS_X : UI_AXIS_Y; + f32 roundness = 0.5*frame->rect.c[2+secondAxis]; + f32 animationTime = 0.5; + + ui_style trackStyle = {.size.width = {UI_SIZE_PARENT, 1}, + .size.height = {UI_SIZE_PARENT, 1}, + .layout.axis = trackAxis, + .layout.align.x = UI_ALIGN_START, + .layout.align.y = UI_ALIGN_START, + .bgColor = {0.5, 0.5, 0.5, 1}, + .roundness = roundness}; + + ui_style beforeStyle = trackStyle; + beforeStyle.size.c[trackAxis] = (ui_size){UI_SIZE_PARENT, beforeRatio}; + + ui_style afterStyle = trackStyle; + afterStyle.size.c[trackAxis] = (ui_size){UI_SIZE_PARENT, afterRatio}; + + ui_style thumbStyle = trackStyle; + thumbStyle.size.c[trackAxis] = (ui_size){UI_SIZE_PARENT, thumbRatio}; + thumbStyle.bgColor = (mg_color){0.3, 0.3, 0.3, 1}; + + ui_style_mask styleMask = UI_STYLE_SIZE_WIDTH + | UI_STYLE_SIZE_HEIGHT + | UI_STYLE_LAYOUT + | UI_STYLE_BG_COLOR + | UI_STYLE_ROUNDNESS; + + ui_flags trackFlags = UI_FLAG_CLIP + | UI_FLAG_DRAW_BACKGROUND + | UI_FLAG_HOT_ANIMATION + | UI_FLAG_ACTIVE_ANIMATION; + + ui_style_next(&trackStyle, styleMask); + ui_box* track = ui_box_begin("track", trackFlags); + + ui_style_next(&beforeStyle, UI_STYLE_SIZE_WIDTH|UI_STYLE_SIZE_HEIGHT); + ui_box* beforeSpacer = ui_box_make("before", 0); + + + ui_flags thumbFlags = UI_FLAG_CLICKABLE + | UI_FLAG_DRAW_BACKGROUND + | UI_FLAG_HOT_ANIMATION + | UI_FLAG_ACTIVE_ANIMATION; + + ui_style_next(&thumbStyle, styleMask); + ui_box* thumb = ui_box_make("thumb", thumbFlags); + + + ui_style_next(&afterStyle, UI_STYLE_SIZE_WIDTH|UI_STYLE_SIZE_HEIGHT); + ui_box* afterSpacer = ui_box_make("after", 0); + + ui_box_end(); + + //NOTE: interaction + ui_sig thumbSig = ui_box_sig(thumb); + if(thumbSig.dragging) + { + f32 trackExtents = track->rect.c[2+trackAxis] - thumb->rect.c[2+trackAxis]; + f32 delta = thumbSig.delta.c[trackAxis]/trackExtents; + f32 oldValue = *scrollValue; + + *scrollValue += delta; + *scrollValue = Clamp(*scrollValue, 0, 1); + } + + ui_sig trackSig = ui_box_sig(track); + + if(ui_box_active(frame)) + { + //NOTE: activated from outside + ui_box_set_hot(track, true); + ui_box_set_hot(thumb, true); + ui_box_activate(track); + ui_box_activate(thumb); + } + + if(trackSig.hovering) + { + ui_box_set_hot(track, true); + ui_box_set_hot(thumb, true); + } + else if(thumbSig.wheel.c[trackAxis] == 0) + { + ui_box_set_hot(track, false); + ui_box_set_hot(thumb, false); + } + + if(thumbSig.dragging) + { + ui_box_activate(track); + ui_box_activate(thumb); + } + else if(thumbSig.wheel.c[trackAxis] == 0) + { + ui_box_deactivate(track); + ui_box_deactivate(thumb); + ui_box_deactivate(frame); + } + + } ui_box_end(); + + return(frame); +} + +//------------------------------------------------------------------------------ +// panels +//------------------------------------------------------------------------------ +void ui_panel_begin(const char* str, ui_flags flags) +{ + flags = flags + | UI_FLAG_CLIP + | UI_FLAG_BLOCK_MOUSE + | UI_FLAG_ALLOW_OVERFLOW_X + | UI_FLAG_ALLOW_OVERFLOW_Y + | UI_FLAG_SCROLL_WHEEL_X + | UI_FLAG_SCROLL_WHEEL_Y; + + ui_box_begin(str, flags); +} + +void ui_panel_end(void) +{ + ui_box* panel = ui_box_top(); + ui_sig sig = ui_box_sig(panel); + + f32 contentsW = ClampLowBound(panel->childrenSum[0], panel->rect.w); + f32 contentsH = ClampLowBound(panel->childrenSum[1], panel->rect.h); + + contentsW = ClampLowBound(contentsW, 1); + contentsH = ClampLowBound(contentsH, 1); + + ui_box* scrollBarX = 0; + ui_box* scrollBarY = 0; + + bool needsScrollX = contentsW > panel->rect.w; + bool needsScrollY = contentsH > panel->rect.h; + + if(needsScrollX) + { + f32 thumbRatioX = panel->rect.w / contentsW; + f32 sliderX = panel->scroll.x /(contentsW - panel->rect.w); + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1., 0}, + .size.height = {UI_SIZE_PIXELS, 10, 0}, + .floating.x = true, + .floating.y = true, + .floatTarget = {0, panel->rect.h - 10}}, + UI_STYLE_SIZE + |UI_STYLE_FLOAT); + + scrollBarX = ui_slider("scrollerX", thumbRatioX, &sliderX); + + panel->scroll.x = sliderX * (contentsW - panel->rect.w); + if(sig.hovering) + { + ui_box_activate(scrollBarX); + } + } + + if(needsScrollY) + { + f32 thumbRatioY = panel->rect.h / contentsH; + f32 sliderY = panel->scroll.y /(contentsH - panel->rect.h); + + f32 spacerSize = needsScrollX ? 10 : 0; + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, 10, 0}, + .size.height = {UI_SIZE_PARENT_MINUS_PIXELS, spacerSize, 0}, + .floating.x = true, + .floating.y = true, + .floatTarget = {panel->rect.w - 10, 0}}, + UI_STYLE_SIZE + |UI_STYLE_FLOAT); + + scrollBarY = ui_slider("scrollerY", thumbRatioY, &sliderY); + + panel->scroll.y = sliderY * (contentsH - panel->rect.h); + if(sig.hovering) + { + ui_box_activate(scrollBarY); + } + } + panel->scroll.x = Clamp(panel->scroll.x, 0, contentsW - panel->rect.w); + panel->scroll.y = Clamp(panel->scroll.y, 0, contentsH - panel->rect.h); + + ui_box_end(); +} + +//------------------------------------------------------------------------------ +// tooltips +//------------------------------------------------------------------------------ + +ui_sig ui_tooltip_begin(const char* name) +{ + ui_context* ui = ui_get_context(); + + vec2 p = ui_mouse_position(); + + ui_style style = {.size.width = {UI_SIZE_CHILDREN}, + .size.height = {UI_SIZE_CHILDREN}, + .floating.x = true, + .floating.y = true, + .floatTarget = {p.x, p.y}}; + ui_style_mask mask = UI_STYLE_SIZE | UI_STYLE_FLOAT; + + ui_style_next(&style, mask); + + ui_flags flags = UI_FLAG_OVERLAY + | UI_FLAG_DRAW_BACKGROUND + | UI_FLAG_DRAW_BORDER; + + ui_box* tooltip = ui_box_make(name, flags); + ui_box_push(tooltip); + + return(ui_box_sig(tooltip)); +} + +void ui_tooltip_end(void) +{ + ui_box_pop(); // tooltip +} + +//------------------------------------------------------------------------------ +// Menus +//------------------------------------------------------------------------------ + +void ui_menu_bar_begin(const char* name) +{ + ui_style style = {.size.width = {UI_SIZE_PARENT, 1, 0}, + .size.height = {UI_SIZE_CHILDREN}, + .layout.axis = UI_AXIS_X, + .layout.spacing = 20,}; + ui_style_mask mask = UI_STYLE_SIZE + | UI_STYLE_LAYOUT_AXIS + | UI_STYLE_LAYOUT_SPACING; + + ui_style_next(&style, mask); + ui_box* bar = ui_box_begin(name, UI_FLAG_DRAW_BACKGROUND); + + ui_sig sig = ui_box_sig(bar); + ui_context* ui = ui_get_context(); + if(!sig.hovering && mp_mouse_released(&ui->input, MP_MOUSE_LEFT)) + { + ui_box_deactivate(bar); + } +} + +void ui_menu_bar_end(void) +{ + ui_box_end(); // menu bar +} + +void ui_menu_begin(const char* label) +{ + ui_box* container = ui_box_make(label, 0); + ui_box_push(container); + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_TEXT}, + .size.height = {UI_SIZE_TEXT}}, + UI_STYLE_SIZE); + + ui_box* button = ui_box_make(label, UI_FLAG_CLICKABLE | UI_FLAG_DRAW_TEXT); + ui_box* bar = container->parent; + + ui_sig sig = ui_box_sig(button); + ui_sig barSig = ui_box_sig(bar); + + ui_context* ui = ui_get_context(); + + ui_style style = {.size.width = {UI_SIZE_CHILDREN}, + .size.height = {UI_SIZE_CHILDREN}, + .floating.x = true, + .floating.y = true, + .floatTarget = {button->rect.x, + button->rect.y + button->rect.h}, + .layout.axis = UI_AXIS_Y, + .layout.spacing = 5, + .layout.margin.x = 0, + .layout.margin.y = 5, + .bgColor = {0.2, 0.2, 0.2, 1}}; + + ui_style_mask mask = UI_STYLE_SIZE + | UI_STYLE_FLOAT + | UI_STYLE_LAYOUT + | UI_STYLE_BG_COLOR; + + ui_flags flags = UI_FLAG_OVERLAY + | UI_FLAG_DRAW_BACKGROUND + | UI_FLAG_DRAW_BORDER; + + ui_style_next(&style, mask); + ui_box* menu = ui_box_make("panel", flags); + + if(ui_box_active(bar)) + { + if(sig.hovering) + { + ui_box_activate(button); + } + else if(barSig.hovering) + { + ui_box_deactivate(button); + } + } + else + { + ui_box_deactivate(button); + if(sig.pressed) + { + ui_box_activate(bar); + ui_box_activate(button); + } + } + + ui_box_set_closed(menu, !ui_box_active(button)); + ui_box_push(menu); +} + +void ui_menu_end(void) +{ + ui_box_pop(); // menu + ui_box_pop(); // container +} + +ui_sig ui_menu_button(const char* name) +{ + ui_context* ui = ui_get_context(); + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_TEXT}, + .size.height = {UI_SIZE_TEXT}, + .layout.margin.x = 5, + .bgColor = {0, 0, 0, 0}}, + UI_STYLE_SIZE + |UI_STYLE_LAYOUT_MARGIN_X + |UI_STYLE_BG_COLOR); + + ui_pattern pattern = {0}; + ui_pattern_push(&ui->frameArena, &pattern, (ui_selector){.kind = UI_SEL_STATUS, .status = UI_HOVER}); + + ui_style style = {.bgColor = {0, 0, 1, 1}}; + ui_style_mask mask = UI_STYLE_BG_COLOR; + ui_style_match_before(pattern, &style, mask); + + ui_flags flags = UI_FLAG_CLICKABLE + | UI_FLAG_CLIP + | UI_FLAG_DRAW_TEXT + | UI_FLAG_DRAW_BACKGROUND; + + ui_box* box = ui_box_make(name, flags); + ui_sig sig = ui_box_sig(box); + return(sig); +} + +void ui_select_popup_draw_arrow(ui_box* box, void* data) +{ + f32 r = minimum(box->parent->style.roundness, box->rect.w); + f32 cr = r*4*(sqrt(2)-1)/3; + + mg_move_to(box->rect.x, box->rect.y); + mg_line_to(box->rect.x + box->rect.w - r, box->rect.y); + mg_cubic_to(box->rect.x + box->rect.w - cr, box->rect.y, + box->rect.x + box->rect.w, box->rect.y + cr, + box->rect.x + box->rect.w, box->rect.y + r); + mg_line_to(box->rect.x + box->rect.w, box->rect.y + box->rect.h - r); + mg_cubic_to(box->rect.x + box->rect.w, box->rect.y + box->rect.h - cr, + box->rect.x + box->rect.w - cr, box->rect.y + box->rect.h, + box->rect.x + box->rect.w - r, box->rect.y + box->rect.h); + mg_line_to(box->rect.x, box->rect.y + box->rect.h); + + mg_set_color(box->style.bgColor); + mg_fill(); + + mg_move_to(box->rect.x + 0.25*box->rect.w, box->rect.y + 0.45*box->rect.h); + mg_line_to(box->rect.x + 0.5*box->rect.w, box->rect.y + 0.75*box->rect.h); + mg_line_to(box->rect.x + 0.75*box->rect.w, box->rect.y + 0.45*box->rect.h); + mg_close_path(); + + mg_set_color(box->style.color); + mg_fill(); +} + +ui_select_popup_info ui_select_popup(const char* name, ui_select_popup_info* info) +{ + ui_select_popup_info result = *info; + + ui_context* ui = ui_get_context(); + + ui_container(name, 0) + { + ui_box* button = ui_box_make("button", + UI_FLAG_CLICKABLE + |UI_FLAG_DRAW_BACKGROUND + |UI_FLAG_DRAW_BORDER + |UI_FLAG_ALLOW_OVERFLOW_X + |UI_FLAG_CLIP); + + f32 maxOptionWidth = 0; + f32 lineHeight = 0; + mp_rect bbox = {0}; + for(int i=0; ioptionCount; i++) + { + bbox = mg_text_bounding_box(button->style.font, button->style.fontSize, info->options[i]); + maxOptionWidth = maximum(maxOptionWidth, bbox.w); + } + f32 buttonWidth = maxOptionWidth + 2*button->style.layout.margin.x + button->rect.h; + + ui_style_box_before(button, + ui_pattern_owner(), + &(ui_style){.size.width = {UI_SIZE_PIXELS, buttonWidth}, + .size.height = {UI_SIZE_CHILDREN}, + .layout.margin.x = 5, + .layout.margin.y = 1, + .roundness = 5, + .borderSize = 1, + .borderColor = {0.3, 0.3, 0.3, 1}}, + UI_STYLE_SIZE + |UI_STYLE_LAYOUT_MARGIN_X + |UI_STYLE_LAYOUT_MARGIN_Y + |UI_STYLE_ROUNDNESS + |UI_STYLE_BORDER_SIZE + |UI_STYLE_BORDER_COLOR); + ui_box_push(button); + { + ui_label_str8(info->options[info->selectedIndex]); + + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PIXELS, button->rect.h}, + .size.height = {UI_SIZE_PIXELS, button->rect.h}, + .floating.x = true, + .floating.y = true, + .floatTarget = {button->rect.w - button->rect.h, 0}, + .color = {0, 0, 0, 1}, + .bgColor = {0.7, 0.7, 0.7, 1}}, + UI_STYLE_SIZE + |UI_STYLE_FLOAT + |UI_STYLE_COLOR + |UI_STYLE_BG_COLOR); + + ui_box* arrow = ui_box_make("arrow", UI_FLAG_DRAW_PROC); + ui_box_set_draw_proc(arrow, ui_select_popup_draw_arrow, 0); + + } ui_box_pop(); + + //panel + ui_box* panel = ui_box_make("panel", + UI_FLAG_DRAW_BACKGROUND + |UI_FLAG_BLOCK_MOUSE + |UI_FLAG_OVERLAY); + + //TODO: set width to max(button.w, max child...) + f32 containerWidth = maximum(maxOptionWidth + 2*panel->style.layout.margin.x, + button->rect.w); + + ui_style_box_before(panel, + ui_pattern_owner(), + &(ui_style){.size.width = {UI_SIZE_PIXELS, containerWidth}, + .size.height = {UI_SIZE_CHILDREN}, + .floating.x = true, + .floating.y = true, + .floatTarget = {button->rect.x, + button->rect.y + button->rect.h}, + .layout.axis = UI_AXIS_Y, + .layout.margin.x = 0, + .layout.margin.y = 5, + .bgColor = {0.2, 0.2, 0.2, 1}}, + UI_STYLE_SIZE + |UI_STYLE_FLOAT + |UI_STYLE_LAYOUT + |UI_STYLE_BG_COLOR); + + ui_box_push(panel); + { + for(int i=0; ioptionCount; i++) + { + ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 1}, + .size.height = {UI_SIZE_TEXT}, + .layout.axis = UI_AXIS_Y, + .layout.align.x = UI_ALIGN_START, + .layout.margin.x = 5, + .layout.margin.y = 2.5}, + UI_STYLE_SIZE + |UI_STYLE_LAYOUT_AXIS + |UI_STYLE_LAYOUT_ALIGN_X + |UI_STYLE_LAYOUT_MARGIN_X + |UI_STYLE_LAYOUT_MARGIN_Y); + + + ui_pattern pattern = {0}; + ui_pattern_push(&ui->frameArena, &pattern, (ui_selector){.kind = UI_SEL_STATUS, .status = UI_HOVER}); + ui_style_match_before(pattern, &(ui_style){.bgColor = {0, 0, 1, 1}}, UI_STYLE_BG_COLOR); + + ui_box* box = ui_box_make_str8(info->options[i], + UI_FLAG_DRAW_TEXT + |UI_FLAG_CLICKABLE + |UI_FLAG_DRAW_BACKGROUND); + ui_sig sig = ui_box_sig(box); + if(sig.pressed) + { + result.selectedIndex = i; + } + } + } + ui_box_pop(); + + ui_context* ui = ui_get_context(); + if(ui_box_active(panel) && mp_mouse_pressed(&ui->input, MP_MOUSE_LEFT)) + { + ui_box_deactivate(panel); + } + else if(ui_box_sig(button).pressed) + { + ui_box_activate(panel); + } + ui_box_set_closed(panel, !ui_box_active(panel)); + } + return(result); +} + +//------------------------------------------------------------------------------ +// text box +//------------------------------------------------------------------------------ +str32 ui_edit_replace_selection_with_codepoints(ui_context* ui, str32 codepoints, str32 input) +{ + u32 start = minimum(ui->editCursor, ui->editMark); + u32 end = maximum(ui->editCursor, ui->editMark); + + str32 before = str32_slice(codepoints, 0, start); + str32 after = str32_slice(codepoints, end, codepoints.len); + + str32_list list = {0}; + str32_list_push(&ui->frameArena, &list, before); + str32_list_push(&ui->frameArena, &list, input); + str32_list_push(&ui->frameArena, &list, after); + + codepoints = str32_list_join(&ui->frameArena, list); + + ui->editCursor = start + input.len; + ui->editMark = ui->editCursor; + return(codepoints); +} + +str32 ui_edit_delete_selection(ui_context* ui, str32 codepoints) +{ + return(ui_edit_replace_selection_with_codepoints(ui, codepoints, (str32){0})); +} + +void ui_edit_copy_selection_to_clipboard(ui_context* ui, str32 codepoints) +{ + if(ui->editCursor == ui->editMark) + { + return; + } + u32 start = minimum(ui->editCursor, ui->editMark); + u32 end = maximum(ui->editCursor, ui->editMark); + str32 selection = str32_slice(codepoints, start, end); + str8 string = utf8_push_from_codepoints(&ui->frameArena, selection); + + mp_clipboard_clear(); + mp_clipboard_set_string(string); +} + +str32 ui_edit_replace_selection_with_clipboard(ui_context* ui, str32 codepoints) +{ + str8 string = mp_clipboard_get_string(&ui->frameArena); + str32 input = utf8_push_to_codepoints(&ui->frameArena, string); + str32 result = ui_edit_replace_selection_with_codepoints(ui, codepoints, input); + return(result); +} + +typedef enum { + UI_EDIT_MOVE, + UI_EDIT_SELECT, + UI_EDIT_SELECT_EXTEND, + UI_EDIT_DELETE, + UI_EDIT_CUT, + UI_EDIT_COPY, + UI_EDIT_PASTE, + UI_EDIT_SELECT_ALL } ui_edit_op; + +typedef enum { + UI_EDIT_MOVE_NONE = 0, + UI_EDIT_MOVE_ONE, + UI_EDIT_MOVE_WORD, + UI_EDIT_MOVE_LINE } ui_edit_move; + +typedef struct ui_edit_command +{ + mp_key_code key; + mp_keymod_flags mods; + + ui_edit_op operation; + ui_edit_move move; + int direction; + +} ui_edit_command; + +#if PLATFORM_WINDOWS + #define OS_COPY_PASTE_MOD MP_KEYMOD_CTRL +#elif PLATFORM_MACOS + #define OS_COPY_PASTE_MOD MP_KEYMOD_CMD +#endif + +const ui_edit_command UI_EDIT_COMMANDS[] = { + //NOTE(martin): move one left + { + .key = MP_KEY_LEFT, + .operation = UI_EDIT_MOVE, + .move = UI_EDIT_MOVE_ONE, + .direction = -1 + }, + //NOTE(martin): move one right + { + .key = MP_KEY_RIGHT, + .operation = UI_EDIT_MOVE, + .move = UI_EDIT_MOVE_ONE, + .direction = 1 + }, + //NOTE(martin): move start + { + .key = MP_KEY_Q, + .mods = MP_KEYMOD_CTRL, + .operation = UI_EDIT_MOVE, + .move = UI_EDIT_MOVE_LINE, + .direction = -1 + }, + { + .key = MP_KEY_UP, + .operation = UI_EDIT_MOVE, + .move = UI_EDIT_MOVE_LINE, + .direction = -1 + }, + //NOTE(martin): move end + { + .key = MP_KEY_E, + .mods = MP_KEYMOD_CTRL, + .operation = UI_EDIT_MOVE, + .move = UI_EDIT_MOVE_LINE, + .direction = 1 + }, + { + .key = MP_KEY_DOWN, + .operation = UI_EDIT_MOVE, + .move = UI_EDIT_MOVE_LINE, + .direction = 1 + }, + //NOTE(martin): select one left + { + .key = MP_KEY_LEFT, + .mods = MP_KEYMOD_SHIFT, + .operation = UI_EDIT_SELECT, + .move = UI_EDIT_MOVE_ONE, + .direction = -1 + }, + //NOTE(martin): select one right + { + .key = MP_KEY_RIGHT, + .mods = MP_KEYMOD_SHIFT, + .operation = UI_EDIT_SELECT, + .move = UI_EDIT_MOVE_ONE, + .direction = 1 + }, + //NOTE(martin): extend select to start + { + .key = MP_KEY_Q, + .mods = MP_KEYMOD_CTRL | MP_KEYMOD_SHIFT, + .operation = UI_EDIT_SELECT_EXTEND, + .move = UI_EDIT_MOVE_LINE, + .direction = -1 + }, + { + .key = MP_KEY_UP, + .mods = MP_KEYMOD_SHIFT, + .operation = UI_EDIT_SELECT_EXTEND, + .move = UI_EDIT_MOVE_LINE, + .direction = -1 + }, + //NOTE(martin): extend select to end + { + .key = MP_KEY_E, + .mods = MP_KEYMOD_CTRL | MP_KEYMOD_SHIFT, + .operation = UI_EDIT_SELECT_EXTEND, + .move = UI_EDIT_MOVE_LINE, + .direction = 1 + }, + { + .key = MP_KEY_DOWN, + .mods = MP_KEYMOD_SHIFT, + .operation = UI_EDIT_SELECT_EXTEND, + .move = UI_EDIT_MOVE_LINE, + .direction = 1 + }, + //NOTE(martin): select all + { + .key = MP_KEY_Q, + .mods = OS_COPY_PASTE_MOD, + .operation = UI_EDIT_SELECT_ALL, + .move = UI_EDIT_MOVE_NONE + }, + //NOTE(martin): delete + { + .key = MP_KEY_DELETE, + .operation = UI_EDIT_DELETE, + .move = UI_EDIT_MOVE_ONE, + .direction = 1 + }, + //NOTE(martin): backspace + { + .key = MP_KEY_BACKSPACE, + .operation = UI_EDIT_DELETE, + .move = UI_EDIT_MOVE_ONE, + .direction = -1 + }, + //NOTE(martin): cut + { + .key = MP_KEY_X, + .mods = OS_COPY_PASTE_MOD, + .operation = UI_EDIT_CUT, + .move = UI_EDIT_MOVE_NONE + }, + //NOTE(martin): copy + { + .key = MP_KEY_C, + .mods = OS_COPY_PASTE_MOD, + .operation = UI_EDIT_COPY, + .move = UI_EDIT_MOVE_NONE + }, + //NOTE(martin): paste + { + .key = MP_KEY_V, + .mods = OS_COPY_PASTE_MOD, + .operation = UI_EDIT_PASTE, + .move = UI_EDIT_MOVE_NONE + } +}; + +const u32 UI_EDIT_COMMAND_COUNT = sizeof(UI_EDIT_COMMANDS)/sizeof(ui_edit_command); + +void ui_edit_perform_move(ui_context* ui, ui_edit_move move, int direction, u32 textLen) +{ + switch(move) + { + case UI_EDIT_MOVE_NONE: + break; + + case UI_EDIT_MOVE_ONE: + { + if(direction < 0 && ui->editCursor > 0) + { + ui->editCursor--; + } + else if(direction > 0 && ui->editCursor < textLen) + { + ui->editCursor++; + } + } break; + + case UI_EDIT_MOVE_LINE: + { + if(direction < 0) + { + ui->editCursor = 0; + } + else if(direction > 0) + { + ui->editCursor = textLen; + } + } break; + + case UI_EDIT_MOVE_WORD: + DEBUG_ASSERT(0, "not implemented yet"); + break; + } +} + +str32 ui_edit_perform_operation(ui_context* ui, ui_edit_op operation, ui_edit_move move, int direction, str32 codepoints) +{ + switch(operation) + { + case UI_EDIT_MOVE: + { + //NOTE(martin): we place the cursor on the direction-most side of the selection + // before performing the move + u32 cursor = direction < 0 ? + minimum(ui->editCursor, ui->editMark) : + maximum(ui->editCursor, ui->editMark); + ui->editCursor = cursor; + + if(ui->editCursor == ui->editMark || move != UI_EDIT_MOVE_ONE) + { + //NOTE: we special case move-one when there is a selection + // (just place the cursor at begining/end of selection) + ui_edit_perform_move(ui, move, direction, codepoints.len); + } + ui->editMark = ui->editCursor; + } break; + + case UI_EDIT_SELECT: + { + ui_edit_perform_move(ui, move, direction, codepoints.len); + } break; + + case UI_EDIT_SELECT_EXTEND: + { + if((direction > 0) != (ui->editCursor > ui->editMark)) + { + u32 tmp = ui->editCursor; + ui->editCursor = ui->editMark; + ui->editMark = tmp; + } + ui_edit_perform_move(ui, move, direction, codepoints.len); + } break; + + case UI_EDIT_DELETE: + { + if(ui->editCursor == ui->editMark) + { + ui_edit_perform_move(ui, move, direction, codepoints.len); + } + codepoints = ui_edit_delete_selection(ui, codepoints); + ui->editMark = ui->editCursor; + } break; + + case UI_EDIT_CUT: + { + ui_edit_copy_selection_to_clipboard(ui, codepoints); + codepoints = ui_edit_delete_selection(ui, codepoints); + } break; + + case UI_EDIT_COPY: + { + ui_edit_copy_selection_to_clipboard(ui, codepoints); + } break; + + case UI_EDIT_PASTE: + { + codepoints = ui_edit_replace_selection_with_clipboard(ui, codepoints); + } break; + + case UI_EDIT_SELECT_ALL: + { + ui->editCursor = 0; + ui->editMark = codepoints.len; + } break; + } + ui->editCursorBlinkStart = ui->frameTime; + + return(codepoints); +} + +void ui_text_box_render(ui_box* box, void* data) +{ + str32 codepoints = *(str32*)data; + ui_context* ui = ui_get_context(); + + u32 firstDisplayedChar = 0; + if(ui_box_active(box)) + { + firstDisplayedChar = ui->editFirstDisplayedChar; + } + + ui_style* style = &box->style; + mg_font_extents extents = mg_font_get_scaled_extents(style->font, style->fontSize); + f32 lineHeight = extents.ascent + extents.descent; + + str32 before = str32_slice(codepoints, 0, firstDisplayedChar); + mp_rect beforeBox = mg_text_bounding_box_utf32(style->font, style->fontSize, before); + + f32 textMargin = 5; //TODO: make that configurable + + f32 textX = textMargin + box->rect.x - beforeBox.w; + f32 textTop = box->rect.y + 0.5*(box->rect.h - lineHeight); + f32 textY = textTop + extents.ascent ; + + if(box->active) + { + u32 selectStart = minimum(ui->editCursor, ui->editMark); + u32 selectEnd = maximum(ui->editCursor, ui->editMark); + + str32 beforeSelect = str32_slice(codepoints, 0, selectStart); + mp_rect beforeSelectBox = mg_text_bounding_box_utf32(style->font, style->fontSize, beforeSelect); + beforeSelectBox.x += textX; + beforeSelectBox.y += textY; + + if(selectStart != selectEnd) + { + str32 select = str32_slice(codepoints, selectStart, selectEnd); + str32 afterSelect = str32_slice(codepoints, selectEnd, codepoints.len); + mp_rect selectBox = mg_text_bounding_box_utf32(style->font, style->fontSize, select); + mp_rect afterSelectBox = mg_text_bounding_box_utf32(style->font, style->fontSize, afterSelect); + + selectBox.x += beforeSelectBox.x + beforeSelectBox.w; + selectBox.y += textY; + + mg_set_color_rgba(0, 0, 1, 1); + mg_rectangle_fill(selectBox.x, selectBox.y, selectBox.w, lineHeight); + + mg_set_font(style->font); + mg_set_font_size(style->fontSize); + mg_set_color(style->color); + + mg_move_to(textX, textY); + mg_codepoints_outlines(beforeSelect); + mg_fill(); + + mg_set_color_rgba(1, 1, 1, 1); + mg_codepoints_outlines(select); + mg_fill(); + + mg_set_color(style->color); + mg_codepoints_outlines(afterSelect); + mg_fill(); + } + else + { + if(!((u64)(2*(ui->frameTime - ui->editCursorBlinkStart)) & 1)) + { + f32 caretX = box->rect.x + textMargin - beforeBox.w + beforeSelectBox.w; + f32 caretY = textTop; + mg_set_color(style->color); + mg_rectangle_fill(caretX, caretY, 1, lineHeight); + } + mg_set_font(style->font); + mg_set_font_size(style->fontSize); + mg_set_color(style->color); + + mg_move_to(textX, textY); + mg_codepoints_outlines(codepoints); + mg_fill(); + } + } + else + { + mg_set_font(style->font); + mg_set_font_size(style->fontSize); + mg_set_color(style->color); + + mg_move_to(textX, textY); + mg_codepoints_outlines(codepoints); + mg_fill(); + } +} + +ui_text_box_result ui_text_box(const char* name, mem_arena* arena, str8 text) +{ + ui_context* ui = ui_get_context(); + + ui_text_box_result result = {.text = text}; + + ui_flags frameFlags = UI_FLAG_CLICKABLE + | UI_FLAG_DRAW_BACKGROUND + | UI_FLAG_DRAW_BORDER + | UI_FLAG_CLIP + | UI_FLAG_DRAW_PROC; + + ui_box* frame = ui_box_make(name, frameFlags); + ui_style* style = &frame->style; + f32 textMargin = 5; //TODO parameterize this margin! must be the same as in ui_text_box_render + + mg_font_extents extents = mg_font_get_scaled_extents(style->font, style->fontSize); + + ui_sig sig = ui_box_sig(frame); + + if(sig.hovering) + { + ui_box_set_hot(frame, true); + + if(sig.pressed) + { + if(!ui_box_active(frame)) + { + ui_box_activate(frame); + + //NOTE: focus + ui->focus = frame; + ui->editFirstDisplayedChar = 0; + ui->editCursor = 0; + ui->editMark = 0; + } + ui->editCursorBlinkStart = ui->frameTime; + } + + if(sig.pressed || sig.dragging) + { + //NOTE: set cursor/extend selection on mouse press or drag + vec2 pos = ui_mouse_position(); + f32 cursorX = pos.x - frame->rect.x - textMargin; + + str32 codepoints = utf8_push_to_codepoints(&ui->frameArena, text); + i32 newCursor = codepoints.len; + f32 x = 0; + for(int i = ui->editFirstDisplayedChar; ifont, style->fontSize, str32_slice(codepoints, i, i+1)); + if(x + 0.5*bbox.w > cursorX) + { + newCursor = i; + break; + } + x += bbox.w; + } + //NOTE: put cursor the closest to new cursor (this maximizes the resulting selection, + // and seems to be the standard behaviour across a number of text editor) + if(abs(newCursor - ui->editCursor) > abs(newCursor - ui->editMark)) + { + i32 tmp = ui->editCursor; + ui->editCursor = ui->editMark; + ui->editMark = tmp; + } + //NOTE: set the new cursor, and set or leave the mark depending on mode + ui->editCursor = newCursor; + if(sig.pressed && !(mp_key_mods(&ui->input) & MP_KEYMOD_SHIFT)) + { + ui->editMark = ui->editCursor; + } + } + } + else + { + ui_box_set_hot(frame, false); + + if(sig.pressed) + { + if(ui_box_active(frame)) + { + ui_box_deactivate(frame); + + //NOTE loose focus + ui->focus = 0; + } + } + } + + if(ui_box_active(frame)) + { + str32 oldCodepoints = utf8_push_to_codepoints(&ui->frameArena, text); + str32 codepoints = oldCodepoints; + ui->editCursor = Clamp(ui->editCursor, 0, codepoints.len); + ui->editMark = Clamp(ui->editMark, 0, codepoints.len); + + //NOTE replace selection with input codepoints + str32 input = mp_input_text_utf32(&ui->input, &ui->frameArena); + if(input.len) + { + codepoints = ui_edit_replace_selection_with_codepoints(ui, codepoints, input); + ui->editCursorBlinkStart = ui->frameTime; + } + + //NOTE handle shortcuts + mp_keymod_flags mods = mp_key_mods(&ui->input); + + for(int i=0; iinput, command->key) || mp_key_repeated(&ui->input, command->key)) + && mods == command->mods) + { + codepoints = ui_edit_perform_operation(ui, command->operation, command->move, command->direction, codepoints); + break; + } + } + + //NOTE(martin): check changed/accepted + if(oldCodepoints.ptr != codepoints.ptr) + { + result.changed = true; + result.text = utf8_push_from_codepoints(arena, codepoints); + } + + if(mp_key_pressed(&ui->input, MP_KEY_ENTER)) + { + //TODO(martin): extract in gui_edit_complete() (and use below) + result.accepted = true; + ui_box_deactivate(frame); + ui->focus = 0; + } + + //NOTE slide contents + { + if(ui->editCursor < ui->editFirstDisplayedChar) + { + ui->editFirstDisplayedChar = ui->editCursor; + } + else + { + i32 firstDisplayedChar = ui->editFirstDisplayedChar; + str32 firstToCursor = str32_slice(codepoints, firstDisplayedChar, ui->editCursor); + mp_rect firstToCursorBox = mg_text_bounding_box_utf32(style->font, style->fontSize, firstToCursor); + + while(firstToCursorBox.w > (frame->rect.w - 2*textMargin)) + { + firstDisplayedChar++; + firstToCursor = str32_slice(codepoints, firstDisplayedChar, ui->editCursor); + firstToCursorBox = mg_text_bounding_box_utf32(style->font, style->fontSize, firstToCursor); + } + + ui->editFirstDisplayedChar = firstDisplayedChar; + } + } + + //NOTE: set renderer + str32* renderCodepoints = mem_arena_alloc_type(&ui->frameArena, str32); + *renderCodepoints = str32_push_copy(&ui->frameArena, codepoints); + ui_box_set_draw_proc(frame, ui_text_box_render, renderCodepoints); + } + else + { + //NOTE: set renderer + str32* renderCodepoints = mem_arena_alloc_type(&ui->frameArena, str32); + *renderCodepoints = utf8_push_to_codepoints(&ui->frameArena, text); + ui_box_set_draw_proc(frame, ui_text_box_render, renderCodepoints); + } + + return(result); +} diff --git a/src/win32_app.c b/src/win32_app.c index c088c39..d9991cd 100644 --- a/src/win32_app.c +++ b/src/win32_app.c @@ -1,1104 +1,1337 @@ -/************************************************************//** -* -* @file: win32_app.c -* @author: Martin Fouilleul -* @date: 16/12/2022 -* @revision: -* -*****************************************************************/ - -#include"mp_app.c" - -void mp_init_keys() -{ - memset(__mpApp.keyCodes, MP_KEY_UNKNOWN, 256*sizeof(int)); - - __mpApp.keyCodes[0x00B] = MP_KEY_0; - __mpApp.keyCodes[0x002] = MP_KEY_1; - __mpApp.keyCodes[0x003] = MP_KEY_2; - __mpApp.keyCodes[0x004] = MP_KEY_3; - __mpApp.keyCodes[0x005] = MP_KEY_4; - __mpApp.keyCodes[0x006] = MP_KEY_5; - __mpApp.keyCodes[0x007] = MP_KEY_6; - __mpApp.keyCodes[0x008] = MP_KEY_7; - __mpApp.keyCodes[0x009] = MP_KEY_8; - __mpApp.keyCodes[0x00A] = MP_KEY_9; - __mpApp.keyCodes[0x01E] = MP_KEY_A; - __mpApp.keyCodes[0x030] = MP_KEY_B; - __mpApp.keyCodes[0x02E] = MP_KEY_C; - __mpApp.keyCodes[0x020] = MP_KEY_D; - __mpApp.keyCodes[0x012] = MP_KEY_E; - __mpApp.keyCodes[0x021] = MP_KEY_F; - __mpApp.keyCodes[0x022] = MP_KEY_G; - __mpApp.keyCodes[0x023] = MP_KEY_H; - __mpApp.keyCodes[0x017] = MP_KEY_I; - __mpApp.keyCodes[0x024] = MP_KEY_J; - __mpApp.keyCodes[0x025] = MP_KEY_K; - __mpApp.keyCodes[0x026] = MP_KEY_L; - __mpApp.keyCodes[0x032] = MP_KEY_M; - __mpApp.keyCodes[0x031] = MP_KEY_N; - __mpApp.keyCodes[0x018] = MP_KEY_O; - __mpApp.keyCodes[0x019] = MP_KEY_P; - __mpApp.keyCodes[0x010] = MP_KEY_Q; - __mpApp.keyCodes[0x013] = MP_KEY_R; - __mpApp.keyCodes[0x01F] = MP_KEY_S; - __mpApp.keyCodes[0x014] = MP_KEY_T; - __mpApp.keyCodes[0x016] = MP_KEY_U; - __mpApp.keyCodes[0x02F] = MP_KEY_V; - __mpApp.keyCodes[0x011] = MP_KEY_W; - __mpApp.keyCodes[0x02D] = MP_KEY_X; - __mpApp.keyCodes[0x015] = MP_KEY_Y; - __mpApp.keyCodes[0x02C] = MP_KEY_Z; - __mpApp.keyCodes[0x028] = MP_KEY_APOSTROPHE; - __mpApp.keyCodes[0x02B] = MP_KEY_BACKSLASH; - __mpApp.keyCodes[0x033] = MP_KEY_COMMA; - __mpApp.keyCodes[0x00D] = MP_KEY_EQUAL; - __mpApp.keyCodes[0x029] = MP_KEY_GRAVE_ACCENT; - __mpApp.keyCodes[0x01A] = MP_KEY_LEFT_BRACKET; - __mpApp.keyCodes[0x00C] = MP_KEY_MINUS; - __mpApp.keyCodes[0x034] = MP_KEY_PERIOD; - __mpApp.keyCodes[0x01B] = MP_KEY_RIGHT_BRACKET; - __mpApp.keyCodes[0x027] = MP_KEY_SEMICOLON; - __mpApp.keyCodes[0x035] = MP_KEY_SLASH; - __mpApp.keyCodes[0x056] = MP_KEY_WORLD_2; - __mpApp.keyCodes[0x00E] = MP_KEY_BACKSPACE; - __mpApp.keyCodes[0x153] = MP_KEY_DELETE; - __mpApp.keyCodes[0x14F] = MP_KEY_END; - __mpApp.keyCodes[0x01C] = MP_KEY_ENTER; - __mpApp.keyCodes[0x001] = MP_KEY_ESCAPE; - __mpApp.keyCodes[0x147] = MP_KEY_HOME; - __mpApp.keyCodes[0x152] = MP_KEY_INSERT; - __mpApp.keyCodes[0x15D] = MP_KEY_MENU; - __mpApp.keyCodes[0x151] = MP_KEY_PAGE_DOWN; - __mpApp.keyCodes[0x149] = MP_KEY_PAGE_UP; - __mpApp.keyCodes[0x045] = MP_KEY_PAUSE; - __mpApp.keyCodes[0x146] = MP_KEY_PAUSE; - __mpApp.keyCodes[0x039] = MP_KEY_SPACE; - __mpApp.keyCodes[0x00F] = MP_KEY_TAB; - __mpApp.keyCodes[0x03A] = MP_KEY_CAPS_LOCK; - __mpApp.keyCodes[0x145] = MP_KEY_NUM_LOCK; - __mpApp.keyCodes[0x046] = MP_KEY_SCROLL_LOCK; - __mpApp.keyCodes[0x03B] = MP_KEY_F1; - __mpApp.keyCodes[0x03C] = MP_KEY_F2; - __mpApp.keyCodes[0x03D] = MP_KEY_F3; - __mpApp.keyCodes[0x03E] = MP_KEY_F4; - __mpApp.keyCodes[0x03F] = MP_KEY_F5; - __mpApp.keyCodes[0x040] = MP_KEY_F6; - __mpApp.keyCodes[0x041] = MP_KEY_F7; - __mpApp.keyCodes[0x042] = MP_KEY_F8; - __mpApp.keyCodes[0x043] = MP_KEY_F9; - __mpApp.keyCodes[0x044] = MP_KEY_F10; - __mpApp.keyCodes[0x057] = MP_KEY_F11; - __mpApp.keyCodes[0x058] = MP_KEY_F12; - __mpApp.keyCodes[0x064] = MP_KEY_F13; - __mpApp.keyCodes[0x065] = MP_KEY_F14; - __mpApp.keyCodes[0x066] = MP_KEY_F15; - __mpApp.keyCodes[0x067] = MP_KEY_F16; - __mpApp.keyCodes[0x068] = MP_KEY_F17; - __mpApp.keyCodes[0x069] = MP_KEY_F18; - __mpApp.keyCodes[0x06A] = MP_KEY_F19; - __mpApp.keyCodes[0x06B] = MP_KEY_F20; - __mpApp.keyCodes[0x06C] = MP_KEY_F21; - __mpApp.keyCodes[0x06D] = MP_KEY_F22; - __mpApp.keyCodes[0x06E] = MP_KEY_F23; - __mpApp.keyCodes[0x076] = MP_KEY_F24; - __mpApp.keyCodes[0x038] = MP_KEY_LEFT_ALT; - __mpApp.keyCodes[0x01D] = MP_KEY_LEFT_CONTROL; - __mpApp.keyCodes[0x02A] = MP_KEY_LEFT_SHIFT; - __mpApp.keyCodes[0x15B] = MP_KEY_LEFT_SUPER; - __mpApp.keyCodes[0x137] = MP_KEY_PRINT_SCREEN; - __mpApp.keyCodes[0x138] = MP_KEY_RIGHT_ALT; - __mpApp.keyCodes[0x11D] = MP_KEY_RIGHT_CONTROL; - __mpApp.keyCodes[0x036] = MP_KEY_RIGHT_SHIFT; - __mpApp.keyCodes[0x15C] = MP_KEY_RIGHT_SUPER; - __mpApp.keyCodes[0x150] = MP_KEY_DOWN; - __mpApp.keyCodes[0x14B] = MP_KEY_LEFT; - __mpApp.keyCodes[0x14D] = MP_KEY_RIGHT; - __mpApp.keyCodes[0x148] = MP_KEY_UP; - __mpApp.keyCodes[0x052] = MP_KEY_KP_0; - __mpApp.keyCodes[0x04F] = MP_KEY_KP_1; - __mpApp.keyCodes[0x050] = MP_KEY_KP_2; - __mpApp.keyCodes[0x051] = MP_KEY_KP_3; - __mpApp.keyCodes[0x04B] = MP_KEY_KP_4; - __mpApp.keyCodes[0x04C] = MP_KEY_KP_5; - __mpApp.keyCodes[0x04D] = MP_KEY_KP_6; - __mpApp.keyCodes[0x047] = MP_KEY_KP_7; - __mpApp.keyCodes[0x048] = MP_KEY_KP_8; - __mpApp.keyCodes[0x049] = MP_KEY_KP_9; - __mpApp.keyCodes[0x04E] = MP_KEY_KP_ADD; - __mpApp.keyCodes[0x053] = MP_KEY_KP_DECIMAL; - __mpApp.keyCodes[0x135] = MP_KEY_KP_DIVIDE; - __mpApp.keyCodes[0x11C] = MP_KEY_KP_ENTER; - __mpApp.keyCodes[0x037] = MP_KEY_KP_MULTIPLY; - __mpApp.keyCodes[0x04A] = MP_KEY_KP_SUBTRACT; - - memset(__mpApp.nativeKeys, 0, sizeof(int)*MP_KEY_COUNT); - for(int nativeKey=0; nativeKey<256; nativeKey++) - { - mp_key_code mpKey = __mpApp.keyCodes[nativeKey]; - if(mpKey) - { - __mpApp.nativeKeys[mpKey] = nativeKey; - } - } -} - -void mp_init() -{ - if(!__mpApp.init) - { - memset(&__mpApp, 0, sizeof(__mpApp)); - - mp_init_common(); - mp_init_keys(); - - __mpApp.win32.savedConsoleCodePage = GetConsoleOutputCP(); - SetConsoleOutputCP(CP_UTF8); - - SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); - } -} - -void mp_terminate() -{ - if(__mpApp.init) - { - SetConsoleOutputCP(__mpApp.win32.savedConsoleCodePage); - - mp_terminate_common(); - __mpApp = (mp_app){0}; - } -} - -static mp_key_code mp_convert_win32_key(int code) -{ - return(__mpApp.keyCodes[code]); -} - -static mp_keymod_flags mp_get_mod_keys() -{ - mp_keymod_flags mods = 0; - if(GetKeyState(VK_SHIFT) & 0x8000) - { - mods |= MP_KEYMOD_SHIFT; - } - if(GetKeyState(VK_CONTROL) & 0x8000) - { - mods |= MP_KEYMOD_CTRL; - } - if(GetKeyState(VK_MENU) & 0x8000) - { - mods |= MP_KEYMOD_ALT; - } - if((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & 0x8000) - { - mods |= MP_KEYMOD_CMD; - } - return(mods); -} - -static void process_mouse_event(mp_window_data* window, mp_key_action action, mp_key_code button) -{ - if(action == MP_KEY_PRESS) - { - if(!__mpApp.win32.mouseCaptureMask) - { - SetCapture(window->win32.hWnd); - } - __mpApp.win32.mouseCaptureMask |= (1<win32.hWnd, - HWND_TOP, - rect.left, - rect.top, - rect.right - rect.left, - rect.bottom - rect.top, - SWP_NOACTIVATE | SWP_NOZORDER); - - //TODO: send a message - - } break; - - //TODO: enter/exit size & move - - case WM_SIZING: - { - //TODO: take dpi into account - - RECT* rect = (RECT*)lParam; - - mp_event event = {0}; - event.window = mp_window_handle_from_ptr(mpWindow); - event.type = MP_EVENT_WINDOW_RESIZE; - event.frame.rect = (mp_rect){rect->bottom, rect->left, rect->top - rect->bottom, rect->right - rect->left}; - mp_queue_event(&event); - } break; - - case WM_MOVING: - { - //TODO: take dpi into account - - RECT* rect = (RECT*)lParam; - - mp_event event = {0}; - event.window = mp_window_handle_from_ptr(mpWindow); - event.type = MP_EVENT_WINDOW_MOVE; - event.frame.rect = (mp_rect){rect->bottom, rect->left, rect->top - rect->bottom, rect->right - rect->left}; - mp_queue_event(&event); - } break; - - case WM_SETFOCUS: - { - mp_event event = {0}; - event.window = mp_window_handle_from_ptr(mpWindow); - event.type = MP_EVENT_WINDOW_FOCUS; - mp_queue_event(&event); - } break; - - case WM_KILLFOCUS: - { - mp_event event = {0}; - event.window = mp_window_handle_from_ptr(mpWindow); - event.type = MP_EVENT_WINDOW_UNFOCUS; - mp_queue_event(&event); - } break; - - case WM_SIZE: - { - bool minimized = (wParam == SIZE_MINIMIZED); - if(minimized != mpWindow->minimized) - { - mpWindow->minimized = minimized; - - mp_event event = {0}; - event.window = mp_window_handle_from_ptr(mpWindow); - - if(minimized) - { - event.type = MP_EVENT_WINDOW_HIDE; - } - else if(mpWindow->minimized) - { - event.type = MP_EVENT_WINDOW_SHOW; - } - mp_queue_event(&event); - } - } break; - - case WM_LBUTTONDOWN: - { - process_mouse_event(mpWindow, MP_KEY_PRESS, MP_MOUSE_LEFT); - } break; - - case WM_RBUTTONDOWN: - { - process_mouse_event(mpWindow, MP_KEY_PRESS, MP_MOUSE_RIGHT); - } break; - - case WM_MBUTTONDOWN: - { - process_mouse_event(mpWindow, MP_KEY_PRESS, MP_MOUSE_MIDDLE); - } break; - - case WM_LBUTTONUP: - { - process_mouse_event(mpWindow, MP_KEY_RELEASE, MP_MOUSE_LEFT); - } break; - - case WM_RBUTTONUP: - { - process_mouse_event(mpWindow, MP_KEY_RELEASE, MP_MOUSE_RIGHT); - } break; - - case WM_MBUTTONUP: - { - process_mouse_event(mpWindow, MP_KEY_RELEASE, MP_MOUSE_MIDDLE); - } break; - - case WM_MOUSEMOVE: - { - RECT rect; - GetClientRect(mpWindow->win32.hWnd, &rect); - - u32 dpi = GetDpiForWindow(mpWindow->win32.hWnd); - f32 scaling = (f32)dpi/96.; - - mp_event event = {0}; - event.window = mp_window_handle_from_ptr(mpWindow); - event.type = MP_EVENT_MOUSE_MOVE; - event.move.x = LOWORD(lParam) / scaling; - event.move.y = HIWORD(lParam) / scaling; - - if(__mpApp.inputState.mouse.posValid) - { - event.move.deltaX = event.move.x - __mpApp.inputState.mouse.pos.x; - event.move.deltaY = event.move.y - __mpApp.inputState.mouse.pos.y; - } - else - { - __mpApp.inputState.mouse.posValid = true; - } - - if(!__mpApp.win32.mouseTracked) - { - __mpApp.win32.mouseTracked = true; - - TRACKMOUSEEVENT track; - memset(&track, 0, sizeof(track)); - track.cbSize = sizeof(track); - track.dwFlags = TME_LEAVE; - track.hwndTrack = mpWindow->win32.hWnd; - TrackMouseEvent(&track); - - mp_event enter = {.window = event.window, - .type = MP_EVENT_MOUSE_ENTER, - .move.x = event.move.x, - .move.y = event.move.y}; - mp_queue_event(&enter); - } - - mp_update_mouse_move(event.move.x, event.move.y, event.move.deltaX, event.move.deltaY); - - mp_queue_event(&event); - } break; - - case WM_MOUSELEAVE: - { - __mpApp.win32.mouseTracked = false; - - if(!__mpApp.win32.mouseCaptureMask) - { - __mpApp.inputState.mouse.posValid = false; - } - - mp_event event = {0}; - event.window = mp_window_handle_from_ptr(mpWindow); - event.type = MP_EVENT_MOUSE_LEAVE; - mp_queue_event(&event); - } break; - - case WM_MOUSEWHEEL: - { - process_wheel_event(mpWindow, 0, (float)((i16)HIWORD(wParam))); - } break; - - case WM_MOUSEHWHEEL: - { - process_wheel_event(mpWindow, (float)((i16)HIWORD(wParam)), 0); - } break; - - case WM_KEYDOWN: - case WM_SYSKEYDOWN: - { - mp_event event = {0}; - event.window = mp_window_handle_from_ptr(mpWindow); - event.type = MP_EVENT_KEYBOARD_KEY; - event.key.action = (lParam & 0x40000000) ? MP_KEY_REPEAT : MP_KEY_PRESS; - event.key.code = mp_convert_win32_key(HIWORD(lParam) & 0x1ff); - event.key.mods = mp_get_mod_keys(); - mp_queue_event(&event); - - mp_update_key_mods(event.key.mods); - mp_update_key_state(&__mpApp.inputState.keyboard.keys[event.key.code], event.key.action); - } break; - - case WM_KEYUP: - case WM_SYSKEYUP: - { - mp_event event = {0}; - event.window = mp_window_handle_from_ptr(mpWindow); - event.type = MP_EVENT_KEYBOARD_KEY; - event.key.action = MP_KEY_RELEASE; - event.key.code = mp_convert_win32_key(HIWORD(lParam) & 0x1ff); - event.key.mods = mp_get_mod_keys(); - mp_queue_event(&event); - - mp_update_key_mods(event.key.mods); - mp_update_key_state(&__mpApp.inputState.keyboard.keys[event.key.code], event.key.action); - - } break; - - case WM_CHAR: - { - if((u32)wParam >= 32) - { - mp_event event = {0}; - event.window = mp_window_handle_from_ptr(mpWindow); - event.type = MP_EVENT_KEYBOARD_CHAR; - event.character.codepoint = (utf32)wParam; - str8 seq = utf8_encode(event.character.sequence, event.character.codepoint); - event.character.seqLen = seq.len; - mp_queue_event(&event); - - mp_update_text(event.character.codepoint); - } - } break; - - case WM_DROPFILES: - { - //TODO - } break; - - default: - { - result = DefWindowProc(windowHandle, message, wParam, lParam); - } break; - } - - return(result); -} - -//-------------------------------------------------------------------- -// app management -//-------------------------------------------------------------------- - -bool mp_should_quit() -{ - return(__mpApp.shouldQuit); -} - -void mp_cancel_quit() -{ - __mpApp.shouldQuit = false; -} - -void mp_request_quit() -{ - __mpApp.shouldQuit = true; -} - -void mp_pump_events(f64 timeout) -{ - __mpApp.inputState.frameCounter++; - - MSG message; - while(PeekMessage(&message, 0, 0, 0, PM_REMOVE)) - { - TranslateMessage(&message); - DispatchMessage(&message); - } -} - -//-------------------------------------------------------------------- -// window management -//-------------------------------------------------------------------- - -//WARN: the following header pulls in objbase.h (even with WIN32_LEAN_AND_MEAN), which -// #defines interface to struct... so make sure to #undef interface since it's a -// name we want to be able to use throughout the codebase -#include -#undef interface - -mp_window mp_window_create(mp_rect rect, const char* title, mp_window_style style) -{ - WNDCLASS windowClass = {.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC, - .lpfnWndProc = WinProc, - .hInstance = GetModuleHandleW(NULL), - .lpszClassName = "ApplicationWindowClass", - .hCursor = LoadCursor(0, IDC_ARROW)}; - - if(!RegisterClass(&windowClass)) - { - //TODO: error - goto quit; - } - - u32 dpiX, dpiY; - HMONITOR monitor = MonitorFromPoint((POINT){rect.x, rect.y}, MONITOR_DEFAULTTOPRIMARY); - GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); - - f32 dpiScalingX = (f32)dpiX/96.; - f32 dpiScalingY = (f32)dpiY/96.; - - HWND windowHandle = CreateWindow("ApplicationWindowClass", "Test Window", - WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, - rect.w * dpiScalingX, rect.h * dpiScalingY, - 0, 0, windowClass.hInstance, 0); - - if(!windowHandle) - { - //TODO: error - goto quit; - } - - UpdateWindow(windowHandle); - - //TODO: return wrapped window - quit:; - mp_window_data* window = mp_window_alloc(); - window->win32.hWnd = windowHandle; - - SetPropW(windowHandle, L"MilePost", window); - - return(mp_window_handle_from_ptr(window)); -} - -void mp_window_destroy(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - DestroyWindow(windowData->win32.hWnd); - //TODO: check when to unregister class - - mp_window_recycle_ptr(windowData); - } -} - -void* mp_window_native_pointer(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - return(windowData->win32.hWnd); - } - else - { - return(0); - } -} - -bool mp_window_should_close(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - return(windowData->shouldClose); - } - else - { - return(false); - } -} - -void mp_window_request_close(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - windowData->shouldClose = true; - PostMessage(windowData->win32.hWnd, WM_CLOSE, 0, 0); - } -} - -void mp_window_cancel_close(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - windowData->shouldClose = false; - } -} - - -bool mp_window_is_hidden(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - return(IsWindowVisible(windowData->win32.hWnd)); - } - else - { - return(false); - } -} - -void mp_window_hide(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - ShowWindow(windowData->win32.hWnd, SW_HIDE); - } -} - -void mp_window_show(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - ShowWindow(windowData->win32.hWnd, SW_NORMAL); - } -} - -bool mp_window_is_minimized(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - return(windowData->minimized); - } - else - { - return(false); - } -} - -void mp_window_minimize(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - ShowWindow(windowData->win32.hWnd, SW_MINIMIZE); - } -} - -void mp_window_maximize(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - ShowWindow(windowData->win32.hWnd, SW_MAXIMIZE); - } -} - -void mp_window_restore(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - ShowWindow(windowData->win32.hWnd, SW_RESTORE); - } -} - -bool mp_window_has_focus(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - return(GetActiveWindow() == windowData->win32.hWnd); - } - else - { - return(false); - } -} - -void mp_window_focus(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - SetFocus(windowData->win32.hWnd); - } -} - -void mp_window_unfocus(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - SetFocus(0); - } -} - -void mp_window_send_to_back(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - SetWindowPos(windowData->win32.hWnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); - } -} - -void mp_window_bring_to_front(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - if(!IsWindowVisible(windowData->win32.hWnd)) - { - ShowWindow(windowData->win32.hWnd, SW_NORMAL); - } - SetWindowPos(windowData->win32.hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); - } -} - -mp_rect mp_window_get_content_rect(mp_window window) -{ - mp_rect rect = {0}; - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - RECT winRect; - if(GetClientRect(windowData->win32.hWnd, &winRect)) - { - u32 dpi = GetDpiForWindow(windowData->win32.hWnd); - f32 scale = (float)dpi/96.; - rect = (mp_rect){0, 0, (winRect.right - winRect.left)/scale, (winRect.bottom - winRect.top)/scale}; - } - } - return(rect); -} - -//-------------------------------------------------------------------------------- -// clipboard functions -//-------------------------------------------------------------------------------- - -MP_API void mp_clipboard_clear(void) -{ - if(OpenClipboard(NULL)) - { - EmptyClipboard(); - CloseClipboard(); - } -} - -MP_API void mp_clipboard_set_string(str8 string) -{ - if(OpenClipboard(NULL)) - { - EmptyClipboard(); - - int wideCount = MultiByteToWideChar(CP_UTF8, 0, string.ptr, string.len, 0, 0); - HANDLE handle = GlobalAlloc(GMEM_MOVEABLE, (wideCount+1)*sizeof(wchar_t)); - if(handle) - { - char* memory = GlobalLock(handle); - if(memory) - { - MultiByteToWideChar(CP_UTF8, 0, string.ptr, string.len, (wchar_t*)memory, wideCount); - ((wchar_t*)memory)[wideCount] = '\0'; - - GlobalUnlock(handle); - SetClipboardData(CF_UNICODETEXT, handle); - } - } - CloseClipboard(); - } -} - -MP_API str8 mp_clipboard_get_string(mem_arena* arena) -{ - str8 string = {0}; - - if(OpenClipboard(NULL)) - { - HANDLE handle = GetClipboardData(CF_UNICODETEXT); - if(handle) - { - char* memory = GlobalLock(handle); - if(memory) - { - u64 size = WideCharToMultiByte(CP_UTF8, 0, (wchar_t*)memory, -1, 0, 0, 0, 0); - if(size) - { - string.ptr = mem_arena_alloc(arena, size); - string.len = size - 1; - WideCharToMultiByte(CP_UTF8, 0, (wchar_t*)memory, -1, string.ptr, size, 0, 0); - GlobalUnlock(handle); - } - } - } - CloseClipboard(); - } - return(string); -} - -MP_API str8 mp_clipboard_copy_string(str8 backing) -{ - //TODO - return((str8){0}); -} - - -//-------------------------------------------------------------------------------- -// win32 surfaces -//-------------------------------------------------------------------------------- - -#include"graphics_surface.h" - -vec2 mg_win32_surface_contents_scaling(mg_surface_data* surface) -{ - u32 dpi = GetDpiForWindow(surface->layer.hWnd); - vec2 contentsScaling = (vec2){(float)dpi/96., (float)dpi/96.}; - return(contentsScaling); -} - -mp_rect mg_win32_surface_get_frame(mg_surface_data* surface) -{ - RECT rect = {0}; - GetClientRect(surface->layer.hWnd, &rect); - - vec2 scale = mg_win32_surface_contents_scaling(surface); - - mp_rect res = {rect.left/scale.x, - rect.bottom/scale.y, - (rect.right - rect.left)/scale.x, - (rect.bottom - rect.top)/scale.y}; - return(res); -} - -void mg_win32_surface_set_frame(mg_surface_data* surface, mp_rect frame) -{ - HWND parent = GetParent(surface->layer.hWnd); - RECT parentContentRect; - - GetClientRect(parent, &parentContentRect); - int parentHeight = parentContentRect.bottom - parentContentRect.top; - - vec2 scale = mg_win32_surface_contents_scaling(surface); - - SetWindowPos(surface->layer.hWnd, - HWND_TOP, - frame.x * scale.x, - parentHeight - (frame.y + frame.h) * scale.y, - frame.w * scale.x, - frame.h * scale.y, - SWP_NOACTIVATE | SWP_NOZORDER); -} - -bool mg_win32_surface_get_hidden(mg_surface_data* surface) -{ - bool hidden = !IsWindowVisible(surface->layer.hWnd); - return(hidden); -} - -void mg_win32_surface_set_hidden(mg_surface_data* surface, bool hidden) -{ - ShowWindow(surface->layer.hWnd, hidden ? SW_HIDE : SW_NORMAL); -} - -void* mg_win32_surface_native_layer(mg_surface_data* surface) -{ - return((void*)surface->layer.hWnd); -} - -mg_surface_id mg_win32_surface_remote_id(mg_surface_data* surface) -{ - return((mg_surface_id)surface->layer.hWnd); -} - -void mg_win32_surface_host_connect(mg_surface_data* surface, mg_surface_id remoteID) -{ - HWND dstWnd = surface->layer.hWnd; - HWND srcWnd = (HWND)remoteID; - - RECT dstRect; - GetClientRect(dstWnd, &dstRect); - - SetParent(srcWnd, dstWnd); - ShowWindow(srcWnd, SW_NORMAL); - - SetWindowPos(srcWnd, - HWND_TOP, - 0, - 0, - dstRect.right - dstRect.left, - dstRect.bottom - dstRect.top, - SWP_NOACTIVATE | SWP_NOZORDER); -} - -void mg_surface_cleanup(mg_surface_data* surface) -{ - DestroyWindow(surface->layer.hWnd); -} - -LRESULT LayerWinProc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam) -{ - if(message == WM_NCHITTEST) - { - return(HTTRANSPARENT); - } - else - { - return(DefWindowProc(windowHandle, message, wParam, lParam)); - } -} - -void mg_surface_init_for_window(mg_surface_data* surface, mp_window_data* window) -{ - surface->contentsScaling = mg_win32_surface_contents_scaling; - surface->getFrame = mg_win32_surface_get_frame; - surface->setFrame = mg_win32_surface_set_frame; - surface->getHidden = mg_win32_surface_get_hidden; - surface->setHidden = mg_win32_surface_set_hidden; - surface->nativeLayer = mg_win32_surface_native_layer; - - //NOTE(martin): create a child window for the surface - WNDCLASS layerWindowClass = {.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC, - .lpfnWndProc = LayerWinProc, - .hInstance = GetModuleHandleW(NULL), - .lpszClassName = "layer_window_class", - .hCursor = LoadCursor(0, IDC_ARROW)}; - - RegisterClass(&layerWindowClass); - - RECT parentRect; - GetClientRect(window->win32.hWnd, &parentRect); - int width = parentRect.right - parentRect.left; - int height = parentRect.bottom - parentRect.top; - - surface->layer.hWnd = CreateWindow("layer_window_class", "layer", - WS_CHILD | WS_VISIBLE, - 0, 0, width, height, - window->win32.hWnd, - 0, - layerWindowClass.hInstance, - 0); -} - -void mg_surface_init_remote(mg_surface_data* surface, u32 width, u32 height) -{ - surface->contentsScaling = mg_win32_surface_contents_scaling; - surface->getFrame = mg_win32_surface_get_frame; - surface->setFrame = mg_win32_surface_set_frame; - surface->getHidden = mg_win32_surface_get_hidden; - surface->setHidden = mg_win32_surface_set_hidden; - surface->nativeLayer = mg_win32_surface_native_layer; - surface->remoteID = mg_win32_surface_remote_id; - - WNDCLASS layerWindowClass = {.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC, - .lpfnWndProc = DefWindowProc, - .hInstance = GetModuleHandleW(NULL), - .lpszClassName = "server_layer_window_class", - .hCursor = LoadCursor(0, IDC_ARROW)}; - - RegisterClass(&layerWindowClass); - - //NOTE(martin): create a temporary parent window. This seems like a necessary hack, because if layer window is created as - // a normal window first, and then parented to the client window, it breaks resizing the parent - // window for some reason... - HWND tmpParent = CreateWindow("server_layer_window_class", "layerParent", - WS_OVERLAPPED, - 0, 0, width, height, - 0, - 0, - layerWindowClass.hInstance, - 0); - - //NOTE: create the layer window - surface->layer.hWnd = CreateWindowEx(WS_EX_NOACTIVATE, - "server_layer_window_class", "layer", - WS_CHILD, - 0, 0, width, height, - tmpParent, - 0, - layerWindowClass.hInstance, - 0); - - //NOTE: unparent it and destroy tmp parent - SetParent(surface->layer.hWnd, 0); - DestroyWindow(tmpParent); -} - -mg_surface_data* mg_win32_surface_create_host(mp_window window) -{ - mg_surface_data* surface = 0; - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - surface = malloc_type(mg_surface_data); - if(surface) - { - memset(surface, 0, sizeof(mg_surface_data)); - mg_surface_init_for_window(surface, windowData); - - surface->backend = MG_BACKEND_HOST; - surface->hostConnect = mg_win32_surface_host_connect; - } - } - return(surface); -} - -/////////////////////////////////////////// WIP /////////////////////////////////////////////// -//TODO: this is thrown here for a quick test. We should: -// - check for errors -// - use utf8 version of API -str8 mp_app_get_executable_path(mem_arena* arena) -{ - char* buffer = mem_arena_alloc_array(arena, char, MAX_PATH+1); - int size = GetModuleFileName(NULL, buffer, MAX_PATH+1); - //TODO: check for errors... - - return(str8_from_buffer(size, buffer)); -} - -str8 mp_app_get_resource_path(mem_arena* arena, const char* name) -{ - str8_list list = {0}; - mem_arena* scratch = mem_scratch(); - - str8 executablePath = mp_app_get_executable_path(scratch); - char* executablePathCString = str8_to_cstring(scratch, executablePath); - - char* driveBuffer = mem_arena_alloc_array(scratch, char, MAX_PATH); - char* dirBuffer = mem_arena_alloc_array(scratch, char, MAX_PATH); - - _splitpath_s(executablePathCString, driveBuffer, MAX_PATH, dirBuffer, MAX_PATH, 0, 0, 0, 0); - - str8 drive = STR8(driveBuffer); - str8 dirPath = STR8(dirBuffer); - - str8_list_push(scratch, &list, drive); - str8_list_push(scratch, &list, dirPath); - str8_list_push(scratch, &list, STR8("\\")); - str8_list_push(scratch, &list, str8_push_cstring(scratch, name)); - str8 path = str8_list_join(scratch, list); - char* pathCString = str8_to_cstring(scratch, path); - - char* buffer = mem_arena_alloc_array(arena, char, path.len+1); - char* filePart = 0; - int size = GetFullPathName(pathCString, MAX_PATH, buffer, &filePart); - - str8 result = str8_from_buffer(size, buffer); - return(result); -} -////////////////////////////////////////////////////////////////////////////////////////////////// +/************************************************************//** +* +* @file: win32_app.c +* @author: Martin Fouilleul +* @date: 16/12/2022 +* @revision: +* +*****************************************************************/ + +#include"mp_app.c" + +void mp_init_keys() +{ + memset(__mpApp.keyCodes, MP_KEY_UNKNOWN, 256*sizeof(int)); + + __mpApp.keyCodes[0x00B] = MP_KEY_0; + __mpApp.keyCodes[0x002] = MP_KEY_1; + __mpApp.keyCodes[0x003] = MP_KEY_2; + __mpApp.keyCodes[0x004] = MP_KEY_3; + __mpApp.keyCodes[0x005] = MP_KEY_4; + __mpApp.keyCodes[0x006] = MP_KEY_5; + __mpApp.keyCodes[0x007] = MP_KEY_6; + __mpApp.keyCodes[0x008] = MP_KEY_7; + __mpApp.keyCodes[0x009] = MP_KEY_8; + __mpApp.keyCodes[0x00A] = MP_KEY_9; + __mpApp.keyCodes[0x01E] = MP_KEY_A; + __mpApp.keyCodes[0x030] = MP_KEY_B; + __mpApp.keyCodes[0x02E] = MP_KEY_C; + __mpApp.keyCodes[0x020] = MP_KEY_D; + __mpApp.keyCodes[0x012] = MP_KEY_E; + __mpApp.keyCodes[0x021] = MP_KEY_F; + __mpApp.keyCodes[0x022] = MP_KEY_G; + __mpApp.keyCodes[0x023] = MP_KEY_H; + __mpApp.keyCodes[0x017] = MP_KEY_I; + __mpApp.keyCodes[0x024] = MP_KEY_J; + __mpApp.keyCodes[0x025] = MP_KEY_K; + __mpApp.keyCodes[0x026] = MP_KEY_L; + __mpApp.keyCodes[0x032] = MP_KEY_M; + __mpApp.keyCodes[0x031] = MP_KEY_N; + __mpApp.keyCodes[0x018] = MP_KEY_O; + __mpApp.keyCodes[0x019] = MP_KEY_P; + __mpApp.keyCodes[0x010] = MP_KEY_Q; + __mpApp.keyCodes[0x013] = MP_KEY_R; + __mpApp.keyCodes[0x01F] = MP_KEY_S; + __mpApp.keyCodes[0x014] = MP_KEY_T; + __mpApp.keyCodes[0x016] = MP_KEY_U; + __mpApp.keyCodes[0x02F] = MP_KEY_V; + __mpApp.keyCodes[0x011] = MP_KEY_W; + __mpApp.keyCodes[0x02D] = MP_KEY_X; + __mpApp.keyCodes[0x015] = MP_KEY_Y; + __mpApp.keyCodes[0x02C] = MP_KEY_Z; + __mpApp.keyCodes[0x028] = MP_KEY_APOSTROPHE; + __mpApp.keyCodes[0x02B] = MP_KEY_BACKSLASH; + __mpApp.keyCodes[0x033] = MP_KEY_COMMA; + __mpApp.keyCodes[0x00D] = MP_KEY_EQUAL; + __mpApp.keyCodes[0x029] = MP_KEY_GRAVE_ACCENT; + __mpApp.keyCodes[0x01A] = MP_KEY_LEFT_BRACKET; + __mpApp.keyCodes[0x00C] = MP_KEY_MINUS; + __mpApp.keyCodes[0x034] = MP_KEY_PERIOD; + __mpApp.keyCodes[0x01B] = MP_KEY_RIGHT_BRACKET; + __mpApp.keyCodes[0x027] = MP_KEY_SEMICOLON; + __mpApp.keyCodes[0x035] = MP_KEY_SLASH; + __mpApp.keyCodes[0x056] = MP_KEY_WORLD_2; + __mpApp.keyCodes[0x00E] = MP_KEY_BACKSPACE; + __mpApp.keyCodes[0x153] = MP_KEY_DELETE; + __mpApp.keyCodes[0x14F] = MP_KEY_END; + __mpApp.keyCodes[0x01C] = MP_KEY_ENTER; + __mpApp.keyCodes[0x001] = MP_KEY_ESCAPE; + __mpApp.keyCodes[0x147] = MP_KEY_HOME; + __mpApp.keyCodes[0x152] = MP_KEY_INSERT; + __mpApp.keyCodes[0x15D] = MP_KEY_MENU; + __mpApp.keyCodes[0x151] = MP_KEY_PAGE_DOWN; + __mpApp.keyCodes[0x149] = MP_KEY_PAGE_UP; + __mpApp.keyCodes[0x045] = MP_KEY_PAUSE; + __mpApp.keyCodes[0x146] = MP_KEY_PAUSE; + __mpApp.keyCodes[0x039] = MP_KEY_SPACE; + __mpApp.keyCodes[0x00F] = MP_KEY_TAB; + __mpApp.keyCodes[0x03A] = MP_KEY_CAPS_LOCK; + __mpApp.keyCodes[0x145] = MP_KEY_NUM_LOCK; + __mpApp.keyCodes[0x046] = MP_KEY_SCROLL_LOCK; + __mpApp.keyCodes[0x03B] = MP_KEY_F1; + __mpApp.keyCodes[0x03C] = MP_KEY_F2; + __mpApp.keyCodes[0x03D] = MP_KEY_F3; + __mpApp.keyCodes[0x03E] = MP_KEY_F4; + __mpApp.keyCodes[0x03F] = MP_KEY_F5; + __mpApp.keyCodes[0x040] = MP_KEY_F6; + __mpApp.keyCodes[0x041] = MP_KEY_F7; + __mpApp.keyCodes[0x042] = MP_KEY_F8; + __mpApp.keyCodes[0x043] = MP_KEY_F9; + __mpApp.keyCodes[0x044] = MP_KEY_F10; + __mpApp.keyCodes[0x057] = MP_KEY_F11; + __mpApp.keyCodes[0x058] = MP_KEY_F12; + __mpApp.keyCodes[0x064] = MP_KEY_F13; + __mpApp.keyCodes[0x065] = MP_KEY_F14; + __mpApp.keyCodes[0x066] = MP_KEY_F15; + __mpApp.keyCodes[0x067] = MP_KEY_F16; + __mpApp.keyCodes[0x068] = MP_KEY_F17; + __mpApp.keyCodes[0x069] = MP_KEY_F18; + __mpApp.keyCodes[0x06A] = MP_KEY_F19; + __mpApp.keyCodes[0x06B] = MP_KEY_F20; + __mpApp.keyCodes[0x06C] = MP_KEY_F21; + __mpApp.keyCodes[0x06D] = MP_KEY_F22; + __mpApp.keyCodes[0x06E] = MP_KEY_F23; + __mpApp.keyCodes[0x076] = MP_KEY_F24; + __mpApp.keyCodes[0x038] = MP_KEY_LEFT_ALT; + __mpApp.keyCodes[0x01D] = MP_KEY_LEFT_CONTROL; + __mpApp.keyCodes[0x02A] = MP_KEY_LEFT_SHIFT; + __mpApp.keyCodes[0x15B] = MP_KEY_LEFT_SUPER; + __mpApp.keyCodes[0x137] = MP_KEY_PRINT_SCREEN; + __mpApp.keyCodes[0x138] = MP_KEY_RIGHT_ALT; + __mpApp.keyCodes[0x11D] = MP_KEY_RIGHT_CONTROL; + __mpApp.keyCodes[0x036] = MP_KEY_RIGHT_SHIFT; + __mpApp.keyCodes[0x15C] = MP_KEY_RIGHT_SUPER; + __mpApp.keyCodes[0x150] = MP_KEY_DOWN; + __mpApp.keyCodes[0x14B] = MP_KEY_LEFT; + __mpApp.keyCodes[0x14D] = MP_KEY_RIGHT; + __mpApp.keyCodes[0x148] = MP_KEY_UP; + __mpApp.keyCodes[0x052] = MP_KEY_KP_0; + __mpApp.keyCodes[0x04F] = MP_KEY_KP_1; + __mpApp.keyCodes[0x050] = MP_KEY_KP_2; + __mpApp.keyCodes[0x051] = MP_KEY_KP_3; + __mpApp.keyCodes[0x04B] = MP_KEY_KP_4; + __mpApp.keyCodes[0x04C] = MP_KEY_KP_5; + __mpApp.keyCodes[0x04D] = MP_KEY_KP_6; + __mpApp.keyCodes[0x047] = MP_KEY_KP_7; + __mpApp.keyCodes[0x048] = MP_KEY_KP_8; + __mpApp.keyCodes[0x049] = MP_KEY_KP_9; + __mpApp.keyCodes[0x04E] = MP_KEY_KP_ADD; + __mpApp.keyCodes[0x053] = MP_KEY_KP_DECIMAL; + __mpApp.keyCodes[0x135] = MP_KEY_KP_DIVIDE; + __mpApp.keyCodes[0x11C] = MP_KEY_KP_ENTER; + __mpApp.keyCodes[0x037] = MP_KEY_KP_MULTIPLY; + __mpApp.keyCodes[0x04A] = MP_KEY_KP_SUBTRACT; + + memset(__mpApp.nativeKeys, 0, sizeof(int)*MP_KEY_COUNT); + for(int nativeKey=0; nativeKey<256; nativeKey++) + { + mp_key_code mpKey = __mpApp.keyCodes[nativeKey]; + if(mpKey) + { + __mpApp.nativeKeys[mpKey] = nativeKey; + } + } +} + +void mp_init() +{ + if(!__mpApp.init) + { + memset(&__mpApp, 0, sizeof(__mpApp)); + + mp_init_common(); + mp_init_keys(); + + __mpApp.win32.savedConsoleCodePage = GetConsoleOutputCP(); + SetConsoleOutputCP(CP_UTF8); + + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + } +} + +void mp_terminate() +{ + if(__mpApp.init) + { + SetConsoleOutputCP(__mpApp.win32.savedConsoleCodePage); + + mp_terminate_common(); + __mpApp = (mp_app){0}; + } +} + +static mp_key_code mp_convert_win32_key(int code) +{ + return(__mpApp.keyCodes[code]); +} + +static mp_keymod_flags mp_get_mod_keys() +{ + mp_keymod_flags mods = 0; + if(GetKeyState(VK_SHIFT) & 0x8000) + { + mods |= MP_KEYMOD_SHIFT; + } + if(GetKeyState(VK_CONTROL) & 0x8000) + { + mods |= MP_KEYMOD_CTRL; + } + if(GetKeyState(VK_MENU) & 0x8000) + { + mods |= MP_KEYMOD_ALT; + } + if((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & 0x8000) + { + mods |= MP_KEYMOD_CMD; + } + return(mods); +} + +static void process_mouse_event(mp_window_data* window, mp_key_action action, mp_key_code button) +{ + if(action == MP_KEY_PRESS) + { + if(!__mpApp.win32.mouseCaptureMask) + { + SetCapture(window->win32.hWnd); + } + __mpApp.win32.mouseCaptureMask |= (1<win32.hWnd, + HWND_TOP, + rect.left, + rect.top, + rect.right - rect.left, + rect.bottom - rect.top, + SWP_NOACTIVATE | SWP_NOZORDER); + + //TODO: send a message + + } break; + + //TODO: enter/exit size & move + + case WM_SIZING: + { + //TODO: take dpi into account + + RECT* rect = (RECT*)lParam; + + mp_event event = {0}; + event.window = mp_window_handle_from_ptr(mpWindow); + event.type = MP_EVENT_WINDOW_RESIZE; + event.frame.rect = (mp_rect){rect->bottom, rect->left, rect->top - rect->bottom, rect->right - rect->left}; + mp_queue_event(&event); + } break; + + case WM_MOVING: + { + //TODO: take dpi into account + + RECT* rect = (RECT*)lParam; + + mp_event event = {0}; + event.window = mp_window_handle_from_ptr(mpWindow); + event.type = MP_EVENT_WINDOW_MOVE; + event.frame.rect = (mp_rect){rect->bottom, rect->left, rect->top - rect->bottom, rect->right - rect->left}; + mp_queue_event(&event); + } break; + + case WM_SETFOCUS: + { + mp_event event = {0}; + event.window = mp_window_handle_from_ptr(mpWindow); + event.type = MP_EVENT_WINDOW_FOCUS; + mp_queue_event(&event); + } break; + + case WM_KILLFOCUS: + { + mp_event event = {0}; + event.window = mp_window_handle_from_ptr(mpWindow); + event.type = MP_EVENT_WINDOW_UNFOCUS; + mp_queue_event(&event); + } break; + + case WM_SIZE: + { + bool minimized = (wParam == SIZE_MINIMIZED); + if(minimized != mpWindow->minimized) + { + mpWindow->minimized = minimized; + + mp_event event = {0}; + event.window = mp_window_handle_from_ptr(mpWindow); + + if(minimized) + { + event.type = MP_EVENT_WINDOW_HIDE; + } + else if(mpWindow->minimized) + { + event.type = MP_EVENT_WINDOW_SHOW; + } + mp_queue_event(&event); + } + } break; + + case WM_LBUTTONDOWN: + { + process_mouse_event(mpWindow, MP_KEY_PRESS, MP_MOUSE_LEFT); + } break; + + case WM_RBUTTONDOWN: + { + process_mouse_event(mpWindow, MP_KEY_PRESS, MP_MOUSE_RIGHT); + } break; + + case WM_MBUTTONDOWN: + { + process_mouse_event(mpWindow, MP_KEY_PRESS, MP_MOUSE_MIDDLE); + } break; + + case WM_LBUTTONUP: + { + process_mouse_event(mpWindow, MP_KEY_RELEASE, MP_MOUSE_LEFT); + } break; + + case WM_RBUTTONUP: + { + process_mouse_event(mpWindow, MP_KEY_RELEASE, MP_MOUSE_RIGHT); + } break; + + case WM_MBUTTONUP: + { + process_mouse_event(mpWindow, MP_KEY_RELEASE, MP_MOUSE_MIDDLE); + } break; + + case WM_MOUSEMOVE: + { + RECT rect; + GetClientRect(mpWindow->win32.hWnd, &rect); + + u32 dpi = GetDpiForWindow(mpWindow->win32.hWnd); + f32 scaling = (f32)dpi/96.; + + mp_event event = {0}; + event.window = mp_window_handle_from_ptr(mpWindow); + event.type = MP_EVENT_MOUSE_MOVE; + event.move.x = LOWORD(lParam) / scaling; + event.move.y = HIWORD(lParam) / scaling; + + if(__mpApp.win32.mouseTracked || __mpApp.win32.mouseCaptureMask) + { + event.move.deltaX = event.move.x - __mpApp.win32.lastMousePos.x; + event.move.deltaY = event.move.y - __mpApp.win32.lastMousePos.y; + } + __mpApp.win32.lastMousePos = (vec2){event.move.x, event.move.y}; + + if(!__mpApp.win32.mouseTracked) + { + __mpApp.win32.mouseTracked = true; + + TRACKMOUSEEVENT track; + memset(&track, 0, sizeof(track)); + track.cbSize = sizeof(track); + track.dwFlags = TME_LEAVE; + track.hwndTrack = mpWindow->win32.hWnd; + TrackMouseEvent(&track); + + mp_event enter = {.window = event.window, + .type = MP_EVENT_MOUSE_ENTER, + .move.x = event.move.x, + .move.y = event.move.y}; + mp_queue_event(&enter); + } + + mp_queue_event(&event); + } break; + + case WM_MOUSELEAVE: + { + __mpApp.win32.mouseTracked = false; + + mp_event event = {0}; + event.window = mp_window_handle_from_ptr(mpWindow); + event.type = MP_EVENT_MOUSE_LEAVE; + mp_queue_event(&event); + } break; + + case WM_MOUSEWHEEL: + { + process_wheel_event(mpWindow, 0, (float)((i16)HIWORD(wParam))); + } break; + + case WM_MOUSEHWHEEL: + { + process_wheel_event(mpWindow, (float)((i16)HIWORD(wParam)), 0); + } break; + + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + { + mp_event event = {0}; + event.window = mp_window_handle_from_ptr(mpWindow); + event.type = MP_EVENT_KEYBOARD_KEY; + event.key.action = (lParam & 0x40000000) ? MP_KEY_REPEAT : MP_KEY_PRESS; + event.key.code = mp_convert_win32_key(HIWORD(lParam) & 0x1ff); + event.key.mods = mp_get_mod_keys(); + mp_queue_event(&event); + } break; + + case WM_KEYUP: + case WM_SYSKEYUP: + { + mp_event event = {0}; + event.window = mp_window_handle_from_ptr(mpWindow); + event.type = MP_EVENT_KEYBOARD_KEY; + event.key.action = MP_KEY_RELEASE; + event.key.code = mp_convert_win32_key(HIWORD(lParam) & 0x1ff); + event.key.mods = mp_get_mod_keys(); + mp_queue_event(&event); + } break; + + case WM_CHAR: + { + if((u32)wParam >= 32) + { + mp_event event = {0}; + event.window = mp_window_handle_from_ptr(mpWindow); + event.type = MP_EVENT_KEYBOARD_CHAR; + event.character.codepoint = (utf32)wParam; + str8 seq = utf8_encode(event.character.sequence, event.character.codepoint); + event.character.seqLen = seq.len; + mp_queue_event(&event); + } + } break; + + case WM_DROPFILES: + { + //TODO + } break; + + default: + { + result = DefWindowProc(windowHandle, message, wParam, lParam); + } break; + } + + return(result); +} + +//-------------------------------------------------------------------- +// app management +//-------------------------------------------------------------------- + +bool mp_should_quit() +{ + return(__mpApp.shouldQuit); +} + +void mp_cancel_quit() +{ + __mpApp.shouldQuit = false; +} + +void mp_request_quit() +{ + __mpApp.shouldQuit = true; +} + +void mp_pump_events(f64 timeout) +{ + MSG message; + while(PeekMessage(&message, 0, 0, 0, PM_REMOVE)) + { + TranslateMessage(&message); + DispatchMessage(&message); + } +} + +//-------------------------------------------------------------------- +// window management +//-------------------------------------------------------------------- + +//WARN: the following header pulls in objbase.h (even with WIN32_LEAN_AND_MEAN), which +// #defines interface to struct... so make sure to #undef interface since it's a +// name we want to be able to use throughout the codebase +#include +#undef interface + +mp_window mp_window_create(mp_rect rect, const char* title, mp_window_style style) +{ + WNDCLASS windowClass = {.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC, + .lpfnWndProc = WinProc, + .hInstance = GetModuleHandleW(NULL), + .lpszClassName = "ApplicationWindowClass", + .hCursor = LoadCursor(0, IDC_ARROW)}; + + if(!RegisterClass(&windowClass)) + { + //TODO: error + goto quit; + } + + u32 dpiX, dpiY; + HMONITOR monitor = MonitorFromPoint((POINT){rect.x, rect.y}, MONITOR_DEFAULTTOPRIMARY); + GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); + + f32 dpiScalingX = (f32)dpiX/96.; + f32 dpiScalingY = (f32)dpiY/96.; + + HWND windowHandle = CreateWindow("ApplicationWindowClass", "Test Window", + WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, + rect.w * dpiScalingX, rect.h * dpiScalingY, + 0, 0, windowClass.hInstance, 0); + + if(!windowHandle) + { + //TODO: error + goto quit; + } + + UpdateWindow(windowHandle); + + //TODO: return wrapped window + quit:; + mp_window_data* window = mp_window_alloc(); + window->win32.hWnd = windowHandle; + + SetPropW(windowHandle, L"MilePost", window); + + return(mp_window_handle_from_ptr(window)); +} + +void mp_window_destroy(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + DestroyWindow(windowData->win32.hWnd); + //TODO: check when to unregister class + + mp_window_recycle_ptr(windowData); + } +} + +void* mp_window_native_pointer(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + return(windowData->win32.hWnd); + } + else + { + return(0); + } +} + +bool mp_window_should_close(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + return(windowData->shouldClose); + } + else + { + return(false); + } +} + +void mp_window_request_close(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + windowData->shouldClose = true; + PostMessage(windowData->win32.hWnd, WM_CLOSE, 0, 0); + } +} + +void mp_window_cancel_close(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + windowData->shouldClose = false; + } +} + + +bool mp_window_is_hidden(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + return(IsWindowVisible(windowData->win32.hWnd)); + } + else + { + return(false); + } +} + +void mp_window_hide(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + ShowWindow(windowData->win32.hWnd, SW_HIDE); + } +} + +void mp_window_show(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + ShowWindow(windowData->win32.hWnd, SW_NORMAL); + } +} + +bool mp_window_is_minimized(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + return(windowData->minimized); + } + else + { + return(false); + } +} + +void mp_window_minimize(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + ShowWindow(windowData->win32.hWnd, SW_MINIMIZE); + } +} + +void mp_window_maximize(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + ShowWindow(windowData->win32.hWnd, SW_MAXIMIZE); + } +} + +void mp_window_restore(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + ShowWindow(windowData->win32.hWnd, SW_RESTORE); + } +} + +bool mp_window_has_focus(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + return(GetActiveWindow() == windowData->win32.hWnd); + } + else + { + return(false); + } +} + +void mp_window_focus(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + SetFocus(windowData->win32.hWnd); + } +} + +void mp_window_unfocus(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + SetFocus(0); + } +} + +void mp_window_send_to_back(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + SetWindowPos(windowData->win32.hWnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); + } +} + +void mp_window_bring_to_front(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + if(!IsWindowVisible(windowData->win32.hWnd)) + { + ShowWindow(windowData->win32.hWnd, SW_NORMAL); + } + SetWindowPos(windowData->win32.hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE); + } +} + +mp_rect mp_window_get_content_rect(mp_window window) +{ + mp_rect rect = {0}; + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + RECT winRect; + if(GetClientRect(windowData->win32.hWnd, &winRect)) + { + u32 dpi = GetDpiForWindow(windowData->win32.hWnd); + f32 scale = (float)dpi/96.; + rect = (mp_rect){0, 0, (winRect.right - winRect.left)/scale, (winRect.bottom - winRect.top)/scale}; + } + } + return(rect); +} + +//-------------------------------------------------------------------------------- +// clipboard functions +//-------------------------------------------------------------------------------- + +MP_API void mp_clipboard_clear(void) +{ + if(OpenClipboard(NULL)) + { + EmptyClipboard(); + CloseClipboard(); + } +} + +MP_API void mp_clipboard_set_string(str8 string) +{ + if(OpenClipboard(NULL)) + { + EmptyClipboard(); + + int wideCount = MultiByteToWideChar(CP_UTF8, 0, string.ptr, string.len, 0, 0); + HANDLE handle = GlobalAlloc(GMEM_MOVEABLE, (wideCount+1)*sizeof(wchar_t)); + if(handle) + { + char* memory = GlobalLock(handle); + if(memory) + { + MultiByteToWideChar(CP_UTF8, 0, string.ptr, string.len, (wchar_t*)memory, wideCount); + ((wchar_t*)memory)[wideCount] = '\0'; + + GlobalUnlock(handle); + SetClipboardData(CF_UNICODETEXT, handle); + } + } + CloseClipboard(); + } +} + +MP_API str8 mp_clipboard_get_string(mem_arena* arena) +{ + str8 string = {0}; + + if(OpenClipboard(NULL)) + { + HANDLE handle = GetClipboardData(CF_UNICODETEXT); + if(handle) + { + char* memory = GlobalLock(handle); + if(memory) + { + u64 size = WideCharToMultiByte(CP_UTF8, 0, (wchar_t*)memory, -1, 0, 0, 0, 0); + if(size) + { + string.ptr = mem_arena_alloc(arena, size); + string.len = size - 1; + WideCharToMultiByte(CP_UTF8, 0, (wchar_t*)memory, -1, string.ptr, size, 0, 0); + GlobalUnlock(handle); + } + } + } + CloseClipboard(); + } + return(string); +} + +MP_API str8 mp_clipboard_copy_string(str8 backing) +{ + //TODO + return((str8){0}); +} + + +//-------------------------------------------------------------------------------- +// win32 surfaces +//-------------------------------------------------------------------------------- + +#include"graphics_surface.h" + +vec2 mg_win32_surface_contents_scaling(mg_surface_data* surface) +{ + u32 dpi = GetDpiForWindow(surface->layer.hWnd); + vec2 contentsScaling = (vec2){(float)dpi/96., (float)dpi/96.}; + return(contentsScaling); +} + +mp_rect mg_win32_surface_get_frame(mg_surface_data* surface) +{ + RECT rect = {0}; + GetClientRect(surface->layer.hWnd, &rect); + + vec2 scale = mg_win32_surface_contents_scaling(surface); + + mp_rect res = {rect.left/scale.x, + rect.bottom/scale.y, + (rect.right - rect.left)/scale.x, + (rect.bottom - rect.top)/scale.y}; + return(res); +} + +void mg_win32_surface_set_frame(mg_surface_data* surface, mp_rect frame) +{ + HWND parent = GetParent(surface->layer.hWnd); + RECT parentContentRect; + + GetClientRect(parent, &parentContentRect); + int parentHeight = parentContentRect.bottom - parentContentRect.top; + + vec2 scale = mg_win32_surface_contents_scaling(surface); + + SetWindowPos(surface->layer.hWnd, + HWND_TOP, + frame.x * scale.x, + parentHeight - (frame.y + frame.h) * scale.y, + frame.w * scale.x, + frame.h * scale.y, + SWP_NOACTIVATE | SWP_NOZORDER); +} + +bool mg_win32_surface_get_hidden(mg_surface_data* surface) +{ + bool hidden = !IsWindowVisible(surface->layer.hWnd); + return(hidden); +} + +void mg_win32_surface_set_hidden(mg_surface_data* surface, bool hidden) +{ + ShowWindow(surface->layer.hWnd, hidden ? SW_HIDE : SW_NORMAL); +} + +void* mg_win32_surface_native_layer(mg_surface_data* surface) +{ + return((void*)surface->layer.hWnd); +} + +mg_surface_id mg_win32_surface_remote_id(mg_surface_data* surface) +{ + return((mg_surface_id)surface->layer.hWnd); +} + +void mg_win32_surface_host_connect(mg_surface_data* surface, mg_surface_id remoteID) +{ + HWND dstWnd = surface->layer.hWnd; + HWND srcWnd = (HWND)remoteID; + + RECT dstRect; + GetClientRect(dstWnd, &dstRect); + + SetParent(srcWnd, dstWnd); + ShowWindow(srcWnd, SW_NORMAL); + + SetWindowPos(srcWnd, + HWND_TOP, + 0, + 0, + dstRect.right - dstRect.left, + dstRect.bottom - dstRect.top, + SWP_NOACTIVATE | SWP_NOZORDER); +} + +void mg_surface_cleanup(mg_surface_data* surface) +{ + DestroyWindow(surface->layer.hWnd); +} + +LRESULT LayerWinProc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam) +{ + if(message == WM_NCHITTEST) + { + return(HTTRANSPARENT); + } + else + { + return(DefWindowProc(windowHandle, message, wParam, lParam)); + } +} + +void mg_surface_init_for_window(mg_surface_data* surface, mp_window_data* window) +{ + surface->contentsScaling = mg_win32_surface_contents_scaling; + surface->getFrame = mg_win32_surface_get_frame; + surface->setFrame = mg_win32_surface_set_frame; + surface->getHidden = mg_win32_surface_get_hidden; + surface->setHidden = mg_win32_surface_set_hidden; + surface->nativeLayer = mg_win32_surface_native_layer; + + //NOTE(martin): create a child window for the surface + WNDCLASS layerWindowClass = {.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC, + .lpfnWndProc = LayerWinProc, + .hInstance = GetModuleHandleW(NULL), + .lpszClassName = "layer_window_class", + .hCursor = LoadCursor(0, IDC_ARROW)}; + + RegisterClass(&layerWindowClass); + + RECT parentRect; + GetClientRect(window->win32.hWnd, &parentRect); + int width = parentRect.right - parentRect.left; + int height = parentRect.bottom - parentRect.top; + + surface->layer.hWnd = CreateWindow("layer_window_class", "layer", + WS_CHILD | WS_VISIBLE, + 0, 0, width, height, + window->win32.hWnd, + 0, + layerWindowClass.hInstance, + 0); +} + +void mg_surface_init_remote(mg_surface_data* surface, u32 width, u32 height) +{ + surface->contentsScaling = mg_win32_surface_contents_scaling; + surface->getFrame = mg_win32_surface_get_frame; + surface->setFrame = mg_win32_surface_set_frame; + surface->getHidden = mg_win32_surface_get_hidden; + surface->setHidden = mg_win32_surface_set_hidden; + surface->nativeLayer = mg_win32_surface_native_layer; + surface->remoteID = mg_win32_surface_remote_id; + + WNDCLASS layerWindowClass = {.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC, + .lpfnWndProc = DefWindowProc, + .hInstance = GetModuleHandleW(NULL), + .lpszClassName = "server_layer_window_class", + .hCursor = LoadCursor(0, IDC_ARROW)}; + + RegisterClass(&layerWindowClass); + + //NOTE(martin): create a temporary parent window. This seems like a necessary hack, because if layer window is created as + // a normal window first, and then parented to the client window, it breaks resizing the parent + // window for some reason... + HWND tmpParent = CreateWindow("server_layer_window_class", "layerParent", + WS_OVERLAPPED, + 0, 0, width, height, + 0, + 0, + layerWindowClass.hInstance, + 0); + + //NOTE: create the layer window + surface->layer.hWnd = CreateWindowEx(WS_EX_NOACTIVATE, + "server_layer_window_class", "layer", + WS_CHILD, + 0, 0, width, height, + tmpParent, + 0, + layerWindowClass.hInstance, + 0); + + //NOTE: unparent it and destroy tmp parent + SetParent(surface->layer.hWnd, 0); + DestroyWindow(tmpParent); +} + +mg_surface_data* mg_win32_surface_create_host(mp_window window) +{ + mg_surface_data* surface = 0; + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + surface = malloc_type(mg_surface_data); + if(surface) + { + memset(surface, 0, sizeof(mg_surface_data)); + mg_surface_init_for_window(surface, windowData); + + surface->api = MG_HOST; + surface->hostConnect = mg_win32_surface_host_connect; + } + } + return(surface); +} + +/////////////////////////////////////////// WIP /////////////////////////////////////////////// +//TODO: this is thrown here for a quick test. We should: +// - check for errors +// - use utf8 version of API +str8 mp_app_get_executable_path(mem_arena* arena) +{ + char* buffer = mem_arena_alloc_array(arena, char, MAX_PATH+1); + int size = GetModuleFileName(NULL, buffer, MAX_PATH+1); + //TODO: check for errors... + + return(str8_from_buffer(size, buffer)); +} + +str8 mp_app_get_resource_path(mem_arena* arena, const char* name) +{ + str8_list list = {0}; + mem_arena* scratch = mem_scratch(); + + str8 executablePath = mp_app_get_executable_path(scratch); + char* executablePathCString = str8_to_cstring(scratch, executablePath); + + char* driveBuffer = mem_arena_alloc_array(scratch, char, MAX_PATH); + char* dirBuffer = mem_arena_alloc_array(scratch, char, MAX_PATH); + + _splitpath_s(executablePathCString, driveBuffer, MAX_PATH, dirBuffer, MAX_PATH, 0, 0, 0, 0); + + str8 drive = STR8(driveBuffer); + str8 dirPath = STR8(dirBuffer); + + str8_list_push(scratch, &list, drive); + str8_list_push(scratch, &list, dirPath); + str8_list_push(scratch, &list, STR8("\\")); + str8_list_push(scratch, &list, str8_push_cstring(scratch, name)); + str8 path = str8_list_join(scratch, list); + char* pathCString = str8_to_cstring(scratch, path); + + char* buffer = mem_arena_alloc_array(arena, char, path.len+1); + char* filePart = 0; + int size = GetFullPathName(pathCString, MAX_PATH, buffer, &filePart); + + str8 result = str8_from_buffer(size, buffer); + return(result); +} +////////////////////////////////////////////////////////////////////////////////////////////////// + +//-------------------------------------------------------------------- +// native open/save/alert windows +//-------------------------------------------------------------------- + +//TODO: GetOpenFileName() doesn't seem to support selecting folders, and +// requires filters which pair a "descriptive" name with an extension + +#define interface struct +#include +#include +#undef interface + + +MP_API str8 mp_open_dialog(mem_arena* arena, + const char* title, + const char* defaultPath, + int filterCount, + const char** filters, + bool directory) +{ + str8 res = {0}; + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if(SUCCEEDED(hr)) + { + IFileOpenDialog* dialog = 0; + hr = CoCreateInstance(&CLSID_FileOpenDialog, NULL, CLSCTX_ALL, &IID_IFileOpenDialog, (void**)&dialog); + if(SUCCEEDED(hr)) + { + if(directory) + { + FILEOPENDIALOGOPTIONS opt; + dialog->lpVtbl->GetOptions(dialog, &opt); + dialog->lpVtbl->SetOptions(dialog, opt | FOS_PICKFOLDERS); + } + + if(filterCount && filters) + { + mem_arena_marker mark = mem_arena_mark(arena); + COMDLG_FILTERSPEC* filterSpecs = mem_arena_alloc_array(arena, COMDLG_FILTERSPEC, filterCount); + for(int i=0; ilpVtbl->SetFileTypes(dialog, filterCount, filterSpecs); + + mem_arena_clear_to(arena, mark); + } + + if(defaultPath) + { + mem_arena_marker mark = mem_arena_mark(arena); + int pathWideSize = MultiByteToWideChar(CP_UTF8, 0, defaultPath, -1, NULL, 0); + LPWSTR pathWide = mem_arena_alloc_array(arena, wchar_t, pathWideSize); + MultiByteToWideChar(CP_UTF8, 0, defaultPath, -1, pathWide, pathWideSize); + + IShellItem* item = 0; + hr = SHCreateItemFromParsingName(pathWide, NULL, &IID_IShellItem, (void**)&item); + if(SUCCEEDED(hr)) + { + hr = dialog->lpVtbl->SetFolder(dialog, item); + item->lpVtbl->Release(item); + } + mem_arena_clear_to(arena, mark); + } + + hr = dialog->lpVtbl->Show(dialog, NULL); + if(SUCCEEDED(hr)) + { + IShellItem* item; + hr = dialog->lpVtbl->GetResult(dialog, &item); + if(SUCCEEDED(hr)) + { + PWSTR filePath; + hr = item->lpVtbl->GetDisplayName(item, SIGDN_FILESYSPATH, &filePath); + + if(SUCCEEDED(hr)) + { + int utf8Size = WideCharToMultiByte(CP_UTF8, 0, filePath, -1, NULL, 0, NULL, NULL); + if(utf8Size > 0) + { + res.ptr = mem_arena_alloc(arena, utf8Size); + res.len = utf8Size-1; + WideCharToMultiByte(CP_UTF8, 0, filePath, -1, res.ptr, utf8Size, NULL, NULL); + } + CoTaskMemFree(filePath); + } + item->lpVtbl->Release(item); + } + } + } + } + CoUninitialize(); + return(res); +} + +MP_API str8 mp_save_dialog(mem_arena* arena, + const char* title, + const char* defaultPath, + int filterCount, + const char** filters) +{ + str8 res = {0}; + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if(SUCCEEDED(hr)) + { + IFileOpenDialog* dialog = 0; + hr = CoCreateInstance(&CLSID_FileSaveDialog, NULL, CLSCTX_ALL, &IID_IFileSaveDialog, (void**)&dialog); + if(SUCCEEDED(hr)) + { + if(filterCount && filters) + { + mem_arena_marker mark = mem_arena_mark(arena); + COMDLG_FILTERSPEC* filterSpecs = mem_arena_alloc_array(arena, COMDLG_FILTERSPEC, filterCount); + for(int i=0; ilpVtbl->SetFileTypes(dialog, filterCount, filterSpecs); + + mem_arena_clear_to(arena, mark); + } + + if(defaultPath) + { + mem_arena_marker mark = mem_arena_mark(arena); + int pathWideSize = MultiByteToWideChar(CP_UTF8, 0, defaultPath, -1, NULL, 0); + LPWSTR pathWide = mem_arena_alloc_array(arena, wchar_t, pathWideSize); + MultiByteToWideChar(CP_UTF8, 0, defaultPath, -1, pathWide, pathWideSize); + + IShellItem* item = 0; + hr = SHCreateItemFromParsingName(pathWide, NULL, &IID_IShellItem, (void**)&item); + if(SUCCEEDED(hr)) + { + hr = dialog->lpVtbl->SetFolder(dialog, item); + item->lpVtbl->Release(item); + } + mem_arena_clear_to(arena, mark); + } + + hr = dialog->lpVtbl->Show(dialog, NULL); + if(SUCCEEDED(hr)) + { + IShellItem* item; + hr = dialog->lpVtbl->GetResult(dialog, &item); + if(SUCCEEDED(hr)) + { + PWSTR filePath; + hr = item->lpVtbl->GetDisplayName(item, SIGDN_FILESYSPATH, &filePath); + + if(SUCCEEDED(hr)) + { + int utf8Size = WideCharToMultiByte(CP_UTF8, 0, filePath, -1, NULL, 0, NULL, NULL); + if(utf8Size > 0) + { + res.ptr = mem_arena_alloc(arena, utf8Size); + res.len = utf8Size-1; + WideCharToMultiByte(CP_UTF8, 0, filePath, -1, res.ptr, utf8Size, NULL, NULL); + } + CoTaskMemFree(filePath); + } + item->lpVtbl->Release(item); + } + } + } + } + CoUninitialize(); + return(res); +} + +#include + +MP_API int mp_alert_popup(const char* title, + const char* message, + u32 count, + const char** options) +{ + mem_arena* scratch = mem_scratch(); + mem_arena_marker marker = mem_arena_mark(scratch); + TASKDIALOG_BUTTON* buttons = mem_arena_alloc_array(scratch, TASKDIALOG_BUTTON, count); + + for(int i=0; i + + +Orca Runtime + + + + + +