| 1 | + | /.claude/ | |
| 2 | + | *.bin |
| 1 | + | [workspace] | |
| 2 | + | name = "window" |
| 1 | + | # Project Rules | |
| 2 | + | - When committing, NO AI ATTRIBUTION | |
| 3 | + | - Ensure we stick to phases and all code is tested | |
| 4 | + | - GLFW and winit are included for reference. | |
| 5 | + | - Minimize API service a la GLFW | |
| 6 | + | - Follow winit's lead in architecture | |
| 7 | + | - Use Odin thought patterns for the actual implementation | |
| 8 | + | - Use "odin run examples/<filename>.odin -file" for testing |
| 1 | + | package basic_example | |
| 2 | + | ||
| 3 | + | import "core:fmt" | |
| 4 | + | import win "../window" | |
| 5 | + | ||
| 6 | + | main :: proc() { | |
| 7 | + | loop, err := win.create_event_loop() | |
| 8 | + | if err != .None { | |
| 9 | + | fmt.eprintln("Failed to create event loop:", err) | |
| 10 | + | return | |
| 11 | + | } | |
| 12 | + | defer win.destroy_event_loop(loop) | |
| 13 | + | ||
| 14 | + | window, win_err := win.create_window(loop, { | |
| 15 | + | title = "Window Library Test", | |
| 16 | + | size = {800, 600}, | |
| 17 | + | resizable = true, | |
| 18 | + | visible = true, | |
| 19 | + | decorated = true, | |
| 20 | + | }) | |
| 21 | + | if win_err != .None { | |
| 22 | + | fmt.eprintln("Failed to create window:", win_err) | |
| 23 | + | return | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | fmt.println("Window created! Close it to exit.") | |
| 27 | + | ||
| 28 | + | for !win.should_close(window) { | |
| 29 | + | win.poll_events(loop) | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | fmt.println("Goodbye!") | |
| 33 | + | } |
| 1 | + | package features_test | |
| 2 | + | ||
| 3 | + | import "core:fmt" | |
| 4 | + | import win "../window" | |
| 5 | + | ||
| 6 | + | // 4x4 red RGBA icon for testing (tiny but valid) | |
| 7 | + | make_test_icon :: proc(r, g, b: u8, size: u32) -> win.Icon { | |
| 8 | + | n := int(size * size * 4) | |
| 9 | + | rgba := make([]u8, n) | |
| 10 | + | for i := 0; i < n; i += 4 { | |
| 11 | + | rgba[i + 0] = r | |
| 12 | + | rgba[i + 1] = g | |
| 13 | + | rgba[i + 2] = b | |
| 14 | + | rgba[i + 3] = 255 | |
| 15 | + | } | |
| 16 | + | return win.Icon{rgba = rgba, width = size, height = size} | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // 16x16 green crosshair cursor | |
| 20 | + | make_test_cursor :: proc() -> win.Cursor_Image { | |
| 21 | + | size : u32 = 16 | |
| 22 | + | n := int(size * size * 4) | |
| 23 | + | rgba := make([]u8, n) | |
| 24 | + | for y in 0..<size { | |
| 25 | + | for x in 0..<size { | |
| 26 | + | i := int((y * size + x) * 4) | |
| 27 | + | is_cross := (x == size/2 || y == size/2) | |
| 28 | + | rgba[i + 0] = is_cross ? u8(0) : u8(0) | |
| 29 | + | rgba[i + 1] = is_cross ? u8(200) : u8(0) | |
| 30 | + | rgba[i + 2] = is_cross ? u8(0) : u8(0) | |
| 31 | + | rgba[i + 3] = is_cross ? u8(255) : u8(0) | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | return win.Cursor_Image{rgba = rgba, width = size, height = size, hotspot = {size/2, size/2}} | |
| 35 | + | } | |
| 36 | + | ||
| 37 | + | main :: proc() { | |
| 38 | + | loop, err := win.create_event_loop() | |
| 39 | + | if err != .None { | |
| 40 | + | fmt.eprintln("Failed to create event loop:", err) | |
| 41 | + | return | |
| 42 | + | } | |
| 43 | + | defer win.destroy_event_loop(loop) | |
| 44 | + | ||
| 45 | + | window, win_err := win.create_window(loop, { | |
| 46 | + | title = "Features Test", | |
| 47 | + | size = {600, 400}, | |
| 48 | + | resizable = true, | |
| 49 | + | visible = true, | |
| 50 | + | decorated = true, | |
| 51 | + | }) | |
| 52 | + | if win_err != .None { | |
| 53 | + | fmt.eprintln("Failed to create window:", win_err) | |
| 54 | + | return | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // --- Tier 1: state tracking --- | |
| 58 | + | fmt.println("is_maximized:", win.is_maximized(window)) // should be false | |
| 59 | + | fmt.println("is_minimized:", win.is_minimized(window)) // should be false | |
| 60 | + | fmt.println("is_decorated:", win.is_decorated(window)) // should be true | |
| 61 | + | fmt.println("is_resizable:", win.is_resizable(window)) // should be true | |
| 62 | + | ||
| 63 | + | // --- Tier 2: icon --- | |
| 64 | + | icon := make_test_icon(255, 0, 0, 32) | |
| 65 | + | defer delete(icon.rgba) | |
| 66 | + | win.set_icon(window, icon) | |
| 67 | + | fmt.println("Set window icon (32x32 red)") | |
| 68 | + | ||
| 69 | + | // --- Tier 2: opacity --- | |
| 70 | + | win.set_opacity(window, 0.9) | |
| 71 | + | fmt.println("Set opacity to 0.9") | |
| 72 | + | ||
| 73 | + | // --- Tier 2: window level --- | |
| 74 | + | win.set_window_level(window, .Always_On_Top) | |
| 75 | + | fmt.println("Set window level: always-on-top") | |
| 76 | + | win.set_window_level(window, .Normal) | |
| 77 | + | fmt.println("Set window level: normal") | |
| 78 | + | ||
| 79 | + | // --- Tier 2: aspect ratio --- | |
| 80 | + | win.set_aspect_ratio(window, [2]u32{16, 9}) | |
| 81 | + | fmt.println("Set aspect ratio 16:9 (try resizing the window)") | |
| 82 | + | ||
| 83 | + | // --- Tier 2: custom cursor --- | |
| 84 | + | cursor := make_test_cursor() | |
| 85 | + | defer delete(cursor.rgba) | |
| 86 | + | win.set_custom_cursor(window, cursor) | |
| 87 | + | fmt.println("Set custom 16x16 green cursor") | |
| 88 | + | ||
| 89 | + | // --- Tier 2: request attention --- | |
| 90 | + | win.request_attention(window) | |
| 91 | + | fmt.println("Requested attention (taskbar flash)") | |
| 92 | + | ||
| 93 | + | step := 0 | |
| 94 | + | ||
| 95 | + | win.run(loop, proc(event: win.Event, loop: ^win.Event_Loop) -> win.Control_Flow { | |
| 96 | + | #partial switch e in event { | |
| 97 | + | case win.Window_Event_Data: | |
| 98 | + | #partial switch we in e.event { | |
| 99 | + | case win.Close_Requested: | |
| 100 | + | win.exit(loop) | |
| 101 | + | ||
| 102 | + | case win.Key_Input: | |
| 103 | + | if we.state == .Pressed { | |
| 104 | + | fmt.printfln("Key: %v repeat=%v mods=%v", we.key, we.repeat, we.modifiers) | |
| 105 | + | if we.key == .Escape { | |
| 106 | + | win.exit(loop) | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | case win.Resized: | |
| 111 | + | fmt.printfln("Resized: %dx%d", we.size.width, we.size.height) | |
| 112 | + | ||
| 113 | + | case win.Scale_Factor_Changed: | |
| 114 | + | fmt.printfln("Scale factor changed: %.2f new_size: %dx%d", | |
| 115 | + | we.scale_factor, we.new_size.width, we.new_size.height) | |
| 116 | + | ||
| 117 | + | case win.Focused: | |
| 118 | + | fmt.println("Focused") | |
| 119 | + | ||
| 120 | + | case win.Unfocused: | |
| 121 | + | fmt.println("Unfocused") | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | case win.New_Events: | |
| 125 | + | if e.cause == .Init { | |
| 126 | + | fmt.println("Event loop initialized — close window or press Escape to exit") | |
| 127 | + | } | |
| 128 | + | } | |
| 129 | + | return win.Wait{} | |
| 130 | + | }) | |
| 131 | + | ||
| 132 | + | fmt.println("Done.") | |
| 133 | + | } |
| 1 | + | package main | |
| 2 | + | ||
| 3 | + | import "core:fmt" | |
| 4 | + | import win "../window" | |
| 5 | + | ||
| 6 | + | // Store window pointer globally for the callback | |
| 7 | + | g_window: ^win.Window | |
| 8 | + | ||
| 9 | + | main :: proc() { | |
| 10 | + | loop, err := win.create_event_loop() | |
| 11 | + | if err != .None { | |
| 12 | + | fmt.println("Failed to create event loop:", err) | |
| 13 | + | return | |
| 14 | + | } | |
| 15 | + | defer win.destroy_event_loop(loop) | |
| 16 | + | ||
| 17 | + | window, err2 := win.create_window(loop, { | |
| 18 | + | title = "Input Test - Press keys to test features", | |
| 19 | + | size = {800, 600}, | |
| 20 | + | resizable = true, | |
| 21 | + | visible = true, | |
| 22 | + | decorated = true, | |
| 23 | + | }) | |
| 24 | + | if err2 != .None { | |
| 25 | + | fmt.println("Failed to create window:", err2) | |
| 26 | + | return | |
| 27 | + | } | |
| 28 | + | defer win.destroy_window(window) | |
| 29 | + | ||
| 30 | + | g_window = window | |
| 31 | + | ||
| 32 | + | fmt.println("=== Input Feature Test ===") | |
| 33 | + | fmt.println("Keys:") | |
| 34 | + | fmt.println(" 1-9,0: Change cursor shape") | |
| 35 | + | fmt.println(" H: Toggle hidden cursor") | |
| 36 | + | fmt.println(" C: Toggle confined cursor") | |
| 37 | + | fmt.println(" G: Toggle grabbed cursor") | |
| 38 | + | fmt.println(" R: Toggle raw mouse motion") | |
| 39 | + | fmt.println(" SPACE: Query pressed keys") | |
| 40 | + | fmt.println(" ESC: Exit") | |
| 41 | + | fmt.println() | |
| 42 | + | fmt.printfln("Raw motion supported: %v", win.supports_raw_mouse_motion(loop)) | |
| 43 | + | fmt.println() | |
| 44 | + | ||
| 45 | + | win.run(loop, proc(event: win.Event, loop: ^win.Event_Loop) -> win.Control_Flow { | |
| 46 | + | window := g_window | |
| 47 | + | ||
| 48 | + | #partial switch e in event { | |
| 49 | + | case win.Window_Event_Data: | |
| 50 | + | #partial switch we in e.event { | |
| 51 | + | case win.Close_Requested: | |
| 52 | + | return nil // Exit | |
| 53 | + | ||
| 54 | + | case win.Key_Input: | |
| 55 | + | if we.state == .Pressed { | |
| 56 | + | #partial switch we.key { | |
| 57 | + | case .Key_1: win.set_cursor_shape(window, .Arrow); fmt.println("Cursor: Arrow") | |
| 58 | + | case .Key_2: win.set_cursor_shape(window, .IBeam); fmt.println("Cursor: IBeam") | |
| 59 | + | case .Key_3: win.set_cursor_shape(window, .Crosshair); fmt.println("Cursor: Crosshair") | |
| 60 | + | case .Key_4: win.set_cursor_shape(window, .Hand); fmt.println("Cursor: Hand") | |
| 61 | + | case .Key_5: win.set_cursor_shape(window, .Resize_EW); fmt.println("Cursor: Resize_EW") | |
| 62 | + | case .Key_6: win.set_cursor_shape(window, .Resize_NS); fmt.println("Cursor: Resize_NS") | |
| 63 | + | case .Key_7: win.set_cursor_shape(window, .Resize_NWSE); fmt.println("Cursor: Resize_NWSE") | |
| 64 | + | case .Key_8: win.set_cursor_shape(window, .Resize_NESW); fmt.println("Cursor: Resize_NESW") | |
| 65 | + | case .Key_9: win.set_cursor_shape(window, .Resize_All); fmt.println("Cursor: Resize_All") | |
| 66 | + | case .Key_0: win.set_cursor_shape(window, .Not_Allowed); fmt.println("Cursor: Not_Allowed") | |
| 67 | + | ||
| 68 | + | case .H: | |
| 69 | + | if win.get_cursor_mode(window) == .Hidden { | |
| 70 | + | win.set_cursor_mode(window, .Normal) | |
| 71 | + | fmt.println("Cursor mode: Normal") | |
| 72 | + | } else { | |
| 73 | + | win.set_cursor_mode(window, .Hidden) | |
| 74 | + | fmt.println("Cursor mode: Hidden") | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | case .C: | |
| 78 | + | if win.get_cursor_mode(window) == .Confined { | |
| 79 | + | win.set_cursor_mode(window, .Normal) | |
| 80 | + | fmt.println("Cursor mode: Normal") | |
| 81 | + | } else { | |
| 82 | + | win.set_cursor_mode(window, .Confined) | |
| 83 | + | fmt.println("Cursor mode: Confined") | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | case .G: | |
| 87 | + | if win.get_cursor_mode(window) == .Grabbed { | |
| 88 | + | win.set_cursor_mode(window, .Normal) | |
| 89 | + | fmt.println("Cursor mode: Normal") | |
| 90 | + | } else { | |
| 91 | + | win.set_cursor_mode(window, .Grabbed) | |
| 92 | + | fmt.println("Cursor mode: Grabbed (press G again to release)") | |
| 93 | + | } | |
| 94 | + | ||
| 95 | + | case .R: | |
| 96 | + | raw_enabled := win.is_raw_mouse_motion_enabled(window) | |
| 97 | + | win.set_raw_mouse_motion(window, !raw_enabled) | |
| 98 | + | if !raw_enabled { | |
| 99 | + | fmt.println("Raw mouse motion: ENABLED") | |
| 100 | + | } else { | |
| 101 | + | fmt.println("Raw mouse motion: DISABLED") | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | case .Space: | |
| 105 | + | fmt.println("--- Key State Polling ---") | |
| 106 | + | mods := win.get_modifiers(loop) | |
| 107 | + | fmt.printfln("Modifiers: Shift=%v Ctrl=%v Alt=%v Super=%v", | |
| 108 | + | mods.shift, mods.ctrl, mods.alt, mods.super) | |
| 109 | + | ||
| 110 | + | // Check some common keys | |
| 111 | + | if win.is_key_pressed(loop, .W) do fmt.println(" W is pressed") | |
| 112 | + | if win.is_key_pressed(loop, .A) do fmt.println(" A is pressed") | |
| 113 | + | if win.is_key_pressed(loop, .S) do fmt.println(" S is pressed") | |
| 114 | + | if win.is_key_pressed(loop, .D) do fmt.println(" D is pressed") | |
| 115 | + | ||
| 116 | + | // Check mouse buttons | |
| 117 | + | if win.is_mouse_button_pressed(loop, .Left) do fmt.println(" Left mouse button pressed") | |
| 118 | + | if win.is_mouse_button_pressed(loop, .Right) do fmt.println(" Right mouse button pressed") | |
| 119 | + | if win.is_mouse_button_pressed(loop, .Middle) do fmt.println(" Middle mouse button pressed") | |
| 120 | + | ||
| 121 | + | case .Escape: | |
| 122 | + | return nil // Exit | |
| 123 | + | } | |
| 124 | + | } | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | case win.Device_Event_Data: | |
| 128 | + | #partial switch de in e.event { | |
| 129 | + | case win.Pointer_Motion: | |
| 130 | + | fmt.printfln("Raw motion: dx=%.2f dy=%.2f", de.delta_x, de.delta_y) | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | case win.Loop_Exiting: | |
| 134 | + | fmt.println("Exiting...") | |
| 135 | + | } | |
| 136 | + | ||
| 137 | + | return win.Wait{} | |
| 138 | + | }) | |
| 139 | + | } |
| 1 | + | package monitors_example | |
| 2 | + | ||
| 3 | + | import "core:fmt" | |
| 4 | + | import win "../window" | |
| 5 | + | ||
| 6 | + | main :: proc() { | |
| 7 | + | fmt.println("Monitor Information Demo") | |
| 8 | + | fmt.println("========================") | |
| 9 | + | ||
| 10 | + | loop, err := win.create_event_loop() | |
| 11 | + | if err != .None { | |
| 12 | + | fmt.eprintln("Failed to create event loop:", err) | |
| 13 | + | return | |
| 14 | + | } | |
| 15 | + | defer win.destroy_event_loop(loop) | |
| 16 | + | ||
| 17 | + | // Get all monitors | |
| 18 | + | monitors := win.get_monitors(loop) | |
| 19 | + | fmt.printfln("\nFound %d monitor(s):\n", len(monitors)) | |
| 20 | + | ||
| 21 | + | for monitor, i in monitors { | |
| 22 | + | info := win.get_monitor_info(loop, monitor) | |
| 23 | + | ||
| 24 | + | primary_str := info.is_primary ? " (Primary)" : "" | |
| 25 | + | fmt.printfln("Monitor %d: %s%s", i + 1, info.name, primary_str) | |
| 26 | + | fmt.printfln(" Position: (%d, %d)", info.position.x, info.position.y) | |
| 27 | + | fmt.printfln(" Physical size: %d x %d mm", info.size_mm.width, info.size_mm.height) | |
| 28 | + | fmt.printfln(" Scale factor: %.2f", info.scale_factor) | |
| 29 | + | fmt.printfln(" Current mode: %dx%d @ %dHz (%d-bit)", | |
| 30 | + | info.current_mode.size.width, | |
| 31 | + | info.current_mode.size.height, | |
| 32 | + | info.current_mode.refresh_rate, | |
| 33 | + | info.current_mode.bit_depth) | |
| 34 | + | ||
| 35 | + | // Get workarea | |
| 36 | + | workarea := win.get_monitor_workarea(loop, monitor) | |
| 37 | + | fmt.printfln(" Workarea: (%d, %d) %dx%d", | |
| 38 | + | workarea.position.x, workarea.position.y, | |
| 39 | + | workarea.size.width, workarea.size.height) | |
| 40 | + | ||
| 41 | + | // Get all video modes | |
| 42 | + | modes := win.get_video_modes(loop, monitor) | |
| 43 | + | defer delete(modes) | |
| 44 | + | fmt.printfln(" Supported modes: %d", len(modes)) | |
| 45 | + | ||
| 46 | + | // Print first few modes | |
| 47 | + | for mode, j in modes { | |
| 48 | + | if j >= 5 { | |
| 49 | + | fmt.printfln(" ... and %d more", len(modes) - 5) | |
| 50 | + | break | |
| 51 | + | } | |
| 52 | + | fmt.printfln(" %dx%d @ %dHz", | |
| 53 | + | mode.size.width, mode.size.height, mode.refresh_rate) | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | fmt.println() | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | // Get primary monitor specifically | |
| 60 | + | primary := win.get_primary_monitor(loop) | |
| 61 | + | if primary != nil { | |
| 62 | + | name := win.get_monitor_name(primary) | |
| 63 | + | fmt.printfln("Primary monitor: %s", name) | |
| 64 | + | ||
| 65 | + | scale_x, scale_y := win.get_monitor_content_scale(loop, primary) | |
| 66 | + | fmt.printfln("Content scale: %.2f x %.2f", scale_x, scale_y) | |
| 67 | + | } | |
| 68 | + | } |
| 1 | + | package test_alloc | |
| 2 | + | ||
| 3 | + | import "core:fmt" | |
| 4 | + | import win "../window" | |
| 5 | + | ||
| 6 | + | main :: proc() { | |
| 7 | + | // Set up tracking allocator | |
| 8 | + | tracking: win.Tracking_Allocator | |
| 9 | + | win.tracking_allocator_init(&tracking, context.allocator) | |
| 10 | + | tracked := win.tracking_allocator_make(&tracking) | |
| 11 | + | ||
| 12 | + | fmt.println("Testing allocation tracking...") | |
| 13 | + | ||
| 14 | + | // Create and destroy event loop + window | |
| 15 | + | loop, err := win.create_event_loop(tracked) | |
| 16 | + | if err != .None { | |
| 17 | + | fmt.eprintln("Failed to create event loop:", err) | |
| 18 | + | win.tracking_allocator_print_stats(&tracking) | |
| 19 | + | return | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | window, win_err := win.create_window(loop, { | |
| 23 | + | title = "Allocation Test", | |
| 24 | + | size = {800, 600}, | |
| 25 | + | resizable = true, | |
| 26 | + | visible = true, | |
| 27 | + | decorated = true, | |
| 28 | + | }) | |
| 29 | + | if win_err != .None { | |
| 30 | + | fmt.eprintln("Failed to create window:", win_err) | |
| 31 | + | win.destroy_event_loop(loop) | |
| 32 | + | win.tracking_allocator_print_stats(&tracking) | |
| 33 | + | return | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | fmt.println("Window created successfully") | |
| 37 | + | fmt.println("Destroying window and event loop...") | |
| 38 | + | ||
| 39 | + | // Clean up | |
| 40 | + | win.destroy_event_loop(loop) | |
| 41 | + | ||
| 42 | + | // Print allocation statistics | |
| 43 | + | win.tracking_allocator_print_stats(&tracking) | |
| 44 | + | win.tracking_allocator_destroy(&tracking) | |
| 45 | + | } |
| 1 | + | package window_ops_example | |
| 2 | + | ||
| 3 | + | import "core:fmt" | |
| 4 | + | import win "../window" | |
| 5 | + | ||
| 6 | + | decorated := true | |
| 7 | + | maximized := false | |
| 8 | + | fullscreen := false | |
| 9 | + | resizable := true | |
| 10 | + | visible := true | |
| 11 | + | ||
| 12 | + | main :: proc() { | |
| 13 | + | fmt.println("Window Operations Demo") | |
| 14 | + | fmt.println("======================") | |
| 15 | + | ||
| 16 | + | loop, err := win.create_event_loop() | |
| 17 | + | if err != .None { | |
| 18 | + | fmt.eprintln("Failed to create event loop:", err) | |
| 19 | + | return | |
| 20 | + | } | |
| 21 | + | defer win.destroy_event_loop(loop) | |
| 22 | + | ||
| 23 | + | _, win_err := win.create_window(loop, { | |
| 24 | + | title = "Window Operations Demo", | |
| 25 | + | size = {800, 600}, | |
| 26 | + | resizable = true, | |
| 27 | + | visible = true, | |
| 28 | + | decorated = true, | |
| 29 | + | }) | |
| 30 | + | if win_err != .None { | |
| 31 | + | fmt.eprintln("Failed to create window:", win_err) | |
| 32 | + | return | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | fmt.println("\nControls:") | |
| 36 | + | fmt.println(" D - Toggle decorations") | |
| 37 | + | fmt.println(" M - Toggle maximize") | |
| 38 | + | fmt.println(" F - Toggle fullscreen") | |
| 39 | + | fmt.println(" R - Toggle resizable") | |
| 40 | + | fmt.println(" H - Hide/show window") | |
| 41 | + | fmt.println(" 1 - Set size to 400x300") | |
| 42 | + | fmt.println(" 2 - Set size to 800x600") | |
| 43 | + | fmt.println(" 3 - Set size to 1200x800") | |
| 44 | + | fmt.println(" Arrow Keys - Move window") | |
| 45 | + | fmt.println(" Space - Focus window") | |
| 46 | + | fmt.println(" I - Minimize window") | |
| 47 | + | fmt.println(" Escape - Exit") | |
| 48 | + | fmt.println("") | |
| 49 | + | ||
| 50 | + | win.run(loop, handle_event) | |
| 51 | + | ||
| 52 | + | fmt.println("Goodbye!") | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | handle_event :: proc(event: win.Event, loop: ^win.Event_Loop) -> win.Control_Flow { | |
| 56 | + | #partial switch e in event { | |
| 57 | + | case win.Window_Event_Data: | |
| 58 | + | // Get the first (and only) window | |
| 59 | + | window: ^win.Window = nil | |
| 60 | + | if len(loop.windows) > 0 { | |
| 61 | + | window = loop.windows[0] | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | #partial switch we in e.event { | |
| 65 | + | case win.Close_Requested: | |
| 66 | + | fmt.println("Exiting...") | |
| 67 | + | win.exit(loop) | |
| 68 | + | ||
| 69 | + | case win.Key_Input: | |
| 70 | + | if we.state == .Pressed && !we.repeat && window != nil { | |
| 71 | + | #partial switch we.key { | |
| 72 | + | case .D: | |
| 73 | + | decorated = !decorated | |
| 74 | + | win.set_decorations(window, decorated) | |
| 75 | + | fmt.printfln("Decorations: %v", decorated) | |
| 76 | + | ||
| 77 | + | case .M: | |
| 78 | + | maximized = !maximized | |
| 79 | + | win.set_maximized(window, maximized) | |
| 80 | + | fmt.printfln("Maximized: %v", maximized) | |
| 81 | + | ||
| 82 | + | case .F: | |
| 83 | + | fullscreen = !fullscreen | |
| 84 | + | if fullscreen { | |
| 85 | + | win.set_fullscreen(window, win.Fullscreen_Mode{}) | |
| 86 | + | } else { | |
| 87 | + | win.set_fullscreen(window, nil) | |
| 88 | + | } | |
| 89 | + | fmt.printfln("Fullscreen: %v", fullscreen) | |
| 90 | + | ||
| 91 | + | case .R: | |
| 92 | + | resizable = !resizable | |
| 93 | + | win.set_resizable(window, resizable) | |
| 94 | + | fmt.printfln("Resizable: %v", resizable) | |
| 95 | + | ||
| 96 | + | case .H: | |
| 97 | + | visible = !visible | |
| 98 | + | win.set_visible(window, visible) | |
| 99 | + | fmt.printfln("Visible: %v", visible) | |
| 100 | + | ||
| 101 | + | case .Key_1: | |
| 102 | + | win.set_size(window, {400, 300}) | |
| 103 | + | fmt.println("Set size to 400x300") | |
| 104 | + | ||
| 105 | + | case .Key_2: | |
| 106 | + | win.set_size(window, {800, 600}) | |
| 107 | + | fmt.println("Set size to 800x600") | |
| 108 | + | ||
| 109 | + | case .Key_3: | |
| 110 | + | win.set_size(window, {1200, 800}) | |
| 111 | + | fmt.println("Set size to 1200x800") | |
| 112 | + | ||
| 113 | + | case .Arrow_Up: | |
| 114 | + | pos := win.get_position(window) | |
| 115 | + | pos.y -= 50 | |
| 116 | + | win.set_position(window, pos) | |
| 117 | + | fmt.printfln("Moved to (%d, %d)", pos.x, pos.y) | |
| 118 | + | ||
| 119 | + | case .Arrow_Down: | |
| 120 | + | pos := win.get_position(window) | |
| 121 | + | pos.y += 50 | |
| 122 | + | win.set_position(window, pos) | |
| 123 | + | fmt.printfln("Moved to (%d, %d)", pos.x, pos.y) | |
| 124 | + | ||
| 125 | + | case .Arrow_Left: | |
| 126 | + | pos := win.get_position(window) | |
| 127 | + | pos.x -= 50 | |
| 128 | + | win.set_position(window, pos) | |
| 129 | + | fmt.printfln("Moved to (%d, %d)", pos.x, pos.y) | |
| 130 | + | ||
| 131 | + | case .Arrow_Right: | |
| 132 | + | pos := win.get_position(window) | |
| 133 | + | pos.x += 50 | |
| 134 | + | win.set_position(window, pos) | |
| 135 | + | fmt.printfln("Moved to (%d, %d)", pos.x, pos.y) | |
| 136 | + | ||
| 137 | + | case .Space: | |
| 138 | + | win.focus(window) | |
| 139 | + | fmt.println("Focusing window") | |
| 140 | + | ||
| 141 | + | case .I: | |
| 142 | + | win.set_minimized(window) | |
| 143 | + | fmt.println("Minimizing window") | |
| 144 | + | ||
| 145 | + | case .Escape: | |
| 146 | + | fmt.println("Exiting...") | |
| 147 | + | win.exit(loop) | |
| 148 | + | ||
| 149 | + | case: | |
| 150 | + | // Ignore other keys | |
| 151 | + | } | |
| 152 | + | } | |
| 153 | + | ||
| 154 | + | case win.Resized: | |
| 155 | + | fmt.printfln("Resized to %dx%d", we.size.width, we.size.height) | |
| 156 | + | ||
| 157 | + | case win.Moved: | |
| 158 | + | fmt.printfln("Moved to (%d, %d)", we.position.x, we.position.y) | |
| 159 | + | ||
| 160 | + | case win.Focused: | |
| 161 | + | fmt.println("Window focused") | |
| 162 | + | ||
| 163 | + | case win.Unfocused: | |
| 164 | + | fmt.println("Window unfocused") | |
| 165 | + | ||
| 166 | + | case: | |
| 167 | + | // Ignore other window events | |
| 168 | + | } | |
| 169 | + | ||
| 170 | + | case win.Loop_Exiting: | |
| 171 | + | fmt.println("Event loop exiting") | |
| 172 | + | ||
| 173 | + | case: | |
| 174 | + | // Ignore other events | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | return win.Wait{} | |
| 178 | + | } |
| 1 | + | # Implementation Reference | |
| 2 | + | ||
| 3 | + | Implementation details extracted from GLFW and winit for our Odin window library. | |
| 4 | + | ||
| 5 | + | --- | |
| 6 | + | ||
| 7 | + | ## GLFW Implementation Details | |
| 8 | + | ||
| 9 | + | ### Architecture Patterns | |
| 10 | + | - **Global state struct** containing all library state (initialized flag, platform vtable, error handling, window/cursor/monitor linked lists) | |
| 11 | + | - **Window struct** with linked list node (`next` pointer), settings/state flags, input state arrays, constraints, callbacks, and platform-specific state via macro embedding | |
| 12 | + | - **Monitor struct** with name, physical dimensions, video modes array, gamma ramp state | |
| 13 | + | - **Opaque handle pattern** - public API only sees pointers, never internal struct details | |
| 14 | + | - **Intrusive linked lists** for windows, cursors, errors - `next` pointer embedded in each structure | |
| 15 | + | ||
| 16 | + | ### Platform Abstraction | |
| 17 | + | - **Virtual function table** - `_GLFWplatform` struct contains ~60 function pointers for all platform operations | |
| 18 | + | - **Platform state macros** - `GLFW_X11_WINDOW_STATE`, `GLFW_WIN32_WINDOW_STATE` etc. embedded in window struct | |
| 19 | + | - **Conditional compilation** - empty stub macros when platform not compiled, same binary types across platforms | |
| 20 | + | - **Platform ID** stored in global state for runtime platform identification | |
| 21 | + | ||
| 22 | + | ### Window Management | |
| 23 | + | - **Hints system** - stateful configuration before creation (`glfwWindowHint()`, `glfwInitHint()`) | |
| 24 | + | - **Hints persist** until explicitly reset or new window created - allows adding new hints without breaking API | |
| 25 | + | - **Simple creation** - `glfwCreateWindow(width, height, title, monitor, share)` | |
| 26 | + | - **Monitor parameter** - NULL for windowed, valid monitor for fullscreen | |
| 27 | + | - **Share parameter** - NULL or reference to existing window for OpenGL context sharing | |
| 28 | + | - **Constraints** - min/max width/height, aspect ratio numerator/denominator | |
| 29 | + | ||
| 30 | + | ### Input Handling | |
| 31 | + | - **Key state array** - `keys[GLFW_KEY_LAST + 1]` stores PRESS/RELEASE/REPEAT | |
| 32 | + | - **Mouse state array** - `mouseButtons[GLFW_MOUSE_BUTTON_LAST + 1]` | |
| 33 | + | - **Sticky input modes** - persistent state capture even when callback missed | |
| 34 | + | - **Virtual cursor position** - software-tracked cursor for disabled cursor mode | |
| 35 | + | - **Raw mouse motion** - platform-specific unfiltered motion data | |
| 36 | + | - **Callback pointers** - 17 callbacks stored directly in window struct (null = no-op) | |
| 37 | + | ||
| 38 | + | ### Event Loop | |
| 39 | + | - `pollEvents()` - non-blocking, processes queue | |
| 40 | + | - `waitEvents()` - blocks until event | |
| 41 | + | - `waitEventsTimeout(t)` - blocks with timeout | |
| 42 | + | - `postEmptyEvent()` - force wake from wait | |
| 43 | + | - **Internal event functions** - `_glfwInputKey()`, `_glfwInputMouseClick()` etc. called by platform code | |
| 44 | + | ||
| 45 | + | ### API Design | |
| 46 | + | - **Error handling** - thread-local storage via TLS slots, last error only (not queued) | |
| 47 | + | - **Input mode configuration** - single function with mode enum (`GLFW_CURSOR`, `GLFW_STICKY_KEYS`, etc.) | |
| 48 | + | - **String hints** for platform extensions (`GLFW_X11_CLASS_NAME`, etc.) | |
| 49 | + | - **Callback registration** returns previous callback pointer | |
| 50 | + | - **User pointer** on windows and monitors for application data | |
| 51 | + | ||
| 52 | + | ### Platform Functions Required | |
| 53 | + | - init/terminate | |
| 54 | + | - Cursor: getCursorPos, setCursorPos, setCursorMode, createCursor, createStandardCursor, destroyCursor, setCursor | |
| 55 | + | - Input: getScancodeName, getKeyScancode, setClipboardString, getClipboardString, rawMouseMotionSupported | |
| 56 | + | - Monitor: freeMonitor, getMonitorPos, getMonitorContentScale, getMonitorWorkarea, getVideoModes, getVideoMode, getGammaRamp, setGammaRamp | |
| 57 | + | - Window: createWindow, destroyWindow, setWindowTitle, setWindowIcon, get/setWindowPos, get/setWindowSize, setWindowSizeLimits, setWindowAspectRatio, getFramebufferSize, getWindowFrameSize, getWindowContentScale, iconifyWindow, restoreWindow, maximizeWindow, showWindow, hideWindow, requestWindowAttention, focusWindow, setWindowMonitor | |
| 58 | + | - Window state: windowFocused, windowIconified, windowVisible, windowMaximized, windowHovered, framebufferTransparent, getWindowOpacity, setWindowResizable, setWindowDecorated, setWindowFloating, setWindowOpacity, setWindowMousePassthrough | |
| 59 | + | - Events: pollEvents, waitEvents, waitEventsTimeout, postEmptyEvent | |
| 60 | + | ||
| 61 | + | --- | |
| 62 | + | ||
| 63 | + | ## Winit Implementation Details | |
| 64 | + | ||
| 65 | + | ### Architecture Patterns | |
| 66 | + | - **Trait-based abstraction** - `ActiveEventLoop`, `Window`, `ApplicationHandler` traits | |
| 67 | + | - **Enum-based event system** - discriminated unions for events (40+ WindowEvent variants) | |
| 68 | + | - **Separate crates per platform** - winit-core (shared), winit-x11, winit-wayland, winit-win32, etc. | |
| 69 | + | - **Runtime backend selection** on Linux - check `WAYLAND_DISPLAY` then `DISPLAY` env vars | |
| 70 | + | - **Window IDs** - distinct u64 identifiers for multi-window handling | |
| 71 | + | ||
| 72 | + | ### ApplicationHandler Callbacks | |
| 73 | + | - `can_create_surfaces()` - lifecycle point for surface creation (critical for Android) | |
| 74 | + | - `window_event(event_loop, window_id, event)` - per-window events | |
| 75 | + | - `device_event(event_loop, device_id, event)` - raw input events | |
| 76 | + | - `about_to_wait()` - pre-blocking hook | |
| 77 | + | - `new_events(cause)` - start of event batch | |
| 78 | + | - `proxy_wake_up()` - inter-thread signaling | |
| 79 | + | ||
| 80 | + | ### Window Attributes (Builder Pattern) | |
| 81 | + | - surface_size, min_surface_size, max_surface_size, surface_resize_increments | |
| 82 | + | - position | |
| 83 | + | - resizable | |
| 84 | + | - enabled_buttons (bitflags for min/max/close) | |
| 85 | + | - title | |
| 86 | + | - maximized, visible, transparent, blur, decorations | |
| 87 | + | - window_icon | |
| 88 | + | - preferred_theme (Light/Dark) | |
| 89 | + | - content_protected | |
| 90 | + | - window_level (Normal, AlwaysOnTop, AlwaysOnBottom) | |
| 91 | + | - active | |
| 92 | + | - cursor | |
| 93 | + | - parent_window | |
| 94 | + | - fullscreen (Borderless or Exclusive with video mode) | |
| 95 | + | - platform (boxed trait object for platform-specific extensions) | |
| 96 | + | ||
| 97 | + | ### Keyboard Input Hierarchy | |
| 98 | + | 1. Physical scancode (XKB keycode, Windows scancode) | |
| 99 | + | 2. Platform-native NativeKeyCode: `Xkb(u32)`, `Windows(u16)`, `MacOS(u16)` | |
| 100 | + | 3. Logical key via NativeKey (keysym, vkcode) | |
| 101 | + | 4. Named key enum: Enter, Escape, ArrowUp, F1-F24, etc. | |
| 102 | + | ||
| 103 | + | ### Key Modifiers | |
| 104 | + | - Shift, Control, Alt, Super (meta/win/cmd) | |
| 105 | + | - Separate left/right tracking | |
| 106 | + | - CapsLock, NumLock state | |
| 107 | + | ||
| 108 | + | ### Pointer Events | |
| 109 | + | - `PointerMoved { position, primary, source }` | |
| 110 | + | - `PointerEntered { position, primary, kind }` | |
| 111 | + | - `PointerLeft { position, primary, kind }` | |
| 112 | + | - `PointerButton { state, position, button }` | |
| 113 | + | - `MouseWheel { delta: LineDelta | PixelDelta, phase }` | |
| 114 | + | - Primary vs secondary pointer for multi-touch | |
| 115 | + | ||
| 116 | + | ### Event Loop Control Flow | |
| 117 | + | - `Poll` - continuous polling (games) | |
| 118 | + | - `Wait` - block until events (UI apps, most efficient) | |
| 119 | + | - `WaitUntil(instant)` - block with timeout | |
| 120 | + | ||
| 121 | + | ### StartCause (Why event loop woke) | |
| 122 | + | - `Init` - first iteration | |
| 123 | + | - `Poll` - Poll control flow triggered | |
| 124 | + | - `WaitCancelled { start, requested_resume }` - new events interrupted wait | |
| 125 | + | - `ResumeTimeReached { start, requested_resume }` - WaitUntil timeout fired | |
| 126 | + | ||
| 127 | + | ### Window Events (Key Variants) | |
| 128 | + | - SurfaceResized, Moved, CloseRequested, Destroyed | |
| 129 | + | - Focused(bool), Occluded(bool) | |
| 130 | + | - KeyboardInput { device_id, event, is_synthetic } | |
| 131 | + | - ModifiersChanged | |
| 132 | + | - PointerMoved, PointerEntered, PointerLeft, PointerButton | |
| 133 | + | - MouseWheel, AxisMotion | |
| 134 | + | - Touch, TouchpadPressure | |
| 135 | + | - PinchGesture, PanGesture, DoubleTapGesture, RotationGesture | |
| 136 | + | - DragEntered, DragMoved, DragDropped, DragLeft | |
| 137 | + | - ScaleFactorChanged { scale_factor, surface_size_writer } | |
| 138 | + | - ThemeChanged | |
| 139 | + | - Ime(Enabled, Preedit, Commit, Disabled) | |
| 140 | + | - RedrawRequested | |
| 141 | + | ||
| 142 | + | ### Coordinate Systems | |
| 143 | + | - **Surface coordinates** - relative to drawable area (0,0 at content top-left) | |
| 144 | + | - **Window coordinates** - relative to window including decorations | |
| 145 | + | - **Desktop coordinates** - absolute screen position | |
| 146 | + | - Methods: `surface_size()`, `outer_size()`, `surface_position()`, `outer_position()` | |
| 147 | + | ||
| 148 | + | ### DPI/Scaling | |
| 149 | + | - `scale_factor()` on Window | |
| 150 | + | - `ScaleFactorChanged` event when DPI changes | |
| 151 | + | - Physical and Logical types that auto-convert | |
| 152 | + | - Fractional scaling support (Wayland) | |
| 153 | + | ||
| 154 | + | ### Monitor API | |
| 155 | + | - `available_monitors()` - iterator of MonitorHandle | |
| 156 | + | - `primary_monitor()` - Option<MonitorHandle> | |
| 157 | + | - MonitorHandle: id, native_id, name, position, scale_factor, video_modes, current_video_mode | |
| 158 | + | ||
| 159 | + | ### Cross-Thread Communication | |
| 160 | + | - `EventLoopProxy::wake_up()` - coalesced (multiple calls = 1 proxy_wake_up event) | |
| 161 | + | - Used for background threads signaling work completion | |
| 162 | + | ||
| 163 | + | ### Raw Window Handles | |
| 164 | + | - `raw_window_handle()` → RawWindowHandle for Vulkan/OpenGL/Metal/DirectX | |
| 165 | + | - `raw_display_handle()` → DisplayHandle for display connection | |
| 166 | + | ||
| 167 | + | ### X11-Specific | |
| 168 | + | - Window types: Desktop, Dock, Toolbar, Menu, Utility, Splash, Dialog, DropdownMenu, PopupMenu, Tooltip, Notification, Combo, Dnd, Normal | |
| 169 | + | - XAtom handling | |
| 170 | + | - Drag & drop (XDND protocol) | |
| 171 | + | - Input method integration | |
| 172 | + | - XSettings for desktop settings | |
| 173 | + | ||
| 174 | + | ### Wayland-Specific | |
| 175 | + | - Client-side decorations (CSD) option | |
| 176 | + | - xdg_toplevel access | |
| 177 | + | - Activation tokens | |
| 178 | + | - Fractional scaling protocol | |
| 179 | + | - Tablet input v2 | |
| 180 | + | ||
| 181 | + | --- | |
| 182 | + | ||
| 183 | + | ## Current Implementation Status | |
| 184 | + | ||
| 185 | + | ### Core Data Structures | |
| 186 | + | - [x] Global library state (`Platform_Event_Loop` in `platform_linux.odin`) | |
| 187 | + | - [x] Window struct (`Platform_Window` with union of X11/Wayland data) | |
| 188 | + | - [x] Monitor struct (`Monitor_Info` with id, name, dimensions, modes, position, scale) | |
| 189 | + | - [x] Event enum (discriminated union: `Window_Event`, `Device_Event`, `Loop_Event`) | |
| 190 | + | ||
| 191 | + | ### Window Management | |
| 192 | + | - [x] Window creation with attributes (`create_window()` + `Window_Attributes`) | |
| 193 | + | - [x] Window destruction (`destroy_window()`) | |
| 194 | + | - [x] Title (`set_title()`) | |
| 195 | + | - [ ] Window icon | |
| 196 | + | - [x] Position (`outer_position()`, `set_outer_position()`, `set_position()`, `get_position()`) | |
| 197 | + | - [x] Size (`surface_size()`, `request_surface_size()`, `outer_size()`, `set_size()`) | |
| 198 | + | - [x] Min/max size constraints (`set_min_surface_size()`, `set_max_surface_size()`) | |
| 199 | + | - [ ] Aspect ratio constraints | |
| 200 | + | - [x] Visible (`set_visible()`, `is_visible()`) | |
| 201 | + | - [x] Focused (`focus()`, `is_focused()`) | |
| 202 | + | - [x] Maximized (`set_maximized()`, `is_maximized()` - state tracking TODO) | |
| 203 | + | - [x] Minimized (`set_minimized()`, `is_minimized()` - state tracking TODO) | |
| 204 | + | - [x] Fullscreen (`set_fullscreen()`, `get_fullscreen()` - borderless + exclusive) | |
| 205 | + | - [x] Decorations (`set_decorations()`, `is_decorated()` - state tracking TODO) | |
| 206 | + | - [x] Resizable (`set_resizable()`, `is_resizable()` - state tracking TODO) | |
| 207 | + | - [ ] Floating/always-on-top | |
| 208 | + | - [ ] Mouse passthrough | |
| 209 | + | - [ ] Opacity | |
| 210 | + | - [~] Content scale / DPI (`scale_factor()` - hardcoded 1.0, detection TODO) | |
| 211 | + | ||
| 212 | + | ### Input Handling | |
| 213 | + | - [x] Keyboard input with scancodes (`Key_Input` event, `Key_Code` enum 126+ keys) | |
| 214 | + | - [ ] Key state array for polling | |
| 215 | + | - [x] Modifier keys tracking (`Modifiers` struct: shift, ctrl, alt, super) | |
| 216 | + | - [x] Mouse button input (`Mouse_Button_Input` event) | |
| 217 | + | - [x] Cursor position (`Pointer_Moved` event with position) | |
| 218 | + | - [x] Scroll wheel (`Mouse_Wheel` event with line + pixel deltas) | |
| 219 | + | - [ ] Cursor modes (normal, hidden, disabled/grabbed) | |
| 220 | + | - [ ] Raw mouse motion (relative pointer protocol) | |
| 221 | + | - [ ] Cursor shapes (standard cursors) | |
| 222 | + | - [~] Key repeat detection (TODO in both X11 and Wayland) | |
| 223 | + | ||
| 224 | + | ### Event System | |
| 225 | + | - [x] Poll events (`poll()` - non-blocking) | |
| 226 | + | - [x] Wait events (`run()` with `Wait` control flow - blocking) | |
| 227 | + | - [x] Wait events with timeout (`Wait_Until` control flow) | |
| 228 | + | - [ ] Post empty event / wake from wait (TODO in code) | |
| 229 | + | - [x] Callback-based design (winit-style `Event_Handler` callback) | |
| 230 | + | - [x] Start cause tracking (`Init`, `Poll`, `Wait_Cancelled`, `Resume_Time_Reached`) | |
| 231 | + | - [x] Loop events (`New_Events`, `About_To_Wait`, `Loop_Exiting`) | |
| 232 | + | ||
| 233 | + | ### Monitor/Display | |
| 234 | + | - [x] Enumerate monitors (`get_monitors()`) | |
| 235 | + | - [x] Primary monitor (`get_primary_monitor()`) | |
| 236 | + | - [x] Monitor name (`get_monitor_name()`) | |
| 237 | + | - [x] Monitor position (`get_monitor_position()`) | |
| 238 | + | - [x] Monitor workarea (`get_monitor_workarea()`) | |
| 239 | + | - [x] Physical size (`get_monitor_physical_size()`) | |
| 240 | + | - [x] Content scale (`get_monitor_content_scale()`) | |
| 241 | + | - [x] Video modes enumeration (`get_video_modes()`) | |
| 242 | + | - [x] Current video mode (`get_current_video_mode()`) | |
| 243 | + | - [x] Combined info (`get_monitor_info()`) | |
| 244 | + | ||
| 245 | + | ### Platform Abstraction | |
| 246 | + | - [x] Platform-specific state embedded in structs (union in `Platform_Window`) | |
| 247 | + | - [x] Runtime platform selection (X11 vs Wayland via `select_backend()`) | |
| 248 | + | - [x] Environment variable checks (`WAYLAND_DISPLAY`, `ODIN_WINDOW_BACKEND`) | |
| 249 | + | - [x] X11 backend (complete) | |
| 250 | + | - [x] Wayland backend (functional - window creation, input, monitors, decorations) | |
| 251 | + | ||
| 252 | + | ### Raw Handle Support | |
| 253 | + | - [x] `raw_window_handle()` - X11 window ID or Wayland wl_surface | |
| 254 | + | - [x] `raw_display_handle()` - X11 display or Wayland wl_display | |
| 255 | + | ||
| 256 | + | ### API Design (Implemented Choices) | |
| 257 | + | - [x] Distinct handle types (`Window_Id`, `Device_Id`, `Monitor_Handle`) | |
| 258 | + | - [x] Attributes struct pattern (not hints) | |
| 259 | + | - [x] Callback-based events (winit-style) | |
| 260 | + | - [ ] User pointer on objects | |
| 261 | + | - [x] Error handling via `Error` enum return values | |
| 262 | + | - [ ] Thread-local error storage | |
| 263 | + | ||
| 264 | + | --- | |
| 265 | + | ||
| 266 | + | ## Not Yet Implemented | |
| 267 | + | ||
| 268 | + | ### Window Features | |
| 269 | + | - Window icon | |
| 270 | + | - Aspect ratio constraints | |
| 271 | + | - Floating/always-on-top window level | |
| 272 | + | - Mouse passthrough | |
| 273 | + | - Window opacity | |
| 274 | + | - Proper DPI detection (currently hardcoded to 1.0) | |
| 275 | + | - State tracking for maximized/minimized/resizable/decorated flags | |
| 276 | + | ||
| 277 | + | ### Input Features | |
| 278 | + | - Key state array for polling (currently event-only) | |
| 279 | + | - Cursor visibility modes (hidden, disabled/grabbed) | |
| 280 | + | - Raw mouse motion (Wayland relative-pointer protocol) | |
| 281 | + | - Standard cursor shapes | |
| 282 | + | - Key repeat detection (marked TODO) | |
| 283 | + | - Event loop wake-up mechanism (marked TODO) | |
| 284 | + | ||
| 285 | + | ### Advanced Features | |
| 286 | + | - Clipboard access (copy/paste) | |
| 287 | + | - Custom cursor images | |
| 288 | + | - Drag & drop | |
| 289 | + | - Input method (IME) integration | |
| 290 | + | - Touch input | |
| 291 | + | - Gamepad/joystick | |
| 292 | + | - Pointer gestures (pinch, pan, rotate) | |
| 293 | + | - Tablet input | |
| 294 | + | ||
| 295 | + | ### Wayland-Specific Incomplete | |
| 296 | + | - Exclusive fullscreen with specific video mode | |
| 297 | + | - Pointer constraints (cursor locking) | |
| 298 | + | - Activation tokens | |
| 299 | + | - Fractional scaling (protocol exists, not wired through) |
| 1 | + | # Odin Windowing Library Implementation Plan | |
| 2 | + | ||
| 3 | + | ## Overview | |
| 4 | + | ||
| 5 | + | A cross-platform windowing library for Odin with: | |
| 6 | + | - **Event-driven architecture** (inspired by winit) | |
| 7 | + | - **Minimal API surface** (inspired by GLFW) | |
| 8 | + | - **No rendering management** - users bring their own Vulkan/OpenGL | |
| 9 | + | - **Single-threaded** event loop on main thread | |
| 10 | + | ||
| 11 | + | **Initial scope**: Linux (X11 + Wayland), windows, input, monitors, fullscreen | |
| 12 | + | ||
| 13 | + | --- | |
| 14 | + | ||
| 15 | + | ## Architecture | |
| 16 | + | ||
| 17 | + | ### File Structure | |
| 18 | + | ||
| 19 | + | ``` | |
| 20 | + | window/ | |
| 21 | + | ├── window.odin # Main package, re-exports public API | |
| 22 | + | ├── types.odin # Event union, Control_Flow, handles | |
| 23 | + | ├── event_loop.odin # Event_Loop and run procedures | |
| 24 | + | ├── window_api.odin # Window struct and operations | |
| 25 | + | ├── monitor.odin # Monitor enumeration and info | |
| 26 | + | ├── input.odin # Key codes, mouse buttons, modifiers | |
| 27 | + | ├── error.odin # Error types | |
| 28 | + | │ | |
| 29 | + | ├── platform/ | |
| 30 | + | │ ├── platform.odin # Platform vtable and dispatch | |
| 31 | + | │ ├── backend.odin # Backend selection (X11 vs Wayland) | |
| 32 | + | │ │ | |
| 33 | + | │ ├── x11/ | |
| 34 | + | │ │ ├── x11.odin | |
| 35 | + | │ │ ├── x11_window.odin | |
| 36 | + | │ │ ├── x11_event_loop.odin | |
| 37 | + | │ │ └── x11_monitor.odin | |
| 38 | + | │ │ | |
| 39 | + | │ └── wayland/ | |
| 40 | + | │ ├── wayland.odin | |
| 41 | + | │ ├── wayland_window.odin | |
| 42 | + | │ ├── wayland_event_loop.odin | |
| 43 | + | │ └── wayland_monitor.odin | |
| 44 | + | │ | |
| 45 | + | └── examples/ | |
| 46 | + | ├── basic.odin | |
| 47 | + | └── fullscreen.odin | |
| 48 | + | ``` | |
| 49 | + | ||
| 50 | + | --- | |
| 51 | + | ||
| 52 | + | ## Core Types | |
| 53 | + | ||
| 54 | + | ### Event System (Discriminated Unions) | |
| 55 | + | ||
| 56 | + | ```odin | |
| 57 | + | // Top-level event | |
| 58 | + | Event :: union { | |
| 59 | + | Window_Event_Data, // Events for a specific window | |
| 60 | + | Device_Event_Data, // Raw input events | |
| 61 | + | New_Events, // Start of event batch | |
| 62 | + | About_To_Wait, // Before blocking | |
| 63 | + | Loop_Exiting, // Event loop shutting down | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // Window events | |
| 67 | + | Window_Event :: union { | |
| 68 | + | Close_Requested, Destroyed, Resized, Moved, | |
| 69 | + | Focused, Unfocused, Key_Input, Char_Input, | |
| 70 | + | Pointer_Moved, Pointer_Entered, Pointer_Left, | |
| 71 | + | Mouse_Button_Input, Mouse_Wheel, | |
| 72 | + | Scale_Factor_Changed, Redraw_Requested, | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // Control flow (how event loop behaves after processing) | |
| 76 | + | Control_Flow :: union { | |
| 77 | + | Poll, // Continuous polling (games) | |
| 78 | + | Wait, // Block until events (UI apps) | |
| 79 | + | Wait_Until, // Block with timeout | |
| 80 | + | } | |
| 81 | + | ``` | |
| 82 | + | ||
| 83 | + | ### Handles | |
| 84 | + | ||
| 85 | + | ```odin | |
| 86 | + | Window_Id :: distinct u64 | |
| 87 | + | Monitor_Handle :: distinct rawptr | |
| 88 | + | ||
| 89 | + | Physical_Size :: struct { width, height: u32 } | |
| 90 | + | Physical_Position :: struct { x, y: i32 } | |
| 91 | + | ``` | |
| 92 | + | ||
| 93 | + | ### Window Attributes | |
| 94 | + | ||
| 95 | + | ```odin | |
| 96 | + | Window_Attributes :: struct { | |
| 97 | + | title: string, | |
| 98 | + | size: Physical_Size, | |
| 99 | + | min_size: Maybe(Physical_Size), | |
| 100 | + | max_size: Maybe(Physical_Size), | |
| 101 | + | position: Maybe(Physical_Position), | |
| 102 | + | resizable: bool, | |
| 103 | + | visible: bool, | |
| 104 | + | decorated: bool, | |
| 105 | + | maximized: bool, | |
| 106 | + | fullscreen: Maybe(Fullscreen_Mode), | |
| 107 | + | } | |
| 108 | + | ``` | |
| 109 | + | ||
| 110 | + | --- | |
| 111 | + | ||
| 112 | + | ## API Design | |
| 113 | + | ||
| 114 | + | ### Event Loop | |
| 115 | + | ||
| 116 | + | ```odin | |
| 117 | + | // Creation | |
| 118 | + | create_event_loop :: proc(allocator := context.allocator) -> (^Event_Loop, Error) | |
| 119 | + | destroy_event_loop :: proc(loop: ^Event_Loop) | |
| 120 | + | ||
| 121 | + | // Running | |
| 122 | + | run :: proc(loop: ^Event_Loop, handler: Event_Handler) -> Error | |
| 123 | + | poll :: proc(loop: ^Event_Loop, handler: Event_Handler) -> bool | |
| 124 | + | exit :: proc(loop: ^Event_Loop) | |
| 125 | + | ||
| 126 | + | // User callback type | |
| 127 | + | Event_Handler :: #type proc(event: Event, loop: ^Event_Loop) -> Control_Flow | |
| 128 | + | ``` | |
| 129 | + | ||
| 130 | + | ### Window | |
| 131 | + | ||
| 132 | + | ```odin | |
| 133 | + | // Lifecycle | |
| 134 | + | create_window :: proc(loop: ^Event_Loop, attrs: Window_Attributes) -> (^Window, Error) | |
| 135 | + | destroy_window :: proc(win: ^Window) | |
| 136 | + | ||
| 137 | + | // Size and position | |
| 138 | + | surface_size :: proc(win: ^Window) -> Physical_Size | |
| 139 | + | request_surface_size :: proc(win: ^Window, size: Physical_Size) | |
| 140 | + | outer_position :: proc(win: ^Window) -> Physical_Position | |
| 141 | + | set_outer_position :: proc(win: ^Window, pos: Physical_Position) | |
| 142 | + | ||
| 143 | + | // State | |
| 144 | + | set_title :: proc(win: ^Window, title: string) | |
| 145 | + | set_visible :: proc(win: ^Window, visible: bool) | |
| 146 | + | set_fullscreen :: proc(win: ^Window, mode: Maybe(Fullscreen_Mode)) | |
| 147 | + | request_redraw :: proc(win: ^Window) | |
| 148 | + | ||
| 149 | + | // Raw handle for rendering APIs | |
| 150 | + | raw_window_handle :: proc(win: ^Window) -> Raw_Window_Handle | |
| 151 | + | ``` | |
| 152 | + | ||
| 153 | + | ### Monitor | |
| 154 | + | ||
| 155 | + | ```odin | |
| 156 | + | available_monitors :: proc(loop: ^Event_Loop) -> []Monitor_Handle | |
| 157 | + | primary_monitor :: proc(loop: ^Event_Loop) -> Maybe(Monitor_Handle) | |
| 158 | + | monitor_info :: proc(handle: Monitor_Handle) -> Monitor_Info | |
| 159 | + | video_modes :: proc(handle: Monitor_Handle) -> []Video_Mode | |
| 160 | + | current_video_mode :: proc(handle: Monitor_Handle) -> Video_Mode | |
| 161 | + | ``` | |
| 162 | + | ||
| 163 | + | --- | |
| 164 | + | ||
| 165 | + | ## Platform Abstraction | |
| 166 | + | ||
| 167 | + | ### Vtable Pattern | |
| 168 | + | ||
| 169 | + | ```odin | |
| 170 | + | Platform_Vtable :: struct { | |
| 171 | + | // Event loop operations | |
| 172 | + | init: proc(loop: ^Platform_Event_Loop) -> Error, | |
| 173 | + | terminate: proc(loop: ^Platform_Event_Loop), | |
| 174 | + | pump_events: proc(loop: ^Platform_Event_Loop, ...), | |
| 175 | + | wait_events: proc(loop: ^Platform_Event_Loop), | |
| 176 | + | wake_up: proc(loop: ^Platform_Event_Loop), | |
| 177 | + | ||
| 178 | + | // Window operations | |
| 179 | + | create_window: proc(...) -> Error, | |
| 180 | + | destroy_window: proc(...), | |
| 181 | + | // ... 20+ window operations | |
| 182 | + | ||
| 183 | + | // Monitor operations | |
| 184 | + | get_monitors: proc(...) -> []Monitor_Handle, | |
| 185 | + | // ... monitor operations | |
| 186 | + | } | |
| 187 | + | ``` | |
| 188 | + | ||
| 189 | + | ### Backend Selection | |
| 190 | + | ||
| 191 | + | ```odin | |
| 192 | + | select_backend :: proc() -> Backend { | |
| 193 | + | if os.getenv("WAYLAND_DISPLAY") != "" { | |
| 194 | + | return .Wayland | |
| 195 | + | } | |
| 196 | + | return .X11 | |
| 197 | + | } | |
| 198 | + | ``` | |
| 199 | + | ||
| 200 | + | --- | |
| 201 | + | ||
| 202 | + | ## Example Usage | |
| 203 | + | ||
| 204 | + | ```odin | |
| 205 | + | package main | |
| 206 | + | import "window" | |
| 207 | + | ||
| 208 | + | main :: proc() { | |
| 209 | + | loop, _ := window.create_event_loop() | |
| 210 | + | defer window.destroy_event_loop(loop) | |
| 211 | + | ||
| 212 | + | attrs := window.default_window_attributes() | |
| 213 | + | attrs.title = "My App" | |
| 214 | + | win, _ := window.create_window(loop, attrs) | |
| 215 | + | ||
| 216 | + | window.run(loop, proc(event: window.Event, loop: ^window.Event_Loop) -> window.Control_Flow { | |
| 217 | + | switch e in event { | |
| 218 | + | case window.Window_Event_Data: | |
| 219 | + | switch we in e.event { | |
| 220 | + | case window.Close_Requested: | |
| 221 | + | window.exit(loop) | |
| 222 | + | case window.Redraw_Requested: | |
| 223 | + | // Render here | |
| 224 | + | } | |
| 225 | + | } | |
| 226 | + | return window.Wait{} | |
| 227 | + | }) | |
| 228 | + | } | |
| 229 | + | ``` | |
| 230 | + | ||
| 231 | + | --- | |
| 232 | + | ||
| 233 | + | ## Implementation Phases | |
| 234 | + | ||
| 235 | + | ### Phase 1: Core Foundation | |
| 236 | + | - Package structure with stub files | |
| 237 | + | - Core types (Event, Control_Flow, Window_Id) | |
| 238 | + | - Basic Event_Loop with X11 backend | |
| 239 | + | - Create window, handle close/resize events | |
| 240 | + | - **Test**: Window that responds to close button | |
| 241 | + | ||
| 242 | + | ### Phase 2: Input Events | |
| 243 | + | - Key events with scancode mapping | |
| 244 | + | - Mouse button/motion/wheel events | |
| 245 | + | - Modifier key tracking | |
| 246 | + | - **Test**: Print all input events | |
| 247 | + | ||
| 248 | + | ### Phase 3: Window Operations | |
| 249 | + | - Resize, move, size constraints | |
| 250 | + | - Visibility, decorations, resizable toggle | |
| 251 | + | - Maximize/minimize, focus | |
| 252 | + | - **Test**: Interactive window management demo | |
| 253 | + | ||
| 254 | + | ### Phase 4: Monitor API | |
| 255 | + | - Monitor enumeration and properties | |
| 256 | + | - Video modes | |
| 257 | + | - Borderless and exclusive fullscreen | |
| 258 | + | - **Test**: Fullscreen toggle demo | |
| 259 | + | ||
| 260 | + | ### Phase 5: Event Loop Polish | |
| 261 | + | - Wait and WaitUntil control flow | |
| 262 | + | - wake_up() for cross-thread signaling | |
| 263 | + | - Redraw request batching | |
| 264 | + | - **Test**: Benchmark control flow modes | |
| 265 | + | ||
| 266 | + | ### Phase 6: Wayland Backend | |
| 267 | + | - Wayland connection/display | |
| 268 | + | - xdg_surface window creation | |
| 269 | + | - Port all operations to Wayland | |
| 270 | + | - Handle CSD and Wayland quirks | |
| 271 | + | - **Test**: All tests pass on Wayland | |
| 272 | + | ||
| 273 | + | ### Phase 7: DPI/Scale Factor | |
| 274 | + | - Scale factor detection | |
| 275 | + | - Scale_Factor_Changed events | |
| 276 | + | - **Test**: Correct rendering at different DPIs | |
| 277 | + | ||
| 278 | + | ### Phase 8: Documentation & Examples | |
| 279 | + | - API documentation | |
| 280 | + | - Examples: basic, input, fullscreen, multi-window, Vulkan | |
| 281 | + | - Performance testing | |
| 282 | + | ||
| 283 | + | --- | |
| 284 | + | ||
| 285 | + | ## Verification | |
| 286 | + | ||
| 287 | + | After each phase: | |
| 288 | + | 1. Build with `odin build .` | |
| 289 | + | 2. Run example programs | |
| 290 | + | 3. Test on both X11 and Wayland (after Phase 6) | |
| 291 | + | 4. Check for memory leaks with allocator tracking |
| 1 | + | # Window | |
| 2 | + |
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.
Inline diff hidden to keep this page fast.