/* - TODO implement movement up and down by paragraph - TODO implement scrolling/viewport - TODO implement deletion by word - TODO implement selection - TODO implement duplicate selection/line - TODO implement move selection/line up/down - TODO implement copy/cut/paste - TODO implement search - TODO implement file picker - TODO implement "execute" command */ package main import "core:fmt" import "core:mem" import "core:os" import "core:strings" import utf8 "core:unicode/utf8" import r "vendor:raylib" read_file :: proc(path: string) -> (string, bool) { content: []u8 success: bool content, success = os.read_entire_file(path) return string(content), success } Cursor :: struct { line: int, char: int, } font_size: f32 = 20.0 text_height: f32 cursor: Cursor lines: [dynamic][dynamic]u8 main :: proc() { r.InitWindow(800, 600, "odit") r.SetTargetFPS(60) font := r.LoadFont("/Users/grant/Library/Fonts/BerkeleyMono-Regular.otf") r.GuiSetFont(font) two_lines_size := r.MeasureTextEx(font, "foo\nbar", font_size, 0.0) text_height = two_lines_size[1] / 2 content, _success := read_file("src/main.odin") for line in strings.split_lines_iterator(&content) { bs := make([dynamic]u8, 0, len(line) == 0 ? 1 : len(line) * 2) append(&bs, line) append(&lines, bs) } text: string dirty := true for !r.WindowShouldClose() { for c := r.GetCharPressed(); c != rune(0); c = r.GetCharPressed() { chars, _ := utf8.encode_rune(c) char := chars[0] inject_at(&lines[cursor.line], cursor.char, char) cursor.char += 1 } if repeatable_key_pressed(r.KeyboardKey.RIGHT) { preferred_position := cursor.char + 1 if cursor.char == len(current_line()) && cursor.line != len(lines) - 1 { cursor.char = 0 cursor.line += 1 } if r.IsKeyDown(r.KeyboardKey.LEFT_ALT) && cursor.char + 1 < len(current_line()) { seen_space := false for c, c_index in current_line()[cursor.char + 1:] { if seen_space && !is_whitespace(c) { preferred_position = cursor.char + c_index break } if is_whitespace(c) { seen_space = true } } if !seen_space { preferred_position = len(current_line()) } } cursor.char = min(preferred_position, len(current_line())) } if repeatable_key_pressed(r.KeyboardKey.LEFT) { preferred_position := cursor.char - 1 if cursor.char == 0 && cursor.line != 0 { cursor.line -= 1 cursor.char = len(current_line()) } if r.IsKeyDown(r.KeyboardKey.LEFT_ALT) && 0 < cursor.char { seen_space := false #reverse for c, c_index in current_line()[:cursor.char - 1] { if is_whitespace(c) { seen_space = true preferred_position = c_index + 1 break } } if !seen_space { preferred_position = 0 } } cursor.char = max(preferred_position, 0) } if repeatable_key_pressed(r.KeyboardKey.UP) { preferred_line := cursor.line - 1 if r.IsKeyDown(r.KeyboardKey.LEFT_ALT) && 0 < cursor.line { #reverse for l, l_index in lines[:cursor.line] { if len(l) == 0 || all_whitespace(l[:]) { preferred_line = l_index break } } } cursor.line = max(0, preferred_line) if len(current_line()) < cursor.char { cursor.char = len(current_line()) } } if repeatable_key_pressed(r.KeyboardKey.DOWN) { preferred_line := cursor.line + 1 if r.IsKeyDown(r.KeyboardKey.LEFT_ALT) && cursor.line + 1 < len(lines) { for l, l_index in lines[cursor.line + 1:] { if len(l) == 0 || all_whitespace(l[:]) { preferred_line = cursor.line + l_index + 1 break } } } cursor.line = min(preferred_line, len(lines)) if len(current_line()) < cursor.char { cursor.char = len(current_line()) } } if repeatable_key_pressed(r.KeyboardKey.ENTER) { new_line_cap := len(current_line()) - cursor.char bs := make([dynamic]u8, 0, len(current_line()) == 0 ? 1 : new_line_cap * 2) if cursor.char < len(current_line()) { for c, i in current_line()[cursor.char:] { append(&bs, c) } } inject_at(&lines, cursor.line + 1, bs) resize(current_line(), cursor.char) cursor.line += 1 cursor.char = 0 } if r.IsKeyDown(r.KeyboardKey.LEFT_SUPER) && r.IsKeyPressed(r.KeyboardKey.S) { builder := strings.builder_make() defer strings.builder_destroy(&builder) for line in lines { strings.write_bytes(&builder, line[:]) strings.write_byte(&builder, '\n') } os.write_entire_file("foo.txt", transmute([]u8)strings.to_string(builder)) fmt.println("Wrote file!") } if repeatable_key_pressed(r.KeyboardKey.BACKSPACE) { if cursor.line == 0 && cursor.char == 0 do continue if cursor.char == 0 { old_len := len(lines[cursor.line - 1]) append(&lines[cursor.line - 1], string(current_line()[:])) line_to_remove := current_line()^ defer delete(line_to_remove) ordered_remove(&lines, cursor.line) cursor.line -= 1 cursor.char = old_len } else { ordered_remove(&lines[cursor.line], cursor.char - 1) cursor.char -= 1 } } r.BeginDrawing() r.ClearBackground(r.BLACK) for &line, line_index in lines { if cap(line) == 0 { reserve(&line, 1) } if len(line) == cap(line) { reserve(&line, len(line) * 2) } raw_data(line)[len(line)] = 0 cstr := strings.unsafe_string_to_cstring(string(line[:])) pos := r.Vector2{0, font_size * f32(line_index)} r.DrawTextEx(font, cstr, pos, font_size, 0, r.WHITE) } render_cursor(&cursor) r.DrawFPS(10, 10) r.EndDrawing() free_all(context.temp_allocator) dirty = false } } render_cursor :: proc(cursor: ^Cursor) { x := cursor.char * int(font_size / 2) y := i32(font_size) * i32(cursor.line) r.DrawLine(i32(x), y, i32(x), y + i32(text_height), r.WHITE) r.DrawLine(i32(x) + 1, y, i32(x) + 1, y + i32(text_height), r.WHITE) } current_line :: proc() -> ^[dynamic]u8 { return &lines[cursor.line] } is_whitespace :: proc(c: u8) -> bool { return c == ' ' || c == '\t' || c == '\n' } all_whitespace :: proc(cs: []u8) -> bool { for c in cs { if !is_whitespace(c) { return false } } return true } repeatable_key_pressed :: proc(k: r.KeyboardKey) -> bool { return r.IsKeyPressed(k) || r.IsKeyPressedRepeat(k) }