From ccacafce64934eacd590db3a7794427859d067e1 Mon Sep 17 00:00:00 2001 From: Ronald Date: Fri, 6 Jun 2025 21:30:20 +0100 Subject: [PATCH] A very good start --- main.odin | 602 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 602 insertions(+) create mode 100644 main.odin diff --git a/main.odin b/main.odin new file mode 100644 index 0000000..83c3543 --- /dev/null +++ b/main.odin @@ -0,0 +1,602 @@ +package main + +import "core:fmt" +import "core:os" +import "core:strings" +import "core:slice" +import "core:unicode/utf8" +import "core:strconv" +import "core:math" +import "core:mem" + +import sdl "vendor:sdl2" +import ttf "vendor:sdl2/ttf" + +// Application constants +WINDOW_WIDTH :: 1200 +WINDOW_HEIGHT :: 800 +TAB_HEIGHT :: 30 +LINE_NUMBER_WIDTH :: 60 +FONT_SIZE :: 14 +TEXT_PADDING :: 10 + +// Colors +COLOR_BACKGROUND :: sdl.Color{30, 30, 30, 255} +COLOR_TEXT :: sdl.Color{220, 220, 220, 255} +COLOR_LINE_NUMBERS :: sdl.Color{150, 150, 150, 255} +COLOR_TAB_ACTIVE :: sdl.Color{60, 60, 60, 255} +COLOR_TAB_INACTIVE :: sdl.Color{45, 45, 45, 255} +COLOR_TAB_BORDER :: sdl.Color{80, 80, 80, 255} +COLOR_CURSOR :: sdl.Color{255, 255, 255, 255} +COLOR_LINE_NUMBER_BG :: sdl.Color{40, 40, 40, 255} + +// Text buffer for a single file +Text_Buffer :: struct { + lines: [dynamic]string, + filename: string, + modified: bool, + cursor_line: int, + cursor_col: int, + scroll_y: int, + max_line_width: int, +} + +// Tab structure +Tab :: struct { + name: string, + buffer: Text_Buffer, + rect: sdl.Rect, +} + +// Application state +App :: struct { + window: ^sdl.Window, + renderer: ^sdl.Renderer, + font: ^ttf.Font, + tabs: [dynamic]Tab, + active_tab: int, + running: bool, + char_width: i32, + char_height: i32, + text_area_width: i32, + visible_lines: int, + cursor_blink_timer: u32, + show_cursor: bool, +} + +app: App + +main :: proc() { + // Get command line arguments + args := os.args[1:] + + if init_sdl() != 0 { + fmt.println("Failed to initialize SDL") + return + } + defer cleanup_sdl() + + // Create tabs from command line arguments + if len(args) == 0 { + // Create empty tab if no files specified + create_tab("Untitled", "") + } else { + for filename in args { + content := "" + if data, ok := os.read_entire_file(filename); ok { + content = string(data) + } else { + fmt.printf("Warning: Could not read file '%s'\n", filename) + content = fmt.tprintf("Error: Could not read file '%s'", filename) + } + + // Extract filename for tab + tab_name := filename + if idx := strings.last_index(filename, "/"); idx != -1 { + tab_name = filename[idx+1:] + } + if idx := strings.last_index(tab_name, "\\"); idx != -1 { + tab_name = tab_name[idx+1:] + } + + create_tab(tab_name, content) + } + } + + app.active_tab = 0 + calculate_layout() + + // Main loop + app.running = true + for app.running { + handle_events() + render() + sdl.Delay(16) // ~60 FPS + } +} + +init_sdl :: proc() -> int { + if sdl.Init(sdl.INIT_VIDEO) < 0 { + fmt.printf("SDL could not initialize: %s\n", sdl.GetError()) + return -1 + } + + if ttf.Init() < 0 { + fmt.printf("TTF could not initialize: %s\n", ttf.GetError()) + return -1 + } + + app.window = sdl.CreateWindow( + "Multi-Tab Text Editor", + sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED, + WINDOW_WIDTH, WINDOW_HEIGHT, + sdl.WINDOW_SHOWN | sdl.WINDOW_RESIZABLE + ) + + if app.window == nil { + fmt.printf("Window could not be created: %s\n", sdl.GetError()) + return -1 + } + + app.renderer = sdl.CreateRenderer(app.window, -1, sdl.RENDERER_ACCELERATED) + if app.renderer == nil { + fmt.printf("Renderer could not be created: %s\n", sdl.GetError()) + return -1 + } + + // Load font - try system fonts first, fallback to a basic one + font_paths := []string{ + "/usr/share/fonts/TTF/FiraCode-Regular.ttf" + } + + for path in font_paths { + app.font = ttf.OpenFont(strings.clone_to_cstring(path), FONT_SIZE) + if app.font != nil do break + } + + if app.font == nil { + fmt.println("Could not load any font - text rendering will fail") + return -1 + } + + // Calculate character dimensions + w, h: i32 + if ttf.SizeText(app.font, "W", &w, &h) == 0 { + app.char_width = w + app.char_height = h + } else { + app.char_width = 8 + app.char_height = 16 + } + + return 0 +} + +cleanup_sdl :: proc() { + if app.font != nil do ttf.CloseFont(app.font) + if app.renderer != nil do sdl.DestroyRenderer(app.renderer) + if app.window != nil do sdl.DestroyWindow(app.window) + + for &tab in app.tabs { + delete(tab.buffer.lines) + } + delete(app.tabs) + + ttf.Quit() + sdl.Quit() +} + +create_tab :: proc(name: string, content: string) { + tab := Tab{ + name = strings.clone(name), + buffer = Text_Buffer{}, + } + + tab.buffer.filename = strings.clone(name) + tab.buffer.lines = make([dynamic]string) + + // Split content into lines + if len(content) > 0 { + lines := strings.split(content, "\n") + defer delete(lines) + for line in lines { + append(&tab.buffer.lines, strings.clone(line)) + } + } else { + append(&tab.buffer.lines, "") + } + + append(&app.tabs, tab) +} + +calculate_layout :: proc() { + w, h: i32 + sdl.GetWindowSize(app.window, &w, &h) + + app.text_area_width = w - LINE_NUMBER_WIDTH + app.visible_lines = int((h - TAB_HEIGHT) / app.char_height) - 1 + + // Calculate tab rectangles + if len(app.tabs) > 0 { + tab_width := w / i32(len(app.tabs)) + for i in 0.. 500 { + app.show_cursor = !app.show_cursor + app.cursor_blink_timer = current_time + } +} + +handle_key_down :: proc(scancode: sdl.Scancode, modifiers: sdl.Keymod) { + if len(app.tabs) == 0 do return + + buffer := &app.tabs[app.active_tab].buffer + + left_ctrl_pressed := (modifiers & sdl.KMOD_CTRL) == sdl.KMOD_LCTRL + right_ctrl_pressed := (modifiers & sdl.KMOD_CTRL) == sdl.KMOD_RCTRL + + #partial switch scancode { + case .S: + if left_ctrl_pressed || right_ctrl_pressed { + // TODO: Tidy up this code and optimize it for big files + filename := buffer.filename + + builder := strings.builder_make() + defer strings.builder_destroy(&builder) + + for line in buffer.lines { + strings.write_string(&builder, line) + strings.write_rune(&builder, '\n') + } + str := strings.to_string(builder) + data := transmute([]u8)str + + ok := os.write_entire_file(filename, data) + } + case .TAB: + // Switch tabs + app.active_tab = (app.active_tab + 1) % len(app.tabs) + + case .RETURN: + // Insert new line + if buffer.cursor_line < len(buffer.lines) { + current_line := buffer.lines[buffer.cursor_line] + left_part := current_line[:buffer.cursor_col] if buffer.cursor_col <= len(current_line) else current_line + right_part := current_line[buffer.cursor_col:] if buffer.cursor_col <= len(current_line) else "" + + buffer.lines[buffer.cursor_line] = strings.clone(left_part) + inject_at(&buffer.lines, buffer.cursor_line + 1, strings.clone(right_part)) + + buffer.cursor_line += 1 + buffer.cursor_col = 0 + buffer.modified = true + } + + case .BACKSPACE: + if buffer.cursor_col > 0 { + // Remove character + line := &buffer.lines[buffer.cursor_line] + if buffer.cursor_col <= len(line^) { + new_line := strings.concatenate({line^[:buffer.cursor_col-1], line^[buffer.cursor_col:]}) + delete(line^) + line^ = new_line + buffer.cursor_col -= 1 + buffer.modified = true + } + } else if buffer.cursor_line > 0 { + // Join with previous line + current_line := buffer.lines[buffer.cursor_line] + prev_line := &buffer.lines[buffer.cursor_line - 1] + buffer.cursor_col = len(prev_line^) + + new_line := strings.concatenate({prev_line^, current_line}) + delete(prev_line^) + prev_line^ = new_line + + ordered_remove(&buffer.lines, buffer.cursor_line) + buffer.cursor_line -= 1 + buffer.modified = true + } + + case .DELETE: + if buffer.cursor_line < len(buffer.lines) { + line := &buffer.lines[buffer.cursor_line] + if buffer.cursor_col < len(line^) { + new_line := strings.concatenate({line^[:buffer.cursor_col], line^[buffer.cursor_col+1:]}) + delete(line^) + line^ = new_line + buffer.modified = true + } + } + + case .LEFT: + if buffer.cursor_col > 0 { + buffer.cursor_col -= 1 + } else if buffer.cursor_line > 0 { + buffer.cursor_line -= 1 + buffer.cursor_col = len(buffer.lines[buffer.cursor_line]) + } + + case .RIGHT: + if buffer.cursor_line < len(buffer.lines) { + if buffer.cursor_col < len(buffer.lines[buffer.cursor_line]) { + buffer.cursor_col += 1 + } else if buffer.cursor_line < len(buffer.lines) - 1 { + buffer.cursor_line += 1 + buffer.cursor_col = 0 + } + } + + case .UP: + if buffer.cursor_line > 0 { + buffer.cursor_line -= 1 + line_len := len(buffer.lines[buffer.cursor_line]) + if buffer.cursor_col > line_len { + buffer.cursor_col = line_len + } + } + + case .DOWN: + if buffer.cursor_line < len(buffer.lines) - 1 { + buffer.cursor_line += 1 + line_len := len(buffer.lines[buffer.cursor_line]) + if buffer.cursor_col > line_len { + buffer.cursor_col = line_len + } + } + + case .HOME: + buffer.cursor_col = 0 + + case .END: + if buffer.cursor_line < len(buffer.lines) { + buffer.cursor_col = len(buffer.lines[buffer.cursor_line]) + } + + case .PAGEUP: + buffer.cursor_line = max(0, buffer.cursor_line - app.visible_lines) + buffer.scroll_y = max(0, buffer.scroll_y - app.visible_lines) + + case .PAGEDOWN: + buffer.cursor_line = min(len(buffer.lines) - 1, buffer.cursor_line + app.visible_lines) + buffer.scroll_y = min(len(buffer.lines) - app.visible_lines, buffer.scroll_y + app.visible_lines) + } + + // Reset cursor blink + app.cursor_blink_timer = sdl.GetTicks() + app.show_cursor = true +} + +handle_text_input :: proc(text: string) { + if len(app.tabs) == 0 do return + + buffer := &app.tabs[app.active_tab].buffer + + if buffer.cursor_line < len(buffer.lines) { + line := &buffer.lines[buffer.cursor_line] + if buffer.cursor_col <= len(line^) { + new_line := strings.concatenate({ + line^[:buffer.cursor_col], + text, + line^[buffer.cursor_col:] + }) + delete(line^) + line^ = new_line + buffer.cursor_col += len(text) + buffer.modified = true + } + } + + // Reset cursor blink + app.cursor_blink_timer = sdl.GetTicks() + app.show_cursor = true +} + +handle_mouse_click :: proc(x, y: i32) { + // Check if click is on tabs + if y <= TAB_HEIGHT { + for i in 0..= tab.rect.x && x < tab.rect.x + tab.rect.w { + app.active_tab = i + return + } + } + } + + // Click in text area + if len(app.tabs) > 0 && y > TAB_HEIGHT { + buffer := &app.tabs[app.active_tab].buffer + + // Calculate line and column + line := int((y - TAB_HEIGHT) / app.char_height) + buffer.scroll_y + col := int((x - LINE_NUMBER_WIDTH) / app.char_width) + + if line >= 0 && line < len(buffer.lines) { + buffer.cursor_line = line + line_len := len(buffer.lines[line]) + buffer.cursor_col = min(col, line_len) + } + + // Reset cursor blink + app.cursor_blink_timer = sdl.GetTicks() + app.show_cursor = true + } +} + +handle_scroll :: proc(y: i32) { + if len(app.tabs) == 0 do return + + buffer := &app.tabs[app.active_tab].buffer + buffer.scroll_y = max(0, min(len(buffer.lines) - app.visible_lines, buffer.scroll_y - int(y * 3))) +} + +render :: proc() { + sdl.SetRenderDrawColor(app.renderer, COLOR_BACKGROUND.r, COLOR_BACKGROUND.g, COLOR_BACKGROUND.b, COLOR_BACKGROUND.a) + sdl.RenderClear(app.renderer) + + render_tabs() + + if len(app.tabs) > 0 { + render_text_area() + } + + sdl.RenderPresent(app.renderer) +} + +render_tabs :: proc() { + for i in 0..= buffer.scroll_y + app.visible_lines { + buffer.scroll_y = buffer.cursor_line - app.visible_lines + 1 + } + + // Render visible lines + start_line := max(0, buffer.scroll_y) + end_line := min(len(buffer.lines), start_line + app.visible_lines) + + for i in start_line.. 0 { + test_line = strings.concatenate({current_line, " ", word}) + } else { + test_line = word + } + + if len(test_line) <= chars_per_line { + current_line = test_line + } else { + // Render current line and start new one + if len(current_line) > 0 { + render_text(current_line, x, line_y, COLOR_TEXT) + line_y += app.char_height + } + current_line = word + } + } + + // Render final line + if len(current_line) > 0 { + render_text(current_line, x, line_y, COLOR_TEXT) + } +} + +