package main import "core:fmt" import "core:log" import "core:math" import "core:mem" import "core:os" import "core:slice" import "core:strconv" import "core:strings" import "core:unicode/utf8" 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 app: App main :: proc() { context.logger = log.create_console_logger(.Debug) defer log.destroy_console_logger(context.logger) // Get command line arguments args := os.args[1:] ok := false app.config, ok = parse_config("config.ini") if !ok { log.fatal("failed to parse config file") return } defer config_destroy(app.config) log.debug("app config:") log.debug(app.config) if init_sdl() != 0 { log.fatal("failed to initialize sdl") return } defer cleanup_sdl() sdl.SetWindowMinimumSize(app.window, 300, 400) // 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( "notepad", 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 } font_cstring := strings.clone_to_cstring(app.config.font) defer delete(font_cstring) app.font = ttf.OpenFont(font_cstring, i32(app.config.font_size)) if app.font == nil { log.fatal("Could not load font") 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, app.config.colours.background.r, app.config.colours.background.g, app.config.colours.background.b, app.config.colours.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, app.config.colours.text) line_y += app.char_height } current_line = word } } // Render final line if len(current_line) > 0 { render_text(current_line, x, line_y, app.config.colours.text) } }