mcap/src/base/platform/sdl.jai

359 lines
13 KiB
Plaintext

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.* = .{};
}