Last active
April 6, 2026 01:11
-
-
Save drZool/92a877ec4488ace293d8f49e7c0d166a to your computer and use it in GitHub Desktop.
This is a proof of concept in Jai for using GetRect/Simp in a resizable window that is updating while resizing and moving the window on Windows.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // This is a proof of concept for using GetRect/Simp in a resizable window that is updating while resizing and moving the window on Windows. | |
| // The goal is to make it easy to create a smooth resizable window with a decent timing for rendering. | |
| // The gist of it is that we render on WM_PAINT but force Windows to send WM_PAINT when we want from a separate thread | |
| // so we render even when moving or resizing the window. | |
| // Internet suggested to use SetTimer to make Windows send a timely message for you to render the application. while this works, | |
| // the granularity of SetTimer is at lowest 1ms. And Windows doesn't like applications that stall the message pump if we wanted to spin in place. | |
| // So instead we use a thread that does the timing. When we want to render a frame we tell Windows to send us a | |
| // WM_PAINT message. | |
| // | |
| // -Christoffer Enedahl, 30 march 2026 | |
| // Discord user Žick found that calling ShellExecuteW inside of application_update would make Windows send WM_PAINT and trigger another application_update | |
| // Added a guard in application_update to prevent it to be called more than once each frame. -Christoffer Enedahl, 3 april 2026 | |
| // TODO Get rid of the flashbang when starting the application | |
| #import "Basic"; | |
| #import "GetRect"; | |
| #import "Thread"; | |
| Simp :: #import "Simp"; | |
| Input :: #import "Input"; | |
| Window_Creation :: #import "Window_Creation"; | |
| my_window : Window_Creation.Window_Type; | |
| window_width : s32 = 640; | |
| window_height : s32 = 360; | |
| application_quit := false; | |
| wanted_framerate :float = 60; | |
| current_time: float64; | |
| last_time: float64; | |
| main :: (){ | |
| initialize_os_specific_window_handling(); | |
| my_window = Window_Creation.create_window(window_width, window_height, "Resizable window"); | |
| finalize_os_specific_window_handling(); | |
| Simp.set_render_target(my_window); | |
| my_init_fonts(); | |
| ui_init(); | |
| last_time = seconds_since_init(); // Prevent wrong dt on first frame | |
| while !application_quit { | |
| Input.update_window_events(); // In Windows on resize and move window, application_update is called inside here also | |
| for event: Input.events_this_frame { | |
| if event.type == .QUIT then application_quit = true; | |
| getrect_handle_event(event); | |
| if event.type == { | |
| case .KEYBOARD; | |
| if event.key_pressed && event.key_code == .ESCAPE { | |
| active_widget_deactivate_all(); | |
| } | |
| } | |
| } | |
| application_update(); | |
| sleep_to_next_frame(); | |
| } | |
| } | |
| sleep_to_next_frame :: (){ | |
| // Calculate how long we need to sleep to hit the wanted_framerate, sleep that many milliseconds -1ms for good measure. | |
| delta_seconds_since_last_frame := seconds_since_init() - last_time; | |
| target_delta_in_seconds := 1.0 / wanted_framerate; | |
| seconds_to_wait := target_delta_in_seconds - delta_seconds_since_last_frame; | |
| milliseconds_to_wait := cast(s32)(1000*seconds_to_wait) - 1; // 1ms margin | |
| if milliseconds_to_wait < 0 | |
| milliseconds_to_wait = 0; | |
| sleep_milliseconds(milliseconds_to_wait); | |
| } | |
| #if OS == .WINDOWS { | |
| // The Windows specific code | |
| #run { | |
| Windows_Resources :: #import "Windows_Resources"; | |
| Windows_Resources.disable_runtime_console(); | |
| }; | |
| user32 :: #system_library "user32"; | |
| InvalidateRect :: (hWnd: Windows.HWND, rect:*Windows.RECT, erase:bool) -> bool #foreign user32; | |
| WM_ENTERSIZEMOVE :: 0x0231; | |
| WM_EXITSIZEMOVE :: 0x0232; | |
| windows_update_thread : Thread; | |
| is_dragging_window := false; | |
| hwnd : Windows.HWND; | |
| initialize_os_specific_window_handling :: (){ | |
| // Intercept windows input message handler so we can listen for the messages without changing the original handler in the Input module | |
| existing_window_proc = context.input_handler.window_proc; | |
| context.input_handler.window_proc = my_window_proc; | |
| Windows.SetProcessDPIAware(); | |
| Windows.timeBeginPeriod(1); // Windows is very bad at thread-switching by default unless you do this. Sad. | |
| } | |
| // We store the original context.input_handler.window_proc in existing_window_proc, | |
| // so we can call it from our function my_window_proc() which we are replacing context.input_handler.window_proc with | |
| existing_window_proc : (hwnd: Windows.HWND, message: u32, wParam: Windows.WPARAM, lParam: Windows.LPARAM) -> s64 #c_call; | |
| // This is the Windows message pump. we intecept a few messages here, but pass the rest on to existing_window_proc | |
| my_window_proc :: (hwnd: Windows.HWND, message: u32, wParam: Windows.WPARAM, lParam: Windows.LPARAM) -> s64 #c_call { | |
| push_context { | |
| if message == { | |
| case WM_ENTERSIZEMOVE; is_dragging_window = true; return 0; | |
| case WM_EXITSIZEMOVE; is_dragging_window = false; return 0; | |
| case Windows.WM_PAINT; | |
| Windows.ValidateRect(hwnd, null); // Notify Windows we have rendered our application | |
| application_update(); | |
| return 0; | |
| case Windows.WM_ERASEBKGND; | |
| return 1; // Notify Windows we have erased our background, if we do not, the window will flicker during move | |
| } | |
| return existing_window_proc(hwnd, message, wParam, lParam); | |
| } | |
| } | |
| finalize_os_specific_window_handling ::(){ | |
| hwnd = Windows.GetActiveWindow(); | |
| // Start a thread that makes the application render whenever the windows message pump is stalled. | |
| Initialize(*windows_update_thread); | |
| if !thread_init(*windows_update_thread, windows_update_thread_function) | |
| assert(false, "Failed to init thread!"); | |
| thread_start(*windows_update_thread); | |
| } | |
| windows_update_thread_function :: (thread: *Thread) -> s64 { | |
| while(hwnd){ | |
| delta_seconds_since_last_frame := seconds_since_init() - last_time; | |
| target_delta_in_seconds := 1.0 / wanted_framerate; | |
| // If for any reason the application is late to update, we issue an update from this thread. | |
| // Will when something has stalled the message pump. For instance when the user has opened the System Menu | |
| force_update := delta_seconds_since_last_frame > target_delta_in_seconds + 0.002; // Add some margin here 0.002 sec, we only want to use this path when necessary | |
| if (is_dragging_window || force_update) | |
| InvalidateRect(hwnd, null, false); // Will make windows send WM_PAINT so we can run application_update() | |
| sleep_to_next_frame(); | |
| } | |
| return 0; | |
| } | |
| }else{ | |
| initialize_os_specific_window_handling :: (){} | |
| finalize_os_specific_window_handling :: (){} | |
| } | |
| ////////////////// User code //////////////// | |
| // Get rect stuff | |
| Font :: Simp.Dynamic_Font; | |
| my_theme: Overall_Theme; // This gets filled out by calling the Theme_Proc for current_theme. | |
| current_theme: s32 = xx Default_Themes.Freddie_Freeloader; | |
| my_font : *Font; | |
| my_font_small : *Font; | |
| // Frame timing for the graph | |
| frame := -1; | |
| frames :: 60*5; | |
| frame_delta :[frames]float; | |
| set_delta_for_frame :: (delta:float){ | |
| frame = (frame + 1) % frames; | |
| frame_delta[frame] = delta; | |
| } | |
| is_in_application_update := false; | |
| application_update :: (){ | |
| // If we inside this function call something like ShellExecuteW, Windows might send a WM_PAINT back to this application | |
| // and that will call this function again. So we use this guard here to prevent application_update() more than once on the same frame. | |
| if is_in_application_update return; | |
| is_in_application_update = true; | |
| defer is_in_application_update = false; | |
| // Keep track of the delta time | |
| current_time = seconds_since_init(); | |
| // dt should ideally be around 1.0/wanted_framerate but we cannot count on it. | |
| // In Windows when we resize the window, we get a much lower dt. | |
| dt :float= cast(float)(current_time - last_time); | |
| last_time = current_time; | |
| //Handle window resize | |
| for Input.get_window_resizes() { | |
| Simp.update_window(it.window); | |
| if it.window == my_window { | |
| if it.width != window_width || it.height != window_height { | |
| window_width = it.width; | |
| window_height = it.height; | |
| my_init_fonts(); // Resize the font for the new window size. | |
| } | |
| } | |
| } | |
| Simp.set_render_target(my_window); // We have to set this every frame in Windows or there is nothing on the screen when updating this way. | |
| ui_per_frame_update(my_window, window_width, window_height, current_time); | |
| // Do the actual rendering | |
| button_theme := my_theme.button_theme; | |
| proc := default_theme_procs[current_theme]; | |
| my_theme = proc(); | |
| set_default_theme(my_theme); // Just in case we don't explicitly pass themes sometimes...! | |
| bg_col := my_theme.background_color; | |
| Simp.clear_render_target(bg_col.x, bg_col.y, bg_col.z, 1); | |
| { | |
| button_theme.font = my_font; | |
| button_theme.enable_variable_frame_thickness = true; | |
| button_theme.label_theme.alignment = .Center; | |
| r := get_rect( noot_w_to_px(15), noot_h_to_px(75), noot_w_to_px(70), noot_min_to_px(20)); | |
| time := formatFloat(seconds_since_init(), width=3, trailing_width=2, zero_removal=.NO); | |
| pressed := button(r, tprint("Hello, % Sailor!", time), *button_theme); | |
| if pressed { | |
| current_theme = (current_theme + 1) % THEME_COUNT; | |
| // Test open file through Windows | |
| // shell32 :: #system_library "Shell32"; | |
| // ShellExecuteW :: (hWnd : Windows.HWND, lpOperation : Windows.LPCWSTR, lpFile : Windows.LPCWSTR, lpParameters : Windows.LPCWSTR, lpDirectory : Windows.LPCWSTR, nShowCmd : s32) -> Windows.HINSTANCE #foreign shell32; | |
| // Utf8 :: #import "Windows_Utf8"; | |
| // ShellExecuteW (hwnd, null, Utf8.utf8_to_wide("resizable_window.jai",, temp), null, null, 0); | |
| } | |
| r = get_rect( noot_w_to_px( xx fmod_cycling( seconds_since_init() *20, 100) ), noot_h_to_px(60), noot_min_to_px(8), noot_min_to_px(8)); | |
| g0, g1, g2, g3 := get_quad(r); | |
| color:= Vector4.{0,0.5,0,1}; | |
| draw_procs.immediate_quad(g0, g1, g2, g3, color, .{0, 0}, .{1, 0}, .{1, 1},.{0, 1}); | |
| } | |
| frame_graph(dt); | |
| Simp.swap_buffers(my_window); | |
| reset_temporary_storage(); | |
| } | |
| // Render frame delta graph | |
| frame_graph ::(dt:float){ | |
| set_delta_for_frame(dt); | |
| button_theme := my_theme.button_theme; | |
| #import "Math"; | |
| graph_width := noot_w_to_px(50); | |
| graph_height:= noot_h_to_px(40); | |
| graph_x := noot_w_to_px(40); | |
| graph_y := noot_h_to_px(10); | |
| range:float = 32.0; //ms | |
| bar_width := graph_width / cast(float)frames; | |
| color := my_theme.background_color_bright; | |
| graph_rect := get_rect( graph_x, graph_y, graph_width, graph_height); | |
| g0, g1, g2, g3 := get_quad(graph_rect); | |
| draw_procs.immediate_quad(g0, g1, g2, g3, color, .{0, 0}, .{1, 0}, .{1, 1},.{0, 1}); | |
| for 0..frames-1{ | |
| r := get_rect( graph_x + bar_width * it, graph_y, bar_width, graph_height * 1000 * frame_delta[it] / range); | |
| p0, p1, p2, p3 := get_quad(r); | |
| color = ifx frame - 1 == it then Vector4.{1,1,1,1} else .{1,0,0,0.4}; | |
| draw_procs.immediate_quad(p0, p1, p2, p3, color, .{0, 0}, .{1, 0}, .{1, 1},.{0, 1}); | |
| } | |
| target_frame_delta :float = 1.0 / wanted_framerate; | |
| //Show target fps button, toggle wanted framerate | |
| { | |
| w := noot_w_to_px(30); | |
| h := noot_w_to_px(2); | |
| r := get_rect( graph_x-w, graph_y + graph_height*0.5 - (h*0.5), w, h); | |
| target_frame_delta_ms := formatFloat(target_frame_delta*1000, width=3, trailing_width=1, zero_removal=.NO); | |
| button_theme.label_theme.alignment = .Right; | |
| button_theme.font = my_font_small; | |
| pressed_a := button(r, tprint("Wanted FPS % delta %ms", wanted_framerate, target_frame_delta_ms), *button_theme); | |
| if pressed_a { | |
| if wanted_framerate == { | |
| case 60; wanted_framerate = 120; | |
| case 120; wanted_framerate = 30; | |
| case 30; wanted_framerate = 60; | |
| } | |
| } | |
| } | |
| //Draw target ms line | |
| { | |
| r := get_rect( graph_x , graph_y + graph_height * target_frame_delta*1000 / range, graph_width, 1); | |
| color = .{0,1,0,0.5}; | |
| p0, p1, p2, p3 := get_quad(r); | |
| draw_procs.immediate_quad(p0, p1, p2, p3, color, .{0, 0}, .{1, 0}, .{1, 1},.{0, 1}); | |
| } | |
| } | |
| noot_w_to_px :: ( value:float ) -> float { return window_width * value * 0.01;} | |
| noot_h_to_px :: ( value:float ) -> float { return window_height * value * 0.01;} | |
| noot_min_to_px :: ( value:float ) -> float { return min(window_width, window_height) * value * 0.01;} | |
| noot_max_to_px :: ( value:float ) -> float { return max(window_width, window_height) * value * 0.01;} | |
| my_init_fonts :: () { | |
| pixel_height :s64= xx noot_min_to_px(8); | |
| my_font = Simp.get_font_at_size("data", "OpenSans-BoldItalic.ttf", pixel_height); //If not found, it will use a default font | |
| // assert(my_font != null, "Could not read file data/OpenSans-BoldItalic.ttf"); // we don't want to ship any other files with this proof of concept, so don't assert here | |
| pixel_height = xx noot_min_to_px(3); | |
| my_font_small = Simp.get_font_at_size("data", "OpenSans-BoldItalic.ttf", pixel_height); | |
| // assert(my_font_small != null, "Could not read file data/OpenSans-BoldItalic.ttf"); // we don't want to ship any other files with this proof of concept, so don't assert here | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment