Compare commits

..

7 Commits

Author SHA1 Message Date
Ronald b92742ede9 Update themes 7 months ago
Ronald 00eac4d5d6 Add support for opening files with a dialog box
This also tidies up some of the code for saving files, the dialogs are
opened in a new thread so that we can continue to render the application
in the main thread
7 months ago
Ronald 0a9bb6ebbc Increase default font size
I think the rendering for fonts is bugged, but this temporarily makes
the font usable
7 months ago
Ronald 18d2c2f0a9 WIP: Add support for open and save file dialogs 7 months ago
Ronald 69169f7ca2 Add support for opening without filename arg
This allows the user to open notepad_squared without needing to pass a
filename as an argument, however, we currently don't support saving
files that have been opened in this manor
7 months ago
Ronald 97d8f60810 Add themes file
Without this you cannot compile notepad_squared, don't know how I missed
this

Also, this commit also tidies up the imports in main, seperating the
core and vendor imports slightly
7 months ago
Ronald 66627e6bb5 Updated README 7 months ago

@ -15,21 +15,21 @@ Show whether a file has been saved or not - Done
Line wrapping that wraps on words when possible Line wrapping that wraps on words when possible
Ensure that Home, End, Delete keys work Ensure that Home, End, Delete keys work - Done
Add shortcuts Add shortcuts
Add menu bar (Right click menu) Add menu bar - should contain, file, edit and settings
Need to add a right click menu Need to add a right click menu
Add support for opening files from the editor - drag and drop support? Add support for opening files from the editor - drag and drop support and file dialog
Syntax highlighting Syntax highlighting
## Bugs ## Bugs
Holding a key (backspace or arrow keys) doesn't work as expected Holding a key (backspace or arrow keys) doesn't work as expected - Done
## Screenshots ## Screenshots

@ -8,7 +8,7 @@ DEFAULT_WINDOW_MINIMUM_WINDOW_HEIGHT :: 200
// FONT // FONT
DEFAULT_FONT :: "/usr/share/fonts/TTF/FiraCode-Regular.ttf" DEFAULT_FONT :: "/usr/share/fonts/TTF/FiraCode-Regular.ttf"
DEFAULT_FONT_SIZE :: 11 DEFAULT_FONT_SIZE :: 18
// DELAYS // DELAYS
DEFAULT_DELAY :: 100 * time.Millisecond DEFAULT_DELAY :: 100 * time.Millisecond

