├── .gitignore ├── CMakeLists.txt ├── README.md ├── build.sh ├── eva.h ├── eva_macos.m ├── eva_windows.c ├── main.c └── run.sh /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .vscode/ 3 | .clangd/ 4 | .DS_Store 5 | compile_commands.json 6 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | 3 | project(eva) 4 | 5 | if (CMAKE_SYSTEM_NAME STREQUAL Darwin) 6 | add_executable(eva main.c eva_macos.m) 7 | target_compile_definitions(eva PRIVATE EVA_MACOS) 8 | target_link_libraries(eva "-framework Cocoa -framework Metal -framework MetalKit") 9 | target_compile_options(eva PRIVATE -g) 10 | elseif(CMAKE_SYSTEM_NAME STREQUAL Windows) 11 | add_executable(eva WIN32 main.c eva.h eva_windows.c) 12 | target_compile_definitions(eva PRIVATE EVA_WINDOWS) 13 | endif() 14 | 15 | 16 | # add_custom_target( 17 | # copy-compile-commands ALL 18 | # ${CMAKE_COMMAND} -E copy_if_different 19 | # ${CMAKE_BINARY_DIR}/compile_commands.json 20 | # ${CMAKE_CURRENT_LIST_DIR}) 21 | # set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PROJECT STATE: Suspended 2 | 3 | # eva 4 | Eva is a simple cross-platform C99 library for creating event-driven applications. 5 | 6 | ## How it works 7 | 8 | Eva provides a framebuffer to draw into intended for use with Event Driven Applications that use a software rendered visuals. 9 | 10 | ## Platforms 11 | 12 | Currently only MacOS and Windows are supported. 13 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cmake --build build 3 | -------------------------------------------------------------------------------- /eva.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /** 4 | * Eva is an library for creating cross-platform event driven applications. 5 | * It provides a framebuffer for an application to render into. 6 | * All windows created by eva are high-dpi when possible. 7 | * 8 | * Eva takes snippets of inspiration and code from the following libraries: 9 | * - https://github.com/floooh/sokol/blob/master/sokol_app.h 10 | * - https://github.com/emoon/minifb 11 | * - https://www.glfw.org 12 | */ 13 | 14 | #include 15 | #include 16 | 17 | typedef struct eva_pixel { 18 | uint8_t b, g, r, a; 19 | } eva_pixel; 20 | 21 | typedef struct eva_framebuffer { 22 | uint32_t w, h; 23 | 24 | uint32_t pitch; // Max width 25 | uint32_t max_height; // Max height 26 | 27 | /** 28 | * @brief The dpi scale for high-dpi displays. 29 | * 30 | * e.g. On a typical retina display the window reports a resolution of 31 | * 1440x900 but that actual framebuffer resolution is 2880x1800. In this 32 | * case the scale will be x=2.0f and y=2.0f. 33 | */ 34 | float scale_x, scale_y; 35 | 36 | eva_pixel *pixels; 37 | } eva_framebuffer; 38 | 39 | /** 40 | * @brief Identifiers for individual mouse buttons. 41 | * 42 | * @see @ref eva_mouse_btn_fn 43 | * 44 | * @ingroup input 45 | */ 46 | typedef enum eva_mouse_btn { 47 | EVA_MOUSE_BTN_LEFT, 48 | EVA_MOUSE_BTN_RIGHT, 49 | EVA_MOUSE_BTN_MIDDLE, 50 | } eva_mouse_btn; 51 | 52 | /** 53 | * @brief Identifiers for individual keyboard keys. 54 | * 55 | * Taken from GLFW. 56 | * 57 | * @see @ref eva_key_fn 58 | * 59 | * @ingroup input 60 | */ 61 | typedef enum eva_key { 62 | /* The unknown key */ 63 | EVA_KEY_UNKNOWN = -1, 64 | 65 | /* Printable keys */ 66 | EVA_KEY_SPACE = 32, 67 | EVA_KEY_APOSTROPHE = 39, /* ' */ 68 | EVA_KEY_COMMA = 44, /* , */ 69 | EVA_KEY_MINUS = 45, /* - */ 70 | EVA_KEY_PERIOD = 46, /* . */ 71 | EVA_KEY_SLASH = 47, /* / */ 72 | EVA_KEY_0 = 48, 73 | EVA_KEY_1 = 49, 74 | EVA_KEY_2 = 50, 75 | EVA_KEY_3 = 51, 76 | EVA_KEY_4 = 52, 77 | EVA_KEY_5 = 53, 78 | EVA_KEY_6 = 54, 79 | EVA_KEY_7 = 55, 80 | EVA_KEY_8 = 56, 81 | EVA_KEY_9 = 57, 82 | EVA_KEY_SEMICOLON = 59, /* ; */ 83 | EVA_KEY_EQUAL = 61, /* = */ 84 | EVA_KEY_A = 65, 85 | EVA_KEY_B = 66, 86 | EVA_KEY_C = 67, 87 | EVA_KEY_D = 68, 88 | EVA_KEY_E = 69, 89 | EVA_KEY_F = 70, 90 | EVA_KEY_G = 71, 91 | EVA_KEY_H = 72, 92 | EVA_KEY_I = 73, 93 | EVA_KEY_J = 74, 94 | EVA_KEY_K = 75, 95 | EVA_KEY_L = 76, 96 | EVA_KEY_M = 77, 97 | EVA_KEY_N = 78, 98 | EVA_KEY_O = 79, 99 | EVA_KEY_P = 80, 100 | EVA_KEY_Q = 81, 101 | EVA_KEY_R = 82, 102 | EVA_KEY_S = 83, 103 | EVA_KEY_T = 84, 104 | EVA_KEY_U = 85, 105 | EVA_KEY_V = 86, 106 | EVA_KEY_W = 87, 107 | EVA_KEY_X = 88, 108 | EVA_KEY_Y = 89, 109 | EVA_KEY_Z = 90, 110 | EVA_KEY_LEFT_BRACKET = 91, /* [ */ 111 | EVA_KEY_BACKSLASH = 92, /* \ */ 112 | EVA_KEY_RIGHT_BRACKET = 93, /* ] */ 113 | EVA_KEY_GRAVE_ACCENT = 96, /* ` */ 114 | EVA_KEY_WORLD_1 = 161, /* non-US #1 */ 115 | EVA_KEY_WORLD_2 = 162, /* non-US #2 */ 116 | 117 | /* Function keys */ 118 | EVA_KEY_ESCAPE = 256, 119 | EVA_KEY_ENTER = 257, 120 | EVA_KEY_TAB = 258, 121 | EVA_KEY_BACKSPACE = 259, 122 | EVA_KEY_INSERT = 260, 123 | EVA_KEY_DELETE = 261, 124 | EVA_KEY_RIGHT = 262, 125 | EVA_KEY_LEFT = 263, 126 | EVA_KEY_DOWN = 264, 127 | EVA_KEY_UP = 265, 128 | EVA_KEY_PAGE_UP = 266, 129 | EVA_KEY_PAGE_DOWN = 267, 130 | EVA_KEY_HOME = 268, 131 | EVA_KEY_END = 269, 132 | EVA_KEY_CAPS_LOCK = 280, 133 | EVA_KEY_SCROLL_LOCK = 281, 134 | EVA_KEY_NUM_LOCK = 282, 135 | EVA_KEY_PRINT_SCREEN = 283, 136 | EVA_KEY_PAUSE = 284, 137 | EVA_KEY_F1 = 290, 138 | EVA_KEY_F2 = 291, 139 | EVA_KEY_F3 = 292, 140 | EVA_KEY_F4 = 293, 141 | EVA_KEY_F5 = 294, 142 | EVA_KEY_F6 = 295, 143 | EVA_KEY_F7 = 296, 144 | EVA_KEY_F8 = 297, 145 | EVA_KEY_F9 = 298, 146 | EVA_KEY_F10 = 299, 147 | EVA_KEY_F11 = 300, 148 | EVA_KEY_F12 = 301, 149 | EVA_KEY_F13 = 302, 150 | EVA_KEY_F14 = 303, 151 | EVA_KEY_F15 = 304, 152 | EVA_KEY_F16 = 305, 153 | EVA_KEY_F17 = 306, 154 | EVA_KEY_F18 = 307, 155 | EVA_KEY_F19 = 308, 156 | EVA_KEY_F20 = 309, 157 | EVA_KEY_F21 = 310, 158 | EVA_KEY_F22 = 311, 159 | EVA_KEY_F23 = 312, 160 | EVA_KEY_F24 = 313, 161 | EVA_KEY_F25 = 314, 162 | EVA_KEY_KP_0 = 320, 163 | EVA_KEY_KP_1 = 321, 164 | EVA_KEY_KP_2 = 322, 165 | EVA_KEY_KP_3 = 323, 166 | EVA_KEY_KP_4 = 324, 167 | EVA_KEY_KP_5 = 325, 168 | EVA_KEY_KP_6 = 326, 169 | EVA_KEY_KP_7 = 327, 170 | EVA_KEY_KP_8 = 328, 171 | EVA_KEY_KP_9 = 329, 172 | EVA_KEY_KP_DECIMAL = 330, 173 | EVA_KEY_KP_DIVIDE = 331, 174 | EVA_KEY_KP_MULTIPLY = 332, 175 | EVA_KEY_KP_SUBTRACT = 333, 176 | EVA_KEY_KP_ADD = 334, 177 | EVA_KEY_KP_ENTER = 335, 178 | EVA_KEY_KP_EQUAL = 336, 179 | EVA_KEY_LEFT_SHIFT = 340, 180 | EVA_KEY_LEFT_CONTROL = 341, 181 | EVA_KEY_LEFT_ALT = 342, 182 | EVA_KEY_LEFT_SUPER = 343, 183 | EVA_KEY_RIGHT_SHIFT = 344, 184 | EVA_KEY_RIGHT_CONTROL = 345, 185 | EVA_KEY_RIGHT_ALT = 346, 186 | EVA_KEY_RIGHT_SUPER = 347, 187 | EVA_KEY_MENU = 348, 188 | EVA_KEY_LAST = EVA_KEY_MENU 189 | } eva_key; 190 | 191 | /** 192 | * @brief Flags for modifier keys. 193 | * 194 | * Taken from GLFW. 195 | * 196 | * @see @ref eva_key_fn 197 | * 198 | * @ingroup input 199 | */ 200 | typedef enum eva_mod_flags { 201 | EVA_MOD_SHIFT = 0x0001, 202 | EVA_MOD_CONTROL = 0x0002, 203 | EVA_MOD_ALT = 0x0004, 204 | EVA_MOD_SUPER = 0x0008, 205 | EVA_MOD_CAPS_LOCK = 0x0010, 206 | EVA_MOD_NUM_LOCK = 0x0020, 207 | } eva_mod_flags; 208 | 209 | /** 210 | * @brief Identifiers for mouse actions. 211 | * 212 | * @see @ref eva_mouse_btn_fn 213 | * 214 | * @ingroup input 215 | */ 216 | typedef enum eva_input_action { 217 | EVA_INPUT_PRESSED, 218 | EVA_INPUT_RELEASED 219 | } eva_input_action; 220 | 221 | /** 222 | * @brief The function pointer type for the initialization callback. 223 | * 224 | * This is the function pointer type for the initialization callback. It has 225 | * the following signature: 226 | * @code 227 | * void init(void); 228 | * @endcode 229 | * 230 | * See @ref eva_set_init_fn 231 | * 232 | * @ingroup initalization 233 | */ 234 | typedef void(*eva_init_fn)(void); 235 | 236 | /** 237 | * @brief The function pointer type for the cleanup callback. 238 | * 239 | * This is the function pointer type for the cleanup callback. It has 240 | * the following signature: 241 | * @code 242 | * void cleanup(void); 243 | * @endcode 244 | * 245 | * See @ref eva_set_cleanup_fn 246 | * 247 | * @ingroup shutdown 248 | */ 249 | typedef void(*eva_cleanup_fn)(void); 250 | 251 | /** 252 | * @brief The function pointer type for the cancel quit callback. 253 | * 254 | * This is the function pointer type for the cancel quit callback. It has 255 | * the following signature: 256 | * @code 257 | * bool cancel_quit(void); 258 | * @endcode 259 | * 260 | * If set this function will be called when the application wants to close. It 261 | * provides an opportunity to cancel the quit sequence. Typically this would be 262 | * used to provide the user to save unsaved work or show a quit confirmation 263 | * dialog. 264 | * 265 | * @return True to continue the quit sequence, false otherwise to cancel the 266 | * quit sequence. 267 | * 268 | * @see @ref eva_set_cancel_quit_fn 269 | * 270 | * @ingroup shutdown 271 | */ 272 | typedef bool(*eva_cancel_quit_fn)(void); 273 | 274 | typedef void(*eva_fail_fn)(int32_t error_code, const char *error_string); 275 | 276 | /** 277 | * @brief The function pointer type for frame callbacks. 278 | * 279 | * This is the function pointer type for frame callbacks. It has the following 280 | * signature: 281 | * @code 282 | * void frame(const eva_framebuffer* fb); 283 | * @endcode 284 | * 285 | * This function is called when it is time to draw to the screen. This happens 286 | * when either a window event happens (e.g. window is resized or moved between 287 | * monitors) or an event is triggered in the application (e.g. 288 | * [mouse click](@ref eva_mouse_btn_fn)) and the application requested a frame 289 | * be drawn via a call to [eva_request_frame](@ref eva_request_frame). 290 | * 291 | * see @ref eva_request_frame 292 | * 293 | * @ingroup drawing 294 | */ 295 | typedef void(*eva_frame_fn)(const eva_framebuffer* fb); 296 | 297 | /** 298 | * @brief The function pointer type for mouse moved event callbacks. 299 | * 300 | * This is the function pointer type for mouse moved event callbacks. It has 301 | * the following signature: 302 | * @code 303 | * void mouse_moved(double mouse_x, double mouse_y); 304 | * @endcode 305 | * 306 | * @param[in] x The mouse's new x position relative to the left of the window's 307 | * content area. 308 | * @param[in] y The mouse's new y position relative to the top of the window's 309 | * content area. 310 | * 311 | * @see @ref eva_set_mouse_moved_fn 312 | * 313 | * @ingroup input 314 | */ 315 | typedef void(*eva_mouse_moved_fn)(double x, double y); 316 | 317 | /** 318 | * @brief The function pointer type for mouse dragged event callbacks. 319 | * 320 | * This is the function pointer type for mouse dragged event callbacks. It has 321 | * the following signature: 322 | * @code 323 | * void mouse_dragged(double mouse_x, double mouse_y, eva_mouse_btn btn); 324 | * @endcode 325 | * 326 | * @param[in] x The mouse's new x position relative to the left of the window's 327 | * content area. 328 | * @param[in] y The mouse's new y position relative to the top of the window's 329 | * content area. 330 | * @param[in] btn The [button](/ref eva_mouse_btn) that was held during the 331 | * dragging movement. 332 | * 333 | * @see @ref eva_set_mouse_dragged_fn 334 | * 335 | * @ingroup input 336 | */ 337 | typedef void(*eva_mouse_dragged_fn)(double x, double y, eva_mouse_btn); 338 | 339 | /** 340 | * @brief The function pointer type for mouse button event callbacks. 341 | * 342 | * This is the function pointer type for mouse button event callbacks. It has 343 | * the following signature: 344 | * @code 345 | * void mouse_button(double x, double y, 346 | * eva_mouse_btn btn, eva_input_action action); 347 | * @endcode 348 | * 349 | * @param[in] x The mouse's x position relative to the left of the window's 350 | * content area at the time of the button action. 351 | * @param[in] y The mouse's y position relative to the top of the window's 352 | * content area at the time of the button action. 353 | * @param[in] btn The [button](/ref eva_mouse_btn) that triggered the action. 354 | * @param[in] action The [action](/ref eva_input_action) that occurred. 355 | * 356 | * @see @ref eva_set_mouse_moved_fn 357 | * 358 | * @ingroup input 359 | */ 360 | typedef void(*eva_mouse_btn_fn)(double x, double y, 361 | eva_mouse_btn btn, eva_input_action action); 362 | 363 | /** 364 | * @brief The function pointer type for scroll event callbacks. 365 | * 366 | * This is the function pointer type for scroll event callbacks. It has 367 | * the following signature: 368 | * @code 369 | * void scroll(double delta_x, double delta_y); 370 | * @endcode 371 | * 372 | * @param[in] delta_x The scroll offset on the x-axis. 373 | * @param[in] delta_y The scroll offset on the y-axis. 374 | * 375 | * @see @ref eva_set_scroll_fn 376 | * 377 | * @ingroup input 378 | */ 379 | typedef void(*eva_scroll_fn)(double delta_x, double delta_y); 380 | 381 | /** 382 | * @brief The function pointer type for physical key press/release event 383 | * callbacks. 384 | * 385 | * This is the function pointer type for physical key press/release event 386 | * callbacks. It has the following signature: 387 | * @code 388 | * void key_action(eva_key key, eva_input_action action); 389 | * @endcode 390 | * 391 | * @param[in] key The [key](/ref eva_key) that triggered the action. 392 | * @param[in] action The [action](/ref eva_input_action) that occurred. 393 | * @param[in] mod The [modifier keys](/ref eva_mod_flags) that were active at 394 | * the time the key action occurred. 395 | * 396 | * @see @ref eva_set_key_fn 397 | * 398 | * @ingroup input 399 | */ 400 | typedef void(*eva_key_fn)(eva_key key, eva_input_action action, 401 | eva_mod_flags mod); 402 | 403 | /** 404 | * @brief The function pointer type for the unicode text input event callback. 405 | * 406 | * This is the function pointer type for the unicode text input event callback. 407 | * It has the following signature: 408 | * @code 409 | * void text_input(const uint16_t* utf16_text, uint32_t len, eva_mod_flags mods); 410 | * @endcode 411 | * 412 | * @param[in] utf8_text The UTF16 encoded text that was input via key-presses or 413 | * via paste. 414 | * @param[in] len The length of the text in UTF16 characters. 415 | * @param[in] mod The [modifier keys](/ref eva_mod_flags) that were active at 416 | * the time the text input occurred. 417 | * 418 | * @see @ref eva_set_text_input_fn 419 | * 420 | * @ingroup input 421 | */ 422 | typedef void(*eva_text_input_fn)(const uint16_t *utf16_text, uint32_t len, 423 | eva_mod_flags mod); 424 | 425 | /** 426 | * @brief The function pointer type for the window resize callback. 427 | * 428 | * This is the function pointer type for the window resize callback. It has the 429 | * following signature: 430 | * @code 431 | * void window_resize(uint32_t framebuffer_width, uint32_t framebuffer_height); 432 | * @endcode 433 | * 434 | * @param[in] framebuffer_width The new width of the framebuffer. 435 | * @param[in] framebuffer_height The new height of the framebuffer. 436 | * 437 | * @see @ref eva_set_window_resize_fn 438 | * 439 | * @ingroup window 440 | */ 441 | typedef void(*eva_window_resize_fn)(uint32_t framebuffer_width, 442 | uint32_t framebuffer_height); 443 | 444 | /** 445 | * Start the application. This will create a window with high-dpi support 446 | * if possible. The provided event function is resposible for populating 447 | * the eva framebuffer and then requesting a draw with 448 | * eva_request_frame(). 449 | */ 450 | void eva_run(const char *window_title, 451 | eva_frame_fn frame_fn, 452 | eva_fail_fn fail_fn); 453 | 454 | /** 455 | * @brief Request that a frame be drawn. 456 | * 457 | * This will trigger the [frame callback](@ref eva_frame_fn) once the current 458 | * event has finished being processed. 459 | * 460 | * Only one call to the (frame callback)[@ref eva_frame_fn] will actually take 461 | * place and only one frame will actually be drawn no matter how many times 462 | * this method is called in the current event handler. 463 | * 464 | * @ingroup draw 465 | */ 466 | void eva_request_frame(void); 467 | 468 | // TODO: Formalizae the idea of content/client area vs window area 469 | uint32_t eva_get_window_width(void); 470 | uint32_t eva_get_window_height(void); 471 | 472 | /** 473 | * Returns the framebuffer struct that should be drawn into before requesting 474 | * it be drawn to the screen with a call to eva_request_frame(). 475 | */ 476 | eva_framebuffer eva_get_framebuffer(void); 477 | 478 | /** 479 | * @brief Set a function to be called during application initialization. 480 | * 481 | * This function is called once during application startup and should be used 482 | * to prepare the application state before the first [frame](@ref eva_frame_fn) 483 | * is rendered. 484 | * 485 | * @see @ref eva_init_fn 486 | * 487 | * @ingroup initialization 488 | */ 489 | void eva_set_init_fn(eva_init_fn init_fn); 490 | 491 | /** 492 | * @brief Set a function to be called during application shutdown. 493 | * 494 | * This function is called once it is confirmed that the application will be 495 | * shutdown. 496 | * 497 | * @see @ref eva_cleanup_fn 498 | * 499 | * @ingroup shutdown 500 | */ 501 | void eva_set_cleanup_fn(eva_cleanup_fn cleanup_fn); 502 | 503 | /** 504 | * @brief Set a function to be called when an application quit is requested. 505 | * 506 | * @see @ref eva_cancel_quit_fn 507 | * 508 | * @ingroup shutdown 509 | */ 510 | void eva_set_cancel_quit_fn(eva_cancel_quit_fn cancel_quit_fn); 511 | 512 | /** 513 | * @brief Sets a function to be called when the mouse is moved. 514 | * 515 | * See @ref eva_mouse_moved_fn 516 | * 517 | * @ingroup input 518 | */ 519 | void eva_set_mouse_moved_fn(eva_mouse_moved_fn mouse_moved_fn); 520 | 521 | /** 522 | * @brief Sets a function to be called scrolling takes place. 523 | * 524 | * See @ref eva_scroll_fn 525 | * 526 | * @ingroup input 527 | */ 528 | void eva_set_scroll_fn(eva_scroll_fn scroll_fn); 529 | 530 | /** 531 | * @brief Sets a function to be called when a mouse button is pressed/released. 532 | * 533 | * See @ref eva_mouse_btn_fn 534 | * 535 | * @ingroup input 536 | */ 537 | void eva_set_mouse_btn_fn(eva_mouse_btn_fn mouse_btn_fn); 538 | 539 | /** 540 | * @brief Sets a function to be called when a key is pressed/released. 541 | * 542 | * This should be used when responding to specific key press events. See 543 | * [eva_set_text_input_fn](@ref eva_set_text_input_fn) for handling text input 544 | * events. 545 | * 546 | * See @ref eva_key_fn 547 | * 548 | * @ingroup input 549 | */ 550 | void eva_set_key_fn(eva_key_fn key_fn); 551 | 552 | /** 553 | * @brief Sets a function to be called when text is input via key presses or 554 | * pasting. 555 | * 556 | * See @ref eva_text_input_fn 557 | * 558 | * @ingroup input 559 | */ 560 | void eva_set_text_input_fn(eva_text_input_fn text_input_fn); 561 | 562 | /** 563 | * @brief Sets a function to be called when the window is resized. 564 | * 565 | * Typically an application would simply request a new frame so that the 566 | * application can be drawn at the new size. 567 | * 568 | * See @ref eva_window_resize_fn 569 | * 570 | * @ingroup window 571 | */ 572 | void eva_set_window_resize_fn(eva_window_resize_fn window_resize_fn); 573 | 574 | /** 575 | * Initialize the time subsystem. 576 | */ 577 | void eva_time_init(void); 578 | uint64_t eva_time_now(void); 579 | uint64_t eva_time_since(uint64_t start); 580 | 581 | float eva_time_ms(uint64_t t); 582 | float eva_time_elapsed_ms(uint64_t start, uint64_t end); 583 | float eva_time_since_ms(uint64_t start); 584 | -------------------------------------------------------------------------------- /eva_macos.m: -------------------------------------------------------------------------------- 1 | #include "eva.h" 2 | 3 | #include 4 | 5 | #import 6 | #import 7 | 8 | static bool try_frame(); 9 | static bool create_shaders(void); 10 | static eva_key translate_key(uint32_t key); 11 | static eva_mod_flags translate_mod_flags(NSUInteger flags); 12 | static void init_key_tables(void); 13 | 14 | @interface eva_app_delegate : NSObject 15 | @end 16 | @interface eva_window_delegate : NSObject 17 | @end 18 | @interface eva_view : MTKView { 19 | NSTrackingArea *trackingArea; 20 | NSMutableAttributedString* markedText; 21 | } 22 | @end 23 | @interface eva_view_delegate : NSViewController 24 | @end 25 | 26 | #define EVA_MAX_MTL_BUFFERS 1 27 | typedef struct eva_ctx { 28 | eva_framebuffer framebuffer; 29 | uint32_t window_width, window_height; 30 | 31 | const char *window_title; 32 | bool quit_requested; 33 | bool quit_ordered; 34 | 35 | eva_init_fn init_fn; 36 | eva_frame_fn frame_fn; 37 | eva_cleanup_fn cleanup_fn; 38 | eva_cancel_quit_fn cancel_quit_fn; 39 | eva_fail_fn fail_fn; 40 | 41 | eva_mouse_moved_fn mouse_moved_fn; 42 | eva_mouse_btn_fn mouse_btn_fn; 43 | 44 | eva_scroll_fn scroll_fn; 45 | 46 | eva_key_fn key_fn; 47 | int16_t keycodes[256]; 48 | int16_t scancodes[EVA_KEY_LAST + 1]; 49 | 50 | eva_text_input_fn text_input_fn; 51 | 52 | eva_window_resize_fn window_resize_fn; 53 | 54 | id mtl_library; 55 | id mtl_device; 56 | id mtl_cmd_queue; 57 | id mtl_pipe_state; 58 | 59 | id mtl_textures[EVA_MAX_MTL_BUFFERS]; 60 | int8_t mtl_texture_index; 61 | 62 | dispatch_semaphore_t semaphore; // Used for syncing with CPU/GPU 63 | 64 | uint64_t start_time; 65 | bool request_frame; 66 | } eva_ctx; 67 | 68 | // The percentage of the texture width / height that are actually in use. 69 | // e.g. The MTLTexture might be 2000x1600 but the framebuffer may only 70 | // be 1000x400. The texture scale would then be 0.5x0.25. 71 | typedef struct eva_uniforms { 72 | float tex_scale_x; 73 | float tex_scale_y; 74 | } eva_uniforms; 75 | 76 | typedef struct eva_vertex { 77 | float x, y, z, w; 78 | } eva_vertex; 79 | 80 | static eva_vertex _vertices[4] = { 81 | {-1.0, -1.0, 0, 1}, 82 | {-1.0, 1.0, 0, 1}, 83 | { 1.0, -1.0, 0, 1}, 84 | { 1.0, 1.0, 0, 1}, 85 | }; 86 | 87 | static eva_ctx _ctx; 88 | 89 | static eva_app_delegate *_app_delegate; 90 | static NSWindow *_app_window; 91 | static eva_window_delegate *_app_window_delegate; 92 | static eva_view *_app_view; 93 | 94 | void eva_run(const char *window_title, 95 | eva_frame_fn frame_fn, 96 | eva_fail_fn fail_fn) 97 | { 98 | _ctx.start_time = eva_time_now(); 99 | 100 | _ctx.window_title = window_title; 101 | _ctx.frame_fn = frame_fn; 102 | _ctx.fail_fn = fail_fn; 103 | 104 | [NSApplication sharedApplication]; 105 | NSApp.activationPolicy = NSApplicationActivationPolicyRegular; 106 | _app_delegate = [[eva_app_delegate alloc] init]; 107 | NSApp.delegate = _app_delegate; 108 | [NSApp activateIgnoringOtherApps:YES]; 109 | [NSApp run]; 110 | } 111 | 112 | void eva_request_frame(void) 113 | { 114 | _ctx.request_frame = true; 115 | } 116 | 117 | uint32_t eva_get_window_width(void) 118 | { 119 | return _ctx.window_width; 120 | } 121 | 122 | uint32_t eva_get_window_height(void) 123 | { 124 | return _ctx.window_height; 125 | } 126 | 127 | eva_framebuffer eva_get_framebuffer(void) 128 | { 129 | return _ctx.framebuffer; 130 | } 131 | 132 | void eva_set_init_fn(eva_init_fn init_fn) 133 | { 134 | _ctx.init_fn = init_fn; 135 | } 136 | 137 | void eva_set_cleanup_fn(eva_cleanup_fn cleanup_fn) 138 | { 139 | _ctx.cleanup_fn = cleanup_fn; 140 | } 141 | 142 | void eva_set_cancel_quit_fn(eva_cancel_quit_fn cancel_quit_fn) 143 | { 144 | _ctx.cancel_quit_fn = cancel_quit_fn; 145 | } 146 | 147 | void eva_set_mouse_moved_fn(eva_mouse_moved_fn mouse_moved_fn) 148 | { 149 | _ctx.mouse_moved_fn = mouse_moved_fn; 150 | } 151 | 152 | void eva_set_mouse_btn_fn(eva_mouse_btn_fn mouse_btn_fn) 153 | { 154 | _ctx.mouse_btn_fn = mouse_btn_fn; 155 | } 156 | 157 | void eva_set_scroll_fn(eva_scroll_fn scroll_fn) 158 | { 159 | _ctx.scroll_fn = scroll_fn; 160 | } 161 | 162 | void eva_set_key_fn(eva_key_fn key_fn) 163 | { 164 | _ctx.key_fn = key_fn; 165 | } 166 | 167 | void eva_set_text_input_fn(eva_text_input_fn text_input_fn) 168 | { 169 | _ctx.text_input_fn = text_input_fn; 170 | } 171 | 172 | void eva_set_window_resize_fn(eva_window_resize_fn window_resize_fn) 173 | { 174 | _ctx.window_resize_fn = window_resize_fn; 175 | } 176 | 177 | static void update_window(void) 178 | { 179 | NSRect content_bounds = _app_window.contentView.bounds; 180 | NSRect backing_bounds = [_app_window convertRectToBacking:content_bounds]; 181 | 182 | _ctx.window_width = (uint32_t)content_bounds.size.width; 183 | _ctx.window_height = (uint32_t)content_bounds.size.height; 184 | 185 | _ctx.framebuffer.w = (uint32_t)(backing_bounds.size.width); 186 | _ctx.framebuffer.h = (uint32_t)(backing_bounds.size.height); 187 | 188 | _ctx.framebuffer.scale_x = (float)(backing_bounds.size.width / 189 | content_bounds.size.width); 190 | _ctx.framebuffer.scale_y = (float)(backing_bounds.size.height / 191 | content_bounds.size.height); 192 | 193 | uint32_t capacity = _ctx.framebuffer.pitch * _ctx.framebuffer.max_height; 194 | if (capacity == 0 || 195 | _ctx.framebuffer.w > _ctx.framebuffer.pitch || 196 | _ctx.framebuffer.h > _ctx.framebuffer.max_height) { 197 | 198 | // If this is happening before the first frame then there will be 199 | // no pixels yet 200 | if (_ctx.framebuffer.pixels != nil) { 201 | free(_ctx.framebuffer.pixels); 202 | } 203 | 204 | // Make the framebuffer large enough to hold pixels for the entire 205 | // screen. This makes it unnecessary to reallocate the framebuffer 206 | // when the window is resized. It should only need to be resized when 207 | // moving to a higher resolution monitor. 208 | NSRect screen_frame = NSScreen.mainScreen.frame; 209 | NSRect scaled_frame = [_app_window convertRectToBacking:screen_frame]; 210 | _ctx.framebuffer.pitch = (uint32_t)scaled_frame.size.width; 211 | _ctx.framebuffer.max_height = (uint32_t)scaled_frame.size.height; 212 | 213 | uint32_t size = _ctx.framebuffer.pitch * _ctx.framebuffer.max_height; 214 | _ctx.framebuffer.pixels = calloc((size_t)size, sizeof(eva_pixel)); 215 | 216 | // Recreate the metal textures that the framebuffer gets written into. 217 | MTLTextureDescriptor *texture_desc 218 | = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm 219 | width:_ctx.framebuffer.pitch 220 | height:_ctx.framebuffer.max_height 221 | mipmapped:false]; 222 | 223 | // Create the texture from the device by using the descriptor 224 | for (size_t i = 0; i < EVA_MAX_MTL_BUFFERS; ++i) { 225 | id texture = _ctx.mtl_textures[i]; 226 | if (texture != nil) { 227 | [texture release]; 228 | } 229 | _ctx.mtl_textures[i] = [_ctx.mtl_device newTextureWithDescriptor:texture_desc]; 230 | } 231 | } 232 | } 233 | 234 | @implementation eva_app_delegate 235 | - (void)applicationDidFinishLaunching:(NSNotification *)aNotification 236 | { 237 | // Setup window 238 | const NSUInteger style = 239 | NSWindowStyleMaskTitled | 240 | NSWindowStyleMaskClosable | 241 | NSWindowStyleMaskMiniaturizable | 242 | NSWindowStyleMaskResizable; 243 | 244 | NSRect screen_rect = NSScreen.mainScreen.frame; 245 | NSRect window_rect = NSMakeRect(0, 0, 246 | screen_rect.size.width * 0.8, 247 | screen_rect.size.height * 0.8); 248 | 249 | _app_window = [[NSWindow alloc] initWithContentRect:window_rect 250 | styleMask:style 251 | backing:NSBackingStoreBuffered 252 | defer:NO]; 253 | 254 | _app_window.title = [NSString stringWithUTF8String:_ctx.window_title]; 255 | _app_window.acceptsMouseMovedEvents = YES; 256 | _app_window.restorable = YES; 257 | 258 | init_key_tables(); 259 | 260 | // Setup window delegate 261 | _app_window_delegate = [[eva_window_delegate alloc] init]; 262 | _app_window.delegate = _app_window_delegate; 263 | 264 | // Setup metal 265 | _ctx.mtl_device = MTLCreateSystemDefaultDevice(); 266 | _ctx.mtl_cmd_queue = [_ctx.mtl_device newCommandQueue]; 267 | _ctx.semaphore = dispatch_semaphore_create(EVA_MAX_MTL_BUFFERS); 268 | 269 | update_window(); 270 | create_shaders(); 271 | 272 | // Setup view 273 | _app_view = [[eva_view alloc] init]; 274 | _app_view.device = _ctx.mtl_device; 275 | _app_view.enableSetNeedsDisplay = NO; 276 | [_app_view updateTrackingAreas]; 277 | eva_view_delegate *viewController = [[eva_view_delegate alloc] init]; 278 | _app_view.delegate = viewController; 279 | 280 | [_app_window makeFirstResponder:_app_view]; 281 | [_app_window center]; 282 | [_app_window makeKeyAndOrderFront:_app_view]; 283 | 284 | if (_ctx.init_fn) { 285 | _ctx.init_fn(); 286 | } 287 | // Assign view to window which will initiate a draw for the first frame. 288 | _app_window.contentView = _app_view; 289 | 290 | [NSApp finishLaunching]; 291 | } 292 | 293 | - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender 294 | { 295 | return YES; 296 | } 297 | @end 298 | 299 | @implementation eva_window_delegate 300 | - (BOOL)windowShouldClose:(id)sender 301 | { 302 | // only give user-code a chance to intervene when eva_quit() wasn't already 303 | // called 304 | if (!_ctx.quit_ordered) { 305 | // if window should be closed and event handling is enabled, give user 306 | // code a chance to intervene via eva_cancel_quit() 307 | _ctx.quit_requested = true; 308 | 309 | if (_ctx.cancel_quit_fn) { 310 | // See if the user code wants to cancel the quit sequence. 311 | _ctx.quit_requested = !_ctx.cancel_quit_fn(); 312 | } 313 | 314 | // user code hasn't intervened, quit the app 315 | if (_ctx.quit_requested) { 316 | _ctx.quit_ordered = true; 317 | } 318 | } 319 | if (_ctx.quit_ordered) { 320 | if (_ctx.cleanup_fn) { 321 | _ctx.cleanup_fn(); 322 | } 323 | return YES; 324 | } else { 325 | return NO; 326 | } 327 | } 328 | 329 | - (void)windowDidResize:(NSNotification *)notification 330 | { 331 | update_window(); 332 | 333 | _ctx.window_resize_fn(_ctx.framebuffer.w, _ctx.framebuffer.h); 334 | 335 | if (try_frame()) { 336 | [_app_view draw]; 337 | } 338 | } 339 | 340 | - (void)windowDidMiniaturize:(NSNotification *)notification 341 | { 342 | update_window(); 343 | } 344 | 345 | - (void)windowDidDeminiaturize:(NSNotification *)notification 346 | { 347 | update_window(); 348 | } 349 | @end 350 | 351 | @implementation eva_view 352 | - (instancetype)init 353 | { 354 | [super init]; 355 | 356 | [self updateTrackingAreas]; 357 | markedText = [[NSMutableAttributedString alloc] init]; 358 | 359 | return self; 360 | } 361 | - (void)dealloc 362 | { 363 | [trackingArea release]; 364 | [markedText release]; 365 | [super dealloc]; 366 | } 367 | - (void)viewDidChangeBackingProperties 368 | { 369 | update_window(); 370 | 371 | _ctx.window_resize_fn(_ctx.framebuffer.w, _ctx.framebuffer.h); 372 | 373 | if (try_frame()) { 374 | [self draw]; 375 | } 376 | } 377 | - (BOOL)isOpaque 378 | { 379 | return YES; 380 | } 381 | - (BOOL)canBecomeKeyView 382 | { 383 | return YES; 384 | } 385 | - (BOOL)acceptsFirstResponder 386 | { 387 | return YES; 388 | } 389 | - (BOOL)acceptsFirstMouse:(NSEvent *)event 390 | { 391 | return YES; 392 | } 393 | - (void)updateTrackingAreas 394 | { 395 | if (trackingArea != nil) { 396 | [self removeTrackingArea:trackingArea]; 397 | trackingArea = nil; 398 | } 399 | const NSTrackingAreaOptions options = 400 | NSTrackingMouseEnteredAndExited | 401 | NSTrackingActiveInKeyWindow | 402 | NSTrackingEnabledDuringMouseDrag | 403 | NSTrackingCursorUpdate | 404 | NSTrackingInVisibleRect | 405 | NSTrackingAssumeInside; 406 | 407 | trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds] 408 | options:options 409 | owner:self 410 | userInfo:nil]; 411 | [self addTrackingArea:trackingArea]; 412 | [super updateTrackingAreas]; 413 | } 414 | - (void)mouseEntered:(NSEvent *)event 415 | { 416 | } 417 | - (void)mouseExited:(NSEvent *)event 418 | { 419 | } 420 | 421 | - (void)mouseDown:(NSEvent *)event 422 | { 423 | if (_ctx.mouse_btn_fn) { 424 | NSPoint location = [event locationInWindow]; 425 | NSPoint mouse_pos = [self convertPoint:location fromView:nil]; 426 | mouse_pos = [self convertPointToBacking:mouse_pos]; 427 | mouse_pos.y = _ctx.framebuffer.h - mouse_pos.y; 428 | _ctx.mouse_btn_fn(mouse_pos.x, mouse_pos.y, 429 | EVA_MOUSE_BTN_LEFT, EVA_INPUT_PRESSED); 430 | if (try_frame()) { 431 | [self draw]; 432 | } 433 | } 434 | } 435 | - (void)mouseUp:(NSEvent *)event 436 | { 437 | if (_ctx.mouse_btn_fn) { 438 | NSPoint location = [event locationInWindow]; 439 | NSPoint mouse_pos = [self convertPoint:location fromView:nil]; 440 | mouse_pos = [self convertPointToBacking:mouse_pos]; 441 | mouse_pos.y = _ctx.framebuffer.h - mouse_pos.y; 442 | _ctx.mouse_btn_fn(mouse_pos.x, mouse_pos.y, 443 | EVA_MOUSE_BTN_LEFT, EVA_INPUT_RELEASED); 444 | if (try_frame()) { 445 | [self draw]; 446 | } 447 | } 448 | } 449 | - (void)rightMouseDown:(NSEvent *)event 450 | { 451 | if (_ctx.mouse_btn_fn) { 452 | NSPoint location = [event locationInWindow]; 453 | NSPoint mouse_pos = [self convertPoint:location fromView:nil]; 454 | mouse_pos = [self convertPointToBacking:mouse_pos]; 455 | mouse_pos.y = _ctx.framebuffer.h - mouse_pos.y; 456 | _ctx.mouse_btn_fn(mouse_pos.x, mouse_pos.y, 457 | EVA_MOUSE_BTN_RIGHT, EVA_INPUT_PRESSED); 458 | if (try_frame()) { 459 | [self draw]; 460 | } 461 | } 462 | } 463 | - (void)rightMouseUp:(NSEvent *)event 464 | { 465 | if (_ctx.mouse_btn_fn) { 466 | NSPoint location = [event locationInWindow]; 467 | NSPoint mouse_pos = [self convertPoint:location fromView:nil]; 468 | mouse_pos = [self convertPointToBacking:mouse_pos]; 469 | mouse_pos.y = _ctx.framebuffer.h - mouse_pos.y; 470 | _ctx.mouse_btn_fn(mouse_pos.x, mouse_pos.y, 471 | EVA_MOUSE_BTN_RIGHT, EVA_INPUT_RELEASED); 472 | if (try_frame()) { 473 | [self draw]; 474 | } 475 | } 476 | } 477 | - (void)otherMouseDown:(NSEvent *)event 478 | { 479 | if (_ctx.mouse_btn_fn) { 480 | NSPoint location = [event locationInWindow]; 481 | NSPoint mouse_pos = [self convertPoint:location fromView:nil]; 482 | mouse_pos = [self convertPointToBacking:mouse_pos]; 483 | mouse_pos.y = _ctx.framebuffer.h - mouse_pos.y; 484 | _ctx.mouse_btn_fn(mouse_pos.x, mouse_pos.y, 485 | EVA_MOUSE_BTN_MIDDLE, EVA_INPUT_PRESSED); 486 | if (try_frame()) { 487 | [self draw]; 488 | } 489 | } 490 | } 491 | - (void)otherMouseUp:(NSEvent *)event 492 | { 493 | if (_ctx.mouse_btn_fn) { 494 | NSPoint location = [event locationInWindow]; 495 | NSPoint mouse_pos = [self convertPoint:location fromView:nil]; 496 | mouse_pos = [self convertPointToBacking:mouse_pos]; 497 | mouse_pos.y = _ctx.framebuffer.h - mouse_pos.y; 498 | _ctx.mouse_btn_fn(mouse_pos.x, mouse_pos.y, 499 | EVA_MOUSE_BTN_MIDDLE, EVA_INPUT_RELEASED); 500 | if (try_frame()) { 501 | [self draw]; 502 | } 503 | } 504 | } 505 | - (void)mouseMoved:(NSEvent *)event 506 | { 507 | if (_ctx.mouse_moved_fn) { 508 | NSPoint location = [event locationInWindow]; 509 | NSPoint mouse_pos = [self convertPoint:location fromView:nil]; 510 | mouse_pos = [self convertPointToBacking:mouse_pos]; 511 | mouse_pos.y = _ctx.framebuffer.h - mouse_pos.y; 512 | _ctx.mouse_moved_fn(mouse_pos.x, mouse_pos.y); 513 | if (try_frame()) { 514 | [self draw]; 515 | } 516 | } 517 | } 518 | - (void)mouseDragged:(NSEvent *)event 519 | { 520 | [self mouseMoved:event]; 521 | } 522 | - (void)rightMouseDragged:(NSEvent *)event 523 | { 524 | [self mouseMoved:event]; 525 | } 526 | - (void)scrollWheel:(NSEvent *)event 527 | { 528 | double delta_x = [event scrollingDeltaX]; 529 | double delta_y = [event scrollingDeltaY]; 530 | 531 | if ([event hasPreciseScrollingDeltas]) 532 | { 533 | delta_x *= 0.1; 534 | delta_y *= 0.1; 535 | } 536 | 537 | if (fabs(delta_x) > 0.0 || fabs(delta_y) > 0.0) { 538 | _ctx.scroll_fn(delta_x, delta_y); 539 | } 540 | 541 | if (try_frame()) { 542 | [self draw]; 543 | } 544 | } 545 | - (void)keyDown:(NSEvent *)event 546 | { 547 | eva_key key = translate_key([event keyCode]); 548 | eva_mod_flags mods = translate_mod_flags([event modifierFlags]); 549 | 550 | if (_ctx.key_fn) { 551 | _ctx.key_fn(key, EVA_INPUT_PRESSED, mods); 552 | } 553 | 554 | // Send the event onward for text handling. 555 | [self interpretKeyEvents:@[event]]; 556 | 557 | if (try_frame()) { 558 | [self draw]; 559 | } 560 | } 561 | - (void)keyUp:(NSEvent *)event 562 | { 563 | eva_key key = translate_key([event keyCode]); 564 | eva_mod_flags mods = translate_mod_flags([event modifierFlags]); 565 | 566 | if (_ctx.key_fn) { 567 | _ctx.key_fn(key, EVA_INPUT_RELEASED, mods); 568 | } 569 | 570 | if (try_frame()) { 571 | [self draw]; 572 | } 573 | } 574 | - (void)flagsChanged:(NSEvent *)event 575 | { 576 | } 577 | - (void)cursorUpdate:(NSEvent *)event 578 | { 579 | } 580 | - (BOOL)hasMarkedText 581 | { 582 | return [markedText length] > 0; 583 | } 584 | 585 | static const NSRange kEmptyRange = { NSNotFound, 0 }; 586 | 587 | - (NSRange)markedRange 588 | { 589 | if ([markedText length] > 0) 590 | return NSMakeRange(0, [markedText length] - 1); 591 | else 592 | return kEmptyRange; 593 | } 594 | 595 | - (NSRange)selectedRange 596 | { 597 | return kEmptyRange; 598 | } 599 | 600 | - (void)setMarkedText:(id)string 601 | selectedRange:(NSRange)selectedRange 602 | replacementRange:(NSRange)replacementRange 603 | { 604 | [markedText release]; 605 | if ([string isKindOfClass:[NSAttributedString class]]) 606 | markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string]; 607 | else 608 | markedText = [[NSMutableAttributedString alloc] initWithString:string]; 609 | } 610 | 611 | - (void)unmarkText 612 | { 613 | [[markedText mutableString] setString:@""]; 614 | } 615 | 616 | - (NSArray*)validAttributesForMarkedText 617 | { 618 | return [NSArray array]; 619 | } 620 | 621 | - (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range 622 | actualRange:(NSRangePointer)actualRange 623 | { 624 | return nil; 625 | } 626 | 627 | - (NSUInteger)characterIndexForPoint:(NSPoint)point 628 | { 629 | return 0; 630 | } 631 | 632 | - (NSRect)firstRectForCharacterRange:(NSRange)range 633 | actualRange:(NSRangePointer)actualRange 634 | { 635 | const NSRect frame = [self frame]; 636 | return NSMakeRect(frame.origin.x, frame.origin.y, 0.0, 0.0); 637 | } 638 | 639 | - (void)insertText:(id)string replacementRange:(NSRange)replacementRange 640 | { 641 | if (_ctx.text_input_fn) { 642 | NSString* characters; 643 | NSEvent* event = [NSApp currentEvent]; 644 | eva_mod_flags mods = translate_mod_flags([event modifierFlags]); 645 | 646 | if ([string isKindOfClass:[NSAttributedString class]]) 647 | characters = [string string]; 648 | else 649 | characters = (NSString*) string; 650 | 651 | uint32_t len = (uint32_t)[characters length]; 652 | uint16_t *buffer = malloc(len * sizeof(uint16_t)); 653 | [characters getCharacters:buffer range:NSMakeRange(0, len)]; 654 | 655 | uint16_t c = buffer[0]; 656 | if (c < 32 || (c > 126 && c < 160)) { 657 | return; 658 | } 659 | 660 | _ctx.text_input_fn(buffer, len, mods); 661 | 662 | if (try_frame()) { 663 | [self draw]; 664 | } 665 | } 666 | } 667 | 668 | - (void)doCommandBySelector:(SEL)selector 669 | { 670 | // Stop the annoying "no suitable handler" beep that occurs when 671 | // "interpretKeyEvents" cannot find the this function. 672 | } 673 | 674 | @end 675 | @implementation eva_view_delegate 676 | - (void) drawInMTKView:(nonnull MTKView *) view { 677 | //uint64_t start = eva_time_now(); 678 | 679 | // Wait to ensure only MaxBuffersInFlight number of frames are getting proccessed 680 | // by any stage in the Metal pipeline (App, Metal, Drivers, GPU, etc) 681 | // If we don't wait here there is a chance our framebuffer will be changing 682 | // while a draw is reading from it which results in a partially filled 683 | // framebuffer being rendered. 684 | dispatch_semaphore_wait(_ctx.semaphore, DISPATCH_TIME_FOREVER); 685 | 686 | _ctx.mtl_texture_index = (_ctx.mtl_texture_index + 1) % EVA_MAX_MTL_BUFFERS; 687 | 688 | // Create a new command buffer for each render pass to the current drawable 689 | id cmd_buf = [_ctx.mtl_cmd_queue commandBuffer]; 690 | cmd_buf.label = @"eva_command_buffer"; 691 | 692 | // Add completion hander which signals semaphore when Metal and the GPU has fully 693 | // finished processing the commands we're encoding this frame. This indicates when the 694 | // dynamic buffers filled with our vertices, that we're writing to this frame, will no longer 695 | // be needed by Metal and the GPU, meaning we can overwrite the buffer contents without 696 | // corrupting the rendering. 697 | __block dispatch_semaphore_t block_sema = _ctx.semaphore; 698 | [cmd_buf addCompletedHandler:^(id buffer) { 699 | (void)buffer; 700 | dispatch_semaphore_signal(block_sema); 701 | }]; 702 | 703 | // Copy the bytes from our data object into the texture 704 | MTLRegion region = { 705 | { 0, 0, 0 }, 706 | { _ctx.framebuffer.w, _ctx.framebuffer.h, 1 } 707 | }; 708 | uint32_t bytes_per_row = _ctx.framebuffer.pitch * sizeof(eva_pixel); 709 | id texture = _ctx.mtl_textures[_ctx.mtl_texture_index]; 710 | [texture replaceRegion:region 711 | mipmapLevel:0 712 | withBytes:_ctx.framebuffer.pixels 713 | bytesPerRow:bytes_per_row]; 714 | 715 | eva_uniforms uniforms = { 716 | .tex_scale_x = _ctx.framebuffer.w / (float)_ctx.framebuffer.pitch, 717 | .tex_scale_y = _ctx.framebuffer.h / (float)_ctx.framebuffer.max_height 718 | }; 719 | 720 | // Delay getting the currentRenderPassDescriptor until absolutely needed. This avoids 721 | // holding onto the drawable and blocking the display pipeline any longer than necessary 722 | MTLRenderPassDescriptor* pass_desc = view.currentRenderPassDescriptor; 723 | if (pass_desc != nil) { 724 | pass_desc.colorAttachments[0].clearColor = MTLClearColorMake(0.0, 1.0, 0.0, 1.0); 725 | 726 | // Create a render command encoder so we can render into something 727 | id render_enc = [cmd_buf renderCommandEncoderWithDescriptor:pass_desc]; 728 | render_enc.label = @"eva_command_encoder"; 729 | 730 | // Set render command encoder state 731 | [render_enc setRenderPipelineState:_ctx.mtl_pipe_state]; 732 | [render_enc setVertexBytes:_vertices length:sizeof(_vertices) atIndex:0]; 733 | [render_enc setVertexBytes:&uniforms length:sizeof(eva_uniforms) atIndex:1]; 734 | 735 | [render_enc setFragmentTexture:texture atIndex:0]; 736 | 737 | // Draw the vertices of our quads 738 | [render_enc drawPrimitives:MTLPrimitiveTypeTriangleStrip vertexStart:0 vertexCount:4]; 739 | 740 | // We're done encoding commands 741 | [render_enc endEncoding]; 742 | 743 | // Schedule a present once the framebuffer is complete using the current drawable 744 | [cmd_buf presentDrawable:view.currentDrawable]; 745 | } 746 | 747 | // Finalize rendering here & push the command buffer to the GPU 748 | [cmd_buf commit]; 749 | 750 | //printf("drawRect %.1fms\n", eva_time_since_ms(start)); 751 | } 752 | - (void) mtkView:(nonnull MTKView *)view drawableSizeWillChange:(CGSize)size { 753 | (void)view; 754 | (void)size; 755 | } 756 | @end 757 | 758 | // time 759 | 760 | #include 761 | 762 | void eva_time_init(void) 763 | { 764 | // No-op on macos. 765 | } 766 | 767 | uint64_t eva_time_now(void) 768 | { 769 | return clock_gettime_nsec_np(CLOCK_UPTIME_RAW); 770 | } 771 | 772 | uint64_t eva_time_since(uint64_t start) 773 | { 774 | return eva_time_now() - start; 775 | } 776 | 777 | float eva_time_ms(uint64_t t) 778 | { 779 | return t / 1000000.0f; 780 | } 781 | 782 | float eva_time_elapsed_ms(uint64_t start, uint64_t end) 783 | { 784 | return eva_time_ms(end - start); 785 | } 786 | 787 | float eva_time_since_ms(uint64_t start) 788 | { 789 | return eva_time_elapsed_ms(start, eva_time_now()); 790 | } 791 | 792 | static bool try_frame() 793 | { 794 | if (_ctx.request_frame) { 795 | _ctx.request_frame = false; 796 | 797 | // There is a chance that the frame_fn is not set and the application 798 | // is just writing directly to the framebuffer in the event handlers 799 | // and then requesting to draw with eva_request_frame(). In this case 800 | // we still want to draw but don't have a frame function to call. 801 | if (_ctx.frame_fn) { 802 | _ctx.frame_fn(&_ctx.framebuffer); 803 | } 804 | 805 | return true; 806 | } 807 | 808 | return false; 809 | } 810 | 811 | #define eva_shader(inc, src) @inc#src 812 | 813 | NSString *_shader_src = eva_shader( 814 | "#include \n", 815 | using namespace metal; 816 | 817 | struct vert_out { 818 | float4 pos [[position]]; 819 | float2 tex_coord; 820 | }; 821 | 822 | struct vert_in { 823 | float4 pos [[position]]; 824 | }; 825 | 826 | struct uniforms { 827 | float tex_scale_x; 828 | float tex_scale_y; 829 | }; 830 | 831 | vertex vert_out 832 | vert_shader(unsigned int vert_id [[ vertex_id ]], 833 | const device vert_in *in [[ buffer(0) ]], 834 | constant uniforms *u [[ buffer(1) ]]) { 835 | vert_out out; 836 | 837 | out.pos = in[vert_id].pos; 838 | out.tex_coord.x = (float) (vert_id / 2) * u[0].tex_scale_x; 839 | out.tex_coord.y = (1.0 - (float) (vert_id % 2)) * u[0].tex_scale_y; 840 | 841 | return out; 842 | } 843 | 844 | fragment float4 845 | frag_shader(vert_out input [[stage_in ]], 846 | texture2d framebuffer [[ texture(0) ]]) { 847 | constexpr sampler tex_sampler(mag_filter::nearest, min_filter::nearest); 848 | 849 | // Sample the framebuffer to obtain a color 850 | const half4 sample = framebuffer.sample(tex_sampler, input.tex_coord); 851 | 852 | return float4(sample); 853 | }; 854 | ); 855 | 856 | static bool create_shaders(void) 857 | { 858 | NSError *error = 0x0; 859 | _ctx.mtl_library = [_ctx.mtl_device newLibraryWithSource:_shader_src 860 | options:[[MTLCompileOptions alloc] init] 861 | error:&error 862 | ]; 863 | if (!error || _ctx.mtl_library) { 864 | id vertex_shader_func = 865 | [_ctx.mtl_library newFunctionWithName:@"vert_shader"]; 866 | if (vertex_shader_func) { 867 | id fragment_shader_func = 868 | [_ctx.mtl_library newFunctionWithName:@"frag_shader"]; 869 | if (fragment_shader_func) { 870 | // Create a reusable pipeline state 871 | MTLRenderPipelineDescriptor *pipe_desc = [[MTLRenderPipelineDescriptor alloc] init]; 872 | pipe_desc.label = @"eva_mtl_pipeline"; 873 | pipe_desc.vertexFunction = vertex_shader_func; 874 | pipe_desc.fragmentFunction = fragment_shader_func; 875 | pipe_desc.colorAttachments[0].pixelFormat = 876 | MTLPixelFormatBGRA8Unorm; 877 | 878 | _ctx.mtl_pipe_state = [_ctx.mtl_device newRenderPipelineStateWithDescriptor:pipe_desc error:&error]; 879 | if (_ctx.mtl_pipe_state) { 880 | return true; 881 | } 882 | else { 883 | _ctx.fail_fn(0, "Failed to metal pipeline state"); // TODO: Error codes 884 | return false; 885 | } 886 | } 887 | else { 888 | _ctx.fail_fn(0, "Failed to get frag_shader function"); 889 | return false; 890 | } 891 | } 892 | else { 893 | _ctx.fail_fn(0, "Failed to get vert_shader function"); 894 | return false; 895 | } 896 | } else { 897 | _ctx.fail_fn(0, "Failed to create metal shader library"); 898 | return false; 899 | } 900 | } 901 | 902 | // Translates a macOS keycode to an eva keycode. Taken from GLFW 903 | static eva_key translate_key(uint32_t key) 904 | { 905 | if (key >= sizeof(_ctx.keycodes) / sizeof(_ctx.keycodes[0])) 906 | return EVA_KEY_UNKNOWN; 907 | 908 | return _ctx.keycodes[key]; 909 | } 910 | 911 | // Translates macOS key modifiers into eva ones. Taken from GLFW 912 | static eva_mod_flags translate_mod_flags(NSUInteger flags) 913 | { 914 | eva_mod_flags mods = 0; 915 | 916 | if (flags & NSEventModifierFlagShift) 917 | mods |= EVA_MOD_SHIFT; 918 | if (flags & NSEventModifierFlagControl) 919 | mods |= EVA_MOD_CONTROL; 920 | if (flags & NSEventModifierFlagOption) 921 | mods |= EVA_MOD_ALT; 922 | if (flags & NSEventModifierFlagCommand) 923 | mods |= EVA_MOD_SUPER; 924 | if (flags & NSEventModifierFlagCapsLock) 925 | mods |= EVA_MOD_CAPS_LOCK; 926 | 927 | return mods; 928 | } 929 | 930 | // Create key code translation tables. Taken from GLFW. 931 | static void init_key_tables(void) 932 | { 933 | memset(_ctx.keycodes, -1, sizeof(_ctx.keycodes)); 934 | memset(_ctx.scancodes, -1, sizeof(_ctx.scancodes)); 935 | 936 | _ctx.keycodes[0x1D] = EVA_KEY_0; 937 | _ctx.keycodes[0x12] = EVA_KEY_1; 938 | _ctx.keycodes[0x13] = EVA_KEY_2; 939 | _ctx.keycodes[0x14] = EVA_KEY_3; 940 | _ctx.keycodes[0x15] = EVA_KEY_4; 941 | _ctx.keycodes[0x17] = EVA_KEY_5; 942 | _ctx.keycodes[0x16] = EVA_KEY_6; 943 | _ctx.keycodes[0x1A] = EVA_KEY_7; 944 | _ctx.keycodes[0x1C] = EVA_KEY_8; 945 | _ctx.keycodes[0x19] = EVA_KEY_9; 946 | _ctx.keycodes[0x00] = EVA_KEY_A; 947 | _ctx.keycodes[0x0B] = EVA_KEY_B; 948 | _ctx.keycodes[0x08] = EVA_KEY_C; 949 | _ctx.keycodes[0x02] = EVA_KEY_D; 950 | _ctx.keycodes[0x0E] = EVA_KEY_E; 951 | _ctx.keycodes[0x03] = EVA_KEY_F; 952 | _ctx.keycodes[0x05] = EVA_KEY_G; 953 | _ctx.keycodes[0x04] = EVA_KEY_H; 954 | _ctx.keycodes[0x22] = EVA_KEY_I; 955 | _ctx.keycodes[0x26] = EVA_KEY_J; 956 | _ctx.keycodes[0x28] = EVA_KEY_K; 957 | _ctx.keycodes[0x25] = EVA_KEY_L; 958 | _ctx.keycodes[0x2E] = EVA_KEY_M; 959 | _ctx.keycodes[0x2D] = EVA_KEY_N; 960 | _ctx.keycodes[0x1F] = EVA_KEY_O; 961 | _ctx.keycodes[0x23] = EVA_KEY_P; 962 | _ctx.keycodes[0x0C] = EVA_KEY_Q; 963 | _ctx.keycodes[0x0F] = EVA_KEY_R; 964 | _ctx.keycodes[0x01] = EVA_KEY_S; 965 | _ctx.keycodes[0x11] = EVA_KEY_T; 966 | _ctx.keycodes[0x20] = EVA_KEY_U; 967 | _ctx.keycodes[0x09] = EVA_KEY_V; 968 | _ctx.keycodes[0x0D] = EVA_KEY_W; 969 | _ctx.keycodes[0x07] = EVA_KEY_X; 970 | _ctx.keycodes[0x10] = EVA_KEY_Y; 971 | _ctx.keycodes[0x06] = EVA_KEY_Z; 972 | 973 | _ctx.keycodes[0x27] = EVA_KEY_APOSTROPHE; 974 | _ctx.keycodes[0x2A] = EVA_KEY_BACKSLASH; 975 | _ctx.keycodes[0x2B] = EVA_KEY_COMMA; 976 | _ctx.keycodes[0x18] = EVA_KEY_EQUAL; 977 | _ctx.keycodes[0x32] = EVA_KEY_GRAVE_ACCENT; 978 | _ctx.keycodes[0x21] = EVA_KEY_LEFT_BRACKET; 979 | _ctx.keycodes[0x1B] = EVA_KEY_MINUS; 980 | _ctx.keycodes[0x2F] = EVA_KEY_PERIOD; 981 | _ctx.keycodes[0x1E] = EVA_KEY_RIGHT_BRACKET; 982 | _ctx.keycodes[0x29] = EVA_KEY_SEMICOLON; 983 | _ctx.keycodes[0x2C] = EVA_KEY_SLASH; 984 | _ctx.keycodes[0x0A] = EVA_KEY_WORLD_1; 985 | 986 | _ctx.keycodes[0x33] = EVA_KEY_BACKSPACE; 987 | _ctx.keycodes[0x39] = EVA_KEY_CAPS_LOCK; 988 | _ctx.keycodes[0x75] = EVA_KEY_DELETE; 989 | _ctx.keycodes[0x7D] = EVA_KEY_DOWN; 990 | _ctx.keycodes[0x77] = EVA_KEY_END; 991 | _ctx.keycodes[0x24] = EVA_KEY_ENTER; 992 | _ctx.keycodes[0x35] = EVA_KEY_ESCAPE; 993 | _ctx.keycodes[0x7A] = EVA_KEY_F1; 994 | _ctx.keycodes[0x78] = EVA_KEY_F2; 995 | _ctx.keycodes[0x63] = EVA_KEY_F3; 996 | _ctx.keycodes[0x76] = EVA_KEY_F4; 997 | _ctx.keycodes[0x60] = EVA_KEY_F5; 998 | _ctx.keycodes[0x61] = EVA_KEY_F6; 999 | _ctx.keycodes[0x62] = EVA_KEY_F7; 1000 | _ctx.keycodes[0x64] = EVA_KEY_F8; 1001 | _ctx.keycodes[0x65] = EVA_KEY_F9; 1002 | _ctx.keycodes[0x6D] = EVA_KEY_F10; 1003 | _ctx.keycodes[0x67] = EVA_KEY_F11; 1004 | _ctx.keycodes[0x6F] = EVA_KEY_F12; 1005 | _ctx.keycodes[0x69] = EVA_KEY_F13; 1006 | _ctx.keycodes[0x6B] = EVA_KEY_F14; 1007 | _ctx.keycodes[0x71] = EVA_KEY_F15; 1008 | _ctx.keycodes[0x6A] = EVA_KEY_F16; 1009 | _ctx.keycodes[0x40] = EVA_KEY_F17; 1010 | _ctx.keycodes[0x4F] = EVA_KEY_F18; 1011 | _ctx.keycodes[0x50] = EVA_KEY_F19; 1012 | _ctx.keycodes[0x5A] = EVA_KEY_F20; 1013 | _ctx.keycodes[0x73] = EVA_KEY_HOME; 1014 | _ctx.keycodes[0x72] = EVA_KEY_INSERT; 1015 | _ctx.keycodes[0x7B] = EVA_KEY_LEFT; 1016 | _ctx.keycodes[0x3A] = EVA_KEY_LEFT_ALT; 1017 | _ctx.keycodes[0x3B] = EVA_KEY_LEFT_CONTROL; 1018 | _ctx.keycodes[0x38] = EVA_KEY_LEFT_SHIFT; 1019 | _ctx.keycodes[0x37] = EVA_KEY_LEFT_SUPER; 1020 | _ctx.keycodes[0x6E] = EVA_KEY_MENU; 1021 | _ctx.keycodes[0x47] = EVA_KEY_NUM_LOCK; 1022 | _ctx.keycodes[0x79] = EVA_KEY_PAGE_DOWN; 1023 | _ctx.keycodes[0x74] = EVA_KEY_PAGE_UP; 1024 | _ctx.keycodes[0x7C] = EVA_KEY_RIGHT; 1025 | _ctx.keycodes[0x3D] = EVA_KEY_RIGHT_ALT; 1026 | _ctx.keycodes[0x3E] = EVA_KEY_RIGHT_CONTROL; 1027 | _ctx.keycodes[0x3C] = EVA_KEY_RIGHT_SHIFT; 1028 | _ctx.keycodes[0x36] = EVA_KEY_RIGHT_SUPER; 1029 | _ctx.keycodes[0x31] = EVA_KEY_SPACE; 1030 | _ctx.keycodes[0x30] = EVA_KEY_TAB; 1031 | _ctx.keycodes[0x7E] = EVA_KEY_UP; 1032 | 1033 | _ctx.keycodes[0x52] = EVA_KEY_KP_0; 1034 | _ctx.keycodes[0x53] = EVA_KEY_KP_1; 1035 | _ctx.keycodes[0x54] = EVA_KEY_KP_2; 1036 | _ctx.keycodes[0x55] = EVA_KEY_KP_3; 1037 | _ctx.keycodes[0x56] = EVA_KEY_KP_4; 1038 | _ctx.keycodes[0x57] = EVA_KEY_KP_5; 1039 | _ctx.keycodes[0x58] = EVA_KEY_KP_6; 1040 | _ctx.keycodes[0x59] = EVA_KEY_KP_7; 1041 | _ctx.keycodes[0x5B] = EVA_KEY_KP_8; 1042 | _ctx.keycodes[0x5C] = EVA_KEY_KP_9; 1043 | _ctx.keycodes[0x45] = EVA_KEY_KP_ADD; 1044 | _ctx.keycodes[0x41] = EVA_KEY_KP_DECIMAL; 1045 | _ctx.keycodes[0x4B] = EVA_KEY_KP_DIVIDE; 1046 | _ctx.keycodes[0x4C] = EVA_KEY_KP_ENTER; 1047 | _ctx.keycodes[0x51] = EVA_KEY_KP_EQUAL; 1048 | _ctx.keycodes[0x43] = EVA_KEY_KP_MULTIPLY; 1049 | _ctx.keycodes[0x4E] = EVA_KEY_KP_SUBTRACT; 1050 | 1051 | for (int16_t scancode = 0; scancode < 256; scancode++) 1052 | { 1053 | // Store the reverse translation for faster key name lookup 1054 | if (_ctx.keycodes[scancode] >= 0) 1055 | _ctx.scancodes[_ctx.keycodes[scancode]] = scancode; 1056 | } 1057 | } 1058 | 1059 | -------------------------------------------------------------------------------- /eva_windows.c: -------------------------------------------------------------------------------- 1 | #include "eva.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | #pragma comment(lib, "User32.lib") 9 | 10 | static LRESULT CALLBACK wnd_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); 11 | static void update_window(); 12 | static void handle_paint(); 13 | static void handle_close(); 14 | static void handle_resize(); 15 | static void try_frame(); 16 | static bool utf8_to_utf16(const char* src, wchar_t* dst, int dst_num_bytes); 17 | static bool utf16_to_utf8(const wchar_t* src, char* dst, int dst_num_bytes); 18 | 19 | typedef struct eva_ctx { 20 | int32_t window_width, window_height; 21 | eva_framebuffer framebuffer; 22 | const char *window_title; 23 | bool quit_requested; 24 | bool quit_ordered; 25 | 26 | eva_init_fn init_fn; 27 | eva_frame_fn frame_fn; 28 | eva_cleanup_fn cleanup_fn; 29 | eva_fail_fn fail_fn; 30 | eva_cancel_quit_fn cancel_quit_fn; 31 | eva_mouse_moved_fn mouse_moved_fn; 32 | eva_mouse_dragged_fn mouse_dragged_fn; 33 | eva_mouse_btn_fn mouse_btn_fn; 34 | eva_scroll_fn scroll_fn; 35 | eva_key_fn key_fn; 36 | eva_text_input_fn text_input_fn; 37 | eva_window_resize_fn window_resize_fn; 38 | 39 | LARGE_INTEGER ticks_per_sec; 40 | HWND hwnd; 41 | bool window_shown; 42 | bool resizing; 43 | bool frame_requested; 44 | } eva_ctx; 45 | 46 | static eva_ctx _ctx; 47 | 48 | void eva_run(const char *window_title, 49 | eva_frame_fn frame_fn, 50 | eva_fail_fn fail_fn) 51 | { 52 | assert(window_title); 53 | assert(frame_fn); 54 | assert(fail_fn); 55 | 56 | eva_time_init(); 57 | 58 | _ctx.window_title = window_title; 59 | _ctx.frame_fn = frame_fn; 60 | _ctx.fail_fn = fail_fn; 61 | 62 | if (!SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) { 63 | // TODO: Comment make a common set of error codes. 64 | _ctx.fail_fn(GetLastError(), "Failed to set DPI"); 65 | } 66 | 67 | WNDCLASSW wndclassw; 68 | memset(&wndclassw, 0, sizeof(wndclassw)); 69 | wndclassw.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; 70 | wndclassw.lpfnWndProc = (WNDPROC) wnd_proc; 71 | wndclassw.hInstance = GetModuleHandleW(NULL); 72 | wndclassw.hCursor = LoadCursor(NULL, IDC_ARROW); 73 | wndclassw.hIcon = LoadIcon(NULL, IDI_WINLOGO); 74 | wndclassw.lpszClassName = L"eva"; 75 | RegisterClassW(&wndclassw); 76 | 77 | DWORD style = WS_CLIPSIBLINGS | 78 | WS_CLIPCHILDREN | 79 | WS_CAPTION | 80 | WS_SYSMENU | 81 | WS_MINIMIZEBOX | 82 | WS_MAXIMIZEBOX | 83 | WS_SIZEBOX; 84 | DWORD ex_style = WS_EX_APPWINDOW | 85 | WS_EX_WINDOWEDGE; 86 | 87 | wchar_t window_title_utf16[256]; 88 | utf8_to_utf16(window_title, window_title_utf16, 256); 89 | 90 | _ctx.hwnd = CreateWindowExW(ex_style, 91 | L"eva", 92 | window_title_utf16, 93 | style, 94 | CW_USEDEFAULT, 95 | CW_USEDEFAULT, 96 | CW_USEDEFAULT, 97 | CW_USEDEFAULT, 98 | NULL, 99 | NULL, 100 | GetModuleHandleW(NULL), 101 | NULL); 102 | update_window(); 103 | if (_ctx.init_fn) { 104 | _ctx.init_fn(); 105 | } 106 | 107 | // Let the application full it's framebuffer before showing the window. 108 | _ctx.frame_fn(&_ctx.framebuffer); 109 | 110 | ShowWindow(_ctx.hwnd, SW_SHOW); 111 | _ctx.window_shown = true; 112 | 113 | bool done = false; 114 | while (!(done || _ctx.quit_ordered)) { 115 | MSG msg; 116 | GetMessageW(&msg, NULL, 0, 0); 117 | 118 | if (WM_QUIT == msg.message) { 119 | done = true; 120 | continue; 121 | } 122 | 123 | TranslateMessage(&msg); 124 | DispatchMessage(&msg); 125 | 126 | if (_ctx.quit_requested) { 127 | PostMessage(_ctx.hwnd, WM_CLOSE, 0, 0); 128 | } 129 | } 130 | _ctx.cleanup_fn(); 131 | 132 | DestroyWindow(_ctx.hwnd); 133 | UnregisterClassW(L"eva", GetModuleHandleW(NULL)); 134 | } 135 | 136 | void eva_request_frame() 137 | { 138 | _ctx.frame_requested = true; 139 | } 140 | 141 | uint32_t eva_get_window_width() 142 | { 143 | return _ctx.window_width; 144 | } 145 | 146 | uint32_t eva_get_window_height() 147 | { 148 | return _ctx.window_height; 149 | } 150 | 151 | eva_framebuffer eva_get_framebuffer() 152 | { 153 | return _ctx.framebuffer; 154 | } 155 | 156 | void eva_set_init_fn(eva_init_fn init_fn) 157 | { 158 | _ctx.init_fn = init_fn; 159 | } 160 | 161 | void eva_set_cleanup_fn(eva_cleanup_fn cleanup_fn) 162 | { 163 | _ctx.cleanup_fn = cleanup_fn; 164 | } 165 | 166 | void eva_set_cancel_quit_fn(eva_cancel_quit_fn cancel_quit_fn) 167 | { 168 | _ctx.cancel_quit_fn = cancel_quit_fn; 169 | } 170 | 171 | void eva_set_mouse_moved_fn(eva_mouse_moved_fn mouse_moved_fn) 172 | { 173 | _ctx.mouse_moved_fn = mouse_moved_fn; 174 | } 175 | 176 | void eva_set_mouse_dragged_fn(eva_mouse_dragged_fn mouse_dragged_fn) 177 | { 178 | _ctx.mouse_dragged_fn = mouse_dragged_fn; 179 | } 180 | 181 | void eva_set_mouse_btn_fn(eva_mouse_btn_fn mouse_btn_fn) 182 | { 183 | _ctx.mouse_btn_fn = mouse_btn_fn; 184 | } 185 | 186 | void eva_set_scroll_fn(eva_scroll_fn scroll_fn) 187 | { 188 | _ctx.scroll_fn = scroll_fn; 189 | } 190 | 191 | void eva_set_key_fn(eva_key_fn key_fn) 192 | { 193 | _ctx.key_fn = key_fn; 194 | } 195 | 196 | void eva_set_text_input_fn(eva_text_input_fn text_input_fn) 197 | { 198 | _ctx.text_input_fn = text_input_fn; 199 | } 200 | 201 | void eva_set_window_resize_fn(eva_window_resize_fn window_resize_fn) 202 | { 203 | _ctx.window_resize_fn = window_resize_fn; 204 | } 205 | 206 | void eva_time_init() 207 | { 208 | QueryPerformanceFrequency(&_ctx.ticks_per_sec); 209 | } 210 | 211 | uint64_t eva_time_now() 212 | { 213 | LARGE_INTEGER qpc; 214 | QueryPerformanceCounter(&qpc); 215 | return qpc.QuadPart; 216 | } 217 | 218 | uint64_t eva_time_since(uint64_t start) 219 | { 220 | return eva_time_now() - start; 221 | } 222 | 223 | float eva_time_ms(uint64_t t) 224 | { 225 | float ticks1000 = t * 1000.0f; 226 | float ms = ticks1000 / _ctx.ticks_per_sec.QuadPart; 227 | return ms; 228 | } 229 | 230 | float eva_time_elapsed_ms(uint64_t start, uint64_t end) 231 | { 232 | float ms = eva_time_ms(end - start); 233 | return ms; 234 | } 235 | 236 | float eva_time_since_ms(uint64_t start) 237 | { 238 | uint64_t now = eva_time_now(); 239 | float ms = eva_time_elapsed_ms(start, now); 240 | return ms; 241 | } 242 | 243 | static LRESULT CALLBACK wnd_proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 244 | { 245 | if (_ctx.window_shown) 246 | { 247 | switch (uMsg) { 248 | case WM_CLOSE: 249 | handle_close(); 250 | break; 251 | case WM_DPICHANGED: 252 | puts("WM_DPICHANGED"); 253 | update_window(); 254 | try_frame(); 255 | // Redraw? 256 | break; 257 | case WM_PAINT: 258 | handle_paint(); 259 | break; 260 | case WM_SIZE: 261 | handle_resize(); 262 | try_frame(); 263 | break; 264 | case WM_MOUSEMOVE: 265 | if (_ctx.mouse_moved_fn) { 266 | POINTS mouse_pos = MAKEPOINTS(lParam); 267 | _ctx.mouse_moved_fn(mouse_pos.x, mouse_pos.y); 268 | try_frame(); 269 | } 270 | break; 271 | case WM_LBUTTONDOWN: 272 | if (_ctx.mouse_btn_fn) { 273 | POINTS mouse_pos = MAKEPOINTS(lParam); 274 | _ctx.mouse_btn_fn(mouse_pos.x, mouse_pos.y, 275 | EVA_MOUSE_BTN_LEFT, EVA_INPUT_PRESSED); 276 | try_frame(); 277 | } 278 | break; 279 | case WM_LBUTTONUP: 280 | if (_ctx.mouse_btn_fn) { 281 | POINTS mouse_pos = MAKEPOINTS(lParam); 282 | _ctx.mouse_btn_fn(mouse_pos.x, mouse_pos.y, 283 | EVA_MOUSE_BTN_LEFT, EVA_INPUT_RELEASED); 284 | try_frame(); 285 | } 286 | break; 287 | case WM_RBUTTONDOWN: 288 | if (_ctx.mouse_btn_fn) { 289 | POINTS mouse_pos = MAKEPOINTS(lParam); 290 | _ctx.mouse_btn_fn(mouse_pos.x, mouse_pos.y, 291 | EVA_MOUSE_BTN_RIGHT, EVA_INPUT_PRESSED); 292 | try_frame(); 293 | } 294 | break; 295 | case WM_RBUTTONUP: 296 | if (_ctx.mouse_btn_fn) { 297 | POINTS mouse_pos = MAKEPOINTS(lParam); 298 | _ctx.mouse_btn_fn(mouse_pos.x, mouse_pos.y, 299 | EVA_MOUSE_BTN_RIGHT, EVA_INPUT_RELEASED); 300 | try_frame(); 301 | } 302 | break; 303 | case WM_MBUTTONDOWN: 304 | if (_ctx.mouse_btn_fn) { 305 | POINTS mouse_pos = MAKEPOINTS(lParam); 306 | _ctx.mouse_btn_fn(mouse_pos.x, mouse_pos.y, 307 | EVA_MOUSE_BTN_MIDDLE, EVA_INPUT_PRESSED); 308 | try_frame(); 309 | } 310 | break; 311 | case WM_MBUTTONUP: 312 | if (_ctx.mouse_btn_fn) { 313 | POINTS mouse_pos = MAKEPOINTS(lParam); 314 | _ctx.mouse_btn_fn(mouse_pos.x, mouse_pos.y, 315 | EVA_MOUSE_BTN_MIDDLE, EVA_INPUT_RELEASED); 316 | try_frame(); 317 | } 318 | break; 319 | default: 320 | break; 321 | 322 | }; 323 | } 324 | 325 | return DefWindowProcW(hWnd, uMsg, wParam, lParam); 326 | } 327 | 328 | static void update_window() 329 | { 330 | INT dpi = GetDpiForWindow(_ctx.hwnd); 331 | _ctx.framebuffer.scale_x = (float)dpi / USER_DEFAULT_SCREEN_DPI; 332 | // Always the same value on windows. 333 | _ctx.framebuffer.scale_y = _ctx.framebuffer.scale_x; 334 | 335 | // TODO: Window width/height is not really what we want. We actually 336 | // want content area. 337 | RECT rect; 338 | if (GetWindowRect(_ctx.hwnd, &rect)) { 339 | _ctx.window_width = rect.right - rect.left; 340 | _ctx.window_height = rect.bottom - rect.top; 341 | } 342 | 343 | if (GetClientRect(_ctx.hwnd, &rect)) { 344 | _ctx.framebuffer.w = rect.right - rect.left; 345 | _ctx.framebuffer.h = rect.bottom - rect.top; 346 | } 347 | 348 | uint32_t capacity = _ctx.framebuffer.pitch * _ctx.framebuffer.max_height; 349 | if (capacity == 0 || 350 | _ctx.framebuffer.w > _ctx.framebuffer.pitch || 351 | _ctx.framebuffer.h > _ctx.framebuffer.max_height) { 352 | 353 | if (_ctx.framebuffer.pixels) { 354 | free(_ctx.framebuffer.pixels); 355 | } 356 | 357 | // Make the framebuffer large enough to hold pixels for the entire 358 | // screen. This makes it unnecessary to reallocate the framebuffer 359 | // when the window is resized. It should only need to be resized when 360 | // moving to a higher resolution monitor. 361 | HMONITOR monitor = MonitorFromWindow(_ctx.hwnd, MONITOR_DEFAULTTOPRIMARY); 362 | 363 | // Get the monitors size in pixels. 364 | MONITORINFO mi = { 365 | .cbSize = sizeof(mi) 366 | }; 367 | GetMonitorInfoW(monitor, &mi); 368 | 369 | // According to win32 docs these may be negative. The docs don't explain 370 | // how or when unfortunately. 371 | uint32_t monitor_w = max(0, mi.rcMonitor.right - mi.rcMonitor.left); 372 | uint32_t monitor_h = max(0, mi.rcMonitor.bottom - mi.rcMonitor.top); 373 | 374 | _ctx.framebuffer.pitch = max(_ctx.framebuffer.w, monitor_w); 375 | _ctx.framebuffer.max_height = max(_ctx.framebuffer.h, monitor_h); 376 | 377 | int32_t size = _ctx.framebuffer.pitch * _ctx.framebuffer.max_height; 378 | _ctx.framebuffer.pixels = calloc((size_t)size, sizeof(eva_pixel)); 379 | } 380 | 381 | printf("window %d x %d\n", _ctx.window_width, _ctx.window_height); 382 | printf("framebuffer %d x %d\n", _ctx.framebuffer.w, _ctx.framebuffer.h); 383 | printf("framebuffer max %d x %d\n", _ctx.framebuffer.pitch, _ctx.framebuffer.max_height); 384 | printf("scale %.1f x %.1f\n", _ctx.framebuffer.scale_x, _ctx.framebuffer.scale_y); 385 | } 386 | 387 | static void handle_paint() 388 | { 389 | //uint64_t start = eva_time_now(); 390 | 391 | BITMAPINFO bmi = {0}; 392 | bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); 393 | bmi.bmiHeader.biWidth = _ctx.framebuffer.pitch; 394 | bmi.bmiHeader.biHeight = -(int32_t)_ctx.framebuffer.max_height; 395 | bmi.bmiHeader.biPlanes = 1; 396 | bmi.bmiHeader.biBitCount = 32; 397 | bmi.bmiHeader.biCompression = BI_RGB; 398 | 399 | // Get a paint DC for current window. 400 | // Paint DC contains the right scaling to match 401 | // the monitor DPI where the window is located. 402 | PAINTSTRUCT ps; 403 | HDC hdc = BeginPaint(_ctx.hwnd, &ps); 404 | 405 | // Draw the framebuffer to screen 406 | SetDIBitsToDevice( 407 | hdc, 408 | 0, // x dest 409 | 0, // y dest 410 | _ctx.framebuffer.w, // width 411 | _ctx.framebuffer.h, // height 412 | 0, // x src 413 | 0, // y src 414 | 0, // scanline 0 415 | _ctx.framebuffer.h, // n scanlines 416 | _ctx.framebuffer.pixels, // buffer 417 | &bmi, // buffer info 418 | DIB_RGB_COLORS // raw colors 419 | ); 420 | 421 | EndPaint(_ctx.hwnd, &ps); 422 | 423 | //printf("handle_paint - %.1f ms\n", eva_time_since_ms(start)); 424 | } 425 | 426 | static void handle_close() 427 | { 428 | // only give user-code a chance to intervene when eva_quit() wasn't already 429 | // called 430 | if (!_ctx.quit_ordered) { 431 | // if window should be closed and event handling is enabled, give user 432 | // code a chance to intervene via eva_cancel_quit() 433 | if (_ctx.cancel_quit_fn) { 434 | _ctx.quit_requested = !_ctx.cancel_quit_fn(); 435 | } 436 | else { 437 | _ctx.quit_requested = true; 438 | } 439 | 440 | // user code hasn't intervened, quit the app 441 | if (_ctx.quit_requested) { 442 | _ctx.quit_ordered = true; 443 | } 444 | } 445 | if (_ctx.quit_ordered) { 446 | PostQuitMessage(0); 447 | } 448 | } 449 | 450 | static void handle_resize() 451 | { 452 | update_window(); 453 | if (_ctx.window_resize_fn) { 454 | _ctx.window_resize_fn(_ctx.framebuffer.w, _ctx.framebuffer.h); 455 | } 456 | } 457 | 458 | static void try_frame() 459 | { 460 | if (_ctx.frame_requested) { 461 | if (_ctx.frame_fn) { 462 | _ctx.frame_fn(&_ctx.framebuffer); 463 | } 464 | 465 | InvalidateRect(_ctx.hwnd, NULL, FALSE); 466 | UpdateWindow(_ctx.hwnd); // Force WM_PAINT immediately 467 | } 468 | } 469 | 470 | static bool utf8_to_utf16(const char* src, wchar_t* dst, int dst_num_bytes) 471 | { 472 | assert(src && dst && (dst_num_bytes > 1)); 473 | memset(dst, 0, dst_num_bytes); 474 | 475 | int dst_chars = dst_num_bytes / sizeof(wchar_t); 476 | int dst_needed = MultiByteToWideChar(CP_UTF8, 0, src, -1, 0, 0); 477 | if ((dst_needed > 0) && (dst_needed < dst_chars)) { 478 | MultiByteToWideChar(CP_UTF8, 0, src, -1, dst, dst_chars); 479 | return true; 480 | } 481 | 482 | return false; 483 | } 484 | 485 | static bool utf16_to_utf8(const wchar_t* src, char* dst, int dst_num_bytes) 486 | { 487 | assert(src && dst && (dst_num_bytes > 1)); 488 | memset(dst, 0, dst_num_bytes); 489 | 490 | int32_t size = WideCharToMultiByte( 491 | CP_UTF8, // Going from UTF16 -> UTF8 492 | 0, // dwFlags must be 0 when using CP_UTF8 493 | src, // UTF16 input data 494 | -1, // Input len. -1 = use null terminator for len 495 | dst, // Output buffer 496 | dst_num_bytes,// Output buffer capacity 497 | NULL, NULL); // Always NULL for CP_UTF8 498 | 499 | return size != 0; 500 | } 501 | 502 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include "eva.h" 2 | 3 | #include 4 | 5 | typedef struct rectangle { 6 | int x, y, w, h; 7 | } rectangle; 8 | 9 | static rectangle rect; 10 | 11 | void clear(const eva_framebuffer *fb) 12 | { 13 | eva_pixel gray = { .r = 20, .g = 20, .b = 20, .a = 255 }; 14 | for (int j = 0; j < fb->h; j++) { 15 | for (int i = 0; i < fb->w; i++) { 16 | fb->pixels[i + j * fb->w] = gray; 17 | } 18 | } 19 | } 20 | 21 | void draw_rect(const eva_framebuffer *fb) 22 | { 23 | eva_pixel red = { .r = 255, .g = 0, .b = 0, .a = 255 }; 24 | for (int j = rect.y; j < rect.y + rect.h; j++) { 25 | for (int i = rect.x; i < rect.x + rect.w; i++) { 26 | fb->pixels[i + j * fb->w] = red; 27 | } 28 | } 29 | } 30 | 31 | void frame(const eva_framebuffer *fb) 32 | { 33 | clear(fb); 34 | draw_rect(fb); 35 | } 36 | 37 | void init() 38 | { 39 | puts("Init"); 40 | rect.x = 10; 41 | rect.y = 10; 42 | rect.w = 100; 43 | rect.h = 100; 44 | } 45 | 46 | void cleanup() 47 | { 48 | puts("Cleaning up"); 49 | } 50 | 51 | void fail(int error_code, const char *error_message) 52 | { 53 | printf("Error %d: %s\n", error_code, error_message); 54 | } 55 | 56 | #ifdef EVA_WINDOWS 57 | #include 58 | int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) 59 | #else 60 | int main() 61 | #endif 62 | { 63 | eva_run("Hello, eva!", frame, fail); 64 | return 0; 65 | } 66 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ./build/eva 3 | --------------------------------------------------------------------------------