diff --git a/.gitignore b/.gitignore index 91dc2bb..5cb0a1d 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index faeb113..44c60e2 100644 --- a/README.md +++ b/README.md @@ -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] diff --git a/scripts/setup.bat b/scripts/setup.bat index abf078d..cdd0a75 100644 --- a/scripts/setup.bat +++ b/scripts/setup.bat @@ -11,4 +11,10 @@ copy %ODIN_ROOT%\vendor\raylib\windows\raylib.dll build\raylib.dll pushd vendors\dialog call .\build.bat -popd \ No newline at end of file +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 diff --git a/scripts/setup.sh b/scripts/setup.sh index 81af715..ed557b0 100644 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -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 \ No newline at end of file diff --git a/src/frontend/globals.odin b/src/frontend/globals.odin index c3c3a94..795a7c1 100644 --- a/src/frontend/globals.odin +++ b/src/frontend/globals.odin @@ -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 diff --git a/src/frontend/main.odin b/src/frontend/main.odin index 1cf4860..6f69642 100644 --- a/src/frontend/main.odin +++ b/src/frontend/main.odin @@ -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 } diff --git a/src/generator/generator.odin b/src/generator/generator.odin index a3745af..fce6a4c 100644 --- a/src/generator/generator.odin +++ b/src/generator/generator.odin @@ -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"}, diff --git a/vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin b/vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin new file mode 100644 index 0000000..0f20238 --- /dev/null +++ b/vendors/clay-odin-raylib-renderer/clay_renderer_raylib.odin @@ -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)) +} diff --git a/vendors/dialog/tinyfiledialog.odin b/vendors/dialog/tinyfiledialog.odin deleted file mode 100644 index 1c82058..0000000 --- a/vendors/dialog/tinyfiledialog.odin +++ /dev/null @@ -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 --- -} diff --git a/vendors/dialog/tinyfiledialogs.odin b/vendors/dialog/tinyfiledialogs.odin new file mode 100644 index 0000000..ba43e6e --- /dev/null +++ b/vendors/dialog/tinyfiledialogs.odin @@ -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 +}