WIP: Add support for open and save file dialogs

open-files
Ronald 6 months ago
parent 69169f7ca2
commit 18d2c2f0a9

@ -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)
}
}

@ -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
}

@ -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,
}

@ -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..<filename_len {
file_buffer[i] = c.char(default_filename[i])
}
file_buffer[filename_len] = 0
}
// Build filter string
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)
// 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",
}
}

@ -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..<min(9, len(app.fonts)) {
if rl.IsKeyPressed(rl.KeyboardKey(int(rl.KeyboardKey.F1) + i)) {
app.current_font_index = i
app.line_height = app.fonts[app.current_font_index].size + 4
}
}
}
}
// Draw tabs
draw_tabs(&app)
// Draw active file content
if len(app.files) > 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..<min(9, len(app.fonts)) {
if rl.IsKeyPressed(rl.KeyboardKey(int(rl.KeyboardKey.F1) + i)) {
app.current_font_index = i
app.line_height = app.fonts[app.current_font_index].size + 4
}
}
}
}
draw :: proc(app: ^App) {
rl.BeginDrawing()
rl.ClearBackground(app.config.colours.background)
// Draw tabs
draw_tabs(app)
// Draw active file content
if len(app.files) > 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
}

@ -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
}

Loading…
Cancel
Save