| 1 | @[has_globals] |
| 2 | module sapp |
| 3 | |
| 4 | // Linux-specific state structs for the V sokol_app backend. |
| 5 | // Shared state (SappTiming, SappMouse, etc.) is in sapp_state.v. |
| 6 | |
| 7 | // X11 and XKB share keypad/navigation keysyms. Keep the stateful keypad mapping in one place so |
| 8 | // Num Lock can flip keypad arrows between navigation keys and keypad digits consistently. |
| 9 | fn linux_translate_navigation_or_keypad_keysym(keysym u32) KeyCode { |
| 10 | return match keysym { |
| 11 | 0xff63, 0xff9e { .insert } |
| 12 | 0xffff, 0xff9f { .delete } |
| 13 | 0xff50, 0xff95 { .home } |
| 14 | 0xff57, 0xff9c { .end } |
| 15 | 0xff55, 0xff9a { .page_up } |
| 16 | 0xff56, 0xff9b { .page_down } |
| 17 | 0xff51, 0xff96 { .left } |
| 18 | 0xff53, 0xff98 { .right } |
| 19 | 0xff52, 0xff97 { .up } |
| 20 | 0xff54, 0xff99 { .down } |
| 21 | 0xffb0 { .kp_0 } |
| 22 | 0xffb1 { .kp_1 } |
| 23 | 0xffb2 { .kp_2 } |
| 24 | 0xffb3 { .kp_3 } |
| 25 | 0xff9d, 0xffb5 { .kp_5 } |
| 26 | 0xffb4 { .kp_4 } |
| 27 | 0xffb6 { .kp_6 } |
| 28 | 0xffb7 { .kp_7 } |
| 29 | 0xffb8 { .kp_8 } |
| 30 | 0xffb9 { .kp_9 } |
| 31 | 0xffac, 0xffae { .kp_decimal } |
| 32 | 0xffaf { .kp_divide } |
| 33 | 0xffaa { .kp_multiply } |
| 34 | 0xffad { .kp_subtract } |
| 35 | 0xffab { .kp_add } |
| 36 | 0xff8d { .kp_enter } |
| 37 | 0xffbd { .kp_equal } |
| 38 | else { .invalid } |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | // Wayland-specific state (only compiled when -d sokol_wayland is used) |
| 43 | $if sokol_wayland ? { |
| 44 | struct SappWayland { |
| 45 | mut: |
| 46 | // Core Wayland objects |
| 47 | display &C.wl_display = unsafe { nil } |
| 48 | registry &C.wl_registry = unsafe { nil } |
| 49 | compositor &C.wl_compositor = unsafe { nil } |
| 50 | surface &C.wl_surface = unsafe { nil } |
| 51 | egl_window &C.wl_egl_window = unsafe { nil } |
| 52 | shm &C.wl_shm = unsafe { nil } |
| 53 | // XDG shell |
| 54 | xdg_wm_base &C.xdg_wm_base = unsafe { nil } |
| 55 | xdg_surface &C.xdg_surface = unsafe { nil } |
| 56 | xdg_toplevel &C.xdg_toplevel = unsafe { nil } |
| 57 | // Input |
| 58 | seat &C.wl_seat = unsafe { nil } |
| 59 | pointer &C.wl_pointer = unsafe { nil } |
| 60 | keyboard &C.wl_keyboard = unsafe { nil } |
| 61 | touch &C.wl_touch = unsafe { nil } |
| 62 | seat_version u32 |
| 63 | // Pointer constraints |
| 64 | pointer_constraints &C.zwp_pointer_constraints_v1 = unsafe { nil } |
| 65 | locked_pointer &C.zwp_locked_pointer_v1 = unsafe { nil } |
| 66 | relative_pointer_mgr &C.zwp_relative_pointer_manager_v1 = unsafe { nil } |
| 67 | relative_pointer &C.zwp_relative_pointer_v1 = unsafe { nil } |
| 68 | // XKB keyboard handling |
| 69 | xkb_context &C.xkb_context = unsafe { nil } |
| 70 | xkb_keymap &C.xkb_keymap = unsafe { nil } |
| 71 | xkb_state &C.xkb_state = unsafe { nil } |
| 72 | xkb_compose_table &C.xkb_compose_table = unsafe { nil } |
| 73 | xkb_compose_state &C.xkb_compose_state = unsafe { nil } |
| 74 | // Cursor |
| 75 | cursor_shape_manager &C.wp_cursor_shape_manager_v1 = unsafe { nil } |
| 76 | cursor_shape_device &C.wp_cursor_shape_device_v1 = unsafe { nil } |
| 77 | cursor_theme &C.wl_cursor_theme = unsafe { nil } |
| 78 | cursor_default &C.wl_cursor = unsafe { nil } |
| 79 | cursor_surface &C.wl_surface = unsafe { nil } |
| 80 | cursor_size int |
| 81 | // Fractional scale |
| 82 | fractional_scale_mgr &C.wp_fractional_scale_manager_v1 = unsafe { nil } |
| 83 | fractional_scale &C.wp_fractional_scale_v1 = unsafe { nil } |
| 84 | scale_numerator u32 |
| 85 | // Viewporter |
| 86 | viewporter &C.wp_viewporter = unsafe { nil } |
| 87 | viewport &C.wp_viewport = unsafe { nil } |
| 88 | // Decorations |
| 89 | decoration_manager &C.zxdg_decoration_manager_v1 = unsafe { nil } |
| 90 | toplevel_decoration &C.zxdg_toplevel_decoration_v1 = unsafe { nil } |
| 91 | // Clipboard / data device |
| 92 | data_device_manager &C.wl_data_device_manager = unsafe { nil } |
| 93 | data_device &C.wl_data_device = unsafe { nil } |
| 94 | data_source &C.wl_data_source = unsafe { nil } |
| 95 | data_offer &C.wl_data_offer = unsafe { nil } |
| 96 | clipboard_string &char = unsafe { nil } |
| 97 | clipboard_size int |
| 98 | // Window state |
| 99 | width int |
| 100 | height int |
| 101 | fb_width int |
| 102 | fb_height int |
| 103 | scale f32 = 1.0 |
| 104 | configured bool |
| 105 | closed bool |
| 106 | fullscreen bool |
| 107 | maximized bool |
| 108 | focused bool |
| 109 | // Pointer state |
| 110 | pointer_x f32 |
| 111 | pointer_y f32 |
| 112 | pointer_enter_serial u32 |
| 113 | // Key repeat |
| 114 | key_repeat_rate int = 25 |
| 115 | key_repeat_delay int = 600 |
| 116 | key_repeat_timer_fd int = -1 |
| 117 | key_repeat_keycode u32 |
| 118 | } |
| 119 | } $else { |
| 120 | // Dummy struct for non-Wayland builds |
| 121 | struct SappWayland {} |
| 122 | } |
| 123 | |
| 124 | // X11 XInput extension state |
| 125 | struct SappXi { |
| 126 | mut: |
| 127 | available bool |
| 128 | major_opcode int |
| 129 | event_base int |
| 130 | error_base int |
| 131 | major int |
| 132 | minor int |
| 133 | } |
| 134 | |
| 135 | // X11 XDnD state |
| 136 | struct SappXdnd { |
| 137 | mut: |
| 138 | version i64 |
| 139 | source Window |
| 140 | format Atom |
| 141 | xdnd_aware Atom |
| 142 | xdnd_enter Atom |
| 143 | xdnd_position Atom |
| 144 | xdnd_status Atom |
| 145 | xdnd_action_copy Atom |
| 146 | xdnd_drop Atom |
| 147 | xdnd_finished Atom |
| 148 | xdnd_selection Atom |
| 149 | xdnd_type_list Atom |
| 150 | text_uri_list Atom |
| 151 | } |
| 152 | |
| 153 | // X11-specific state |
| 154 | struct SappX11 { |
| 155 | mut: |
| 156 | mouse_buttons u8 |
| 157 | display &C.Display = unsafe { nil } |
| 158 | screen int |
| 159 | root Window |
| 160 | colormap Colormap |
| 161 | window Window |
| 162 | hidden_cursor Cursor |
| 163 | standard_cursors [mousecursor_num]Cursor |
| 164 | custom_cursors [mousecursor_num]Cursor |
| 165 | window_state int |
| 166 | dpi f32 |
| 167 | error_code u8 |
| 168 | // X atoms |
| 169 | utf8_string Atom |
| 170 | clipboard_atom Atom |
| 171 | targets Atom |
| 172 | wm_protocols Atom |
| 173 | wm_delete_window Atom |
| 174 | wm_state Atom |
| 175 | net_wm_name Atom |
| 176 | net_wm_icon_name Atom |
| 177 | net_wm_icon Atom |
| 178 | net_wm_state Atom |
| 179 | net_wm_state_fullscreen Atom |
| 180 | // Extensions |
| 181 | xi SappXi |
| 182 | xdnd SappXdnd |
| 183 | // Key repeat tracking (keycodes 0..255) |
| 184 | key_repeat [256]bool |
| 185 | } |
| 186 | |
| 187 | // EGL state |
| 188 | struct SappEgl { |
| 189 | mut: |
| 190 | display EGLDisplay |
| 191 | context EGLContext |
| 192 | surface EGLSurface |
| 193 | config EGLConfig |
| 194 | } |
| 195 | |
| 196 | // Main application state - Linux version with Wayland/X11/EGL backends |
| 197 | struct SappState { |
| 198 | mut: |
| 199 | desc Desc |
| 200 | valid bool |
| 201 | fullscreen bool |
| 202 | first_frame bool |
| 203 | init_called bool |
| 204 | cleanup_called bool |
| 205 | quit_requested bool |
| 206 | quit_ordered bool |
| 207 | event_consumed bool |
| 208 | html5_ask_leave_site bool |
| 209 | onscreen_keyboard_shown bool |
| 210 | window_width int |
| 211 | window_height int |
| 212 | framebuffer_width int |
| 213 | framebuffer_height int |
| 214 | sample_count int |
| 215 | swap_interval int |
| 216 | dpi_scale f32 = 1.0 |
| 217 | frame_count u64 |
| 218 | timing SappTiming |
| 219 | event Event |
| 220 | mouse SappMouse |
| 221 | clipboard SappClipboard |
| 222 | drop SappDrop |
| 223 | // Platform-specific state |
| 224 | wl SappWayland |
| 225 | x11 SappX11 |
| 226 | egl SappEgl |
| 227 | gl SappGl |
| 228 | // Keycode translation table |
| 229 | keycodes [max_keycodes]KeyCode |
| 230 | // Window title |
| 231 | window_title [max_title_length]u8 |
| 232 | // V patch |
| 233 | v_native_render bool |
| 234 | // Custom cursors |
| 235 | custom_cursor_bound [mousecursor_num]bool |
| 236 | } |
| 237 | |
| 238 | // The global state instance |
| 239 | __global g_sapp_state = SappState{} |
| 240 | |
| 241 | // === Shared helper functions === |
| 242 | |
| 243 | // init_sapp_event initializes an event struct with common fields. |
| 244 | fn init_sapp_event(event_type EventType) { |
| 245 | unsafe { C.memset(&g_sapp_state.event, 0, int(sizeof(g_sapp_state.event))) } |
| 246 | g_sapp_state.event.@type = event_type |
| 247 | g_sapp_state.event.frame_count = g_sapp_state.frame_count |
| 248 | g_sapp_state.event.mouse_button = .invalid |
| 249 | g_sapp_state.event.window_width = g_sapp_state.window_width |
| 250 | g_sapp_state.event.window_height = g_sapp_state.window_height |
| 251 | g_sapp_state.event.framebuffer_width = g_sapp_state.framebuffer_width |
| 252 | g_sapp_state.event.framebuffer_height = g_sapp_state.framebuffer_height |
| 253 | g_sapp_state.event.mouse_x = g_sapp_state.mouse.x |
| 254 | g_sapp_state.event.mouse_y = g_sapp_state.mouse.y |
| 255 | g_sapp_state.event.mouse_dx = g_sapp_state.mouse.dx |
| 256 | g_sapp_state.event.mouse_dy = g_sapp_state.mouse.dy |
| 257 | } |
| 258 | |
| 259 | // call_sapp_event dispatches the event to the user's callback. |
| 260 | fn call_sapp_event(e &Event) bool { |
| 261 | if !g_sapp_state.cleanup_called { |
| 262 | if g_sapp_state.desc.event_cb != unsafe { nil } { |
| 263 | g_sapp_state.desc.event_cb(e) |
| 264 | } else if g_sapp_state.desc.event_userdata_cb != unsafe { nil } { |
| 265 | g_sapp_state.desc.event_userdata_cb(e, g_sapp_state.desc.user_data) |
| 266 | } |
| 267 | } |
| 268 | if g_sapp_state.event_consumed { |
| 269 | g_sapp_state.event_consumed = false |
| 270 | return true |
| 271 | } |
| 272 | return false |
| 273 | } |
| 274 | |
| 275 | // sapp_call_init calls the user's init callback. |
| 276 | fn sapp_call_init() { |
| 277 | if g_sapp_state.desc.init_cb != unsafe { nil } { |
| 278 | g_sapp_state.desc.init_cb() |
| 279 | } else if g_sapp_state.desc.init_userdata_cb != unsafe { nil } { |
| 280 | g_sapp_state.desc.init_userdata_cb(g_sapp_state.desc.user_data) |
| 281 | } |
| 282 | g_sapp_state.init_called = true |
| 283 | } |
| 284 | |
| 285 | // sapp_call_frame calls the user's frame callback. |
| 286 | fn sapp_call_frame() { |
| 287 | if g_sapp_state.desc.frame_cb != unsafe { nil } { |
| 288 | g_sapp_state.desc.frame_cb() |
| 289 | } else if g_sapp_state.desc.frame_userdata_cb != unsafe { nil } { |
| 290 | g_sapp_state.desc.frame_userdata_cb(g_sapp_state.desc.user_data) |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | // sapp_call_cleanup calls the user's cleanup callback. |
| 295 | fn sapp_call_cleanup() { |
| 296 | if !g_sapp_state.cleanup_called { |
| 297 | if g_sapp_state.desc.cleanup_cb != unsafe { nil } { |
| 298 | g_sapp_state.desc.cleanup_cb() |
| 299 | } else if g_sapp_state.desc.cleanup_userdata_cb != unsafe { nil } { |
| 300 | g_sapp_state.desc.cleanup_userdata_cb(g_sapp_state.desc.user_data) |
| 301 | } |
| 302 | g_sapp_state.cleanup_called = true |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | // sapp_do_frame handles the per-frame logic. |
| 307 | fn sapp_do_frame() { |
| 308 | if g_sapp_state.first_frame { |
| 309 | g_sapp_state.first_frame = false |
| 310 | sapp_call_init() |
| 311 | } |
| 312 | sapp_call_frame() |
| 313 | g_sapp_state.frame_count++ |
| 314 | } |
| 315 | |
| 316 | // === Timing helpers === |
| 317 | |
| 318 | fn sapp_timing_reset(t &SappTiming) { |
| 319 | unsafe { |
| 320 | mut mt := t |
| 321 | mt.last = 0.0 |
| 322 | mt.accum = 0.0 |
| 323 | mt.avg = 0.0 |
| 324 | mt.spike_count = 0 |
| 325 | mt.num = 0 |
| 326 | mt.ring_head = 0 |
| 327 | mt.ring_tail = 0 |
| 328 | } |
| 329 | } |
| 330 | |
| 331 | fn sapp_timing_get_avg(t &SappTiming) f64 { |
| 332 | return t.avg |
| 333 | } |
| 334 | |
| 335 | // === Drop file helpers === |
| 336 | |
| 337 | fn sapp_dropped_file_path_ptr(index int) &char { |
| 338 | assert g_sapp_state.drop.buffer != unsafe { nil } |
| 339 | assert index >= 0 && index <= g_sapp_state.drop.max_files |
| 340 | offset := index * g_sapp_state.drop.max_path_length |
| 341 | assert offset < g_sapp_state.drop.buf_size |
| 342 | return unsafe { &char(g_sapp_state.drop.buffer + offset) } |
| 343 | } |
| 344 | |
| 345 | // === State initialization === |
| 346 | |
| 347 | fn sapp_desc_defaults(desc &Desc) Desc { |
| 348 | mut res := *desc |
| 349 | if res.sample_count == 0 { |
| 350 | res.sample_count = 1 |
| 351 | } |
| 352 | if res.swap_interval == 0 { |
| 353 | res.swap_interval = 1 |
| 354 | } |
| 355 | if res.gl.major_version == 0 { |
| 356 | res.gl.major_version = 4 |
| 357 | res.gl.minor_version = 1 |
| 358 | } |
| 359 | if res.clipboard_size == 0 { |
| 360 | res.clipboard_size = 8192 |
| 361 | } |
| 362 | if res.max_dropped_files == 0 { |
| 363 | res.max_dropped_files = 1 |
| 364 | } |
| 365 | if res.max_dropped_file_path_length == 0 { |
| 366 | res.max_dropped_file_path_length = 2048 |
| 367 | } |
| 368 | return res |
| 369 | } |
| 370 | |
| 371 | fn sapp_init_state(desc &Desc) { |
| 372 | unsafe { C.memset(&g_sapp_state, 0, int(sizeof(g_sapp_state))) } |
| 373 | g_sapp_state.desc = sapp_desc_defaults(desc) |
| 374 | g_sapp_state.first_frame = true |
| 375 | g_sapp_state.window_width = g_sapp_state.desc.width |
| 376 | g_sapp_state.window_height = g_sapp_state.desc.height |
| 377 | g_sapp_state.framebuffer_width = g_sapp_state.window_width |
| 378 | g_sapp_state.framebuffer_height = g_sapp_state.window_height |
| 379 | g_sapp_state.sample_count = g_sapp_state.desc.sample_count |
| 380 | g_sapp_state.swap_interval = g_sapp_state.desc.swap_interval |
| 381 | g_sapp_state.clipboard.enabled = g_sapp_state.desc.enable_clipboard |
| 382 | if g_sapp_state.clipboard.enabled { |
| 383 | g_sapp_state.clipboard.buf_size = g_sapp_state.desc.clipboard_size |
| 384 | g_sapp_state.clipboard.buffer = unsafe { |
| 385 | &char(C.calloc(1, usize(g_sapp_state.clipboard.buf_size))) |
| 386 | } |
| 387 | } |
| 388 | g_sapp_state.drop.enabled = g_sapp_state.desc.enable_dragndrop |
| 389 | if g_sapp_state.drop.enabled { |
| 390 | g_sapp_state.drop.max_files = g_sapp_state.desc.max_dropped_files |
| 391 | g_sapp_state.drop.max_path_length = g_sapp_state.desc.max_dropped_file_path_length |
| 392 | g_sapp_state.drop.buf_size = g_sapp_state.drop.max_files * g_sapp_state.drop.max_path_length |
| 393 | g_sapp_state.drop.buffer = unsafe { &char(C.calloc(1, usize(g_sapp_state.drop.buf_size))) } |
| 394 | } |
| 395 | // Copy window title |
| 396 | if g_sapp_state.desc.window_title != unsafe { nil } |
| 397 | && unsafe { C.strlen(g_sapp_state.desc.window_title) } > 0 { |
| 398 | title := g_sapp_state.desc.window_title |
| 399 | mut i := 0 |
| 400 | for i < max_title_length - 1 { |
| 401 | ch := unsafe { title[i] } |
| 402 | if ch == 0 { |
| 403 | break |
| 404 | } |
| 405 | g_sapp_state.window_title[i] = u8(ch) |
| 406 | i++ |
| 407 | } |
| 408 | g_sapp_state.window_title[i] = 0 |
| 409 | } else { |
| 410 | // Default title |
| 411 | g_sapp_state.window_title[0] = `s` |
| 412 | g_sapp_state.window_title[1] = `o` |
| 413 | g_sapp_state.window_title[2] = `k` |
| 414 | g_sapp_state.window_title[3] = `o` |
| 415 | g_sapp_state.window_title[4] = `l` |
| 416 | g_sapp_state.window_title[5] = 0 |
| 417 | } |
| 418 | g_sapp_state.dpi_scale = 1.0 |
| 419 | g_sapp_state.fullscreen = g_sapp_state.desc.fullscreen |
| 420 | g_sapp_state.mouse.shown = true |
| 421 | } |
| 422 | |
| 423 | fn sapp_discard_state() { |
| 424 | if g_sapp_state.clipboard.enabled && g_sapp_state.clipboard.buffer != unsafe { nil } { |
| 425 | unsafe { C.free(g_sapp_state.clipboard.buffer) } |
| 426 | } |
| 427 | if g_sapp_state.drop.enabled && g_sapp_state.drop.buffer != unsafe { nil } { |
| 428 | unsafe { C.free(g_sapp_state.drop.buffer) } |
| 429 | } |
| 430 | } |
| 431 | |