Platform :: struct { // UNIX window: *SDL_Window; gpu_device: *SDL_GPUDevice; tray: *SDL_Tray; tray_image: Image_Data; tray_icon_surface: *SDL_Surface; tray_menu: *SDL_TrayMenu; tray_icon_failed: bool; // set on the first failure, so minimizing stops retrying and re-logging it app_icon_surface: *SDL_Surface; app_icon_pending: bool; // set again at the start of the next frame, once the window is up #if OS == .LINUX { desktop_environment: Linux_Desktop_Environment; display_backend: Linux_Display_Backend; } #if OS == .MACOS { main_context: *#Context; polling_events: bool; // inside the main loop's SDL_PollEvent, where a live-resize frame may run in_live_resize_frame: bool; live_resize_frame_drawn: bool; // during this poll; the frame polling must then end, see app_frame } } // Calls SDL_GetError then SDL_ClearError // // The returned string is owned by SDL and only valid until the next SDL call consume_sdl_error :: () -> string { data := SDL_GetError(); SDL_ClearError(); return to_string(data); } ok_or_exit :: inline (success: bool, err_msg_format: string, args: ..Any, flags := Log_Flags.ERROR, exit_code : s32 = 1) { if success return; log(err_msg_format, ..args, flags=flags); exit(exit_code); } @PrintLike platform_restore_window :: () { using app; SDL_ShowWindow(platform.window); SDL_RestoreWindow(platform.window); SDL_RaiseWindow(platform.window); #if OS == .MACOS { NSApplication.setActivationPolicy(NSApplication.sharedApplication(), NSApplicationActivationPolicyRegular); // The Dock tile is made anew and takes the bundle's icon, which a bare executable lacks. platform.app_icon_pending = true; // A global hotkey arrives while another app is active, and raising the window alone does not // make this the active app. NSApplication.activateIgnoringOtherApps(NSApplication.sharedApplication(), YES); platform_hide_tray_icon(); } #if OS == .LINUX { // Right now, this doesn't work on my XFCE Arch Desktop, so I'm just going to leave the icon in the tray. Sad. if platform.desktop_environment == .XFCE { log("platform_hide_tray_icon is broken on XFCE, so it is currently disabled!"); } else platform_hide_tray_icon(); } app.minimized_to_tray = false; } // The display with the most area, as Windows opens on its largest monitor. The primary when SDL // cannot list them. sdl_largest_display :: () -> SDL_DisplayID { largest := SDL_GetPrimaryDisplay(); count: s32; displays := SDL_GetDisplays(*count); if !displays return largest; defer SDL_free(displays); largest_area: s64 = -1; for i: 0..count-1 { bounds: SDL_Rect; if !SDL_GetDisplayBounds(displays[i], *bounds) continue; area := cast(s64) bounds.w * bounds.h; if area > largest_area { largest = displays[i]; largest_area = area; } } return largest; } // SDL converts the whole surface on every set, so icon.png is scaled down once; 512 is plenty // for the Dock and the taskbar. APP_ICON_PIXELS :: 512; // The window's icon. On macOS, run outside the .app (a debug build), it stands in for the Dock // icon. Kept for sdl_apply_pending_app_icon. sdl_set_app_icon :: () { using platform; #if OS == .MACOS if app_running_from_app_bundle() return; // the Dock takes the bundle's own icon path := tprint("%images/icon.png", app_install_directory()); image := Load_Image(path); if !image.data return; defer stbi_image_free(image.data); full := SDL_CreateSurfaceFrom(image.width, image.height, .RGBA32, image.data, image.width * 4); if !full { log_error("[sdl_set_app_icon] SDL_CreateSurfaceFrom failed for `%`: %", path, consume_sdl_error()); return; } defer SDL_DestroySurface(full); app_icon_surface = SDL_ScaleSurface(full, APP_ICON_PIXELS, APP_ICON_PIXELS, .LINEAR); if !app_icon_surface { log_error("[sdl_set_app_icon] SDL_ScaleSurface failed for `%`: %", path, consume_sdl_error()); return; } app_icon_pending = true; sdl_apply_pending_app_icon(); } sdl_apply_pending_app_icon :: () { using platform; if !app_icon_pending || !app_icon_surface return; app_icon_pending = false; if !SDL_SetWindowIcon(window, app_icon_surface) log_error("[sdl_set_app_icon] SDL_SetWindowIcon failed: %", consume_sdl_error()); } #if OS == .MACOS { // While a window edge is dragged, the main loop is held inside SDL_PollEvent. SDL sends a window // exposed event with data1 = 1 from there and allows drawing from an event watch for it. sdl_live_resize_watch :: (userdata: *void, event: *SDL_Event) -> bool #c_call { using platform; if event.type != xx SDL_EventType.WINDOW_EXPOSED || event.window.data1 != 1 return true; if !polling_events || in_live_resize_frame || !main_context return true; in_live_resize_frame = true; live_resize_frame_drawn = true; push_context main_context.* { auto_release_temp(); app_frame(in_live_resize = true); } in_live_resize_frame = false; return true; } } sdl_default_window_size :: (mode: *SDL_DisplayMode) -> w: s32, h: s32 { w := cast(s32) (mode.w * app_user_settings.window_width_fraction); h := cast(s32) (mode.h * app_user_settings.window_height_fraction); return w, h; } // Back to the size and place the window starts at. platform_reset_window_placement :: () -> bool { using platform; display := sdl_largest_display(); mode := SDL_GetCurrentDisplayMode(display); if !mode { log_error("SDL_GetCurrentDisplayMode failed: %; the window was not reset.", consume_sdl_error()); return false; } w, h := sdl_default_window_size(mode); flags := SDL_GetWindowFlags(window); if flags & SDL_WINDOW_FULLSCREEN SDL_SetWindowFullscreen(window, false); if flags & SDL_WINDOW_MAXIMIZED SDL_RestoreWindow(window); // Sized first: centering uses the window's size. centered := SDL_WINDOWPOS_CENTERED_DISPLAY(display); if !SDL_SetWindowSize(window, w, h) || !SDL_SetWindowPosition(window, xx centered, xx centered) { log_error("Could not reset the window: %", consume_sdl_error()); return false; } log("Window reset: size: %x%", w, h); return true; } // Returns as soon as an event is queued, or after timeout_ms. The event stays queued for the next // frame's SDL_PollEvent. platform_wait_while_hidden :: (timeout_ms: u32) { SDL_WaitEventTimeout(null, xx timeout_ms); } platform_minimize_to_tray :: inline () { if !platform_show_tray_icon() { SDL_MinimizeWindow(platform.window); return; } SDL_HideWindow(platform.window); // Hidden in the menu bar, the app leaves the Dock and Cmd+Tab, as menu bar apps do. #if OS == .MACOS NSApplication.setActivationPolicy(NSApplication.sharedApplication(), NSApplicationActivationPolicyAccessory); app.minimized_to_tray = true; } create_tray_from_image :: (image_path: string) -> bool { using platform; if tray_icon_surface return true; tray_image = Load_Image(image_path); if !tray_image.data return false; tray_icon_surface = SDL_CreateSurfaceFrom(tray_image.width, tray_image.height, .RGBA32, tray_image.data, tray_image.width * 4); if !tray_icon_surface { log_error("[create_tray_from_image] SDL_CreateSurfaceFrom failed for `%`: %", image_path, consume_sdl_error()); stbi_image_free(tray_image.data); tray_image.data = null; return false; } return true; } // False when there is no tray to minimize to (e.g. GNOME without an AppIndicator extension) platform_show_tray_icon :: () -> bool { using platform; if tray return true; if tray_icon_failed return false; if !create_tray_from_image(get_app_tray_icon_path()) { log_error("[platform_show_tray_icon] No tray icon image; minimizing instead of hiding to the tray."); tray_icon_failed = true; return false; } tray = SDL_CreateTray(tray_icon_surface, APP_TRAY_TOOLTIP_TEXT); if !tray { log_error("[platform_show_tray_icon] SDL_CreateTray failed: %. Minimizing instead of hiding to the tray.", consume_sdl_error()); tray_icon_failed = true; return false; } tray_menu = SDL_CreateTrayMenu(tray); show_entry := ifx tray_menu then SDL_InsertTrayEntryAt(tray_menu, -1, "Show", 0) else null; if !show_entry { log_error("[platform_show_tray_icon] Could not build the tray menu: %. Minimizing instead of hiding to the tray.", consume_sdl_error()); SDL_DestroyTray(tray); tray = null; tray_menu = null; tray_icon_failed = true; return false; } SDL_SetTrayEntryCallback(show_entry, tray_show, *app); quit_entry := SDL_InsertTrayEntryAt(tray_menu, -1, "Quit", 0); if quit_entry SDL_SetTrayEntryCallback(quit_entry, tray_quit, *app); else log_error("[platform_show_tray_icon] Could not add the tray's Quit entry: %", consume_sdl_error()); return true; } platform_hide_tray_icon :: () { if !platform.tray return; // Maybe I need to unparent menu items first: SDL_DestroyTray(platform.tray); platform.tray = null; SDL_PumpEvents(); } tray_show :: (userdata: *void, te: *SDL_TrayEntry) -> void #c_call { new_context: #Context; use_app_logger(*new_context); push_context,defer_pop new_context; app := cast(*App)userdata; app.restore_window_this_frame = true; } tray_quit :: (userdata: *void, te: *SDL_TrayEntry) -> void #c_call { using app := cast(*App)userdata; app.should_exit = true; } Image_Data :: struct { data: *u8; width: s32; height: s32; depth: s32; scale_factor: float32 = 1.0; } Load_Image :: (path: string) -> Image_Data { id: Image_Data = ---; path_c_string := to_c_string(path,, temp); id.data = stbi_load(path_c_string, *id.width, *id.height, *id.depth, 4); if id.data { log("% loaded: %x%x%\n", path, id.width, id.height, id.depth); } else { log_error("failed to load %\n", path); } return id; } // A texture from tightly packed RGBA pixels, at most GPU_TEXTURE_MAX_SIDE a side. Its handle is the // SDL_GPUTexture, which is what the ImGui SDL_GPU backend draws. A zero handle when it failed. // Modeled on ImGui_ImplSDLGPU3_UpdateTexture. gpu_texture_create :: (pixels: *u8, width: s32, height: s32) -> Gpu_Texture { result: Gpu_Texture; device := platform.gpu_device; if !device || width <= 0 || height <= 0 || width > GPU_TEXTURE_MAX_SIDE || height > GPU_TEXTURE_MAX_SIDE return result; size := cast(s64) width * height * 4; texture_info: SDL_GPUTextureCreateInfo; texture_info.type = ._2D; texture_info.format = .R8G8B8A8_UNORM; texture_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER; texture_info.width = xx width; texture_info.height = xx height; texture_info.layer_count_or_depth = 1; texture_info.num_levels = 1; texture_info.sample_count = ._1; texture := SDL_CreateGPUTexture(device, *texture_info); if !texture { log_error("[SDL] SDL_CreateGPUTexture (%x%) failed: %", width, height, consume_sdl_error()); return result; } transfer_info: SDL_GPUTransferBufferCreateInfo; transfer_info.usage = .UPLOAD; transfer_info.size = xx size; transfer_buffer := SDL_CreateGPUTransferBuffer(device, *transfer_info); if !transfer_buffer { log_error("[SDL] SDL_CreateGPUTransferBuffer failed: %", consume_sdl_error()); SDL_ReleaseGPUTexture(device, texture); return result; } defer SDL_ReleaseGPUTransferBuffer(device, transfer_buffer); mapped := SDL_MapGPUTransferBuffer(device, transfer_buffer, false); if !mapped { log_error("[SDL] SDL_MapGPUTransferBuffer failed: %", consume_sdl_error()); SDL_ReleaseGPUTexture(device, texture); return result; } memcpy(mapped, pixels, size); SDL_UnmapGPUTransferBuffer(device, transfer_buffer); source: SDL_GPUTextureTransferInfo; source.transfer_buffer = transfer_buffer; destination: SDL_GPUTextureRegion; destination.texture = texture; destination.w = xx width; destination.h = xx height; destination.d = 1; // Submitted before the frame's own command buffer, so the texture is ready when ImGui draws it. command_buffer := SDL_AcquireGPUCommandBuffer(device); if !command_buffer { log_error("[SDL] SDL_AcquireGPUCommandBuffer failed: %", consume_sdl_error()); SDL_ReleaseGPUTexture(device, texture); return result; } copy_pass := SDL_BeginGPUCopyPass(command_buffer); SDL_UploadToGPUTexture(copy_pass, *source, *destination, false); SDL_EndGPUCopyPass(copy_pass); SDL_SubmitGPUCommandBuffer(command_buffer); result.handle = texture; result.width = width; result.height = height; return result; } gpu_texture_release :: (texture: *Gpu_Texture) { if texture.handle SDL_ReleaseGPUTexture(platform.gpu_device, cast(*SDL_GPUTexture) texture.handle); texture.* = .{}; }