commit 057d793c4c57a520cf139398d3f0bc3d8be25b12 Author: Ronald Date: Thu Jun 12 21:01:26 2025 +0100 Add the start of this to a repository diff --git a/colours.odin b/colours.odin new file mode 100644 index 0000000..11f6b85 --- /dev/null +++ b/colours.odin @@ -0,0 +1,42 @@ +package main + +import "core:encoding/hex" +import "core:log" +import "core:strings" + +hex_to_rgb :: proc(hex_str: string) -> (red, blue, green: u8, ok: bool) { + builder, err := strings.builder_make() + if err != nil { + log.error("failed to initialize memory") + return 0, 0, 0, false + } + + rgb_values: [3]u8 + rgb_count := 0 + for char, char_idx in hex_str { + if char == '#' { + continue + } + if rgb_count > 2 { + log.error("Malformed hex colour") + return 0, 0, 0, false + } + strings.write_rune(&builder, char) + if strings.builder_len(builder) == 2 { + str := strings.to_string(builder) + rgb_value, ok := hex.decode_sequence(str) + if !ok { + log.error("failed to initialize memory") + return 0, 0, 0, false + } + strings.builder_reset(&builder) + + rgb_values[rgb_count] = rgb_value + + rgb_count += 1 + } + } + + return rgb_values[0], rgb_values[1], rgb_values[2], true +} + diff --git a/config.odin b/config.odin new file mode 100644 index 0000000..29fff16 --- /dev/null +++ b/config.odin @@ -0,0 +1,81 @@ +package main + +import "core:encoding/hex" +import "core:encoding/ini" +import "core:fmt" +import "core:log" +import "core:mem" +import "core:strconv" +import "core:strings" + +parse_config :: proc(config_path: string, allocator := context.allocator) -> (Config, bool) { + ini_map, err, ok := ini.load_map_from_path(config_path, allocator=allocator) + if err != nil { + log.error("failed to parse config file") + return {}, false + } + defer ini.delete_map(ini_map) + + config: Config + + config.font_size = DEFAULT_FONT_SIZE + + config.colours.background = DEFAULT_BACKGROUND_COLOUR + config.colours.text = DEFAULT_TEXT_COLOUR + config.colours.line_numbers = DEFAULT_LINE_NUMBERS_COLOUR + config.colours.line_numbers_background = DEFAULT_LINE_NUMBER_BG_COLOUR + config.colours.active_tab = DEFAULT_ACTIVE_TAB_COLOUR + config.colours.inactive_tab = DEFAULT_INACTIVE_TAB_COLOUR + config.colours.cursor = DEFAULT_CURSOR_COLOUR + config.colours.highlight = DEFAULT_HIGHLIGHT_COLOUR + config.colours.status_bar = DEFAULT_STATUS_BAR_COLOUR + config.colours.status_bar_text = DEFAULT_STATUS_BAR_TEXT_COLUR + + // TODO: Tidy this up + for section in ini_map { + lower_case_section := strings.to_lower(section) + defer delete(lower_case_section) + + if lower_case_section == "general" { + for key, value in ini_map[section] { + lower_case_key := strings.to_lower(key) + defer delete(lower_case_key) + + if lower_case_key == "font" { + config.font = strings.clone(ini_map[section][key]) + } else if lower_case_key == "font size" { + config.font_size = strconv.atoi(ini_map[section][key]) + } + } + } else if lower_case_section == "colours" { + for key, value in ini_map[section] { + lower_case_key := strings.to_lower(key) + defer delete(lower_case_key) + + red, green, blue, ok := hex_to_rgb(ini_map[section][key]) + if !ok { + log.error("failed to parse hex value for", lower_case_key) + } + + if lower_case_key == "background" do config.colours.background = Colour{red, green, blue, 255} + else if lower_case_key == "text" do config.colours.text = Colour{red, green, blue, 255} + else if lower_case_key == "line numbers" do config.colours.line_numbers = Colour{red, green, blue, 255} + else if lower_case_key == "line number background" do config.colours.line_numbers_background = Colour{ + red, green, blue, 255 + } + else if lower_case_key == "active tab" do config.colours.active_tab = Colour{red, green, blue, 255} + else if lower_case_key == "inactive tab" do config.colours.inactive_tab = Colour{red, green, blue, 255} + else if lower_case_key == "tab border" do config.colours.tab_border = Colour{red, green, blue, 255} + else if lower_case_key == "cursor" do config.colours.cursor = Colour{red, green, blue, 255} + } + } + } + + if len(config.font) == 0 do config.font = strings.clone(DEFAULT_FONT) + + return config, true +} + +config_destroy :: proc(config: Config, allocator := context.allocator) { + delete(config.font) +} diff --git a/constants.odin b/constants.odin new file mode 100644 index 0000000..b04d827 --- /dev/null +++ b/constants.odin @@ -0,0 +1,23 @@ +package main + +// WINDOW SIZES +DEFAULT_WINDOW_MINIMUM_WINDOW_WIDTH :: 300 +DEFAULT_WINDOW_MINIMUM_WINDOW_HEIGHT :: 200 + +// Gruvbox Dark Hard Color Scheme Constants +DEFAULT_BACKGROUND_COLOUR :: Colour{29, 32, 33, 255} +DEFAULT_TEXT_COLOUR :: Colour{227, 218, 186, 255} +DEFAULT_LINE_NUMBERS_COLOUR :: Colour{146, 131, 116, 255} +DEFAULT_LINE_NUMBER_BG_COLOUR :: Colour{40, 40, 40, 255} +DEFAULT_ACTIVE_TAB_COLOUR :: Colour{60, 56, 54, 255} +DEFAULT_INACTIVE_TAB_COLOUR :: Colour{50, 48, 47, 255} +DEFAULT_TAB_BORDER_COLOUR :: Colour{102, 92, 84, 255} +DEFAULT_CURSOR_COLOUR :: Colour{251, 241, 199, 255} +DEFAULT_HIGHLIGHT_COLOUR :: Colour{60, 56, 54, 255} +DEFAULT_STATUS_BAR_COLOUR :: Colour{60, 56, 54, 255} +DEFAULT_STATUS_BAR_TEXT_COLUR :: Colour{197, 197, 55, 255} + +// FONT +DEFAULT_FONT :: "/usr/share/fonts/TTF/FiraCode-Regular.ttf" +DEFAULT_FONT_SIZE :: 11 + diff --git a/main.odin b/main.odin new file mode 100644 index 0000000..efdbeb0 --- /dev/null +++ b/main.odin @@ -0,0 +1,787 @@ +package main + +import "core:fmt" +import "core:log" +import "core:os" +import "core:strings" +import "core:slice" +import "core:math" +import rl "vendor:raylib" + + +main :: proc() { + context.logger = log.create_console_logger(.Debug) + defer log.destroy_console_logger(context.logger) + + args := os.args[1:] + + if len(args) == 0 { + fmt.println("Usage: gui_cat [file2] [file3] ...") + return + } + + config, ok := parse_config("config.ini") + if !ok { + log.fatal("failed to parse config file") + return + } + + // Initialize application + app := App{ + files = make([dynamic]FileBuffer), + fonts = make([dynamic]FontConfig), + active_tab = 0, + current_font_index = 0, + line_height = 24, + tab_height = 40, + window_width = 1200, + window_height = 800, + line_number_width = 30, + mouse_drag_start = -1, + is_dragging = false, + copied_text = "", + config = config + } + app.text_area_y = app.tab_height + 10 + + // Initialize Raylib + rl.InitWindow(app.window_width, app.window_height, "notepad_squared") + rl.SetWindowState(rl.ConfigFlags{.WINDOW_RESIZABLE}) + rl.SetWindowMinSize(DEFAULT_WINDOW_MINIMUM_WINDOW_WIDTH, DEFAULT_WINDOW_MINIMUM_WINDOW_HEIGHT) + rl.SetTargetFPS(60) + + // Load fonts + load_fonts(&app) + + // Load files from command line arguments + for filename in args { + load_file(&app, filename) + } + + if len(app.files) == 0 { + fmt.println("No files could be loaded") + rl.CloseWindow() + return + } + + defer { + rl.CloseWindow() + delete(app.files) + delete(app.fonts) + } + + // Main loop + for !rl.WindowShouldClose() { + update(&app) + draw(&app) + } +} + +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.. 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) +} + +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.IsKeyPressed(.ENTER) { + if current_file.selection.active { + delete_selection(current_file) + } + insert_character(current_file, '\n') + } + if rl.IsKeyPressed(.BACKSPACE) { + if current_file.selection.active { + delete_selection(current_file) + } else { + delete_character(current_file) + } + } + if rl.IsKeyPressed(.TAB) { + if current_file.selection.active { + delete_selection(current_file) + } + insert_character(current_file, '\t') + } + + // Handle cursor movement + shift_held := rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) + + if rl.IsKeyPressed(.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 + } + } + } + if rl.IsKeyPressed(.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 + } + } + } + if rl.IsKeyPressed(.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 + } + } + if rl.IsKeyPressed(.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 + } + } + + // 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) { + 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) { + 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) { + if len(app.files) == 0 do return + + tab_width := f32(app.window_width) / f32(len(app.files)) + + for i in 0.. 20 { + filename = fmt.tprintf("...%s", filename[len(filename)-17:]) + } + + // Add asterisk if modified + display_name := filename + if app.files[i].modified { + display_name = fmt.tprintf("%s*", filename) + } + + font := &app.fonts[app.current_font_index] + text_size := rl.MeasureTextEx(font.font, strings.clone_to_cstring(display_name), font.size, 1) + text_x := x + tab_width/2 - text_size.x/2 + text_y := app.tab_height/2 - text_size.y/2 + + rl.DrawTextEx( + font.font, + strings.clone_to_cstring(display_name), + {text_x, text_y}, + font.size, + 1, + colours.text + ) + } +} + +draw_file_content :: proc(app: ^App, file: ^FileBuffer) { + font := &app.fonts[app.current_font_index] + + // Content area (excluding line numbers) + content_area := rl.Rectangle{ + x = 10 + app.line_number_width, + y = app.text_area_y, + width = f32(app.window_width - 20) - app.line_number_width, + height = f32(app.window_height) - app.text_area_y - 30, + } + + // Line number area + line_num_area := rl.Rectangle{ + x = 10, + y = app.text_area_y, + width = app.line_number_width, + height = f32(app.window_height) - app.text_area_y - 30, + } + + // Background + colours := app.config.colours + + rl.DrawRectangleRec(line_num_area, colours.line_numbers_background) + rl.DrawRectangleLinesEx(line_num_area, 2, colours.line_numbers_background) + + rl.DrawRectangleRec(content_area, colours.background) + rl.DrawRectangleLinesEx(content_area, 2, colours.background) + + // Get wrapped lines + content_width := content_area.width + wrapped_lines := get_wrapped_lines(file.content, font, content_width) + defer delete(wrapped_lines) + + // Enable scissor test for scrolling + rl.BeginScissorMode( + i32(content_area.x), + i32(content_area.y), + i32(content_area.width), + i32(content_area.height) + ) + + // Draw text content and selection + y_pos := content_area.y - file.scroll_y + 5 + cursor_y := y_pos + cursor_x := content_area.x + 5 + + for wrapped_line, i in wrapped_lines { + if y_pos > content_area.y + content_area.height { + break + } + + if y_pos + app.line_height > content_area.y { + // Draw selection background for this line + if file.selection.active { + line_start := wrapped_line.character_offset + line_end := wrapped_line.character_offset + len(wrapped_line.text) + + if file.selection.start <= line_end && file.selection.end >= line_start { + sel_start_in_line := max(0, file.selection.start - line_start) + sel_end_in_line := min(len(wrapped_line.text), file.selection.end - line_start) + + if sel_start_in_line < sel_end_in_line { + before_sel := wrapped_line.text[:sel_start_in_line] + sel_x := content_area.x + 5 + + rl.MeasureTextEx(font.font, strings.clone_to_cstring(before_sel), font.size, 1).x + + selected_text := wrapped_line.text[sel_start_in_line:sel_end_in_line] + sel_width := rl.MeasureTextEx(font.font, strings.clone_to_cstring(selected_text), font.size, 1).x + + rl.DrawRectangle( + i32(sel_x), + i32(y_pos), + i32(sel_width), + i32(app.line_height), + app.config.colours.highlight + ) + } + } + } + + // Draw text + rl.DrawTextEx( + font.font, + strings.clone_to_cstring(wrapped_line.text), + {content_area.x + 5, y_pos}, + font.size, + 1, + app.config.colours.text + ) + } + + // Calculate cursor position + if wrapped_line.character_offset <= file.cursor_pos && + file.cursor_pos <= wrapped_line.character_offset + len(wrapped_line.text) { + cursor_y = y_pos + cursor_character_pos := file.cursor_pos - wrapped_line.character_offset + cursor_text := wrapped_line.text[:cursor_character_pos] if cursor_character_pos <= len(wrapped_line.text) else wrapped_line.text + cursor_x = content_area.x + 5 + rl.MeasureTextEx(font.font, strings.clone_to_cstring(cursor_text), font.size, 1).x + } + + y_pos += app.line_height + } + + // Draw cursor + if !file.selection.active && int(rl.GetTime() * 2) % 2 == 0 { // Blinking cursor + rl.DrawLine(i32(cursor_x), i32(cursor_y), i32(cursor_x), i32(cursor_y + app.line_height), rl.RED) + } + + rl.EndScissorMode() + + // Draw line numbers (show original line numbers) + rl.BeginScissorMode(i32(line_num_area.x), i32(line_num_area.y), + i32(line_num_area.width), i32(line_num_area.height)) + + y_pos = line_num_area.y - file.scroll_y + 5 + current_original_line := -1 + + for wrapped_line, i in wrapped_lines { + if y_pos > line_num_area.y + line_num_area.height { + break + } + + if y_pos + app.line_height > line_num_area.y { + // Only show line number for the first wrapped line of each original line + if wrapped_line.original_line != current_original_line { + current_original_line = wrapped_line.original_line + line_num_text := fmt.tprintf("%d", wrapped_line.original_line + 1) + text_size := rl.MeasureTextEx(font.font, strings.clone_to_cstring(line_num_text), font.size, 1) + text_x := line_num_area.x + line_num_area.width - text_size.x - 5 + rl.DrawTextEx( + font.font, + strings.clone_to_cstring(line_num_text), + {text_x, y_pos}, + font.size, + 1, + app.config.colours.line_numbers + ) + } + } + + y_pos += app.line_height + } + + rl.EndScissorMode() +} + +draw_status_bar :: proc(app: ^App) { + status_y := app.window_height - 25 + rl.DrawRectangle(0, status_y, app.window_width, 25, app.config.colours.status_bar) + + if len(app.files) > 0 && app.active_tab < len(app.files) { + file := &app.files[app.active_tab] + font_name := app.fonts[app.current_font_index].name + selection_info := "" + if file.selection.active { + selection_info = fmt.tprintf(" | Selected: %d characters", file.selection.end - file.selection.start) + } + status_text := fmt.tprintf( + "File: %s | Font: %s (F1-F%d) | Cursor: %d%s | Ctrl+S: Save | Ctrl+A: Select All | Ctrl+C/V: Copy/Paste", + file.filename, + font_name, + len(app.fonts), + file.cursor_pos, + selection_info + ) + + font := &app.fonts[app.current_font_index] + rl.DrawTextEx( + font.font, + strings.clone_to_cstring(status_text), + {f32(5), f32(status_y + 5)}, + font.size, + 1, + app.config.colours.status_bar_text + ) + } +} + +insert_character :: proc(file: ^FileBuffer, character: rune) { + if file.cursor_pos <= len(file.content) { + before := file.content[:file.cursor_pos] + after := file.content[file.cursor_pos:] + file.content = fmt.aprintf("%s%c%s", before, character, after) + file.cursor_pos += 1 + file.modified = true + clear_selection(file) + } +} + +delete_character :: proc(file: ^FileBuffer) { + if file.cursor_pos > 0 { + before := file.content[:file.cursor_pos-1] + after := file.content[file.cursor_pos:] + file.content = fmt.aprintf("%s%s", before, after) + file.cursor_pos -= 1 + file.modified = true + clear_selection(file) + } +} + +delete_selection :: proc(file: ^FileBuffer) { + if !file.selection.active do return + + start := min(file.selection.start, file.selection.end) + end := max(file.selection.start, file.selection.end) + + before := file.content[:start] + after := file.content[end:] + file.content = fmt.aprintf("%s%s", before, after) + file.cursor_pos = start + file.modified = true + clear_selection(file) +} + +clear_selection :: proc(file: ^FileBuffer) { + file.selection.active = false + file.selection.start = 0 + file.selection.end = 0 +} + +select_all :: proc(file: ^FileBuffer) { + 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) { + if !file.selection.active do return + + start := min(file.selection.start, file.selection.end) + end := max(file.selection.start, file.selection.end) + app.copied_text = file.content[start:end] + + // Set clipboard (Raylib function) + rl.SetClipboardText(strings.clone_to_cstring(app.copied_text)) +} + +paste_text :: proc(file: ^FileBuffer, text: string) { + if file.selection.active { + delete_selection(file) + } + + // Get clipboard text + clipboard := rl.GetClipboardText() + if clipboard != nil { + text_to_paste := string(clipboard) + before := file.content[:file.cursor_pos] + after := file.content[file.cursor_pos:] + file.content = fmt.aprintf("%s%s%s", before, text_to_paste, after) + file.cursor_pos += len(text_to_paste) + file.modified = true + } +} + +move_cursor_up :: proc(file: ^FileBuffer) { + // For wrapped text, we need to handle cursor movement differently + // This is a simplified version - you may want to enhance it further + if file.cursor_pos > 0 { + // Find the previous newline + for i := file.cursor_pos - 1; i >= 0; i -= 1 { + if file.content[i] == '\n' { + // Move to the previous line + prev_newline := -1 + for j := i - 1; j >= 0; j -= 1 { + if file.content[j] == '\n' { + prev_newline = j + break + } + } + + current_pos_in_line := file.cursor_pos - i - 1 + prev_line_length := i - prev_newline - 1 + new_pos_in_line := min(current_pos_in_line, prev_line_length) + file.cursor_pos = prev_newline + 1 + new_pos_in_line + return + } + } + // If no newline found, go to beginning + file.cursor_pos = 0 + } +} + +move_cursor_down :: proc(file: ^FileBuffer) { + // For wrapped text, we need to handle cursor movement differently + // This is a simplified version - you may want to enhance it further + if file.cursor_pos < len(file.content) { + // Find the current line start + current_line_start := 0 + for i := file.cursor_pos - 1; i >= 0; i -= 1 { + if file.content[i] == '\n' { + current_line_start = i + 1 + break + } + } + + // Find the next newline + for i := file.cursor_pos; i < len(file.content); i += 1 { + if file.content[i] == '\n' { + // Move to the next line + next_newline := len(file.content) + for j := i + 1; j < len(file.content); j += 1 { + if file.content[j] == '\n' { + next_newline = j + break + } + } + + current_pos_in_line := file.cursor_pos - current_line_start + next_line_length := next_newline - i - 1 + new_pos_in_line := min(current_pos_in_line, next_line_length) + file.cursor_pos = i + 1 + new_pos_in_line + return + } + } + // If no newline found, go to end + file.cursor_pos = len(file.content) + } +} + +save_file :: proc(file: ^FileBuffer) { + 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) + } +} + diff --git a/types.odin b/types.odin new file mode 100644 index 0000000..ae5e9bd --- /dev/null +++ b/types.odin @@ -0,0 +1,74 @@ +package main + +import rl "vendor:raylib" + +Colour :: rl.Color + +// Font configuration +FontConfig :: struct { + font: rl.Font, + size: f32, + name: string, +} + +// Text selection +Selection :: struct { + start: int, + end: int, + active: bool, +} + +// Wrapped line information +WrappedLine :: struct { + text: string, + original_line: int, + character_offset: int, +} + +// File buffer structure +FileBuffer :: struct { + filename: string, + content: string, + modified: bool, + cursor_pos: int, + scroll_y: f32, + selection: Selection, +} + +Colours :: struct { + background: Colour, + text: Colour, + line_numbers_background: Colour, + line_numbers: Colour, + highlight: Colour, + status_bar: Colour, + status_bar_text: Colour, + active_tab: Colour, + inactive_tab: Colour, + tab_border: Colour, + cursor: Colour +} + +Config :: struct { + font: string, + font_size: int, + colours: Colours, +} + +// Application state +App :: struct { + files: [dynamic]FileBuffer, + active_tab: int, + fonts: [dynamic]FontConfig, + current_font_index: int, + line_height: f32, + tab_height: f32, + window_width: i32, + window_height: i32, + text_area_y: f32, + line_number_width: f32, + mouse_drag_start: int, + is_dragging: bool, + copied_text: string, + config: Config, +}