@ -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,13 +2,16 @@ package main
import "core:fmt" import "core:fmt"
import "core:log" import "core:log"
import "core:math"
import "core:os" import "core:os"
import "core:slice" import "core:slice"
import "core:strings" import "core:strings"
import "core:thread"
import "core:time" import "core:time"
import "core:math"
import rl "vendor:raylib" import rl "vendor:raylib"
import "file_dialog"
main :: proc() { main :: proc() {
context.logger = log.create_console_logger(.Debug) context.logger = log.create_console_logger(.Debug)
@ -16,11 +19,6 @@ main :: proc() {
args := os.args[1:] args := os.args[1:]
if len(args) == 0 {
fmt.println("Usage: gui_cat <file1> [file2] [file3] ...")
return
}
config, ok := parse_config("config.ini") config, ok := parse_config("config.ini")
if !ok { if !ok {
log.fatal("failed to parse config file") log.fatal("failed to parse config file")
@ -29,7 +27,7 @@ main :: proc() {
// Initialize application // Initialize application
app := App{ app := App{
files = make([dynamic]FileBuffer), files = make([dynamic]File_Buffer),
fonts = make([dynamic]FontConfig), fonts = make([dynamic]FontConfig),
active_tab = 0, active_tab = 0,
current_font_index = 0, current_font_index = 0,
@ -55,12 +53,16 @@ main :: proc() {
load_fonts(&app) load_fonts(&app)
// Load files from command line arguments // Load files from command line arguments
if len(args) > 1 {
for filename in args { for filename in args {
load_file(&app, filename) create_file_buffer(&app, filename)
}
} else {
create_file_buffer(&app)
} }
if len(app.files) == 0 { if len(app.files) == 0 {
fmt.println("No files could be loaded") log.error("no files could be loaded")
rl.CloseWindow() rl.CloseWindow()
return return
} }
@ -73,141 +75,15 @@ main :: proc() {
// Main loop // Main loop
for !rl.WindowShouldClose() { for !rl.WindowShouldClose() {
update(&app) rl.BeginDrawing()
draw(&app) rl.ClearBackground(app.config.colours.background)
}
}
load_fonts :: proc(app: ^App) {
// TODO: Support loading multiple fonts
// TODO: Allow searching for a font based on it's name and search fc-list database
font := FontConfig{
font = rl.LoadFont(strings.clone_to_cstring(app.config.font)),
size = f32(app.config.font_size),
name = "Default",
}
append(&app.fonts, font)
app.line_height = app.fonts[app.current_font_index].size + 4
}
load_file :: proc(app: ^App, filename: string) {
data, ok := os.read_entire_file(filename)
if !ok {
fmt.printf("Failed to read file: %s\n", filename)
return
}
buffer := FileBuffer{
filename = strings.clone(filename),
content = string(data),
modified = false,
cursor_pos = 0,
scroll_y = 0,
selection = Selection{start = 0, end = 0, active = false},
}
append(&app.files, buffer)
}
get_wrapped_lines :: proc(content: string, font: ^FontConfig, max_width: f32) -> []WrappedLine {
lines := strings.split(content, "\n")
defer delete(lines)
wrapped_lines := make([dynamic]WrappedLine)
character_offset := 0
for line, line_index in lines {
if len(line) == 0 {
// Empty line
append(&wrapped_lines, WrappedLine{
text = "",
original_line = line_index,
character_offset = character_offset,
})
} else {
// Wrap long lines
current_pos := 0
for current_pos < len(line) {
// Find how many characters fit in the width
fit_characters := 0
current_width: f32 = 0
for i in current_pos..<len(line) {
character_width := rl.MeasureTextEx(font.font, strings.clone_to_cstring(fmt.tprint(rune(line[i]))), font.size, 1).x
if current_width + character_width > max_width && fit_characters > 0 {
break
}
current_width += character_width
fit_characters += 1
}
// If we couldn't fit even one character, force at least one
if fit_characters == 0 {
fit_characters = 1
}
// Extract the text that fits
end_pos := min(current_pos + fit_characters, len(line))
wrapped_text := line[current_pos:end_pos]
append(&wrapped_lines, WrappedLine{
text = wrapped_text,
original_line = line_index,
character_offset = character_offset + current_pos,
})
current_pos = end_pos
}
}
character_offset += len(line) + 1 // +1 for newline
}
return wrapped_lines[:]
}
get_character_at_position :: proc(app: ^App, mouse_pos: rl.Vector2) -> int {
if len(app.files) == 0 || app.active_tab >= len(app.files) do return 0
file := &app.files[app.active_tab]
font := &app.fonts[app.current_font_index]
content_area_x := 10 + app.line_number_width
content_area_y := app.text_area_y
content_width := f32(app.window_width - 20) - app.line_number_width - 10
if mouse_pos.x < content_area_x || mouse_pos.y < content_area_y do return 0
wrapped_lines := get_wrapped_lines(file.content, font, content_width)
defer delete(wrapped_lines)
y_pos := content_area_y - file.scroll_y + 5
for wrapped_line, i in wrapped_lines {
if mouse_pos.y >= y_pos && mouse_pos.y < y_pos + app.line_height {
// Found the line, now find the character
x_offset := mouse_pos.x - content_area_x - 5
if x_offset <= 0 { for file in app.files {
return wrapped_line.character_offset if app.thread_finished && thread.is_done(app.thread) {
thread.destroy(app.thread)
} }
for j in 0..=len(wrapped_line.text) {
text_slice := wrapped_line.text[:j]
text_width := rl.MeasureTextEx(font.font, strings.clone_to_cstring(text_slice), font.size, 1).x
if x_offset <= text_width {
return wrapped_line.character_offset + j
}
}
return wrapped_line.character_offset + len(wrapped_line.text)
}
y_pos += app.line_height
} }
return len(file.content)
}
update :: proc(app: ^App) {
// Handle window resize // Handle window resize
if rl.IsWindowResized() { if rl.IsWindowResized() {
app.window_width = rl.GetScreenWidth() app.window_width = rl.GetScreenWidth()
@ -226,6 +102,7 @@ update :: proc(app: ^App) {
} }
} }
if !app.file_dialog_open {
// Handle text selection with mouse // Handle text selection with mouse
if len(app.files) > 0 && app.active_tab < len(app.files) { if len(app.files) > 0 && app.active_tab < len(app.files) {
current_file := &app.files[app.active_tab] current_file := &app.files[app.active_tab]
@ -233,7 +110,7 @@ update :: proc(app: ^App) {
if rl.IsMouseButtonPressed(.LEFT) { if rl.IsMouseButtonPressed(.LEFT) {
if mouse_pos.y > app.text_area_y { if mouse_pos.y > app.text_area_y {
character_pos := get_character_at_position(app, mouse_pos) character_pos := get_character_at_position(&app, mouse_pos)
current_file.cursor_pos = character_pos current_file.cursor_pos = character_pos
current_file.selection.start = character_pos current_file.selection.start = character_pos
current_file.selection.end = character_pos current_file.selection.end = character_pos
@ -245,7 +122,7 @@ update :: proc(app: ^App) {
if app.is_dragging && rl.IsMouseButtonDown(.LEFT) { if app.is_dragging && rl.IsMouseButtonDown(.LEFT) {
if mouse_pos.y > app.text_area_y { if mouse_pos.y > app.text_area_y {
character_pos := get_character_at_position(app, mouse_pos) character_pos := get_character_at_position(&app, mouse_pos)
current_file.cursor_pos = character_pos current_file.cursor_pos = character_pos
if character_pos != app.mouse_drag_start { if character_pos != app.mouse_drag_start {
current_file.selection.start = min(app.mouse_drag_start, character_pos) current_file.selection.start = min(app.mouse_drag_start, character_pos)
@ -386,13 +263,16 @@ update :: proc(app: ^App) {
// Handle keyboard shortcuts // Handle keyboard shortcuts
if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) { if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) {
if rl.IsKeyPressed(.S) { if rl.IsKeyPressed(.S) {
save_file(current_file) app.thread = thread.create_and_start_with_poly_data2(&app, current_file, save_file)
}
if rl.IsKeyPressed(.O) {
app.thread = thread.create_and_start_with_poly_data(&app, open_file)
} }
if rl.IsKeyPressed(.A) { if rl.IsKeyPressed(.A) {
select_all(current_file) select_all(current_file)
} }
if rl.IsKeyPressed(.C) { if rl.IsKeyPressed(.C) {
copy_selection(app, current_file) copy_selection(&app, current_file)
} }
if rl.IsKeyPressed(.V) { if rl.IsKeyPressed(.V) {
paste_text(current_file, app.copied_text) paste_text(current_file, app.copied_text)
@ -414,25 +294,194 @@ update :: proc(app: ^App) {
} }
} }
} }
}
draw :: proc(app: ^App) { }
rl.BeginDrawing()
rl.ClearBackground(app.config.colours.background)
// Draw tabs // Draw tabs
draw_tabs(app) draw_tabs(&app)
// Draw active file content // Draw active file content
if len(app.files) > 0 && app.active_tab < len(app.files) { if len(app.files) > 0 && app.active_tab < len(app.files) {
draw_file_content(app, &app.files[app.active_tab]) draw_file_content(&app, &app.files[app.active_tab])
} }
// Draw status bar // Draw status bar
draw_status_bar(app) draw_status_bar(&app)
rl.EndDrawing() rl.EndDrawing()
} }
}
load_fonts :: proc(app: ^App) {
// TODO: Support loading multiple fonts
// TODO: Allow searching for a font based on it's name and search fc-list database
font := FontConfig{
font = rl.LoadFont(strings.clone_to_cstring(app.config.font)),
size = f32(app.config.font_size),
name = "Default",
}
append(&app.fonts, font)
app.line_height = app.fonts[app.current_font_index].size + 4
}
create_file_buffer :: proc(app: ^App, filename: string = "") -> (ok: bool) {
ok = false
buffer: File_Buffer
if len(filename) == 0 {
buffer = File_Buffer{
filename = "",
content = "",
modified = false,
cursor_pos = 0,
scroll_y = 0,
selection = Selection{start = 0, end = 0, active = false},
}
} else {
data, ok := os.read_entire_file(filename)
if !ok {
fmt.printf("Failed to read file: %s\n", filename)
return
}
buffer = File_Buffer{
filename = strings.clone(filename),
content = string(data),
modified = false,
cursor_pos = 0,
scroll_y = 0,
selection = Selection{start = 0, end = 0, active = false},
}
}
append(&app.files, buffer)
ok = true
return
}
open_file :: proc(app: ^App) {
app.file_dialog_open = true
result: file_dialog.Result
result = file_dialog.show_open_file_dialog()
app.file_dialog_open = false
if !result.success {
log.error(result.error_message)
return
}
defer file_dialog.destroy_result(&result)
ok := create_file_buffer(app, result.file_path)
if !ok {
log.error("failed ot create file buffer")
return
}
app.active_tab+=1
return
}
get_wrapped_lines :: proc(content: string, font: ^FontConfig, max_width: f32) -> []WrappedLine {
lines := strings.split(content, "\n")
defer delete(lines)
wrapped_lines := make([dynamic]WrappedLine)
character_offset := 0
for line, line_index in lines {
if len(line) == 0 {
// Empty line
append(&wrapped_lines, WrappedLine{
text = "",
original_line = line_index,
character_offset = character_offset,
})
} else {
// Wrap long lines
current_pos := 0
for current_pos < len(line) {
// Find how many characters fit in the width
fit_characters := 0
current_width: f32 = 0
for i in current_pos..<len(line) {
character_width := rl.MeasureTextEx(font.font, strings.clone_to_cstring(fmt.tprint(rune(line[i]))), font.size, 1).x
if current_width + character_width > max_width && fit_characters > 0 {
break
}
current_width += character_width
fit_characters += 1
}
// If we couldn't fit even one character, force at least one
if fit_characters == 0 {
fit_characters = 1
}
// Extract the text that fits
end_pos := min(current_pos + fit_characters, len(line))
wrapped_text := line[current_pos:end_pos]
append(&wrapped_lines, WrappedLine{
text = wrapped_text,
original_line = line_index,
character_offset = character_offset + current_pos,
})
current_pos = end_pos
}
}
character_offset += len(line) + 1 // +1 for newline
}
return wrapped_lines[:]
}
get_character_at_position :: proc(app: ^App, mouse_pos: rl.Vector2) -> int {
if len(app.files) == 0 || app.active_tab >= len(app.files) do return 0
file := &app.files[app.active_tab]
font := &app.fonts[app.current_font_index]
content_area_x := 10 + app.line_number_width
content_area_y := app.text_area_y
content_width := f32(app.window_width - 20) - app.line_number_width - 10
if mouse_pos.x < content_area_x || mouse_pos.y < content_area_y do return 0
wrapped_lines := get_wrapped_lines(file.content, font, content_width)
defer delete(wrapped_lines)
y_pos := content_area_y - file.scroll_y + 5
for wrapped_line, i in wrapped_lines {
if mouse_pos.y >= y_pos && mouse_pos.y < y_pos + app.line_height {
// Found the line, now find the character
x_offset := mouse_pos.x - content_area_x - 5
if x_offset <= 0 {
return wrapped_line.character_offset
}
for j in 0..=len(wrapped_line.text) {
text_slice := wrapped_line.text[:j]
text_width := rl.MeasureTextEx(font.font, strings.clone_to_cstring(text_slice), font.size, 1).x
if x_offset <= text_width {
return wrapped_line.character_offset + j
}
}
return wrapped_line.character_offset + len(wrapped_line.text)
}
y_pos += app.line_height
}
return len(file.content)
}
draw :: proc(app: ^App) {
}
draw_tabs :: proc(app: ^App) { draw_tabs :: proc(app: ^App) {
if len(app.files) == 0 do return if len(app.files) == 0 do return
@ -479,7 +528,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] font := &app.fonts[app.current_font_index]
// Content area (excluding line numbers) // Content area (excluding line numbers)
@ -657,7 +706,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) { if file.cursor_pos <= len(file.content) {
before := file.content[:file.cursor_pos] before := file.content[:file.cursor_pos]
after := file.content[file.cursor_pos:] after := file.content[file.cursor_pos:]
@ -668,7 +717,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 { if file.cursor_pos > 0 && !delete_backwards {
before := file.content[:file.cursor_pos-1] before := file.content[:file.cursor_pos-1]
after := file.content[file.cursor_pos:] after := file.content[file.cursor_pos:]
@ -685,7 +734,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 if !file.selection.active do return
start := min(file.selection.start, file.selection.end) start := min(file.selection.start, file.selection.end)
@ -699,20 +748,20 @@ delete_selection :: proc(file: ^FileBuffer) {
clear_selection(file) clear_selection(file)
} }
clear_selection :: proc(file: ^FileBuffer) { clear_selection :: proc(file: ^File_Buffer) {
file.selection.active = false file.selection.active = false
file.selection.start = 0 file.selection.start = 0
file.selection.end = 0 file.selection.end = 0
} }
select_all :: proc(file: ^FileBuffer) { select_all :: proc(file: ^File_Buffer) {
file.selection.start = 0 file.selection.start = 0
file.selection.end = len(file.content) file.selection.end = len(file.content)
file.selection.active = true file.selection.active = true
file.cursor_pos = len(file.content) 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 if !file.selection.active do return
start := min(file.selection.start, file.selection.end) start := min(file.selection.start, file.selection.end)
@ -723,7 +772,7 @@ copy_selection :: proc(app: ^App, file: ^FileBuffer) {
rl.SetClipboardText(strings.clone_to_cstring(app.copied_text)) 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 { if file.selection.active {
delete_selection(file) delete_selection(file)
} }
@ -740,7 +789,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 // For wrapped text, we need to handle cursor movement differently
if file.cursor_pos > 0 { if file.cursor_pos > 0 {
// Find the previous newline // Find the previous newline
@ -767,7 +816,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 // For wrapped text, we need to handle cursor movement differently
if file.cursor_pos < len(file.content) { if file.cursor_pos < len(file.content) {
// Find the current line start // Find the current line start
@ -803,7 +852,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 // For wrapped text, we need to handle cursor movement differently
if file.cursor_pos < len(file.content) { if file.cursor_pos < len(file.content) {
// Find the current line start // Find the current line start
@ -819,7 +868,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 // For wrapped text, we need to handle cursor movement differently
if file.cursor_pos < len(file.content) { if file.cursor_pos < len(file.content) {
// Find the next newline // Find the next newline
@ -835,13 +884,26 @@ move_cursor_to_end_of_line :: proc(file: ^FileBuffer) {
} }
} }
save_file :: proc(file: ^FileBuffer) { save_file :: proc(app: ^App, file: ^File_Buffer) {
success := os.write_entire_file(file.filename, transmute([]u8)file.content) app.thread_finished = false
if success { if len(file.filename) == 0 {
file.modified = false app.file_dialog_open = true
fmt.printf("Saved: %s\n", file.filename) result := file_dialog.show_save_file_dialog()
} else { app.file_dialog_open = false
fmt.printf("Failed to save: %s\n", file.filename) if !result.success {
log.error(result.error_message)
}
defer file_dialog.destroy_result(&result)
file.filename = strings.clone(result.file_path)
}
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
} }
app.thread_finished = true
} }

@ -0,0 +1,31 @@
#+feature dynamic-literals
package main
themes := map[string]Colours {
"gruvbox dark hard" = Colours{
background = Colour{29, 32, 33, 255},
text = Colour{227, 218, 186, 255},
status_bar_text = Colour{133, 153, 0, 255},
line_numbers = Colour{146, 131, 116, 255},
line_numbers_background = Colour{40, 40, 40, 255},
active_tab = Colour{60, 56, 54, 255},
inactive_tab = Colour{50, 48, 47, 255},
tab_border = Colour{0, 218, 0, 255},
cursor = Colour{251, 241, 199, 255},
highlight = Colour{61, 63, 64, 255}
},
"solarized dark" = Colours{
background = Colour{0, 43, 54, 255},
text = Colour{131, 148, 150, 255},
status_bar_text = Colour{133, 153, 0, 255},
line_numbers = Colour{88, 110, 117, 255},
line_numbers_background = Colour{7, 54, 66, 255},
active_tab = Colour{7, 54, 66, 255},
inactive_tab = Colour{0, 43, 54, 255},
tab_border = Colour{88, 110, 117, 255},
cursor = Colour{147, 161, 161, 255},
highlight = Colour{61, 63, 64, 255}
},
}

@ -1,5 +1,7 @@
package main package main
import "core:thread"
import rl "vendor:raylib" import rl "vendor:raylib"
Colour :: rl.Color Colour :: rl.Color
@ -8,31 +10,31 @@ Colour :: rl.Color
FontConfig :: struct { FontConfig :: struct {
font: rl.Font, font: rl.Font,
size: f32, size: f32,
name: string, name: string
} }
// Text selection // Text selection
Selection :: struct { Selection :: struct {
start: int, start: int,
end: int, end: int,
active: bool, active: bool
} }
// Wrapped line information // Wrapped line information
WrappedLine :: struct { WrappedLine :: struct {
text: string, text: string,
original_line: int, original_line: int,
character_offset: int, character_offset: int
} }
// File buffer structure // File buffer structure
FileBuffer :: struct { File_Buffer :: struct {
filename: string, filename: string,
content: string, content: string,
modified: bool, modified: bool,
cursor_pos: int, cursor_pos: int,
scroll_y: f32, scroll_y: f32,
selection: Selection, selection: Selection
} }
Colours :: struct { Colours :: struct {
@ -52,12 +54,11 @@ Colours :: struct {
Config :: struct { Config :: struct {
font: string, font: string,
font_size: int, font_size: int,
colours: Colours, colours: Colours
} }
// Application state
App :: struct { App :: struct {
files: [dynamic]FileBuffer, files: [dynamic]File_Buffer,
active_tab: int, active_tab: int,
fonts: [dynamic]FontConfig, fonts: [dynamic]FontConfig,
current_font_index: int, current_font_index: int,
@ -71,4 +72,7 @@ App :: struct {
is_dragging: bool, is_dragging: bool,
copied_text: string, copied_text: string,
config: Config, config: Config,
thread: ^thread.Thread,
thread_finished: bool,
file_dialog_open: bool
} }

Loading…
Cancel
Save