diff --git a/milepost/src/mp_app.c b/milepost/src/app/mp_app.c similarity index 100% rename from milepost/src/mp_app.c rename to milepost/src/app/mp_app.c diff --git a/milepost/src/mp_app.h b/milepost/src/app/mp_app.h similarity index 100% rename from milepost/src/mp_app.h rename to milepost/src/app/mp_app.h diff --git a/milepost/src/mp_app_internal.h b/milepost/src/app/mp_app_internal.h similarity index 100% rename from milepost/src/mp_app_internal.h rename to milepost/src/app/mp_app_internal.h diff --git a/milepost/src/osx_app.h b/milepost/src/app/osx_app.h similarity index 98% rename from milepost/src/osx_app.h rename to milepost/src/app/osx_app.h index 0dfbc67..1be6020 100644 --- a/milepost/src/osx_app.h +++ b/milepost/src/app/osx_app.h @@ -10,7 +10,7 @@ #define __OSX_APP_H_ #include"mp_app.h" -#include"graphics.h" +#include"graphics/graphics.h" #ifdef __OBJC__ #import diff --git a/milepost/src/osx_app.m b/milepost/src/app/osx_app.m similarity index 99% rename from milepost/src/osx_app.m rename to milepost/src/app/osx_app.m index 1653d6a..56cf479 100644 --- a/milepost/src/osx_app.m +++ b/milepost/src/app/osx_app.m @@ -18,7 +18,7 @@ #include"macro_helpers.h" #include"platform_log.h" #include"platform_clock.h" -#include"graphics_surface.h" +#include"graphics/graphics_surface.h" #include"mp_app.c" diff --git a/milepost/src/win32_app.c b/milepost/src/app/win32_app.c similarity index 96% rename from milepost/src/win32_app.c rename to milepost/src/app/win32_app.c index 354e4c9..977564d 100644 --- a/milepost/src/win32_app.c +++ b/milepost/src/app/win32_app.c @@ -1,1526 +1,1526 @@ -/************************************************************//** -* -* @file: win32_app.c -* @author: Martin Fouilleul -* @date: 16/12/2022 -* @revision: -* -*****************************************************************/ - -#include -#include"mp_app.c" -#include"platform_thread.h" - -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); - - DWORD mode; - GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &mode); - mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; - SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), mode); - - SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - - u32 wheelScrollLines = 3; - SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &wheelScrollLines, 0); - __mpApp.win32.wheelScrollLines = wheelScrollLines; - } -} - -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; - mods |= MP_KEYMOD_MAIN_MODIFIER; - } - 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, &clientRect); - POINT point = {0}; - ClientToScreen(window->win32.hWnd, &point); - - int clientWidth = (clientRect.right - clientRect.left); - int clientHeight = (clientRect.bottom - clientRect.top); - - HWND insertAfter = window->win32.hWnd; - - for_list(&window->win32.layers, layer, mp_layer, listElt) - { - SetWindowPos(layer->hWnd, - insertAfter, - point.x, - point.y, - clientWidth, - clientHeight, - SWP_NOACTIVATE|SWP_NOOWNERZORDER|SWP_NOZORDER); - - insertAfter = layer->hWnd; - } -} - -LRESULT WinProc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam) -{ - LRESULT result = 0; - - mp_window_data* mpWindow = GetPropW(windowHandle, L"MilePost"); - //TODO: put messages in queue - - switch(message) - { - case WM_CLOSE: - { - mp_event event = {0}; - event.window = mp_window_handle_from_ptr(mpWindow); - event.type = MP_EVENT_WINDOW_CLOSE; - mp_queue_event(&event); - } break; - - case WM_DPICHANGED: - { - u32 dpi = HIWORD(wParam); - RECT rect = *(RECT*)lParam; - - SetWindowPos(mpWindow->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_WINDOWPOSCHANGED: - { - win32_update_child_layers(mpWindow); - result = DefWindowProc(windowHandle, message, wParam, lParam); - } break; - - case WM_SIZING: - case WM_MOVING: - { - RECT* rect = (RECT*)lParam; - - mp_event event = {0}; - event.type = message == WM_SIZING ? MP_EVENT_WINDOW_RESIZE : MP_EVENT_WINDOW_MOVE; - event.window = mp_window_handle_from_ptr(mpWindow); - - event.move.frame = mp_window_get_frame_rect(event.window); - event.move.content = mp_window_get_content_rect(event.window); - - mp_queue_event(&event); - - win32_update_child_layers(mpWindow); - } 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.mouse.x = LOWORD(lParam) / scaling; - event.mouse.y = HIWORD(lParam) / scaling; - - if(__mpApp.win32.mouseTracked || __mpApp.win32.mouseCaptureMask) - { - event.mouse.deltaX = event.mouse.x - __mpApp.win32.lastMousePos.x; - event.mouse.deltaY = event.mouse.y - __mpApp.win32.lastMousePos.y; - } - __mpApp.win32.lastMousePos = (vec2){event.mouse.x, event.mouse.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, - .mouse.x = event.mouse.x, - .mouse.y = event.mouse.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_SETTINGCHANGE: - { - if((u32)wParam == SPI_SETWHEELSCROLLLINES) - { - u32 wheelScrollLines; - if(SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &wheelScrollLines, 0) != 0) { - __mpApp.win32.wheelScrollLines = wheelScrollLines; - } - } - } break; - - case WM_DROPFILES: - { - //TODO - } break; - - case MP_WM_USER_DISPATCH_PROC: - { - mp_dispatch_proc proc = (mp_dispatch_proc)wParam; - void* user = (void*)lParam; - result = proc(user); - } 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; - - if(timeout < 0) - { - WaitMessage(); - } - else if(timeout > 0) - { - MsgWaitForMultipleObjects(0, NULL, FALSE, (DWORD) (timeout * 1e3), QS_ALLEVENTS); - } - - while(PeekMessage(&message, 0, 0, 0, PM_REMOVE)) - { - TranslateMessage(&message); - DispatchMessage(&message); - } -} - -i32 mp_dispatch_on_main_thread_sync(mp_window main_window, mp_dispatch_proc proc, void* user) -{ - mp_window_data* window_data = mp_window_ptr_from_handle(main_window); - DEBUG_ASSERT(window_data != NULL); - - LRESULT result = SendMessage(window_data->win32.hWnd, MP_WM_USER_DISPATCH_PROC, (WPARAM)proc, (LPARAM)user); - return result; -} - -//-------------------------------------------------------------------- -// 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 scaleX = (f32)dpiX/96.; - f32 scaleY = (f32)dpiY/96.; - - RECT frame = { - rect.x * scaleX, - rect.y * scaleY, - (rect.x + rect.w)*scaleX, - (rect.y + rect.h)*scaleY - }; - - DWORD winStyle = WS_OVERLAPPEDWINDOW; - AdjustWindowRect(&frame, winStyle, FALSE); - - HWND windowHandle = CreateWindow("ApplicationWindowClass", "Test Window", - winStyle, - frame.left, frame.top, - frame.right-frame.left, - frame.bottom-frame.top, - 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; - window->win32.layers = (list_info){0}; - - 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_frame_rect(mp_window window) -{ - mp_rect rect = {0}; - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - //NOTE: GetWindowRect() includes the drop shadow, which we don't want, so we call - // DwmGetWindowAttribute() instead. - // Note that contrary to what the GetWindowRect() docs suggests when mentionning - // this, DwmGetWindowAttributes() _does_ seem to adjust for DPI. - u32 dpi = GetDpiForWindow(windowData->win32.hWnd); - f32 scale = (float)dpi/96.; - - RECT frame; - HRESULT res = DwmGetWindowAttribute(windowData->win32.hWnd, - DWMWA_EXTENDED_FRAME_BOUNDS, - &frame, - sizeof(RECT)); - if(res == S_OK) - { - rect = (mp_rect){ - frame.left / scale, - frame.top / scale, - (frame.right - frame.left)/scale, - (frame.bottom - frame.top)/scale}; - } - } - return(rect); -} - -mp_rect win32_get_drop_shadow_offsets(HWND hWnd) -{ - RECT frameIncludingShadow; - RECT frameExcludingShadow; - - GetWindowRect(hWnd, &frameIncludingShadow); - DwmGetWindowAttribute(hWnd, - DWMWA_EXTENDED_FRAME_BOUNDS, - &frameExcludingShadow, - sizeof(RECT)); - - mp_rect extents = { - .x = frameIncludingShadow.left - frameExcludingShadow.left, - .y = frameIncludingShadow.top - frameExcludingShadow.top, - .w = frameIncludingShadow.right - frameExcludingShadow.right, - .h = frameIncludingShadow.bottom- frameExcludingShadow.bottom - }; - - return(extents); -} - -void mp_window_set_frame_rect(mp_window window, mp_rect rect) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - u32 dpi = GetDpiForWindow(windowData->win32.hWnd); - f32 scale = (float)dpi/96.; - - //NOTE compute the size of the drop shadow to add it in setwindowpos - mp_rect shadowOffsets = win32_get_drop_shadow_offsets(windowData->win32.hWnd); - - RECT frame = { - rect.x * scale + shadowOffsets.x, - rect.y * scale + shadowOffsets.y, - (rect.x + rect.w)*scale + shadowOffsets.w, - (rect.y + rect.h)*scale + shadowOffsets.h - }; - - SetWindowPos(windowData->win32.hWnd, - HWND_TOP, - frame.left, - frame.top, - frame.right - frame.left, - frame.bottom - frame.top, - SWP_NOZORDER|SWP_NOACTIVATE); - } -} - -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 client; - if(GetClientRect(windowData->win32.hWnd, &client)) - { - u32 dpi = GetDpiForWindow(windowData->win32.hWnd); - f32 scale = (float)dpi/96.; - - POINT origin = {0, 0}; - ClientToScreen(windowData->win32.hWnd, &origin); - - rect = (mp_rect){ - origin.x/scale, - origin.y/scale, - (client.right - client.left)/scale, - (client.bottom - client.top)/scale}; - } - } - return(rect); -} - -void mp_window_set_content_rect(mp_window window, mp_rect rect) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - u32 dpi = GetDpiForWindow(windowData->win32.hWnd); - f32 scale = (float)dpi/96.; - - RECT frame = { - rect.x * scale, - rect.y * scale, - (rect.x + rect.w)*scale, - (rect.y + rect.h)*scale}; - - DWORD style = GetWindowLong(windowData->win32.hWnd, GWL_STYLE); - BOOL menu = (GetMenu(windowData->win32.hWnd) != NULL); - AdjustWindowRect(&frame, style, menu); - - SetWindowPos(windowData->win32.hWnd, - HWND_TOP, - frame.left, - frame.top, - frame.right - frame.left, - frame.bottom - frame.top, - SWP_NOZORDER|SWP_NOACTIVATE); - } -} - -void mp_window_center(mp_window window) -{ - mp_window_data* windowData = mp_window_ptr_from_handle(window); - if(windowData) - { - - mp_rect frame = mp_window_get_frame_rect(window); - - HMONITOR monitor = MonitorFromWindow(windowData->win32.hWnd, MONITOR_DEFAULTTOPRIMARY); - if(monitor) - { - MONITORINFO monitorInfo = {.cbSize = sizeof(MONITORINFO)}; - GetMonitorInfoW(monitor, &monitorInfo); - - int dpiX, dpiY; - GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); - f32 scaleX = dpiX/96.; - f32 scaleY = dpiY/96.; - - f32 monX = monitorInfo.rcWork.left/scaleX; - f32 monY = monitorInfo.rcWork.top/scaleY; - f32 monW = (monitorInfo.rcWork.right - monitorInfo.rcWork.left)/scaleX; - f32 monH = (monitorInfo.rcWork.bottom - monitorInfo.rcWork.top)/scaleY; - - frame.x = monX + 0.5*(monW - frame.w); - frame.y = monY + 0.5*(monH - frame.h); - - mp_window_set_frame_rect(window, frame); - } - } -} - - -//-------------------------------------------------------------------------------- -// 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); -} - -vec2 mg_win32_surface_get_size(mg_surface_data* surface) -{ - vec2 size = {0}; - RECT rect; - if(GetClientRect(surface->layer.hWnd, &rect)) - { - u32 dpi = GetDpiForWindow(surface->layer.hWnd); - f32 scale = (float)dpi/96.; - size = (vec2){(rect.right - rect.left)/scale, (rect.bottom - rect.top)/scale}; - } - return(size); -} - -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) -{ - list_remove(&surface->layer.parent->win32.layers, &surface->layer.listElt); - 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->getSize = mg_win32_surface_get_size; - 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); - POINT point = {0}; - ClientToScreen(window->win32.hWnd, &point); - - int clientWidth = parentRect.right - parentRect.left; - int clientHeight = parentRect.bottom - parentRect.top; - - surface->layer.hWnd = CreateWindow("layer_window_class", "layer", - WS_POPUP | WS_VISIBLE, - point.x, point.y, clientWidth, clientHeight, - window->win32.hWnd, - 0, - layerWindowClass.hInstance, - 0); - - HRGN region = CreateRectRgn(0, 0, -1, -1); - - DWM_BLURBEHIND bb = {0}; - bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; - bb.hRgnBlur = region; - bb.fEnable = TRUE; - - HRESULT res = DwmEnableBlurBehindWindow(surface->layer.hWnd, &bb); - - DeleteObject(region); - if(res != S_OK) - { - log_error("couldn't enable blur behind\n"); - } - - surface->layer.parent = window; - list_append(&window->win32.layers, &surface->layer.listElt); -} - -void mg_surface_init_remote(mg_surface_data* surface, u32 width, u32 height) -{ - surface->contentsScaling = mg_win32_surface_contents_scaling; - surface->getSize = mg_win32_surface_get_size; - 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); -} - -//-------------------------------------------------------------------- -// native open/save/alert windows -//-------------------------------------------------------------------- - -//TODO: GetOpenFileName() doesn't seem to support selecting folders, and -// requires filters which pair a "descriptive" name with an extension - -#define interface struct -#include -#include -#undef interface - - -MP_API str8 mp_open_dialog(mem_arena* arena, - const char* title, - const char* defaultPath, - int filterCount, - const char** filters, - bool directory) -{ - str8 res = {0}; - HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); - if(SUCCEEDED(hr)) - { - IFileOpenDialog* dialog = 0; - hr = CoCreateInstance(&CLSID_FileOpenDialog, NULL, CLSCTX_ALL, &IID_IFileOpenDialog, (void**)&dialog); - if(SUCCEEDED(hr)) - { - if(directory) - { - FILEOPENDIALOGOPTIONS opt; - dialog->lpVtbl->GetOptions(dialog, &opt); - dialog->lpVtbl->SetOptions(dialog, opt | FOS_PICKFOLDERS); - } - - if(filterCount && filters) - { - mem_arena_scope tmp = mem_arena_scope_begin(arena); - COMDLG_FILTERSPEC* filterSpecs = mem_arena_alloc_array(arena, COMDLG_FILTERSPEC, filterCount); - for(int i=0; ilpVtbl->SetFileTypes(dialog, filterCount, filterSpecs); - - mem_arena_scope_end(tmp); - } - - if(defaultPath) - { - mem_arena_scope tmp = mem_arena_scope_begin(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_scope_end(tmp); - } - - hr = dialog->lpVtbl->Show(dialog, NULL); - if(SUCCEEDED(hr)) - { - IShellItem* item; - hr = dialog->lpVtbl->GetResult(dialog, &item); - if(SUCCEEDED(hr)) - { - PWSTR filePath; - hr = item->lpVtbl->GetDisplayName(item, SIGDN_FILESYSPATH, &filePath); - - if(SUCCEEDED(hr)) - { - int utf8Size = WideCharToMultiByte(CP_UTF8, 0, filePath, -1, NULL, 0, NULL, NULL); - if(utf8Size > 0) - { - res.ptr = mem_arena_alloc(arena, utf8Size); - res.len = utf8Size-1; - WideCharToMultiByte(CP_UTF8, 0, filePath, -1, res.ptr, utf8Size, NULL, NULL); - } - CoTaskMemFree(filePath); - } - item->lpVtbl->Release(item); - } - } - } - } - CoUninitialize(); - return(res); -} - -MP_API str8 mp_save_dialog(mem_arena* arena, - const char* title, - const char* defaultPath, - int filterCount, - const char** filters) -{ - str8 res = {0}; - HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); - if(SUCCEEDED(hr)) - { - IFileOpenDialog* dialog = 0; - hr = CoCreateInstance(&CLSID_FileSaveDialog, NULL, CLSCTX_ALL, &IID_IFileSaveDialog, (void**)&dialog); - if(SUCCEEDED(hr)) - { - if(filterCount && filters) - { - mem_arena_scope tmp = mem_arena_scope_begin(arena); - COMDLG_FILTERSPEC* filterSpecs = mem_arena_alloc_array(arena, COMDLG_FILTERSPEC, filterCount); - for(int i=0; ilpVtbl->SetFileTypes(dialog, filterCount, filterSpecs); - - mem_arena_scope_end(tmp); - } - - if(defaultPath) - { - mem_arena_scope tmp = mem_arena_scope_begin(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_scope_end(tmp); - } - - hr = dialog->lpVtbl->Show(dialog, NULL); - if(SUCCEEDED(hr)) - { - IShellItem* item; - hr = dialog->lpVtbl->GetResult(dialog, &item); - if(SUCCEEDED(hr)) - { - PWSTR filePath; - hr = item->lpVtbl->GetDisplayName(item, SIGDN_FILESYSPATH, &filePath); - - if(SUCCEEDED(hr)) - { - int utf8Size = WideCharToMultiByte(CP_UTF8, 0, filePath, -1, NULL, 0, NULL, NULL); - if(utf8Size > 0) - { - res.ptr = mem_arena_alloc(arena, utf8Size); - res.len = utf8Size-1; - WideCharToMultiByte(CP_UTF8, 0, filePath, -1, res.ptr, utf8Size, NULL, NULL); - } - CoTaskMemFree(filePath); - } - item->lpVtbl->Release(item); - } - } - } - } - CoUninitialize(); - return(res); -} - -#include - -MP_API int mp_alert_popup(const char* title, - const char* message, - u32 count, - const char** options) -{ - mem_arena* scratch = mem_scratch(); - mem_arena_scope tmp = mem_arena_scope_begin(scratch); - TASKDIALOG_BUTTON* buttons = mem_arena_alloc_array(scratch, TASKDIALOG_BUTTON, count); - - for(int i=0; i +#include"mp_app.c" +#include"platform_thread.h" + +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); + + DWORD mode; + GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &mode); + mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; + SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), mode); + + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + + u32 wheelScrollLines = 3; + SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &wheelScrollLines, 0); + __mpApp.win32.wheelScrollLines = wheelScrollLines; + } +} + +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; + mods |= MP_KEYMOD_MAIN_MODIFIER; + } + 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, &clientRect); + POINT point = {0}; + ClientToScreen(window->win32.hWnd, &point); + + int clientWidth = (clientRect.right - clientRect.left); + int clientHeight = (clientRect.bottom - clientRect.top); + + HWND insertAfter = window->win32.hWnd; + + for_list(&window->win32.layers, layer, mp_layer, listElt) + { + SetWindowPos(layer->hWnd, + insertAfter, + point.x, + point.y, + clientWidth, + clientHeight, + SWP_NOACTIVATE|SWP_NOOWNERZORDER|SWP_NOZORDER); + + insertAfter = layer->hWnd; + } +} + +LRESULT WinProc(HWND windowHandle, UINT message, WPARAM wParam, LPARAM lParam) +{ + LRESULT result = 0; + + mp_window_data* mpWindow = GetPropW(windowHandle, L"MilePost"); + //TODO: put messages in queue + + switch(message) + { + case WM_CLOSE: + { + mp_event event = {0}; + event.window = mp_window_handle_from_ptr(mpWindow); + event.type = MP_EVENT_WINDOW_CLOSE; + mp_queue_event(&event); + } break; + + case WM_DPICHANGED: + { + u32 dpi = HIWORD(wParam); + RECT rect = *(RECT*)lParam; + + SetWindowPos(mpWindow->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_WINDOWPOSCHANGED: + { + win32_update_child_layers(mpWindow); + result = DefWindowProc(windowHandle, message, wParam, lParam); + } break; + + case WM_SIZING: + case WM_MOVING: + { + RECT* rect = (RECT*)lParam; + + mp_event event = {0}; + event.type = message == WM_SIZING ? MP_EVENT_WINDOW_RESIZE : MP_EVENT_WINDOW_MOVE; + event.window = mp_window_handle_from_ptr(mpWindow); + + event.move.frame = mp_window_get_frame_rect(event.window); + event.move.content = mp_window_get_content_rect(event.window); + + mp_queue_event(&event); + + win32_update_child_layers(mpWindow); + } 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.mouse.x = LOWORD(lParam) / scaling; + event.mouse.y = HIWORD(lParam) / scaling; + + if(__mpApp.win32.mouseTracked || __mpApp.win32.mouseCaptureMask) + { + event.mouse.deltaX = event.mouse.x - __mpApp.win32.lastMousePos.x; + event.mouse.deltaY = event.mouse.y - __mpApp.win32.lastMousePos.y; + } + __mpApp.win32.lastMousePos = (vec2){event.mouse.x, event.mouse.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, + .mouse.x = event.mouse.x, + .mouse.y = event.mouse.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_SETTINGCHANGE: + { + if((u32)wParam == SPI_SETWHEELSCROLLLINES) + { + u32 wheelScrollLines; + if(SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &wheelScrollLines, 0) != 0) { + __mpApp.win32.wheelScrollLines = wheelScrollLines; + } + } + } break; + + case WM_DROPFILES: + { + //TODO + } break; + + case MP_WM_USER_DISPATCH_PROC: + { + mp_dispatch_proc proc = (mp_dispatch_proc)wParam; + void* user = (void*)lParam; + result = proc(user); + } 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; + + if(timeout < 0) + { + WaitMessage(); + } + else if(timeout > 0) + { + MsgWaitForMultipleObjects(0, NULL, FALSE, (DWORD) (timeout * 1e3), QS_ALLEVENTS); + } + + while(PeekMessage(&message, 0, 0, 0, PM_REMOVE)) + { + TranslateMessage(&message); + DispatchMessage(&message); + } +} + +i32 mp_dispatch_on_main_thread_sync(mp_window main_window, mp_dispatch_proc proc, void* user) +{ + mp_window_data* window_data = mp_window_ptr_from_handle(main_window); + DEBUG_ASSERT(window_data != NULL); + + LRESULT result = SendMessage(window_data->win32.hWnd, MP_WM_USER_DISPATCH_PROC, (WPARAM)proc, (LPARAM)user); + return result; +} + +//-------------------------------------------------------------------- +// 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 scaleX = (f32)dpiX/96.; + f32 scaleY = (f32)dpiY/96.; + + RECT frame = { + rect.x * scaleX, + rect.y * scaleY, + (rect.x + rect.w)*scaleX, + (rect.y + rect.h)*scaleY + }; + + DWORD winStyle = WS_OVERLAPPEDWINDOW; + AdjustWindowRect(&frame, winStyle, FALSE); + + HWND windowHandle = CreateWindow("ApplicationWindowClass", "Test Window", + winStyle, + frame.left, frame.top, + frame.right-frame.left, + frame.bottom-frame.top, + 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; + window->win32.layers = (list_info){0}; + + 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_frame_rect(mp_window window) +{ + mp_rect rect = {0}; + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + //NOTE: GetWindowRect() includes the drop shadow, which we don't want, so we call + // DwmGetWindowAttribute() instead. + // Note that contrary to what the GetWindowRect() docs suggests when mentionning + // this, DwmGetWindowAttributes() _does_ seem to adjust for DPI. + u32 dpi = GetDpiForWindow(windowData->win32.hWnd); + f32 scale = (float)dpi/96.; + + RECT frame; + HRESULT res = DwmGetWindowAttribute(windowData->win32.hWnd, + DWMWA_EXTENDED_FRAME_BOUNDS, + &frame, + sizeof(RECT)); + if(res == S_OK) + { + rect = (mp_rect){ + frame.left / scale, + frame.top / scale, + (frame.right - frame.left)/scale, + (frame.bottom - frame.top)/scale}; + } + } + return(rect); +} + +mp_rect win32_get_drop_shadow_offsets(HWND hWnd) +{ + RECT frameIncludingShadow; + RECT frameExcludingShadow; + + GetWindowRect(hWnd, &frameIncludingShadow); + DwmGetWindowAttribute(hWnd, + DWMWA_EXTENDED_FRAME_BOUNDS, + &frameExcludingShadow, + sizeof(RECT)); + + mp_rect extents = { + .x = frameIncludingShadow.left - frameExcludingShadow.left, + .y = frameIncludingShadow.top - frameExcludingShadow.top, + .w = frameIncludingShadow.right - frameExcludingShadow.right, + .h = frameIncludingShadow.bottom- frameExcludingShadow.bottom + }; + + return(extents); +} + +void mp_window_set_frame_rect(mp_window window, mp_rect rect) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + u32 dpi = GetDpiForWindow(windowData->win32.hWnd); + f32 scale = (float)dpi/96.; + + //NOTE compute the size of the drop shadow to add it in setwindowpos + mp_rect shadowOffsets = win32_get_drop_shadow_offsets(windowData->win32.hWnd); + + RECT frame = { + rect.x * scale + shadowOffsets.x, + rect.y * scale + shadowOffsets.y, + (rect.x + rect.w)*scale + shadowOffsets.w, + (rect.y + rect.h)*scale + shadowOffsets.h + }; + + SetWindowPos(windowData->win32.hWnd, + HWND_TOP, + frame.left, + frame.top, + frame.right - frame.left, + frame.bottom - frame.top, + SWP_NOZORDER|SWP_NOACTIVATE); + } +} + +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 client; + if(GetClientRect(windowData->win32.hWnd, &client)) + { + u32 dpi = GetDpiForWindow(windowData->win32.hWnd); + f32 scale = (float)dpi/96.; + + POINT origin = {0, 0}; + ClientToScreen(windowData->win32.hWnd, &origin); + + rect = (mp_rect){ + origin.x/scale, + origin.y/scale, + (client.right - client.left)/scale, + (client.bottom - client.top)/scale}; + } + } + return(rect); +} + +void mp_window_set_content_rect(mp_window window, mp_rect rect) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + u32 dpi = GetDpiForWindow(windowData->win32.hWnd); + f32 scale = (float)dpi/96.; + + RECT frame = { + rect.x * scale, + rect.y * scale, + (rect.x + rect.w)*scale, + (rect.y + rect.h)*scale}; + + DWORD style = GetWindowLong(windowData->win32.hWnd, GWL_STYLE); + BOOL menu = (GetMenu(windowData->win32.hWnd) != NULL); + AdjustWindowRect(&frame, style, menu); + + SetWindowPos(windowData->win32.hWnd, + HWND_TOP, + frame.left, + frame.top, + frame.right - frame.left, + frame.bottom - frame.top, + SWP_NOZORDER|SWP_NOACTIVATE); + } +} + +void mp_window_center(mp_window window) +{ + mp_window_data* windowData = mp_window_ptr_from_handle(window); + if(windowData) + { + + mp_rect frame = mp_window_get_frame_rect(window); + + HMONITOR monitor = MonitorFromWindow(windowData->win32.hWnd, MONITOR_DEFAULTTOPRIMARY); + if(monitor) + { + MONITORINFO monitorInfo = {.cbSize = sizeof(MONITORINFO)}; + GetMonitorInfoW(monitor, &monitorInfo); + + int dpiX, dpiY; + GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); + f32 scaleX = dpiX/96.; + f32 scaleY = dpiY/96.; + + f32 monX = monitorInfo.rcWork.left/scaleX; + f32 monY = monitorInfo.rcWork.top/scaleY; + f32 monW = (monitorInfo.rcWork.right - monitorInfo.rcWork.left)/scaleX; + f32 monH = (monitorInfo.rcWork.bottom - monitorInfo.rcWork.top)/scaleY; + + frame.x = monX + 0.5*(monW - frame.w); + frame.y = monY + 0.5*(monH - frame.h); + + mp_window_set_frame_rect(window, frame); + } + } +} + + +//-------------------------------------------------------------------------------- +// 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/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); +} + +vec2 mg_win32_surface_get_size(mg_surface_data* surface) +{ + vec2 size = {0}; + RECT rect; + if(GetClientRect(surface->layer.hWnd, &rect)) + { + u32 dpi = GetDpiForWindow(surface->layer.hWnd); + f32 scale = (float)dpi/96.; + size = (vec2){(rect.right - rect.left)/scale, (rect.bottom - rect.top)/scale}; + } + return(size); +} + +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) +{ + list_remove(&surface->layer.parent->win32.layers, &surface->layer.listElt); + 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->getSize = mg_win32_surface_get_size; + 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); + POINT point = {0}; + ClientToScreen(window->win32.hWnd, &point); + + int clientWidth = parentRect.right - parentRect.left; + int clientHeight = parentRect.bottom - parentRect.top; + + surface->layer.hWnd = CreateWindow("layer_window_class", "layer", + WS_POPUP | WS_VISIBLE, + point.x, point.y, clientWidth, clientHeight, + window->win32.hWnd, + 0, + layerWindowClass.hInstance, + 0); + + HRGN region = CreateRectRgn(0, 0, -1, -1); + + DWM_BLURBEHIND bb = {0}; + bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION; + bb.hRgnBlur = region; + bb.fEnable = TRUE; + + HRESULT res = DwmEnableBlurBehindWindow(surface->layer.hWnd, &bb); + + DeleteObject(region); + if(res != S_OK) + { + log_error("couldn't enable blur behind\n"); + } + + surface->layer.parent = window; + list_append(&window->win32.layers, &surface->layer.listElt); +} + +void mg_surface_init_remote(mg_surface_data* surface, u32 width, u32 height) +{ + surface->contentsScaling = mg_win32_surface_contents_scaling; + surface->getSize = mg_win32_surface_get_size; + 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); +} + +//-------------------------------------------------------------------- +// native open/save/alert windows +//-------------------------------------------------------------------- + +//TODO: GetOpenFileName() doesn't seem to support selecting folders, and +// requires filters which pair a "descriptive" name with an extension + +#define interface struct +#include +#include +#undef interface + + +MP_API str8 mp_open_dialog(mem_arena* arena, + const char* title, + const char* defaultPath, + int filterCount, + const char** filters, + bool directory) +{ + str8 res = {0}; + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if(SUCCEEDED(hr)) + { + IFileOpenDialog* dialog = 0; + hr = CoCreateInstance(&CLSID_FileOpenDialog, NULL, CLSCTX_ALL, &IID_IFileOpenDialog, (void**)&dialog); + if(SUCCEEDED(hr)) + { + if(directory) + { + FILEOPENDIALOGOPTIONS opt; + dialog->lpVtbl->GetOptions(dialog, &opt); + dialog->lpVtbl->SetOptions(dialog, opt | FOS_PICKFOLDERS); + } + + if(filterCount && filters) + { + mem_arena_scope tmp = mem_arena_scope_begin(arena); + COMDLG_FILTERSPEC* filterSpecs = mem_arena_alloc_array(arena, COMDLG_FILTERSPEC, filterCount); + for(int i=0; ilpVtbl->SetFileTypes(dialog, filterCount, filterSpecs); + + mem_arena_scope_end(tmp); + } + + if(defaultPath) + { + mem_arena_scope tmp = mem_arena_scope_begin(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_scope_end(tmp); + } + + hr = dialog->lpVtbl->Show(dialog, NULL); + if(SUCCEEDED(hr)) + { + IShellItem* item; + hr = dialog->lpVtbl->GetResult(dialog, &item); + if(SUCCEEDED(hr)) + { + PWSTR filePath; + hr = item->lpVtbl->GetDisplayName(item, SIGDN_FILESYSPATH, &filePath); + + if(SUCCEEDED(hr)) + { + int utf8Size = WideCharToMultiByte(CP_UTF8, 0, filePath, -1, NULL, 0, NULL, NULL); + if(utf8Size > 0) + { + res.ptr = mem_arena_alloc(arena, utf8Size); + res.len = utf8Size-1; + WideCharToMultiByte(CP_UTF8, 0, filePath, -1, res.ptr, utf8Size, NULL, NULL); + } + CoTaskMemFree(filePath); + } + item->lpVtbl->Release(item); + } + } + } + } + CoUninitialize(); + return(res); +} + +MP_API str8 mp_save_dialog(mem_arena* arena, + const char* title, + const char* defaultPath, + int filterCount, + const char** filters) +{ + str8 res = {0}; + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); + if(SUCCEEDED(hr)) + { + IFileOpenDialog* dialog = 0; + hr = CoCreateInstance(&CLSID_FileSaveDialog, NULL, CLSCTX_ALL, &IID_IFileSaveDialog, (void**)&dialog); + if(SUCCEEDED(hr)) + { + if(filterCount && filters) + { + mem_arena_scope tmp = mem_arena_scope_begin(arena); + COMDLG_FILTERSPEC* filterSpecs = mem_arena_alloc_array(arena, COMDLG_FILTERSPEC, filterCount); + for(int i=0; ilpVtbl->SetFileTypes(dialog, filterCount, filterSpecs); + + mem_arena_scope_end(tmp); + } + + if(defaultPath) + { + mem_arena_scope tmp = mem_arena_scope_begin(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_scope_end(tmp); + } + + hr = dialog->lpVtbl->Show(dialog, NULL); + if(SUCCEEDED(hr)) + { + IShellItem* item; + hr = dialog->lpVtbl->GetResult(dialog, &item); + if(SUCCEEDED(hr)) + { + PWSTR filePath; + hr = item->lpVtbl->GetDisplayName(item, SIGDN_FILESYSPATH, &filePath); + + if(SUCCEEDED(hr)) + { + int utf8Size = WideCharToMultiByte(CP_UTF8, 0, filePath, -1, NULL, 0, NULL, NULL); + if(utf8Size > 0) + { + res.ptr = mem_arena_alloc(arena, utf8Size); + res.len = utf8Size-1; + WideCharToMultiByte(CP_UTF8, 0, filePath, -1, res.ptr, utf8Size, NULL, NULL); + } + CoTaskMemFree(filePath); + } + item->lpVtbl->Release(item); + } + } + } + } + CoUninitialize(); + return(res); +} + +#include + +MP_API int mp_alert_popup(const char* title, + const char* message, + u32 count, + const char** options) +{ + mem_arena* scratch = mem_scratch(); + mem_arena_scope tmp = mem_arena_scope_begin(scratch); + TASKDIALOG_BUTTON* buttons = mem_arena_alloc_array(scratch, TASKDIALOG_BUTTON, count); + + for(int i=0; i - - - - - - - - -Orca Runtime - - - - - - + + + + + + + + + +Orca Runtime + + + + + + diff --git a/milepost/src/egl_surface.c b/milepost/src/graphics/egl_surface.c similarity index 99% rename from milepost/src/egl_surface.c rename to milepost/src/graphics/egl_surface.c index 970b2b4..dd2e28e 100644 --- a/milepost/src/egl_surface.c +++ b/milepost/src/graphics/egl_surface.c @@ -10,7 +10,7 @@ #define EGL_EGLEXT_PROTOTYPES #include #include -#include"mp_app_internal.h" +#include"app/mp_app_internal.h" #include"graphics_surface.h" #include"gl_loader.h" diff --git a/milepost/src/egl_surface.h b/milepost/src/graphics/egl_surface.h similarity index 95% rename from milepost/src/egl_surface.h rename to milepost/src/graphics/egl_surface.h index e73de5c..1846cf8 100644 --- a/milepost/src/egl_surface.h +++ b/milepost/src/graphics/egl_surface.h @@ -10,7 +10,7 @@ #define __EGL_SURFACE_H_ #include"graphics_surface.h" -#include"mp_app.h" +#include"app/mp_app.h" mg_surface_data* mg_egl_surface_create_for_window(mp_window window); mg_surface_data* mg_egl_surface_create_remote(u32 width, u32 height); diff --git a/milepost/src/gl_api.h b/milepost/src/graphics/gl_api.h similarity index 100% rename from milepost/src/gl_api.h rename to milepost/src/graphics/gl_api.h diff --git a/milepost/src/gl_canvas.c b/milepost/src/graphics/gl_canvas.c similarity index 96% rename from milepost/src/gl_canvas.c rename to milepost/src/graphics/gl_canvas.c index 50a81df..c50d525 100644 --- a/milepost/src/gl_canvas.c +++ b/milepost/src/graphics/gl_canvas.c @@ -1,1826 +1,1826 @@ -/************************************************************//** -* -* @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_image -{ - mg_image_data interface; - GLuint texture; -} mg_gl_image; - -enum _mg_gl_cmd { - MG_GL_FILL, - MG_GL_STROKE, -}; -typedef int mg_gl_cmd; - -typedef struct mg_gl_path -{ - float uvTransform[12]; - vec4 color; - vec4 box; - vec4 clip; - mg_gl_cmd cmd; - int textureID; - u8 pad[8]; -} mg_gl_path; - -enum _mg_gl_seg_kind{ - MG_GL_LINE = 1, - MG_GL_QUADRATIC, - MG_GL_CUBIC, -}; -typedef int mg_gl_seg_kind; - -typedef struct mg_gl_path_elt -{ - vec2 p[4]; - int pathIndex; - mg_gl_seg_kind kind; - -} mg_gl_path_elt; - -enum { - LAYOUT_PATH_SIZE = sizeof(mg_gl_path), - LAYOUT_PATH_ELT_SIZE = sizeof(mg_gl_path_elt), -}; - - -typedef struct mg_gl_dispatch_indirect_command -{ - u32 num_groups_x; - u32 num_groups_y; - u32 num_groups_z; - -} mg_gl_dispatch_indirect_command; -//////////////////////////////////////////////////////////// -//NOTE: these are just here for the sizes... - -#define MG_GL_LAYOUT_FIRST(name, type) \ - MG_GL_##name##_OFFSET = 0, \ - MG_GL_##name##_SIZE = MG_GL_##type##_SIZE, - -#define MG_GL_LAYOUT_NEXT(name, type, prev) \ - MG_GL_##name##_OFFSET = AlignUpOnPow2(MG_GL_##prev##_OFFSET + MG_GL_##prev##_SIZE, MG_GL_##type##_ALIGN), \ - MG_GL_##name##_SIZE = MG_GL_##type##_SIZE, - -#define MG_GL_LAYOUT_SIZE(name, last, maxAlignType) \ - MG_GL_##name##_ALIGN = AlignUpOnPow2(MG_GL_##maxAlignType##_ALIGN, MG_GL_VEC4_ALIGN), \ - MG_GL_##name##_SIZE = AlignUpOnPow2(MG_GL_##last##_OFFSET + MG_GL_##last##_SIZE, MG_GL_##name##_ALIGN), - -enum -{ - MG_GL_I32_SIZE = sizeof(i32), - MG_GL_I32_ALIGN = sizeof(i32), - MG_GL_F32_SIZE = sizeof(f32), - MG_GL_F32_ALIGN = sizeof(f32), - MG_GL_VEC2_SIZE = 2*sizeof(f32), - MG_GL_VEC2_ALIGN = 2*sizeof(f32), - MG_GL_VEC3_SIZE = 4*sizeof(f32), - MG_GL_VEC3_ALIGN = 4*sizeof(f32), - MG_GL_VEC4_SIZE = 4*sizeof(f32), - MG_GL_VEC4_ALIGN = 4*sizeof(f32), - MG_GL_MAT3_SIZE = 3*3*MG_GL_VEC3_SIZE, - MG_GL_MAT3_ALIGN = MG_GL_VEC3_ALIGN, - - MG_GL_LAYOUT_FIRST(SEGMENT_KIND, I32) - MG_GL_LAYOUT_NEXT(SEGMENT_PATH_INDEX, I32, SEGMENT_KIND) - MG_GL_LAYOUT_NEXT(SEGMENT_CONFIG, I32, SEGMENT_PATH_INDEX) - MG_GL_LAYOUT_NEXT(SEGMENT_WINDING, I32, SEGMENT_CONFIG) - MG_GL_LAYOUT_NEXT(SEGMENT_BOX, VEC4, SEGMENT_WINDING) - MG_GL_LAYOUT_NEXT(SEGMENT_IMPLICIT_MATRIX, MAT3, SEGMENT_BOX) - MG_GL_LAYOUT_NEXT(SEGMENT_HULL_VERTEX, VEC2, SEGMENT_IMPLICIT_MATRIX) - MG_GL_LAYOUT_NEXT(SEGMENT_SIGN, F32, SEGMENT_HULL_VERTEX) - MG_GL_LAYOUT_SIZE(SEGMENT, SEGMENT_SIGN, MAT3) - - MG_GL_LAYOUT_FIRST(PATH_QUEUE_AREA, VEC4) - MG_GL_LAYOUT_NEXT(PATH_QUEUE_TILE_QUEUES, I32, PATH_QUEUE_AREA) - MG_GL_LAYOUT_SIZE(PATH_QUEUE, PATH_QUEUE_TILE_QUEUES, VEC4) - - MG_GL_LAYOUT_FIRST(TILE_OP_KIND, I32) - MG_GL_LAYOUT_NEXT(TILE_OP_NEXT, I32, TILE_OP_KIND) - MG_GL_LAYOUT_NEXT(TILE_OP_INDEX, I32, TILE_OP_NEXT) - MG_GL_LAYOUT_NEXT(TILE_OP_WINDING, I32, TILE_OP_INDEX) - MG_GL_LAYOUT_SIZE(TILE_OP, TILE_OP_WINDING, I32) - - MG_GL_LAYOUT_FIRST(TILE_QUEUE_WINDING, I32) - MG_GL_LAYOUT_NEXT(TILE_QUEUE_FIRST, I32, TILE_QUEUE_WINDING) - MG_GL_LAYOUT_NEXT(TILE_QUEUE_LAST, I32, TILE_QUEUE_FIRST) - MG_GL_LAYOUT_SIZE(TILE_QUEUE, TILE_QUEUE_LAST, I32) - - MG_GL_LAYOUT_FIRST(SCREEN_TILE_COORD, VEC2) - MG_GL_LAYOUT_NEXT(SCREEN_TILE_FIRST, I32, SCREEN_TILE_COORD) - MG_GL_LAYOUT_SIZE(SCREEN_TILE, SCREEN_TILE_FIRST, VEC2) -}; - -enum { - MG_GL_INPUT_BUFFERS_COUNT = 3, - MG_GL_TILE_SIZE = 16, - MG_GL_MSAA_COUNT = 8, - MG_GL_MAX_IMAGES_PER_BATCH = 8, -}; - -typedef struct mg_gl_mapped_buffer -{ - GLuint buffer; - int size; - char* contents; -} mg_gl_mapped_buffer; - -typedef struct mg_gl_canvas_backend -{ - mg_canvas_backend interface; - mg_wgl_surface* surface; - - int msaaCount; - vec2 frameSize; - - // gl stuff - GLuint vao; - - GLuint pathSetup; - GLuint segmentSetup; - GLuint backprop; - GLuint merge; - GLuint balanceWorkgroups; - GLuint raster; - GLuint blit; - - GLuint outTexture; - - int bufferIndex; - GLsync bufferSync[MG_GL_INPUT_BUFFERS_COUNT]; - mg_gl_mapped_buffer pathBuffer[MG_GL_INPUT_BUFFERS_COUNT]; - mg_gl_mapped_buffer elementBuffer[MG_GL_INPUT_BUFFERS_COUNT]; - - GLuint segmentBuffer; - GLuint segmentCountBuffer; - GLuint pathQueueBuffer; - GLuint tileQueueBuffer; - GLuint tileQueueCountBuffer; - GLuint tileOpBuffer; - GLuint tileOpCountBuffer; - GLuint screenTilesBuffer; - GLuint screenTilesCountBuffer; - GLuint rasterDispatchBuffer; - GLuint dummyVertexBuffer; - - //encoding context - int pathCount; - int eltCount; - - int pathBatchStart; - int eltBatchStart; - - mg_primitive* primitive; - vec4 pathScreenExtents; - vec4 pathUserExtents; - - int maxTileQueueCount; - int maxSegmentCount; - - int currentImageIndex; -} mg_gl_canvas_backend; - -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_gl_grow_input_buffer(mg_gl_mapped_buffer* buffer, int copyStart, int copySize, int newSize) -{ - mg_gl_mapped_buffer newBuffer = {0}; - newBuffer.size = newSize; - glGenBuffers(1, &newBuffer.buffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, newBuffer.buffer); - glBufferStorage(GL_SHADER_STORAGE_BUFFER, newBuffer.size, 0, GL_MAP_WRITE_BIT|GL_MAP_PERSISTENT_BIT); - newBuffer.contents = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, - 0, - newBuffer.size, - GL_MAP_WRITE_BIT - |GL_MAP_PERSISTENT_BIT - |GL_MAP_FLUSH_EXPLICIT_BIT); - - memcpy(newBuffer.contents + copyStart, buffer->contents + copyStart, copySize); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer->buffer); - glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); - glDeleteBuffers(1, &buffer->buffer); - - *buffer = newBuffer; -} - -void mg_gl_canvas_encode_element(mg_gl_canvas_backend* backend, mg_path_elt_type kind, vec2* p) -{ - int bufferIndex = backend->bufferIndex; - int bufferCap = backend->elementBuffer[bufferIndex].size / sizeof(mg_gl_path_elt); - if(backend->eltCount >= bufferCap) - { - int newBufferCap = (int)(bufferCap * 1.5); - int newBufferSize = newBufferCap * sizeof(mg_gl_path_elt); - - log_info("growing element buffer to %i elements\n", newBufferCap); - - mg_gl_grow_input_buffer(&backend->elementBuffer[bufferIndex], - backend->eltBatchStart * sizeof(mg_gl_path_elt), - backend->eltCount * sizeof(mg_gl_path_elt), - newBufferSize); - } - - mg_gl_path_elt* elementData = (mg_gl_path_elt*)backend->elementBuffer[bufferIndex].contents; - mg_gl_path_elt* elt = &elementData[backend->eltCount]; - backend->eltCount++; - - elt->pathIndex = backend->pathCount - backend->pathBatchStart; - int count = 0; - switch(kind) - { - case MG_PATH_LINE: - backend->maxSegmentCount += 1; - elt->kind = MG_GL_LINE; - count = 2; - break; - - case MG_PATH_QUADRATIC: - backend->maxSegmentCount += 3; - elt->kind = MG_GL_QUADRATIC; - count = 3; - break; - - case MG_PATH_CUBIC: - backend->maxSegmentCount += 7; - elt->kind = MG_GL_CUBIC; - count = 4; - break; - - default: - break; - } - - for(int i=0; ipathUserExtents, p[i]); - - vec2 screenP = mg_mat2x3_mul(backend->primitive->attributes.transform, p[i]); - elt->p[i] = (vec2){screenP.x, screenP.y}; - - mg_update_path_extents(&backend->pathScreenExtents, screenP); - } -} - -void mg_gl_canvas_encode_path(mg_gl_canvas_backend* backend, mg_primitive* primitive, f32 scale) -{ - int bufferIndex = backend->bufferIndex; - int bufferCap = backend->pathBuffer[bufferIndex].size / sizeof(mg_gl_path); - if(backend->pathCount >= bufferCap) - { - int newBufferCap = (int)(bufferCap * 1.5); - int newBufferSize = newBufferCap * sizeof(mg_gl_path); - - log_info("growing path buffer to %i elements\n", newBufferCap); - - mg_gl_grow_input_buffer(&backend->pathBuffer[bufferIndex], - backend->pathBatchStart * sizeof(mg_gl_path), - backend->eltCount * sizeof(mg_gl_path), - newBufferSize); - } - - mg_gl_path* pathData = (mg_gl_path*)backend->pathBuffer[backend->bufferIndex].contents; - mg_gl_path* path = &pathData[backend->pathCount]; - backend->pathCount++; - - path->cmd = (mg_gl_cmd)primitive->cmd; - - path->box = (vec4){ - backend->pathScreenExtents.x, - backend->pathScreenExtents.y, - backend->pathScreenExtents.z, - backend->pathScreenExtents.w}; - - path->clip = (vec4){ - 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 = (vec4){ - 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 = { - backend->pathUserExtents.x, - backend->pathUserExtents.y, - backend->pathUserExtents.z - backend->pathUserExtents.x, - backend->pathUserExtents.w - backend->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); - - //NOTE: mat3 std430 layout is an array of vec3, which are padded to _vec4_ alignment - path->uvTransform[0] = uvTransform.m[0]/scale; - path->uvTransform[1] = uvTransform.m[3]/scale; - path->uvTransform[2] = 0; - path->uvTransform[3] = 0; - path->uvTransform[4] = uvTransform.m[1]/scale; - path->uvTransform[5] = uvTransform.m[4]/scale; - path->uvTransform[6] = 0; - path->uvTransform[7] = 0; - path->uvTransform[8] = uvTransform.m[2]; - path->uvTransform[9] = uvTransform.m[5]; - path->uvTransform[10] = 1; - path->uvTransform[11] = 0; - - path->textureID = backend->currentImageIndex; - } - else - { - path->textureID = -1; - } - - int nTilesX = ((path->box.z - path->box.x)*scale - 1) / MG_GL_TILE_SIZE + 1; - int nTilesY = ((path->box.w - path->box.y)*scale - 1) / MG_GL_TILE_SIZE + 1; - backend->maxTileQueueCount += (nTilesX * nTilesY); -} - -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_gl_canvas_encode_element(backend, MG_PATH_LINE, right); - - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, left); - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, joint0); - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, joint1); -} - -enum { MG_HULL_CHECK_SAMPLE_COUNT = 5 }; - -void mg_gl_encode_stroke_quadratic(mg_gl_canvas_backend* backend, vec2* p) -{ - f32 width = backend->primitive->attributes.width; - f32 tolerance = minimum(backend->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_gl_encode_stroke_line(backend, p+1); - return; - } - else if(vec2_close(p[1], p[2], equalEps)) - { - mg_gl_encode_stroke_line(backend, 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_gl_encode_stroke_quadratic(backend, splitLeft); - mg_gl_encode_stroke_quadratic(backend, splitRight); - } - else - { - f32 checkSamples[MG_HULL_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_gl_encode_stroke_quadratic(backend, splitLeft); - mg_gl_encode_stroke_quadratic(backend, splitRight); - } - else - { - vec2 tmp = leftHull[0]; - leftHull[0] = leftHull[2]; - leftHull[2] = tmp; - - mg_gl_canvas_encode_element(backend, MG_PATH_QUADRATIC, rightHull); - mg_gl_canvas_encode_element(backend, MG_PATH_QUADRATIC, leftHull); - - vec2 joint0[2] = {rightHull[2], leftHull[0]}; - vec2 joint1[2] = {leftHull[2], rightHull[0]}; - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, joint0); - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, joint1); - } - } -} - -void mg_gl_encode_stroke_cubic(mg_gl_canvas_backend* backend, vec2* p) -{ - f32 width = backend->primitive->attributes.width; - f32 tolerance = minimum(backend->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_gl_encode_stroke_line(backend, 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_gl_encode_stroke_line(backend, 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_gl_encode_stroke_line(backend, 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_gl_encode_stroke_cubic(backend, splitLeft); - mg_gl_encode_stroke_cubic(backend, splitRight); - } - else - { - f32 checkSamples[MG_HULL_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_gl_encode_stroke_cubic(backend, splitLeft); - mg_gl_encode_stroke_cubic(backend, splitRight); - } - else - { - vec2 tmp = leftHull[0]; - leftHull[0] = leftHull[3]; - leftHull[3] = tmp; - tmp = leftHull[1]; - leftHull[1] = leftHull[2]; - leftHull[2] = tmp; - - mg_gl_canvas_encode_element(backend, MG_PATH_CUBIC, rightHull); - mg_gl_canvas_encode_element(backend, MG_PATH_CUBIC, leftHull); - - vec2 joint0[2] = {rightHull[3], leftHull[0]}; - vec2 joint1[2] = {leftHull[3], rightHull[0]}; - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, joint0); - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, joint1); - } - } -} - -void mg_gl_encode_stroke_element(mg_gl_canvas_backend* backend, - 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_gl_encode_stroke_line(backend, controlPoints); - endPointIndex = 1; - break; - - case MG_PATH_QUADRATIC: - mg_gl_encode_stroke_quadratic(backend, controlPoints); - endPointIndex = 2; - break; - - case MG_PATH_CUBIC: - mg_gl_encode_stroke_cubic(backend, 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_gl_stroke_cap(mg_gl_canvas_backend* backend, - vec2 p0, - vec2 direction) -{ - mg_attributes* attributes = &backend->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_gl_canvas_encode_element(backend, MG_PATH_LINE, points); - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+1); - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+2); - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+3); -} - -void mg_gl_stroke_joint(mg_gl_canvas_backend* backend, - vec2 p0, - vec2 t0, - vec2 t1) -{ - mg_attributes* attributes = &backend->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_gl_canvas_encode_element(backend, MG_PATH_LINE, points); - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+1); - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+2); - mg_gl_canvas_encode_element(backend, 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_gl_canvas_encode_element(backend, MG_PATH_LINE, points); - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+1); - mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+2); - } -} - -u32 mg_gl_encode_stroke_subpath(mg_gl_canvas_backend* backend, - 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): encode first element and compute first tangent - mg_gl_encode_stroke_element(backend, elements + startIndex, currentPoint, &startTangent, &endTangent, &endPoint); - - firstTangent = startTangent; - previousEndTangent = endTangent; - currentPoint = endPoint; - - //NOTE(martin): encode subsequent elements along with their joints - - mg_attributes* attributes = &backend->primitive->attributes; - - u32 eltIndex = startIndex + 1; - for(; - eltIndexjoint != MG_JOINT_NONE) - { - mg_gl_stroke_joint(backend, 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_gl_stroke_joint(backend, endPoint, endTangent, firstTangent); - } - } - else if(attributes->cap == MG_CAP_SQUARE) - { - //NOTE(martin): add start and end cap - mg_gl_stroke_cap(backend, startPoint, (vec2){-startTangent.x, -startTangent.y}); - mg_gl_stroke_cap(backend, endPoint, endTangent); - } - return(eltIndex); -} - -void mg_gl_encode_stroke(mg_gl_canvas_backend* backend, - 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_gl_encode_stroke_subpath(backend, elements, path, startIndex, startPoint); - } - } -} - -void mg_gl_grow_buffer_if_needed(GLuint buffer, i32 wantedSize, const char* name) -{ - i32 oldSize = 0; - glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer); - glGetBufferParameteriv(GL_SHADER_STORAGE_BUFFER, GL_BUFFER_SIZE, &oldSize); - - if(oldSize < wantedSize) - { - log_info("growing %s buffer\n", name); - - int newSize = wantedSize * 1.2; - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, newSize, 0, GL_DYNAMIC_COPY); - } -} - - - -void mg_gl_render_batch(mg_gl_canvas_backend* backend, - mg_wgl_surface* surface, - mg_image* images, - int tileSize, - int nTilesX, - int nTilesY, - vec2 viewportSize, - f32 scale) -{ - GLuint pathBuffer = backend->pathBuffer[backend->bufferIndex].buffer; - GLuint elementBuffer = backend->elementBuffer[backend->bufferIndex].buffer; - - int pathBufferOffset = backend->pathBatchStart * sizeof(mg_gl_path); - int elementBufferOffset = backend->eltBatchStart * sizeof(mg_gl_path_elt); - int pathCount = backend->pathCount - backend->pathBatchStart; - int eltCount = backend->eltCount - backend->eltBatchStart; - - if(!pathCount || !eltCount) - { - return; - } - - //NOTE: update intermediate buffers size if needed - //TODO: compute correct sizes - - mg_gl_grow_buffer_if_needed(backend->pathQueueBuffer, pathCount * MG_GL_PATH_QUEUE_SIZE, "path queues"); - mg_gl_grow_buffer_if_needed(backend->tileQueueBuffer, backend->maxTileQueueCount * MG_GL_TILE_QUEUE_SIZE, "tile queues"); - mg_gl_grow_buffer_if_needed(backend->segmentBuffer, backend->maxSegmentCount * MG_GL_SEGMENT_SIZE, "segments"); - mg_gl_grow_buffer_if_needed(backend->screenTilesBuffer, nTilesX * nTilesY * MG_GL_SCREEN_TILE_SIZE, "screen tiles"); - mg_gl_grow_buffer_if_needed(backend->tileOpBuffer, backend->maxSegmentCount * 30 * MG_GL_TILE_OP_SIZE, "tile ops"); - - //NOTE: make the buffers visible to gl - glBindBuffer(GL_SHADER_STORAGE_BUFFER, pathBuffer); - glFlushMappedBufferRange(GL_SHADER_STORAGE_BUFFER, pathBufferOffset, pathCount*sizeof(mg_gl_path)); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, elementBuffer); - glFlushMappedBufferRange(GL_SHADER_STORAGE_BUFFER, elementBufferOffset, eltCount*sizeof(mg_gl_path_elt)); - - //NOTE: clear out texture - u8 clearColor[4] = {0}; - glClearTexImage(backend->outTexture, 0, GL_RGBA, GL_BYTE, clearColor); - - //NOTE: clear counters - int zero = 0; - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->segmentCountBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), &zero, GL_DYNAMIC_COPY); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileQueueCountBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), &zero, GL_DYNAMIC_COPY); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileOpCountBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), &zero, GL_DYNAMIC_COPY); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->rasterDispatchBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(mg_gl_dispatch_indirect_command), &zero, GL_DYNAMIC_COPY); - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->screenTilesCountBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), &zero, GL_DYNAMIC_COPY); - - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - - int err = glGetError(); - if(err) - { - log_error("gl error %i\n", err); - } - - //NOTE: path setup pass - int maxWorkGroupCount = 0; - glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0, &maxWorkGroupCount); - //NOTE: glDispatchCompute errors if work group count is greater _or equal_ to GL_MAX_COMPUTE_WORK_GROUP_COUNT - // so the maximum _allowed_ group count is one less. - maxWorkGroupCount--; - - glUseProgram(backend->pathSetup); - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, backend->tileQueueCountBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, backend->tileQueueBuffer); - - glUniform1i(0, tileSize); - glUniform1f(1, scale); - - for(int i=0; ipathBatchStart + i); - glUniform1i(3, i); - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, pathBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->pathQueueBuffer); - - glDispatchCompute(count, 1, 1); - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - } - - if(!err) - { - err = glGetError(); - if(err) - { - log_error("gl error %i\n", err); - } - } - - //NOTE: segment setup pass - glUseProgram(backend->segmentSetup); - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->segmentCountBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, backend->segmentBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, backend->pathQueueBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, backend->tileQueueBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, backend->tileOpCountBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, backend->tileOpBuffer); - - glUniform1f(0, scale); - glUniform1ui(1, tileSize); - - for(int i=0; ieltBatchStart + i)); - - glDispatchCompute(count, 1, 1); - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - } - - if(!err) - { - err = glGetError(); - if(err) - { - log_error("gl error %i\n", err); - } - } - - //NOTE: backprop pass - glUseProgram(backend->backprop); - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->tileQueueBuffer); - - for(int i=0; ipathQueueBuffer); - - glDispatchCompute(count, 1, 1); - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - } - - if(!err) - { - err = glGetError(); - if(err) - { - log_error("gl error %i\n", err); - } - } - - //NOTE: merge pass - glUseProgram(backend->merge); - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, pathBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->pathQueueBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, backend->tileQueueBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, backend->tileOpCountBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, backend->tileOpBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, backend->screenTilesBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, backend->screenTilesCountBuffer); - - glUniform1i(0, tileSize); - glUniform1f(1, scale); - glUniform1i(2, pathCount); - glUniform1i(3, backend->pathBatchStart); - - glDispatchCompute(nTilesX, nTilesY, 1); - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - - if(!err) - { - err = glGetError(); - if(err) - { - log_error("gl error %i\n", err); - } - } - - //NOTE: balance work groups - glUseProgram(backend->balanceWorkgroups); - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, backend->screenTilesCountBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->rasterDispatchBuffer); - glUniform1ui(0, maxWorkGroupCount); - - glDispatchCompute(1, 1, 1); - glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); - - //NOTE: raster pass - glUseProgram(backend->raster); - - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, pathBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->segmentBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, backend->tileOpBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, backend->screenTilesBuffer); - glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, backend->screenTilesCountBuffer); - - glUniform1f(0, scale); - glUniform1i(1, backend->msaaCount); - - glBindImageTexture(0, backend->outTexture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8); - - for(int i=0; itexture); - } - } - } - - glUniform1i(2, backend->pathBatchStart); - glUniform1ui(3, maxWorkGroupCount); - - glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, backend->rasterDispatchBuffer); - glDispatchComputeIndirect(0); - - glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT); - - if(!err) - { - err = glGetError(); - if(err) - { - log_error("gl error %i\n", err); - } - } - - //NOTE: blit pass - glUseProgram(backend->blit); - glBindBuffer(GL_ARRAY_BUFFER, backend->dummyVertexBuffer); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, backend->outTexture); - glUniform1i(0, 0); - - glDrawArrays(GL_TRIANGLES, 0, 6); - - if(!err) - { - err = glGetError(); - if(err) - { - log_error("gl error %i\n", err); - } - } - - backend->pathBatchStart = backend->pathCount; - backend->eltBatchStart = backend->eltCount; - - backend->maxSegmentCount = 0; - backend->maxTileQueueCount = 0; -} - -void mg_gl_canvas_resize(mg_gl_canvas_backend* backend, vec2 size) -{ - int tileSize = MG_GL_TILE_SIZE; - int nTilesX = (int)(size.x + tileSize - 1)/tileSize; - int nTilesY = (int)(size.y + tileSize - 1)/tileSize; - - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->screenTilesBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, nTilesX*nTilesY*MG_GL_SCREEN_TILE_SIZE, 0, GL_DYNAMIC_COPY); - - if(backend->outTexture) - { - //NOTE: do we need to explicitly glDeleteTextures()? - glDeleteTextures(1, &backend->outTexture); - glGenTextures(1, &backend->outTexture); - glBindTexture(GL_TEXTURE_2D, backend->outTexture); - 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); - } - - backend->frameSize = size; -} - -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; - - //NOTE: roll input buffers - backend->bufferIndex = (backend->bufferIndex + 1) % MG_GL_INPUT_BUFFERS_COUNT; - if(backend->bufferSync[backend->bufferIndex] != 0) - { - glClientWaitSync(backend->bufferSync[backend->bufferIndex], GL_SYNC_FLUSH_COMMANDS_BIT, 0xffffffff); - glDeleteSync(backend->bufferSync[backend->bufferIndex]); - backend->bufferSync[backend->bufferIndex] = 0; - } - - //NOTE update screen tiles buffer size - mg_wgl_surface* surface = backend->surface; - vec2 surfaceSize = surface->interface.getSize((mg_surface_data*)surface); - vec2 contentsScaling = surface->interface.contentsScaling((mg_surface_data*)surface); - //TODO support scaling in both axes? - f32 scale = contentsScaling.x; - - vec2 viewportSize = {surfaceSize.x * scale, surfaceSize.y * scale}; - int tileSize = MG_GL_TILE_SIZE; - int nTilesX = (int)(viewportSize.x + tileSize - 1)/tileSize; - int nTilesY = (int)(viewportSize.y + tileSize - 1)/tileSize; - - if(viewportSize.x != backend->frameSize.x || viewportSize.y != backend->frameSize.y) - { - mg_gl_canvas_resize(backend, viewportSize); - } - - glViewport(0, 0, viewportSize.x, viewportSize.y); - - //NOTE: clear screen and reset input buffer offsets - glEnable(GL_BLEND); - glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); - - glClearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a); - glClear(GL_COLOR_BUFFER_BIT); - - backend->pathCount = 0; - backend->pathBatchStart = 0; - backend->eltCount = 0; - backend->eltBatchStart = 0; - backend->maxSegmentCount = 0; - backend->maxTileQueueCount = 0; - - //NOTE: encode and render batches - vec2 currentPos = {0}; - mg_image images[MG_GL_MAX_IMAGES_PER_BATCH] = {0}; - int imageCount = 0; - backend->eltCount = 0; - - for(int primitiveIndex = 0; primitiveIndex < primitiveCount; primitiveIndex++) - { - mg_primitive* primitive = &primitives[primitiveIndex]; - - if(primitive->attributes.image.h != 0) - { - backend->currentImageIndex = -1; - for(int i=0; iattributes.image.h) - { - backend->currentImageIndex = i; - } - } - if(backend->currentImageIndex <= 0) - { - if(imageCountattributes.image; - backend->currentImageIndex = imageCount; - imageCount++; - } - else - { - mg_gl_render_batch(backend, - surface, - images, - tileSize, - nTilesX, - nTilesY, - viewportSize, - scale); - - images[0] = primitive->attributes.image; - backend->currentImageIndex = 0; - imageCount = 1; - } - } - } - else - { - backend->currentImageIndex = -1; - } - - if(primitive->path.count) - { - backend->primitive = primitive; - backend->pathScreenExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; - backend->pathUserExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; - - if(primitive->cmd == MG_CMD_STROKE) - { - mg_gl_encode_stroke(backend, pathElements + primitive->path.startIndex, &primitive->path); - } - else - { - int segCount = 0; - for(int eltIndex = 0; - (eltIndex < primitive->path.count) && (primitive->path.startIndex + eltIndex < eltCount); - eltIndex++) - { - 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_gl_canvas_encode_element(backend, 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_gl_canvas_encode_path(backend, primitive, scale); - } - } - - mg_gl_render_batch(backend, - surface, - images, - tileSize, - nTilesX, - nTilesY, - viewportSize, - scale); - - //NOTE: add fence for rolling input buffers - backend->bufferSync[backend->bufferIndex] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); -} - -//-------------------------------------------------------------------- -// Image API -//-------------------------------------------------------------------- -mg_image_data* mg_gl_canvas_image_create(mg_canvas_backend* interface, vec2 size) -{ - mg_gl_image* image = 0; - - image = malloc_type(mg_gl_image); - if(image) - { - glGenTextures(1, &image->texture); - glBindTexture(GL_TEXTURE_2D, image->texture); - 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 backend - mg_gl_image* image = (mg_gl_image*)imageInterface; - glDeleteTextures(1, &image->texture); - 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 backend - mg_gl_image* image = (mg_gl_image*)imageInterface; - glBindTexture(GL_TEXTURE_2D, image->texture); - glTexSubImage2D(GL_TEXTURE_2D, 0, region.x, region.y, region.w, region.h, 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; - - //////////////////////////////////////////////////////////////////// - //TODO - //////////////////////////////////////////////////////////////////// - - free(backend); -} - -static int mg_gl_compile_shader(const char* name, GLuint shader, const char* source) -{ - int res = 0; - - const char* sources[3] = {"#version 430", glsl_common, source}; - - glShaderSource(shader, 3, sources, 0); - glCompileShader(shader); - - int status = 0; - glGetShaderiv(shader, GL_COMPILE_STATUS, &status); - if(!status) - { - char buffer[256]; - int size = 0; - glGetShaderInfoLog(shader, 256, &size, buffer); - printf("Shader compile error (%s): %.*s\n", name, size, buffer); - res = -1; - } - return(res); -} - -static int mg_gl_canvas_compile_compute_program_named(const char* name, const char* source, GLuint* outProgram) -{ - int res = 0; - *outProgram = 0; - - GLuint shader = glCreateShader(GL_COMPUTE_SHADER); - GLuint program = glCreateProgram(); - - res |= mg_gl_compile_shader(name, shader, source); - - if(!res) - { - glAttachShader(program, shader); - glLinkProgram(program); - - int status = 0; - glGetProgramiv(program, GL_LINK_STATUS, &status); - if(!status) - { - char buffer[256]; - int size = 0; - glGetProgramInfoLog(program, 256, &size, buffer); - log_error("Shader link error (%s): %.*s\n", name, size, buffer); - - res = -1; - } - else - { - *outProgram = program; - } - } - return(res); -} - -int mg_gl_canvas_compile_render_program_named(const char* progName, - const char* vertexName, - const char* fragmentName, - const char* vertexSrc, - const char* fragmentSrc, - GLuint* outProgram) -{ - int res = 0; - *outProgram = 0; - - GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); - GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); - GLuint program = glCreateProgram(); - - res |= mg_gl_compile_shader(vertexName, vertexShader, vertexSrc); - res |= mg_gl_compile_shader(fragmentName, fragmentShader, fragmentSrc); - - if(!res) - { - glAttachShader(program, vertexShader); - glAttachShader(program, fragmentShader); - glLinkProgram(program); - - int status = 0; - glGetProgramiv(program, GL_LINK_STATUS, &status); - if(!status) - { - char buffer[256]; - int size = 0; - glGetProgramInfoLog(program, 256, &size, buffer); - log_error("Shader link error (%s): %.*s\n", progName, size, buffer); - res = -1; - } - else - { - *outProgram = program; - } - } - return(res); -} - -#define mg_gl_canvas_compile_compute_program(src, out) \ - mg_gl_canvas_compile_compute_program_named(#src, src, out) - -#define mg_gl_canvas_compile_render_program(progName, shaderSrc, vertexSrc, out) \ - mg_gl_canvas_compile_render_program_named(progName, #shaderSrc, #vertexSrc, shaderSrc, vertexSrc, out) - -const u32 MG_GL_PATH_BUFFER_SIZE = (4<<10)*sizeof(mg_gl_path), - MG_GL_ELEMENT_BUFFER_SIZE = (4<<12)*sizeof(mg_gl_path_elt), - MG_GL_SEGMENT_BUFFER_SIZE = (4<<10)*MG_GL_SEGMENT_SIZE, - MG_GL_PATH_QUEUE_BUFFER_SIZE = (4<<10)*MG_GL_PATH_QUEUE_SIZE, - MG_GL_TILE_QUEUE_BUFFER_SIZE = (4<<10)*MG_GL_TILE_QUEUE_SIZE, - MG_GL_TILE_OP_BUFFER_SIZE = (4<<20)*MG_GL_TILE_OP_SIZE; - -mg_canvas_backend* gl_canvas_backend_create(mg_wgl_surface* surface) -{ - mg_gl_canvas_backend* backend = malloc_type(mg_gl_canvas_backend); - if(backend) - { - memset(backend, 0, sizeof(mg_gl_canvas_backend)); - backend->surface = surface; - - backend->msaaCount = MG_GL_MSAA_COUNT; - - //NOTE(martin): setup interface functions - backend->interface.destroy = mg_gl_canvas_destroy; - backend->interface.render = mg_gl_canvas_render; - backend->interface.imageCreate = mg_gl_canvas_image_create; - backend->interface.imageDestroy = mg_gl_canvas_image_destroy; - backend->interface.imageUploadRegion = mg_gl_canvas_image_upload_region; - - surface->interface.prepare((mg_surface_data*)surface); - - glGenVertexArrays(1, &backend->vao); - glBindVertexArray(backend->vao); - - //NOTE: create programs - int err = 0; - err |= mg_gl_canvas_compile_compute_program(glsl_path_setup, &backend->pathSetup); - err |= mg_gl_canvas_compile_compute_program(glsl_segment_setup, &backend->segmentSetup); - err |= mg_gl_canvas_compile_compute_program(glsl_backprop, &backend->backprop); - err |= mg_gl_canvas_compile_compute_program(glsl_merge, &backend->merge); - err |= mg_gl_canvas_compile_compute_program(glsl_balance_workgroups, &backend->balanceWorkgroups); - err |= mg_gl_canvas_compile_compute_program(glsl_raster, &backend->raster); - err |= mg_gl_canvas_compile_render_program("blit", glsl_blit_vertex, glsl_blit_fragment, &backend->blit); - - if(glGetError() != GL_NO_ERROR) - { - err |= -1; - } - - //NOTE: create out texture - vec2 size = surface->interface.getSize((mg_surface_data*)surface); - vec2 scale = surface->interface.contentsScaling((mg_surface_data*)surface); - - backend->frameSize = (vec2){size.x * scale.x, size.y * scale.y}; - - glGenTextures(1, &backend->outTexture); - glBindTexture(GL_TEXTURE_2D, backend->outTexture); - glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, backend->frameSize.x, backend->frameSize.y); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - //NOTE: generate buffers - glGenBuffers(1, &backend->dummyVertexBuffer); - glBindBuffer(GL_ARRAY_BUFFER, backend->dummyVertexBuffer); - - for(int i=0; ipathBuffer[i].buffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->pathBuffer[i].buffer); - glBufferStorage(GL_SHADER_STORAGE_BUFFER, MG_GL_PATH_BUFFER_SIZE, 0, GL_MAP_WRITE_BIT|GL_MAP_PERSISTENT_BIT); - backend->pathBuffer[i].size = MG_GL_PATH_BUFFER_SIZE; - backend->pathBuffer[i].contents = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, - 0, - MG_GL_PATH_BUFFER_SIZE, - GL_MAP_WRITE_BIT - |GL_MAP_PERSISTENT_BIT - |GL_MAP_FLUSH_EXPLICIT_BIT); - - glGenBuffers(1, &backend->elementBuffer[i].buffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->elementBuffer[i].buffer); - glBufferStorage(GL_SHADER_STORAGE_BUFFER, MG_GL_ELEMENT_BUFFER_SIZE, 0, GL_MAP_WRITE_BIT|GL_MAP_PERSISTENT_BIT); - backend->elementBuffer[i].size = MG_GL_ELEMENT_BUFFER_SIZE; - backend->elementBuffer[i].contents = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, - 0, - MG_GL_ELEMENT_BUFFER_SIZE, - GL_MAP_WRITE_BIT - |GL_MAP_PERSISTENT_BIT - |GL_MAP_FLUSH_EXPLICIT_BIT); - } - - glGenBuffers(1, &backend->segmentBuffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->segmentBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_SEGMENT_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); - - glGenBuffers(1, &backend->segmentCountBuffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->segmentCountBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), 0, GL_DYNAMIC_COPY); - - glGenBuffers(1, &backend->pathQueueBuffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->pathQueueBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_PATH_QUEUE_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); - - glGenBuffers(1, &backend->tileQueueBuffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileQueueBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_TILE_QUEUE_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); - - glGenBuffers(1, &backend->tileQueueCountBuffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileQueueCountBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), 0, GL_DYNAMIC_COPY); - - glGenBuffers(1, &backend->tileOpBuffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileOpBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_TILE_OP_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); - - glGenBuffers(1, &backend->tileOpCountBuffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileOpCountBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), 0, GL_DYNAMIC_COPY); - - int tileSize = MG_GL_TILE_SIZE; - int nTilesX = (int)(backend->frameSize.x + tileSize - 1)/tileSize; - int nTilesY = (int)(backend->frameSize.y + tileSize - 1)/tileSize; - - glGenBuffers(1, &backend->screenTilesBuffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->screenTilesBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, nTilesX*nTilesY*MG_GL_SCREEN_TILE_SIZE, 0, GL_DYNAMIC_COPY); - - glGenBuffers(1, &backend->screenTilesCountBuffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->screenTilesCountBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), 0, GL_DYNAMIC_COPY); - - glGenBuffers(1, &backend->rasterDispatchBuffer); - glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->rasterDispatchBuffer); - glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(mg_gl_dispatch_indirect_command), 0, GL_DYNAMIC_COPY); - - if(err) - { - mg_gl_canvas_destroy((mg_canvas_backend*)backend); - backend = 0; - } - } - 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); -} +/************************************************************//** +* +* @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_image +{ + mg_image_data interface; + GLuint texture; +} mg_gl_image; + +enum _mg_gl_cmd { + MG_GL_FILL, + MG_GL_STROKE, +}; +typedef int mg_gl_cmd; + +typedef struct mg_gl_path +{ + float uvTransform[12]; + vec4 color; + vec4 box; + vec4 clip; + mg_gl_cmd cmd; + int textureID; + u8 pad[8]; +} mg_gl_path; + +enum _mg_gl_seg_kind{ + MG_GL_LINE = 1, + MG_GL_QUADRATIC, + MG_GL_CUBIC, +}; +typedef int mg_gl_seg_kind; + +typedef struct mg_gl_path_elt +{ + vec2 p[4]; + int pathIndex; + mg_gl_seg_kind kind; + +} mg_gl_path_elt; + +enum { + LAYOUT_PATH_SIZE = sizeof(mg_gl_path), + LAYOUT_PATH_ELT_SIZE = sizeof(mg_gl_path_elt), +}; + + +typedef struct mg_gl_dispatch_indirect_command +{ + u32 num_groups_x; + u32 num_groups_y; + u32 num_groups_z; + +} mg_gl_dispatch_indirect_command; +//////////////////////////////////////////////////////////// +//NOTE: these are just here for the sizes... + +#define MG_GL_LAYOUT_FIRST(name, type) \ + MG_GL_##name##_OFFSET = 0, \ + MG_GL_##name##_SIZE = MG_GL_##type##_SIZE, + +#define MG_GL_LAYOUT_NEXT(name, type, prev) \ + MG_GL_##name##_OFFSET = AlignUpOnPow2(MG_GL_##prev##_OFFSET + MG_GL_##prev##_SIZE, MG_GL_##type##_ALIGN), \ + MG_GL_##name##_SIZE = MG_GL_##type##_SIZE, + +#define MG_GL_LAYOUT_SIZE(name, last, maxAlignType) \ + MG_GL_##name##_ALIGN = AlignUpOnPow2(MG_GL_##maxAlignType##_ALIGN, MG_GL_VEC4_ALIGN), \ + MG_GL_##name##_SIZE = AlignUpOnPow2(MG_GL_##last##_OFFSET + MG_GL_##last##_SIZE, MG_GL_##name##_ALIGN), + +enum +{ + MG_GL_I32_SIZE = sizeof(i32), + MG_GL_I32_ALIGN = sizeof(i32), + MG_GL_F32_SIZE = sizeof(f32), + MG_GL_F32_ALIGN = sizeof(f32), + MG_GL_VEC2_SIZE = 2*sizeof(f32), + MG_GL_VEC2_ALIGN = 2*sizeof(f32), + MG_GL_VEC3_SIZE = 4*sizeof(f32), + MG_GL_VEC3_ALIGN = 4*sizeof(f32), + MG_GL_VEC4_SIZE = 4*sizeof(f32), + MG_GL_VEC4_ALIGN = 4*sizeof(f32), + MG_GL_MAT3_SIZE = 3*3*MG_GL_VEC3_SIZE, + MG_GL_MAT3_ALIGN = MG_GL_VEC3_ALIGN, + + MG_GL_LAYOUT_FIRST(SEGMENT_KIND, I32) + MG_GL_LAYOUT_NEXT(SEGMENT_PATH_INDEX, I32, SEGMENT_KIND) + MG_GL_LAYOUT_NEXT(SEGMENT_CONFIG, I32, SEGMENT_PATH_INDEX) + MG_GL_LAYOUT_NEXT(SEGMENT_WINDING, I32, SEGMENT_CONFIG) + MG_GL_LAYOUT_NEXT(SEGMENT_BOX, VEC4, SEGMENT_WINDING) + MG_GL_LAYOUT_NEXT(SEGMENT_IMPLICIT_MATRIX, MAT3, SEGMENT_BOX) + MG_GL_LAYOUT_NEXT(SEGMENT_HULL_VERTEX, VEC2, SEGMENT_IMPLICIT_MATRIX) + MG_GL_LAYOUT_NEXT(SEGMENT_SIGN, F32, SEGMENT_HULL_VERTEX) + MG_GL_LAYOUT_SIZE(SEGMENT, SEGMENT_SIGN, MAT3) + + MG_GL_LAYOUT_FIRST(PATH_QUEUE_AREA, VEC4) + MG_GL_LAYOUT_NEXT(PATH_QUEUE_TILE_QUEUES, I32, PATH_QUEUE_AREA) + MG_GL_LAYOUT_SIZE(PATH_QUEUE, PATH_QUEUE_TILE_QUEUES, VEC4) + + MG_GL_LAYOUT_FIRST(TILE_OP_KIND, I32) + MG_GL_LAYOUT_NEXT(TILE_OP_NEXT, I32, TILE_OP_KIND) + MG_GL_LAYOUT_NEXT(TILE_OP_INDEX, I32, TILE_OP_NEXT) + MG_GL_LAYOUT_NEXT(TILE_OP_WINDING, I32, TILE_OP_INDEX) + MG_GL_LAYOUT_SIZE(TILE_OP, TILE_OP_WINDING, I32) + + MG_GL_LAYOUT_FIRST(TILE_QUEUE_WINDING, I32) + MG_GL_LAYOUT_NEXT(TILE_QUEUE_FIRST, I32, TILE_QUEUE_WINDING) + MG_GL_LAYOUT_NEXT(TILE_QUEUE_LAST, I32, TILE_QUEUE_FIRST) + MG_GL_LAYOUT_SIZE(TILE_QUEUE, TILE_QUEUE_LAST, I32) + + MG_GL_LAYOUT_FIRST(SCREEN_TILE_COORD, VEC2) + MG_GL_LAYOUT_NEXT(SCREEN_TILE_FIRST, I32, SCREEN_TILE_COORD) + MG_GL_LAYOUT_SIZE(SCREEN_TILE, SCREEN_TILE_FIRST, VEC2) +}; + +enum { + MG_GL_INPUT_BUFFERS_COUNT = 3, + MG_GL_TILE_SIZE = 16, + MG_GL_MSAA_COUNT = 8, + MG_GL_MAX_IMAGES_PER_BATCH = 8, +}; + +typedef struct mg_gl_mapped_buffer +{ + GLuint buffer; + int size; + char* contents; +} mg_gl_mapped_buffer; + +typedef struct mg_gl_canvas_backend +{ + mg_canvas_backend interface; + mg_wgl_surface* surface; + + int msaaCount; + vec2 frameSize; + + // gl stuff + GLuint vao; + + GLuint pathSetup; + GLuint segmentSetup; + GLuint backprop; + GLuint merge; + GLuint balanceWorkgroups; + GLuint raster; + GLuint blit; + + GLuint outTexture; + + int bufferIndex; + GLsync bufferSync[MG_GL_INPUT_BUFFERS_COUNT]; + mg_gl_mapped_buffer pathBuffer[MG_GL_INPUT_BUFFERS_COUNT]; + mg_gl_mapped_buffer elementBuffer[MG_GL_INPUT_BUFFERS_COUNT]; + + GLuint segmentBuffer; + GLuint segmentCountBuffer; + GLuint pathQueueBuffer; + GLuint tileQueueBuffer; + GLuint tileQueueCountBuffer; + GLuint tileOpBuffer; + GLuint tileOpCountBuffer; + GLuint screenTilesBuffer; + GLuint screenTilesCountBuffer; + GLuint rasterDispatchBuffer; + GLuint dummyVertexBuffer; + + //encoding context + int pathCount; + int eltCount; + + int pathBatchStart; + int eltBatchStart; + + mg_primitive* primitive; + vec4 pathScreenExtents; + vec4 pathUserExtents; + + int maxTileQueueCount; + int maxSegmentCount; + + int currentImageIndex; +} mg_gl_canvas_backend; + +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_gl_grow_input_buffer(mg_gl_mapped_buffer* buffer, int copyStart, int copySize, int newSize) +{ + mg_gl_mapped_buffer newBuffer = {0}; + newBuffer.size = newSize; + glGenBuffers(1, &newBuffer.buffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, newBuffer.buffer); + glBufferStorage(GL_SHADER_STORAGE_BUFFER, newBuffer.size, 0, GL_MAP_WRITE_BIT|GL_MAP_PERSISTENT_BIT); + newBuffer.contents = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, + 0, + newBuffer.size, + GL_MAP_WRITE_BIT + |GL_MAP_PERSISTENT_BIT + |GL_MAP_FLUSH_EXPLICIT_BIT); + + memcpy(newBuffer.contents + copyStart, buffer->contents + copyStart, copySize); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer->buffer); + glUnmapBuffer(GL_SHADER_STORAGE_BUFFER); + glDeleteBuffers(1, &buffer->buffer); + + *buffer = newBuffer; +} + +void mg_gl_canvas_encode_element(mg_gl_canvas_backend* backend, mg_path_elt_type kind, vec2* p) +{ + int bufferIndex = backend->bufferIndex; + int bufferCap = backend->elementBuffer[bufferIndex].size / sizeof(mg_gl_path_elt); + if(backend->eltCount >= bufferCap) + { + int newBufferCap = (int)(bufferCap * 1.5); + int newBufferSize = newBufferCap * sizeof(mg_gl_path_elt); + + log_info("growing element buffer to %i elements\n", newBufferCap); + + mg_gl_grow_input_buffer(&backend->elementBuffer[bufferIndex], + backend->eltBatchStart * sizeof(mg_gl_path_elt), + backend->eltCount * sizeof(mg_gl_path_elt), + newBufferSize); + } + + mg_gl_path_elt* elementData = (mg_gl_path_elt*)backend->elementBuffer[bufferIndex].contents; + mg_gl_path_elt* elt = &elementData[backend->eltCount]; + backend->eltCount++; + + elt->pathIndex = backend->pathCount - backend->pathBatchStart; + int count = 0; + switch(kind) + { + case MG_PATH_LINE: + backend->maxSegmentCount += 1; + elt->kind = MG_GL_LINE; + count = 2; + break; + + case MG_PATH_QUADRATIC: + backend->maxSegmentCount += 3; + elt->kind = MG_GL_QUADRATIC; + count = 3; + break; + + case MG_PATH_CUBIC: + backend->maxSegmentCount += 7; + elt->kind = MG_GL_CUBIC; + count = 4; + break; + + default: + break; + } + + for(int i=0; ipathUserExtents, p[i]); + + vec2 screenP = mg_mat2x3_mul(backend->primitive->attributes.transform, p[i]); + elt->p[i] = (vec2){screenP.x, screenP.y}; + + mg_update_path_extents(&backend->pathScreenExtents, screenP); + } +} + +void mg_gl_canvas_encode_path(mg_gl_canvas_backend* backend, mg_primitive* primitive, f32 scale) +{ + int bufferIndex = backend->bufferIndex; + int bufferCap = backend->pathBuffer[bufferIndex].size / sizeof(mg_gl_path); + if(backend->pathCount >= bufferCap) + { + int newBufferCap = (int)(bufferCap * 1.5); + int newBufferSize = newBufferCap * sizeof(mg_gl_path); + + log_info("growing path buffer to %i elements\n", newBufferCap); + + mg_gl_grow_input_buffer(&backend->pathBuffer[bufferIndex], + backend->pathBatchStart * sizeof(mg_gl_path), + backend->eltCount * sizeof(mg_gl_path), + newBufferSize); + } + + mg_gl_path* pathData = (mg_gl_path*)backend->pathBuffer[backend->bufferIndex].contents; + mg_gl_path* path = &pathData[backend->pathCount]; + backend->pathCount++; + + path->cmd = (mg_gl_cmd)primitive->cmd; + + path->box = (vec4){ + backend->pathScreenExtents.x, + backend->pathScreenExtents.y, + backend->pathScreenExtents.z, + backend->pathScreenExtents.w}; + + path->clip = (vec4){ + 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 = (vec4){ + 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 = { + backend->pathUserExtents.x, + backend->pathUserExtents.y, + backend->pathUserExtents.z - backend->pathUserExtents.x, + backend->pathUserExtents.w - backend->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); + + //NOTE: mat3 std430 layout is an array of vec3, which are padded to _vec4_ alignment + path->uvTransform[0] = uvTransform.m[0]/scale; + path->uvTransform[1] = uvTransform.m[3]/scale; + path->uvTransform[2] = 0; + path->uvTransform[3] = 0; + path->uvTransform[4] = uvTransform.m[1]/scale; + path->uvTransform[5] = uvTransform.m[4]/scale; + path->uvTransform[6] = 0; + path->uvTransform[7] = 0; + path->uvTransform[8] = uvTransform.m[2]; + path->uvTransform[9] = uvTransform.m[5]; + path->uvTransform[10] = 1; + path->uvTransform[11] = 0; + + path->textureID = backend->currentImageIndex; + } + else + { + path->textureID = -1; + } + + int nTilesX = ((path->box.z - path->box.x)*scale - 1) / MG_GL_TILE_SIZE + 1; + int nTilesY = ((path->box.w - path->box.y)*scale - 1) / MG_GL_TILE_SIZE + 1; + backend->maxTileQueueCount += (nTilesX * nTilesY); +} + +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_gl_canvas_encode_element(backend, MG_PATH_LINE, right); + + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, left); + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, joint0); + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, joint1); +} + +enum { MG_HULL_CHECK_SAMPLE_COUNT = 5 }; + +void mg_gl_encode_stroke_quadratic(mg_gl_canvas_backend* backend, vec2* p) +{ + f32 width = backend->primitive->attributes.width; + f32 tolerance = minimum(backend->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_gl_encode_stroke_line(backend, p+1); + return; + } + else if(vec2_close(p[1], p[2], equalEps)) + { + mg_gl_encode_stroke_line(backend, 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_gl_encode_stroke_quadratic(backend, splitLeft); + mg_gl_encode_stroke_quadratic(backend, splitRight); + } + else + { + f32 checkSamples[MG_HULL_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_gl_encode_stroke_quadratic(backend, splitLeft); + mg_gl_encode_stroke_quadratic(backend, splitRight); + } + else + { + vec2 tmp = leftHull[0]; + leftHull[0] = leftHull[2]; + leftHull[2] = tmp; + + mg_gl_canvas_encode_element(backend, MG_PATH_QUADRATIC, rightHull); + mg_gl_canvas_encode_element(backend, MG_PATH_QUADRATIC, leftHull); + + vec2 joint0[2] = {rightHull[2], leftHull[0]}; + vec2 joint1[2] = {leftHull[2], rightHull[0]}; + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, joint0); + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, joint1); + } + } +} + +void mg_gl_encode_stroke_cubic(mg_gl_canvas_backend* backend, vec2* p) +{ + f32 width = backend->primitive->attributes.width; + f32 tolerance = minimum(backend->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_gl_encode_stroke_line(backend, 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_gl_encode_stroke_line(backend, 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_gl_encode_stroke_line(backend, 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_gl_encode_stroke_cubic(backend, splitLeft); + mg_gl_encode_stroke_cubic(backend, splitRight); + } + else + { + f32 checkSamples[MG_HULL_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_gl_encode_stroke_cubic(backend, splitLeft); + mg_gl_encode_stroke_cubic(backend, splitRight); + } + else + { + vec2 tmp = leftHull[0]; + leftHull[0] = leftHull[3]; + leftHull[3] = tmp; + tmp = leftHull[1]; + leftHull[1] = leftHull[2]; + leftHull[2] = tmp; + + mg_gl_canvas_encode_element(backend, MG_PATH_CUBIC, rightHull); + mg_gl_canvas_encode_element(backend, MG_PATH_CUBIC, leftHull); + + vec2 joint0[2] = {rightHull[3], leftHull[0]}; + vec2 joint1[2] = {leftHull[3], rightHull[0]}; + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, joint0); + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, joint1); + } + } +} + +void mg_gl_encode_stroke_element(mg_gl_canvas_backend* backend, + 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_gl_encode_stroke_line(backend, controlPoints); + endPointIndex = 1; + break; + + case MG_PATH_QUADRATIC: + mg_gl_encode_stroke_quadratic(backend, controlPoints); + endPointIndex = 2; + break; + + case MG_PATH_CUBIC: + mg_gl_encode_stroke_cubic(backend, 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_gl_stroke_cap(mg_gl_canvas_backend* backend, + vec2 p0, + vec2 direction) +{ + mg_attributes* attributes = &backend->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_gl_canvas_encode_element(backend, MG_PATH_LINE, points); + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+1); + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+2); + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+3); +} + +void mg_gl_stroke_joint(mg_gl_canvas_backend* backend, + vec2 p0, + vec2 t0, + vec2 t1) +{ + mg_attributes* attributes = &backend->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_gl_canvas_encode_element(backend, MG_PATH_LINE, points); + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+1); + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+2); + mg_gl_canvas_encode_element(backend, 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_gl_canvas_encode_element(backend, MG_PATH_LINE, points); + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+1); + mg_gl_canvas_encode_element(backend, MG_PATH_LINE, points+2); + } +} + +u32 mg_gl_encode_stroke_subpath(mg_gl_canvas_backend* backend, + 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): encode first element and compute first tangent + mg_gl_encode_stroke_element(backend, elements + startIndex, currentPoint, &startTangent, &endTangent, &endPoint); + + firstTangent = startTangent; + previousEndTangent = endTangent; + currentPoint = endPoint; + + //NOTE(martin): encode subsequent elements along with their joints + + mg_attributes* attributes = &backend->primitive->attributes; + + u32 eltIndex = startIndex + 1; + for(; + eltIndexjoint != MG_JOINT_NONE) + { + mg_gl_stroke_joint(backend, 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_gl_stroke_joint(backend, endPoint, endTangent, firstTangent); + } + } + else if(attributes->cap == MG_CAP_SQUARE) + { + //NOTE(martin): add start and end cap + mg_gl_stroke_cap(backend, startPoint, (vec2){-startTangent.x, -startTangent.y}); + mg_gl_stroke_cap(backend, endPoint, endTangent); + } + return(eltIndex); +} + +void mg_gl_encode_stroke(mg_gl_canvas_backend* backend, + 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_gl_encode_stroke_subpath(backend, elements, path, startIndex, startPoint); + } + } +} + +void mg_gl_grow_buffer_if_needed(GLuint buffer, i32 wantedSize, const char* name) +{ + i32 oldSize = 0; + glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer); + glGetBufferParameteriv(GL_SHADER_STORAGE_BUFFER, GL_BUFFER_SIZE, &oldSize); + + if(oldSize < wantedSize) + { + log_info("growing %s buffer\n", name); + + int newSize = wantedSize * 1.2; + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, newSize, 0, GL_DYNAMIC_COPY); + } +} + + + +void mg_gl_render_batch(mg_gl_canvas_backend* backend, + mg_wgl_surface* surface, + mg_image* images, + int tileSize, + int nTilesX, + int nTilesY, + vec2 viewportSize, + f32 scale) +{ + GLuint pathBuffer = backend->pathBuffer[backend->bufferIndex].buffer; + GLuint elementBuffer = backend->elementBuffer[backend->bufferIndex].buffer; + + int pathBufferOffset = backend->pathBatchStart * sizeof(mg_gl_path); + int elementBufferOffset = backend->eltBatchStart * sizeof(mg_gl_path_elt); + int pathCount = backend->pathCount - backend->pathBatchStart; + int eltCount = backend->eltCount - backend->eltBatchStart; + + if(!pathCount || !eltCount) + { + return; + } + + //NOTE: update intermediate buffers size if needed + //TODO: compute correct sizes + + mg_gl_grow_buffer_if_needed(backend->pathQueueBuffer, pathCount * MG_GL_PATH_QUEUE_SIZE, "path queues"); + mg_gl_grow_buffer_if_needed(backend->tileQueueBuffer, backend->maxTileQueueCount * MG_GL_TILE_QUEUE_SIZE, "tile queues"); + mg_gl_grow_buffer_if_needed(backend->segmentBuffer, backend->maxSegmentCount * MG_GL_SEGMENT_SIZE, "segments"); + mg_gl_grow_buffer_if_needed(backend->screenTilesBuffer, nTilesX * nTilesY * MG_GL_SCREEN_TILE_SIZE, "screen tiles"); + mg_gl_grow_buffer_if_needed(backend->tileOpBuffer, backend->maxSegmentCount * 30 * MG_GL_TILE_OP_SIZE, "tile ops"); + + //NOTE: make the buffers visible to gl + glBindBuffer(GL_SHADER_STORAGE_BUFFER, pathBuffer); + glFlushMappedBufferRange(GL_SHADER_STORAGE_BUFFER, pathBufferOffset, pathCount*sizeof(mg_gl_path)); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, elementBuffer); + glFlushMappedBufferRange(GL_SHADER_STORAGE_BUFFER, elementBufferOffset, eltCount*sizeof(mg_gl_path_elt)); + + //NOTE: clear out texture + u8 clearColor[4] = {0}; + glClearTexImage(backend->outTexture, 0, GL_RGBA, GL_BYTE, clearColor); + + //NOTE: clear counters + int zero = 0; + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->segmentCountBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), &zero, GL_DYNAMIC_COPY); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileQueueCountBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), &zero, GL_DYNAMIC_COPY); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileOpCountBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), &zero, GL_DYNAMIC_COPY); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->rasterDispatchBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(mg_gl_dispatch_indirect_command), &zero, GL_DYNAMIC_COPY); + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->screenTilesCountBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), &zero, GL_DYNAMIC_COPY); + + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + int err = glGetError(); + if(err) + { + log_error("gl error %i\n", err); + } + + //NOTE: path setup pass + int maxWorkGroupCount = 0; + glGetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_COUNT, 0, &maxWorkGroupCount); + //NOTE: glDispatchCompute errors if work group count is greater _or equal_ to GL_MAX_COMPUTE_WORK_GROUP_COUNT + // so the maximum _allowed_ group count is one less. + maxWorkGroupCount--; + + glUseProgram(backend->pathSetup); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, backend->tileQueueCountBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, backend->tileQueueBuffer); + + glUniform1i(0, tileSize); + glUniform1f(1, scale); + + for(int i=0; ipathBatchStart + i); + glUniform1i(3, i); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, pathBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->pathQueueBuffer); + + glDispatchCompute(count, 1, 1); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + } + + if(!err) + { + err = glGetError(); + if(err) + { + log_error("gl error %i\n", err); + } + } + + //NOTE: segment setup pass + glUseProgram(backend->segmentSetup); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->segmentCountBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, backend->segmentBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, backend->pathQueueBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, backend->tileQueueBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, backend->tileOpCountBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, backend->tileOpBuffer); + + glUniform1f(0, scale); + glUniform1ui(1, tileSize); + + for(int i=0; ieltBatchStart + i)); + + glDispatchCompute(count, 1, 1); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + } + + if(!err) + { + err = glGetError(); + if(err) + { + log_error("gl error %i\n", err); + } + } + + //NOTE: backprop pass + glUseProgram(backend->backprop); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->tileQueueBuffer); + + for(int i=0; ipathQueueBuffer); + + glDispatchCompute(count, 1, 1); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + } + + if(!err) + { + err = glGetError(); + if(err) + { + log_error("gl error %i\n", err); + } + } + + //NOTE: merge pass + glUseProgram(backend->merge); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, pathBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->pathQueueBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, backend->tileQueueBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, backend->tileOpCountBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, backend->tileOpBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 5, backend->screenTilesBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 6, backend->screenTilesCountBuffer); + + glUniform1i(0, tileSize); + glUniform1f(1, scale); + glUniform1i(2, pathCount); + glUniform1i(3, backend->pathBatchStart); + + glDispatchCompute(nTilesX, nTilesY, 1); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + if(!err) + { + err = glGetError(); + if(err) + { + log_error("gl error %i\n", err); + } + } + + //NOTE: balance work groups + glUseProgram(backend->balanceWorkgroups); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, backend->screenTilesCountBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->rasterDispatchBuffer); + glUniform1ui(0, maxWorkGroupCount); + + glDispatchCompute(1, 1, 1); + glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT); + + //NOTE: raster pass + glUseProgram(backend->raster); + + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, pathBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, backend->segmentBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, backend->tileOpBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 3, backend->screenTilesBuffer); + glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 4, backend->screenTilesCountBuffer); + + glUniform1f(0, scale); + glUniform1i(1, backend->msaaCount); + + glBindImageTexture(0, backend->outTexture, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8); + + for(int i=0; itexture); + } + } + } + + glUniform1i(2, backend->pathBatchStart); + glUniform1ui(3, maxWorkGroupCount); + + glBindBuffer(GL_DISPATCH_INDIRECT_BUFFER, backend->rasterDispatchBuffer); + glDispatchComputeIndirect(0); + + glMemoryBarrier(GL_TEXTURE_FETCH_BARRIER_BIT); + + if(!err) + { + err = glGetError(); + if(err) + { + log_error("gl error %i\n", err); + } + } + + //NOTE: blit pass + glUseProgram(backend->blit); + glBindBuffer(GL_ARRAY_BUFFER, backend->dummyVertexBuffer); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, backend->outTexture); + glUniform1i(0, 0); + + glDrawArrays(GL_TRIANGLES, 0, 6); + + if(!err) + { + err = glGetError(); + if(err) + { + log_error("gl error %i\n", err); + } + } + + backend->pathBatchStart = backend->pathCount; + backend->eltBatchStart = backend->eltCount; + + backend->maxSegmentCount = 0; + backend->maxTileQueueCount = 0; +} + +void mg_gl_canvas_resize(mg_gl_canvas_backend* backend, vec2 size) +{ + int tileSize = MG_GL_TILE_SIZE; + int nTilesX = (int)(size.x + tileSize - 1)/tileSize; + int nTilesY = (int)(size.y + tileSize - 1)/tileSize; + + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->screenTilesBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, nTilesX*nTilesY*MG_GL_SCREEN_TILE_SIZE, 0, GL_DYNAMIC_COPY); + + if(backend->outTexture) + { + //NOTE: do we need to explicitly glDeleteTextures()? + glDeleteTextures(1, &backend->outTexture); + glGenTextures(1, &backend->outTexture); + glBindTexture(GL_TEXTURE_2D, backend->outTexture); + 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); + } + + backend->frameSize = size; +} + +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; + + //NOTE: roll input buffers + backend->bufferIndex = (backend->bufferIndex + 1) % MG_GL_INPUT_BUFFERS_COUNT; + if(backend->bufferSync[backend->bufferIndex] != 0) + { + glClientWaitSync(backend->bufferSync[backend->bufferIndex], GL_SYNC_FLUSH_COMMANDS_BIT, 0xffffffff); + glDeleteSync(backend->bufferSync[backend->bufferIndex]); + backend->bufferSync[backend->bufferIndex] = 0; + } + + //NOTE update screen tiles buffer size + mg_wgl_surface* surface = backend->surface; + vec2 surfaceSize = surface->interface.getSize((mg_surface_data*)surface); + vec2 contentsScaling = surface->interface.contentsScaling((mg_surface_data*)surface); + //TODO support scaling in both axes? + f32 scale = contentsScaling.x; + + vec2 viewportSize = {surfaceSize.x * scale, surfaceSize.y * scale}; + int tileSize = MG_GL_TILE_SIZE; + int nTilesX = (int)(viewportSize.x + tileSize - 1)/tileSize; + int nTilesY = (int)(viewportSize.y + tileSize - 1)/tileSize; + + if(viewportSize.x != backend->frameSize.x || viewportSize.y != backend->frameSize.y) + { + mg_gl_canvas_resize(backend, viewportSize); + } + + glViewport(0, 0, viewportSize.x, viewportSize.y); + + //NOTE: clear screen and reset input buffer offsets + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + glClearColor(clearColor.r, clearColor.g, clearColor.b, clearColor.a); + glClear(GL_COLOR_BUFFER_BIT); + + backend->pathCount = 0; + backend->pathBatchStart = 0; + backend->eltCount = 0; + backend->eltBatchStart = 0; + backend->maxSegmentCount = 0; + backend->maxTileQueueCount = 0; + + //NOTE: encode and render batches + vec2 currentPos = {0}; + mg_image images[MG_GL_MAX_IMAGES_PER_BATCH] = {0}; + int imageCount = 0; + backend->eltCount = 0; + + for(int primitiveIndex = 0; primitiveIndex < primitiveCount; primitiveIndex++) + { + mg_primitive* primitive = &primitives[primitiveIndex]; + + if(primitive->attributes.image.h != 0) + { + backend->currentImageIndex = -1; + for(int i=0; iattributes.image.h) + { + backend->currentImageIndex = i; + } + } + if(backend->currentImageIndex <= 0) + { + if(imageCountattributes.image; + backend->currentImageIndex = imageCount; + imageCount++; + } + else + { + mg_gl_render_batch(backend, + surface, + images, + tileSize, + nTilesX, + nTilesY, + viewportSize, + scale); + + images[0] = primitive->attributes.image; + backend->currentImageIndex = 0; + imageCount = 1; + } + } + } + else + { + backend->currentImageIndex = -1; + } + + if(primitive->path.count) + { + backend->primitive = primitive; + backend->pathScreenExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; + backend->pathUserExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; + + if(primitive->cmd == MG_CMD_STROKE) + { + mg_gl_encode_stroke(backend, pathElements + primitive->path.startIndex, &primitive->path); + } + else + { + int segCount = 0; + for(int eltIndex = 0; + (eltIndex < primitive->path.count) && (primitive->path.startIndex + eltIndex < eltCount); + eltIndex++) + { + 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_gl_canvas_encode_element(backend, 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_gl_canvas_encode_path(backend, primitive, scale); + } + } + + mg_gl_render_batch(backend, + surface, + images, + tileSize, + nTilesX, + nTilesY, + viewportSize, + scale); + + //NOTE: add fence for rolling input buffers + backend->bufferSync[backend->bufferIndex] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); +} + +//-------------------------------------------------------------------- +// Image API +//-------------------------------------------------------------------- +mg_image_data* mg_gl_canvas_image_create(mg_canvas_backend* interface, vec2 size) +{ + mg_gl_image* image = 0; + + image = malloc_type(mg_gl_image); + if(image) + { + glGenTextures(1, &image->texture); + glBindTexture(GL_TEXTURE_2D, image->texture); + 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 backend + mg_gl_image* image = (mg_gl_image*)imageInterface; + glDeleteTextures(1, &image->texture); + 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 backend + mg_gl_image* image = (mg_gl_image*)imageInterface; + glBindTexture(GL_TEXTURE_2D, image->texture); + glTexSubImage2D(GL_TEXTURE_2D, 0, region.x, region.y, region.w, region.h, 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; + + //////////////////////////////////////////////////////////////////// + //TODO + //////////////////////////////////////////////////////////////////// + + free(backend); +} + +static int mg_gl_compile_shader(const char* name, GLuint shader, const char* source) +{ + int res = 0; + + const char* sources[3] = {"#version 430", glsl_common, source}; + + glShaderSource(shader, 3, sources, 0); + glCompileShader(shader); + + int status = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &status); + if(!status) + { + char buffer[256]; + int size = 0; + glGetShaderInfoLog(shader, 256, &size, buffer); + printf("Shader compile error (%s): %.*s\n", name, size, buffer); + res = -1; + } + return(res); +} + +static int mg_gl_canvas_compile_compute_program_named(const char* name, const char* source, GLuint* outProgram) +{ + int res = 0; + *outProgram = 0; + + GLuint shader = glCreateShader(GL_COMPUTE_SHADER); + GLuint program = glCreateProgram(); + + res |= mg_gl_compile_shader(name, shader, source); + + if(!res) + { + glAttachShader(program, shader); + glLinkProgram(program); + + int status = 0; + glGetProgramiv(program, GL_LINK_STATUS, &status); + if(!status) + { + char buffer[256]; + int size = 0; + glGetProgramInfoLog(program, 256, &size, buffer); + log_error("Shader link error (%s): %.*s\n", name, size, buffer); + + res = -1; + } + else + { + *outProgram = program; + } + } + return(res); +} + +int mg_gl_canvas_compile_render_program_named(const char* progName, + const char* vertexName, + const char* fragmentName, + const char* vertexSrc, + const char* fragmentSrc, + GLuint* outProgram) +{ + int res = 0; + *outProgram = 0; + + GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); + GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); + GLuint program = glCreateProgram(); + + res |= mg_gl_compile_shader(vertexName, vertexShader, vertexSrc); + res |= mg_gl_compile_shader(fragmentName, fragmentShader, fragmentSrc); + + if(!res) + { + glAttachShader(program, vertexShader); + glAttachShader(program, fragmentShader); + glLinkProgram(program); + + int status = 0; + glGetProgramiv(program, GL_LINK_STATUS, &status); + if(!status) + { + char buffer[256]; + int size = 0; + glGetProgramInfoLog(program, 256, &size, buffer); + log_error("Shader link error (%s): %.*s\n", progName, size, buffer); + res = -1; + } + else + { + *outProgram = program; + } + } + return(res); +} + +#define mg_gl_canvas_compile_compute_program(src, out) \ + mg_gl_canvas_compile_compute_program_named(#src, src, out) + +#define mg_gl_canvas_compile_render_program(progName, shaderSrc, vertexSrc, out) \ + mg_gl_canvas_compile_render_program_named(progName, #shaderSrc, #vertexSrc, shaderSrc, vertexSrc, out) + +const u32 MG_GL_PATH_BUFFER_SIZE = (4<<10)*sizeof(mg_gl_path), + MG_GL_ELEMENT_BUFFER_SIZE = (4<<12)*sizeof(mg_gl_path_elt), + MG_GL_SEGMENT_BUFFER_SIZE = (4<<10)*MG_GL_SEGMENT_SIZE, + MG_GL_PATH_QUEUE_BUFFER_SIZE = (4<<10)*MG_GL_PATH_QUEUE_SIZE, + MG_GL_TILE_QUEUE_BUFFER_SIZE = (4<<10)*MG_GL_TILE_QUEUE_SIZE, + MG_GL_TILE_OP_BUFFER_SIZE = (4<<20)*MG_GL_TILE_OP_SIZE; + +mg_canvas_backend* gl_canvas_backend_create(mg_wgl_surface* surface) +{ + mg_gl_canvas_backend* backend = malloc_type(mg_gl_canvas_backend); + if(backend) + { + memset(backend, 0, sizeof(mg_gl_canvas_backend)); + backend->surface = surface; + + backend->msaaCount = MG_GL_MSAA_COUNT; + + //NOTE(martin): setup interface functions + backend->interface.destroy = mg_gl_canvas_destroy; + backend->interface.render = mg_gl_canvas_render; + backend->interface.imageCreate = mg_gl_canvas_image_create; + backend->interface.imageDestroy = mg_gl_canvas_image_destroy; + backend->interface.imageUploadRegion = mg_gl_canvas_image_upload_region; + + surface->interface.prepare((mg_surface_data*)surface); + + glGenVertexArrays(1, &backend->vao); + glBindVertexArray(backend->vao); + + //NOTE: create programs + int err = 0; + err |= mg_gl_canvas_compile_compute_program(glsl_path_setup, &backend->pathSetup); + err |= mg_gl_canvas_compile_compute_program(glsl_segment_setup, &backend->segmentSetup); + err |= mg_gl_canvas_compile_compute_program(glsl_backprop, &backend->backprop); + err |= mg_gl_canvas_compile_compute_program(glsl_merge, &backend->merge); + err |= mg_gl_canvas_compile_compute_program(glsl_balance_workgroups, &backend->balanceWorkgroups); + err |= mg_gl_canvas_compile_compute_program(glsl_raster, &backend->raster); + err |= mg_gl_canvas_compile_render_program("blit", glsl_blit_vertex, glsl_blit_fragment, &backend->blit); + + if(glGetError() != GL_NO_ERROR) + { + err |= -1; + } + + //NOTE: create out texture + vec2 size = surface->interface.getSize((mg_surface_data*)surface); + vec2 scale = surface->interface.contentsScaling((mg_surface_data*)surface); + + backend->frameSize = (vec2){size.x * scale.x, size.y * scale.y}; + + glGenTextures(1, &backend->outTexture); + glBindTexture(GL_TEXTURE_2D, backend->outTexture); + glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, backend->frameSize.x, backend->frameSize.y); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + //NOTE: generate buffers + glGenBuffers(1, &backend->dummyVertexBuffer); + glBindBuffer(GL_ARRAY_BUFFER, backend->dummyVertexBuffer); + + for(int i=0; ipathBuffer[i].buffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->pathBuffer[i].buffer); + glBufferStorage(GL_SHADER_STORAGE_BUFFER, MG_GL_PATH_BUFFER_SIZE, 0, GL_MAP_WRITE_BIT|GL_MAP_PERSISTENT_BIT); + backend->pathBuffer[i].size = MG_GL_PATH_BUFFER_SIZE; + backend->pathBuffer[i].contents = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, + 0, + MG_GL_PATH_BUFFER_SIZE, + GL_MAP_WRITE_BIT + |GL_MAP_PERSISTENT_BIT + |GL_MAP_FLUSH_EXPLICIT_BIT); + + glGenBuffers(1, &backend->elementBuffer[i].buffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->elementBuffer[i].buffer); + glBufferStorage(GL_SHADER_STORAGE_BUFFER, MG_GL_ELEMENT_BUFFER_SIZE, 0, GL_MAP_WRITE_BIT|GL_MAP_PERSISTENT_BIT); + backend->elementBuffer[i].size = MG_GL_ELEMENT_BUFFER_SIZE; + backend->elementBuffer[i].contents = glMapBufferRange(GL_SHADER_STORAGE_BUFFER, + 0, + MG_GL_ELEMENT_BUFFER_SIZE, + GL_MAP_WRITE_BIT + |GL_MAP_PERSISTENT_BIT + |GL_MAP_FLUSH_EXPLICIT_BIT); + } + + glGenBuffers(1, &backend->segmentBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->segmentBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_SEGMENT_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); + + glGenBuffers(1, &backend->segmentCountBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->segmentCountBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), 0, GL_DYNAMIC_COPY); + + glGenBuffers(1, &backend->pathQueueBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->pathQueueBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_PATH_QUEUE_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); + + glGenBuffers(1, &backend->tileQueueBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileQueueBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_TILE_QUEUE_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); + + glGenBuffers(1, &backend->tileQueueCountBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileQueueCountBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), 0, GL_DYNAMIC_COPY); + + glGenBuffers(1, &backend->tileOpBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileOpBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, MG_GL_TILE_OP_BUFFER_SIZE, 0, GL_DYNAMIC_COPY); + + glGenBuffers(1, &backend->tileOpCountBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->tileOpCountBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), 0, GL_DYNAMIC_COPY); + + int tileSize = MG_GL_TILE_SIZE; + int nTilesX = (int)(backend->frameSize.x + tileSize - 1)/tileSize; + int nTilesY = (int)(backend->frameSize.y + tileSize - 1)/tileSize; + + glGenBuffers(1, &backend->screenTilesBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->screenTilesBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, nTilesX*nTilesY*MG_GL_SCREEN_TILE_SIZE, 0, GL_DYNAMIC_COPY); + + glGenBuffers(1, &backend->screenTilesCountBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->screenTilesCountBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(int), 0, GL_DYNAMIC_COPY); + + glGenBuffers(1, &backend->rasterDispatchBuffer); + glBindBuffer(GL_SHADER_STORAGE_BUFFER, backend->rasterDispatchBuffer); + glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(mg_gl_dispatch_indirect_command), 0, GL_DYNAMIC_COPY); + + if(err) + { + mg_gl_canvas_destroy((mg_canvas_backend*)backend); + backend = 0; + } + } + 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/milepost/src/gl_loader.c b/milepost/src/graphics/gl_loader.c similarity index 100% rename from milepost/src/gl_loader.c rename to milepost/src/graphics/gl_loader.c diff --git a/milepost/src/gl_loader.h b/milepost/src/graphics/gl_loader.h similarity index 100% rename from milepost/src/gl_loader.h rename to milepost/src/graphics/gl_loader.h diff --git a/milepost/src/glsl_shaders/backprop.glsl b/milepost/src/graphics/glsl_shaders/backprop.glsl similarity index 100% rename from milepost/src/glsl_shaders/backprop.glsl rename to milepost/src/graphics/glsl_shaders/backprop.glsl diff --git a/milepost/src/glsl_shaders/balance_workgroups.glsl b/milepost/src/graphics/glsl_shaders/balance_workgroups.glsl similarity index 100% rename from milepost/src/glsl_shaders/balance_workgroups.glsl rename to milepost/src/graphics/glsl_shaders/balance_workgroups.glsl diff --git a/milepost/src/glsl_shaders/blit_fragment.glsl b/milepost/src/graphics/glsl_shaders/blit_fragment.glsl similarity index 100% rename from milepost/src/glsl_shaders/blit_fragment.glsl rename to milepost/src/graphics/glsl_shaders/blit_fragment.glsl diff --git a/milepost/src/glsl_shaders/blit_vertex.glsl b/milepost/src/graphics/glsl_shaders/blit_vertex.glsl similarity index 100% rename from milepost/src/glsl_shaders/blit_vertex.glsl rename to milepost/src/graphics/glsl_shaders/blit_vertex.glsl diff --git a/milepost/src/glsl_shaders/common.glsl b/milepost/src/graphics/glsl_shaders/common.glsl similarity index 100% rename from milepost/src/glsl_shaders/common.glsl rename to milepost/src/graphics/glsl_shaders/common.glsl diff --git a/milepost/src/glsl_shaders/merge.glsl b/milepost/src/graphics/glsl_shaders/merge.glsl similarity index 100% rename from milepost/src/glsl_shaders/merge.glsl rename to milepost/src/graphics/glsl_shaders/merge.glsl diff --git a/milepost/src/glsl_shaders/path_setup.glsl b/milepost/src/graphics/glsl_shaders/path_setup.glsl similarity index 100% rename from milepost/src/glsl_shaders/path_setup.glsl rename to milepost/src/graphics/glsl_shaders/path_setup.glsl diff --git a/milepost/src/glsl_shaders/raster.glsl b/milepost/src/graphics/glsl_shaders/raster.glsl similarity index 100% rename from milepost/src/glsl_shaders/raster.glsl rename to milepost/src/graphics/glsl_shaders/raster.glsl diff --git a/milepost/src/glsl_shaders/segment_setup.glsl b/milepost/src/graphics/glsl_shaders/segment_setup.glsl similarity index 100% rename from milepost/src/glsl_shaders/segment_setup.glsl rename to milepost/src/graphics/glsl_shaders/segment_setup.glsl diff --git a/milepost/src/graphics.h b/milepost/src/graphics/graphics.h similarity index 97% rename from milepost/src/graphics.h rename to milepost/src/graphics/graphics.h index 3a9a875..c9ad390 100644 --- a/milepost/src/graphics.h +++ b/milepost/src/graphics/graphics.h @@ -1,330 +1,330 @@ -/************************************************************//** -* -* @file: graphics.h -* @author: Martin Fouilleul -* @date: 23/01/2023 -* @revision: -* -*****************************************************************/ -#ifndef __GRAPHICS_H_ -#define __GRAPHICS_H_ - -#include"util/typedefs.h" -#include"platform/platform.h" -#include"mp_app.h" - -//------------------------------------------------------------------------------------------ -//NOTE(martin): backends selection -//------------------------------------------------------------------------------------------ - -typedef enum { - MG_NONE, - MG_METAL, - MG_GL, - MG_GLES, - MG_CANVAS, - MG_HOST } mg_surface_api; - -//NOTE: these macros are used to select which backend to include when building milepost -// they can be overridden by passing them to the compiler command line -#if PLATFORM_MACOS - #ifndef MG_COMPILE_METAL - #define MG_COMPILE_METAL 1 - #endif - - #ifndef MG_COMPILE_GLES - #define MG_COMPILE_GLES 1 - #endif - - #ifndef MG_COMPILE_CANVAS - #if !MG_COMPILE_METAL - #error "Canvas surface requires a Metal backend on macOS. Make sure you define MG_COMPILE_METAL to 1." - #endif - #define MG_COMPILE_CANVAS 1 - #endif - - #define MG_COMPILE_GL 0 - -#elif PLATFORM_WINDOWS - #ifndef MG_COMPILE_GL - #define MG_COMPILE_GL 1 - #endif - - #ifndef MG_COMPILE_GLES - #define MG_COMPILE_GLES 1 - #endif - - #ifndef MG_COMPILE_CANVAS - #if !MG_COMPILE_GL - #error "Canvas surface requires an OpenGL backend on Windows. Make sure you define MG_COMPILE_GL to 1." - #endif - #define MG_COMPILE_CANVAS 1 - #endif - -#elif PLATFORM_LINUX - #ifndef MG_COMPILE_GL - #define MG_COMPILE_GL 1 - #endif - - #ifndef MG_COMPILE_CANVAS - #if !MG_COMPILE_GL - #error "Canvas surface requires an OpenGL backend on Linux. Make sure you define MG_COMPILE_GL to 1." - #endif - #define MG_COMPILE_CANVAS 1 - #endif -#endif - -//NOTE: these macros are used to select backend-specific APIs to include when using milepost -#ifdef MG_EXPOSE_SURFACE_METAL - #include"mtl_surface.h" -#endif - -#ifdef MG_EXPOSE_SURFACE_WGL - #include"wgl_surface.h" -#endif - -//TODO: expose nsgl surface when supported, expose egl surface, etc... - -//TODO: add MG_INCLUDE_OPENGL/GLES/etc, once we know how we make different gl versions co-exist - -MP_API bool mg_is_surface_api_available(mg_surface_api api); - -//------------------------------------------------------------------------------------------ -//NOTE(martin): graphics surface -//------------------------------------------------------------------------------------------ -typedef struct mg_surface { u64 h; } mg_surface; - -MP_API mg_surface mg_surface_nil(void); -MP_API bool mg_surface_is_nil(mg_surface surface); - -MP_API mg_surface mg_surface_create_for_window(mp_window window, mg_surface_api api); -MP_API void mg_surface_destroy(mg_surface surface); - -MP_API void mg_surface_prepare(mg_surface surface); -MP_API void mg_surface_present(mg_surface surface); -MP_API void mg_surface_deselect(void); - -MP_API void mg_surface_swap_interval(mg_surface surface, int swap); -MP_API vec2 mg_surface_get_size(mg_surface surface); -MP_API vec2 mg_surface_contents_scaling(mg_surface surface); -MP_API bool mg_surface_get_hidden(mg_surface surface); -MP_API void mg_surface_set_hidden(mg_surface surface, bool hidden); - -//NOTE(martin): surface sharing -typedef u64 mg_surface_id; - -MP_API mg_surface mg_surface_create_remote(u32 width, u32 height, mg_surface_api api); -MP_API mg_surface mg_surface_create_host(mp_window window); -MP_API mg_surface_id mg_surface_remote_id(mg_surface surface); -MP_API void mg_surface_host_connect(mg_surface surface, mg_surface_id remoteId); - -//------------------------------------------------------------------------------------------ -//NOTE(martin): graphics canvas structs -//------------------------------------------------------------------------------------------ -typedef struct mg_canvas { u64 h; } mg_canvas; -typedef struct mg_font { u64 h; } mg_font; -typedef struct mg_image { u64 h; } mg_image; - -typedef struct mg_mat2x3 -{ - f32 m[6]; -} mg_mat2x3; - -typedef struct mg_color -{ - union - { - struct - { - f32 r; - f32 g; - f32 b; - f32 a; - }; - f32 c[4]; - }; -} mg_color; - -typedef enum {MG_JOINT_MITER = 0, - MG_JOINT_BEVEL, - MG_JOINT_NONE } mg_joint_type; - -typedef enum {MG_CAP_NONE = 0, - MG_CAP_SQUARE } mg_cap_type; - -typedef struct mg_font_extents -{ - f32 ascent; // the extent above the baseline (by convention a positive value extends above the baseline) - f32 descent; // the extent below the baseline (by convention, positive value extends below the baseline) - f32 leading; // spacing between one row's descent and the next row's ascent - f32 xHeight; // height of the lower case letter 'x' - f32 capHeight; // height of the upper case letter 'M' - f32 width; // maximum width of the font - -} mg_font_extents; - -typedef struct mg_text_extents -{ - f32 xBearing; - f32 yBearing; - f32 width; - f32 height; - f32 xAdvance; - f32 yAdvance; - -} mg_text_extents; - -//------------------------------------------------------------------------------------------ -//NOTE(martin): graphics canvas -//------------------------------------------------------------------------------------------ -MP_API mg_canvas mg_canvas_nil(void); -MP_API bool mg_canvas_is_nil(mg_canvas canvas); - -MP_API mg_canvas mg_canvas_create(void); -MP_API void mg_canvas_destroy(mg_canvas canvas); -MP_API mg_canvas mg_canvas_set_current(mg_canvas canvas); -MP_API void mg_render(mg_surface surface, mg_canvas canvas); - -//------------------------------------------------------------------------------------------ -//NOTE(martin): fonts -//------------------------------------------------------------------------------------------ -MP_API mg_font mg_font_nil(void); -MP_API mg_font mg_font_create_from_memory(u32 size, byte* buffer, u32 rangeCount, unicode_range* ranges); -MP_API void mg_font_destroy(mg_font font); - -//NOTE(martin): the following int valued functions return -1 if font is invalid or codepoint is not present in font// -//TODO(martin): add enum error codes - -MP_API mg_font_extents mg_font_get_extents(mg_font font); -MP_API mg_font_extents mg_font_get_scaled_extents(mg_font font, f32 emSize); -MP_API f32 mg_font_get_scale_for_em_pixels(mg_font font, f32 emSize); - -//NOTE(martin): if you need to process more than one codepoint, first convert your codepoints to glyph indices, then use the -// glyph index versions of the functions, which can take an array of glyph indices. - -MP_API str32 mg_font_get_glyph_indices(mg_font font, str32 codePoints, str32 backing); -MP_API str32 mg_font_push_glyph_indices(mg_font font, mem_arena* arena, str32 codePoints); -MP_API u32 mg_font_get_glyph_index(mg_font font, utf32 codePoint); - -MP_API int mg_font_get_codepoint_extents(mg_font font, utf32 codePoint, mg_text_extents* outExtents); - -MP_API int mg_font_get_glyph_extents(mg_font font, str32 glyphIndices, mg_text_extents* outExtents); - -MP_API mp_rect mg_text_bounding_box_utf32(mg_font font, f32 fontSize, str32 text); -MP_API mp_rect mg_text_bounding_box(mg_font font, f32 fontSize, str8 text); - -//------------------------------------------------------------------------------------------ -//NOTE(martin): images -//------------------------------------------------------------------------------------------ -MP_API mg_image mg_image_nil(void); -MP_API bool mg_image_is_nil(mg_image a); - -MP_API mg_image mg_image_create(mg_surface surface, u32 width, u32 height); -MP_API mg_image mg_image_create_from_rgba8(mg_surface surface, u32 width, u32 height, u8* pixels); -MP_API mg_image mg_image_create_from_data(mg_surface surface, str8 data, bool flip); -MP_API mg_image mg_image_create_from_file(mg_surface surface, str8 path, bool flip); - -MP_API void mg_image_destroy(mg_image image); - -MP_API void mg_image_upload_region_rgba8(mg_image image, mp_rect region, u8* pixels); -MP_API vec2 mg_image_size(mg_image image); - -//------------------------------------------------------------------------------------------ -//NOTE(martin): atlasing -//------------------------------------------------------------------------------------------ - -//NOTE: rectangle allocator -typedef struct mg_rect_atlas mg_rect_atlas; - -MP_API mg_rect_atlas* mg_rect_atlas_create(mem_arena* arena, i32 width, i32 height); -MP_API mp_rect mg_rect_atlas_alloc(mg_rect_atlas* atlas, i32 width, i32 height); -MP_API void mg_rect_atlas_recycle(mg_rect_atlas* atlas, mp_rect rect); - -//NOTE: image atlas helpers -typedef struct mg_image_region -{ - mg_image image; - mp_rect rect; -} mg_image_region; - -MP_API mg_image_region mg_image_atlas_alloc_from_rgba8(mg_rect_atlas* atlas, mg_image backingImage, u32 width, u32 height, u8* pixels); -MP_API mg_image_region mg_image_atlas_alloc_from_data(mg_rect_atlas* atlas, mg_image backingImage, str8 data, bool flip); -MP_API mg_image_region mg_image_atlas_alloc_from_file(mg_rect_atlas* atlas, mg_image backingImage, str8 path, bool flip); -MP_API void mg_image_atlas_recycle(mg_rect_atlas* atlas, mg_image_region imageRgn); - -//------------------------------------------------------------------------------------------ -//NOTE(martin): transform, viewport and clipping -//------------------------------------------------------------------------------------------ -MP_API void mg_viewport(mp_rect viewPort); - -MP_API void mg_matrix_push(mg_mat2x3 matrix); -MP_API void mg_matrix_pop(void); - -MP_API void mg_clip_push(f32 x, f32 y, f32 w, f32 h); -MP_API void mg_clip_pop(void); - -//------------------------------------------------------------------------------------------ -//NOTE(martin): graphics attributes setting/getting -//------------------------------------------------------------------------------------------ -MP_API void mg_set_color(mg_color color); -MP_API void mg_set_color_rgba(f32 r, f32 g, f32 b, f32 a); -MP_API void mg_set_width(f32 width); -MP_API void mg_set_tolerance(f32 tolerance); -MP_API void mg_set_joint(mg_joint_type joint); -MP_API void mg_set_max_joint_excursion(f32 maxJointExcursion); -MP_API void mg_set_cap(mg_cap_type cap); -MP_API void mg_set_font(mg_font font); -MP_API void mg_set_font_size(f32 size); -MP_API void mg_set_text_flip(bool flip); -MP_API void mg_set_image(mg_image image); -MP_API void mg_set_image_source_region(mp_rect region); - -MP_API mg_color mg_get_color(void); -MP_API f32 mg_get_width(void); -MP_API f32 mg_get_tolerance(void); -MP_API mg_joint_type mg_get_joint(void); -MP_API f32 mg_get_max_joint_excursion(void); -MP_API mg_cap_type mg_get_cap(void); -MP_API mg_font mg_get_font(void); -MP_API f32 mg_get_font_size(void); -MP_API bool mg_get_text_flip(void); - -//------------------------------------------------------------------------------------------ -//NOTE(martin): path construction -//------------------------------------------------------------------------------------------ -MP_API vec2 mg_get_position(void); -MP_API void mg_move_to(f32 x, f32 y); -MP_API void mg_line_to(f32 x, f32 y); -MP_API void mg_quadratic_to(f32 x1, f32 y1, f32 x2, f32 y2); -MP_API void mg_cubic_to(f32 x1, f32 y1, f32 x2, f32 y2, f32 x3, f32 y3); -MP_API void mg_close_path(void); - -MP_API mp_rect mg_glyph_outlines(str32 glyphIndices); -MP_API void mg_codepoints_outlines(str32 string); -MP_API void mg_text_outlines(str8 string); - -//------------------------------------------------------------------------------------------ -//NOTE(martin): clear/fill/stroke -//------------------------------------------------------------------------------------------ -MP_API void mg_clear(void); -MP_API void mg_fill(void); -MP_API void mg_stroke(void); - -//------------------------------------------------------------------------------------------ -//NOTE(martin): 'fast' shapes primitives -//------------------------------------------------------------------------------------------ -MP_API void mg_rectangle_fill(f32 x, f32 y, f32 w, f32 h); -MP_API void mg_rectangle_stroke(f32 x, f32 y, f32 w, f32 h); -MP_API void mg_rounded_rectangle_fill(f32 x, f32 y, f32 w, f32 h, f32 r); -MP_API void mg_rounded_rectangle_stroke(f32 x, f32 y, f32 w, f32 h, f32 r); -MP_API void mg_ellipse_fill(f32 x, f32 y, f32 rx, f32 ry); -MP_API void mg_ellipse_stroke(f32 x, f32 y, f32 rx, f32 ry); -MP_API void mg_circle_fill(f32 x, f32 y, f32 r); -MP_API void mg_circle_stroke(f32 x, f32 y, f32 r); -MP_API void mg_arc(f32 x, f32 y, f32 r, f32 arcAngle, f32 startAngle); - -//NOTE: image helpers -MP_API void mg_image_draw(mg_image image, mp_rect rect); -MP_API void mg_image_draw_region(mg_image image, mp_rect srcRegion, mp_rect dstRegion); - -#endif //__GRAPHICS_H_ +/************************************************************//** +* +* @file: graphics.h +* @author: Martin Fouilleul +* @date: 23/01/2023 +* @revision: +* +*****************************************************************/ +#ifndef __GRAPHICS_H_ +#define __GRAPHICS_H_ + +#include"util/typedefs.h" +#include"platform/platform.h" +#include"app/mp_app.h" + +//------------------------------------------------------------------------------------------ +//NOTE(martin): backends selection +//------------------------------------------------------------------------------------------ + +typedef enum { + MG_NONE, + MG_METAL, + MG_GL, + MG_GLES, + MG_CANVAS, + MG_HOST } mg_surface_api; + +//NOTE: these macros are used to select which backend to include when building milepost +// they can be overridden by passing them to the compiler command line +#if PLATFORM_MACOS + #ifndef MG_COMPILE_METAL + #define MG_COMPILE_METAL 1 + #endif + + #ifndef MG_COMPILE_GLES + #define MG_COMPILE_GLES 1 + #endif + + #ifndef MG_COMPILE_CANVAS + #if !MG_COMPILE_METAL + #error "Canvas surface requires a Metal backend on macOS. Make sure you define MG_COMPILE_METAL to 1." + #endif + #define MG_COMPILE_CANVAS 1 + #endif + + #define MG_COMPILE_GL 0 + +#elif PLATFORM_WINDOWS + #ifndef MG_COMPILE_GL + #define MG_COMPILE_GL 1 + #endif + + #ifndef MG_COMPILE_GLES + #define MG_COMPILE_GLES 1 + #endif + + #ifndef MG_COMPILE_CANVAS + #if !MG_COMPILE_GL + #error "Canvas surface requires an OpenGL backend on Windows. Make sure you define MG_COMPILE_GL to 1." + #endif + #define MG_COMPILE_CANVAS 1 + #endif + +#elif PLATFORM_LINUX + #ifndef MG_COMPILE_GL + #define MG_COMPILE_GL 1 + #endif + + #ifndef MG_COMPILE_CANVAS + #if !MG_COMPILE_GL + #error "Canvas surface requires an OpenGL backend on Linux. Make sure you define MG_COMPILE_GL to 1." + #endif + #define MG_COMPILE_CANVAS 1 + #endif +#endif + +//NOTE: these macros are used to select backend-specific APIs to include when using milepost +#ifdef MG_EXPOSE_SURFACE_METAL + #include"mtl_surface.h" +#endif + +#ifdef MG_EXPOSE_SURFACE_WGL + #include"wgl_surface.h" +#endif + +//TODO: expose nsgl surface when supported, expose egl surface, etc... + +//TODO: add MG_INCLUDE_OPENGL/GLES/etc, once we know how we make different gl versions co-exist + +MP_API bool mg_is_surface_api_available(mg_surface_api api); + +//------------------------------------------------------------------------------------------ +//NOTE(martin): graphics surface +//------------------------------------------------------------------------------------------ +typedef struct mg_surface { u64 h; } mg_surface; + +MP_API mg_surface mg_surface_nil(void); +MP_API bool mg_surface_is_nil(mg_surface surface); + +MP_API mg_surface mg_surface_create_for_window(mp_window window, mg_surface_api api); +MP_API void mg_surface_destroy(mg_surface surface); + +MP_API void mg_surface_prepare(mg_surface surface); +MP_API void mg_surface_present(mg_surface surface); +MP_API void mg_surface_deselect(void); + +MP_API void mg_surface_swap_interval(mg_surface surface, int swap); +MP_API vec2 mg_surface_get_size(mg_surface surface); +MP_API vec2 mg_surface_contents_scaling(mg_surface surface); +MP_API bool mg_surface_get_hidden(mg_surface surface); +MP_API void mg_surface_set_hidden(mg_surface surface, bool hidden); + +//NOTE(martin): surface sharing +typedef u64 mg_surface_id; + +MP_API mg_surface mg_surface_create_remote(u32 width, u32 height, mg_surface_api api); +MP_API mg_surface mg_surface_create_host(mp_window window); +MP_API mg_surface_id mg_surface_remote_id(mg_surface surface); +MP_API void mg_surface_host_connect(mg_surface surface, mg_surface_id remoteId); + +//------------------------------------------------------------------------------------------ +//NOTE(martin): graphics canvas structs +//------------------------------------------------------------------------------------------ +typedef struct mg_canvas { u64 h; } mg_canvas; +typedef struct mg_font { u64 h; } mg_font; +typedef struct mg_image { u64 h; } mg_image; + +typedef struct mg_mat2x3 +{ + f32 m[6]; +} mg_mat2x3; + +typedef struct mg_color +{ + union + { + struct + { + f32 r; + f32 g; + f32 b; + f32 a; + }; + f32 c[4]; + }; +} mg_color; + +typedef enum {MG_JOINT_MITER = 0, + MG_JOINT_BEVEL, + MG_JOINT_NONE } mg_joint_type; + +typedef enum {MG_CAP_NONE = 0, + MG_CAP_SQUARE } mg_cap_type; + +typedef struct mg_font_extents +{ + f32 ascent; // the extent above the baseline (by convention a positive value extends above the baseline) + f32 descent; // the extent below the baseline (by convention, positive value extends below the baseline) + f32 leading; // spacing between one row's descent and the next row's ascent + f32 xHeight; // height of the lower case letter 'x' + f32 capHeight; // height of the upper case letter 'M' + f32 width; // maximum width of the font + +} mg_font_extents; + +typedef struct mg_text_extents +{ + f32 xBearing; + f32 yBearing; + f32 width; + f32 height; + f32 xAdvance; + f32 yAdvance; + +} mg_text_extents; + +//------------------------------------------------------------------------------------------ +//NOTE(martin): graphics canvas +//------------------------------------------------------------------------------------------ +MP_API mg_canvas mg_canvas_nil(void); +MP_API bool mg_canvas_is_nil(mg_canvas canvas); + +MP_API mg_canvas mg_canvas_create(void); +MP_API void mg_canvas_destroy(mg_canvas canvas); +MP_API mg_canvas mg_canvas_set_current(mg_canvas canvas); +MP_API void mg_render(mg_surface surface, mg_canvas canvas); + +//------------------------------------------------------------------------------------------ +//NOTE(martin): fonts +//------------------------------------------------------------------------------------------ +MP_API mg_font mg_font_nil(void); +MP_API mg_font mg_font_create_from_memory(u32 size, byte* buffer, u32 rangeCount, unicode_range* ranges); +MP_API void mg_font_destroy(mg_font font); + +//NOTE(martin): the following int valued functions return -1 if font is invalid or codepoint is not present in font// +//TODO(martin): add enum error codes + +MP_API mg_font_extents mg_font_get_extents(mg_font font); +MP_API mg_font_extents mg_font_get_scaled_extents(mg_font font, f32 emSize); +MP_API f32 mg_font_get_scale_for_em_pixels(mg_font font, f32 emSize); + +//NOTE(martin): if you need to process more than one codepoint, first convert your codepoints to glyph indices, then use the +// glyph index versions of the functions, which can take an array of glyph indices. + +MP_API str32 mg_font_get_glyph_indices(mg_font font, str32 codePoints, str32 backing); +MP_API str32 mg_font_push_glyph_indices(mg_font font, mem_arena* arena, str32 codePoints); +MP_API u32 mg_font_get_glyph_index(mg_font font, utf32 codePoint); + +MP_API int mg_font_get_codepoint_extents(mg_font font, utf32 codePoint, mg_text_extents* outExtents); + +MP_API int mg_font_get_glyph_extents(mg_font font, str32 glyphIndices, mg_text_extents* outExtents); + +MP_API mp_rect mg_text_bounding_box_utf32(mg_font font, f32 fontSize, str32 text); +MP_API mp_rect mg_text_bounding_box(mg_font font, f32 fontSize, str8 text); + +//------------------------------------------------------------------------------------------ +//NOTE(martin): images +//------------------------------------------------------------------------------------------ +MP_API mg_image mg_image_nil(void); +MP_API bool mg_image_is_nil(mg_image a); + +MP_API mg_image mg_image_create(mg_surface surface, u32 width, u32 height); +MP_API mg_image mg_image_create_from_rgba8(mg_surface surface, u32 width, u32 height, u8* pixels); +MP_API mg_image mg_image_create_from_data(mg_surface surface, str8 data, bool flip); +MP_API mg_image mg_image_create_from_file(mg_surface surface, str8 path, bool flip); + +MP_API void mg_image_destroy(mg_image image); + +MP_API void mg_image_upload_region_rgba8(mg_image image, mp_rect region, u8* pixels); +MP_API vec2 mg_image_size(mg_image image); + +//------------------------------------------------------------------------------------------ +//NOTE(martin): atlasing +//------------------------------------------------------------------------------------------ + +//NOTE: rectangle allocator +typedef struct mg_rect_atlas mg_rect_atlas; + +MP_API mg_rect_atlas* mg_rect_atlas_create(mem_arena* arena, i32 width, i32 height); +MP_API mp_rect mg_rect_atlas_alloc(mg_rect_atlas* atlas, i32 width, i32 height); +MP_API void mg_rect_atlas_recycle(mg_rect_atlas* atlas, mp_rect rect); + +//NOTE: image atlas helpers +typedef struct mg_image_region +{ + mg_image image; + mp_rect rect; +} mg_image_region; + +MP_API mg_image_region mg_image_atlas_alloc_from_rgba8(mg_rect_atlas* atlas, mg_image backingImage, u32 width, u32 height, u8* pixels); +MP_API mg_image_region mg_image_atlas_alloc_from_data(mg_rect_atlas* atlas, mg_image backingImage, str8 data, bool flip); +MP_API mg_image_region mg_image_atlas_alloc_from_file(mg_rect_atlas* atlas, mg_image backingImage, str8 path, bool flip); +MP_API void mg_image_atlas_recycle(mg_rect_atlas* atlas, mg_image_region imageRgn); + +//------------------------------------------------------------------------------------------ +//NOTE(martin): transform, viewport and clipping +//------------------------------------------------------------------------------------------ +MP_API void mg_viewport(mp_rect viewPort); + +MP_API void mg_matrix_push(mg_mat2x3 matrix); +MP_API void mg_matrix_pop(void); + +MP_API void mg_clip_push(f32 x, f32 y, f32 w, f32 h); +MP_API void mg_clip_pop(void); + +//------------------------------------------------------------------------------------------ +//NOTE(martin): graphics attributes setting/getting +//------------------------------------------------------------------------------------------ +MP_API void mg_set_color(mg_color color); +MP_API void mg_set_color_rgba(f32 r, f32 g, f32 b, f32 a); +MP_API void mg_set_width(f32 width); +MP_API void mg_set_tolerance(f32 tolerance); +MP_API void mg_set_joint(mg_joint_type joint); +MP_API void mg_set_max_joint_excursion(f32 maxJointExcursion); +MP_API void mg_set_cap(mg_cap_type cap); +MP_API void mg_set_font(mg_font font); +MP_API void mg_set_font_size(f32 size); +MP_API void mg_set_text_flip(bool flip); +MP_API void mg_set_image(mg_image image); +MP_API void mg_set_image_source_region(mp_rect region); + +MP_API mg_color mg_get_color(void); +MP_API f32 mg_get_width(void); +MP_API f32 mg_get_tolerance(void); +MP_API mg_joint_type mg_get_joint(void); +MP_API f32 mg_get_max_joint_excursion(void); +MP_API mg_cap_type mg_get_cap(void); +MP_API mg_font mg_get_font(void); +MP_API f32 mg_get_font_size(void); +MP_API bool mg_get_text_flip(void); + +//------------------------------------------------------------------------------------------ +//NOTE(martin): path construction +//------------------------------------------------------------------------------------------ +MP_API vec2 mg_get_position(void); +MP_API void mg_move_to(f32 x, f32 y); +MP_API void mg_line_to(f32 x, f32 y); +MP_API void mg_quadratic_to(f32 x1, f32 y1, f32 x2, f32 y2); +MP_API void mg_cubic_to(f32 x1, f32 y1, f32 x2, f32 y2, f32 x3, f32 y3); +MP_API void mg_close_path(void); + +MP_API mp_rect mg_glyph_outlines(str32 glyphIndices); +MP_API void mg_codepoints_outlines(str32 string); +MP_API void mg_text_outlines(str8 string); + +//------------------------------------------------------------------------------------------ +//NOTE(martin): clear/fill/stroke +//------------------------------------------------------------------------------------------ +MP_API void mg_clear(void); +MP_API void mg_fill(void); +MP_API void mg_stroke(void); + +//------------------------------------------------------------------------------------------ +//NOTE(martin): 'fast' shapes primitives +//------------------------------------------------------------------------------------------ +MP_API void mg_rectangle_fill(f32 x, f32 y, f32 w, f32 h); +MP_API void mg_rectangle_stroke(f32 x, f32 y, f32 w, f32 h); +MP_API void mg_rounded_rectangle_fill(f32 x, f32 y, f32 w, f32 h, f32 r); +MP_API void mg_rounded_rectangle_stroke(f32 x, f32 y, f32 w, f32 h, f32 r); +MP_API void mg_ellipse_fill(f32 x, f32 y, f32 rx, f32 ry); +MP_API void mg_ellipse_stroke(f32 x, f32 y, f32 rx, f32 ry); +MP_API void mg_circle_fill(f32 x, f32 y, f32 r); +MP_API void mg_circle_stroke(f32 x, f32 y, f32 r); +MP_API void mg_arc(f32 x, f32 y, f32 r, f32 arcAngle, f32 startAngle); + +//NOTE: image helpers +MP_API void mg_image_draw(mg_image image, mp_rect rect); +MP_API void mg_image_draw_region(mg_image image, mp_rect srcRegion, mp_rect dstRegion); + +#endif //__GRAPHICS_H_ diff --git a/milepost/src/graphics_common.c b/milepost/src/graphics/graphics_common.c similarity index 96% rename from milepost/src/graphics_common.c rename to milepost/src/graphics/graphics_common.c index 10b3cb8..b8b1c93 100644 --- a/milepost/src/graphics_common.c +++ b/milepost/src/graphics/graphics_common.c @@ -1,1772 +1,1772 @@ -/************************************************************//** -* -* @file: graphics_common.c -* @author: Martin Fouilleul -* @date: 23/01/2023 -* @revision: -* -*****************************************************************/ - -#include"platform/platform.h" -#include"platform/platform_math.h" - -#define STB_IMAGE_IMPLEMENTATION -#if PLATFORM_ORCA - #define STBI_NO_STDIO - #define STBI_NO_HDR -#endif -#include"stb_image.h" - -#define STB_TRUETYPE_IMPLEMENTATION -#include"stb_truetype.h" - -#include"platform/platform_log.h" -#include"platform/platform_assert.h" -#include"graphics_common.h" - -typedef struct mg_glyph_map_entry -{ - unicode_range range; - u32 firstGlyphIndex; - -} mg_glyph_map_entry; - -typedef struct mg_glyph_data -{ - bool exists; - utf32 codePoint; - mg_path_descriptor pathDescriptor; - mg_text_extents extents; - //... - -} mg_glyph_data; - -enum -{ - MG_MATRIX_STACK_MAX_DEPTH = 64, - MG_CLIP_STACK_MAX_DEPTH = 64, - MG_MAX_PATH_ELEMENT_COUNT = 2<<20, - MG_MAX_PRIMITIVE_COUNT = 8<<10 -}; - -typedef struct mg_font_data -{ - list_elt freeListElt; - - u32 rangeCount; - u32 glyphCount; - u32 outlineCount; - mg_glyph_map_entry* glyphMap; - mg_glyph_data* glyphs; - mg_path_elt* outlines; - - f32 unitsPerEm; - mg_font_extents extents; - -} mg_font_data; - -typedef struct mg_canvas_data mg_canvas_data; - -typedef enum mg_handle_kind -{ - MG_HANDLE_NONE = 0, - MG_HANDLE_SURFACE, - MG_HANDLE_CANVAS, - MG_HANDLE_FONT, - MG_HANDLE_IMAGE, - MG_HANDLE_SURFACE_SERVER, -} mg_handle_kind; - -typedef struct mg_handle_slot -{ - list_elt freeListElt; - u32 generation; - mg_handle_kind kind; - - void* data; - -} mg_handle_slot; - -enum { MG_HANDLES_MAX_COUNT = 512 }; - -typedef struct mg_data -{ - bool init; - - mg_handle_slot handleArray[MG_HANDLES_MAX_COUNT]; - int handleNextIndex; - list_info handleFreeList; - - mem_arena resourceArena; - list_info canvasFreeList; - list_info fontFreeList; - -} mg_data; - -typedef struct mg_canvas_data -{ - list_elt freeListElt; - - mg_attributes attributes; - bool textFlip; - - mg_path_elt pathElements[MG_MAX_PATH_ELEMENT_COUNT]; - mg_path_descriptor path; - vec2 subPathStartPoint; - vec2 subPathLastPoint; - - mg_mat2x3 matrixStack[MG_MATRIX_STACK_MAX_DEPTH]; - u32 matrixStackSize; - - mp_rect clipStack[MG_CLIP_STACK_MAX_DEPTH]; - u32 clipStackSize; - - u32 primitiveCount; - mg_primitive primitives[MG_MAX_PRIMITIVE_COUNT]; - - //NOTE: these are used at render time - mg_color clearColor; - - vec4 shapeExtents; - vec4 shapeScreenExtents; - -} mg_canvas_data; - -static mg_data __mgData = {0}; - - -void mg_init() -{ - if(!__mgData.init) - { - __mgData.handleNextIndex = 0; - mem_arena_init(&__mgData.resourceArena); - __mgData.init = true; - } -} - -//------------------------------------------------------------------------ -// handle pools procedures -//------------------------------------------------------------------------ - -u64 mg_handle_alloc(mg_handle_kind kind, void* data) -{ - if(!__mgData.init) - { - mg_init(); - } - - mg_handle_slot* slot = list_pop_entry(&__mgData.handleFreeList, mg_handle_slot, freeListElt); - if(!slot && __mgData.handleNextIndex < MG_HANDLES_MAX_COUNT) - { - slot = &__mgData.handleArray[__mgData.handleNextIndex]; - __mgData.handleNextIndex++; - - slot->generation = 1; - } - u64 h = 0; - if(slot) - { - slot->kind = kind; - slot->data = data; - - h = ((u64)(slot - __mgData.handleArray))<<32 - |((u64)(slot->generation)); - } - return(h); -} - -void mg_handle_recycle(u64 h) -{ - DEBUG_ASSERT(__mgData.init); - - u32 index = h>>32; - u32 generation = h & 0xffffffff; - - if(index*sizeof(mg_handle_slot) < __mgData.handleNextIndex) - { - mg_handle_slot* slot = &__mgData.handleArray[index]; - if(slot->generation == generation) - { - DEBUG_ASSERT(slot->generation != UINT32_MAX, "surface slot generation wrap around\n"); - slot->generation++; - list_push(&__mgData.handleFreeList, &slot->freeListElt); - } - } -} - -void* mg_data_from_handle(mg_handle_kind kind, u64 h) -{ - DEBUG_ASSERT(__mgData.init); - - void* data = 0; - - u32 index = h>>32; - u32 generation = h & 0xffffffff; - - if(index < __mgData.handleNextIndex) - { - mg_handle_slot* slot = &__mgData.handleArray[index]; - if( slot->generation == generation - && slot->kind == kind) - { - data = slot->data; - } - } - return(data); -} - -//------------------------------------------------------------------------------------------ -//NOTE(martin): graphics canvas internal -//------------------------------------------------------------------------------------------ - -mp_thread_local mg_canvas_data* __mgCurrentCanvas = 0; -mp_thread_local mg_canvas __mgCurrentCanvasHandle = {0}; - -//TODO put these elsewhere -bool vec2_equal(vec2 v0, vec2 v1) -{ - return(v0.x == v1.x && v0.y == v1.y); -} - -bool vec2_close(vec2 p0, vec2 p1, f32 tolerance) -{ - f32 norm2 = (p1.x - p0.x)*(p1.x - p0.x) + (p1.y - p0.y)*(p1.y - p0.y); - return(fabs(norm2) < tolerance); -} - -vec2 vec2_mul(f32 f, vec2 v) -{ - return((vec2){f*v.x, f*v.y}); -} - -vec2 vec2_add(vec2 v0, vec2 v1) -{ - return((vec2){v0.x + v1.x, v0.y + v1.y}); -} - -mg_mat2x3 mg_mat2x3_mul_m(mg_mat2x3 lhs, mg_mat2x3 rhs) -{ - mg_mat2x3 res; - res.m[0] = lhs.m[0]*rhs.m[0] + lhs.m[1]*rhs.m[3]; - res.m[1] = lhs.m[0]*rhs.m[1] + lhs.m[1]*rhs.m[4]; - res.m[2] = lhs.m[0]*rhs.m[2] + lhs.m[1]*rhs.m[5] + lhs.m[2]; - res.m[3] = lhs.m[3]*rhs.m[0] + lhs.m[4]*rhs.m[3]; - res.m[4] = lhs.m[3]*rhs.m[1] + lhs.m[4]*rhs.m[4]; - res.m[5] = lhs.m[3]*rhs.m[2] + lhs.m[4]*rhs.m[5] + lhs.m[5]; - - return(res); -} - -mg_mat2x3 mg_mat2x3_inv(mg_mat2x3 x) -{ - mg_mat2x3 res; - res.m[0] = x.m[4]/(x.m[0]*x.m[4] - x.m[1]*x.m[3]); - res.m[1] = x.m[1]/(x.m[1]*x.m[3] - x.m[0]*x.m[4]); - res.m[3] = x.m[3]/(x.m[1]*x.m[3] - x.m[0]*x.m[4]); - res.m[4] = x.m[0]/(x.m[0]*x.m[4] - x.m[1]*x.m[3]); - res.m[2] = -(x.m[2]*res.m[0] + x.m[5]*res.m[1]); - res.m[5] = -(x.m[2]*res.m[3] + x.m[5]*res.m[4]); - return(res); -} - -vec2 mg_mat2x3_mul(mg_mat2x3 m, vec2 p) -{ - f32 x = p.x*m.m[0] + p.y*m.m[1] + m.m[2]; - f32 y = p.x*m.m[3] + p.y*m.m[4] + m.m[5]; - return((vec2){x, y}); -} - -mg_mat2x3 mg_matrix_stack_top(mg_canvas_data* canvas) -{ - if(canvas->matrixStackSize == 0) - { - return((mg_mat2x3){1, 0, 0, - 0, 1, 0}); - } - else - { - return(canvas->matrixStack[canvas->matrixStackSize-1]); - } -} - -void mg_matrix_stack_push(mg_canvas_data* canvas, mg_mat2x3 transform) -{ - if(canvas->matrixStackSize >= MG_MATRIX_STACK_MAX_DEPTH) - { - log_error("matrix stack overflow\n"); - } - else - { - canvas->matrixStack[canvas->matrixStackSize] = transform; - canvas->matrixStackSize++; - } -} - -void mg_matrix_stack_pop(mg_canvas_data* canvas) -{ - if(canvas->matrixStackSize == 0) - { - log_error("matrix stack underflow\n"); - } - else - { - canvas->matrixStackSize--; - mg_matrix_stack_top(canvas); - } -} - -mp_rect mg_clip_stack_top(mg_canvas_data* canvas) -{ - if(canvas->clipStackSize == 0) - { - return((mp_rect){-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX}); - } - else - { - return(canvas->clipStack[canvas->clipStackSize-1]); - } -} - -void mg_clip_stack_push(mg_canvas_data* canvas, mp_rect clip) -{ - if(canvas->clipStackSize >= MG_CLIP_STACK_MAX_DEPTH) - { - log_error("clip stack overflow\n"); - } - else - { - canvas->clipStack[canvas->clipStackSize] = clip; - canvas->clipStackSize++; - } -} - -void mg_clip_stack_pop(mg_canvas_data* canvas) -{ - if(canvas->clipStackSize == 0) - { - log_error("clip stack underflow\n"); - } - else - { - canvas->clipStackSize--; - } -} - -void mg_push_command(mg_canvas_data* canvas, mg_primitive primitive) -{ - //NOTE(martin): push primitive and updates current stream, eventually patching a pending jump. - ASSERT(canvas->primitiveCount < MG_MAX_PRIMITIVE_COUNT); - - canvas->primitives[canvas->primitiveCount] = primitive; - canvas->primitives[canvas->primitiveCount].attributes = canvas->attributes; - canvas->primitives[canvas->primitiveCount].attributes.transform = mg_matrix_stack_top(canvas); - canvas->primitives[canvas->primitiveCount].attributes.clip = mg_clip_stack_top(canvas); - canvas->primitiveCount++; -} - -void mg_new_path(mg_canvas_data* canvas) -{ - canvas->path.startIndex += canvas->path.count; - canvas->path.count = 0; - canvas->subPathStartPoint = canvas->subPathLastPoint; - canvas->path.startPoint = canvas->subPathStartPoint; -} - -void mg_path_push_elements(mg_canvas_data* canvas, u32 count, mg_path_elt* elements) -{ - ASSERT(canvas->path.count + canvas->path.startIndex + count < MG_MAX_PATH_ELEMENT_COUNT); - memcpy(canvas->pathElements + canvas->path.startIndex + canvas->path.count, elements, count*sizeof(mg_path_elt)); - canvas->path.count += count; - - ASSERT(canvas->path.count < MG_MAX_PATH_ELEMENT_COUNT); -} - -void mg_path_push_element(mg_canvas_data* canvas, mg_path_elt elt) -{ - mg_path_push_elements(canvas, 1, &elt); -} - -//------------------------------------------------------------------------------------------ -//NOTE(martin): fonts -//------------------------------------------------------------------------------------------ - -mg_font mg_font_nil() { return((mg_font){.h = 0}); } -bool mg_font_is_nil(mg_font font) { return(font.h == 0); } - -mg_font mg_font_handle_alloc(mg_font_data* font) -{ - mg_font handle = {.h = mg_handle_alloc(MG_HANDLE_FONT, (void*)font) }; - return(handle); -} - -mg_font_data* mg_font_data_from_handle(mg_font handle) -{ - mg_font_data* data = mg_data_from_handle(MG_HANDLE_FONT, handle.h); - return(data); -} - -mg_font mg_font_create_from_memory(u32 size, byte* buffer, u32 rangeCount, unicode_range* ranges) -{ - if(!__mgData.init) - { - mg_init(); - } - mg_font fontHandle = mg_font_nil(); - - mg_font_data* font = list_pop_entry(&__mgData.fontFreeList, mg_font_data, freeListElt); - if(!font) - { - font = mem_arena_alloc_type(&__mgData.resourceArena, mg_font_data); - } - if(font) - { - memset(font, 0, sizeof(mg_font_data)); - fontHandle = mg_font_handle_alloc(font); - - stbtt_fontinfo stbttFontInfo; - stbtt_InitFont(&stbttFontInfo, buffer, 0); - - //NOTE(martin): load font metrics data - font->unitsPerEm = 1./stbtt_ScaleForMappingEmToPixels(&stbttFontInfo, 1); - - int ascent, descent, lineGap, x0, x1, y0, y1; - stbtt_GetFontVMetrics(&stbttFontInfo, &ascent, &descent, &lineGap); - stbtt_GetFontBoundingBox(&stbttFontInfo, &x0, &y0, &x1, &y1); - - font->extents.ascent = ascent; - font->extents.descent = -descent; - font->extents.leading = lineGap; - font->extents.width = x1 - x0; - - stbtt_GetCodepointBox(&stbttFontInfo, 'x', &x0, &y0, &x1, &y1); - font->extents.xHeight = y1 - y0; - - stbtt_GetCodepointBox(&stbttFontInfo, 'M', &x0, &y0, &x1, &y1); - font->extents.capHeight = y1 - y0; - - //NOTE(martin): load codepoint ranges - font->rangeCount = rangeCount; - font->glyphMap = malloc_array(mg_glyph_map_entry, rangeCount); - font->glyphCount = 0; - - for(int i=0; iglyphMap[i].range = ranges[i]; - font->glyphMap[i].firstGlyphIndex = font->glyphCount + 1; - font->glyphCount += ranges[i].count; - } - - font->glyphs = malloc_array(mg_glyph_data, font->glyphCount); - - //NOTE(martin): first do a count of outlines - int outlineCount = 0; - for(int rangeIndex=0; rangeIndexglyphMap[rangeIndex].range.firstCodePoint; - u32 firstGlyphIndex = font->glyphMap[rangeIndex].firstGlyphIndex; - u32 endGlyphIndex = firstGlyphIndex + font->glyphMap[rangeIndex].range.count; - - for(int glyphIndex = firstGlyphIndex; - glyphIndex < endGlyphIndex; glyphIndex++) - { - int stbttGlyphIndex = stbtt_FindGlyphIndex(&stbttFontInfo, codePoint); - if(stbttGlyphIndex == 0) - { - //NOTE(martin): the codepoint is not found in the font - codePoint++; - continue; - } - //NOTE(martin): load glyph outlines - stbtt_vertex* vertices = 0; - outlineCount += stbtt_GetGlyphShape(&stbttFontInfo, stbttGlyphIndex, &vertices); - stbtt_FreeShape(&stbttFontInfo, vertices); - codePoint++; - } - } - //NOTE(martin): allocate outlines - font->outlines = malloc_array(mg_path_elt, outlineCount); - font->outlineCount = 0; - - //NOTE(martin): load metrics and outlines - for(int rangeIndex=0; rangeIndexglyphMap[rangeIndex].range.firstCodePoint; - u32 firstGlyphIndex = font->glyphMap[rangeIndex].firstGlyphIndex; - u32 endGlyphIndex = firstGlyphIndex + font->glyphMap[rangeIndex].range.count; - - for(int glyphIndex = firstGlyphIndex; - glyphIndex < endGlyphIndex; glyphIndex++) - { - mg_glyph_data* glyph = &(font->glyphs[glyphIndex-1]); - - int stbttGlyphIndex = stbtt_FindGlyphIndex(&stbttFontInfo, codePoint); - if(stbttGlyphIndex == 0) - { - //NOTE(martin): the codepoint is not found in the font, we zero the glyph info - memset(glyph, 0, sizeof(*glyph)); - codePoint++; - continue; - } - - glyph->exists = true; - glyph->codePoint = codePoint; - - //NOTE(martin): load glyph metric - int xAdvance, xBearing, x0, y0, x1, y1; - stbtt_GetGlyphHMetrics(&stbttFontInfo, stbttGlyphIndex, &xAdvance, &xBearing); - stbtt_GetGlyphBox(&stbttFontInfo, stbttGlyphIndex, &x0, &y0, &x1, &y1); - - glyph->extents.xAdvance = (f32)xAdvance; - glyph->extents.yAdvance = 0; - glyph->extents.xBearing = (f32)xBearing; - glyph->extents.yBearing = y0; - - glyph->extents.width = x1 - x0; - glyph->extents.height = y1 - y0; - - //NOTE(martin): load glyph outlines - - stbtt_vertex* vertices = 0; - int vertexCount = stbtt_GetGlyphShape(&stbttFontInfo, stbttGlyphIndex, &vertices); - - glyph->pathDescriptor = (mg_path_descriptor){.startIndex = font->outlineCount, - .count = vertexCount, - .startPoint = {0, 0}}; - - mg_path_elt* elements = font->outlines + font->outlineCount; - font->outlineCount += vertexCount; - vec2 currentPos = {0, 0}; - - for(int vertIndex = 0; vertIndex < vertexCount; vertIndex++) - { - f32 x = vertices[vertIndex].x; - f32 y = vertices[vertIndex].y; - f32 cx = vertices[vertIndex].cx; - f32 cy = vertices[vertIndex].cy; - f32 cx1 = vertices[vertIndex].cx1; - f32 cy1 = vertices[vertIndex].cy1; - - switch(vertices[vertIndex].type) - { - case STBTT_vmove: - elements[vertIndex].type = MG_PATH_MOVE; - elements[vertIndex].p[0] = (vec2){x, y}; - break; - - case STBTT_vline: - elements[vertIndex].type = MG_PATH_LINE; - elements[vertIndex].p[0] = (vec2){x, y}; - break; - - case STBTT_vcurve: - { - elements[vertIndex].type = MG_PATH_QUADRATIC; - elements[vertIndex].p[0] = (vec2){cx, cy}; - elements[vertIndex].p[1] = (vec2){x, y}; - } break; - - case STBTT_vcubic: - elements[vertIndex].type = MG_PATH_CUBIC; - elements[vertIndex].p[0] = (vec2){cx, cy}; - elements[vertIndex].p[1] = (vec2){cx1, cy1}; - elements[vertIndex].p[2] = (vec2){x, y}; - break; - } - currentPos = (vec2){x, y}; - } - stbtt_FreeShape(&stbttFontInfo, vertices); - codePoint++; - } - } - } - return(fontHandle); -} - -void mg_font_destroy(mg_font fontHandle) -{ - mg_font_data* fontData = mg_font_data_from_handle(fontHandle); - if(fontData) - { - free(fontData->glyphMap); - free(fontData->glyphs); - free(fontData->outlines); - - list_push(&__mgData.fontFreeList, &fontData->freeListElt); - mg_handle_recycle(fontHandle.h); - } -} - -str32 mg_font_get_glyph_indices_from_font_data(mg_font_data* fontData, str32 codePoints, str32 backing) -{ - u64 count = minimum(codePoints.len, backing.len); - - for(int i = 0; irangeCount; rangeIndex++) - { - if(codePoints.ptr[i] >= fontData->glyphMap[rangeIndex].range.firstCodePoint - && codePoints.ptr[i] < (fontData->glyphMap[rangeIndex].range.firstCodePoint + fontData->glyphMap[rangeIndex].range.count)) - { - u32 rangeOffset = codePoints.ptr[i] - fontData->glyphMap[rangeIndex].range.firstCodePoint; - glyphIndex = fontData->glyphMap[rangeIndex].firstGlyphIndex + rangeOffset; - break; - } - } - if(glyphIndex && !fontData->glyphs[glyphIndex].exists) - { - backing.ptr[i] = 0; - } - backing.ptr[i] = glyphIndex; - } - str32 res = {.len = count, .ptr = backing.ptr}; - return(res); -} - -u32 mg_font_get_glyph_index_from_font_data(mg_font_data* fontData, utf32 codePoint) -{ - u32 glyphIndex = 0; - str32 codePoints = {1, &codePoint}; - str32 backing = {1, &glyphIndex}; - mg_font_get_glyph_indices_from_font_data(fontData, codePoints, backing); - return(glyphIndex); -} - -str32 mg_font_get_glyph_indices(mg_font font, str32 codePoints, str32 backing) -{ - mg_font_data* fontData = mg_font_data_from_handle(font); - if(!fontData) - { - return((str32){0}); - } - return(mg_font_get_glyph_indices_from_font_data(fontData, codePoints, backing)); -} - -str32 mg_font_push_glyph_indices(mg_font font, mem_arena* arena, str32 codePoints) -{ - u32* buffer = mem_arena_alloc_array(arena, u32, codePoints.len); - str32 backing = {codePoints.len, buffer}; - return(mg_font_get_glyph_indices(font, codePoints, backing)); -} - -u32 mg_font_get_glyph_index(mg_font font, utf32 codePoint) -{ - u32 glyphIndex = 0; - str32 codePoints = {1, &codePoint}; - str32 backing = {1, &glyphIndex}; - mg_font_get_glyph_indices(font, codePoints, backing); - return(glyphIndex); -} - -mg_glyph_data* mg_font_get_glyph_data(mg_font_data* fontData, u32 glyphIndex) -{ - DEBUG_ASSERT(glyphIndex); - DEBUG_ASSERT(glyphIndex < fontData->glyphCount); - return(&(fontData->glyphs[glyphIndex-1])); -} - -mg_font_extents mg_font_get_extents(mg_font font) -{ - mg_font_data* fontData = mg_font_data_from_handle(font); - if(!fontData) - { - return((mg_font_extents){0}); - } - return(fontData->extents); -} - -mg_font_extents mg_font_get_scaled_extents(mg_font font, f32 emSize) -{ - mg_font_data* fontData = mg_font_data_from_handle(font); - if(!fontData) - { - return((mg_font_extents){0}); - } - f32 scale = emSize/fontData->unitsPerEm; - mg_font_extents extents = fontData->extents; - - extents.ascent *= scale; - extents.descent *= scale; - extents.leading *= scale; - extents.xHeight *= scale; - extents.capHeight *= scale; - extents.width *= scale; - - return(extents); -} - - -f32 mg_font_get_scale_for_em_pixels(mg_font font, f32 emSize) -{ - mg_font_data* fontData = mg_font_data_from_handle(font); - if(!fontData) - { - return(0); - } - return(emSize/fontData->unitsPerEm); -} - -void mg_font_get_glyph_extents_from_font_data(mg_font_data* fontData, - str32 glyphIndices, - mg_text_extents* outExtents) -{ - for(int i=0; i= fontData->glyphCount) - { - continue; - } - mg_glyph_data* glyph = mg_font_get_glyph_data(fontData, glyphIndices.ptr[i]); - outExtents[i] = glyph->extents; - } -} - -int mg_font_get_glyph_extents(mg_font font, str32 glyphIndices, mg_text_extents* outExtents) -{ - mg_font_data* fontData = mg_font_data_from_handle(font); - if(!fontData) - { - return(-1); - } - mg_font_get_glyph_extents_from_font_data(fontData, glyphIndices, outExtents); - return(0); -} - -int mg_font_get_codepoint_extents(mg_font font, utf32 codePoint, mg_text_extents* outExtents) -{ - mg_font_data* fontData = mg_font_data_from_handle(font); - if(!fontData) - { - return(-1); - } - u32 glyphIndex = 0; - str32 codePoints = {1, &codePoint}; - str32 backing = {1, &glyphIndex}; - str32 glyphs = mg_font_get_glyph_indices_from_font_data(fontData, codePoints, backing); - mg_font_get_glyph_extents_from_font_data(fontData, glyphs, outExtents); - return(0); -} - -mp_rect mg_text_bounding_box_utf32(mg_font font, f32 fontSize, str32 codePoints) -{ - if(!codePoints.len || !codePoints.ptr) - { - return((mp_rect){0}); - } - - mg_font_data* fontData = mg_font_data_from_handle(font); - if(!fontData) - { - return((mp_rect){0}); - } - - mem_arena* scratch = mem_scratch(); - str32 glyphIndices = mg_font_push_glyph_indices(font, scratch, codePoints); - - //NOTE(martin): find width of missing character - //TODO(martin): should cache that at font creation... - mg_text_extents missingGlyphExtents; - u32 missingGlyphIndex = mg_font_get_glyph_index_from_font_data(fontData, 0xfffd); - - if(missingGlyphIndex) - { - mg_font_get_glyph_extents_from_font_data(fontData, (str32){1, &missingGlyphIndex}, &missingGlyphExtents); - } - else - { - //NOTE(martin): could not find replacement glyph, try to get an 'x' to get a somewhat correct width - // to render an empty rectangle. Otherwise just render with the max font width - f32 boxWidth = fontData->extents.width * 0.8; - f32 xBearing = fontData->extents.width * 0.1; - f32 xAdvance = fontData->extents.width; - - missingGlyphIndex = mg_font_get_glyph_index_from_font_data(fontData, 'x'); - if(missingGlyphIndex) - { - mg_font_get_glyph_extents_from_font_data(fontData, (str32){1, &missingGlyphIndex}, &missingGlyphExtents); - } - else - { - missingGlyphExtents.xBearing = fontData->extents.width * 0.1; - missingGlyphExtents.yBearing = 0; - missingGlyphExtents.width = fontData->extents.width * 0.8; - missingGlyphExtents.xAdvance = fontData->extents.width; - missingGlyphExtents.yAdvance = 0; - } - } - - //NOTE(martin): accumulate text extents - f32 width = 0; - f32 x = 0; - f32 y = 0; - f32 lineHeight = fontData->extents.descent + fontData->extents.ascent; - - for(int i=0; i= fontData->glyphCount) - { - extents = missingGlyphExtents; - } - else - { - glyph = mg_font_get_glyph_data(fontData, glyphIndices.ptr[i]); - extents = glyph->extents; - } - x += extents.xAdvance; - y += extents.yAdvance; - - if(glyph && glyph->codePoint == '\n') - { - width = maximum(width, x); - x = 0; - y += lineHeight + fontData->extents.leading; - } - } - width = maximum(width, x); - - f32 fontScale = mg_font_get_scale_for_em_pixels(font, fontSize); - mp_rect rect = {0, -fontData->extents.ascent * fontScale, width * fontScale, (y + lineHeight) * fontScale }; - return(rect); -} - -mp_rect mg_text_bounding_box(mg_font font, f32 fontSize, str8 text) -{ - if(!text.len || !text.ptr) - { - return((mp_rect){0}); - } - - mem_arena* scratch = mem_scratch(); - str32 codePoints = utf8_push_to_codepoints(scratch, text); - return(mg_text_bounding_box_utf32(font, fontSize, codePoints)); -} - -//------------------------------------------------------------------------------------------ -//NOTE(martin): graphics canvas API -//------------------------------------------------------------------------------------------ - -mg_canvas mg_canvas_nil() { return((mg_canvas){.h = 0}); } -bool mg_canvas_is_nil(mg_canvas canvas) { return(canvas.h == 0); } - -mg_canvas mg_canvas_handle_alloc(mg_canvas_data* canvas) -{ - mg_canvas handle = {.h = mg_handle_alloc(MG_HANDLE_CANVAS, (void*)canvas) }; - return(handle); -} - -mg_canvas_data* mg_canvas_data_from_handle(mg_canvas handle) -{ - mg_canvas_data* data = mg_data_from_handle(MG_HANDLE_CANVAS, handle.h); - return(data); -} - -mg_canvas mg_canvas_create() -{ - if(!__mgData.init) - { - mg_init(); - } - - mg_canvas canvasHandle = mg_canvas_nil(); - mg_canvas_data* canvas = list_pop_entry(&__mgData.canvasFreeList, mg_canvas_data, freeListElt); - if(!canvas) - { - canvas = mem_arena_alloc_type(&__mgData.resourceArena, mg_canvas_data); - } - if(canvas) - { - canvas->textFlip = false; - canvas->path = (mg_path_descriptor){0}; - canvas->matrixStackSize = 0; - canvas->clipStackSize = 0; - canvas->primitiveCount = 0; - canvas->clearColor = (mg_color){0, 0, 0, 0}; - - canvas->attributes = (mg_attributes){0}; - canvas->attributes.color = (mg_color){0, 0, 0, 1}; - canvas->attributes.tolerance = 1; - canvas->attributes.width = 10; - canvas->attributes.clip = (mp_rect){-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX}; - - canvasHandle = mg_canvas_handle_alloc(canvas); - - mg_canvas_set_current(canvasHandle); - } - return(canvasHandle); -} - -void mg_canvas_destroy(mg_canvas handle) -{ - mg_canvas_data* canvas = mg_canvas_data_from_handle(handle); - if(canvas) - { - if(__mgCurrentCanvas == canvas) - { - __mgCurrentCanvas = 0; - __mgCurrentCanvasHandle = mg_canvas_nil(); - } - list_push(&__mgData.canvasFreeList, &canvas->freeListElt); - mg_handle_recycle(handle.h); - } -} - -mg_canvas mg_canvas_set_current(mg_canvas canvas) -{ - mg_canvas old = __mgCurrentCanvasHandle; - __mgCurrentCanvasHandle = canvas; - __mgCurrentCanvas = mg_canvas_data_from_handle(canvas); - return(old); -} - -void mg_render(mg_surface surface, mg_canvas canvas) -{ - mg_canvas_data* canvasData = mg_canvas_data_from_handle(canvas); - if(canvasData) - { - int eltCount = canvasData->path.startIndex + canvasData->path.count; - mg_surface_render_commands(surface, - canvasData->clearColor, - canvasData->primitiveCount, - canvasData->primitives, - eltCount, - canvasData->pathElements); - - canvasData->primitiveCount = 0; - canvasData->path.startIndex = 0; - canvasData->path.count = 0; - } -} - -//------------------------------------------------------------------------------------------ -//NOTE(martin): transform, viewport and clipping -//------------------------------------------------------------------------------------------ - -void mg_matrix_push(mg_mat2x3 matrix) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - mg_mat2x3 transform = mg_matrix_stack_top(canvas); - mg_matrix_stack_push(canvas, mg_mat2x3_mul_m(transform, matrix)); - } -} - -void mg_matrix_pop() -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - mg_matrix_stack_pop(canvas); - } -} - -void mg_clip_push(f32 x, f32 y, f32 w, f32 h) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - mp_rect clip = {x, y, w, h}; - - //NOTE(martin): transform clip - mg_mat2x3 transform = mg_matrix_stack_top(canvas); - vec2 p0 = mg_mat2x3_mul(transform, (vec2){clip.x, clip.y}); - vec2 p1 = mg_mat2x3_mul(transform, (vec2){clip.x + clip.w, clip.y}); - vec2 p2 = mg_mat2x3_mul(transform, (vec2){clip.x + clip.w, clip.y + clip.h}); - vec2 p3 = mg_mat2x3_mul(transform, (vec2){clip.x, clip.y + clip.h}); - - f32 x0 = minimum(p0.x, minimum(p1.x, minimum(p2.x, p3.x))); - f32 y0 = minimum(p0.y, minimum(p1.y, minimum(p2.y, p3.y))); - f32 x1 = maximum(p0.x, maximum(p1.x, maximum(p2.x, p3.x))); - f32 y1 = maximum(p0.y, maximum(p1.y, maximum(p2.y, p3.y))); - - mp_rect current = mg_clip_stack_top(canvas); - - //NOTE(martin): intersect with current clip - x0 = maximum(current.x, x0); - y0 = maximum(current.y, y0); - x1 = minimum(current.x + current.w, x1); - y1 = minimum(current.y + current.h, y1); - - mp_rect r = {x0, y0, maximum(0, x1-x0), maximum(0, y1-y0)}; - mg_clip_stack_push(canvas, r); - - canvas->attributes.clip = r; - } -} - -void mg_clip_pop() -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - mg_clip_stack_pop(canvas); - canvas->attributes.clip = mg_clip_stack_top(canvas); - } -} - -//------------------------------------------------------------------------------------------ -//NOTE(martin): graphics attributes setting/getting -//------------------------------------------------------------------------------------------ -void mg_set_color(mg_color color) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - canvas->attributes.color = color; - } -} - -void mg_set_color_rgba(f32 r, f32 g, f32 b, f32 a) -{ - mg_set_color((mg_color){r, g, b, a}); -} - -void mg_set_width(f32 width) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - canvas->attributes.width = width; - } -} - -void mg_set_tolerance(f32 tolerance) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - canvas->attributes.tolerance = tolerance; - } -} - -void mg_set_joint(mg_joint_type joint) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - canvas->attributes.joint = joint; - } -} - -void mg_set_max_joint_excursion(f32 maxJointExcursion) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - canvas->attributes.maxJointExcursion = maxJointExcursion; - } -} - -void mg_set_cap(mg_cap_type cap) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - canvas->attributes.cap = cap; - } -} - -void mg_set_font(mg_font font) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - canvas->attributes.font = font; - } -} - -void mg_set_font_size(f32 fontSize) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - canvas->attributes.fontSize = fontSize; - } -} - -void mg_set_text_flip(bool flip) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - canvas->textFlip = flip; - } -} - -void mg_set_image(mg_image image) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - canvas->attributes.image = image; - vec2 size = mg_image_size(image); - canvas->attributes.srcRegion = (mp_rect){0, 0, size.x, size.y}; - } -} - -void mg_set_image_source_region(mp_rect region) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - canvas->attributes.srcRegion = region; - } -} - -mg_color mg_get_color() -{ - mg_color color = {0}; - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - color = canvas->attributes.color; - } - return(color); -} - -f32 mg_get_width() -{ - f32 width = 0; - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - width = canvas->attributes.width; - } - return(width); -} - -f32 mg_get_tolerance() -{ - f32 tolerance = 0; - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - tolerance = canvas->attributes.tolerance; - } - return(tolerance); -} - -mg_joint_type mg_get_joint() -{ - mg_joint_type joint = 0; - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - joint = canvas->attributes.joint; - } - return(joint); -} - -f32 mg_get_max_joint_excursion() -{ - f32 maxJointExcursion = 0; - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - maxJointExcursion = canvas->attributes.maxJointExcursion; - } - return(maxJointExcursion); -} - -mg_cap_type mg_get_cap() -{ - mg_cap_type cap = 0; - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - cap = canvas->attributes.cap; - } - return(cap); -} - -mg_font mg_get_font() -{ - mg_font font = mg_font_nil(); - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - font = canvas->attributes.font; - } - return(font); -} - -f32 mg_get_font_size() -{ - f32 fontSize = 0; - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - fontSize = canvas->attributes.fontSize; - } - return(fontSize); -} - -bool mg_get_text_flip() -{ - bool flip = false; - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - flip = canvas->textFlip; - } - return(flip); -} - -//------------------------------------------------------------------------------------------ -//NOTE(martin): path construction -//------------------------------------------------------------------------------------------ -vec2 mg_get_position() -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(!canvas) - { - return((vec2){0, 0}); - } - return(canvas->subPathLastPoint); -} - -void mg_move_to(f32 x, f32 y) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(!canvas) - { - return; - } - mg_path_push_element(canvas, ((mg_path_elt){.type = MG_PATH_MOVE, .p[0] = {x, y}})); - canvas->subPathStartPoint = (vec2){x, y}; - canvas->subPathLastPoint = (vec2){x, y}; -} - -void mg_line_to(f32 x, f32 y) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(!canvas) - { - return; - } - mg_path_push_element(canvas, ((mg_path_elt){.type = MG_PATH_LINE, .p[0] = {x, y}})); - canvas->subPathLastPoint = (vec2){x, y}; -} - -void mg_quadratic_to(f32 x1, f32 y1, f32 x2, f32 y2) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(!canvas) - { - return; - } - mg_path_push_element(canvas, ((mg_path_elt){.type = MG_PATH_QUADRATIC, .p = {{x1, y1}, {x2, y2}}})); - canvas->subPathLastPoint = (vec2){x2, y2}; -} - -void mg_cubic_to(f32 x1, f32 y1, f32 x2, f32 y2, f32 x3, f32 y3) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(!canvas) - { - return; - } - mg_path_push_element(canvas, ((mg_path_elt){.type = MG_PATH_CUBIC, .p = {{x1, y1}, {x2, y2}, {x3, y3}}})); - canvas->subPathLastPoint = (vec2){x3, y3}; -} - -void mg_close_path() -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(!canvas) - { - return; - } - if( canvas->subPathStartPoint.x != canvas->subPathLastPoint.x - || canvas->subPathStartPoint.y != canvas->subPathLastPoint.y) - { - mg_line_to(canvas->subPathStartPoint.x, canvas->subPathStartPoint.y); - } - canvas->subPathStartPoint = canvas->subPathLastPoint; -} - -mp_rect mg_glyph_outlines_from_font_data(mg_font_data* fontData, str32 glyphIndices) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - - f32 startX = canvas->subPathLastPoint.x; - f32 startY = canvas->subPathLastPoint.y; - f32 maxWidth = 0; - - f32 scale = canvas->attributes.fontSize/fontData->unitsPerEm; - - for(int i=0; isubPathLastPoint.x; - f32 yOffset = canvas->subPathLastPoint.y; - f32 flip = canvas->textFlip ? 1 : -1; - - if(!glyphIndex || glyphIndex >= fontData->glyphCount) - { - log_warning("code point is not present in font ranges\n"); - //NOTE(martin): try to find the replacement character - glyphIndex = mg_font_get_glyph_index_from_font_data(fontData, 0xfffd); - if(!glyphIndex) - { - //NOTE(martin): could not find replacement glyph, try to get an 'x' to get a somewhat correct width - // to render an empty rectangle. Otherwise just render with the max font width - f32 boxWidth = fontData->extents.width * 0.8; - f32 xBearing = fontData->extents.width * 0.1; - f32 xAdvance = fontData->extents.width; - - glyphIndex = mg_font_get_glyph_index_from_font_data(fontData, 'x'); - if(glyphIndex) - { - mg_glyph_data* glyph = &(fontData->glyphs[glyphIndex]); - boxWidth = glyph->extents.width; - xBearing = glyph->extents.xBearing; - xAdvance = glyph->extents.xAdvance; - } - f32 oldStrokeWidth = canvas->attributes.width; - - mg_set_width(boxWidth*0.005); - mg_rectangle_stroke(xOffset + xBearing * scale, - yOffset, - boxWidth * scale * flip, - fontData->extents.capHeight*scale); - - mg_set_width(oldStrokeWidth); - mg_move_to(xOffset + xAdvance * scale, yOffset); - maxWidth = maximum(maxWidth, xOffset + xAdvance*scale - startX); - continue; - } - } - - mg_glyph_data* glyph = mg_font_get_glyph_data(fontData, glyphIndex); - - mg_path_push_elements(canvas, glyph->pathDescriptor.count, fontData->outlines + glyph->pathDescriptor.startIndex); - - mg_path_elt* elements = canvas->pathElements + canvas->path.count + canvas->path.startIndex - glyph->pathDescriptor.count; - for(int eltIndex=0; eltIndexpathDescriptor.count; eltIndex++) - { - for(int pIndex = 0; pIndex < 3; pIndex++) - { - elements[eltIndex].p[pIndex].x = elements[eltIndex].p[pIndex].x * scale + xOffset; - elements[eltIndex].p[pIndex].y = elements[eltIndex].p[pIndex].y * scale * flip + yOffset; - } - } - mg_move_to(xOffset + scale*glyph->extents.xAdvance, yOffset); - - maxWidth = maximum(maxWidth, xOffset + scale*glyph->extents.xAdvance - startX); - } - f32 lineHeight = (fontData->extents.ascent + fontData->extents.descent)*scale; - mp_rect box = {startX, startY, maxWidth, canvas->subPathLastPoint.y - startY + lineHeight }; - return(box); -} - -mp_rect mg_glyph_outlines(str32 glyphIndices) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(!canvas) - { - return((mp_rect){0}); - } - mg_font_data* fontData = mg_font_data_from_handle(canvas->attributes.font); - if(!fontData) - { - return((mp_rect){0}); - } - return(mg_glyph_outlines_from_font_data(fontData, glyphIndices)); -} - -void mg_codepoints_outlines(str32 codePoints) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(!canvas) - { - return; - } - mg_font_data* fontData = mg_font_data_from_handle(canvas->attributes.font); - if(!fontData) - { - return; - } - - str32 glyphIndices = mg_font_push_glyph_indices(canvas->attributes.font, mem_scratch(), codePoints); - mg_glyph_outlines_from_font_data(fontData, glyphIndices); -} - -void mg_text_outlines(str8 text) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(!canvas) - { - return; - } - mg_font_data* fontData = mg_font_data_from_handle(canvas->attributes.font); - if(!fontData) - { - return; - } - - mem_arena* scratch = mem_scratch(); - str32 codePoints = utf8_push_to_codepoints(scratch, text); - str32 glyphIndices = mg_font_push_glyph_indices(canvas->attributes.font, scratch, codePoints); - - mg_glyph_outlines_from_font_data(fontData, glyphIndices); -} - -//------------------------------------------------------------------------------------------ -//NOTE(martin): clear/fill/stroke -//------------------------------------------------------------------------------------------ - -void mg_clear() -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - canvas->primitiveCount = 0; - canvas->clearColor = canvas->attributes.color; - } -} - -void mg_fill() -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas && canvas->path.count) - { - mg_push_command(canvas, (mg_primitive){.cmd = MG_CMD_FILL, .path = canvas->path}); - mg_new_path(canvas); - } -} - -void mg_stroke() -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas && canvas->path.count) - { - mg_push_command(canvas, (mg_primitive){.cmd = MG_CMD_STROKE, .path = canvas->path}); - mg_new_path(canvas); - } -} - -//------------------------------------------------------------------------------------------ -//NOTE(martin): simple shape helpers -//------------------------------------------------------------------------------------------ - -void mg_rectangle_path(f32 x, f32 y, f32 w, f32 h) -{ - mg_move_to(x, y); - mg_line_to(x+w, y); - mg_line_to(x+w, y+h); - mg_line_to(x, y+h); - mg_close_path(); -} - -void mg_rectangle_fill(f32 x, f32 y, f32 w, f32 h) -{ - mg_rectangle_path(x, y, w, h); - mg_fill(); -} - -void mg_rectangle_stroke(f32 x, f32 y, f32 w, f32 h) -{ - mg_rectangle_path(x, y, w, h); - mg_stroke(); -} - -void mg_rounded_rectangle_path(f32 x, f32 y, f32 w, f32 h, f32 r) -{ - f32 c = r*4*(sqrt(2)-1)/3; - - mg_move_to(x+r, y); - mg_line_to(x+w-r, y); - mg_cubic_to(x+w-r+c, y, x+w, y+r-c, x+w, y+r); - mg_line_to(x+w, y+h-r); - mg_cubic_to(x+w, y+h-r+c, x+w-r+c, y+h, x+w-r, y+h); - mg_line_to(x+r, y+h); - mg_cubic_to(x+r-c, y+h, x, y+h-r+c, x, y+h-r); - mg_line_to(x, y+r); - mg_cubic_to(x, y+r-c, x+r-c, y, x+r, y); -} - -void mg_rounded_rectangle_fill(f32 x, f32 y, f32 w, f32 h, f32 r) -{ - mg_rounded_rectangle_path(x, y, w, h, r); - mg_fill(); -} - -void mg_rounded_rectangle_stroke(f32 x, f32 y, f32 w, f32 h, f32 r) -{ - mg_rounded_rectangle_path(x, y, w, h, r); - mg_stroke(); -} - -void mg_ellipse_path(f32 x, f32 y, f32 rx, f32 ry) -{ - f32 cx = rx*4*(sqrt(2)-1)/3; - f32 cy = ry*4*(sqrt(2)-1)/3; - - mg_move_to(x-rx, y); - mg_cubic_to(x-rx, y+cy, x-cx, y+ry, x, y+ry); - mg_cubic_to(x+cx, y+ry, x+rx, y+cy, x+rx, y); - mg_cubic_to(x+rx, y-cy, x+cx, y-ry, x, y-ry); - mg_cubic_to(x-cx, y-ry, x-rx, y-cy, x-rx, y); -} - -void mg_ellipse_fill(f32 x, f32 y, f32 rx, f32 ry) -{ - mg_ellipse_path(x, y, rx, ry); - mg_fill(); -} - -void mg_ellipse_stroke(f32 x, f32 y, f32 rx, f32 ry) -{ - mg_ellipse_path(x, y, rx, ry); - mg_stroke(); -} - -void mg_circle_fill(f32 x, f32 y, f32 r) -{ - mg_ellipse_fill(x, y, r, r); -} - -void mg_circle_stroke(f32 x, f32 y, f32 r) -{ - mg_ellipse_stroke(x, y, r, r); -} - -//TODO: change to arc_to? -void mg_arc(f32 x, f32 y, f32 r, f32 arcAngle, f32 startAngle) -{ - f32 endAngle = startAngle + arcAngle; - - while(startAngle < endAngle) - { - f32 smallAngle = minimum(endAngle - startAngle, M_PI/4.); - if(smallAngle < 0.001) - { - break; - } - - vec2 v0 = {cos(smallAngle/2), sin(smallAngle/2)}; - vec2 v1 = {(4-v0.x)/3, (1-v0.x)*(3-v0.x)/(3*v0.y)}; - vec2 v2 = {v1.x, -v1.y}; - vec2 v3 = {v0.x, -v0.y}; - - f32 rotAngle = smallAngle/2 + startAngle; - f32 rotCos = cos(rotAngle); - f32 rotSin = sin(rotAngle); - - mg_mat2x3 t = {r*rotCos, -r*rotSin, x, - r*rotSin, r*rotCos, y}; - - v0 = mg_mat2x3_mul(t, v0); - v1 = mg_mat2x3_mul(t, v1); - v2 = mg_mat2x3_mul(t, v2); - v3 = mg_mat2x3_mul(t, v3); - - mg_move_to(v0.x, v0.y); - mg_cubic_to(v1.x, v1.y, v2.x, v2.y, v3.x, v3.y); - - startAngle += smallAngle; - } -} - -//------------------------------------------------------------------------------------------ -//NOTE(martin): images -//------------------------------------------------------------------------------------------ - -mg_image mg_image_nil() { return((mg_image){.h = 0}); } -bool mg_image_is_nil(mg_image image) { return(image.h == 0); } - -mg_image mg_image_create_from_rgba8(mg_surface surface, u32 width, u32 height, u8* pixels) -{ - mg_image image = mg_image_create(surface, width, height); - if(!mg_image_is_nil(image)) - { - mg_image_upload_region_rgba8(image, (mp_rect){0, 0, width, height}, pixels); - } - return(image); -} - -mg_image mg_image_create_from_data(mg_surface surface, str8 data, bool flip) -{ - mg_image image = mg_image_nil(); - int width, height, channels; - - stbi_set_flip_vertically_on_load(flip ? 1 : 0); - u8* pixels = stbi_load_from_memory((u8*)data.ptr, data.len, &width, &height, &channels, 4); - - if(pixels) - { - image = mg_image_create_from_rgba8(surface, width, height, pixels); - free(pixels); - } - return(image); -} - -#if !PLATFORM_ORCA - -mg_image mg_image_create_from_file(mg_surface surface, str8 path, bool flip) -{ - mg_image image = mg_image_nil(); - int width, height, channels; - - const char* cpath = str8_to_cstring(mem_scratch(), path); - - stbi_set_flip_vertically_on_load(flip ? 1 : 0); - u8* pixels = stbi_load(cpath, &width, &height, &channels, 4); - if(pixels) - { - image = mg_image_create_from_rgba8(surface, width, height, pixels); - free(pixels); - } - return(image); -} - -#endif // !PLATFORM_ORCA - - -void mg_image_draw_region(mg_image image, mp_rect srcRegion, mp_rect dstRegion) -{ - mg_canvas_data* canvas = __mgCurrentCanvas; - if(canvas) - { - mg_image oldImage = canvas->attributes.image; - mp_rect oldSrcRegion = canvas->attributes.srcRegion; - mg_color oldColor = canvas->attributes.color; - - canvas->attributes.image = image; - canvas->attributes.srcRegion = srcRegion; - canvas->attributes.color = (mg_color){1, 1, 1, 1}; - - mg_move_to(dstRegion.x, dstRegion.y); - mg_line_to(dstRegion.x+dstRegion.w, dstRegion.y); - mg_line_to(dstRegion.x+dstRegion.w, dstRegion.y+dstRegion.h); - mg_line_to(dstRegion.x, dstRegion.y+dstRegion.h); - mg_close_path(); - - mg_fill(); - - canvas->attributes.image = oldImage; - canvas->attributes.srcRegion = oldSrcRegion; - canvas->attributes.color = oldColor; - } -} - -void mg_image_draw(mg_image image, mp_rect rect) -{ - vec2 size = mg_image_size(image); - mg_image_draw_region(image, (mp_rect){0, 0, size.x, size.y}, rect); -} - -//------------------------------------------------------------------------------------------ -//NOTE(martin): atlasing -//------------------------------------------------------------------------------------------ - -//NOTE: rectangle allocator -typedef struct mg_rect_atlas -{ - mem_arena* arena; - ivec2 size; - ivec2 pos; - u32 lineHeight; - -} mg_rect_atlas; - -mg_rect_atlas* mg_rect_atlas_create(mem_arena* arena, i32 width, i32 height) -{ - mg_rect_atlas* atlas = mem_arena_alloc_type(arena, mg_rect_atlas); - memset(atlas, 0, sizeof(mg_rect_atlas)); - atlas->arena = arena; - atlas->size = (ivec2){width, height}; - return(atlas); -} - -mp_rect mg_rect_atlas_alloc(mg_rect_atlas* atlas, i32 width, i32 height) -{ - mp_rect rect = {0, 0, 0, 0}; - if(width > 0 && height > 0) - { - if(atlas->pos.x + width >= atlas->size.x) - { - atlas->pos.x = 0; - atlas->pos.y += (atlas->lineHeight + 1); - atlas->lineHeight = 0; - } - if( atlas->pos.x + width < atlas->size.x - && atlas->pos.y + height < atlas->size.y) - { - rect = (mp_rect){atlas->pos.x, atlas->pos.y, width, height}; - - atlas->pos.x += (width + 1); - atlas->lineHeight = maximum(atlas->lineHeight, height); - } - } - return(rect); -} - -void mg_rect_atlas_recycle(mg_rect_atlas* atlas, mp_rect rect) -{ - //TODO -} - -mg_image_region mg_image_atlas_alloc_from_rgba8(mg_rect_atlas* atlas, mg_image backingImage, u32 width, u32 height, u8* pixels) -{ - mg_image_region imageRgn = {0}; - - mp_rect rect = mg_rect_atlas_alloc(atlas, width, height); - if(rect.w == width && rect.h == height) - { - mg_image_upload_region_rgba8(backingImage, rect, pixels); - imageRgn.rect = rect; - imageRgn.image = backingImage; - } - return(imageRgn); -} - -#if !PLATFORM_ORCA - -mg_image_region mg_image_atlas_alloc_from_data(mg_rect_atlas* atlas, mg_image backingImage, str8 data, bool flip) -{ - mg_image_region imageRgn = {0}; - - stbi_set_flip_vertically_on_load(flip ? 1 : 0); - - int width, height, channels; - u8* pixels = stbi_load_from_memory((u8*)data.ptr, data.len, &width, &height, &channels, 4); - if(pixels) - { - imageRgn = mg_image_atlas_alloc_from_rgba8(atlas, backingImage, width, height, pixels); - free(pixels); - } - return(imageRgn); -} - -mg_image_region mg_image_atlas_alloc_from_file(mg_rect_atlas* atlas, mg_image backingImage, str8 path, bool flip) -{ - mg_image_region imageRgn = {0}; - - stbi_set_flip_vertically_on_load(flip ? 1 : 0); - - const char* cpath = str8_to_cstring(mem_scratch(), path); - int width, height, channels; - u8* pixels = stbi_load(cpath, &width, &height, &channels, 4); - if(pixels) - { - imageRgn = mg_image_atlas_alloc_from_rgba8(atlas, backingImage, width, height, pixels); - free(pixels); - } - return(imageRgn); -} -#endif // !PLATFORM_ORCA - -void mg_image_atlas_recycle(mg_rect_atlas* atlas, mg_image_region imageRgn) -{ - mg_rect_atlas_recycle(atlas, imageRgn.rect); -} +/************************************************************//** +* +* @file: graphics_common.c +* @author: Martin Fouilleul +* @date: 23/01/2023 +* @revision: +* +*****************************************************************/ + +#include"platform/platform.h" +#include"platform/platform_math.h" + +#define STB_IMAGE_IMPLEMENTATION +#if PLATFORM_ORCA + #define STBI_NO_STDIO + #define STBI_NO_HDR +#endif +#include"stb_image.h" + +#define STB_TRUETYPE_IMPLEMENTATION +#include"stb_truetype.h" + +#include"platform/platform_log.h" +#include"platform/platform_assert.h" +#include"graphics_common.h" + +typedef struct mg_glyph_map_entry +{ + unicode_range range; + u32 firstGlyphIndex; + +} mg_glyph_map_entry; + +typedef struct mg_glyph_data +{ + bool exists; + utf32 codePoint; + mg_path_descriptor pathDescriptor; + mg_text_extents extents; + //... + +} mg_glyph_data; + +enum +{ + MG_MATRIX_STACK_MAX_DEPTH = 64, + MG_CLIP_STACK_MAX_DEPTH = 64, + MG_MAX_PATH_ELEMENT_COUNT = 2<<20, + MG_MAX_PRIMITIVE_COUNT = 8<<10 +}; + +typedef struct mg_font_data +{ + list_elt freeListElt; + + u32 rangeCount; + u32 glyphCount; + u32 outlineCount; + mg_glyph_map_entry* glyphMap; + mg_glyph_data* glyphs; + mg_path_elt* outlines; + + f32 unitsPerEm; + mg_font_extents extents; + +} mg_font_data; + +typedef struct mg_canvas_data mg_canvas_data; + +typedef enum mg_handle_kind +{ + MG_HANDLE_NONE = 0, + MG_HANDLE_SURFACE, + MG_HANDLE_CANVAS, + MG_HANDLE_FONT, + MG_HANDLE_IMAGE, + MG_HANDLE_SURFACE_SERVER, +} mg_handle_kind; + +typedef struct mg_handle_slot +{ + list_elt freeListElt; + u32 generation; + mg_handle_kind kind; + + void* data; + +} mg_handle_slot; + +enum { MG_HANDLES_MAX_COUNT = 512 }; + +typedef struct mg_data +{ + bool init; + + mg_handle_slot handleArray[MG_HANDLES_MAX_COUNT]; + int handleNextIndex; + list_info handleFreeList; + + mem_arena resourceArena; + list_info canvasFreeList; + list_info fontFreeList; + +} mg_data; + +typedef struct mg_canvas_data +{ + list_elt freeListElt; + + mg_attributes attributes; + bool textFlip; + + mg_path_elt pathElements[MG_MAX_PATH_ELEMENT_COUNT]; + mg_path_descriptor path; + vec2 subPathStartPoint; + vec2 subPathLastPoint; + + mg_mat2x3 matrixStack[MG_MATRIX_STACK_MAX_DEPTH]; + u32 matrixStackSize; + + mp_rect clipStack[MG_CLIP_STACK_MAX_DEPTH]; + u32 clipStackSize; + + u32 primitiveCount; + mg_primitive primitives[MG_MAX_PRIMITIVE_COUNT]; + + //NOTE: these are used at render time + mg_color clearColor; + + vec4 shapeExtents; + vec4 shapeScreenExtents; + +} mg_canvas_data; + +static mg_data __mgData = {0}; + + +void mg_init() +{ + if(!__mgData.init) + { + __mgData.handleNextIndex = 0; + mem_arena_init(&__mgData.resourceArena); + __mgData.init = true; + } +} + +//------------------------------------------------------------------------ +// handle pools procedures +//------------------------------------------------------------------------ + +u64 mg_handle_alloc(mg_handle_kind kind, void* data) +{ + if(!__mgData.init) + { + mg_init(); + } + + mg_handle_slot* slot = list_pop_entry(&__mgData.handleFreeList, mg_handle_slot, freeListElt); + if(!slot && __mgData.handleNextIndex < MG_HANDLES_MAX_COUNT) + { + slot = &__mgData.handleArray[__mgData.handleNextIndex]; + __mgData.handleNextIndex++; + + slot->generation = 1; + } + u64 h = 0; + if(slot) + { + slot->kind = kind; + slot->data = data; + + h = ((u64)(slot - __mgData.handleArray))<<32 + |((u64)(slot->generation)); + } + return(h); +} + +void mg_handle_recycle(u64 h) +{ + DEBUG_ASSERT(__mgData.init); + + u32 index = h>>32; + u32 generation = h & 0xffffffff; + + if(index*sizeof(mg_handle_slot) < __mgData.handleNextIndex) + { + mg_handle_slot* slot = &__mgData.handleArray[index]; + if(slot->generation == generation) + { + DEBUG_ASSERT(slot->generation != UINT32_MAX, "surface slot generation wrap around\n"); + slot->generation++; + list_push(&__mgData.handleFreeList, &slot->freeListElt); + } + } +} + +void* mg_data_from_handle(mg_handle_kind kind, u64 h) +{ + DEBUG_ASSERT(__mgData.init); + + void* data = 0; + + u32 index = h>>32; + u32 generation = h & 0xffffffff; + + if(index < __mgData.handleNextIndex) + { + mg_handle_slot* slot = &__mgData.handleArray[index]; + if( slot->generation == generation + && slot->kind == kind) + { + data = slot->data; + } + } + return(data); +} + +//------------------------------------------------------------------------------------------ +//NOTE(martin): graphics canvas internal +//------------------------------------------------------------------------------------------ + +mp_thread_local mg_canvas_data* __mgCurrentCanvas = 0; +mp_thread_local mg_canvas __mgCurrentCanvasHandle = {0}; + +//TODO put these elsewhere +bool vec2_equal(vec2 v0, vec2 v1) +{ + return(v0.x == v1.x && v0.y == v1.y); +} + +bool vec2_close(vec2 p0, vec2 p1, f32 tolerance) +{ + f32 norm2 = (p1.x - p0.x)*(p1.x - p0.x) + (p1.y - p0.y)*(p1.y - p0.y); + return(fabs(norm2) < tolerance); +} + +vec2 vec2_mul(f32 f, vec2 v) +{ + return((vec2){f*v.x, f*v.y}); +} + +vec2 vec2_add(vec2 v0, vec2 v1) +{ + return((vec2){v0.x + v1.x, v0.y + v1.y}); +} + +mg_mat2x3 mg_mat2x3_mul_m(mg_mat2x3 lhs, mg_mat2x3 rhs) +{ + mg_mat2x3 res; + res.m[0] = lhs.m[0]*rhs.m[0] + lhs.m[1]*rhs.m[3]; + res.m[1] = lhs.m[0]*rhs.m[1] + lhs.m[1]*rhs.m[4]; + res.m[2] = lhs.m[0]*rhs.m[2] + lhs.m[1]*rhs.m[5] + lhs.m[2]; + res.m[3] = lhs.m[3]*rhs.m[0] + lhs.m[4]*rhs.m[3]; + res.m[4] = lhs.m[3]*rhs.m[1] + lhs.m[4]*rhs.m[4]; + res.m[5] = lhs.m[3]*rhs.m[2] + lhs.m[4]*rhs.m[5] + lhs.m[5]; + + return(res); +} + +mg_mat2x3 mg_mat2x3_inv(mg_mat2x3 x) +{ + mg_mat2x3 res; + res.m[0] = x.m[4]/(x.m[0]*x.m[4] - x.m[1]*x.m[3]); + res.m[1] = x.m[1]/(x.m[1]*x.m[3] - x.m[0]*x.m[4]); + res.m[3] = x.m[3]/(x.m[1]*x.m[3] - x.m[0]*x.m[4]); + res.m[4] = x.m[0]/(x.m[0]*x.m[4] - x.m[1]*x.m[3]); + res.m[2] = -(x.m[2]*res.m[0] + x.m[5]*res.m[1]); + res.m[5] = -(x.m[2]*res.m[3] + x.m[5]*res.m[4]); + return(res); +} + +vec2 mg_mat2x3_mul(mg_mat2x3 m, vec2 p) +{ + f32 x = p.x*m.m[0] + p.y*m.m[1] + m.m[2]; + f32 y = p.x*m.m[3] + p.y*m.m[4] + m.m[5]; + return((vec2){x, y}); +} + +mg_mat2x3 mg_matrix_stack_top(mg_canvas_data* canvas) +{ + if(canvas->matrixStackSize == 0) + { + return((mg_mat2x3){1, 0, 0, + 0, 1, 0}); + } + else + { + return(canvas->matrixStack[canvas->matrixStackSize-1]); + } +} + +void mg_matrix_stack_push(mg_canvas_data* canvas, mg_mat2x3 transform) +{ + if(canvas->matrixStackSize >= MG_MATRIX_STACK_MAX_DEPTH) + { + log_error("matrix stack overflow\n"); + } + else + { + canvas->matrixStack[canvas->matrixStackSize] = transform; + canvas->matrixStackSize++; + } +} + +void mg_matrix_stack_pop(mg_canvas_data* canvas) +{ + if(canvas->matrixStackSize == 0) + { + log_error("matrix stack underflow\n"); + } + else + { + canvas->matrixStackSize--; + mg_matrix_stack_top(canvas); + } +} + +mp_rect mg_clip_stack_top(mg_canvas_data* canvas) +{ + if(canvas->clipStackSize == 0) + { + return((mp_rect){-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX}); + } + else + { + return(canvas->clipStack[canvas->clipStackSize-1]); + } +} + +void mg_clip_stack_push(mg_canvas_data* canvas, mp_rect clip) +{ + if(canvas->clipStackSize >= MG_CLIP_STACK_MAX_DEPTH) + { + log_error("clip stack overflow\n"); + } + else + { + canvas->clipStack[canvas->clipStackSize] = clip; + canvas->clipStackSize++; + } +} + +void mg_clip_stack_pop(mg_canvas_data* canvas) +{ + if(canvas->clipStackSize == 0) + { + log_error("clip stack underflow\n"); + } + else + { + canvas->clipStackSize--; + } +} + +void mg_push_command(mg_canvas_data* canvas, mg_primitive primitive) +{ + //NOTE(martin): push primitive and updates current stream, eventually patching a pending jump. + ASSERT(canvas->primitiveCount < MG_MAX_PRIMITIVE_COUNT); + + canvas->primitives[canvas->primitiveCount] = primitive; + canvas->primitives[canvas->primitiveCount].attributes = canvas->attributes; + canvas->primitives[canvas->primitiveCount].attributes.transform = mg_matrix_stack_top(canvas); + canvas->primitives[canvas->primitiveCount].attributes.clip = mg_clip_stack_top(canvas); + canvas->primitiveCount++; +} + +void mg_new_path(mg_canvas_data* canvas) +{ + canvas->path.startIndex += canvas->path.count; + canvas->path.count = 0; + canvas->subPathStartPoint = canvas->subPathLastPoint; + canvas->path.startPoint = canvas->subPathStartPoint; +} + +void mg_path_push_elements(mg_canvas_data* canvas, u32 count, mg_path_elt* elements) +{ + ASSERT(canvas->path.count + canvas->path.startIndex + count < MG_MAX_PATH_ELEMENT_COUNT); + memcpy(canvas->pathElements + canvas->path.startIndex + canvas->path.count, elements, count*sizeof(mg_path_elt)); + canvas->path.count += count; + + ASSERT(canvas->path.count < MG_MAX_PATH_ELEMENT_COUNT); +} + +void mg_path_push_element(mg_canvas_data* canvas, mg_path_elt elt) +{ + mg_path_push_elements(canvas, 1, &elt); +} + +//------------------------------------------------------------------------------------------ +//NOTE(martin): fonts +//------------------------------------------------------------------------------------------ + +mg_font mg_font_nil() { return((mg_font){.h = 0}); } +bool mg_font_is_nil(mg_font font) { return(font.h == 0); } + +mg_font mg_font_handle_alloc(mg_font_data* font) +{ + mg_font handle = {.h = mg_handle_alloc(MG_HANDLE_FONT, (void*)font) }; + return(handle); +} + +mg_font_data* mg_font_data_from_handle(mg_font handle) +{ + mg_font_data* data = mg_data_from_handle(MG_HANDLE_FONT, handle.h); + return(data); +} + +mg_font mg_font_create_from_memory(u32 size, byte* buffer, u32 rangeCount, unicode_range* ranges) +{ + if(!__mgData.init) + { + mg_init(); + } + mg_font fontHandle = mg_font_nil(); + + mg_font_data* font = list_pop_entry(&__mgData.fontFreeList, mg_font_data, freeListElt); + if(!font) + { + font = mem_arena_alloc_type(&__mgData.resourceArena, mg_font_data); + } + if(font) + { + memset(font, 0, sizeof(mg_font_data)); + fontHandle = mg_font_handle_alloc(font); + + stbtt_fontinfo stbttFontInfo; + stbtt_InitFont(&stbttFontInfo, buffer, 0); + + //NOTE(martin): load font metrics data + font->unitsPerEm = 1./stbtt_ScaleForMappingEmToPixels(&stbttFontInfo, 1); + + int ascent, descent, lineGap, x0, x1, y0, y1; + stbtt_GetFontVMetrics(&stbttFontInfo, &ascent, &descent, &lineGap); + stbtt_GetFontBoundingBox(&stbttFontInfo, &x0, &y0, &x1, &y1); + + font->extents.ascent = ascent; + font->extents.descent = -descent; + font->extents.leading = lineGap; + font->extents.width = x1 - x0; + + stbtt_GetCodepointBox(&stbttFontInfo, 'x', &x0, &y0, &x1, &y1); + font->extents.xHeight = y1 - y0; + + stbtt_GetCodepointBox(&stbttFontInfo, 'M', &x0, &y0, &x1, &y1); + font->extents.capHeight = y1 - y0; + + //NOTE(martin): load codepoint ranges + font->rangeCount = rangeCount; + font->glyphMap = malloc_array(mg_glyph_map_entry, rangeCount); + font->glyphCount = 0; + + for(int i=0; iglyphMap[i].range = ranges[i]; + font->glyphMap[i].firstGlyphIndex = font->glyphCount + 1; + font->glyphCount += ranges[i].count; + } + + font->glyphs = malloc_array(mg_glyph_data, font->glyphCount); + + //NOTE(martin): first do a count of outlines + int outlineCount = 0; + for(int rangeIndex=0; rangeIndexglyphMap[rangeIndex].range.firstCodePoint; + u32 firstGlyphIndex = font->glyphMap[rangeIndex].firstGlyphIndex; + u32 endGlyphIndex = firstGlyphIndex + font->glyphMap[rangeIndex].range.count; + + for(int glyphIndex = firstGlyphIndex; + glyphIndex < endGlyphIndex; glyphIndex++) + { + int stbttGlyphIndex = stbtt_FindGlyphIndex(&stbttFontInfo, codePoint); + if(stbttGlyphIndex == 0) + { + //NOTE(martin): the codepoint is not found in the font + codePoint++; + continue; + } + //NOTE(martin): load glyph outlines + stbtt_vertex* vertices = 0; + outlineCount += stbtt_GetGlyphShape(&stbttFontInfo, stbttGlyphIndex, &vertices); + stbtt_FreeShape(&stbttFontInfo, vertices); + codePoint++; + } + } + //NOTE(martin): allocate outlines + font->outlines = malloc_array(mg_path_elt, outlineCount); + font->outlineCount = 0; + + //NOTE(martin): load metrics and outlines + for(int rangeIndex=0; rangeIndexglyphMap[rangeIndex].range.firstCodePoint; + u32 firstGlyphIndex = font->glyphMap[rangeIndex].firstGlyphIndex; + u32 endGlyphIndex = firstGlyphIndex + font->glyphMap[rangeIndex].range.count; + + for(int glyphIndex = firstGlyphIndex; + glyphIndex < endGlyphIndex; glyphIndex++) + { + mg_glyph_data* glyph = &(font->glyphs[glyphIndex-1]); + + int stbttGlyphIndex = stbtt_FindGlyphIndex(&stbttFontInfo, codePoint); + if(stbttGlyphIndex == 0) + { + //NOTE(martin): the codepoint is not found in the font, we zero the glyph info + memset(glyph, 0, sizeof(*glyph)); + codePoint++; + continue; + } + + glyph->exists = true; + glyph->codePoint = codePoint; + + //NOTE(martin): load glyph metric + int xAdvance, xBearing, x0, y0, x1, y1; + stbtt_GetGlyphHMetrics(&stbttFontInfo, stbttGlyphIndex, &xAdvance, &xBearing); + stbtt_GetGlyphBox(&stbttFontInfo, stbttGlyphIndex, &x0, &y0, &x1, &y1); + + glyph->extents.xAdvance = (f32)xAdvance; + glyph->extents.yAdvance = 0; + glyph->extents.xBearing = (f32)xBearing; + glyph->extents.yBearing = y0; + + glyph->extents.width = x1 - x0; + glyph->extents.height = y1 - y0; + + //NOTE(martin): load glyph outlines + + stbtt_vertex* vertices = 0; + int vertexCount = stbtt_GetGlyphShape(&stbttFontInfo, stbttGlyphIndex, &vertices); + + glyph->pathDescriptor = (mg_path_descriptor){.startIndex = font->outlineCount, + .count = vertexCount, + .startPoint = {0, 0}}; + + mg_path_elt* elements = font->outlines + font->outlineCount; + font->outlineCount += vertexCount; + vec2 currentPos = {0, 0}; + + for(int vertIndex = 0; vertIndex < vertexCount; vertIndex++) + { + f32 x = vertices[vertIndex].x; + f32 y = vertices[vertIndex].y; + f32 cx = vertices[vertIndex].cx; + f32 cy = vertices[vertIndex].cy; + f32 cx1 = vertices[vertIndex].cx1; + f32 cy1 = vertices[vertIndex].cy1; + + switch(vertices[vertIndex].type) + { + case STBTT_vmove: + elements[vertIndex].type = MG_PATH_MOVE; + elements[vertIndex].p[0] = (vec2){x, y}; + break; + + case STBTT_vline: + elements[vertIndex].type = MG_PATH_LINE; + elements[vertIndex].p[0] = (vec2){x, y}; + break; + + case STBTT_vcurve: + { + elements[vertIndex].type = MG_PATH_QUADRATIC; + elements[vertIndex].p[0] = (vec2){cx, cy}; + elements[vertIndex].p[1] = (vec2){x, y}; + } break; + + case STBTT_vcubic: + elements[vertIndex].type = MG_PATH_CUBIC; + elements[vertIndex].p[0] = (vec2){cx, cy}; + elements[vertIndex].p[1] = (vec2){cx1, cy1}; + elements[vertIndex].p[2] = (vec2){x, y}; + break; + } + currentPos = (vec2){x, y}; + } + stbtt_FreeShape(&stbttFontInfo, vertices); + codePoint++; + } + } + } + return(fontHandle); +} + +void mg_font_destroy(mg_font fontHandle) +{ + mg_font_data* fontData = mg_font_data_from_handle(fontHandle); + if(fontData) + { + free(fontData->glyphMap); + free(fontData->glyphs); + free(fontData->outlines); + + list_push(&__mgData.fontFreeList, &fontData->freeListElt); + mg_handle_recycle(fontHandle.h); + } +} + +str32 mg_font_get_glyph_indices_from_font_data(mg_font_data* fontData, str32 codePoints, str32 backing) +{ + u64 count = minimum(codePoints.len, backing.len); + + for(int i = 0; irangeCount; rangeIndex++) + { + if(codePoints.ptr[i] >= fontData->glyphMap[rangeIndex].range.firstCodePoint + && codePoints.ptr[i] < (fontData->glyphMap[rangeIndex].range.firstCodePoint + fontData->glyphMap[rangeIndex].range.count)) + { + u32 rangeOffset = codePoints.ptr[i] - fontData->glyphMap[rangeIndex].range.firstCodePoint; + glyphIndex = fontData->glyphMap[rangeIndex].firstGlyphIndex + rangeOffset; + break; + } + } + if(glyphIndex && !fontData->glyphs[glyphIndex].exists) + { + backing.ptr[i] = 0; + } + backing.ptr[i] = glyphIndex; + } + str32 res = {.len = count, .ptr = backing.ptr}; + return(res); +} + +u32 mg_font_get_glyph_index_from_font_data(mg_font_data* fontData, utf32 codePoint) +{ + u32 glyphIndex = 0; + str32 codePoints = {1, &codePoint}; + str32 backing = {1, &glyphIndex}; + mg_font_get_glyph_indices_from_font_data(fontData, codePoints, backing); + return(glyphIndex); +} + +str32 mg_font_get_glyph_indices(mg_font font, str32 codePoints, str32 backing) +{ + mg_font_data* fontData = mg_font_data_from_handle(font); + if(!fontData) + { + return((str32){0}); + } + return(mg_font_get_glyph_indices_from_font_data(fontData, codePoints, backing)); +} + +str32 mg_font_push_glyph_indices(mg_font font, mem_arena* arena, str32 codePoints) +{ + u32* buffer = mem_arena_alloc_array(arena, u32, codePoints.len); + str32 backing = {codePoints.len, buffer}; + return(mg_font_get_glyph_indices(font, codePoints, backing)); +} + +u32 mg_font_get_glyph_index(mg_font font, utf32 codePoint) +{ + u32 glyphIndex = 0; + str32 codePoints = {1, &codePoint}; + str32 backing = {1, &glyphIndex}; + mg_font_get_glyph_indices(font, codePoints, backing); + return(glyphIndex); +} + +mg_glyph_data* mg_font_get_glyph_data(mg_font_data* fontData, u32 glyphIndex) +{ + DEBUG_ASSERT(glyphIndex); + DEBUG_ASSERT(glyphIndex < fontData->glyphCount); + return(&(fontData->glyphs[glyphIndex-1])); +} + +mg_font_extents mg_font_get_extents(mg_font font) +{ + mg_font_data* fontData = mg_font_data_from_handle(font); + if(!fontData) + { + return((mg_font_extents){0}); + } + return(fontData->extents); +} + +mg_font_extents mg_font_get_scaled_extents(mg_font font, f32 emSize) +{ + mg_font_data* fontData = mg_font_data_from_handle(font); + if(!fontData) + { + return((mg_font_extents){0}); + } + f32 scale = emSize/fontData->unitsPerEm; + mg_font_extents extents = fontData->extents; + + extents.ascent *= scale; + extents.descent *= scale; + extents.leading *= scale; + extents.xHeight *= scale; + extents.capHeight *= scale; + extents.width *= scale; + + return(extents); +} + + +f32 mg_font_get_scale_for_em_pixels(mg_font font, f32 emSize) +{ + mg_font_data* fontData = mg_font_data_from_handle(font); + if(!fontData) + { + return(0); + } + return(emSize/fontData->unitsPerEm); +} + +void mg_font_get_glyph_extents_from_font_data(mg_font_data* fontData, + str32 glyphIndices, + mg_text_extents* outExtents) +{ + for(int i=0; i= fontData->glyphCount) + { + continue; + } + mg_glyph_data* glyph = mg_font_get_glyph_data(fontData, glyphIndices.ptr[i]); + outExtents[i] = glyph->extents; + } +} + +int mg_font_get_glyph_extents(mg_font font, str32 glyphIndices, mg_text_extents* outExtents) +{ + mg_font_data* fontData = mg_font_data_from_handle(font); + if(!fontData) + { + return(-1); + } + mg_font_get_glyph_extents_from_font_data(fontData, glyphIndices, outExtents); + return(0); +} + +int mg_font_get_codepoint_extents(mg_font font, utf32 codePoint, mg_text_extents* outExtents) +{ + mg_font_data* fontData = mg_font_data_from_handle(font); + if(!fontData) + { + return(-1); + } + u32 glyphIndex = 0; + str32 codePoints = {1, &codePoint}; + str32 backing = {1, &glyphIndex}; + str32 glyphs = mg_font_get_glyph_indices_from_font_data(fontData, codePoints, backing); + mg_font_get_glyph_extents_from_font_data(fontData, glyphs, outExtents); + return(0); +} + +mp_rect mg_text_bounding_box_utf32(mg_font font, f32 fontSize, str32 codePoints) +{ + if(!codePoints.len || !codePoints.ptr) + { + return((mp_rect){0}); + } + + mg_font_data* fontData = mg_font_data_from_handle(font); + if(!fontData) + { + return((mp_rect){0}); + } + + mem_arena* scratch = mem_scratch(); + str32 glyphIndices = mg_font_push_glyph_indices(font, scratch, codePoints); + + //NOTE(martin): find width of missing character + //TODO(martin): should cache that at font creation... + mg_text_extents missingGlyphExtents; + u32 missingGlyphIndex = mg_font_get_glyph_index_from_font_data(fontData, 0xfffd); + + if(missingGlyphIndex) + { + mg_font_get_glyph_extents_from_font_data(fontData, (str32){1, &missingGlyphIndex}, &missingGlyphExtents); + } + else + { + //NOTE(martin): could not find replacement glyph, try to get an 'x' to get a somewhat correct width + // to render an empty rectangle. Otherwise just render with the max font width + f32 boxWidth = fontData->extents.width * 0.8; + f32 xBearing = fontData->extents.width * 0.1; + f32 xAdvance = fontData->extents.width; + + missingGlyphIndex = mg_font_get_glyph_index_from_font_data(fontData, 'x'); + if(missingGlyphIndex) + { + mg_font_get_glyph_extents_from_font_data(fontData, (str32){1, &missingGlyphIndex}, &missingGlyphExtents); + } + else + { + missingGlyphExtents.xBearing = fontData->extents.width * 0.1; + missingGlyphExtents.yBearing = 0; + missingGlyphExtents.width = fontData->extents.width * 0.8; + missingGlyphExtents.xAdvance = fontData->extents.width; + missingGlyphExtents.yAdvance = 0; + } + } + + //NOTE(martin): accumulate text extents + f32 width = 0; + f32 x = 0; + f32 y = 0; + f32 lineHeight = fontData->extents.descent + fontData->extents.ascent; + + for(int i=0; i= fontData->glyphCount) + { + extents = missingGlyphExtents; + } + else + { + glyph = mg_font_get_glyph_data(fontData, glyphIndices.ptr[i]); + extents = glyph->extents; + } + x += extents.xAdvance; + y += extents.yAdvance; + + if(glyph && glyph->codePoint == '\n') + { + width = maximum(width, x); + x = 0; + y += lineHeight + fontData->extents.leading; + } + } + width = maximum(width, x); + + f32 fontScale = mg_font_get_scale_for_em_pixels(font, fontSize); + mp_rect rect = {0, -fontData->extents.ascent * fontScale, width * fontScale, (y + lineHeight) * fontScale }; + return(rect); +} + +mp_rect mg_text_bounding_box(mg_font font, f32 fontSize, str8 text) +{ + if(!text.len || !text.ptr) + { + return((mp_rect){0}); + } + + mem_arena* scratch = mem_scratch(); + str32 codePoints = utf8_push_to_codepoints(scratch, text); + return(mg_text_bounding_box_utf32(font, fontSize, codePoints)); +} + +//------------------------------------------------------------------------------------------ +//NOTE(martin): graphics canvas API +//------------------------------------------------------------------------------------------ + +mg_canvas mg_canvas_nil() { return((mg_canvas){.h = 0}); } +bool mg_canvas_is_nil(mg_canvas canvas) { return(canvas.h == 0); } + +mg_canvas mg_canvas_handle_alloc(mg_canvas_data* canvas) +{ + mg_canvas handle = {.h = mg_handle_alloc(MG_HANDLE_CANVAS, (void*)canvas) }; + return(handle); +} + +mg_canvas_data* mg_canvas_data_from_handle(mg_canvas handle) +{ + mg_canvas_data* data = mg_data_from_handle(MG_HANDLE_CANVAS, handle.h); + return(data); +} + +mg_canvas mg_canvas_create() +{ + if(!__mgData.init) + { + mg_init(); + } + + mg_canvas canvasHandle = mg_canvas_nil(); + mg_canvas_data* canvas = list_pop_entry(&__mgData.canvasFreeList, mg_canvas_data, freeListElt); + if(!canvas) + { + canvas = mem_arena_alloc_type(&__mgData.resourceArena, mg_canvas_data); + } + if(canvas) + { + canvas->textFlip = false; + canvas->path = (mg_path_descriptor){0}; + canvas->matrixStackSize = 0; + canvas->clipStackSize = 0; + canvas->primitiveCount = 0; + canvas->clearColor = (mg_color){0, 0, 0, 0}; + + canvas->attributes = (mg_attributes){0}; + canvas->attributes.color = (mg_color){0, 0, 0, 1}; + canvas->attributes.tolerance = 1; + canvas->attributes.width = 10; + canvas->attributes.clip = (mp_rect){-FLT_MAX/2, -FLT_MAX/2, FLT_MAX, FLT_MAX}; + + canvasHandle = mg_canvas_handle_alloc(canvas); + + mg_canvas_set_current(canvasHandle); + } + return(canvasHandle); +} + +void mg_canvas_destroy(mg_canvas handle) +{ + mg_canvas_data* canvas = mg_canvas_data_from_handle(handle); + if(canvas) + { + if(__mgCurrentCanvas == canvas) + { + __mgCurrentCanvas = 0; + __mgCurrentCanvasHandle = mg_canvas_nil(); + } + list_push(&__mgData.canvasFreeList, &canvas->freeListElt); + mg_handle_recycle(handle.h); + } +} + +mg_canvas mg_canvas_set_current(mg_canvas canvas) +{ + mg_canvas old = __mgCurrentCanvasHandle; + __mgCurrentCanvasHandle = canvas; + __mgCurrentCanvas = mg_canvas_data_from_handle(canvas); + return(old); +} + +void mg_render(mg_surface surface, mg_canvas canvas) +{ + mg_canvas_data* canvasData = mg_canvas_data_from_handle(canvas); + if(canvasData) + { + int eltCount = canvasData->path.startIndex + canvasData->path.count; + mg_surface_render_commands(surface, + canvasData->clearColor, + canvasData->primitiveCount, + canvasData->primitives, + eltCount, + canvasData->pathElements); + + canvasData->primitiveCount = 0; + canvasData->path.startIndex = 0; + canvasData->path.count = 0; + } +} + +//------------------------------------------------------------------------------------------ +//NOTE(martin): transform, viewport and clipping +//------------------------------------------------------------------------------------------ + +void mg_matrix_push(mg_mat2x3 matrix) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + mg_mat2x3 transform = mg_matrix_stack_top(canvas); + mg_matrix_stack_push(canvas, mg_mat2x3_mul_m(transform, matrix)); + } +} + +void mg_matrix_pop() +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + mg_matrix_stack_pop(canvas); + } +} + +void mg_clip_push(f32 x, f32 y, f32 w, f32 h) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + mp_rect clip = {x, y, w, h}; + + //NOTE(martin): transform clip + mg_mat2x3 transform = mg_matrix_stack_top(canvas); + vec2 p0 = mg_mat2x3_mul(transform, (vec2){clip.x, clip.y}); + vec2 p1 = mg_mat2x3_mul(transform, (vec2){clip.x + clip.w, clip.y}); + vec2 p2 = mg_mat2x3_mul(transform, (vec2){clip.x + clip.w, clip.y + clip.h}); + vec2 p3 = mg_mat2x3_mul(transform, (vec2){clip.x, clip.y + clip.h}); + + f32 x0 = minimum(p0.x, minimum(p1.x, minimum(p2.x, p3.x))); + f32 y0 = minimum(p0.y, minimum(p1.y, minimum(p2.y, p3.y))); + f32 x1 = maximum(p0.x, maximum(p1.x, maximum(p2.x, p3.x))); + f32 y1 = maximum(p0.y, maximum(p1.y, maximum(p2.y, p3.y))); + + mp_rect current = mg_clip_stack_top(canvas); + + //NOTE(martin): intersect with current clip + x0 = maximum(current.x, x0); + y0 = maximum(current.y, y0); + x1 = minimum(current.x + current.w, x1); + y1 = minimum(current.y + current.h, y1); + + mp_rect r = {x0, y0, maximum(0, x1-x0), maximum(0, y1-y0)}; + mg_clip_stack_push(canvas, r); + + canvas->attributes.clip = r; + } +} + +void mg_clip_pop() +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + mg_clip_stack_pop(canvas); + canvas->attributes.clip = mg_clip_stack_top(canvas); + } +} + +//------------------------------------------------------------------------------------------ +//NOTE(martin): graphics attributes setting/getting +//------------------------------------------------------------------------------------------ +void mg_set_color(mg_color color) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + canvas->attributes.color = color; + } +} + +void mg_set_color_rgba(f32 r, f32 g, f32 b, f32 a) +{ + mg_set_color((mg_color){r, g, b, a}); +} + +void mg_set_width(f32 width) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + canvas->attributes.width = width; + } +} + +void mg_set_tolerance(f32 tolerance) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + canvas->attributes.tolerance = tolerance; + } +} + +void mg_set_joint(mg_joint_type joint) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + canvas->attributes.joint = joint; + } +} + +void mg_set_max_joint_excursion(f32 maxJointExcursion) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + canvas->attributes.maxJointExcursion = maxJointExcursion; + } +} + +void mg_set_cap(mg_cap_type cap) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + canvas->attributes.cap = cap; + } +} + +void mg_set_font(mg_font font) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + canvas->attributes.font = font; + } +} + +void mg_set_font_size(f32 fontSize) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + canvas->attributes.fontSize = fontSize; + } +} + +void mg_set_text_flip(bool flip) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + canvas->textFlip = flip; + } +} + +void mg_set_image(mg_image image) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + canvas->attributes.image = image; + vec2 size = mg_image_size(image); + canvas->attributes.srcRegion = (mp_rect){0, 0, size.x, size.y}; + } +} + +void mg_set_image_source_region(mp_rect region) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + canvas->attributes.srcRegion = region; + } +} + +mg_color mg_get_color() +{ + mg_color color = {0}; + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + color = canvas->attributes.color; + } + return(color); +} + +f32 mg_get_width() +{ + f32 width = 0; + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + width = canvas->attributes.width; + } + return(width); +} + +f32 mg_get_tolerance() +{ + f32 tolerance = 0; + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + tolerance = canvas->attributes.tolerance; + } + return(tolerance); +} + +mg_joint_type mg_get_joint() +{ + mg_joint_type joint = 0; + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + joint = canvas->attributes.joint; + } + return(joint); +} + +f32 mg_get_max_joint_excursion() +{ + f32 maxJointExcursion = 0; + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + maxJointExcursion = canvas->attributes.maxJointExcursion; + } + return(maxJointExcursion); +} + +mg_cap_type mg_get_cap() +{ + mg_cap_type cap = 0; + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + cap = canvas->attributes.cap; + } + return(cap); +} + +mg_font mg_get_font() +{ + mg_font font = mg_font_nil(); + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + font = canvas->attributes.font; + } + return(font); +} + +f32 mg_get_font_size() +{ + f32 fontSize = 0; + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + fontSize = canvas->attributes.fontSize; + } + return(fontSize); +} + +bool mg_get_text_flip() +{ + bool flip = false; + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + flip = canvas->textFlip; + } + return(flip); +} + +//------------------------------------------------------------------------------------------ +//NOTE(martin): path construction +//------------------------------------------------------------------------------------------ +vec2 mg_get_position() +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(!canvas) + { + return((vec2){0, 0}); + } + return(canvas->subPathLastPoint); +} + +void mg_move_to(f32 x, f32 y) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(!canvas) + { + return; + } + mg_path_push_element(canvas, ((mg_path_elt){.type = MG_PATH_MOVE, .p[0] = {x, y}})); + canvas->subPathStartPoint = (vec2){x, y}; + canvas->subPathLastPoint = (vec2){x, y}; +} + +void mg_line_to(f32 x, f32 y) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(!canvas) + { + return; + } + mg_path_push_element(canvas, ((mg_path_elt){.type = MG_PATH_LINE, .p[0] = {x, y}})); + canvas->subPathLastPoint = (vec2){x, y}; +} + +void mg_quadratic_to(f32 x1, f32 y1, f32 x2, f32 y2) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(!canvas) + { + return; + } + mg_path_push_element(canvas, ((mg_path_elt){.type = MG_PATH_QUADRATIC, .p = {{x1, y1}, {x2, y2}}})); + canvas->subPathLastPoint = (vec2){x2, y2}; +} + +void mg_cubic_to(f32 x1, f32 y1, f32 x2, f32 y2, f32 x3, f32 y3) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(!canvas) + { + return; + } + mg_path_push_element(canvas, ((mg_path_elt){.type = MG_PATH_CUBIC, .p = {{x1, y1}, {x2, y2}, {x3, y3}}})); + canvas->subPathLastPoint = (vec2){x3, y3}; +} + +void mg_close_path() +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(!canvas) + { + return; + } + if( canvas->subPathStartPoint.x != canvas->subPathLastPoint.x + || canvas->subPathStartPoint.y != canvas->subPathLastPoint.y) + { + mg_line_to(canvas->subPathStartPoint.x, canvas->subPathStartPoint.y); + } + canvas->subPathStartPoint = canvas->subPathLastPoint; +} + +mp_rect mg_glyph_outlines_from_font_data(mg_font_data* fontData, str32 glyphIndices) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + + f32 startX = canvas->subPathLastPoint.x; + f32 startY = canvas->subPathLastPoint.y; + f32 maxWidth = 0; + + f32 scale = canvas->attributes.fontSize/fontData->unitsPerEm; + + for(int i=0; isubPathLastPoint.x; + f32 yOffset = canvas->subPathLastPoint.y; + f32 flip = canvas->textFlip ? 1 : -1; + + if(!glyphIndex || glyphIndex >= fontData->glyphCount) + { + log_warning("code point is not present in font ranges\n"); + //NOTE(martin): try to find the replacement character + glyphIndex = mg_font_get_glyph_index_from_font_data(fontData, 0xfffd); + if(!glyphIndex) + { + //NOTE(martin): could not find replacement glyph, try to get an 'x' to get a somewhat correct width + // to render an empty rectangle. Otherwise just render with the max font width + f32 boxWidth = fontData->extents.width * 0.8; + f32 xBearing = fontData->extents.width * 0.1; + f32 xAdvance = fontData->extents.width; + + glyphIndex = mg_font_get_glyph_index_from_font_data(fontData, 'x'); + if(glyphIndex) + { + mg_glyph_data* glyph = &(fontData->glyphs[glyphIndex]); + boxWidth = glyph->extents.width; + xBearing = glyph->extents.xBearing; + xAdvance = glyph->extents.xAdvance; + } + f32 oldStrokeWidth = canvas->attributes.width; + + mg_set_width(boxWidth*0.005); + mg_rectangle_stroke(xOffset + xBearing * scale, + yOffset, + boxWidth * scale * flip, + fontData->extents.capHeight*scale); + + mg_set_width(oldStrokeWidth); + mg_move_to(xOffset + xAdvance * scale, yOffset); + maxWidth = maximum(maxWidth, xOffset + xAdvance*scale - startX); + continue; + } + } + + mg_glyph_data* glyph = mg_font_get_glyph_data(fontData, glyphIndex); + + mg_path_push_elements(canvas, glyph->pathDescriptor.count, fontData->outlines + glyph->pathDescriptor.startIndex); + + mg_path_elt* elements = canvas->pathElements + canvas->path.count + canvas->path.startIndex - glyph->pathDescriptor.count; + for(int eltIndex=0; eltIndexpathDescriptor.count; eltIndex++) + { + for(int pIndex = 0; pIndex < 3; pIndex++) + { + elements[eltIndex].p[pIndex].x = elements[eltIndex].p[pIndex].x * scale + xOffset; + elements[eltIndex].p[pIndex].y = elements[eltIndex].p[pIndex].y * scale * flip + yOffset; + } + } + mg_move_to(xOffset + scale*glyph->extents.xAdvance, yOffset); + + maxWidth = maximum(maxWidth, xOffset + scale*glyph->extents.xAdvance - startX); + } + f32 lineHeight = (fontData->extents.ascent + fontData->extents.descent)*scale; + mp_rect box = {startX, startY, maxWidth, canvas->subPathLastPoint.y - startY + lineHeight }; + return(box); +} + +mp_rect mg_glyph_outlines(str32 glyphIndices) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(!canvas) + { + return((mp_rect){0}); + } + mg_font_data* fontData = mg_font_data_from_handle(canvas->attributes.font); + if(!fontData) + { + return((mp_rect){0}); + } + return(mg_glyph_outlines_from_font_data(fontData, glyphIndices)); +} + +void mg_codepoints_outlines(str32 codePoints) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(!canvas) + { + return; + } + mg_font_data* fontData = mg_font_data_from_handle(canvas->attributes.font); + if(!fontData) + { + return; + } + + str32 glyphIndices = mg_font_push_glyph_indices(canvas->attributes.font, mem_scratch(), codePoints); + mg_glyph_outlines_from_font_data(fontData, glyphIndices); +} + +void mg_text_outlines(str8 text) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(!canvas) + { + return; + } + mg_font_data* fontData = mg_font_data_from_handle(canvas->attributes.font); + if(!fontData) + { + return; + } + + mem_arena* scratch = mem_scratch(); + str32 codePoints = utf8_push_to_codepoints(scratch, text); + str32 glyphIndices = mg_font_push_glyph_indices(canvas->attributes.font, scratch, codePoints); + + mg_glyph_outlines_from_font_data(fontData, glyphIndices); +} + +//------------------------------------------------------------------------------------------ +//NOTE(martin): clear/fill/stroke +//------------------------------------------------------------------------------------------ + +void mg_clear() +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + canvas->primitiveCount = 0; + canvas->clearColor = canvas->attributes.color; + } +} + +void mg_fill() +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas && canvas->path.count) + { + mg_push_command(canvas, (mg_primitive){.cmd = MG_CMD_FILL, .path = canvas->path}); + mg_new_path(canvas); + } +} + +void mg_stroke() +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas && canvas->path.count) + { + mg_push_command(canvas, (mg_primitive){.cmd = MG_CMD_STROKE, .path = canvas->path}); + mg_new_path(canvas); + } +} + +//------------------------------------------------------------------------------------------ +//NOTE(martin): simple shape helpers +//------------------------------------------------------------------------------------------ + +void mg_rectangle_path(f32 x, f32 y, f32 w, f32 h) +{ + mg_move_to(x, y); + mg_line_to(x+w, y); + mg_line_to(x+w, y+h); + mg_line_to(x, y+h); + mg_close_path(); +} + +void mg_rectangle_fill(f32 x, f32 y, f32 w, f32 h) +{ + mg_rectangle_path(x, y, w, h); + mg_fill(); +} + +void mg_rectangle_stroke(f32 x, f32 y, f32 w, f32 h) +{ + mg_rectangle_path(x, y, w, h); + mg_stroke(); +} + +void mg_rounded_rectangle_path(f32 x, f32 y, f32 w, f32 h, f32 r) +{ + f32 c = r*4*(sqrt(2)-1)/3; + + mg_move_to(x+r, y); + mg_line_to(x+w-r, y); + mg_cubic_to(x+w-r+c, y, x+w, y+r-c, x+w, y+r); + mg_line_to(x+w, y+h-r); + mg_cubic_to(x+w, y+h-r+c, x+w-r+c, y+h, x+w-r, y+h); + mg_line_to(x+r, y+h); + mg_cubic_to(x+r-c, y+h, x, y+h-r+c, x, y+h-r); + mg_line_to(x, y+r); + mg_cubic_to(x, y+r-c, x+r-c, y, x+r, y); +} + +void mg_rounded_rectangle_fill(f32 x, f32 y, f32 w, f32 h, f32 r) +{ + mg_rounded_rectangle_path(x, y, w, h, r); + mg_fill(); +} + +void mg_rounded_rectangle_stroke(f32 x, f32 y, f32 w, f32 h, f32 r) +{ + mg_rounded_rectangle_path(x, y, w, h, r); + mg_stroke(); +} + +void mg_ellipse_path(f32 x, f32 y, f32 rx, f32 ry) +{ + f32 cx = rx*4*(sqrt(2)-1)/3; + f32 cy = ry*4*(sqrt(2)-1)/3; + + mg_move_to(x-rx, y); + mg_cubic_to(x-rx, y+cy, x-cx, y+ry, x, y+ry); + mg_cubic_to(x+cx, y+ry, x+rx, y+cy, x+rx, y); + mg_cubic_to(x+rx, y-cy, x+cx, y-ry, x, y-ry); + mg_cubic_to(x-cx, y-ry, x-rx, y-cy, x-rx, y); +} + +void mg_ellipse_fill(f32 x, f32 y, f32 rx, f32 ry) +{ + mg_ellipse_path(x, y, rx, ry); + mg_fill(); +} + +void mg_ellipse_stroke(f32 x, f32 y, f32 rx, f32 ry) +{ + mg_ellipse_path(x, y, rx, ry); + mg_stroke(); +} + +void mg_circle_fill(f32 x, f32 y, f32 r) +{ + mg_ellipse_fill(x, y, r, r); +} + +void mg_circle_stroke(f32 x, f32 y, f32 r) +{ + mg_ellipse_stroke(x, y, r, r); +} + +//TODO: change to arc_to? +void mg_arc(f32 x, f32 y, f32 r, f32 arcAngle, f32 startAngle) +{ + f32 endAngle = startAngle + arcAngle; + + while(startAngle < endAngle) + { + f32 smallAngle = minimum(endAngle - startAngle, M_PI/4.); + if(smallAngle < 0.001) + { + break; + } + + vec2 v0 = {cos(smallAngle/2), sin(smallAngle/2)}; + vec2 v1 = {(4-v0.x)/3, (1-v0.x)*(3-v0.x)/(3*v0.y)}; + vec2 v2 = {v1.x, -v1.y}; + vec2 v3 = {v0.x, -v0.y}; + + f32 rotAngle = smallAngle/2 + startAngle; + f32 rotCos = cos(rotAngle); + f32 rotSin = sin(rotAngle); + + mg_mat2x3 t = {r*rotCos, -r*rotSin, x, + r*rotSin, r*rotCos, y}; + + v0 = mg_mat2x3_mul(t, v0); + v1 = mg_mat2x3_mul(t, v1); + v2 = mg_mat2x3_mul(t, v2); + v3 = mg_mat2x3_mul(t, v3); + + mg_move_to(v0.x, v0.y); + mg_cubic_to(v1.x, v1.y, v2.x, v2.y, v3.x, v3.y); + + startAngle += smallAngle; + } +} + +//------------------------------------------------------------------------------------------ +//NOTE(martin): images +//------------------------------------------------------------------------------------------ + +mg_image mg_image_nil() { return((mg_image){.h = 0}); } +bool mg_image_is_nil(mg_image image) { return(image.h == 0); } + +mg_image mg_image_create_from_rgba8(mg_surface surface, u32 width, u32 height, u8* pixels) +{ + mg_image image = mg_image_create(surface, width, height); + if(!mg_image_is_nil(image)) + { + mg_image_upload_region_rgba8(image, (mp_rect){0, 0, width, height}, pixels); + } + return(image); +} + +mg_image mg_image_create_from_data(mg_surface surface, str8 data, bool flip) +{ + mg_image image = mg_image_nil(); + int width, height, channels; + + stbi_set_flip_vertically_on_load(flip ? 1 : 0); + u8* pixels = stbi_load_from_memory((u8*)data.ptr, data.len, &width, &height, &channels, 4); + + if(pixels) + { + image = mg_image_create_from_rgba8(surface, width, height, pixels); + free(pixels); + } + return(image); +} + +#if !PLATFORM_ORCA + +mg_image mg_image_create_from_file(mg_surface surface, str8 path, bool flip) +{ + mg_image image = mg_image_nil(); + int width, height, channels; + + const char* cpath = str8_to_cstring(mem_scratch(), path); + + stbi_set_flip_vertically_on_load(flip ? 1 : 0); + u8* pixels = stbi_load(cpath, &width, &height, &channels, 4); + if(pixels) + { + image = mg_image_create_from_rgba8(surface, width, height, pixels); + free(pixels); + } + return(image); +} + +#endif // !PLATFORM_ORCA + + +void mg_image_draw_region(mg_image image, mp_rect srcRegion, mp_rect dstRegion) +{ + mg_canvas_data* canvas = __mgCurrentCanvas; + if(canvas) + { + mg_image oldImage = canvas->attributes.image; + mp_rect oldSrcRegion = canvas->attributes.srcRegion; + mg_color oldColor = canvas->attributes.color; + + canvas->attributes.image = image; + canvas->attributes.srcRegion = srcRegion; + canvas->attributes.color = (mg_color){1, 1, 1, 1}; + + mg_move_to(dstRegion.x, dstRegion.y); + mg_line_to(dstRegion.x+dstRegion.w, dstRegion.y); + mg_line_to(dstRegion.x+dstRegion.w, dstRegion.y+dstRegion.h); + mg_line_to(dstRegion.x, dstRegion.y+dstRegion.h); + mg_close_path(); + + mg_fill(); + + canvas->attributes.image = oldImage; + canvas->attributes.srcRegion = oldSrcRegion; + canvas->attributes.color = oldColor; + } +} + +void mg_image_draw(mg_image image, mp_rect rect) +{ + vec2 size = mg_image_size(image); + mg_image_draw_region(image, (mp_rect){0, 0, size.x, size.y}, rect); +} + +//------------------------------------------------------------------------------------------ +//NOTE(martin): atlasing +//------------------------------------------------------------------------------------------ + +//NOTE: rectangle allocator +typedef struct mg_rect_atlas +{ + mem_arena* arena; + ivec2 size; + ivec2 pos; + u32 lineHeight; + +} mg_rect_atlas; + +mg_rect_atlas* mg_rect_atlas_create(mem_arena* arena, i32 width, i32 height) +{ + mg_rect_atlas* atlas = mem_arena_alloc_type(arena, mg_rect_atlas); + memset(atlas, 0, sizeof(mg_rect_atlas)); + atlas->arena = arena; + atlas->size = (ivec2){width, height}; + return(atlas); +} + +mp_rect mg_rect_atlas_alloc(mg_rect_atlas* atlas, i32 width, i32 height) +{ + mp_rect rect = {0, 0, 0, 0}; + if(width > 0 && height > 0) + { + if(atlas->pos.x + width >= atlas->size.x) + { + atlas->pos.x = 0; + atlas->pos.y += (atlas->lineHeight + 1); + atlas->lineHeight = 0; + } + if( atlas->pos.x + width < atlas->size.x + && atlas->pos.y + height < atlas->size.y) + { + rect = (mp_rect){atlas->pos.x, atlas->pos.y, width, height}; + + atlas->pos.x += (width + 1); + atlas->lineHeight = maximum(atlas->lineHeight, height); + } + } + return(rect); +} + +void mg_rect_atlas_recycle(mg_rect_atlas* atlas, mp_rect rect) +{ + //TODO +} + +mg_image_region mg_image_atlas_alloc_from_rgba8(mg_rect_atlas* atlas, mg_image backingImage, u32 width, u32 height, u8* pixels) +{ + mg_image_region imageRgn = {0}; + + mp_rect rect = mg_rect_atlas_alloc(atlas, width, height); + if(rect.w == width && rect.h == height) + { + mg_image_upload_region_rgba8(backingImage, rect, pixels); + imageRgn.rect = rect; + imageRgn.image = backingImage; + } + return(imageRgn); +} + +#if !PLATFORM_ORCA + +mg_image_region mg_image_atlas_alloc_from_data(mg_rect_atlas* atlas, mg_image backingImage, str8 data, bool flip) +{ + mg_image_region imageRgn = {0}; + + stbi_set_flip_vertically_on_load(flip ? 1 : 0); + + int width, height, channels; + u8* pixels = stbi_load_from_memory((u8*)data.ptr, data.len, &width, &height, &channels, 4); + if(pixels) + { + imageRgn = mg_image_atlas_alloc_from_rgba8(atlas, backingImage, width, height, pixels); + free(pixels); + } + return(imageRgn); +} + +mg_image_region mg_image_atlas_alloc_from_file(mg_rect_atlas* atlas, mg_image backingImage, str8 path, bool flip) +{ + mg_image_region imageRgn = {0}; + + stbi_set_flip_vertically_on_load(flip ? 1 : 0); + + const char* cpath = str8_to_cstring(mem_scratch(), path); + int width, height, channels; + u8* pixels = stbi_load(cpath, &width, &height, &channels, 4); + if(pixels) + { + imageRgn = mg_image_atlas_alloc_from_rgba8(atlas, backingImage, width, height, pixels); + free(pixels); + } + return(imageRgn); +} +#endif // !PLATFORM_ORCA + +void mg_image_atlas_recycle(mg_rect_atlas* atlas, mg_image_region imageRgn) +{ + mg_rect_atlas_recycle(atlas, imageRgn.rect); +} diff --git a/milepost/src/graphics_common.h b/milepost/src/graphics/graphics_common.h similarity index 95% rename from milepost/src/graphics_common.h rename to milepost/src/graphics/graphics_common.h index 9596694..a29e44b 100644 --- a/milepost/src/graphics_common.h +++ b/milepost/src/graphics/graphics_common.h @@ -1,82 +1,82 @@ -/************************************************************//** -* -* @file: graphics_common.h -* @author: Martin Fouilleul -* @date: 26/04/2023 -* -*****************************************************************/ -#ifndef __GRAPHICS_COMMON_H_ -#define __GRAPHICS_COMMON_H_ - -#include"graphics.h" - -//------------------------------------------------------------------------ -// canvas structs -//------------------------------------------------------------------------ -typedef enum { MG_PATH_MOVE, - MG_PATH_LINE, - MG_PATH_QUADRATIC, - MG_PATH_CUBIC } mg_path_elt_type; - -typedef struct mg_path_elt -{ - mg_path_elt_type type; - vec2 p[3]; - -} mg_path_elt; - -typedef struct mg_path_descriptor -{ - u32 startIndex; - u32 count; - vec2 startPoint; - -} mg_path_descriptor; - -typedef struct mg_attributes -{ - f32 width; - f32 tolerance; - mg_color color; - mg_joint_type joint; - f32 maxJointExcursion; - mg_cap_type cap; - - mg_font font; - f32 fontSize; - - mg_image image; - mp_rect srcRegion; - - mg_mat2x3 transform; - mp_rect clip; - -} mg_attributes; - -typedef enum { MG_CMD_FILL, - MG_CMD_STROKE, - MG_CMD_JUMP - } mg_primitive_cmd; - -typedef struct mg_primitive -{ - mg_primitive_cmd cmd; - mg_attributes attributes; - - union - { - mg_path_descriptor path; - mp_rect rect; - u32 jump; - }; - -} mg_primitive; - -MP_API void mg_surface_render_commands(mg_surface surface, - mg_color clearColor, - u32 primitiveCount, - mg_primitive* primitives, - u32 eltCount, - mg_path_elt* elements); - -#endif //__GRAPHICS_COMMON_H_ +/************************************************************//** +* +* @file: graphics_common.h +* @author: Martin Fouilleul +* @date: 26/04/2023 +* +*****************************************************************/ +#ifndef __GRAPHICS_COMMON_H_ +#define __GRAPHICS_COMMON_H_ + +#include"graphics.h" + +//------------------------------------------------------------------------ +// canvas structs +//------------------------------------------------------------------------ +typedef enum { MG_PATH_MOVE, + MG_PATH_LINE, + MG_PATH_QUADRATIC, + MG_PATH_CUBIC } mg_path_elt_type; + +typedef struct mg_path_elt +{ + mg_path_elt_type type; + vec2 p[3]; + +} mg_path_elt; + +typedef struct mg_path_descriptor +{ + u32 startIndex; + u32 count; + vec2 startPoint; + +} mg_path_descriptor; + +typedef struct mg_attributes +{ + f32 width; + f32 tolerance; + mg_color color; + mg_joint_type joint; + f32 maxJointExcursion; + mg_cap_type cap; + + mg_font font; + f32 fontSize; + + mg_image image; + mp_rect srcRegion; + + mg_mat2x3 transform; + mp_rect clip; + +} mg_attributes; + +typedef enum { MG_CMD_FILL, + MG_CMD_STROKE, + MG_CMD_JUMP + } mg_primitive_cmd; + +typedef struct mg_primitive +{ + mg_primitive_cmd cmd; + mg_attributes attributes; + + union + { + mg_path_descriptor path; + mp_rect rect; + u32 jump; + }; + +} mg_primitive; + +MP_API void mg_surface_render_commands(mg_surface surface, + mg_color clearColor, + u32 primitiveCount, + mg_primitive* primitives, + u32 eltCount, + mg_path_elt* elements); + +#endif //__GRAPHICS_COMMON_H_ diff --git a/milepost/src/graphics_surface.c b/milepost/src/graphics/graphics_surface.c similarity index 95% rename from milepost/src/graphics_surface.c rename to milepost/src/graphics/graphics_surface.c index 240db5b..dd120f0 100644 --- a/milepost/src/graphics_surface.c +++ b/milepost/src/graphics/graphics_surface.c @@ -1,454 +1,454 @@ -/************************************************************//** -* -* @file: graphics_surface.c -* @author: Martin Fouilleul -* @date: 25/04/2023 -* -*****************************************************************/ - -#include"graphics_surface.h" - -//--------------------------------------------------------------- -// per-thread selected surface -//--------------------------------------------------------------- - -mp_thread_local mg_surface __mgSelectedSurface = {0}; - -//--------------------------------------------------------------- -// 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); - mg_surface_prepare(surfaceHandle); - } - 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); - mg_surface_prepare(surfaceHandle); - } - 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(__mgSelectedSurface.h == handle.h) - { - mg_surface_deselect(); - } - - if(surface->backend && surface->backend->destroy) - { - surface->backend->destroy(surface->backend); - } - surface->destroy(surface); - mg_handle_recycle(handle.h); - } -} - -void mg_surface_deselect() -{ - DEBUG_ASSERT(__mgData.init); - - mg_surface_data* prevSurface = mg_surface_data_from_handle(__mgSelectedSurface); - if(prevSurface && prevSurface->deselect) - { - prevSurface->deselect(prevSurface); - } - __mgSelectedSurface = mg_surface_nil(); -} - -void mg_surface_prepare(mg_surface surface) -{ - DEBUG_ASSERT(__mgData.init); - - if(surface.h != __mgSelectedSurface.h) - { - mg_surface_deselect(); - } - - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->prepare) - { - surfaceData->prepare(surfaceData); - __mgSelectedSurface = surface; - } -} - -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_get_size(mg_surface surface) -{ - DEBUG_ASSERT(__mgData.init); - vec2 size = {0}; - mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); - if(surfaceData && surfaceData->getSize) - { - size = surfaceData->getSize(surfaceData); - } - return(size); -} - -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_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(surface.h != __mgSelectedSurface.h) - { - log_error("surface is not selected. Make sure to call mg_surface_prepare() before drawing onto a surface.\n"); - } - else 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(surface.h != __mgSelectedSurface.h) - { - log_error("surface is not selected. Make sure to call mg_surface_prepare() before modifying graphics resources.\n"); - } - else 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) - { - if(imageData->surface.h != __mgSelectedSurface.h) - { - log_error("surface is not selected. Make sure to call mg_surface_prepare() before modifying graphics resources.\n"); - } - else - { - 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) - { - if(imageData->surface.h != __mgSelectedSurface.h) - { - log_error("surface is not selected. Make sure to call mg_surface_prepare() before modifying graphics resources.\n"); - } - else - { - 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" + +//--------------------------------------------------------------- +// per-thread selected surface +//--------------------------------------------------------------- + +mp_thread_local mg_surface __mgSelectedSurface = {0}; + +//--------------------------------------------------------------- +// 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); + mg_surface_prepare(surfaceHandle); + } + 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); + mg_surface_prepare(surfaceHandle); + } + 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(__mgSelectedSurface.h == handle.h) + { + mg_surface_deselect(); + } + + if(surface->backend && surface->backend->destroy) + { + surface->backend->destroy(surface->backend); + } + surface->destroy(surface); + mg_handle_recycle(handle.h); + } +} + +void mg_surface_deselect() +{ + DEBUG_ASSERT(__mgData.init); + + mg_surface_data* prevSurface = mg_surface_data_from_handle(__mgSelectedSurface); + if(prevSurface && prevSurface->deselect) + { + prevSurface->deselect(prevSurface); + } + __mgSelectedSurface = mg_surface_nil(); +} + +void mg_surface_prepare(mg_surface surface) +{ + DEBUG_ASSERT(__mgData.init); + + if(surface.h != __mgSelectedSurface.h) + { + mg_surface_deselect(); + } + + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->prepare) + { + surfaceData->prepare(surfaceData); + __mgSelectedSurface = surface; + } +} + +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_get_size(mg_surface surface) +{ + DEBUG_ASSERT(__mgData.init); + vec2 size = {0}; + mg_surface_data* surfaceData = mg_surface_data_from_handle(surface); + if(surfaceData && surfaceData->getSize) + { + size = surfaceData->getSize(surfaceData); + } + return(size); +} + +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_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(surface.h != __mgSelectedSurface.h) + { + log_error("surface is not selected. Make sure to call mg_surface_prepare() before drawing onto a surface.\n"); + } + else 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(surface.h != __mgSelectedSurface.h) + { + log_error("surface is not selected. Make sure to call mg_surface_prepare() before modifying graphics resources.\n"); + } + else 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) + { + if(imageData->surface.h != __mgSelectedSurface.h) + { + log_error("surface is not selected. Make sure to call mg_surface_prepare() before modifying graphics resources.\n"); + } + else + { + 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) + { + if(imageData->surface.h != __mgSelectedSurface.h) + { + log_error("surface is not selected. Make sure to call mg_surface_prepare() before modifying graphics resources.\n"); + } + else + { + 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/milepost/src/graphics_surface.h b/milepost/src/graphics/graphics_surface.h similarity index 96% rename from milepost/src/graphics_surface.h rename to milepost/src/graphics/graphics_surface.h index d494d9a..8b85f3f 100644 --- a/milepost/src/graphics_surface.h +++ b/milepost/src/graphics/graphics_surface.h @@ -1,112 +1,112 @@ -/************************************************************//** -* -* @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_deselect_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_get_size_proc)(mg_surface_data* surface); -typedef vec2 (*mg_surface_contents_scaling_proc)(mg_surface_data* surface); -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_deselect_proc deselect; - mg_surface_swap_interval_proc swapInterval; - mg_surface_get_size_proc getSize; - mg_surface_contents_scaling_proc contentsScaling; - 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 void (*mg_canvas_backend_destroy_proc)(mg_canvas_backend* backend); - -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_canvas_backend_destroy_proc destroy; - - 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"app/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_deselect_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_get_size_proc)(mg_surface_data* surface); +typedef vec2 (*mg_surface_contents_scaling_proc)(mg_surface_data* surface); +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_deselect_proc deselect; + mg_surface_swap_interval_proc swapInterval; + mg_surface_get_size_proc getSize; + mg_surface_contents_scaling_proc contentsScaling; + 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 void (*mg_canvas_backend_destroy_proc)(mg_canvas_backend* backend); + +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_canvas_backend_destroy_proc destroy; + + 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/milepost/src/mtl_renderer.h b/milepost/src/graphics/mtl_renderer.h similarity index 100% rename from milepost/src/mtl_renderer.h rename to milepost/src/graphics/mtl_renderer.h diff --git a/milepost/src/mtl_renderer.m b/milepost/src/graphics/mtl_renderer.m similarity index 97% rename from milepost/src/mtl_renderer.m rename to milepost/src/graphics/mtl_renderer.m index 61de9db..aac6899 100644 --- a/milepost/src/mtl_renderer.m +++ b/milepost/src/graphics/mtl_renderer.m @@ -1,1611 +1,1611 @@ -/************************************************************//** -* -* @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 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; - id rasterDispatchBuffer; - - int msaaCount; - vec2 frameSize; - - // encoding context - int eltCap; - int eltCount; - int eltBatchStart; - - int pathCap; - int pathCount; - int pathBatchStart; - - mg_primitive* primitive; - vec4 pathScreenExtents; - vec4 pathUserExtents; - - int maxTileQueueCount; - int maxSegmentCount; - - int currentImageIndex; - -} 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); - } - } -} - -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); -} - -id mg_mtl_grow_input_buffer(id device, id oldBuffer, int oldCopySize, int newSize) -{ - @autoreleasepool - { - MTLResourceOptions bufferOptions = MTLResourceCPUCacheModeWriteCombined - | MTLResourceStorageModeShared; - - id newBuffer = [device newBufferWithLength: newSize options: bufferOptions]; - - memcpy([newBuffer contents], [oldBuffer contents], oldCopySize); - - [oldBuffer release]; - return(newBuffer); - } -} - -void mg_mtl_canvas_encode_element(mg_mtl_canvas_backend* backend, mg_path_elt_type kind, vec2* p) -{ - int bufferIndex = backend->bufferIndex; - int bufferCap = [backend->elementBuffer[bufferIndex] length] / sizeof(mg_mtl_path_elt); - if(backend->eltCount >= bufferCap) - { - int newBufferCap = (int)(bufferCap * 1.5); - int newBufferSize = newBufferCap * sizeof(mg_mtl_path_elt); - - log_info("growing element buffer to %i elements\n", newBufferCap); - - backend->elementBuffer[bufferIndex] = mg_mtl_grow_input_buffer(backend->surface->device, - backend->elementBuffer[bufferIndex], - backend->eltCount * sizeof(mg_mtl_path_elt), - newBufferSize); - } - - mg_mtl_path_elt* elements = (mg_mtl_path_elt*)[backend->elementBuffer[bufferIndex] contents]; - mg_mtl_path_elt* elt = &elements[backend->eltCount]; - backend->eltCount++; - - elt->pathIndex = backend->pathCount - backend->pathBatchStart; - int count = 0; - switch(kind) - { - case MG_PATH_LINE: - backend->maxSegmentCount += 1; - elt->kind = MG_MTL_LINE; - count = 2; - break; - - case MG_PATH_QUADRATIC: - backend->maxSegmentCount += 3; - elt->kind = MG_MTL_QUADRATIC; - count = 3; - break; - - case MG_PATH_CUBIC: - backend->maxSegmentCount += 7; - elt->kind = MG_MTL_CUBIC; - count = 4; - break; - - default: - break; - } - - for(int i=0; ipathUserExtents, p[i]); - - vec2 screenP = mg_mat2x3_mul(backend->primitive->attributes.transform, p[i]); - elt->p[i] = (vector_float2){screenP.x, screenP.y}; - - mg_update_path_extents(&backend->pathScreenExtents, screenP); - } -} - - -void mg_mtl_encode_path(mg_mtl_canvas_backend* backend, mg_primitive* primitive, float scale) -{ - int bufferIndex = backend->bufferIndex; - int bufferCap = [backend->pathBuffer[bufferIndex] length] / sizeof(mg_mtl_path); - if(backend->pathCount >= bufferCap) - { - int newBufferCap = (int)(bufferCap * 1.5); - int newBufferSize = newBufferCap * sizeof(mg_mtl_path); - - log_info("growing path buffer to %i elements\n", newBufferCap); - - backend->pathBuffer[bufferIndex] = mg_mtl_grow_input_buffer(backend->surface->device, - backend->pathBuffer[bufferIndex], - backend->eltCount * sizeof(mg_mtl_path), - newBufferSize); - } - - mg_mtl_path* pathBufferData = (mg_mtl_path*)[backend->pathBuffer[backend->bufferIndex] contents]; - mg_mtl_path* path = &(pathBufferData[backend->pathCount]); - backend->pathCount++; - - path->cmd = (mg_mtl_cmd)primitive->cmd; - - path->box = (vector_float4){backend->pathScreenExtents.x, - backend->pathScreenExtents.y, - backend->pathScreenExtents.z, - backend->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 = {backend->pathUserExtents.x, - backend->pathUserExtents.y, - backend->pathUserExtents.z - backend->pathUserExtents.x, - backend->pathUserExtents.w - backend->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)); - - } - path->texture = backend->currentImageIndex; - - int nTilesX = ((path->box.z - path->box.x)*scale - 1) / MG_MTL_TILE_SIZE + 1; - int nTilesY = ((path->box.w - path->box.y)*scale - 1) / MG_MTL_TILE_SIZE + 1; - backend->maxTileQueueCount += (nTilesX * nTilesY); -} - -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(backend, MG_PATH_LINE, right); - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, left); - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, joint0); - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, joint1); -} - -void mg_mtl_render_stroke_quadratic(mg_mtl_canvas_backend* backend, vec2* p) -{ - f32 width = backend->primitive->attributes.width; - f32 tolerance = minimum(backend->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(backend, p+1); - return; - } - else if(vec2_close(p[1], p[2], equalEps)) - { - mg_mtl_render_stroke_line(backend, 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(backend, splitLeft); - mg_mtl_render_stroke_quadratic(backend, 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(backend, splitLeft); - mg_mtl_render_stroke_quadratic(backend, splitRight); - } - else - { - vec2 tmp = leftHull[0]; - leftHull[0] = leftHull[2]; - leftHull[2] = tmp; - - mg_mtl_canvas_encode_element(backend, MG_PATH_QUADRATIC, rightHull); - mg_mtl_canvas_encode_element(backend, MG_PATH_QUADRATIC, leftHull); - - vec2 joint0[2] = {rightHull[2], leftHull[0]}; - vec2 joint1[2] = {leftHull[2], rightHull[0]}; - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, joint0); - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, joint1); - } - } -} - -void mg_mtl_render_stroke_cubic(mg_mtl_canvas_backend* backend, vec2* p) -{ - f32 width = backend->primitive->attributes.width; - f32 tolerance = minimum(backend->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(backend, 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(backend, 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(backend, 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(backend, splitLeft); - mg_mtl_render_stroke_cubic(backend, 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(backend, splitLeft); - mg_mtl_render_stroke_cubic(backend, 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(backend, MG_PATH_CUBIC, rightHull); - mg_mtl_canvas_encode_element(backend, MG_PATH_CUBIC, leftHull); - - vec2 joint0[2] = {rightHull[3], leftHull[0]}; - vec2 joint1[2] = {leftHull[3], rightHull[0]}; - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, joint0); - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, joint1); - } - } -} - -void mg_mtl_render_stroke_element(mg_mtl_canvas_backend* backend, - 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(backend, controlPoints); - endPointIndex = 1; - break; - - case MG_PATH_QUADRATIC: - mg_mtl_render_stroke_quadratic(backend, controlPoints); - endPointIndex = 2; - break; - - case MG_PATH_CUBIC: - mg_mtl_render_stroke_cubic(backend, 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_canvas_backend* backend, - vec2 p0, - vec2 direction) -{ - mg_attributes* attributes = &backend->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(backend, MG_PATH_LINE, points); - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+1); - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+2); - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+3); -} - -void mg_mtl_stroke_joint(mg_mtl_canvas_backend* backend, - vec2 p0, - vec2 t0, - vec2 t1) -{ - mg_attributes* attributes = &backend->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(backend, MG_PATH_LINE, points); - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+1); - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+2); - mg_mtl_canvas_encode_element(backend, 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(backend, MG_PATH_LINE, points); - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+1); - mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+2); - } -} - -u32 mg_mtl_render_stroke_subpath(mg_mtl_canvas_backend* backend, - 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(backend, elements + startIndex, currentPoint, &startTangent, &endTangent, &endPoint); - - firstTangent = startTangent; - previousEndTangent = endTangent; - currentPoint = endPoint; - - //NOTE(martin): render subsequent elements along with their joints - - mg_attributes* attributes = &backend->primitive->attributes; - - u32 eltIndex = startIndex + 1; - for(; - eltIndexjoint != MG_JOINT_NONE) - { - mg_mtl_stroke_joint(backend, 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(backend, endPoint, endTangent, firstTangent); - } - } - else if(attributes->cap == MG_CAP_SQUARE) - { - //NOTE(martin): add start and end cap - mg_mtl_stroke_cap(backend, startPoint, (vec2){-startTangent.x, -startTangent.y}); - mg_mtl_stroke_cap(backend, endPoint, endTangent); - } - return(eltIndex); -} - -void mg_mtl_render_stroke(mg_mtl_canvas_backend* backend, - 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(backend, elements, path, startIndex, startPoint); - } - } -} - - -void mg_mtl_grow_buffer_if_needed(mg_mtl_canvas_backend* backend, id* buffer, u64 wantedSize) -{ - u64 bufferSize = [(*buffer) length]; - if(bufferSize < wantedSize) - { - int newSize = wantedSize * 1.2; - - @autoreleasepool - { - //NOTE: MTLBuffers are retained by the command buffer, so we don't risk deallocating while the buffer is in use - [*buffer release]; - *buffer = nil; - - id device = backend->surface->device; - MTLResourceOptions bufferOptions = MTLResourceStorageModePrivate; - - *buffer = [device newBufferWithLength: newSize options: bufferOptions]; - } - } -} - -void mg_mtl_render_batch(mg_mtl_canvas_backend* backend, - mg_mtl_surface* surface, - mg_image* images, - int tileSize, - int nTilesX, - int nTilesY, - vec2 viewportSize, - f32 scale) -{ - int pathBufferOffset = backend->pathBatchStart * sizeof(mg_mtl_path); - int elementBufferOffset = backend->eltBatchStart * sizeof(mg_mtl_path_elt); - int pathCount = backend->pathCount - backend->pathBatchStart; - int eltCount = backend->eltCount - backend->eltBatchStart; - - //NOTE: update intermediate buffers sizes if needed - - mg_mtl_grow_buffer_if_needed(backend, &backend->pathQueueBuffer, pathCount * sizeof(mg_mtl_path_queue)); - mg_mtl_grow_buffer_if_needed(backend, &backend->tileQueueBuffer, backend->maxTileQueueCount * sizeof(mg_mtl_tile_queue)); - mg_mtl_grow_buffer_if_needed(backend, &backend->segmentBuffer, backend->maxSegmentCount * sizeof(mg_mtl_segment)); - mg_mtl_grow_buffer_if_needed(backend, &backend->screenTilesBuffer, nTilesX * nTilesY * sizeof(mg_mtl_screen_tile)); - mg_mtl_grow_buffer_if_needed(backend, &backend->tileOpBuffer, backend->maxSegmentCount * 30 * sizeof(mg_mtl_tile_op)); - - //NOTE: encode GPU commands - @autoreleasepool - { - //NOTE: clear output texture - MTLRenderPassDescriptor* clearDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; - clearDescriptor.colorAttachments[0].texture = backend->outTexture; - clearDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; - clearDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0); - clearDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; - - id clearEncoder = [surface->commandBuffer renderCommandEncoderWithDescriptor:clearDescriptor]; - clearEncoder.label = @"clear out texture pass"; - [clearEncoder endEncoding]; - - //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 fillBuffer: backend->rasterDispatchBuffer range: NSMakeRange(0, sizeof(MTLDispatchThreadgroupsIndirectArguments)) value: 0]; - [blitEncoder endEncoding]; - - //NOTE: path setup pass - id pathEncoder = [surface->commandBuffer computeCommandEncoder]; - pathEncoder.label = @"path pass"; - [pathEncoder setComputePipelineState: backend->pathPipeline]; - - int tileQueueMax = [backend->tileQueueBuffer length] / sizeof(mg_mtl_tile_queue); - - [pathEncoder setBytes:&pathCount length:sizeof(int) atIndex:0]; - [pathEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset: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:&tileQueueMax length:sizeof(int) atIndex:5]; - [pathEncoder setBytes:&tileSize length:sizeof(int) atIndex:6]; - [pathEncoder setBytes:&scale length:sizeof(int) atIndex:7]; - - 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]; - - int tileOpMax = [backend->tileOpBuffer length] / sizeof(mg_mtl_tile_op); - int segmentMax = [backend->segmentBuffer length] / sizeof(mg_mtl_segment); - - [segmentEncoder setBytes:&eltCount length:sizeof(int) atIndex:0]; - [segmentEncoder setBuffer:backend->elementBuffer[backend->bufferIndex] offset: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:&tileOpMax length:sizeof(int) atIndex:8]; - [segmentEncoder setBytes:&segmentMax length:sizeof(int) atIndex:9]; - [segmentEncoder setBytes:&tileSize length:sizeof(int) atIndex:10]; - [segmentEncoder setBytes:&scale length:sizeof(int) atIndex:11]; - [segmentEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:12]; - [segmentEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:13]; - - 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: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->rasterDispatchBuffer offset:0 atIndex:6]; - [mergeEncoder setBuffer:backend->screenTilesBuffer offset:0 atIndex:7]; - [mergeEncoder setBytes:&tileOpMax length:sizeof(int) atIndex:8]; - [mergeEncoder setBytes:&tileSize length:sizeof(int) atIndex:9]; - [mergeEncoder setBytes:&scale length:sizeof(float) atIndex:10]; - [mergeEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:11]; - [mergeEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:12]; - - MTLSize mergeGridSize = MTLSizeMake(nTilesX, nTilesY, 1); - MTLSize mergeGroupSize = MTLSizeMake(MG_MTL_TILE_SIZE, MG_MTL_TILE_SIZE, 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: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]; - - for(int i=0; itexture atIndex: 1+i]; - } - } - } - - MTLSize rasterGridSize = MTLSizeMake(viewportSize.x, viewportSize.y, 1); - MTLSize rasterGroupSize = MTLSizeMake(MG_MTL_TILE_SIZE, MG_MTL_TILE_SIZE, 1); - - [rasterEncoder dispatchThreadgroupsWithIndirectBuffer: backend->rasterDispatchBuffer - indirectBufferOffset: 0 - 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]; - } - - backend->pathBatchStart = backend->pathCount; - backend->eltBatchStart = backend->eltCount; - - backend->maxSegmentCount = 0; - backend->maxTileQueueCount = 0; -} - -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(mg_mtl_screen_tile) - 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 | MTLTextureUsageRenderTarget; - texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; - texDesc.width = size.x; - texDesc.height = size.y; - - backend->outTexture = [backend->surface->device newTextureWithDescriptor:texDesc]; - - backend->surface->mtlLayer.drawableSize = (CGSize){size.x, size.y}; - - 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 input buffers - dispatch_semaphore_wait(backend->bufferSemaphore, DISPATCH_TIME_FOREVER); - backend->bufferIndex = (backend->bufferIndex + 1) % MG_MTL_INPUT_BUFFERS_COUNT; - - //NOTE: ensure screen tiles buffer is correct size - mg_mtl_surface* surface = backend->surface; - - vec2 frameSize = surface->interface.getSize((mg_surface_data*)surface); - - f32 scale = surface->mtlLayer.contentsScale; - vec2 viewportSize = {frameSize.x * scale, frameSize.y * scale}; - int tileSize = MG_MTL_TILE_SIZE; - int nTilesX = (int)(viewportSize.x * scale + tileSize - 1)/tileSize; - int nTilesY = (int)(viewportSize.y * scale + tileSize - 1)/tileSize; - - if(viewportSize.x != backend->frameSize.x || viewportSize.y != backend->frameSize.y) - { - mg_mtl_canvas_resize(backend, viewportSize); - } - - //NOTE: acquire metal resources for rendering - 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->pathCount = 0; - backend->pathBatchStart = 0; - backend->eltCount = 0; - backend->eltBatchStart = 0; - backend->maxSegmentCount = 0; - backend->maxTileQueueCount = 0; - - //NOTE: encode and render batches - vec2 currentPos = {0}; - mg_image images[MG_MTL_MAX_IMAGES_PER_BATCH] = {0}; - int imageCount = 0; - - for(int primitiveIndex = 0; primitiveIndex < primitiveCount; primitiveIndex++) - { - mg_primitive* primitive = &primitives[primitiveIndex]; - - if(primitive->attributes.image.h != 0) - { - backend->currentImageIndex = -1; - for(int i=0; iattributes.image.h) - { - backend->currentImageIndex = i; - } - } - if(backend->currentImageIndex <= 0) - { - if(imageCountattributes.image; - backend->currentImageIndex = imageCount; - imageCount++; - } - else - { - mg_mtl_render_batch(backend, - surface, - images, - tileSize, - nTilesX, - nTilesY, - viewportSize, - scale); - - images[0] = primitive->attributes.image; - backend->currentImageIndex = 0; - imageCount = 1; - } - } - } - else - { - backend->currentImageIndex = -1; - } - - if(primitive->path.count) - { - backend->primitive = primitive; - backend->pathScreenExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; - backend->pathUserExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; - - if(primitive->cmd == MG_CMD_STROKE) - { - mg_mtl_render_stroke(backend, pathElements + primitive->path.startIndex, &primitive->path); - } - else - { - for(int eltIndex = 0; - (eltIndex < primitive->path.count) && (primitive->path.startIndex + eltIndex < eltCount); - eltIndex++) - { - 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(backend, elt->type, p); - } - 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: encode path - mg_mtl_encode_path(backend, primitive, scale); - } - } - - mg_mtl_render_batch(backend, - surface, - images, - 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_DEFAULT_PATH_BUFFER_LEN = (4<<10), - MG_MTL_DEFAULT_ELT_BUFFER_LEN = (4<<10), - - MG_MTL_DEFAULT_SEGMENT_BUFFER_LEN = (4<<10), - MG_MTL_DEFAULT_PATH_QUEUE_BUFFER_LEN = (4<<10), - MG_MTL_DEFAULT_TILE_QUEUE_BUFFER_LEN = (4<<10), - MG_MTL_DEFAULT_TILE_OP_BUFFER_LEN = (4<<20); - -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 = path_executable_relative(mem_scratch(), STR8("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 - vec2 size = surface->interface.getSize((mg_surface_data*)surface); - f32 scale = surface->mtlLayer.contentsScale; - - backend->frameSize = (vec2){size.x*scale, size.y*scale}; - - MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; - texDesc.textureType = MTLTextureType2D; - texDesc.storageMode = MTLStorageModePrivate; - texDesc.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite | MTLTextureUsageRenderTarget; - 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_DEFAULT_PATH_BUFFER_LEN * sizeof(mg_mtl_path) - options: bufferOptions]; - - backend->elementBuffer[i] = [surface->device newBufferWithLength: MG_MTL_DEFAULT_ELT_BUFFER_LEN * sizeof(mg_mtl_path_elt) - options: bufferOptions]; - } - - bufferOptions = MTLResourceStorageModePrivate; - backend->segmentBuffer = [surface->device newBufferWithLength: MG_MTL_DEFAULT_SEGMENT_BUFFER_LEN * sizeof(mg_mtl_segment) - options: bufferOptions]; - - backend->segmentCountBuffer = [surface->device newBufferWithLength: sizeof(int) - options: bufferOptions]; - - backend->pathQueueBuffer = [surface->device newBufferWithLength: MG_MTL_DEFAULT_PATH_QUEUE_BUFFER_LEN * sizeof(mg_mtl_path_queue) - options: bufferOptions]; - - backend->tileQueueBuffer = [surface->device newBufferWithLength: MG_MTL_DEFAULT_TILE_QUEUE_BUFFER_LEN * sizeof(mg_mtl_tile_queue) - options: bufferOptions]; - - backend->tileQueueCountBuffer = [surface->device newBufferWithLength: sizeof(int) - options: bufferOptions]; - - backend->tileOpBuffer = [surface->device newBufferWithLength: MG_MTL_DEFAULT_TILE_OP_BUFFER_LEN * sizeof(mg_mtl_tile_op) - options: bufferOptions]; - - backend->tileOpCountBuffer = [surface->device newBufferWithLength: sizeof(int) - options: bufferOptions]; - - backend->rasterDispatchBuffer = [surface->device newBufferWithLength: sizeof(MTLDispatchThreadgroupsIndirectArguments) - options: bufferOptions]; - - int tileSize = MG_MTL_TILE_SIZE; - int nTilesX = (int)(backend->frameSize.x + tileSize - 1)/tileSize; - int nTilesY = (int)(backend->frameSize.y + tileSize - 1)/tileSize; - backend->screenTilesBuffer = [surface->device newBufferWithLength: nTilesX*nTilesY*sizeof(mg_mtl_screen_tile) - 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"util/macro_helpers.h" +#include"app/osx_app.h" + +#include"graphics_surface.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 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; + id rasterDispatchBuffer; + + int msaaCount; + vec2 frameSize; + + // encoding context + int eltCap; + int eltCount; + int eltBatchStart; + + int pathCap; + int pathCount; + int pathBatchStart; + + mg_primitive* primitive; + vec4 pathScreenExtents; + vec4 pathUserExtents; + + int maxTileQueueCount; + int maxSegmentCount; + + int currentImageIndex; + +} 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); + } + } +} + +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); +} + +id mg_mtl_grow_input_buffer(id device, id oldBuffer, int oldCopySize, int newSize) +{ + @autoreleasepool + { + MTLResourceOptions bufferOptions = MTLResourceCPUCacheModeWriteCombined + | MTLResourceStorageModeShared; + + id newBuffer = [device newBufferWithLength: newSize options: bufferOptions]; + + memcpy([newBuffer contents], [oldBuffer contents], oldCopySize); + + [oldBuffer release]; + return(newBuffer); + } +} + +void mg_mtl_canvas_encode_element(mg_mtl_canvas_backend* backend, mg_path_elt_type kind, vec2* p) +{ + int bufferIndex = backend->bufferIndex; + int bufferCap = [backend->elementBuffer[bufferIndex] length] / sizeof(mg_mtl_path_elt); + if(backend->eltCount >= bufferCap) + { + int newBufferCap = (int)(bufferCap * 1.5); + int newBufferSize = newBufferCap * sizeof(mg_mtl_path_elt); + + log_info("growing element buffer to %i elements\n", newBufferCap); + + backend->elementBuffer[bufferIndex] = mg_mtl_grow_input_buffer(backend->surface->device, + backend->elementBuffer[bufferIndex], + backend->eltCount * sizeof(mg_mtl_path_elt), + newBufferSize); + } + + mg_mtl_path_elt* elements = (mg_mtl_path_elt*)[backend->elementBuffer[bufferIndex] contents]; + mg_mtl_path_elt* elt = &elements[backend->eltCount]; + backend->eltCount++; + + elt->pathIndex = backend->pathCount - backend->pathBatchStart; + int count = 0; + switch(kind) + { + case MG_PATH_LINE: + backend->maxSegmentCount += 1; + elt->kind = MG_MTL_LINE; + count = 2; + break; + + case MG_PATH_QUADRATIC: + backend->maxSegmentCount += 3; + elt->kind = MG_MTL_QUADRATIC; + count = 3; + break; + + case MG_PATH_CUBIC: + backend->maxSegmentCount += 7; + elt->kind = MG_MTL_CUBIC; + count = 4; + break; + + default: + break; + } + + for(int i=0; ipathUserExtents, p[i]); + + vec2 screenP = mg_mat2x3_mul(backend->primitive->attributes.transform, p[i]); + elt->p[i] = (vector_float2){screenP.x, screenP.y}; + + mg_update_path_extents(&backend->pathScreenExtents, screenP); + } +} + + +void mg_mtl_encode_path(mg_mtl_canvas_backend* backend, mg_primitive* primitive, float scale) +{ + int bufferIndex = backend->bufferIndex; + int bufferCap = [backend->pathBuffer[bufferIndex] length] / sizeof(mg_mtl_path); + if(backend->pathCount >= bufferCap) + { + int newBufferCap = (int)(bufferCap * 1.5); + int newBufferSize = newBufferCap * sizeof(mg_mtl_path); + + log_info("growing path buffer to %i elements\n", newBufferCap); + + backend->pathBuffer[bufferIndex] = mg_mtl_grow_input_buffer(backend->surface->device, + backend->pathBuffer[bufferIndex], + backend->eltCount * sizeof(mg_mtl_path), + newBufferSize); + } + + mg_mtl_path* pathBufferData = (mg_mtl_path*)[backend->pathBuffer[backend->bufferIndex] contents]; + mg_mtl_path* path = &(pathBufferData[backend->pathCount]); + backend->pathCount++; + + path->cmd = (mg_mtl_cmd)primitive->cmd; + + path->box = (vector_float4){backend->pathScreenExtents.x, + backend->pathScreenExtents.y, + backend->pathScreenExtents.z, + backend->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 = {backend->pathUserExtents.x, + backend->pathUserExtents.y, + backend->pathUserExtents.z - backend->pathUserExtents.x, + backend->pathUserExtents.w - backend->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)); + + } + path->texture = backend->currentImageIndex; + + int nTilesX = ((path->box.z - path->box.x)*scale - 1) / MG_MTL_TILE_SIZE + 1; + int nTilesY = ((path->box.w - path->box.y)*scale - 1) / MG_MTL_TILE_SIZE + 1; + backend->maxTileQueueCount += (nTilesX * nTilesY); +} + +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(backend, MG_PATH_LINE, right); + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, left); + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, joint0); + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, joint1); +} + +void mg_mtl_render_stroke_quadratic(mg_mtl_canvas_backend* backend, vec2* p) +{ + f32 width = backend->primitive->attributes.width; + f32 tolerance = minimum(backend->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(backend, p+1); + return; + } + else if(vec2_close(p[1], p[2], equalEps)) + { + mg_mtl_render_stroke_line(backend, 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(backend, splitLeft); + mg_mtl_render_stroke_quadratic(backend, 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(backend, splitLeft); + mg_mtl_render_stroke_quadratic(backend, splitRight); + } + else + { + vec2 tmp = leftHull[0]; + leftHull[0] = leftHull[2]; + leftHull[2] = tmp; + + mg_mtl_canvas_encode_element(backend, MG_PATH_QUADRATIC, rightHull); + mg_mtl_canvas_encode_element(backend, MG_PATH_QUADRATIC, leftHull); + + vec2 joint0[2] = {rightHull[2], leftHull[0]}; + vec2 joint1[2] = {leftHull[2], rightHull[0]}; + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, joint0); + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, joint1); + } + } +} + +void mg_mtl_render_stroke_cubic(mg_mtl_canvas_backend* backend, vec2* p) +{ + f32 width = backend->primitive->attributes.width; + f32 tolerance = minimum(backend->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(backend, 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(backend, 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(backend, 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(backend, splitLeft); + mg_mtl_render_stroke_cubic(backend, 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(backend, splitLeft); + mg_mtl_render_stroke_cubic(backend, 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(backend, MG_PATH_CUBIC, rightHull); + mg_mtl_canvas_encode_element(backend, MG_PATH_CUBIC, leftHull); + + vec2 joint0[2] = {rightHull[3], leftHull[0]}; + vec2 joint1[2] = {leftHull[3], rightHull[0]}; + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, joint0); + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, joint1); + } + } +} + +void mg_mtl_render_stroke_element(mg_mtl_canvas_backend* backend, + 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(backend, controlPoints); + endPointIndex = 1; + break; + + case MG_PATH_QUADRATIC: + mg_mtl_render_stroke_quadratic(backend, controlPoints); + endPointIndex = 2; + break; + + case MG_PATH_CUBIC: + mg_mtl_render_stroke_cubic(backend, 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_canvas_backend* backend, + vec2 p0, + vec2 direction) +{ + mg_attributes* attributes = &backend->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(backend, MG_PATH_LINE, points); + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+1); + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+2); + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+3); +} + +void mg_mtl_stroke_joint(mg_mtl_canvas_backend* backend, + vec2 p0, + vec2 t0, + vec2 t1) +{ + mg_attributes* attributes = &backend->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(backend, MG_PATH_LINE, points); + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+1); + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+2); + mg_mtl_canvas_encode_element(backend, 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(backend, MG_PATH_LINE, points); + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+1); + mg_mtl_canvas_encode_element(backend, MG_PATH_LINE, points+2); + } +} + +u32 mg_mtl_render_stroke_subpath(mg_mtl_canvas_backend* backend, + 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(backend, elements + startIndex, currentPoint, &startTangent, &endTangent, &endPoint); + + firstTangent = startTangent; + previousEndTangent = endTangent; + currentPoint = endPoint; + + //NOTE(martin): render subsequent elements along with their joints + + mg_attributes* attributes = &backend->primitive->attributes; + + u32 eltIndex = startIndex + 1; + for(; + eltIndexjoint != MG_JOINT_NONE) + { + mg_mtl_stroke_joint(backend, 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(backend, endPoint, endTangent, firstTangent); + } + } + else if(attributes->cap == MG_CAP_SQUARE) + { + //NOTE(martin): add start and end cap + mg_mtl_stroke_cap(backend, startPoint, (vec2){-startTangent.x, -startTangent.y}); + mg_mtl_stroke_cap(backend, endPoint, endTangent); + } + return(eltIndex); +} + +void mg_mtl_render_stroke(mg_mtl_canvas_backend* backend, + 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(backend, elements, path, startIndex, startPoint); + } + } +} + + +void mg_mtl_grow_buffer_if_needed(mg_mtl_canvas_backend* backend, id* buffer, u64 wantedSize) +{ + u64 bufferSize = [(*buffer) length]; + if(bufferSize < wantedSize) + { + int newSize = wantedSize * 1.2; + + @autoreleasepool + { + //NOTE: MTLBuffers are retained by the command buffer, so we don't risk deallocating while the buffer is in use + [*buffer release]; + *buffer = nil; + + id device = backend->surface->device; + MTLResourceOptions bufferOptions = MTLResourceStorageModePrivate; + + *buffer = [device newBufferWithLength: newSize options: bufferOptions]; + } + } +} + +void mg_mtl_render_batch(mg_mtl_canvas_backend* backend, + mg_mtl_surface* surface, + mg_image* images, + int tileSize, + int nTilesX, + int nTilesY, + vec2 viewportSize, + f32 scale) +{ + int pathBufferOffset = backend->pathBatchStart * sizeof(mg_mtl_path); + int elementBufferOffset = backend->eltBatchStart * sizeof(mg_mtl_path_elt); + int pathCount = backend->pathCount - backend->pathBatchStart; + int eltCount = backend->eltCount - backend->eltBatchStart; + + //NOTE: update intermediate buffers sizes if needed + + mg_mtl_grow_buffer_if_needed(backend, &backend->pathQueueBuffer, pathCount * sizeof(mg_mtl_path_queue)); + mg_mtl_grow_buffer_if_needed(backend, &backend->tileQueueBuffer, backend->maxTileQueueCount * sizeof(mg_mtl_tile_queue)); + mg_mtl_grow_buffer_if_needed(backend, &backend->segmentBuffer, backend->maxSegmentCount * sizeof(mg_mtl_segment)); + mg_mtl_grow_buffer_if_needed(backend, &backend->screenTilesBuffer, nTilesX * nTilesY * sizeof(mg_mtl_screen_tile)); + mg_mtl_grow_buffer_if_needed(backend, &backend->tileOpBuffer, backend->maxSegmentCount * 30 * sizeof(mg_mtl_tile_op)); + + //NOTE: encode GPU commands + @autoreleasepool + { + //NOTE: clear output texture + MTLRenderPassDescriptor* clearDescriptor = [MTLRenderPassDescriptor renderPassDescriptor]; + clearDescriptor.colorAttachments[0].texture = backend->outTexture; + clearDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; + clearDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(0, 0, 0, 0); + clearDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; + + id clearEncoder = [surface->commandBuffer renderCommandEncoderWithDescriptor:clearDescriptor]; + clearEncoder.label = @"clear out texture pass"; + [clearEncoder endEncoding]; + + //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 fillBuffer: backend->rasterDispatchBuffer range: NSMakeRange(0, sizeof(MTLDispatchThreadgroupsIndirectArguments)) value: 0]; + [blitEncoder endEncoding]; + + //NOTE: path setup pass + id pathEncoder = [surface->commandBuffer computeCommandEncoder]; + pathEncoder.label = @"path pass"; + [pathEncoder setComputePipelineState: backend->pathPipeline]; + + int tileQueueMax = [backend->tileQueueBuffer length] / sizeof(mg_mtl_tile_queue); + + [pathEncoder setBytes:&pathCount length:sizeof(int) atIndex:0]; + [pathEncoder setBuffer:backend->pathBuffer[backend->bufferIndex] offset: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:&tileQueueMax length:sizeof(int) atIndex:5]; + [pathEncoder setBytes:&tileSize length:sizeof(int) atIndex:6]; + [pathEncoder setBytes:&scale length:sizeof(int) atIndex:7]; + + 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]; + + int tileOpMax = [backend->tileOpBuffer length] / sizeof(mg_mtl_tile_op); + int segmentMax = [backend->segmentBuffer length] / sizeof(mg_mtl_segment); + + [segmentEncoder setBytes:&eltCount length:sizeof(int) atIndex:0]; + [segmentEncoder setBuffer:backend->elementBuffer[backend->bufferIndex] offset: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:&tileOpMax length:sizeof(int) atIndex:8]; + [segmentEncoder setBytes:&segmentMax length:sizeof(int) atIndex:9]; + [segmentEncoder setBytes:&tileSize length:sizeof(int) atIndex:10]; + [segmentEncoder setBytes:&scale length:sizeof(int) atIndex:11]; + [segmentEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:12]; + [segmentEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:13]; + + 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: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->rasterDispatchBuffer offset:0 atIndex:6]; + [mergeEncoder setBuffer:backend->screenTilesBuffer offset:0 atIndex:7]; + [mergeEncoder setBytes:&tileOpMax length:sizeof(int) atIndex:8]; + [mergeEncoder setBytes:&tileSize length:sizeof(int) atIndex:9]; + [mergeEncoder setBytes:&scale length:sizeof(float) atIndex:10]; + [mergeEncoder setBuffer:backend->logBuffer[backend->bufferIndex] offset:0 atIndex:11]; + [mergeEncoder setBuffer:backend->logOffsetBuffer[backend->bufferIndex] offset:0 atIndex:12]; + + MTLSize mergeGridSize = MTLSizeMake(nTilesX, nTilesY, 1); + MTLSize mergeGroupSize = MTLSizeMake(MG_MTL_TILE_SIZE, MG_MTL_TILE_SIZE, 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: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]; + + for(int i=0; itexture atIndex: 1+i]; + } + } + } + + MTLSize rasterGridSize = MTLSizeMake(viewportSize.x, viewportSize.y, 1); + MTLSize rasterGroupSize = MTLSizeMake(MG_MTL_TILE_SIZE, MG_MTL_TILE_SIZE, 1); + + [rasterEncoder dispatchThreadgroupsWithIndirectBuffer: backend->rasterDispatchBuffer + indirectBufferOffset: 0 + 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]; + } + + backend->pathBatchStart = backend->pathCount; + backend->eltBatchStart = backend->eltCount; + + backend->maxSegmentCount = 0; + backend->maxTileQueueCount = 0; +} + +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(mg_mtl_screen_tile) + 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 | MTLTextureUsageRenderTarget; + texDesc.pixelFormat = MTLPixelFormatRGBA8Unorm; + texDesc.width = size.x; + texDesc.height = size.y; + + backend->outTexture = [backend->surface->device newTextureWithDescriptor:texDesc]; + + backend->surface->mtlLayer.drawableSize = (CGSize){size.x, size.y}; + + 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 input buffers + dispatch_semaphore_wait(backend->bufferSemaphore, DISPATCH_TIME_FOREVER); + backend->bufferIndex = (backend->bufferIndex + 1) % MG_MTL_INPUT_BUFFERS_COUNT; + + //NOTE: ensure screen tiles buffer is correct size + mg_mtl_surface* surface = backend->surface; + + vec2 frameSize = surface->interface.getSize((mg_surface_data*)surface); + + f32 scale = surface->mtlLayer.contentsScale; + vec2 viewportSize = {frameSize.x * scale, frameSize.y * scale}; + int tileSize = MG_MTL_TILE_SIZE; + int nTilesX = (int)(viewportSize.x * scale + tileSize - 1)/tileSize; + int nTilesY = (int)(viewportSize.y * scale + tileSize - 1)/tileSize; + + if(viewportSize.x != backend->frameSize.x || viewportSize.y != backend->frameSize.y) + { + mg_mtl_canvas_resize(backend, viewportSize); + } + + //NOTE: acquire metal resources for rendering + 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->pathCount = 0; + backend->pathBatchStart = 0; + backend->eltCount = 0; + backend->eltBatchStart = 0; + backend->maxSegmentCount = 0; + backend->maxTileQueueCount = 0; + + //NOTE: encode and render batches + vec2 currentPos = {0}; + mg_image images[MG_MTL_MAX_IMAGES_PER_BATCH] = {0}; + int imageCount = 0; + + for(int primitiveIndex = 0; primitiveIndex < primitiveCount; primitiveIndex++) + { + mg_primitive* primitive = &primitives[primitiveIndex]; + + if(primitive->attributes.image.h != 0) + { + backend->currentImageIndex = -1; + for(int i=0; iattributes.image.h) + { + backend->currentImageIndex = i; + } + } + if(backend->currentImageIndex <= 0) + { + if(imageCountattributes.image; + backend->currentImageIndex = imageCount; + imageCount++; + } + else + { + mg_mtl_render_batch(backend, + surface, + images, + tileSize, + nTilesX, + nTilesY, + viewportSize, + scale); + + images[0] = primitive->attributes.image; + backend->currentImageIndex = 0; + imageCount = 1; + } + } + } + else + { + backend->currentImageIndex = -1; + } + + if(primitive->path.count) + { + backend->primitive = primitive; + backend->pathScreenExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; + backend->pathUserExtents = (vec4){FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX}; + + if(primitive->cmd == MG_CMD_STROKE) + { + mg_mtl_render_stroke(backend, pathElements + primitive->path.startIndex, &primitive->path); + } + else + { + for(int eltIndex = 0; + (eltIndex < primitive->path.count) && (primitive->path.startIndex + eltIndex < eltCount); + eltIndex++) + { + 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(backend, elt->type, p); + } + 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: encode path + mg_mtl_encode_path(backend, primitive, scale); + } + } + + mg_mtl_render_batch(backend, + surface, + images, + 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_DEFAULT_PATH_BUFFER_LEN = (4<<10), + MG_MTL_DEFAULT_ELT_BUFFER_LEN = (4<<10), + + MG_MTL_DEFAULT_SEGMENT_BUFFER_LEN = (4<<10), + MG_MTL_DEFAULT_PATH_QUEUE_BUFFER_LEN = (4<<10), + MG_MTL_DEFAULT_TILE_QUEUE_BUFFER_LEN = (4<<10), + MG_MTL_DEFAULT_TILE_OP_BUFFER_LEN = (4<<20); + +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 = path_executable_relative(mem_scratch(), STR8("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 + vec2 size = surface->interface.getSize((mg_surface_data*)surface); + f32 scale = surface->mtlLayer.contentsScale; + + backend->frameSize = (vec2){size.x*scale, size.y*scale}; + + MTLTextureDescriptor* texDesc = [[MTLTextureDescriptor alloc] init]; + texDesc.textureType = MTLTextureType2D; + texDesc.storageMode = MTLStorageModePrivate; + texDesc.usage = MTLTextureUsageShaderRead | MTLTextureUsageShaderWrite | MTLTextureUsageRenderTarget; + 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_DEFAULT_PATH_BUFFER_LEN * sizeof(mg_mtl_path) + options: bufferOptions]; + + backend->elementBuffer[i] = [surface->device newBufferWithLength: MG_MTL_DEFAULT_ELT_BUFFER_LEN * sizeof(mg_mtl_path_elt) + options: bufferOptions]; + } + + bufferOptions = MTLResourceStorageModePrivate; + backend->segmentBuffer = [surface->device newBufferWithLength: MG_MTL_DEFAULT_SEGMENT_BUFFER_LEN * sizeof(mg_mtl_segment) + options: bufferOptions]; + + backend->segmentCountBuffer = [surface->device newBufferWithLength: sizeof(int) + options: bufferOptions]; + + backend->pathQueueBuffer = [surface->device newBufferWithLength: MG_MTL_DEFAULT_PATH_QUEUE_BUFFER_LEN * sizeof(mg_mtl_path_queue) + options: bufferOptions]; + + backend->tileQueueBuffer = [surface->device newBufferWithLength: MG_MTL_DEFAULT_TILE_QUEUE_BUFFER_LEN * sizeof(mg_mtl_tile_queue) + options: bufferOptions]; + + backend->tileQueueCountBuffer = [surface->device newBufferWithLength: sizeof(int) + options: bufferOptions]; + + backend->tileOpBuffer = [surface->device newBufferWithLength: MG_MTL_DEFAULT_TILE_OP_BUFFER_LEN * sizeof(mg_mtl_tile_op) + options: bufferOptions]; + + backend->tileOpCountBuffer = [surface->device newBufferWithLength: sizeof(int) + options: bufferOptions]; + + backend->rasterDispatchBuffer = [surface->device newBufferWithLength: sizeof(MTLDispatchThreadgroupsIndirectArguments) + options: bufferOptions]; + + int tileSize = MG_MTL_TILE_SIZE; + int nTilesX = (int)(backend->frameSize.x + tileSize - 1)/tileSize; + int nTilesY = (int)(backend->frameSize.y + tileSize - 1)/tileSize; + backend->screenTilesBuffer = [surface->device newBufferWithLength: nTilesX*nTilesY*sizeof(mg_mtl_screen_tile) + options: bufferOptions]; + + bufferOptions = MTLResourceStorageModeShared; + for(int i=0; ilogBuffer[i] = [surface->device newBufferWithLength: 1<<20 + options: bufferOptions]; + + backend->logOffsetBuffer[i] = [surface->device newBufferWithLength: sizeof(int) + options: bufferOptions]; + } + } + return((mg_canvas_backend*)backend); +} + +mg_surface_data* mtl_canvas_surface_create_for_window(mp_window window) +{ + mg_mtl_surface* surface = (mg_mtl_surface*)mg_mtl_surface_create_for_window(window); + + if(surface) + { + surface->interface.backend = mtl_canvas_backend_create(surface); + if(surface->interface.backend) + { + surface->interface.api = MG_CANVAS; + } + else + { + surface->interface.destroy((mg_surface_data*)surface); + surface = 0; + } + } + return((mg_surface_data*)surface); +} diff --git a/milepost/src/mtl_renderer.metal b/milepost/src/graphics/mtl_renderer.metal similarity index 100% rename from milepost/src/mtl_renderer.metal rename to milepost/src/graphics/mtl_renderer.metal diff --git a/milepost/src/mtl_surface.h b/milepost/src/graphics/mtl_surface.h similarity index 100% rename from milepost/src/mtl_surface.h rename to milepost/src/graphics/mtl_surface.h diff --git a/milepost/src/mtl_surface.m b/milepost/src/graphics/mtl_surface.m similarity index 99% rename from milepost/src/mtl_surface.m rename to milepost/src/graphics/mtl_surface.m index 179e6b5..9b3d14c 100644 --- a/milepost/src/mtl_surface.m +++ b/milepost/src/graphics/mtl_surface.m @@ -12,8 +12,8 @@ #include #include"graphics_surface.h" -#include"macro_helpers.h" -#include"osx_app.h" +#include"util/macro_helpers.h" +#include"app/osx_app.h" typedef struct mg_mtl_surface { diff --git a/milepost/src/wgl_surface.c b/milepost/src/graphics/wgl_surface.c similarity index 99% rename from milepost/src/wgl_surface.c rename to milepost/src/graphics/wgl_surface.c index 40cc5e5..912b760 100644 --- a/milepost/src/wgl_surface.c +++ b/milepost/src/graphics/wgl_surface.c @@ -6,12 +6,12 @@ * @revision: * *****************************************************************/ -#include"win32_app.h" +#include"app/win32_app.h" #include"graphics_surface.h" #include"gl_loader.h" #include -#include"macro_helpers.h" +#include"util/macro_helpers.h" #define WGL_PROC_LIST \ WGL_PROC(WGLCHOOSEPIXELFORMATARB, wglChoosePixelFormatARB) \ diff --git a/milepost/src/wgl_surface.h b/milepost/src/graphics/wgl_surface.h similarity index 100% rename from milepost/src/wgl_surface.h rename to milepost/src/graphics/wgl_surface.h diff --git a/milepost/src/milepost.c b/milepost/src/milepost.c index 3e7a381..914a686 100644 --- a/milepost/src/milepost.c +++ b/milepost/src/milepost.c @@ -60,24 +60,24 @@ //--------------------------------------------------------------- #if PLATFORM_WINDOWS - #include"win32_app.c" - #include"graphics_common.c" - #include"graphics_surface.c" + #include"app/win32_app.c" + #include"graphics/graphics_common.c" + #include"graphics/graphics_surface.c" #if MG_COMPILE_GL || MG_COMPILE_GLES - #include"gl_loader.c" + #include"graphics/gl_loader.c" #endif #if MG_COMPILE_GL - #include"wgl_surface.c" + #include"graphics/wgl_surface.c" #endif #if MG_COMPILE_CANVAS - #include"gl_canvas.c" + #include"graphics/gl_canvas.c" #endif #if MG_COMPILE_GLES - #include"egl_surface.c" + #include"graphics/egl_surface.c" #endif #elif PLATFORM_MACOS @@ -86,5 +86,5 @@ #error "Unsupported platform" #endif -#include"input_state.c" -#include"ui.c" +#include"ui/input_state.c" +#include"ui/ui.c" diff --git a/milepost/src/milepost.h b/milepost/src/milepost.h index 19b385b..d3ac098 100644 --- a/milepost/src/milepost.h +++ b/milepost/src/milepost.h @@ -38,15 +38,13 @@ //---------------------------------------------------------------- // application/graphics layer //---------------------------------------------------------------- -#include"mp_app.h" -#include"graphics.h" -#include"input_state.h" -#include"ui.h" +#include"app/mp_app.h" +#include"graphics/graphics.h" +#include"ui/input_state.h" +#include"ui/ui.h" #ifdef MG_INCLUDE_GL_API - #include"gl_api.h" + #include"graphics/gl_api.h" #endif -//#include"ui.h" - #endif //__MILEPOST_H_ diff --git a/milepost/src/milepost.m b/milepost/src/milepost.m index 6cfb464..ef58b91 100644 --- a/milepost/src/milepost.m +++ b/milepost/src/milepost.m @@ -7,20 +7,20 @@ * *****************************************************************/ -#include"osx_path.m" -#include"osx_app.m" -#include"graphics_common.c" -#include"graphics_surface.c" +#include"platform/osx_path.m" +#include"app/osx_app.m" +#include"graphics/graphics_common.c" +#include"graphics/graphics_surface.c" #if MG_COMPILE_METAL - #include"mtl_surface.m" + #include"graphics/mtl_surface.m" #endif #if MG_COMPILE_CANVAS - #include"mtl_renderer.m" + #include"graphics/mtl_renderer.m" #endif #if MG_COMPILE_GLES - #include"gl_loader.c" - #include"egl_surface.c" + #include"graphics/gl_loader.c" + #include"graphics/egl_surface.c" #endif diff --git a/milepost/src/input_state.c b/milepost/src/ui/input_state.c similarity index 100% rename from milepost/src/input_state.c rename to milepost/src/ui/input_state.c diff --git a/milepost/src/input_state.h b/milepost/src/ui/input_state.h similarity index 99% rename from milepost/src/input_state.h rename to milepost/src/ui/input_state.h index 7af604e..e9d92d1 100644 --- a/milepost/src/input_state.h +++ b/milepost/src/ui/input_state.h @@ -12,7 +12,7 @@ #include"util/typedefs.h" #include"util/strings.h" #include"util/utf8.h" -#include"mp_app.h" +#include"app/mp_app.h" typedef struct mp_key_state { diff --git a/milepost/src/ui.c b/milepost/src/ui/ui.c similarity index 96% rename from milepost/src/ui.c rename to milepost/src/ui/ui.c index 79153b5..41ff8a9 100644 --- a/milepost/src/ui.c +++ b/milepost/src/ui/ui.c @@ -1,2818 +1,2818 @@ -/************************************************************//** -* -* @file: ui.c -* @author: Martin Fouilleul -* @date: 08/08/2022 -* @revision: -* -*****************************************************************/ -#include"platform/platform.h" -#include"platform/platform_assert.h" -#include"platform/platform_clock.h" -#include"util/memory.h" -#include"util/hash.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 !PLATFORM_ORCA - 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); - #endif -} - -str32 ui_edit_replace_selection_with_clipboard(ui_context* ui, str32 codepoints) -{ - #if PLATFORM_ORCA - str32 result = {0}; - #else - 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); - #endif - 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; - -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 = MP_KEYMOD_MAIN_MODIFIER, - .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 = MP_KEYMOD_MAIN_MODIFIER, - .operation = UI_EDIT_CUT, - .move = UI_EDIT_MOVE_NONE - }, - //NOTE(martin): copy - { - .key = MP_KEY_C, - .mods = MP_KEYMOD_MAIN_MODIFIER, - .operation = UI_EDIT_COPY, - .move = UI_EDIT_MOVE_NONE - }, - //NOTE(martin): paste - { - .key = MP_KEY_V, - .mods = MP_KEYMOD_MAIN_MODIFIER, - .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/platform.h" +#include"platform/platform_assert.h" +#include"platform/platform_clock.h" +#include"util/memory.h" +#include"util/hash.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 !PLATFORM_ORCA + 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); + #endif +} + +str32 ui_edit_replace_selection_with_clipboard(ui_context* ui, str32 codepoints) +{ + #if PLATFORM_ORCA + str32 result = {0}; + #else + 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); + #endif + 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; + +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 = MP_KEYMOD_MAIN_MODIFIER, + .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 = MP_KEYMOD_MAIN_MODIFIER, + .operation = UI_EDIT_CUT, + .move = UI_EDIT_MOVE_NONE + }, + //NOTE(martin): copy + { + .key = MP_KEY_C, + .mods = MP_KEYMOD_MAIN_MODIFIER, + .operation = UI_EDIT_COPY, + .move = UI_EDIT_MOVE_NONE + }, + //NOTE(martin): paste + { + .key = MP_KEY_V, + .mods = MP_KEYMOD_MAIN_MODIFIER, + .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/milepost/src/ui.h b/milepost/src/ui/ui.h similarity index 95% rename from milepost/src/ui.h rename to milepost/src/ui/ui.h index 4c4a843..cd25c36 100644 --- a/milepost/src/ui.h +++ b/milepost/src/ui/ui.h @@ -1,549 +1,549 @@ -/************************************************************//** -* -* @file: ui.h -* @author: Martin Fouilleul -* @date: 08/08/2022 -* @revision: -* -*****************************************************************/ -#ifndef __UI_H_ -#define __UI_H_ - -#include"util/typedefs.h" -#include"util/lists.h" -#include"input_state.h" -#include"graphics.h" - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct ui_key -{ - u64 hash; -} ui_key; - -typedef enum -{ - UI_AXIS_X, - UI_AXIS_Y, - UI_AXIS_COUNT -} ui_axis; - -typedef enum -{ - UI_ALIGN_START, - UI_ALIGN_END, - UI_ALIGN_CENTER, -} ui_align; - -typedef union ui_layout_align -{ - struct - { - ui_align x; - ui_align y; - }; - ui_align c[UI_AXIS_COUNT]; -} ui_layout_align; - -typedef struct ui_layout -{ - ui_axis axis; - f32 spacing; - union - { - struct - { - f32 x; - f32 y; - }; - f32 c[UI_AXIS_COUNT]; - } margin; - ui_layout_align align; - -} ui_layout; - -typedef enum ui_size_kind -{ - UI_SIZE_TEXT, - UI_SIZE_PIXELS, - UI_SIZE_CHILDREN, - UI_SIZE_PARENT, - UI_SIZE_PARENT_MINUS_PIXELS, - -} ui_size_kind; - -typedef struct ui_size -{ - ui_size_kind kind; - f32 value; - f32 relax; -} ui_size; - -typedef union ui_box_size -{ - struct - { - ui_size width; - ui_size height; - }; - ui_size c[UI_AXIS_COUNT]; -} ui_box_size; - -typedef union ui_box_floating -{ - struct - { - bool x; - bool y; - }; - bool c[UI_AXIS_COUNT]; -} ui_box_floating; - -//NOTE: flags for axis-dependent properties (e.g. UI_STYLE_FLOAT_X/Y) need to be consecutive bits -// in order to play well with axis agnostic functions -typedef u64 ui_style_mask; -enum -{ - UI_STYLE_NONE = 0, - UI_STYLE_SIZE_WIDTH = 1<<1, - UI_STYLE_SIZE_HEIGHT = 1<<2, - UI_STYLE_LAYOUT_AXIS = 1<<3, - UI_STYLE_LAYOUT_ALIGN_X = 1<<4, - UI_STYLE_LAYOUT_ALIGN_Y = 1<<5, - UI_STYLE_LAYOUT_SPACING = 1<<6, - UI_STYLE_LAYOUT_MARGIN_X = 1<<7, - UI_STYLE_LAYOUT_MARGIN_Y = 1<<8, - UI_STYLE_FLOAT_X = 1<<9, - UI_STYLE_FLOAT_Y = 1<<10, - UI_STYLE_COLOR = 1<<11, - UI_STYLE_BG_COLOR = 1<<12, - UI_STYLE_BORDER_COLOR = 1<<13, - UI_STYLE_BORDER_SIZE = 1<<14, - UI_STYLE_ROUNDNESS = 1<<15, - UI_STYLE_FONT = 1<<16, - UI_STYLE_FONT_SIZE = 1<<17, - UI_STYLE_ANIMATION_TIME = 1<<18, - UI_STYLE_ANIMATION_MASK = 1<<19, - - //masks - UI_STYLE_SIZE = UI_STYLE_SIZE_WIDTH - | UI_STYLE_SIZE_HEIGHT, - - UI_STYLE_LAYOUT_MARGINS = UI_STYLE_LAYOUT_MARGIN_X - | UI_STYLE_LAYOUT_MARGIN_Y, - - UI_STYLE_LAYOUT = UI_STYLE_LAYOUT_AXIS - | UI_STYLE_LAYOUT_ALIGN_X - | UI_STYLE_LAYOUT_ALIGN_Y - | UI_STYLE_LAYOUT_SPACING - | UI_STYLE_LAYOUT_MARGIN_X - | UI_STYLE_LAYOUT_MARGIN_Y, - - UI_STYLE_FLOAT = UI_STYLE_FLOAT_X - | UI_STYLE_FLOAT_Y, - - UI_STYLE_MASK_INHERITED = UI_STYLE_COLOR - | UI_STYLE_FONT - | UI_STYLE_FONT_SIZE - | UI_STYLE_ANIMATION_TIME - | UI_STYLE_ANIMATION_MASK, -}; - -typedef struct ui_style -{ - ui_box_size size; - ui_layout layout; - ui_box_floating floating; - vec2 floatTarget; - mg_color color; - mg_color bgColor; - mg_color borderColor; - mg_font font; - f32 fontSize; - f32 borderSize; - f32 roundness; - f32 animationTime; - ui_style_mask animationMask; -} ui_style; - -typedef struct ui_tag { u64 hash; } ui_tag; - -typedef enum -{ - UI_SEL_ANY, - UI_SEL_OWNER, - UI_SEL_TEXT, - UI_SEL_TAG, - UI_SEL_STATUS, - UI_SEL_KEY, - //... -} ui_selector_kind; - -typedef u8 ui_status; -enum -{ - UI_NONE = 0, - UI_HOVER = 1<<1, - UI_ACTIVE = 1<<2, - UI_DRAGGING = 1<<3, -}; - -typedef enum -{ - UI_SEL_DESCENDANT = 0, - UI_SEL_AND = 1, - //... -} ui_selector_op; - -typedef struct ui_selector -{ - list_elt listElt; - ui_selector_kind kind; - ui_selector_op op; - union - { - str8 text; - ui_key key; - ui_tag tag; - ui_status status; - //... - }; -} ui_selector; - -typedef struct ui_pattern { list_info l; } ui_pattern; - -typedef struct ui_box ui_box; - -typedef struct ui_style_rule -{ - list_elt boxElt; - list_elt buildElt; - list_elt tmpElt; - - ui_box* owner; - ui_pattern pattern; - ui_style_mask mask; - ui_style* style; -} ui_style_rule; - -typedef struct ui_sig -{ - ui_box* box; - - vec2 mouse; - vec2 delta; - vec2 wheel; - - bool pressed; - bool released; - bool clicked; - bool doubleClicked; - bool rightPressed; - - bool dragging; - bool hovering; - -} ui_sig; - -typedef void(*ui_box_draw_proc)(ui_box* box, void* data); - -typedef enum -{ - UI_FLAG_CLICKABLE = (1<<0), - UI_FLAG_SCROLL_WHEEL_X = (1<<1), - UI_FLAG_SCROLL_WHEEL_Y = (1<<2), - UI_FLAG_BLOCK_MOUSE = (1<<3), - UI_FLAG_HOT_ANIMATION = (1<<4), - UI_FLAG_ACTIVE_ANIMATION = (1<<5), - //WARN: these two following flags need to be kept as consecutive bits to - // play well with axis-agnostic functions - UI_FLAG_ALLOW_OVERFLOW_X = (1<<6), - UI_FLAG_ALLOW_OVERFLOW_Y = (1<<7), - UI_FLAG_CLIP = (1<<8), - UI_FLAG_DRAW_BACKGROUND = (1<<9), - UI_FLAG_DRAW_FOREGROUND = (1<<10), - UI_FLAG_DRAW_BORDER = (1<<11), - UI_FLAG_DRAW_TEXT = (1<<12), - UI_FLAG_DRAW_PROC = (1<<13), - - UI_FLAG_OVERLAY = (1<<14), -} ui_flags; - -struct ui_box -{ - // hierarchy - list_elt listElt; - list_info children; - ui_box* parent; - - list_elt overlayElt; - - // keying and caching - list_elt bucketElt; - ui_key key; - u64 frameCounter; - - // builder-provided info - ui_flags flags; - str8 string; - list_info tags; - - ui_box_draw_proc drawProc; - void* drawData; - - // styling - list_info beforeRules; - list_info afterRules; - - //ui_style_tag tag; - ui_style* targetStyle; - ui_style style; - u32 z; - - vec2 floatPos; - f32 childrenSum[2]; - f32 spacing[2]; - mp_rect rect; - - // signals - ui_sig* sig; - - // stateful behaviour - bool fresh; - bool closed; - bool parentClosed; - bool dragging; - bool hot; - bool active; - vec2 scroll; - - // animation data - f32 hotTransition; - f32 activeTransition; -}; - -//----------------------------------------------------------------------------- -// context -//----------------------------------------------------------------------------- - -enum { UI_MAX_INPUT_CHAR_PER_FRAME = 64 }; - -typedef struct ui_input_text -{ - u8 count; - utf32 codePoints[UI_MAX_INPUT_CHAR_PER_FRAME]; - -} ui_input_text; - -typedef struct ui_stack_elt ui_stack_elt; -struct ui_stack_elt -{ - ui_stack_elt* parent; - union - { - ui_box* box; - ui_size size; - mp_rect clip; - }; -}; - -typedef struct ui_tag_elt -{ - list_elt listElt; - ui_tag tag; -} ui_tag_elt; - -enum { UI_BOX_MAP_BUCKET_COUNT = 1024 }; - -typedef struct ui_context -{ - bool init; - - mp_input_state input; - - u64 frameCounter; - f64 frameTime; - f64 lastFrameDuration; - - mem_arena frameArena; - mem_pool boxPool; - list_info boxMap[UI_BOX_MAP_BUCKET_COUNT]; - - ui_box* root; - ui_box* overlay; - list_info overlayList; - ui_stack_elt* boxStack; - ui_stack_elt* clipStack; - - list_info nextBoxBeforeRules; - list_info nextBoxAfterRules; - list_info nextBoxTags; - - u32 z; - ui_box* hovered; - - ui_box* focus; - i32 editCursor; - i32 editMark; - i32 editFirstDisplayedChar; - f64 editCursorBlinkStart; - -} ui_context; - -//------------------------------------------------------------------------------------- -// UI context initialization and frame cycle -//------------------------------------------------------------------------------------- -MP_API void ui_init(ui_context* context); -MP_API ui_context* ui_get_context(void); -MP_API void ui_set_context(ui_context* context); - -MP_API void ui_process_event(mp_event* event); -MP_API void ui_begin_frame(vec2 size, ui_style* defaultStyle, ui_style_mask mask); -MP_API void ui_end_frame(void); -MP_API void ui_draw(void); - -#define ui_frame(size, style, mask) defer_loop(ui_begin_frame((size), (style), (mask)), ui_end_frame()) - -//------------------------------------------------------------------------------------- -// Box keys -//------------------------------------------------------------------------------------- -MP_API ui_key ui_key_make_str8(str8 string); -MP_API ui_key ui_key_make_path(str8_list path); - -MP_API ui_box* ui_box_lookup_key(ui_key key); -MP_API ui_box* ui_box_lookup_str8(str8 string); - -// C-string helper -#define ui_key_make(s) ui_key_make_str8(STR8(s)) -#define ui_box_lookup(s) ui_box_lookup_str8(STR8(s)) - -//------------------------------------------------------------------------------------- -// Box hierarchy building -//------------------------------------------------------------------------------------- -MP_API ui_box* ui_box_make_str8(str8 string, ui_flags flags); -MP_API ui_box* ui_box_begin_str8(str8 string, ui_flags flags); - -MP_API ui_box* ui_box_end(void); -#define ui_container(name, flags) defer_loop(ui_box_begin(name, flags), ui_box_end()) -#define ui_container_str8(name, flags) defer_loop(ui_box_begin_str8(name, flags), ui_box_end()) - -MP_API void ui_box_push(ui_box* box); -MP_API void ui_box_pop(void); -MP_API ui_box* ui_box_top(void); - -MP_API void ui_box_set_draw_proc(ui_box* box, ui_box_draw_proc proc, void* data); - -// C-string helpers -#define ui_box_lookup(s) ui_box_lookup_str8(STR8(s)) -#define ui_box_make(s, flags) ui_box_make_str8(STR8(s), flags) -#define ui_box_begin(s, flags) ui_box_begin_str8(STR8(s), flags) - -//------------------------------------------------------------------------------------- -// Box status and signals -//------------------------------------------------------------------------------------- -MP_API bool ui_box_closed(ui_box* box); -MP_API void ui_box_set_closed(ui_box* box, bool closed); - -MP_API bool ui_box_active(ui_box* box); -MP_API void ui_box_activate(ui_box* box); -MP_API void ui_box_deactivate(ui_box* box); - -MP_API bool ui_box_hot(ui_box* box); -MP_API void ui_box_set_hot(ui_box* box, bool hot); - -MP_API ui_sig ui_box_sig(ui_box* box); - -//------------------------------------------------------------------------------------- -// Tagging -//------------------------------------------------------------------------------------- -MP_API ui_tag ui_tag_make_str8(str8 string); -MP_API void ui_tag_box_str8(ui_box* box, str8 string); -MP_API void ui_tag_next_str8(str8 string); - -// C-string helpers -#define ui_tag_make(s) ui_tag_make_str8(STR8(s)) -#define ui_tag_box(b, s) ui_tag_box_str8(b, STR8(s)) -#define ui_tag_next(s) ui_tag_next_str8(STR8(s)) - -//------------------------------------------------------------------------------------- -// Styling -//------------------------------------------------------------------------------------- -//NOTE: styling API -//WARN: You can use a pattern in multiple rules, but be aware that a pattern is references an underlying list of selectors, -// hence pushing to a pattern also modifies rules in which the pattern was previously used! -MP_API void ui_apply_style_with_mask(ui_style* dst, ui_style* src, ui_style_mask mask); - -MP_API void ui_pattern_push(mem_arena* arena, ui_pattern* pattern, ui_selector selector); -MP_API ui_pattern ui_pattern_all(void); -MP_API ui_pattern ui_pattern_owner(void); - -MP_API void ui_style_next(ui_style* style, ui_style_mask mask); -MP_API void ui_style_match_before(ui_pattern pattern, ui_style* style, ui_style_mask mask); -MP_API void ui_style_match_after(ui_pattern pattern, ui_style* style, ui_style_mask mask); - -//------------------------------------------------------------------------- -// Basic widget helpers -//------------------------------------------------------------------------- -enum { - UI_STYLE_TAG_USER_MAX = 1<<16, - UI_STYLE_TAG_LABEL, - UI_STYLE_TAG_BUTTON, - UI_STYLE_TAG_SCROLLBAR, - UI_STYLE_TAG_PANEL, - UI_STYLE_TAG_TOOLTIP, - UI_STYLE_TAG_MENU -}; - -MP_API ui_sig ui_label(const char* label); -MP_API ui_sig ui_label_str8(str8 label); - -MP_API ui_sig ui_button(const char* label); -MP_API ui_sig ui_checkbox(const char* name, bool* checked); -MP_API ui_box* ui_slider(const char* label, f32 thumbRatio, f32* scrollValue); - -MP_API void ui_panel_begin(const char* name, ui_flags flags); -MP_API void ui_panel_end(void); -#define ui_panel(s, f) defer_loop(ui_panel_begin(s, f), ui_panel_end()) - -MP_API ui_sig ui_tooltip_begin(const char* name); -MP_API void ui_tooltip_end(void); -#define ui_tooltip(name) defer_loop(ui_tooltip_begin(name), ui_tooltip_end()) - -MP_API void ui_menu_bar_begin(const char* label); -MP_API void ui_menu_bar_end(void); -#define ui_menu_bar(name) defer_loop(ui_menu_bar_begin(name), ui_menu_bar_end()) - -MP_API void ui_menu_begin(const char* label); -MP_API void ui_menu_end(void); -#define ui_menu(name) defer_loop(ui_menu_begin(name), ui_menu_end()) - -MP_API ui_sig ui_menu_button(const char* name); - -typedef struct ui_text_box_result -{ - bool changed; - bool accepted; - str8 text; - -}ui_text_box_result; - -MP_API ui_text_box_result ui_text_box(const char* name, mem_arena* arena, str8 text); - - -typedef struct ui_select_popup_info -{ - bool changed; - int selectedIndex; - int optionCount; - str8* options; -} ui_select_popup_info; - -MP_API ui_select_popup_info ui_select_popup(const char* name, ui_select_popup_info* info); - -#ifdef __cplusplus -} // extern "C" -#endif - -#endif //__UI_H_ +/************************************************************//** +* +* @file: ui.h +* @author: Martin Fouilleul +* @date: 08/08/2022 +* @revision: +* +*****************************************************************/ +#ifndef __UI_H_ +#define __UI_H_ + +#include"util/typedefs.h" +#include"util/lists.h" +#include"input_state.h" +#include"graphics/graphics.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ui_key +{ + u64 hash; +} ui_key; + +typedef enum +{ + UI_AXIS_X, + UI_AXIS_Y, + UI_AXIS_COUNT +} ui_axis; + +typedef enum +{ + UI_ALIGN_START, + UI_ALIGN_END, + UI_ALIGN_CENTER, +} ui_align; + +typedef union ui_layout_align +{ + struct + { + ui_align x; + ui_align y; + }; + ui_align c[UI_AXIS_COUNT]; +} ui_layout_align; + +typedef struct ui_layout +{ + ui_axis axis; + f32 spacing; + union + { + struct + { + f32 x; + f32 y; + }; + f32 c[UI_AXIS_COUNT]; + } margin; + ui_layout_align align; + +} ui_layout; + +typedef enum ui_size_kind +{ + UI_SIZE_TEXT, + UI_SIZE_PIXELS, + UI_SIZE_CHILDREN, + UI_SIZE_PARENT, + UI_SIZE_PARENT_MINUS_PIXELS, + +} ui_size_kind; + +typedef struct ui_size +{ + ui_size_kind kind; + f32 value; + f32 relax; +} ui_size; + +typedef union ui_box_size +{ + struct + { + ui_size width; + ui_size height; + }; + ui_size c[UI_AXIS_COUNT]; +} ui_box_size; + +typedef union ui_box_floating +{ + struct + { + bool x; + bool y; + }; + bool c[UI_AXIS_COUNT]; +} ui_box_floating; + +//NOTE: flags for axis-dependent properties (e.g. UI_STYLE_FLOAT_X/Y) need to be consecutive bits +// in order to play well with axis agnostic functions +typedef u64 ui_style_mask; +enum +{ + UI_STYLE_NONE = 0, + UI_STYLE_SIZE_WIDTH = 1<<1, + UI_STYLE_SIZE_HEIGHT = 1<<2, + UI_STYLE_LAYOUT_AXIS = 1<<3, + UI_STYLE_LAYOUT_ALIGN_X = 1<<4, + UI_STYLE_LAYOUT_ALIGN_Y = 1<<5, + UI_STYLE_LAYOUT_SPACING = 1<<6, + UI_STYLE_LAYOUT_MARGIN_X = 1<<7, + UI_STYLE_LAYOUT_MARGIN_Y = 1<<8, + UI_STYLE_FLOAT_X = 1<<9, + UI_STYLE_FLOAT_Y = 1<<10, + UI_STYLE_COLOR = 1<<11, + UI_STYLE_BG_COLOR = 1<<12, + UI_STYLE_BORDER_COLOR = 1<<13, + UI_STYLE_BORDER_SIZE = 1<<14, + UI_STYLE_ROUNDNESS = 1<<15, + UI_STYLE_FONT = 1<<16, + UI_STYLE_FONT_SIZE = 1<<17, + UI_STYLE_ANIMATION_TIME = 1<<18, + UI_STYLE_ANIMATION_MASK = 1<<19, + + //masks + UI_STYLE_SIZE = UI_STYLE_SIZE_WIDTH + | UI_STYLE_SIZE_HEIGHT, + + UI_STYLE_LAYOUT_MARGINS = UI_STYLE_LAYOUT_MARGIN_X + | UI_STYLE_LAYOUT_MARGIN_Y, + + UI_STYLE_LAYOUT = UI_STYLE_LAYOUT_AXIS + | UI_STYLE_LAYOUT_ALIGN_X + | UI_STYLE_LAYOUT_ALIGN_Y + | UI_STYLE_LAYOUT_SPACING + | UI_STYLE_LAYOUT_MARGIN_X + | UI_STYLE_LAYOUT_MARGIN_Y, + + UI_STYLE_FLOAT = UI_STYLE_FLOAT_X + | UI_STYLE_FLOAT_Y, + + UI_STYLE_MASK_INHERITED = UI_STYLE_COLOR + | UI_STYLE_FONT + | UI_STYLE_FONT_SIZE + | UI_STYLE_ANIMATION_TIME + | UI_STYLE_ANIMATION_MASK, +}; + +typedef struct ui_style +{ + ui_box_size size; + ui_layout layout; + ui_box_floating floating; + vec2 floatTarget; + mg_color color; + mg_color bgColor; + mg_color borderColor; + mg_font font; + f32 fontSize; + f32 borderSize; + f32 roundness; + f32 animationTime; + ui_style_mask animationMask; +} ui_style; + +typedef struct ui_tag { u64 hash; } ui_tag; + +typedef enum +{ + UI_SEL_ANY, + UI_SEL_OWNER, + UI_SEL_TEXT, + UI_SEL_TAG, + UI_SEL_STATUS, + UI_SEL_KEY, + //... +} ui_selector_kind; + +typedef u8 ui_status; +enum +{ + UI_NONE = 0, + UI_HOVER = 1<<1, + UI_ACTIVE = 1<<2, + UI_DRAGGING = 1<<3, +}; + +typedef enum +{ + UI_SEL_DESCENDANT = 0, + UI_SEL_AND = 1, + //... +} ui_selector_op; + +typedef struct ui_selector +{ + list_elt listElt; + ui_selector_kind kind; + ui_selector_op op; + union + { + str8 text; + ui_key key; + ui_tag tag; + ui_status status; + //... + }; +} ui_selector; + +typedef struct ui_pattern { list_info l; } ui_pattern; + +typedef struct ui_box ui_box; + +typedef struct ui_style_rule +{ + list_elt boxElt; + list_elt buildElt; + list_elt tmpElt; + + ui_box* owner; + ui_pattern pattern; + ui_style_mask mask; + ui_style* style; +} ui_style_rule; + +typedef struct ui_sig +{ + ui_box* box; + + vec2 mouse; + vec2 delta; + vec2 wheel; + + bool pressed; + bool released; + bool clicked; + bool doubleClicked; + bool rightPressed; + + bool dragging; + bool hovering; + +} ui_sig; + +typedef void(*ui_box_draw_proc)(ui_box* box, void* data); + +typedef enum +{ + UI_FLAG_CLICKABLE = (1<<0), + UI_FLAG_SCROLL_WHEEL_X = (1<<1), + UI_FLAG_SCROLL_WHEEL_Y = (1<<2), + UI_FLAG_BLOCK_MOUSE = (1<<3), + UI_FLAG_HOT_ANIMATION = (1<<4), + UI_FLAG_ACTIVE_ANIMATION = (1<<5), + //WARN: these two following flags need to be kept as consecutive bits to + // play well with axis-agnostic functions + UI_FLAG_ALLOW_OVERFLOW_X = (1<<6), + UI_FLAG_ALLOW_OVERFLOW_Y = (1<<7), + UI_FLAG_CLIP = (1<<8), + UI_FLAG_DRAW_BACKGROUND = (1<<9), + UI_FLAG_DRAW_FOREGROUND = (1<<10), + UI_FLAG_DRAW_BORDER = (1<<11), + UI_FLAG_DRAW_TEXT = (1<<12), + UI_FLAG_DRAW_PROC = (1<<13), + + UI_FLAG_OVERLAY = (1<<14), +} ui_flags; + +struct ui_box +{ + // hierarchy + list_elt listElt; + list_info children; + ui_box* parent; + + list_elt overlayElt; + + // keying and caching + list_elt bucketElt; + ui_key key; + u64 frameCounter; + + // builder-provided info + ui_flags flags; + str8 string; + list_info tags; + + ui_box_draw_proc drawProc; + void* drawData; + + // styling + list_info beforeRules; + list_info afterRules; + + //ui_style_tag tag; + ui_style* targetStyle; + ui_style style; + u32 z; + + vec2 floatPos; + f32 childrenSum[2]; + f32 spacing[2]; + mp_rect rect; + + // signals + ui_sig* sig; + + // stateful behaviour + bool fresh; + bool closed; + bool parentClosed; + bool dragging; + bool hot; + bool active; + vec2 scroll; + + // animation data + f32 hotTransition; + f32 activeTransition; +}; + +//----------------------------------------------------------------------------- +// context +//----------------------------------------------------------------------------- + +enum { UI_MAX_INPUT_CHAR_PER_FRAME = 64 }; + +typedef struct ui_input_text +{ + u8 count; + utf32 codePoints[UI_MAX_INPUT_CHAR_PER_FRAME]; + +} ui_input_text; + +typedef struct ui_stack_elt ui_stack_elt; +struct ui_stack_elt +{ + ui_stack_elt* parent; + union + { + ui_box* box; + ui_size size; + mp_rect clip; + }; +}; + +typedef struct ui_tag_elt +{ + list_elt listElt; + ui_tag tag; +} ui_tag_elt; + +enum { UI_BOX_MAP_BUCKET_COUNT = 1024 }; + +typedef struct ui_context +{ + bool init; + + mp_input_state input; + + u64 frameCounter; + f64 frameTime; + f64 lastFrameDuration; + + mem_arena frameArena; + mem_pool boxPool; + list_info boxMap[UI_BOX_MAP_BUCKET_COUNT]; + + ui_box* root; + ui_box* overlay; + list_info overlayList; + ui_stack_elt* boxStack; + ui_stack_elt* clipStack; + + list_info nextBoxBeforeRules; + list_info nextBoxAfterRules; + list_info nextBoxTags; + + u32 z; + ui_box* hovered; + + ui_box* focus; + i32 editCursor; + i32 editMark; + i32 editFirstDisplayedChar; + f64 editCursorBlinkStart; + +} ui_context; + +//------------------------------------------------------------------------------------- +// UI context initialization and frame cycle +//------------------------------------------------------------------------------------- +MP_API void ui_init(ui_context* context); +MP_API ui_context* ui_get_context(void); +MP_API void ui_set_context(ui_context* context); + +MP_API void ui_process_event(mp_event* event); +MP_API void ui_begin_frame(vec2 size, ui_style* defaultStyle, ui_style_mask mask); +MP_API void ui_end_frame(void); +MP_API void ui_draw(void); + +#define ui_frame(size, style, mask) defer_loop(ui_begin_frame((size), (style), (mask)), ui_end_frame()) + +//------------------------------------------------------------------------------------- +// Box keys +//------------------------------------------------------------------------------------- +MP_API ui_key ui_key_make_str8(str8 string); +MP_API ui_key ui_key_make_path(str8_list path); + +MP_API ui_box* ui_box_lookup_key(ui_key key); +MP_API ui_box* ui_box_lookup_str8(str8 string); + +// C-string helper +#define ui_key_make(s) ui_key_make_str8(STR8(s)) +#define ui_box_lookup(s) ui_box_lookup_str8(STR8(s)) + +//------------------------------------------------------------------------------------- +// Box hierarchy building +//------------------------------------------------------------------------------------- +MP_API ui_box* ui_box_make_str8(str8 string, ui_flags flags); +MP_API ui_box* ui_box_begin_str8(str8 string, ui_flags flags); + +MP_API ui_box* ui_box_end(void); +#define ui_container(name, flags) defer_loop(ui_box_begin(name, flags), ui_box_end()) +#define ui_container_str8(name, flags) defer_loop(ui_box_begin_str8(name, flags), ui_box_end()) + +MP_API void ui_box_push(ui_box* box); +MP_API void ui_box_pop(void); +MP_API ui_box* ui_box_top(void); + +MP_API void ui_box_set_draw_proc(ui_box* box, ui_box_draw_proc proc, void* data); + +// C-string helpers +#define ui_box_lookup(s) ui_box_lookup_str8(STR8(s)) +#define ui_box_make(s, flags) ui_box_make_str8(STR8(s), flags) +#define ui_box_begin(s, flags) ui_box_begin_str8(STR8(s), flags) + +//------------------------------------------------------------------------------------- +// Box status and signals +//------------------------------------------------------------------------------------- +MP_API bool ui_box_closed(ui_box* box); +MP_API void ui_box_set_closed(ui_box* box, bool closed); + +MP_API bool ui_box_active(ui_box* box); +MP_API void ui_box_activate(ui_box* box); +MP_API void ui_box_deactivate(ui_box* box); + +MP_API bool ui_box_hot(ui_box* box); +MP_API void ui_box_set_hot(ui_box* box, bool hot); + +MP_API ui_sig ui_box_sig(ui_box* box); + +//------------------------------------------------------------------------------------- +// Tagging +//------------------------------------------------------------------------------------- +MP_API ui_tag ui_tag_make_str8(str8 string); +MP_API void ui_tag_box_str8(ui_box* box, str8 string); +MP_API void ui_tag_next_str8(str8 string); + +// C-string helpers +#define ui_tag_make(s) ui_tag_make_str8(STR8(s)) +#define ui_tag_box(b, s) ui_tag_box_str8(b, STR8(s)) +#define ui_tag_next(s) ui_tag_next_str8(STR8(s)) + +//------------------------------------------------------------------------------------- +// Styling +//------------------------------------------------------------------------------------- +//NOTE: styling API +//WARN: You can use a pattern in multiple rules, but be aware that a pattern is references an underlying list of selectors, +// hence pushing to a pattern also modifies rules in which the pattern was previously used! +MP_API void ui_apply_style_with_mask(ui_style* dst, ui_style* src, ui_style_mask mask); + +MP_API void ui_pattern_push(mem_arena* arena, ui_pattern* pattern, ui_selector selector); +MP_API ui_pattern ui_pattern_all(void); +MP_API ui_pattern ui_pattern_owner(void); + +MP_API void ui_style_next(ui_style* style, ui_style_mask mask); +MP_API void ui_style_match_before(ui_pattern pattern, ui_style* style, ui_style_mask mask); +MP_API void ui_style_match_after(ui_pattern pattern, ui_style* style, ui_style_mask mask); + +//------------------------------------------------------------------------- +// Basic widget helpers +//------------------------------------------------------------------------- +enum { + UI_STYLE_TAG_USER_MAX = 1<<16, + UI_STYLE_TAG_LABEL, + UI_STYLE_TAG_BUTTON, + UI_STYLE_TAG_SCROLLBAR, + UI_STYLE_TAG_PANEL, + UI_STYLE_TAG_TOOLTIP, + UI_STYLE_TAG_MENU +}; + +MP_API ui_sig ui_label(const char* label); +MP_API ui_sig ui_label_str8(str8 label); + +MP_API ui_sig ui_button(const char* label); +MP_API ui_sig ui_checkbox(const char* name, bool* checked); +MP_API ui_box* ui_slider(const char* label, f32 thumbRatio, f32* scrollValue); + +MP_API void ui_panel_begin(const char* name, ui_flags flags); +MP_API void ui_panel_end(void); +#define ui_panel(s, f) defer_loop(ui_panel_begin(s, f), ui_panel_end()) + +MP_API ui_sig ui_tooltip_begin(const char* name); +MP_API void ui_tooltip_end(void); +#define ui_tooltip(name) defer_loop(ui_tooltip_begin(name), ui_tooltip_end()) + +MP_API void ui_menu_bar_begin(const char* label); +MP_API void ui_menu_bar_end(void); +#define ui_menu_bar(name) defer_loop(ui_menu_bar_begin(name), ui_menu_bar_end()) + +MP_API void ui_menu_begin(const char* label); +MP_API void ui_menu_end(void); +#define ui_menu(name) defer_loop(ui_menu_begin(name), ui_menu_end()) + +MP_API ui_sig ui_menu_button(const char* name); + +typedef struct ui_text_box_result +{ + bool changed; + bool accepted; + str8 text; + +}ui_text_box_result; + +MP_API ui_text_box_result ui_text_box(const char* name, mem_arena* arena, str8 text); + + +typedef struct ui_select_popup_info +{ + bool changed; + int selectedIndex; + int optionCount; + str8* options; +} ui_select_popup_info; + +MP_API ui_select_popup_info ui_select_popup(const char* name, ui_select_popup_info* info); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif //__UI_H_ diff --git a/samples/glesTriangle/src/main.c b/samples/glesTriangle/src/main.c index c0b0dd6..9f6736e 100644 --- a/samples/glesTriangle/src/main.c +++ b/samples/glesTriangle/src/main.c @@ -1,5 +1,4 @@ #include -#include #include #include diff --git a/samples/pong/build.sh b/samples/pong/build.sh index c53ab3b..d5ad35f 100755 --- a/samples/pong/build.sh +++ b/samples/pong/build.sh @@ -11,6 +11,7 @@ else CLANG=clang fi +ORCA_DIR=../.. STDLIB_DIR=../../cstdlib ORCA_SDK_DIR=../../sdk MILEPOST_DIR=../../milepost @@ -25,6 +26,7 @@ wasmFlags="--target=wasm32 \ -mbulk-memory \ -D__ORCA__ \ -I $STDLIB_DIR/include \ + -I $ORCA_DIR/ext \ -I $ORCA_SDK_DIR \ -I $MILEPOST_DIR/ext -I $MILEPOST_DIR -I $MILEPOST_DIR/src" diff --git a/samples/pong/src/main.c b/samples/pong/src/main.c index 77ed230..6f3baee 100644 --- a/samples/pong/src/main.c +++ b/samples/pong/src/main.c @@ -1,5 +1,4 @@ #include -#include #include #include diff --git a/samples/ui/build.sh b/samples/ui/build.sh index 8001b8d..8706156 100755 --- a/samples/ui/build.sh +++ b/samples/ui/build.sh @@ -11,6 +11,8 @@ else CLANG=clang fi +ORCA_DIR=../.. + wasmFlags="--target=wasm32 \ --no-standard-libraries \ -fno-builtin \ @@ -20,6 +22,7 @@ wasmFlags="--target=wasm32 \ -O2 \ -mbulk-memory \ -D__ORCA__ \ + -I $ORCA_DIR/ext \ -isystem ../../cstdlib/include -I ../../sdk -I../../milepost/ext -I ../../milepost -I ../../milepost/src" $CLANG $wasmFlags -o ./module.wasm ../../sdk/orca.c ../../cstdlib/src/*.c src/main.c diff --git a/samples/ui/src/main.c b/samples/ui/src/main.c index 2656209..4d3d142 100644 --- a/samples/ui/src/main.c +++ b/samples/ui/src/main.c @@ -1,6 +1,4 @@ #include"keys.h" -#include"graphics.h" -#include"ui.h" #include"orca.h" diff --git a/scripts/dev.py b/scripts/dev.py index e3ffa09..17bb897 100644 --- a/scripts/dev.py +++ b/scripts/dev.py @@ -168,7 +168,7 @@ def build_milepost_lib_mac(release): # TODO: shaderFlagParam "-fno-fast-math", "-c", "-o", "../build/mtl_renderer.air", - "src/mtl_renderer.metal", + "src/graphics/mtl_renderer.metal", ], check=True) subprocess.run([ "xcrun", "-sdk", "macosx", "metallib", @@ -302,7 +302,8 @@ def build_orca_win(release): includes = [ "/I", "src", "/I", "sdk", - "/I", "ext" + "/I", "ext", + "/I", "ext/angle/include", "/I", "ext/wasm3/source", "/I", "milepost/src" ] @@ -332,6 +333,7 @@ def build_orca_mac(release): "-Imilepost/src/util", "-Imilepost/src/platform", "-Iext", + "-Iext/angle/include", "-Iext/wasm3/source" ] libs = ["-Lbuild/bin", "-Lbuild/lib", "-lmilepost", "-lwasm3"] @@ -380,12 +382,12 @@ def gen_all_bindings(): bindgen("canvas", "src/canvas_api.json", guest_stubs="sdk/orca_surface.c", - guest_include="graphics.h", + guest_include="graphics/graphics.h", wasm3_bindings="src/canvas_api_bind_gen.c", ) bindgen("clock", "src/clock_api.json", guest_stubs="sdk/orca_clock.c", - guest_include="platform_clock.h", + guest_include="platform/platform_clock.h", wasm3_bindings="src/clock_api_bind_gen.c", ) bindgen("io", "src/io_api.json", diff --git a/sdk/orca.c b/sdk/orca.c index c92ab81..747e565 100644 --- a/sdk/orca.c +++ b/sdk/orca.c @@ -18,10 +18,10 @@ #include"util/strings.c" #include"util/utf8.c" -#include"graphics_common.c" -#include"input_state.c" +#include"graphics/graphics_common.c" +#include"ui/input_state.c" +#include"ui/ui.c" + #include"orca_exports.c" #include"orca_surface.c" -#include"ui.c" - #include"io_stubs.c" diff --git a/sdk/orca.h b/sdk/orca.h index 2c96e5b..3c00108 100644 --- a/sdk/orca.h +++ b/sdk/orca.h @@ -21,7 +21,10 @@ #include"math.h" -#include"graphics.h" +#include"graphics/graphics.h" +#include"ui/input_state.h" +#include"ui/ui.h" + #include"gl31.h" #if COMPILER_CLANG diff --git a/src/main.c b/src/main.c index a90d6c9..b066368 100644 --- a/src/main.c +++ b/src/main.c @@ -11,7 +11,7 @@ #define MG_INCLUDE_GL_API #include"milepost.h" -#include"graphics_common.h" +#include"graphics/graphics_common.h" #include"orca_app.h"