359 lines
13 KiB
Plaintext
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.* = .{};
|
|
}
|