diff --git a/file_dialog/file_dialog.odin b/file_dialog/file_dialog.odin new file mode 100644 index 0000000..cb1ecaa --- /dev/null +++ b/file_dialog/file_dialog.odin @@ -0,0 +1,55 @@ +package file_dialog + +// Save file dialog function +show_save_file_dialog :: proc( + title: string = "Save File", + filters: []File_Filter = {}, + default_filename: string = "", + default_directory: string = "" +) -> Result { + when ODIN_OS == .Windows { + return show_save_file_dialog_windows(title, filters, default_filename, default_directory) + } else when ODIN_OS == .Linux { + return show_save_file_dialog_linux(title, filters, default_filename, default_directory) + } else { + return { + success = false, + error_message = "Unsupported platform", + } + } +} + +// Main cross-platform function +show_open_file_dialog :: proc( + title: string = "Open File", + filters: []File_Filter = {}, + allow_multiple: bool = false +) -> Result { + when ODIN_OS == .Windows { + return show_open_file_dialog_windows(title, filters, allow_multiple) + } else when ODIN_OS == .Linux { + return show_open_file_dialog_linux(title, filters, allow_multiple) + } else { + return { + success = false, + error_message = "Unsupported platform", + } + } +} + +// Convenience function to clean up results +destroy_result :: proc(result: ^Result) { + if result.file_path != "" { + delete(result.file_path) + } + if len(result.file_paths) > 0 { + for path in result.file_paths { + delete(path) + } + delete(result.file_paths) + } + if result.error_message != "" { + delete(result.error_message) + } +} + diff --git a/file_dialog/linux.odin b/file_dialog/linux.odin new file mode 100644 index 0000000..2e4d38b --- /dev/null +++ b/file_dialog/linux.odin @@ -0,0 +1,77 @@ +#+build linux +#+feature dynamic-literals +package file_dialog + +import "core:log" +import "core:strings" +import os2 "core:os/os2" + +SAVE_FILE_COMMANDS :: [][]string{ + {"yad", "--file", "--save"} +} + +OPEN_SINGLE_FILE_COMMANDS :: [][]string{ + {"yad", "--file"} +} + +show_save_file_dialog_linux :: proc(title: string, filters: []File_Filter, default_filename: string = "", default_directory: string = "") -> (result: Result) { + + result.success = false + process_desc: os2.Process_Desc + for command in SAVE_FILE_COMMANDS { + log.debug("attempting to use", command[0], "to get file path") + process_desc.command = command + + state, stdout_bytes, stderr_bytes, err := os2.process_exec(process_desc, context.allocator) + if err == .Not_Exist { + log.debug("couldn't find", command[0]) + continue + } else if err != nil { + log.error("error whilst getting filename: ", err) + return + } + + stdout := string(stdout_bytes) + + if len(stdout) == 0 do result.file_path = strings.clone("") + else if stdout[len(stdout)-1] == '\n' do result.file_path = strings.clone(stdout[:len(stdout)-1]) + else do result.file_path = strings.clone(string(stdout)) + + result.success = true + } + + return result +} + +show_open_file_dialog_linux :: proc( + title: string, + filters: []File_Filter = {}, + allow_multiple: bool = false +) -> (result: Result) { + result.success = false + process_desc: os2.Process_Desc + for command in OPEN_SINGLE_FILE_COMMANDS { + log.debug("attempting to use", command[0], "to get file path") + process_desc.command = command + + state, stdout_bytes, stderr_bytes, err := os2.process_exec(process_desc, context.allocator) + if err == .Not_Exist { + log.debug("couldn't find", command[0]) + continue + } else if err != nil { + log.error("error whilst getting filename: ", err) + return + } + + stdout := string(stdout_bytes) + + if len(stdout) == 0 do result.file_path = strings.clone("") + else if stdout[len(stdout)-1] == '\n' do result.file_path = strings.clone(stdout[:len(stdout)-1]) + else do result.file_path = strings.clone(string(stdout)) + + result.success = true + } + + return result +} + diff --git a/file_dialog/types.odin b/file_dialog/types.odin new file mode 100644 index 0000000..ae32000 --- /dev/null +++ b/file_dialog/types.odin @@ -0,0 +1,15 @@ +package file_dialog + +// Cross-platform file dialog result +Result :: struct { + success: bool, + file_path: string, // For single selection (backward compatibility) + file_paths: []string, // For multiple selection + error_message: string, +} + +// File filter for dialog +File_Filter :: struct { + name: string, + pattern: string, +} diff --git a/file_dialog/windows.odin b/file_dialog/windows.odin new file mode 100644 index 0000000..249fc12 --- /dev/null +++ b/file_dialog/windows.odin @@ -0,0 +1,267 @@ +#+build windows +package file_dialog + +import "core:strings" +import "core:c" +import "core:fmt" + +foreign import comdlg32 "system:comdlg32.lib" +foreign import ole32 "system:ole32.lib" +foreign import shell32 "system:shell32.lib" + +OPENFILENAME :: struct { + lStructSize: c.ulong, + hwndOwner: rawptr, + hInstance: rawptr, + lpstrFilter: cstring, + lpstrCustomFilter: cstring, + nMaxCustFilter: c.ulong, + nFilterIndex: c.ulong, + lpstrFile: cstring, + nMaxFile: c.ulong, + lpstrFileTitle: cstring, + nMaxFileTitle: c.ulong, + lpstrInitialDir: cstring, + lpstrTitle: cstring, + Flags: c.ulong, + nFileOffset: c.ushort, + nFileExtension: c.ushort, + lpstrDefExt: cstring, + lCustData: c.ulong_ptr, + lpfnHook: rawptr, + lpTemplateName: cstring, + pvReserved: rawptr, + dwReserved: c.ulong, + FlagsEx: c.ulong, +} + +OFN_PATHMUSTEXIST :: 0x00000800 +OFN_FILEMUSTEXIST :: 0x00001000 +OFN_HIDEREADONLY :: 0x00000004 +OFN_ALLOWMULTISELECT :: 0x00000200 + +@(default_calling_convention="stdcall") +foreign comdlg32 { + GetOpenFileNameA :: proc(lpofn: ^OPENFILENAME) -> c.int --- + GetSaveFileNameA :: proc(lpofn: ^OPENFILENAME) -> c.int --- +} + +show_open_file_dialog_windows :: proc(title: string, filters: []File_Filter, allow_multiple: bool) -> File_Dialog_Result { + MAX_PATH :: 260 + BUFFER_SIZE :: 32768 // Larger buffer for multiple files + file_buffer: [BUFFER_SIZE]c.char + + // Build filter string (format: "Description\0*.ext\0Description2\0*.ext2\0\0") + filter_str := strings.builder_make() + defer strings.builder_destroy(&filter_str) + + if len(filters) > 0 { + for filter in filters { + strings.write_string(&filter_str, filter.name) + strings.write_byte(&filter_str, 0) + strings.write_string(&filter_str, filter.pattern) + strings.write_byte(&filter_str, 0) + } + } else { + strings.write_string(&filter_str, "All Files") + strings.write_byte(&filter_str, 0) + strings.write_string(&filter_str, "*.*") + strings.write_byte(&filter_str, 0) + } + strings.write_byte(&filter_str, 0) // Double null terminator + + filter_cstr := strings.clone_to_cstring(strings.to_string(filter_str)) + defer delete(filter_cstr) + + title_cstr := strings.clone_to_cstring(title) + defer delete(title_cstr) + + flags := OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY + if allow_multiple { + flags |= OFN_ALLOWMULTISELECT + } + + ofn := OPENFILENAME{ + lStructSize = size_of(OPENFILENAME), + lpstrFile = &file_buffer[0], + nMaxFile = BUFFER_SIZE, + lpstrFilter = filter_cstr, + nFilterIndex = 1, + lpstrTitle = title_cstr, + Flags = flags, + } + + if GetOpenFileNameA(&ofn) != 0 { + buffer_str := string(cstring(&file_buffer[0])) + + if allow_multiple { + // Parse multiple files format: "directory\0file1\0file2\0\0" + parts := strings.split(buffer_str, "\x00") + defer delete(parts) + + if len(parts) > 1 && parts[1] != "" { + // Multiple files selected + directory := parts[0] + file_paths := make([]string, len(parts) - 1) + + for i := 1; i < len(parts); i+=1 { + if parts[i] != "" { + full_path := strings.concatenate({directory, "\\", parts[i]}) + file_paths[i-1] = full_path + } + } + + return { + success = true, + file_path = file_paths[0], // First file for compatibility + file_paths = file_paths, + } + } else { + // Single file selected (even with multiple selection enabled) + return { + success = true, + file_path = strings.clone(buffer_str), + file_paths = {strings.clone(buffer_str)}, + } + } + } else { + // Single file selection + return { + success = true, + file_path = strings.clone(buffer_str), + file_paths = {strings.clone(buffer_str)}, + } + } + } + + return { + success = false, + error_message = "User cancelled or dialog failed", + } +} + +show_save_file_dialog_macos :: proc(title: string, filters: []File_Filter, default_filename: string = "", default_directory: string = "") -> File_Dialog_Result { + panel := NSSavePanel_savePanel() + + title_nsstr := NSStringFromCString(strings.clone_to_cstring(title)) + NSSavePanel_setTitle(panel, title_nsstr) + + // Set default filename if provided + if default_filename != "" { + filename_nsstr := NSStringFromCString(strings.clone_to_cstring(default_filename)) + NSSavePanel_setNameFieldStringValue(panel, filename_nsstr) + } + + // Set default directory if provided + if default_directory != "" { + dir_nsstr := NSStringFromCString(strings.clone_to_cstring(default_directory)) + dir_url := NSURL_fileURLWithPath(dir_nsstr) + if dir_url != nil { + NSSavePanel_setDirectoryURL(panel, dir_url) + } + } + + // Note: File filters in NSSavePanel require more complex setup + // This is a simplified version - full implementation would use UTType + + response := NSSavePanel_runModal(panel) + + if response == NSModalResponseOK { + url := NSSavePanel_URL(panel) + if url != nil { + path_nsstr := NSURL_path(url) + path_cstr := NSString_UTF8String(path_nsstr) + file_path := strings.clone(string(path_cstr)) + return { + success = true, + file_path = file_path, + file_paths = {file_path}, + } + } + } + + return { + success = false, + error_message = "User cancelled or dialog failed", + } +} + +show_save_file_dialog_windows :: proc( + title: string, + filters: []File_Filter, + default_filename: string = "", + default_directory: string = "" +) -> File_Dialog_Result { + MAX_PATH :: 260 + file_buffer: [MAX_PATH]c.char + + // Set default filename if provided + if default_filename != "" { + filename_cstr := strings.clone_to_cstring(default_filename) + defer delete(filename_cstr) + + // Copy to buffer (ensure null termination) + filename_len := min(len(default_filename), MAX_PATH - 1) + for i in 0.. 0 { + for filter in filters { + strings.write_string(&filter_str, filter.name) + strings.write_byte(&filter_str, 0) + strings.write_string(&filter_str, filter.pattern) + strings.write_byte(&filter_str, 0) + } + } else { + strings.write_string(&filter_str, "All Files") + strings.write_byte(&filter_str, 0) + strings.write_string(&filter_str, "*.*") + strings.write_byte(&filter_str, 0) + } + strings.write_byte(&filter_str, 0) // Double null terminator + + filter_cstr := strings.clone_to_cstring(strings.to_string(filter_str)) + defer delete(filter_cstr) + + title_cstr := strings.clone_to_cstring(title) + defer delete(title_cstr) + + // Default directory (optional) + dir_cstr: cstring = nil + if default_directory != "" { + dir_cstr = strings.clone_to_cstring(default_directory) + defer delete(dir_cstr) + } + + ofn := OPENFILENAME{ + lStructSize = size_of(OPENFILENAME), + lpstrFile = &file_buffer[0], + nMaxFile = MAX_PATH, + lpstrFilter = filter_cstr, + nFilterIndex = 1, + lpstrTitle = title_cstr, + lpstrInitialDir = dir_cstr, + Flags = OFN_PATHMUSTEXIST | OFN_HIDEREADONLY, + } + + if GetSaveFileNameA(&ofn) != 0 { + return { + success = true, + file_path = strings.clone(string(cstring(&file_buffer[0]))), + file_paths = {strings.clone(string(cstring(&file_buffer[0])))}, + } + } + + return { + success = false, + error_message = "User cancelled or dialog failed", + } +} + diff --git a/main.odin b/main.odin index 451f3ac..4b991e2 100644 --- a/main.odin +++ b/main.odin @@ -2,14 +2,17 @@ package main import "core:fmt" import "core:log" +import "core:math" import "core:os" import "core:slice" import "core:strings" +import "core:thread" import "core:time" -import "core:math" import rl "vendor:raylib" +import "file_dialog" + main :: proc() { context.logger = log.create_console_logger(.Debug) defer log.destroy_console_logger(context.logger) @@ -24,7 +27,7 @@ main :: proc() { // Initialize application app := App{ - files = make([dynamic]FileBuffer), + files = make([dynamic]File_Buffer), fonts = make([dynamic]FontConfig), active_tab = 0, current_font_index = 0, @@ -52,10 +55,10 @@ main :: proc() { // Load files from command line arguments if len(args) > 1 { for filename in args { - load_file(&app, filename) + open_file(&app, filename) } } else { - load_file(&app) + open_file(&app) } if len(app.files) == 0 { @@ -72,8 +75,248 @@ main :: proc() { // Main loop for !rl.WindowShouldClose() { - update(&app) - draw(&app) + rl.BeginDrawing() + rl.ClearBackground(app.config.colours.background) + + for file in app.files { + if file.finished_saving && thread.is_done(file.thread) { + thread.destroy(file.thread) + } + } + + // Handle window resize + if rl.IsWindowResized() { + app.window_width = rl.GetScreenWidth() + app.window_height = rl.GetScreenHeight() + } + + // Handle tab switching with mouse + if rl.IsMouseButtonPressed(.LEFT) { + mouse_pos := rl.GetMousePosition() + if mouse_pos.y <= app.tab_height { + tab_width := f32(app.window_width) / f32(len(app.files)) + clicked_tab := int(mouse_pos.x / tab_width) + if clicked_tab >= 0 && clicked_tab < len(app.files) { + app.active_tab = clicked_tab + } + } + } + + if !app.file_dialog_open { + // Handle text selection with mouse + if len(app.files) > 0 && app.active_tab < len(app.files) { + current_file := &app.files[app.active_tab] + mouse_pos := rl.GetMousePosition() + + if rl.IsMouseButtonPressed(.LEFT) { + if mouse_pos.y > app.text_area_y { + character_pos := get_character_at_position(&app, mouse_pos) + current_file.cursor_pos = character_pos + current_file.selection.start = character_pos + current_file.selection.end = character_pos + current_file.selection.active = false + app.mouse_drag_start = character_pos + app.is_dragging = true + } + } + + if app.is_dragging && rl.IsMouseButtonDown(.LEFT) { + if mouse_pos.y > app.text_area_y { + character_pos := get_character_at_position(&app, mouse_pos) + current_file.cursor_pos = character_pos + if character_pos != app.mouse_drag_start { + current_file.selection.start = min(app.mouse_drag_start, character_pos) + current_file.selection.end = max(app.mouse_drag_start, character_pos) + current_file.selection.active = true + } + } + } + + if rl.IsMouseButtonReleased(.LEFT) { + app.is_dragging = false + } + } + + // Handle keyboard input for active file + if len(app.files) > 0 && app.active_tab < len(app.files) { + current_file := &app.files[app.active_tab] + + // Handle text input + key := rl.GetCharPressed() + for key != 0 { + if key >= 32 && key <= 126 { // Printable ASCII + if current_file.selection.active { + delete_selection(current_file) + } + insert_character(current_file, rune(key)) + } + key = rl.GetCharPressed() + } + + // Handle special keys + if rl.IsKeyDown(.ENTER) { + if current_file.selection.active { + delete_selection(current_file) + } + insert_character(current_file, '\n') + time.sleep(DEFAULT_DELAY) + } + if rl.IsKeyDown(.BACKSPACE) { + if current_file.selection.active { + delete_selection(current_file) + } + delete_character(current_file) + time.sleep(DEFAULT_DELAY) + } + if rl.IsKeyDown(.TAB) { + if current_file.selection.active { + delete_selection(current_file) + } + insert_character(current_file, '\t') + time.sleep(DEFAULT_DELAY) + } + if rl.IsKeyDown(.DELETE) { + if current_file.selection.active { + delete_selection(current_file) + } + delete_character(current_file, delete_backwards = true) + time.sleep(DEFAULT_DELAY) + } + + // Handle cursor movement + shift_held := rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) + + if rl.IsKeyDown(.LEFT) { + if !shift_held && current_file.selection.active { + current_file.cursor_pos = current_file.selection.start + current_file.selection.active = false + } else { + if !current_file.selection.active && shift_held { + current_file.selection.start = current_file.cursor_pos + current_file.selection.active = true + } + if current_file.cursor_pos > 0 { + current_file.cursor_pos -= 1 + } + if shift_held { + current_file.selection.end = current_file.cursor_pos + } + } + time.sleep(DEFAULT_DELAY) + } + if rl.IsKeyDown(.RIGHT) { + if !shift_held && current_file.selection.active { + current_file.cursor_pos = current_file.selection.end + current_file.selection.active = false + } else { + if !current_file.selection.active && shift_held { + current_file.selection.start = current_file.cursor_pos + current_file.selection.active = true + } + if current_file.cursor_pos < len(current_file.content) { + current_file.cursor_pos += 1 + } + if shift_held { + current_file.selection.end = current_file.cursor_pos + } + } + time.sleep(DEFAULT_DELAY) + } + if rl.IsKeyDown(.UP) { + if !shift_held && current_file.selection.active { + current_file.selection.active = false + } else if !current_file.selection.active && shift_held { + current_file.selection.start = current_file.cursor_pos + current_file.selection.active = true + } + move_cursor_up(current_file) + if shift_held { + current_file.selection.end = current_file.cursor_pos + } + time.sleep(DEFAULT_DELAY) + } + if rl.IsKeyDown(.DOWN) { + if !shift_held && current_file.selection.active { + current_file.selection.active = false + } else if !current_file.selection.active && shift_held { + current_file.selection.start = current_file.cursor_pos + current_file.selection.active = true + } + move_cursor_down(current_file) + if shift_held { + current_file.selection.end = current_file.cursor_pos + } + time.sleep(DEFAULT_DELAY) + } + if rl.IsKeyDown(.HOME) { + move_cursor_to_start_of_line(current_file) + } + if rl.IsKeyDown(.END) { + move_cursor_to_end_of_line(current_file) + } + + // Handle scrolling + wheel := rl.GetMouseWheelMove() + current_file.scroll_y -= wheel * app.line_height * 3 + current_file.scroll_y = max(0, current_file.scroll_y) + + // Handle keyboard shortcuts + if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) { + if rl.IsKeyPressed(.S) { + current_file.thread = thread.create_and_start_with_poly_data2(&app, current_file, save_file) + } + if rl.IsKeyPressed(.O) { + app.file_dialog_open = true + result := file_dialog.show_open_file_dialog() + app.file_dialog_open = false + if !result.success { + log.error(result.error_message) + } + defer file_dialog.destroy_result(&result) + + open_file(&app, result.file_path) + } + if rl.IsKeyPressed(.A) { + select_all(current_file) + } + if rl.IsKeyPressed(.C) { + copy_selection(&app, current_file) + } + if rl.IsKeyPressed(.V) { + paste_text(current_file, app.copied_text) + } + if rl.IsKeyPressed(.TAB) { + if rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) { + app.active_tab = (app.active_tab - 1) % len(app.files) + } else { + app.active_tab = (app.active_tab + 1) % len(app.files) + } + } + } + + // Handle font switching (F1-F9) + for i in 0.. 0 && app.active_tab < len(app.files) { + draw_file_content(&app, &app.files[app.active_tab]) + } + + // Draw status bar + draw_status_bar(&app) + + rl.EndDrawing() } } @@ -90,10 +333,10 @@ load_fonts :: proc(app: ^App) { app.line_height = app.fonts[app.current_font_index].size + 4 } -load_file :: proc(app: ^App, filename: string = "") { - buffer: FileBuffer +open_file :: proc(app: ^App, filename: string = "") { + buffer: File_Buffer if len(filename) == 0 { - buffer = FileBuffer{ + buffer = File_Buffer{ filename = "", content = "", modified = false, @@ -108,7 +351,7 @@ load_file :: proc(app: ^App, filename: string = "") { return } - buffer = FileBuffer{ + buffer = File_Buffer{ filename = strings.clone(filename), content = string(data), modified = false, @@ -218,231 +461,7 @@ get_character_at_position :: proc(app: ^App, mouse_pos: rl.Vector2) -> int { return len(file.content) } -update :: proc(app: ^App) { - // Handle window resize - if rl.IsWindowResized() { - app.window_width = rl.GetScreenWidth() - app.window_height = rl.GetScreenHeight() - } - - // Handle tab switching with mouse - if rl.IsMouseButtonPressed(.LEFT) { - mouse_pos := rl.GetMousePosition() - if mouse_pos.y <= app.tab_height { - tab_width := f32(app.window_width) / f32(len(app.files)) - clicked_tab := int(mouse_pos.x / tab_width) - if clicked_tab >= 0 && clicked_tab < len(app.files) { - app.active_tab = clicked_tab - } - } - } - - // Handle text selection with mouse - if len(app.files) > 0 && app.active_tab < len(app.files) { - current_file := &app.files[app.active_tab] - mouse_pos := rl.GetMousePosition() - - if rl.IsMouseButtonPressed(.LEFT) { - if mouse_pos.y > app.text_area_y { - character_pos := get_character_at_position(app, mouse_pos) - current_file.cursor_pos = character_pos - current_file.selection.start = character_pos - current_file.selection.end = character_pos - current_file.selection.active = false - app.mouse_drag_start = character_pos - app.is_dragging = true - } - } - - if app.is_dragging && rl.IsMouseButtonDown(.LEFT) { - if mouse_pos.y > app.text_area_y { - character_pos := get_character_at_position(app, mouse_pos) - current_file.cursor_pos = character_pos - if character_pos != app.mouse_drag_start { - current_file.selection.start = min(app.mouse_drag_start, character_pos) - current_file.selection.end = max(app.mouse_drag_start, character_pos) - current_file.selection.active = true - } - } - } - - if rl.IsMouseButtonReleased(.LEFT) { - app.is_dragging = false - } - } - - // Handle keyboard input for active file - if len(app.files) > 0 && app.active_tab < len(app.files) { - current_file := &app.files[app.active_tab] - - // Handle text input - key := rl.GetCharPressed() - for key != 0 { - if key >= 32 && key <= 126 { // Printable ASCII - if current_file.selection.active { - delete_selection(current_file) - } - insert_character(current_file, rune(key)) - } - key = rl.GetCharPressed() - } - - // Handle special keys - if rl.IsKeyDown(.ENTER) { - if current_file.selection.active { - delete_selection(current_file) - } - insert_character(current_file, '\n') - time.sleep(DEFAULT_DELAY) - } - if rl.IsKeyDown(.BACKSPACE) { - if current_file.selection.active { - delete_selection(current_file) - } - delete_character(current_file) - time.sleep(DEFAULT_DELAY) - } - if rl.IsKeyDown(.TAB) { - if current_file.selection.active { - delete_selection(current_file) - } - insert_character(current_file, '\t') - time.sleep(DEFAULT_DELAY) - } - if rl.IsKeyDown(.DELETE) { - if current_file.selection.active { - delete_selection(current_file) - } - delete_character(current_file, delete_backwards = true) - time.sleep(DEFAULT_DELAY) - } - - // Handle cursor movement - shift_held := rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) - - if rl.IsKeyDown(.LEFT) { - if !shift_held && current_file.selection.active { - current_file.cursor_pos = current_file.selection.start - current_file.selection.active = false - } else { - if !current_file.selection.active && shift_held { - current_file.selection.start = current_file.cursor_pos - current_file.selection.active = true - } - if current_file.cursor_pos > 0 { - current_file.cursor_pos -= 1 - } - if shift_held { - current_file.selection.end = current_file.cursor_pos - } - } - time.sleep(DEFAULT_DELAY) - } - if rl.IsKeyDown(.RIGHT) { - if !shift_held && current_file.selection.active { - current_file.cursor_pos = current_file.selection.end - current_file.selection.active = false - } else { - if !current_file.selection.active && shift_held { - current_file.selection.start = current_file.cursor_pos - current_file.selection.active = true - } - if current_file.cursor_pos < len(current_file.content) { - current_file.cursor_pos += 1 - } - if shift_held { - current_file.selection.end = current_file.cursor_pos - } - } - time.sleep(DEFAULT_DELAY) - } - if rl.IsKeyDown(.UP) { - if !shift_held && current_file.selection.active { - current_file.selection.active = false - } else if !current_file.selection.active && shift_held { - current_file.selection.start = current_file.cursor_pos - current_file.selection.active = true - } - move_cursor_up(current_file) - if shift_held { - current_file.selection.end = current_file.cursor_pos - } - time.sleep(DEFAULT_DELAY) - } - if rl.IsKeyDown(.DOWN) { - if !shift_held && current_file.selection.active { - current_file.selection.active = false - } else if !current_file.selection.active && shift_held { - current_file.selection.start = current_file.cursor_pos - current_file.selection.active = true - } - move_cursor_down(current_file) - if shift_held { - current_file.selection.end = current_file.cursor_pos - } - time.sleep(DEFAULT_DELAY) - } - if rl.IsKeyDown(.HOME) { - move_cursor_to_start_of_line(current_file) - } - if rl.IsKeyDown(.END) { - move_cursor_to_end_of_line(current_file) - } - - // Handle scrolling - wheel := rl.GetMouseWheelMove() - current_file.scroll_y -= wheel * app.line_height * 3 - current_file.scroll_y = max(0, current_file.scroll_y) - - // Handle keyboard shortcuts - if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) { - if rl.IsKeyPressed(.S) { - save_file(current_file) - } - if rl.IsKeyPressed(.A) { - select_all(current_file) - } - if rl.IsKeyPressed(.C) { - copy_selection(app, current_file) - } - if rl.IsKeyPressed(.V) { - paste_text(current_file, app.copied_text) - } - if rl.IsKeyPressed(.TAB) { - if rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) { - app.active_tab = (app.active_tab - 1) % len(app.files) - } else { - app.active_tab = (app.active_tab + 1) % len(app.files) - } - } - } - - // Handle font switching (F1-F9) - for i in 0.. 0 && app.active_tab < len(app.files) { - draw_file_content(app, &app.files[app.active_tab]) - } - - // Draw status bar - draw_status_bar(app) - - rl.EndDrawing() } draw_tabs :: proc(app: ^App) { @@ -490,7 +509,7 @@ draw_tabs :: proc(app: ^App) { } } -draw_file_content :: proc(app: ^App, file: ^FileBuffer) { +draw_file_content :: proc(app: ^App, file: ^File_Buffer) { font := &app.fonts[app.current_font_index] // Content area (excluding line numbers) @@ -668,7 +687,7 @@ draw_status_bar :: proc(app: ^App) { } } -insert_character :: proc(file: ^FileBuffer, character: rune) { +insert_character :: proc(file: ^File_Buffer, character: rune) { if file.cursor_pos <= len(file.content) { before := file.content[:file.cursor_pos] after := file.content[file.cursor_pos:] @@ -679,7 +698,7 @@ insert_character :: proc(file: ^FileBuffer, character: rune) { } } -delete_character :: proc(file: ^FileBuffer, delete_backwards: bool = false) { +delete_character :: proc(file: ^File_Buffer, delete_backwards: bool = false) { if file.cursor_pos > 0 && !delete_backwards { before := file.content[:file.cursor_pos-1] after := file.content[file.cursor_pos:] @@ -696,7 +715,7 @@ delete_character :: proc(file: ^FileBuffer, delete_backwards: bool = false) { } } -delete_selection :: proc(file: ^FileBuffer) { +delete_selection :: proc(file: ^File_Buffer) { if !file.selection.active do return start := min(file.selection.start, file.selection.end) @@ -710,20 +729,20 @@ delete_selection :: proc(file: ^FileBuffer) { clear_selection(file) } -clear_selection :: proc(file: ^FileBuffer) { +clear_selection :: proc(file: ^File_Buffer) { file.selection.active = false file.selection.start = 0 file.selection.end = 0 } -select_all :: proc(file: ^FileBuffer) { +select_all :: proc(file: ^File_Buffer) { file.selection.start = 0 file.selection.end = len(file.content) file.selection.active = true file.cursor_pos = len(file.content) } -copy_selection :: proc(app: ^App, file: ^FileBuffer) { +copy_selection :: proc(app: ^App, file: ^File_Buffer) { if !file.selection.active do return start := min(file.selection.start, file.selection.end) @@ -734,7 +753,7 @@ copy_selection :: proc(app: ^App, file: ^FileBuffer) { rl.SetClipboardText(strings.clone_to_cstring(app.copied_text)) } -paste_text :: proc(file: ^FileBuffer, text: string) { +paste_text :: proc(file: ^File_Buffer, text: string) { if file.selection.active { delete_selection(file) } @@ -751,7 +770,7 @@ paste_text :: proc(file: ^FileBuffer, text: string) { } } -move_cursor_up :: proc(file: ^FileBuffer) { +move_cursor_up :: proc(file: ^File_Buffer) { // For wrapped text, we need to handle cursor movement differently if file.cursor_pos > 0 { // Find the previous newline @@ -778,7 +797,7 @@ move_cursor_up :: proc(file: ^FileBuffer) { } } -move_cursor_down :: proc(file: ^FileBuffer) { +move_cursor_down :: proc(file: ^File_Buffer) { // For wrapped text, we need to handle cursor movement differently if file.cursor_pos < len(file.content) { // Find the current line start @@ -814,7 +833,7 @@ move_cursor_down :: proc(file: ^FileBuffer) { } } -move_cursor_to_start_of_line :: proc(file: ^FileBuffer) { +move_cursor_to_start_of_line :: proc(file: ^File_Buffer) { // For wrapped text, we need to handle cursor movement differently if file.cursor_pos < len(file.content) { // Find the current line start @@ -830,7 +849,7 @@ move_cursor_to_start_of_line :: proc(file: ^FileBuffer) { } } -move_cursor_to_end_of_line :: proc(file: ^FileBuffer) { +move_cursor_to_end_of_line :: proc(file: ^File_Buffer) { // For wrapped text, we need to handle cursor movement differently if file.cursor_pos < len(file.content) { // Find the next newline @@ -846,17 +865,26 @@ move_cursor_to_end_of_line :: proc(file: ^FileBuffer) { } } -save_file :: proc(file: ^FileBuffer) { - if file.filename == "" { - fmt.println("No filename, currently this feature is not supported, sorry :-(") - return +save_file :: proc(app: ^App, file: ^File_Buffer) { + file.finished_saving = false + if len(file.filename) == 0 { + app.file_dialog_open = true + result := file_dialog.show_save_file_dialog() + app.file_dialog_open = false + if !result.success { + log.error(result.error_message) + } + defer file_dialog.destroy_result(&result) + + file.filename = strings.clone(result.file_path) } - success := os.write_entire_file(file.filename, transmute([]u8)file.content) - if success { - // file.modified = false - fmt.printf("Saved: %s\n", file.filename) - } else { - fmt.printf("Failed to save: %s\n", file.filename) + + ok := os.write_entire_file(file.filename, transmute([]u8)file.content) + file.modified = !ok + if !ok { + log.error("failed to save file, \"", file.filename, "\"", sep="") // TODO: Show popup window that displays this to the user + return } + file.finished_saving = true } diff --git a/types.odin b/types.odin index ae5e9bd..999bc80 100644 --- a/types.odin +++ b/types.odin @@ -1,5 +1,7 @@ package main +import "core:thread" + import rl "vendor:raylib" Colour :: rl.Color @@ -8,31 +10,33 @@ Colour :: rl.Color FontConfig :: struct { font: rl.Font, size: f32, - name: string, + name: string } // Text selection Selection :: struct { start: int, end: int, - active: bool, + active: bool } // Wrapped line information WrappedLine :: struct { text: string, original_line: int, - character_offset: int, + character_offset: int } // File buffer structure -FileBuffer :: struct { +File_Buffer :: struct { filename: string, content: string, modified: bool, cursor_pos: int, scroll_y: f32, selection: Selection, + thread: ^thread.Thread, + finished_saving: bool } Colours :: struct { @@ -52,12 +56,11 @@ Colours :: struct { Config :: struct { font: string, font_size: int, - colours: Colours, + colours: Colours } -// Application state App :: struct { - files: [dynamic]FileBuffer, + files: [dynamic]File_Buffer, active_tab: int, fonts: [dynamic]FontConfig, current_font_index: int, @@ -71,4 +74,5 @@ App :: struct { is_dragging: bool, copied_text: string, config: Config, + file_dialog_open: bool }