Refactor depencies, add clay to the repo in preparation to move from raygui to clay

This commit is contained in:
Stefan Stefanov 2026-01-02 21:13:34 +02:00
parent d748f742f7
commit 3c4ad68059
10 changed files with 528 additions and 81 deletions

15
.gitignore vendored
View file

@ -1,18 +1,11 @@
*.exe
*.sublime-workspace
*.pdb
*.raddbgi
*.dll
*.exp
*.lib
log.txt
*.bin
*.dylib
*.so
*.dSYM
*.a
linux
# gets automagically downloaded through scripts/setup.sh
vendors/clay-odin
vendors/clay-odin.zip
build/
build_generator/
ols.json

View file

@ -6,8 +6,10 @@ Yet-Another-Atlas-Packer by bersK (Stefan Stefanov)
> [!IMPORTANT]
> Pull this repo with `--recursive`, the `odin-aseprite` library is pulled in as a submodule.
* At least odin compiler version `dev-2025-07:204edd0fc`
* Build the `libtinyfiledialog` library in the `vendors/dialog` fold
At least odin compiler version `dev-2025-07:204edd0fc`
Since we need to download/compile some 3rd party dependencies in the vendors folder we need to call
`scripts/setup.[sh|bat]` to compile libtinydialog & download the already compiled `clay` binaries and bindings.
## Description
> [!NOTE]

View file

@ -11,4 +11,10 @@ copy %ODIN_ROOT%\vendor\raylib\windows\raylib.dll build\raylib.dll
pushd vendors\dialog
call .\build.bat
popd
popd
pushd vendors
curl.exe --output clay.zip --url https://github.com/nicbarker/clay/releases/download/v0.14/clay-odin.zip
@REM Apparently available on Win10 since build 17063 - https://superuser.com/a/1473255
tar -xf .\clay.zip
popd

View file

@ -4,4 +4,10 @@ set -e
pushd vendors/dialog
sh build.sh
popd
pushd vendors
wget https://github.com/nicbarker/clay/releases/download/v0.14/clay-odin.zip
unzip clay-odin.zip
rm -rf ./__MACOSX # Yeah...
popd

View file

