Skip to content

Instantly share code, notes, and snippets.

@drZool
Last active April 6, 2026 01:11
Show Gist options
  • Select an option

  • Save drZool/92a877ec4488ace293d8f49e7c0d166a to your computer and use it in GitHub Desktop.

Select an option

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 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