From 62b8b3323ea5f8996326fa20782bdbbbabcee9a6 Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Fri, 12 May 2023 11:42:35 +0200 Subject: [PATCH 01/14] [win32, wip] Remove input state from win32_app code --- src/win32_app.c | 30 ++++-------------------------- src/win32_app.h | 1 + 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/src/win32_app.c b/src/win32_app.c index c088c39..2baf620 100644 --- a/src/win32_app.c +++ b/src/win32_app.c @@ -216,7 +216,6 @@ static void process_mouse_event(mp_window_data* window, mp_key_action action, mp } } - mp_update_key_state(&__mpApp.inputState.mouse.buttons[button], action); //TODO click/double click mp_event event = {0}; @@ -385,15 +384,12 @@ LRESULT WinProc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam) event.move.x = LOWORD(lParam) / scaling; event.move.y = HIWORD(lParam) / scaling; - if(__mpApp.inputState.mouse.posValid) + if(__mpApp.win32.mouseTracked || __mpApp.win32.mouseCaptureMask) { - 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; + 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) { @@ -413,8 +409,6 @@ LRESULT WinProc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam) 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; @@ -422,11 +416,6 @@ LRESULT WinProc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam) { __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; @@ -453,9 +442,6 @@ LRESULT WinProc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam) 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: @@ -468,10 +454,6 @@ LRESULT WinProc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam) 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: @@ -485,8 +467,6 @@ LRESULT WinProc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam) 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; @@ -525,8 +505,6 @@ void mp_request_quit() void mp_pump_events(f64 timeout) { - __mpApp.inputState.frameCounter++; - MSG message; while(PeekMessage(&message, 0, 0, 0, PM_REMOVE)) { diff --git a/src/win32_app.h b/src/win32_app.h index 102ba07..6ea0110 100644 --- a/src/win32_app.h +++ b/src/win32_app.h @@ -35,6 +35,7 @@ typedef struct win32_app_data int mouseCaptureMask; bool mouseTracked; + vec2 lastMousePos; } win32_app_data; From 52538248d9db32022711e2917ea8c2726482b67a Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Fri, 12 May 2023 16:46:13 +0200 Subject: [PATCH 02/14] [win32, wip] compile and run simple window example --- examples/simpleWindow/build.bat | 4 +- examples/simpleWindow/main.c | 224 ++-- ext/angle_install_notes.md | 161 +-- src/gl_canvas.c | 1003 +++++++------- src/glsl_shaders.h | 469 ------- src/graphics_surface.c | 814 ++++++------ src/platform/platform_math.h | 53 +- src/win32_app.c | 2164 +++++++++++++++---------------- 8 files changed, 2218 insertions(+), 2674 deletions(-) delete mode 100644 src/glsl_shaders.h diff --git a/examples/simpleWindow/build.bat b/examples/simpleWindow/build.bat index 3e918f6..971b6e5 100644 --- a/examples/simpleWindow/build.bat +++ b/examples/simpleWindow/build.bat @@ -1,2 +1,2 @@ -set INCLUDES=/I ..\..\src /I ..\..\src\util /I ..\..\src\platform /I ../../ext -cl /we4013 /Zi /Zc:preprocessor /std:c11 %INCLUDES% main.c /link /LIBPATH:../../bin milepost.lib user32.lib opengl32.lib gdi32.lib /out:test.exe +set INCLUDES=/I ..\..\src /I ..\..\src\util /I ..\..\src\platform /I ../../ext +cl /we4013 /Zi /Zc:preprocessor /std:c11 %INCLUDES% main.c /link /LIBPATH:../../bin milepost.dll.lib user32.lib opengl32.lib gdi32.lib /out:../../bin/example_window.exe diff --git a/examples/simpleWindow/main.c b/examples/simpleWindow/main.c index a80d837..c659847 100644 --- a/examples/simpleWindow/main.c +++ b/examples/simpleWindow/main.c @@ -1,113 +1,111 @@ -/************************************************************//** -* -* @file: main.cpp -* @author: Martin Fouilleul -* @date: 30/07/2022 -* @revision: -* -*****************************************************************/ -#include -#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/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..908c08a 100644 --- a/src/gl_canvas.c +++ b/src/gl_canvas.c @@ -1,498 +1,505 @@ -/************************************************************//** -* -* @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_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_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); + + +/* + mg_gl_canvas_backend* backend = 0; + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + + int err = 0; + + if(surfaceData && surfaceData->api == MG_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); +*/ + + 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/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/win32_app.c b/src/win32_app.c index 2baf620..776387a 100644 --- a/src/win32_app.c +++ b/src/win32_app.c @@ -1,1082 +1,1082 @@ -/************************************************************//** -* -* @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.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->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); + } +} + +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); +} +////////////////////////////////////////////////////////////////////////////////////////////////// From b6db5107a39e05d0355c2b79d9579e7e39e53be0 Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Fri, 12 May 2023 16:50:14 +0200 Subject: [PATCH 03/14] [win32, wip] simple GL triangle example --- examples/triangleGL/main.c | 328 ++++++++++++++++++------------------- 1 file changed, 163 insertions(+), 165 deletions(-) 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); +} From 2bec7a633a2af3455d4d8ddd6ad75988d0445c94 Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Tue, 16 May 2023 18:06:22 +0200 Subject: [PATCH 04/14] [win32] reflected changes to canvas surface interface on GL backend. But implementation still uses the (slower) Loop-Blinn + triangle fan method. --- examples/canvas/main.c | 419 +++--- src/gl_canvas.c | 1648 +++++++++++++++++++++- src/graphics_surface.h | 312 +++-- src/mtl_renderer.m | 2950 ++++++++++++++++++++-------------------- 4 files changed, 3443 insertions(+), 1886 deletions(-) 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/src/gl_canvas.c b/src/gl_canvas.c index 908c08a..7cba07c 100644 --- a/src/gl_canvas.c +++ b/src/gl_canvas.c @@ -14,7 +14,22 @@ typedef struct mg_gl_canvas_backend { mg_canvas_backend interface; - mg_surface surface; + 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; @@ -61,6 +76,1435 @@ typedef struct debug_shape 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)) @@ -101,7 +1545,7 @@ enum { void mg_gl_canvas_update_vertex_layout(mg_gl_canvas_backend* backend) { - backend->interface.vertexLayout = (mg_vertex_layout){ + 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, @@ -134,14 +1578,16 @@ void mg_gl_send_buffers(mg_gl_canvas_backend* backend, int shapeCount, int verte glBufferData(GL_SHADER_STORAGE_BUFFER, LAYOUT_INT_SIZE*indexCount, backend->indexMapping, GL_STREAM_DRAW); } -void mg_gl_canvas_begin(mg_canvas_backend* interface) +void mg_gl_canvas_begin(mg_gl_canvas_backend* backend) { - mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; 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_canvas_backend* interface) +void mg_gl_canvas_end(mg_gl_canvas_backend* backend) { //NOTE: nothing to do here... } @@ -154,9 +1600,9 @@ void mg_gl_canvas_clear(mg_canvas_backend* interface, mg_color clearColor) 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) +void mg_gl_canvas_draw_batch(mg_gl_canvas_backend* backend, mg_image_data* imageInterface, u32 shapeCount, u32 vertexCount, u32 indexCount) { - mg_gl_canvas_backend* backend = (mg_gl_canvas_backend*)interface; + 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; @@ -165,8 +1611,10 @@ void mg_gl_canvas_draw_batch(mg_canvas_backend* interface, mg_image_data* imageI //*/ 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); + 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; @@ -245,29 +1693,100 @@ void mg_gl_canvas_draw_batch(mg_canvas_backend* interface, mg_image_data* imageI 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_destroy(mg_canvas_backend* interface) + +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; - glDeleteTextures(1, &backend->outTexture); + u32 nextIndex = 0; - glDeleteBuffers(1, &backend->dummyVertexBuffer); - glDeleteBuffers(1, &backend->vertexBuffer); - glDeleteBuffers(1, &backend->shapeBuffer); - glDeleteBuffers(1, &backend->indexBuffer); - glDeleteBuffers(1, &backend->tileCounterBuffer); - glDeleteBuffers(1, &backend->tileArrayBuffer); + mg_reset_shape_index(backend); - glDeleteVertexArrays(1, &backend->vao); + 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); - free(backend->shapeMapping); - free(backend->vertexMapping); - free(backend->indexMapping); - free(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; @@ -305,6 +1824,31 @@ void mg_gl_canvas_image_upload_region(mg_canvas_backend* interface, 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; @@ -408,34 +1952,33 @@ int mg_gl_canvas_compile_render_program_named(const char* progName, #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_surface_data* gl_canvas_surface_create_for_window(mp_window window) +mg_canvas_backend* gl_canvas_backend_create(mg_wgl_surface* surface) { - mg_wgl_surface* surface = (mg_wgl_surface*)mg_wgl_surface_create_for_window(window); - - -/* - mg_gl_canvas_backend* backend = 0; - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - - int err = 0; - - if(surfaceData && surfaceData->api == MG_GL) + mg_gl_canvas_backend* backend = malloc_type(mg_gl_canvas_backend); + if(backend) { - backend = malloc_type(mg_gl_canvas_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.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.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; - mg_surface_prepare(surface); + /* + 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); @@ -455,8 +1998,8 @@ mg_surface_data* gl_canvas_surface_create_for_window(mp_window window) 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); + 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); @@ -465,6 +2008,7 @@ mg_surface_data* gl_canvas_surface_create_for_window(mp_window window) 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); @@ -497,9 +2041,25 @@ mg_surface_data* gl_canvas_surface_create_for_window(mp_window window) 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/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); +} From 9f7cfd985cf7aa09a857cab97579252a0bc8bacb Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Wed, 17 May 2023 15:14:14 +0200 Subject: [PATCH 05/14] [win32] re-introduced perf_text, tiger, and ui examples --- examples/perf_text/main.c | 653 ++++++++++---------- examples/tiger/build.bat | 8 +- examples/tiger/main.c | 495 +++++++-------- examples/ui/main.c | 1215 +++++++++++++++++++------------------ 4 files changed, 1189 insertions(+), 1182 deletions(-) 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 - -#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/ui/main.c b/examples/ui/main.c index 161af0a..9b6b457 100644 --- a/examples/ui/main.c +++ b/examples/ui/main.c @@ -1,605 +1,610 @@ -/************************************************************//** -* -* @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") + { + 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 = {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); +} From 2a01cba026a66f7c9db1d3e6ab93b4c6fe3cffaf Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Wed, 17 May 2023 15:48:25 +0200 Subject: [PATCH 06/14] [ui] Fix ui scrolling --- src/ui.c | 5632 ++++++++++++++++++++++++----------------------- src/win32_app.c | 2 +- 2 files changed, 2819 insertions(+), 2815 deletions(-) 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 776387a..107e430 100644 --- a/src/win32_app.c +++ b/src/win32_app.c @@ -234,7 +234,7 @@ static void process_wheel_event(mp_window_data* window, f32 x, f32 y) event.window = mp_window_handle_from_ptr(window); event.type = MP_EVENT_MOUSE_WHEEL; event.move.deltaX = x/30.0f; - event.move.deltaY = y/30.0f; + event.move.deltaY = -y/30.0f; event.move.mods = mp_get_mod_keys(); mp_queue_event(&event); From a6a37c874ecdbf1197f34878484a5e1b82186390 Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Wed, 17 May 2023 16:50:07 +0200 Subject: [PATCH 07/14] [win32] adding prototypes for open/save/alert dialogs in win32_app.c --- src/win32_app.c | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/win32_app.c b/src/win32_app.c index 107e430..826c12a 100644 --- a/src/win32_app.c +++ b/src/win32_app.c @@ -1080,3 +1080,28 @@ str8 mp_app_get_resource_path(mem_arena* arena, const char* name) 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 +MP_API str8 mp_open_dialog(mem_arena* arena, + const char* title, + const char* defaultPath, + int filterCount, + const char** filters, + bool directory); + +MP_API str8 mp_save_dialog(mem_arena* arena, + const char* title, + const char* defaultPath, + int filterCount, + const char** filters); + +//TODO: MessageBox() doesn't offer custom buttons? +MP_API int mp_alert_popup(const char* title, + const char* message, + u32 count, + const char** options); From 259d381e56e70d00ee450186e8a390df62eed287 Mon Sep 17 00:00:00 2001 From: Martin Fouilleul Date: Mon, 22 May 2023 10:10:08 +0200 Subject: [PATCH 08/14] [perf_text example] track CMUSerif-Roman.ttf --- resources/CMUSerif-Roman.ttf | Bin 0 -> 438120 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/CMUSerif-Roman.ttf diff --git a/resources/CMUSerif-Roman.ttf b/resources/CMUSerif-Roman.ttf new file mode 100644 index 0000000000000000000000000000000000000000..634922a15a49a4fd44f5448b4687ce9e9ca45633 GIT binary patch literal 438120 zcmd?Sdwf(y^7mhT&Y3fl3rQxK+(;pAoyX&E&5nU7XJF7}!0L{!O;wozlf zuY7ff*mKK8dijTp9Y3J)+nRQ_N>d!dgiOA9~2qCQCu$!pE+ad^x(mr-%)lL)XgNr_1E|= z@T=juVCMOA=DqOFGbGhEk-TNIW>1@%zH@hhNOcL%%g>)Wuidj(9-w?8m4X*cJ%7gB zOT8|!JxL;Far^9x=QPGW+AMZlgUHEqMfY9qy|*>4u+QYgrXM8E+^utVG+YP&F#Y+C z&MRG?T7!A?h!Jj~|23Zo*|NzMqI|0B(@4H<FU$ki_cRYD5HSG9G!Cn*F5_S`Te7W28)_$t1N7OOvSS7i}V#zg= zoU&YXj$|83N~xP`TuA;>DWpxV({`2QJ9G!_x>5f__)rE?EK`!986eJ>CJpF36MY(? zsXWg`hjBbl*E-TRTdkKoXdX0M^^{!lvw04pd#>sk+GZq@Pa69Ko&0clHu-t#M#^rc z-a<*yW#pxxOENTGryZXAVHDL0KDs+@mVitJABql7WrdHdyZlo^0eLm z1I!!A|B!M$4#z-`!?7?9W3R|K=rK4Bv9c81v-T!W38)-eYt^NbeistM2to1a#jE|<#=LmJ|q;=Hx8X{dCTc?J8R_BC5DmCh*1u<4CLU1y3`chV(a{f|gTP`=R|#UvsrT zGS8*Xa>qyHsc)s1vnE#3-)XGN#i1{;Nxb8$zD4Jd1k}Ufu`!MyBZ+x_y;MP6R#1G# zH=!>{JGRv8gSJ`bIM&HDDZ+n{SMnm&&A=4|HF=bQX@3%-w!UHh)%^oXw0D2=SQpCa^!sQf7X)jU6b zerlf+p6_~I>-jv8xv@hMqPAk(a~ZE(qa^GDow36&9Mvo0Ponbv8>PN8-__NjBk%~Q z>q7?|KhU2s1DcYcLpD;pl zj11NpaBMLmv`dY^$Ihk?BSJfoRnNF5JAC!}U|l;pYX72j%|E)Q>-{M_ABmliAH@JY zUijn!@=A?X`l$6ggRx3t9ljm0QKY^(75je3c-;%V-;tYaY(UOI>ghe^blRRHG4Q$o zo#Hg`>q*!nNtT87$VN$sLd8q6Q`bYB_k<+U=4$E<0plGyf|zcEOhs=UThx=L&tSh? z45rB6WinL9AAbhtInN_P|0bjMEPi58fQ8Dh+8P1W_+X4pmDSr3$Dek z`=ISg>vaoyzlr`HvE?T{&hS4Gx#*TF@gkj^5jy_?t`<466*?L^6gpcZL=>xO=VO3a zzwEff8;ti!tPWUwNlC+QF|yGG|AXc|!|p7m1c>|N{; z1JuG}m~MJt-1bGyim~?bM;Z69+Lj^L2fwj_EQ;H_om8xf12m7Uf z@vMcG0R8M`{${cE!{wS%$7t00!AtLt+UL2+-zMq09(kqg$9|{HdMTl9srnPo@MJ&I zv2d9~@0VQqxY5a{jYG-Pyo~{lPu2dQ4*hhz>6|H`wR&wEol=DUWf8r?cERsqKgU*1 zKehjk#JoDTvU*Dv$R+O6XAW&Y?Jv_JRG(q;9Y3mV9UD5VZEZb;zSE>i*KrVTm!)kT zd2VppbB;l#KELEUJo_KDv(wI+0WV-)hV0Fb99_TO$%}RRuH$HJOYLhjv5|siGv6Z@w)Iwf&-iL_xpWopouOyufznbg;Nd8XlK9Y^*O z+Ga2hGS%)({UCYhEcN%SenHQKR<=9NTDq{pCY(HKZjpJQbv~X)rsFI0 z96BL}ipE%a?$hUNgE*PBn}yB}jmnAI1R2iW`78d>Ge)HEC)hyGyEHvdPoz;k&Ur`A zAH7zXuhesF6Q$I1`sb|Kd}57nnERKp$Jb%sd!&$O9UH{xJ)6C@)VP)Y=-5L08R8t~ zh2BH7kFNs#;2Vx5^*si63638)mg@PX$4;LexnuYrY1DpE%6*7FvqWQDfzlm>B53GfiByszVx z2z?j2PJ>vc6~r4?6Ej>L+6Rt+L+pFXfsTj%1I*`;``oYPqN|R*{wp}&$6EpP44~r# zWayab{{r(aVr8_Oedya3MH;LUtrCqarc=PI-=WQ*M}qc0;=K6Bgg0C z_p!GLI2*7Bq%nWBtP|&hj>E&TxHC`m{_0>2GWGd^bwKRaMT7H(`JZfL&ZDkAZ%l%6 z-Y{27xjviFzj)+{eq(Z!a!O+^2<1*5d_^{aY;epUy6U^#Sl#Yu+x_w!^`qz438cq! zR{WWIo}8G{0k{-H|DYjbMH+XZIA9i$M?g)Nlsel>$8DA z<8U93J(7txb&RHCE<@kj5<6_`Q_=wL`Co-1FBH2kS8^XYYY;=mjuPKS!X?R`u#G-YeWC zY4mffhI97jZULSbf=1mhN9X&oYnog|+v(sub9d+m_>N^8y-v5h`w=(tJSFq!y*dtT=8vpQA| z{p`e%`ivQ=r};Sb!|O-4DR22`h>d!;XEw#DUAKni?T*|h_bs62Pz~C^ROIG3 zGE<#!!9wp?R8cX@w>)^Q$rn|QCnx#KFJjb6K-fLnpK!8F}B-AB?FfEN0m z=^*^9^BQSqGxl1a+2Cg8UZ!<&Xt$NdJj;_ZUU8PWazThSH-kB&F-oc(v~#yuLB7T{ z15%@5yvBX}4U}scE%Zx6^Sc6DBg1@$HF<#d zbyqlfI$tB(`1)vGwUbwYys(b>@Y3&sG|!x9JM#{B0q*egx&IH#p}v!M1@|4%KIt=} zmQR_Zdj;=ZE5xT(Nq4gyU(-%mJ9VP^Xj!Dq?#O#UDoowK15%)EXbu#gHIloJKZ%<< zwIGK&=?-Q{s;Q7qJEyJIUGszox^y$*Nh|JjTTZaSYvf-++xV~!&@x9SZS!CHpF-a? zav0Azhi_Ehwb%}5yGHV}k1N-9jgI-}jES~OIeL$RS7a>cPqeMpMe7}gmapv*g|-W} z#@FaQU>Z34=)1hgz8&4a9ZH<7V{a$^j@$>)=2!16SwF|VtNh7{vGIl4A4UN`eP^T* zrvabi-di&N_4~?zbA}=2WsRIpoqMhOq!q+l9};7JNbH6W1&3TQqBN}ADZicaw<&*{ z^0z5}n;5N=GL4%kzlrkKDSw^v*C~IU@rCf7Tzc$%j_yCY>$?Jd-ge$2=(@V^Jf}s@;>X`vMcN=Q8y~69Q^(yaAu~vs zzIW=uKBv>^PF;K%G>e$RMOiv>gZMeU&**c0c+Y8c>SoYR+t_(utlLmGjd(i;>L+&E zsq;*mSgi|XTgaOo`jN6KeP>14pMdsTm(mygd4aP*68*smGmjduoAxEzXE^(_Q9ABpIJ)`U9M5@=3mLK&o5sGZXkst9fj((bdbUmF&!?J(z zwR(*!bk}r|3paI4-y#_;7G#@bTc2!PUVRa?RZITz_sbH!rt1w<7nh++}%oUUFV~ zUe~<0^7Hb`3i1n$!qJ8A7IqYMFKRA2x9HiT7m9Pr+bc3Ev!2NQ_WN&t*q69((!T4z z8~NRH`)BUI=!c{q0ta3@@XmoP2euygRfzZK%gN+9-KlsSO0|!GtTb*a| z2`l{EY6-U5n6t?*{tUn0U*@m$kMqy;&&O6P0usmw1Ou&swur4J2Nncg47?TiB=A|_ zyTE~<1e35;hGVPBU?aBb7i>AfR!=*&N{iU42wN@5z2`Vvl@{ckV5?`2+N$dlS&pq_ zpLgHneK&qLY5$b{=l&o+Bs;cxFJh}7BetqMI2Bv%IC$u13EL|4W$2Hg4WZ{lt3ywR zo(eq~S`~UC^iXJd=%&!T(45ePp;@8xLg$9g37s999y&8LH8dtPDl{n65*irlAL??jPDrTF{)!k$FPop9sN3bcl7G0>!|Fg=qT?f z>nQ0c?kMUg>?r8S>&Wd0cKADTIccgWsb|iNsb;Nd<9Y%-R)w%2YUEl3` zd)E`Y9^18Y*CV?g-nD$!@Lj!j)$FR?Rkf?fuF{>GcD}iD;?5B}Pue-;%jIA8|FZW# zTD~~zi;Nu)?YMqNza8CoSfBs#^FMq({;%e?NuQiHc~aU9Y1gN6Lsb?q`Z`} zE~Q&anRl`GTJKffE4-I`FZQ0|J=r_KTjeeGO447FmM7ic<^EWG!}0&mzncH|Zrium zi|tz_$-d3L-CknfVc%)rWiPewwtr*aV=uGswePdaa?MLiK?UnXp z_T%;w_A2{H`ziZrd$s+H{jB|*y~cjte!>2&z1IGn{i6MQd!7A~{j&Xvz21J+e$D=a zy}^Fn{-gbdz0rQte#?H_-ekXH|H*#W-faKbe$Rg2-eP}Xe`tSXZ?!+xZ&`7rm60AT zJE3Pmp05JhH?LsldjonV^eymDa0B=eaKblucdDzIiKJIUyQ6*C2NC7<`4CMSPB zeIv%3P%LTIlHLyG{fpU$^xvU_0q+n9q$F9MqXX6P1xy)5iu9R~Pii$1%OfzIpCXJY$>VZk#8dumwl z%yEZd!80co#F3Aka=L_JwL&vMCi&Pd2Rmz6!=OvRU8ILY?+L?dgKh+jgT;IG9ByTD z-XT3!2!(*wXFL?1{w&hxKq>GSlRh6>2D+2J6j}+8W8p>o#+Q2G!!ne;s9 zDPg!4Ku-(9bq)0NFkII{Cxa`ge;riUTTA*8=!*bbxITrx1wJC(0o6MFjr4x#XMj27 zVy*|i1LWD*K5zhZl1>sqO$m}`GX_EZ>?4i6f@xvc_=!^W1u zm0{S-nc(AL*h8UDhG8?V!KcA$_+$Uz3t`wJg%CdsdlZyD=B87JG0A0~XxQkI8w|r{ zUggqn4I903>3c4=v6*AJ6=B$nW$qGi7x@#R_kd-jCyC_QVc2Itlf$qX&pc#m*i)cg z!?349-wMM%QzRdm8aDGXA3NtWckSsS1>_avlRiuMXmJ=ebGDH6pkbc_eK!pITxdra z_IV;jjGu-*3)&opeLj?W8ukUyXTY^UNp z^rbS3^jt1ap--@u?Mp?z{XPu)GUyLs*q4j!6Oc%qE1+JGLi!dcb5KK)pscNZ*wVfY zitYAYPntQt??#dDMv^`PrTn|+*c09q*^f>4V-FAWc|U#HkG>vkhFJLlex7d7WDp?T zBy!-jFg!z{?}XtQ27M1~p$_xsz}7H4%$Wno*6_4J{~m@1yC3)w{6zj3C^~6)#z8}2 zc*ctyEC|Ch0a^)IC!Uj`jQv6E;nDPwFg)1g;0|zr{Q1yB0A3#E+0XRlXN&axyw7DG zXn0nLQZ0bB;(1uqr<_87cpl@XDwYPKod0y{=${g1kngi1Qo)^KrSz04sg@e4l{!Ax&>)S{OL|L_^pU>OPnxB_43L4+B7|L@<9+XGr z5m_mZ%VXRqKPgYkQ?go~k>})DStHNOZ{>HgR$i2K@_Sh?ugI(Nnq4aw$#j_^=h*c! zPac+bIS;>Zi7?M(5#N#!VMXwFwlM4UpUKLVSu)>DmR0hyT*K|+1@=_Myuc@%FIVD4 z&y;KBdbvmWRH|KOce8uiRd#o~LSCZf>#CPMz#d^Y*?m0AJrCG*a+%%3uExhMl*MwL z+$uNA?XpDfklW;L`3)_W%5uIra=(0U+`#96FEo}J_Zs)f<;DZX{RS&0@;4EBEY>nL zlE8N2i*0DVk-cpV*va0(JmqXFb~acDHUskA@qo4A=Go%|OGRRuz%q`*(AX^^aphn= zXW}-n80;14g8%Na2<#?in8TNL;E@QAB
=leG;V3SC4GuR`NvIHQ{M?K#mBK}Dt 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

ZRi6l@RIZRFlxl^Xz=>GH1 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�ZC7J|$M?F`-wv0yVA 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 z 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�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 literal 0 HcmV?d00001 From f1d8dd1ca9a54beff7996d4e1e729b6d84104aa7 Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Tue, 23 May 2023 12:55:57 +0200 Subject: [PATCH 09/14] [win32] crappy message box with custom button text. Not DPI aware, doesn't show message box image (eg exclamation/information mark), and has very crude layout... --- examples/ui/main.c | 7 ++ src/win32_app.c | 257 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 260 insertions(+), 4 deletions(-) diff --git a/examples/ui/main.c b/examples/ui/main.c index 9b6b457..b615173 100644 --- a/examples/ui/main.c +++ b/examples/ui/main.c @@ -409,6 +409,13 @@ int main() ui_button("Button 1"); ui_button("Button 2"); ui_button("Button 3"); + + if(ui_button("Test Dialog").clicked) + { + char* options[] = {"OK", "Cancel"}; + int res = mp_alert_popup("test dialog", "dialog message", 2, options); + printf("selected options %i\n", res); + } } ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 0.5}, diff --git a/src/win32_app.c b/src/win32_app.c index 826c12a..667ddb0 100644 --- a/src/win32_app.c +++ b/src/win32_app.c @@ -155,7 +155,7 @@ void mp_init() __mpApp.win32.savedConsoleCodePage = GetConsoleOutputCP(); SetConsoleOutputCP(CP_UTF8); - SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); } } @@ -1101,7 +1101,256 @@ MP_API str8 mp_save_dialog(mem_arena* arena, const char** filters); //TODO: MessageBox() doesn't offer custom buttons? + +#include + +BOOL CALLBACK mp_dialog_proc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) +{ + switch(message) + { + case WM_INITDIALOG: + { + // Get the owner window and dialog box rectangles. + HWND hWndOwner = GetParent(hWnd); + if(hWndOwner == NULL) + { + hWndOwner = GetDesktopWindow(); + } + + //TODO Get all child items, get their ideal sizes, set size, and resize dialog accordingly + + vec2 buttonsTotalSize = {0, 20}; + vec2 messageSize = {400, 200}; + + for(HWND hWndChild = GetWindow(hWnd, GW_CHILD); + hWndChild != NULL; + hWndChild = GetWindow(hWndChild, GW_HWNDNEXT)) + { + int id = GetDlgCtrlID(hWndChild); + if(id == 0xffff) + { + //message + //TODO compute size with an "ideal" aspect ratio + SetWindowPos(hWndChild, + HWND_TOP, + 0, + 0, + messageSize.x, + messageSize.y, + SWP_NOMOVE | SWP_NOZORDER); + } + else + { + //button + SIZE size = {0}; + Button_GetIdealSize(hWndChild, &size); + + size.cx = maximum((int)size.cx, 100); + size.cy = maximum((int)size.cy, 40); + + SetWindowPos(hWndChild, + HWND_TOP, + 0, + 0, + size.cx, + size.cy, + SWP_NOMOVE | SWP_NOZORDER); + + buttonsTotalSize.x += size.cx + 10; + buttonsTotalSize.y = maximum(buttonsTotalSize.y, size.cy); + } + } + buttonsTotalSize.x -= 10; + + vec2 dialogSize = {maximum(messageSize.x, buttonsTotalSize.x) + 20, + 10 + messageSize.y + 10 + buttonsTotalSize.y + 10}; + + RECT dialogRect = {0, 0, dialogSize.x, dialogSize.y}; + + AdjustWindowRect(&dialogRect, WS_POPUP | WS_BORDER | WS_SYSMENU | DS_MODALFRAME | WS_CAPTION, FALSE); + + SetWindowPos(hWnd, + HWND_TOP, + 0, 0, + dialogRect.right - dialogRect.left, + dialogRect.bottom - dialogRect.top, + SWP_NOMOVE | SWP_NOZORDER); + + vec2 buttonPos = {dialogSize.x - buttonsTotalSize.x - 10, + dialogSize.y - buttonsTotalSize.y - 10}; + + for(HWND hWndChild = GetWindow(hWnd, GW_CHILD); + hWndChild != NULL; + hWndChild = GetWindow(hWndChild, GW_HWNDNEXT)) + { + int id = GetDlgCtrlID(hWndChild); + if(id == 0xffff) + { + //message + SetWindowPos(hWndChild, + HWND_TOP, + 0.5*(dialogSize.x - messageSize.x), + 10, + 0, 0, + SWP_NOSIZE | SWP_NOZORDER); + } + else + { + //button + SIZE size; + Button_GetIdealSize(hWndChild, &size); + size.cx = maximum((int)size.cx, 100); + + SetWindowPos(hWndChild, + HWND_TOP, + buttonPos.x, + buttonPos.y, + 0, 0, + SWP_NOSIZE | SWP_NOZORDER); + + buttonPos.x += size.cx + 10; + } + } + + RECT rc, rcDlg, rcOwner; + + GetWindowRect(hWndOwner, &rcOwner); + GetWindowRect(hWnd, &rcDlg); + CopyRect(&rc, &rcOwner); + + // Offset the owner and dialog box rectangles so that right and bottom + // values represent the width and height, and then offset the owner again + // to discard space taken up by the dialog box. + + OffsetRect(&rcDlg, -rcDlg.left, -rcDlg.top); + OffsetRect(&rc, -rc.left, -rc.top); + OffsetRect(&rc, -rcDlg.right, -rcDlg.bottom); + + // The new position is the sum of half the remaining space and the owner's + // original position. + + SetWindowPos(hWnd, + HWND_TOP, + rcOwner.left + (rc.right / 2), + rcOwner.top + (rc.bottom / 2), + 0, 0, // Ignores size arguments. + SWP_NOSIZE); + + return TRUE; + } + + case WM_COMMAND: + EndDialog(hWnd, wParam); + return TRUE; + } + return(FALSE); +} + MP_API int mp_alert_popup(const char* title, - const char* message, - u32 count, - const char** options); + const char* message, + u32 count, + const char** options) +{ + //NOTE compute size needed + int size = sizeof(DLGTEMPLATE); // template struct + size += 2*sizeof(WORD); // menu and box class + + int titleWideSize = 1 + MultiByteToWideChar(CP_UTF8, 0, title, -1, NULL, 0); + size += titleWideSize; + + size = AlignUpOnPow2(size, sizeof(DWORD)); + size += sizeof(DLGITEMTEMPLATE); + size += 2*sizeof(WORD); // menu and box class + size += 1 + MultiByteToWideChar(CP_UTF8, 0, message, -1, NULL, 0); // dialog message + size++; // no creation data + + for(int i=0; istyle = WS_POPUP | WS_BORDER | WS_SYSMENU | DS_MODALFRAME | WS_CAPTION; + template->cdit = count + 1; + template->x = 10; + template->y = 10; + template->cx = 100; + template->cy = 100; + + LPWORD lpw = (LPWORD)(template + 1); + *lpw = 0; + lpw++; + *lpw = 0; + lpw++; + + MultiByteToWideChar(CP_UTF8, 0, title, -1, (LPWSTR)lpw, titleWideSize); + lpw += titleWideSize; + + { + lpw = (LPWORD)AlignUpOnPow2((uintptr_t)lpw, sizeof(DWORD)); + + LPDLGITEMTEMPLATE item = (LPDLGITEMTEMPLATE)lpw; + item->x = 10; + item->y = 10; + item->cx = 80; + item->cy = 40; + item->id = 0xffff; + item->style = WS_CHILD | WS_VISIBLE; + + lpw = (LPWORD)(item+1); + *lpw = 0xffff; + lpw++; + *lpw = 0x0082; + lpw++; + + int wideSize = 1 + MultiByteToWideChar(CP_UTF8, 0, message, -1, NULL, 0); + MultiByteToWideChar(CP_UTF8, 0, message, -1, (LPWSTR)lpw, wideSize); + lpw += wideSize; + + *lpw = 0; + lpw++; + } + + for(int i=0; ix = 10; + item->y = 70; + item->cx = 80; + item->cy = 20; + item->id = i+1; + item->style = WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON; + + lpw = (LPWORD)(item+1); + *lpw = 0xffff; + lpw++; + *lpw = 0x0080; + lpw++; + + int wideSize = 1 + MultiByteToWideChar(CP_UTF8, 0, options[i], -1, NULL, 0); + MultiByteToWideChar(CP_UTF8, 0, options[i], -1, (LPWSTR)lpw, wideSize); + lpw += wideSize; + + *lpw = 0; + lpw++; + } + + LRESULT ret = DialogBoxIndirect(NULL, template, NULL, (DLGPROC)mp_dialog_proc); + + mem_arena_clear_to(scratch, marker); + + return((int)ret-1); +} From a54c8b4f4b2f1fa8f501a2f06b4011bf63c1f6c8 Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Tue, 23 May 2023 14:50:31 +0200 Subject: [PATCH 10/14] [win32] alert popup using TaskDialogIndirect(): automatically handles icons, dpi, and text/buttons layout (but requires Vista or higher) --- build.bat | 22 ++-- examples/ui/main.c | 11 +- src/win32_app.c | 281 +++++++---------------------------------- src/win32_manifest.xml | 22 ++++ 4 files changed, 87 insertions(+), 249 deletions(-) create mode 100644 src/win32_manifest.xml diff --git a/build.bat b/build.bat index e11b7d3..335b7c3 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 /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/ui/main.c b/examples/ui/main.c index b615173..1d820be 100644 --- a/examples/ui/main.c +++ b/examples/ui/main.c @@ -412,9 +412,16 @@ int main() if(ui_button("Test Dialog").clicked) { - char* options[] = {"OK", "Cancel"}; + char* options[] = {"Accept", "Reject"}; int res = mp_alert_popup("test dialog", "dialog message", 2, options); - printf("selected options %i\n", res); + if(res >= 0) + { + printf("selected options %i: %s\n", res, options[res]); + } + else + { + printf("no options selected\n"); + } } } diff --git a/src/win32_app.c b/src/win32_app.c index 667ddb0..1656280 100644 --- a/src/win32_app.c +++ b/src/win32_app.c @@ -1100,257 +1100,66 @@ MP_API str8 mp_save_dialog(mem_arena* arena, int filterCount, const char** filters); -//TODO: MessageBox() doesn't offer custom buttons? - #include -BOOL CALLBACK mp_dialog_proc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) -{ - switch(message) - { - case WM_INITDIALOG: - { - // Get the owner window and dialog box rectangles. - HWND hWndOwner = GetParent(hWnd); - if(hWndOwner == NULL) - { - hWndOwner = GetDesktopWindow(); - } - - //TODO Get all child items, get their ideal sizes, set size, and resize dialog accordingly - - vec2 buttonsTotalSize = {0, 20}; - vec2 messageSize = {400, 200}; - - for(HWND hWndChild = GetWindow(hWnd, GW_CHILD); - hWndChild != NULL; - hWndChild = GetWindow(hWndChild, GW_HWNDNEXT)) - { - int id = GetDlgCtrlID(hWndChild); - if(id == 0xffff) - { - //message - //TODO compute size with an "ideal" aspect ratio - SetWindowPos(hWndChild, - HWND_TOP, - 0, - 0, - messageSize.x, - messageSize.y, - SWP_NOMOVE | SWP_NOZORDER); - } - else - { - //button - SIZE size = {0}; - Button_GetIdealSize(hWndChild, &size); - - size.cx = maximum((int)size.cx, 100); - size.cy = maximum((int)size.cy, 40); - - SetWindowPos(hWndChild, - HWND_TOP, - 0, - 0, - size.cx, - size.cy, - SWP_NOMOVE | SWP_NOZORDER); - - buttonsTotalSize.x += size.cx + 10; - buttonsTotalSize.y = maximum(buttonsTotalSize.y, size.cy); - } - } - buttonsTotalSize.x -= 10; - - vec2 dialogSize = {maximum(messageSize.x, buttonsTotalSize.x) + 20, - 10 + messageSize.y + 10 + buttonsTotalSize.y + 10}; - - RECT dialogRect = {0, 0, dialogSize.x, dialogSize.y}; - - AdjustWindowRect(&dialogRect, WS_POPUP | WS_BORDER | WS_SYSMENU | DS_MODALFRAME | WS_CAPTION, FALSE); - - SetWindowPos(hWnd, - HWND_TOP, - 0, 0, - dialogRect.right - dialogRect.left, - dialogRect.bottom - dialogRect.top, - SWP_NOMOVE | SWP_NOZORDER); - - vec2 buttonPos = {dialogSize.x - buttonsTotalSize.x - 10, - dialogSize.y - buttonsTotalSize.y - 10}; - - for(HWND hWndChild = GetWindow(hWnd, GW_CHILD); - hWndChild != NULL; - hWndChild = GetWindow(hWndChild, GW_HWNDNEXT)) - { - int id = GetDlgCtrlID(hWndChild); - if(id == 0xffff) - { - //message - SetWindowPos(hWndChild, - HWND_TOP, - 0.5*(dialogSize.x - messageSize.x), - 10, - 0, 0, - SWP_NOSIZE | SWP_NOZORDER); - } - else - { - //button - SIZE size; - Button_GetIdealSize(hWndChild, &size); - size.cx = maximum((int)size.cx, 100); - - SetWindowPos(hWndChild, - HWND_TOP, - buttonPos.x, - buttonPos.y, - 0, 0, - SWP_NOSIZE | SWP_NOZORDER); - - buttonPos.x += size.cx + 10; - } - } - - RECT rc, rcDlg, rcOwner; - - GetWindowRect(hWndOwner, &rcOwner); - GetWindowRect(hWnd, &rcDlg); - CopyRect(&rc, &rcOwner); - - // Offset the owner and dialog box rectangles so that right and bottom - // values represent the width and height, and then offset the owner again - // to discard space taken up by the dialog box. - - OffsetRect(&rcDlg, -rcDlg.left, -rcDlg.top); - OffsetRect(&rc, -rc.left, -rc.top); - OffsetRect(&rc, -rcDlg.right, -rcDlg.bottom); - - // The new position is the sum of half the remaining space and the owner's - // original position. - - SetWindowPos(hWnd, - HWND_TOP, - rcOwner.left + (rc.right / 2), - rcOwner.top + (rc.bottom / 2), - 0, 0, // Ignores size arguments. - SWP_NOSIZE); - - return TRUE; - } - - case WM_COMMAND: - EndDialog(hWnd, wParam); - return TRUE; - } - return(FALSE); -} - MP_API int mp_alert_popup(const char* title, const char* message, u32 count, const char** options) { - //NOTE compute size needed - int size = sizeof(DLGTEMPLATE); // template struct - size += 2*sizeof(WORD); // menu and box class - - int titleWideSize = 1 + MultiByteToWideChar(CP_UTF8, 0, title, -1, NULL, 0); - size += titleWideSize; - - size = AlignUpOnPow2(size, sizeof(DWORD)); - size += sizeof(DLGITEMTEMPLATE); - size += 2*sizeof(WORD); // menu and box class - size += 1 + MultiByteToWideChar(CP_UTF8, 0, message, -1, NULL, 0); // dialog message - size++; // no creation data - - for(int i=0; istyle = WS_POPUP | WS_BORDER | WS_SYSMENU | DS_MODALFRAME | WS_CAPTION; - template->cdit = count + 1; - template->x = 10; - template->y = 10; - template->cx = 100; - template->cy = 100; - - LPWORD lpw = (LPWORD)(template + 1); - *lpw = 0; - lpw++; - *lpw = 0; - lpw++; - - MultiByteToWideChar(CP_UTF8, 0, title, -1, (LPWSTR)lpw, titleWideSize); - lpw += titleWideSize; - - { - lpw = (LPWORD)AlignUpOnPow2((uintptr_t)lpw, sizeof(DWORD)); - - LPDLGITEMTEMPLATE item = (LPDLGITEMTEMPLATE)lpw; - item->x = 10; - item->y = 10; - item->cx = 80; - item->cy = 40; - item->id = 0xffff; - item->style = WS_CHILD | WS_VISIBLE; - - lpw = (LPWORD)(item+1); - *lpw = 0xffff; - lpw++; - *lpw = 0x0082; - lpw++; - - int wideSize = 1 + MultiByteToWideChar(CP_UTF8, 0, message, -1, NULL, 0); - MultiByteToWideChar(CP_UTF8, 0, message, -1, (LPWSTR)lpw, wideSize); - lpw += wideSize; - - *lpw = 0; - lpw++; - } + TASKDIALOG_BUTTON* buttons = mem_arena_alloc_array(scratch, TASKDIALOG_BUTTON, count); for(int i=0; ix = 10; - item->y = 70; - item->cx = 80; - item->cy = 20; - item->id = i+1; - item->style = WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON; - - lpw = (LPWORD)(item+1); - *lpw = 0xffff; - lpw++; - *lpw = 0x0080; - lpw++; - - int wideSize = 1 + MultiByteToWideChar(CP_UTF8, 0, options[i], -1, NULL, 0); - MultiByteToWideChar(CP_UTF8, 0, options[i], -1, (LPWSTR)lpw, wideSize); - lpw += wideSize; - - *lpw = 0; - lpw++; + buttons[i].nButtonID = i+1; + buttons[i].pszButtonText = textWide; } - LRESULT ret = DialogBoxIndirect(NULL, template, NULL, (DLGPROC)mp_dialog_proc); + int titleWideSize = MultiByteToWideChar(CP_UTF8, 0, title, -1, NULL, 0); + wchar_t* titleWide = mem_arena_alloc_array(scratch, wchar_t, titleWideSize); + MultiByteToWideChar(CP_UTF8, 0, title, -1, titleWide, titleWideSize); + + int messageWideSize = MultiByteToWideChar(CP_UTF8, 0, message, -1, NULL, 0); + wchar_t* messageWide = mem_arena_alloc_array(scratch, wchar_t, messageWideSize); + MultiByteToWideChar(CP_UTF8, 0, message, -1, messageWide, messageWideSize); + + TASKDIALOGCONFIG config = + { + .cbSize = sizeof(TASKDIALOGCONFIG), + .hwndParent = NULL, + .hInstance = NULL, + .dwFlags = 0, + .dwCommonButtons = 0, + .pszWindowTitle = titleWide, + .hMainIcon = 0, + .pszMainIcon = TD_WARNING_ICON, + .pszMainInstruction = messageWide, + .pszContent = NULL, + .cButtons = count, + .pButtons = buttons, + .nDefaultButton = 0, + }; + + int button = -1; + HRESULT hRes = TaskDialogIndirect(&config, &button, NULL, NULL); + if(hRes == S_OK) + { + if(button == IDCANCEL) + { + button = -1; + } + else + { + button--; + } + } mem_arena_clear_to(scratch, marker); - - return((int)ret-1); + return(button); } diff --git a/src/win32_manifest.xml b/src/win32_manifest.xml new file mode 100644 index 0000000..b28a978 --- /dev/null +++ b/src/win32_manifest.xml @@ -0,0 +1,22 @@ + + + +Orca Runtime + + + + + + From e24500e18d58198876d39bfff3bfbbe3708cd9d5 Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Tue, 23 May 2023 15:59:29 +0200 Subject: [PATCH 11/14] [win32] added basic mp_open_dialog() implementation using COM IFileOpenDialog --- build.bat | 2 +- examples/ui/main.c | 8 +++++-- src/win32_app.c | 52 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/build.bat b/build.bat index 335b7c3..9f9dfc1 100644 --- a/build.bat +++ b/build.bat @@ -6,6 +6,6 @@ set glsl_shaders=src\glsl_shaders\common.glsl src\glsl_shaders\blit_vertex.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 /LIBPATH:./bin libEGL.dll.lib libGLESv2.dll.lib /DELAYLOAD:libEGL.dll /DELAYLOAD:libGLESv2.dll +set LIBS=user32.lib opengl32.lib gdi32.lib shcore.lib delayimp.lib dwmapi.lib comctl32.lib ole32.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/ui/main.c b/examples/ui/main.c index 1d820be..ed521fe 100644 --- a/examples/ui/main.c +++ b/examples/ui/main.c @@ -407,8 +407,6 @@ int main() widget_view("Buttons") { ui_button("Button 1"); - ui_button("Button 2"); - ui_button("Button 3"); if(ui_button("Test Dialog").clicked) { @@ -423,6 +421,12 @@ int main() printf("no options selected\n"); } } + + if(ui_button("Open").clicked) + { + str8 file = mp_open_dialog(mem_scratch(), "Open File", 0, 0, 0, true); + printf("selected file %.*s\n", (int)file.len, file.ptr); + } } ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 0.5}, diff --git a/src/win32_app.c b/src/win32_app.c index 1656280..a0d9ff0 100644 --- a/src/win32_app.c +++ b/src/win32_app.c @@ -1087,12 +1087,62 @@ str8 mp_app_get_resource_path(mem_arena* arena, const char* name) //TODO: GetOpenFileName() doesn't seem to support selecting folders, and // requires filters which pair a "descriptive" name with an extension + +#define interface struct +#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); + 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); + } + + 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, From 49de9d0290bcdbf6faab45f973c9dc64278934aa Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Tue, 23 May 2023 16:04:49 +0200 Subject: [PATCH 12/14] [win32] added basic mp_save_dialogue() implementation using COM IFileSaveDialog --- examples/ui/main.c | 8 ++++++-- src/win32_app.c | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/examples/ui/main.c b/examples/ui/main.c index ed521fe..21701c1 100644 --- a/examples/ui/main.c +++ b/examples/ui/main.c @@ -406,8 +406,6 @@ int main() UI_STYLE_SIZE); widget_view("Buttons") { - ui_button("Button 1"); - if(ui_button("Test Dialog").clicked) { char* options[] = {"Accept", "Reject"}; @@ -427,6 +425,12 @@ int main() str8 file = mp_open_dialog(mem_scratch(), "Open File", 0, 0, 0, true); printf("selected file %.*s\n", (int)file.len, file.ptr); } + + if(ui_button("Save").clicked) + { + str8 file = mp_save_dialog(mem_scratch(), "Save File", 0, 0, 0, true); + printf("selected file %.*s\n", (int)file.len, file.ptr); + } } ui_style_next(&(ui_style){.size.width = {UI_SIZE_PARENT, 0.5}, diff --git a/src/win32_app.c b/src/win32_app.c index a0d9ff0..e30b4e0 100644 --- a/src/win32_app.c +++ b/src/win32_app.c @@ -1148,7 +1148,45 @@ MP_API str8 mp_save_dialog(mem_arena* arena, const char* title, const char* defaultPath, int filterCount, - const char** filters); + 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)) + { + 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 From ed45d88cfda81daa845b73991801459106054544 Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Tue, 23 May 2023 16:43:57 +0200 Subject: [PATCH 13/14] [win32] add file type filters to open/save dialogs --- examples/ui/main.c | 5 +++-- src/win32_app.c | 50 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/examples/ui/main.c b/examples/ui/main.c index 21701c1..8529a40 100644 --- a/examples/ui/main.c +++ b/examples/ui/main.c @@ -422,13 +422,14 @@ int main() if(ui_button("Open").clicked) { - str8 file = mp_open_dialog(mem_scratch(), "Open File", 0, 0, 0, true); + char* filters[] = {"md"}; + str8 file = mp_open_dialog(mem_scratch(), "Open File", 0, 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", 0, 0, 0, true); + str8 file = mp_save_dialog(mem_scratch(), "Save File", 0, 0, 0); printf("selected file %.*s\n", (int)file.len, file.ptr); } } diff --git a/src/win32_app.c b/src/win32_app.c index e30b4e0..f45764f 100644 --- a/src/win32_app.c +++ b/src/win32_app.c @@ -1090,8 +1090,10 @@ str8 mp_app_get_resource_path(mem_arena* arena, const char* name) #define interface struct #include +#include #undef interface + MP_API str8 mp_open_dialog(mem_arena* arena, const char* title, const char* defaultPath, @@ -1114,6 +1116,30 @@ MP_API str8 mp_open_dialog(mem_arena* arena, 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); + } + hr = dialog->lpVtbl->Show(dialog, NULL); if(SUCCEEDED(hr)) { @@ -1158,6 +1184,30 @@ MP_API str8 mp_save_dialog(mem_arena* arena, 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); + } + hr = dialog->lpVtbl->Show(dialog, NULL); if(SUCCEEDED(hr)) { From db0dadd12860fd72ad440e5823ccb1a5a2138904 Mon Sep 17 00:00:00 2001 From: martinfouilleul Date: Tue, 23 May 2023 17:24:38 +0200 Subject: [PATCH 14/14] [win32] allow specifying start folder in open/save dialogs --- build.bat | 2 +- examples/ui/main.c | 4 ++-- src/win32_app.c | 38 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/build.bat b/build.bat index 9f9dfc1..9d1c053 100644 --- a/build.bat +++ b/build.bat @@ -6,6 +6,6 @@ set glsl_shaders=src\glsl_shaders\common.glsl src\glsl_shaders\blit_vertex.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 /LIBPATH:./bin libEGL.dll.lib libGLESv2.dll.lib /DELAYLOAD:libEGL.dll /DELAYLOAD:libGLESv2.dll +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/ui/main.c b/examples/ui/main.c index 8529a40..fa63e87 100644 --- a/examples/ui/main.c +++ b/examples/ui/main.c @@ -423,13 +423,13 @@ int main() if(ui_button("Open").clicked) { char* filters[] = {"md"}; - str8 file = mp_open_dialog(mem_scratch(), "Open File", 0, 1, filters, false); + 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", 0, 0, 0); + str8 file = mp_save_dialog(mem_scratch(), "Save File", "C:\\Users", 0, 0); printf("selected file %.*s\n", (int)file.len, file.ptr); } } diff --git a/src/win32_app.c b/src/win32_app.c index f45764f..d9991cd 100644 --- a/src/win32_app.c +++ b/src/win32_app.c @@ -1128,7 +1128,7 @@ MP_API str8 mp_open_dialog(mem_arena* arena, str8 filter = str8_list_join(arena, list); int filterWideSize = 1 + MultiByteToWideChar(CP_UTF8, 0, filter.ptr, filter.len, NULL, 0); - filterSpecs[i].pszSpec = mem_arena_alloc(arena, filterWideSize); + filterSpecs[i].pszSpec = mem_arena_alloc_array(arena, wchar_t, filterWideSize); MultiByteToWideChar(CP_UTF8, 0, filter.ptr, filter.len, (LPWSTR)filterSpecs[i].pszSpec, filterWideSize); ((LPWSTR)(filterSpecs[i].pszSpec))[filterWideSize-1] = 0; @@ -1140,6 +1140,23 @@ MP_API str8 mp_open_dialog(mem_arena* arena, 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)) { @@ -1196,7 +1213,7 @@ MP_API str8 mp_save_dialog(mem_arena* arena, str8 filter = str8_list_join(arena, list); int filterWideSize = 1 + MultiByteToWideChar(CP_UTF8, 0, filter.ptr, filter.len, NULL, 0); - filterSpecs[i].pszSpec = mem_arena_alloc(arena, filterWideSize); + filterSpecs[i].pszSpec = mem_arena_alloc_array(arena, wchar_t, filterWideSize); MultiByteToWideChar(CP_UTF8, 0, filter.ptr, filter.len, (LPWSTR)filterSpecs[i].pszSpec, filterWideSize); ((LPWSTR)(filterSpecs[i].pszSpec))[filterWideSize-1] = 0; @@ -1208,6 +1225,23 @@ MP_API str8 mp_save_dialog(mem_arena* arena, 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)) {