@ -41,15 +41,15 @@ PackerSettings :: struct {
packer_settings: PackerSettings
// Where the output files will be written (atlas.png json output, etc)
output_folder_path: Maybe(string)
output_folder_path: string
// If a folder was chosen as input - the path
source_files_to_pack: Maybe([]string)
source_files_to_pack: []string
Atlas :: struct {
render_texture_target: rl.RenderTexture2D,
checked_background: rl.RenderTexture2D,
render_has_preview: bool,
render_size: i32,
metadata: Maybe([dynamic]generator.SpriteAtlasMetadata),
metadata: [dynamic]generator.SpriteAtlasMetadata,
}
atlas: Atlas

View file

@ -55,6 +55,9 @@ init :: proc() {
}
cleanup :: proc() {
delete(atlas.metadata)
delete(output_folder_path)
delete(source_files_to_pack)
log.info("Bye")
rl.CloseWindow()
}
@ -113,10 +116,17 @@ pack_atlas_and_render :: proc() {
defer rl.EndTextureMode()
atlas_entries: [dynamic]generator.AtlasEntry
delete(atlas_entries)
defer {
for entry in atlas_entries {
delete(entry.cells)
delete(entry.layer_cell_count)
delete(entry.layer_names)
}
delete(atlas_entries)
}
if files, ok := source_files_to_pack.([]string); ok {
generator.unmarshall_aseprite_files(files, &atlas_entries)
if len(source_files_to_pack) > 0 {
generator.unmarshall_aseprite_files(source_files_to_pack, &atlas_entries)
} else {
log.error("No source folder or files set! Can't pack the void!!!")
should_pack_atlas_and_render = false
@ -128,6 +138,7 @@ pack_atlas_and_render :: proc() {
padding_x := packer_settings.pixel_padding_x_int if packer_settings.padding_enabled else 0
padding_y := packer_settings.pixel_padding_y_int if packer_settings.padding_enabled else 0
delete(atlas.metadata)
atlas.metadata = generator.pack_atlas_entries(
atlas_entries[:],
&atlas_img,
@ -439,12 +450,9 @@ open_file_dialog :: proc(dialog_type: FileDialogType) {
case .SourceFiles:
// `open_file_dialog` returns a single cstring with one or more paths divided by a separator ('|'),
// https://github.com/native-toolkit/libtinyfiledialogs/blob/master/tinyfiledialogs.c#L2706
file_paths_conc := cstring(
diag.open_file_dialog("Select source files", nil, 0, nil, "", 1),
)
if len(file_paths_conc) > 0 {
source_files, ok := diag.open_file_dialog("Select source files", {}, 0, {}, "", 1)
if len(source_files) > 0 {
// todo(stefan): We're assuming the filepaths returned libtinydialog are valid...
source_files := strings.clone_from_cstring(file_paths_conc, context.allocator)
source_files_to_pack = strings.split(source_files, "|")
log.info(source_files_to_pack)
@ -453,10 +461,10 @@ open_file_dialog :: proc(dialog_type: FileDialogType) {
}
case .OutputFolder:
file := cstring(diag.select_folder_dialog("Select source folder", nil))
if len(file) > 0 {
output_folder_path = strings.clone_from_cstring(file)
log.info(output_folder_path)
file, ok := diag.select_folder_dialog("Select source folder", {})
if len(file) > 0 && ok {
output_folder_path = file
log.info(file)
} else {
log.error("Got an empty path from the file dialog!")
}
@ -464,9 +472,7 @@ open_file_dialog :: proc(dialog_type: FileDialogType) {
}
clear_atlas_data :: proc() {
if metadata, ok := atlas.metadata.([dynamic]generator.SpriteAtlasMetadata); ok {
delete(metadata)
}
delete(atlas.metadata)
atlas.render_has_preview = false
}

View file

@ -99,7 +99,6 @@ unmarshall_aseprite_files_file_info :: proc(
}
unmarshall_aseprite_files(paths[:], atlas_entries, alloc)
}
unmarshall_aseprite_files :: proc(
@ -114,7 +113,7 @@ unmarshall_aseprite_files :: proc(
extension := fp.ext(file)
if extension != ".aseprite" do continue
log.infof("Unmarshalling file: ", file)
log.info("Unmarshalling file:", file)
ase.unmarshal_from_filename(&aseprite_document, file)
atlas_entry := atlas_entry_from_compressed_cells(aseprite_document)
atlas_entry.path = file
@ -137,7 +136,7 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr
// instead of iterating all layers for each frame
// might be even quicker to first get that information an allocate at once the amount of cells we need
for frame, frameIdx in document.frames {
log.infof("Frame_{0} Chunks: ", frameIdx, len(frame.chunks))
log.infof("Frame_{0} Chunks: {1}", frameIdx, len(frame.chunks))
for chunk in frame.chunks {
if cel_chunk, ok := chunk.(ase.Cel_Chunk); ok {
cel_img, ci_ok := cel_chunk.cel.(ase.Com_Image_Cel)
@ -161,7 +160,7 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr
}
if layer_chunk, ok := chunk.(ase.Layer_Chunk); ok {
log.infof("Layer chunk: ", layer_chunk)
log.info("Layer chunk:", layer_chunk)
append(&atlas_entry.layer_names, layer_chunk.name)
}
}
@ -187,6 +186,7 @@ pack_atlas_entries :: proc(
assert(atlas.height != 0, "Atlas height shouldn't be 0!")
all_cell_images := make([dynamic]rl.Image) // it's fine to store it like this, rl.Image just stores a pointer to the data
defer delete(all_cell_images)
for &entry in entries {
for cell in entry.cells {
append(&all_cell_images, cell.img)
@ -197,12 +197,15 @@ pack_atlas_entries :: proc(
num_entries := len(all_cell_images)
nodes := make([]stbrp.Node, num_entries)
rects := make([]stbrp.Rect, num_entries)
defer delete(nodes)
defer delete(rects)
EntryAndCell :: struct {
entry: ^AtlasEntry,
cell_of_entry: ^CellData,
}
rect_idx_to_entry_and_cell := make(map[int]EntryAndCell, 100)
defer delete(rect_idx_to_entry_and_cell)
// Set the custom IDs
cellIdx: int
@ -275,10 +278,7 @@ pack_atlas_entries :: proc(
}
cell_metadata := SpriteAtlasMetadata {
name = cell_name,
location = {
i32(rect.x) + offset_x,
i32(rect.y) + offset_y,
},
location = {i32(rect.x) + offset_x, i32(rect.y) + offset_y},
size = {auto_cast cell.img.width, auto_cast cell.img.height},
}
append(&metadata, cell_metadata)
@ -451,17 +451,16 @@ metadata_source_code_generate :: proc(
}
save_output :: proc(
output_folder_path: Maybe(string),
atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata),
atlas_render_texture_target: rl.RenderTexture2D,
output_path: string,
metadata: [dynamic]SpriteAtlasMetadata,
render_texture_target: rl.RenderTexture2D,
) {
output_path, ok := output_folder_path.(string)
if !ok || output_path == "" {
if len(output_path) == 0 {
log.error("Output path is empty!")
return
}
image := rl.LoadImageFromTexture(atlas_render_texture_target.texture)
image := rl.LoadImageFromTexture(render_texture_target.texture)
rl.ImageFlipVertical(&image)
cstring_atlas_output_path := strings.clone_to_cstring(
@ -470,13 +469,17 @@ save_output :: proc(
rl.ExportImage(image, cstring_atlas_output_path)
if metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata); ok {
if len(metadata) > 0 {
log.info("Building metadata...")
if json_metadata, jok := json.marshal(metadata); jok == nil {
os.write_entire_file(
strings.concatenate({output_path, OS_FILE_SEPARATOR, "metadata.json"}),
strings.concatenate(
{output_path, OS_FILE_SEPARATOR, "metadata.json"},
context.temp_allocator,
),
json_metadata,
)
delete(json_metadata)
} else {
log.error("Failed to marshall the atlas metadata to a json!")
}
@ -485,9 +488,13 @@ save_output :: proc(
// maybe supply a config.json that defines the start, end, line by line entry and enum format strings
// this way you can essentially support any language
sb := generate_odin_enums_and_atlas_offsets_file_sb(metadata[:])
defer strings.builder_destroy(&sb)
odin_metadata := strings.to_string(sb)
ok := os.write_entire_file(
strings.concatenate({output_path, OS_FILE_SEPARATOR, "metadata.odin"}),
strings.concatenate(
{output_path, OS_FILE_SEPARATOR, "metadata.odin"},
context.temp_allocator,
),
transmute([]byte)odin_metadata,
)
if !ok {
@ -537,6 +544,7 @@ save_metadata_simple :: proc(
// if src_gen_metadata
if codegen, cok := source_gen_metadata.(SourceCodeGeneratorMetadata); cok {
sb := metadata_source_code_generate(metadata[:], codegen)
defer strings.builder_destroy(&sb)
source_metadata := strings.to_string(sb)
source_output_path := strings.concatenate(
@ -553,6 +561,7 @@ save_metadata_simple :: proc(
}
} else {
sb := metadata_source_code_generate(metadata[:], odin_source_generator_metadata)
defer strings.builder_destroy(&sb)
odin_metadata := strings.to_string(sb)
source_output_path := strings.concatenate(
{output_path, OS_FILE_SEPARATOR, "metadata.odin"},

View file

@ -0,0 +1,296 @@
package main
import clay "../clay-odin"
import "base:runtime"
import "core:math"
import "core:strings"
import "core:unicode/utf8"
import rl "vendor:raylib"
Raylib_Font :: struct {
fontId: u16,
font: rl.Font,
}
clay_color_to_rl_color :: proc(color: clay.Color) -> rl.Color {
return {u8(color.r), u8(color.g), u8(color.b), u8(color.a)}
}
raylib_fonts := [dynamic]Raylib_Font{}
// Alias for compatibility, default to ascii support
measure_text :: measure_text_ascii
measure_text_unicode :: proc "c" (
text: clay.StringSlice,
config: ^clay.TextElementConfig,
userData: rawptr,
) -> clay.Dimensions {
// Needed for grapheme_count
context = runtime.default_context()
line_width: f32 = 0
font := raylib_fonts[config.fontId].font
text_str := string(text.chars[:text.length])
// This function seems somewhat expensive, if you notice performance issues, you could assume
// - 1 codepoint per visual character (no grapheme clusters), where you can get the length from the loop
// - 1 byte per visual character (ascii), where you can get the length with `text.length`
// see `measure_text_ascii`
grapheme_count, _, _ := utf8.grapheme_count(text_str)
for letter, byte_idx in text_str {
glyph_index := rl.GetGlyphIndex(font, letter)
glyph := font.glyphs[glyph_index]
if glyph.advanceX != 0 {
line_width += f32(glyph.advanceX)
} else {
line_width += font.recs[glyph_index].width + f32(font.glyphs[glyph_index].offsetX)
}
}
scaleFactor := f32(config.fontSize) / f32(font.baseSize)
// Note:
// I'd expect this to be `grapheme_count - 1`,
// but that seems to be one letterSpacing too small
// maybe that's a raylib bug, maybe that's Clay?
total_spacing := f32(grapheme_count) * f32(config.letterSpacing)
return {width = line_width * scaleFactor + total_spacing, height = f32(config.fontSize)}
}
measure_text_ascii :: proc "c" (
text: clay.StringSlice,
config: ^clay.TextElementConfig,
userData: rawptr,
) -> clay.Dimensions {
line_width: f32 = 0
font := raylib_fonts[config.fontId].font
text_str := string(text.chars[:text.length])
for i in 0 ..< len(text_str) {
glyph_index := text_str[i] - 32
glyph := font.glyphs[glyph_index]
if glyph.advanceX != 0 {
line_width += f32(glyph.advanceX)
} else {
line_width += font.recs[glyph_index].width + f32(font.glyphs[glyph_index].offsetX)
}
}
scaleFactor := f32(config.fontSize) / f32(font.baseSize)
// Note:
// I'd expect this to be `len(text_str) - 1`,
// but that seems to be one letterSpacing too small
// maybe that's a raylib bug, maybe that's Clay?
total_spacing := f32(len(text_str)) * f32(config.letterSpacing)
return {width = line_width * scaleFactor + total_spacing, height = f32(config.fontSize)}
}
clay_raylib_render :: proc(
render_commands: ^clay.ClayArray(clay.RenderCommand),
allocator := context.temp_allocator,
) {
for i in 0 ..< render_commands.length {
render_command := clay.RenderCommandArray_Get(render_commands, i)
bounds := render_command.boundingBox
switch render_command.commandType {
case .None: // None
case .Text:
config := render_command.renderData.text
text := string(config.stringContents.chars[:config.stringContents.length])
// Raylib uses C strings instead of Odin strings, so we need to clone
// Assume this will be freed elsewhere since we default to the temp allocator
cstr_text := strings.clone_to_cstring(text, allocator)
font := raylib_fonts[config.fontId].font
rl.DrawTextEx(
font,
cstr_text,
{bounds.x, bounds.y},
f32(config.fontSize),
f32(config.letterSpacing),
clay_color_to_rl_color(config.textColor),
)
case .Image:
config := render_command.renderData.image
tint := config.backgroundColor
if tint == 0 {
tint = {255, 255, 255, 255}
}
imageTexture := (^rl.Texture2D)(config.imageData)
rl.DrawTextureEx(
imageTexture^,
{bounds.x, bounds.y},
0,
bounds.width / f32(imageTexture.width),
clay_color_to_rl_color(tint),
)
case .ScissorStart:
rl.BeginScissorMode(
i32(math.round(bounds.x)),
i32(math.round(bounds.y)),
i32(math.round(bounds.width)),
i32(math.round(bounds.height)),
)
case .ScissorEnd:
rl.EndScissorMode()
case .Rectangle:
config := render_command.renderData.rectangle
if config.cornerRadius.topLeft > 0 {
radius: f32 = (config.cornerRadius.topLeft * 2) / min(bounds.width, bounds.height)
draw_rect_rounded(
bounds.x,
bounds.y,
bounds.width,
bounds.height,
radius,
config.backgroundColor,
)
} else {
draw_rect(bounds.x, bounds.y, bounds.width, bounds.height, config.backgroundColor)
}
case .Border:
config := render_command.renderData.border
// Left border
if config.width.left > 0 {
draw_rect(
bounds.x,
bounds.y + config.cornerRadius.topLeft,
f32(config.width.left),
bounds.height - config.cornerRadius.topLeft - config.cornerRadius.bottomLeft,
config.color,
)
}
// Right border
if config.width.right > 0 {
draw_rect(
bounds.x + bounds.width - f32(config.width.right),
bounds.y + config.cornerRadius.topRight,
f32(config.width.right),
bounds.height - config.cornerRadius.topRight - config.cornerRadius.bottomRight,
config.color,
)
}
// Top border
if config.width.top > 0 {
draw_rect(
bounds.x + config.cornerRadius.topLeft,
bounds.y,
bounds.width - config.cornerRadius.topLeft - config.cornerRadius.topRight,
f32(config.width.top),
config.color,
)
}
// Bottom border
if config.width.bottom > 0 {
draw_rect(
bounds.x + config.cornerRadius.bottomLeft,
bounds.y + bounds.height - f32(config.width.bottom),
bounds.width -
config.cornerRadius.bottomLeft -
config.cornerRadius.bottomRight,
f32(config.width.bottom),
config.color,
)
}
// Rounded Borders
if config.cornerRadius.topLeft > 0 {
draw_arc(
bounds.x + config.cornerRadius.topLeft,
bounds.y + config.cornerRadius.topLeft,
config.cornerRadius.topLeft - f32(config.width.top),
config.cornerRadius.topLeft,
180,
270,
config.color,
)
}
if config.cornerRadius.topRight > 0 {
draw_arc(
bounds.x + bounds.width - config.cornerRadius.topRight,
bounds.y + config.cornerRadius.topRight,
config.cornerRadius.topRight - f32(config.width.top),
config.cornerRadius.topRight,
270,
360,
config.color,
)
}
if config.cornerRadius.bottomLeft > 0 {
draw_arc(
bounds.x + config.cornerRadius.bottomLeft,
bounds.y + bounds.height - config.cornerRadius.bottomLeft,
config.cornerRadius.bottomLeft - f32(config.width.top),
config.cornerRadius.bottomLeft,
90,
180,
config.color,
)
}
if config.cornerRadius.bottomRight > 0 {
draw_arc(
bounds.x + bounds.width - config.cornerRadius.bottomRight,
bounds.y + bounds.height - config.cornerRadius.bottomRight,
config.cornerRadius.bottomRight - f32(config.width.bottom),
config.cornerRadius.bottomRight,
0.1,
90,
config.color,
)
}
case clay.RenderCommandType.Custom:
// Implement custom element rendering here
}
}
}
// Helper procs, mainly for repeated conversions
@(private = "file")
draw_arc :: proc(
x, y: f32,
inner_rad, outer_rad: f32,
start_angle, end_angle: f32,
color: clay.Color,
) {
rl.DrawRing(
{math.round(x), math.round(y)},
math.round(inner_rad),
outer_rad,
start_angle,
end_angle,
10,
clay_color_to_rl_color(color),
)
}
@(private = "file")
draw_rect :: proc(x, y, w, h: f32, color: clay.Color) {
rl.DrawRectangle(
i32(math.round(x)),
i32(math.round(y)),
i32(math.round(w)),
i32(math.round(h)),
clay_color_to_rl_color(color),
)
}
@(private = "file")
draw_rect_rounded :: proc(x, y, w, h: f32, radius: f32, color: clay.Color) {
rl.DrawRectangleRounded({x, y, w, h}, radius, 8, clay_color_to_rl_color(color))
}

View file

@ -1,32 +0,0 @@
package tinyfiledialogs
import "core:c"
when ODIN_OS == .Windows {
foreign import lib {"tinyfiledialogs.lib", "system:comdlg32.lib", "system:Ole32.lib"}
} else when ODIN_OS == .Linux || ODIN_OS == .Darwin {
foreign import lib "libtinyfiledialogs.a"
}
foreign lib {
@(link_name = "tinyfd_notifyPopup")
notify_popup :: proc(title, message, icon_type: cstring) -> c.int ---
@(link_name = "tinyfd_messageBox")
message_box :: proc(title, message, dialog_type, icon_type: cstring, default_button: c.int) -> c.int ---
@(link_name = "tinyfd_inputBox")
input_box :: proc(title, message, default_input: cstring) -> [^]c.char ---
@(link_name = "tinyfd_saveFileDialog")
save_file_dialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring) -> [^]c.char ---
@(link_name = "tinyfd_openFileDialog")
open_file_dialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring, allow_multi: c.int) -> [^]c.char ---
@(link_name = "tinyfd_selectFolderDialog")
select_folder_dialog :: proc(title, default_path: cstring) -> [^]c.char ---
@(link_name = "tinyfd_colorChooser")
color_chooser :: proc(title, default_hex_rgb: cstring, default_rgb, result_rgb: [3]byte) -> [^]c.char ---
}

161
vendors/dialog/tinyfiledialogs.odin vendored Normal file
View file

@ -0,0 +1,161 @@
package tinyfiledialogs
import "core:c"
import "core:mem"
import "core:strings"
when ODIN_OS == .Windows {
foreign import lib {"tinyfiledialogs.lib", "system:comdlg32.lib", "system:Ole32.lib"}
} else when ODIN_OS == .Linux || ODIN_OS == .Darwin {
foreign import lib "libtinyfiledialogs.a"
}
@(default_calling_convention = "c", link_prefix = "tinyfd_")
foreign lib {
notifyPopup :: proc(title, message, icon_type: cstring) -> c.int ---
messageBox :: proc(title, message, dialog_type, icon_type: cstring, default_button: c.int) -> c.int ---
inputBox :: proc(title, message, default_input: cstring) -> cstring ---
saveFileDialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring) -> cstring ---
openFileDialog :: proc(title, default_path: cstring, pattern_count: c.int, patterns: [^]cstring, file_desc: cstring, allow_multi: c.int) -> cstring ---
selectFolderDialog :: proc(title, default_path: cstring) -> cstring ---
colorChooser :: proc(title, default_hex_rgb: cstring, default_rgb, result_rgb: [3]byte) -> cstring ---
}
select_folder_dialog :: proc(
title, default_path: string,
alloc := context.allocator,
temp_alloc := context.temp_allocator,
) -> (
path: string,
success: bool,
) {
ctitle: cstring = nil
cdefault_path: cstring = nil
err: mem.Allocator_Error
if len(title) > 0 {
ctitle, err = strings.clone_to_cstring(title, temp_alloc)
if err != nil {
return {}, false
}
}
if len(default_path) > 0 {
cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc)
if err != nil {
return {}, false
}
}
res := selectFolderDialog(ctitle, cdefault_path)
path, err = strings.clone_from_cstring(res, alloc)
if err != nil {
return {}, false
}
return path, true
}
save_file_dialog :: proc(
title, default_path: string,
pattern_count: i32,
patterns: []string,
file_desc: string,
alloc := context.allocator,
temp_alloc := context.temp_allocator,
) -> (
path: string,
success: bool,
) {
ctitle: cstring = nil
cdefault_path: cstring = nil
cfile_desc: cstring = nil
cpatterns: [^]cstring = nil
err: mem.Allocator_Error
if len(title) > 0 {
ctitle, err = strings.clone_to_cstring(title, temp_alloc)
if err != nil {
return {}, false
}
}
if len(default_path) > 0 {
cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc)
if err != nil {
return {}, false
}
}
if len(cfile_desc) > 0 {
cfile_desc, err = strings.clone_to_cstring(file_desc, temp_alloc)
if err != nil {
return {}, false
}
}
if pattern_count > 0 {
cpatterns = make([^]cstring, pattern_count + 1, temp_alloc)
for p, i in patterns {
cpatterns[i] = strings.clone_to_cstring(p)
}
cpatterns[pattern_count] = nil // null terminate the array
}
res := saveFileDialog(ctitle, cdefault_path, pattern_count, cpatterns, cfile_desc)
path, err = strings.clone_from_cstring(res, alloc)
if err != nil {
return {}, false
}
return path, true
}
open_file_dialog :: proc(
title, default_path: string,
pattern_count: i32,
patterns: []string,
file_desc: string,
allow_multi: i32,
alloc := context.allocator,
temp_alloc := context.temp_allocator,
) -> (
path: string,
success: bool,
) {
ctitle: cstring = nil
cdefault_path: cstring = nil
cfile_desc: cstring = nil
cpatterns: [^]cstring = nil
err: mem.Allocator_Error
if len(title) > 0 {
ctitle, err = strings.clone_to_cstring(title, temp_alloc)
if err != nil {
return {}, false
}
}
if len(default_path) > 0 {
cdefault_path, err = strings.clone_to_cstring(default_path, temp_alloc)
if err != nil {
return {}, false
}
}
if len(cfile_desc) > 0 {
cfile_desc, err = strings.clone_to_cstring(file_desc, temp_alloc)
if err != nil {
return {}, false
}
}
if pattern_count > 0 {
cpatterns = make([^]cstring, pattern_count + 1, temp_alloc)
for p, i in patterns {
cpatterns[i] = strings.clone_to_cstring(p)
}
cpatterns[pattern_count] = nil // null terminate the array
}
res := openFileDialog(ctitle, cdefault_path, pattern_count, cpatterns, cfile_desc, allow_multi)
path, err = strings.clone_from_cstring(res, alloc)
if err != nil {
return {}, false
}
return path, true
}