// Todos: // - move by word // - select text // - copy selected text // - cut selected text // - move line up // - move line down // - duplicate line // - refactor to make cursor_x be aligned with characters themselves FPS :: 60; MS_PER_FRAME :: 1000 / FPS; State :: struct { file_name: string; cursor_x: int; cursor_y: int; window_width: int; window_height: int; buffer: string; lines: [..][..]u8; line_height: int; first_line: int; my_font: *Simp.Dynamic_Font; dt: int; } White :: Vector4.{1, 1, 1, 1}; Special_Character_Lookup : [10]u8 : .[ xx #char ")", xx #char "!", xx #char "@", xx #char "#", xx #char "$", xx #char "%", xx #char "^", xx #char "&", xx #char "*", xx #char "(", ]; run_editor :: (file_name: string) { state: State; state.window_width = 800; state.window_height = 600; state.file_name = file_name; window := create_window(state.window_width, state.window_height, state.file_name); my_init_fonts(*state); Simp.set_render_target(window); should_quit := false; prev := get_time(); assert(read_file_lines(*state, state.file_name), "Must be able to read file!"); while !should_quit { reset_temporary_storage(); now := get_time(); state.dt = now - prev; prev = now; Input.update_window_events(); handle_window_resizes(*state, window); for event : Input.events_this_frame { if event.type == .QUIT { should_quit = true; break; } if event.type == .KEYBOARD && event.key_pressed { if event.key_code == { case .ESCAPE; should_quit = true; case .BACKSPACE; handle_backspace(*state); case .ENTER; handle_enter(*state); case .ARROW_UP; #through; case .ARROW_DOWN; #through; case .ARROW_LEFT; #through; case .ARROW_RIGHT; handle_arrow(*state, event); } if #char "A" <= event.key_code && event.key_code <= #char "z" { key := event.key_code; if !event.shift_pressed { key += #char "a" - #char "A"; } if event.ctrl_pressed { if event.key_code == { case #char "S"; #through; case #char "s"; handle_save(*state); } } else { insert_character_at_cursor(*state, cast(u8) key); } } else if 33 <= event.key_code && event.key_code <= 126 { // NOTE: this doesn't quite work for some keys, like ` or [] key := event.key_code; insert_character_at_cursor(*state, cast(u8) key); } } } Simp.clear_render_target(.0, .0, .0, 1); render_edit_buffer(state); Simp.swap_buffers(window); now = get_time(); if now - prev < MS_PER_FRAME { sleep_milliseconds(xx (now - prev)); } } } render_edit_buffer :: (using state: State) { visible_lines := get_visible_lines(state); for line_buffer: visible_lines { Simp.set_shader_for_text(); line_str := array_view_to_string(line_buffer); prefix: string; suffix: string; has_cursor_on_line := it_index == cursor_y; if has_cursor_on_line { prefix, suffix = string_to_line(line_str, cursor_x); } else { prefix, suffix = string_to_line(line_str); } prefix_width := Simp.prepare_text(my_font, prefix); top_of_line := window_height - (it_index + 1) * line_height; Left_Max :: 5; Simp.draw_prepared_text(my_font, Left_Max, top_of_line, White); if has_cursor_on_line { Simp.set_shader_for_color(); start := ifx cursor_x == 0 then Left_Max else prefix_width + Left_Max; Simp.immediate_quad( cast(float) start, cast(float) top_of_line + line_height, cast(float) start + 1, cast(float) top_of_line, White); Simp.set_shader_for_text(); Simp.prepare_text(my_font, suffix); Simp.draw_prepared_text(my_font, start, top_of_line, White); } } } insert_character_at_cursor :: (using state: *State, char: u8) { line := *get_visible_lines(state)[cursor_y]; array_insert_at(line, char, cursor_x); cursor_x += 1; } handle_save :: (using state: *State) { out_builder: String_Builder; init_string_builder(*out_builder); for line: lines { line_str := string.{count=line.count, data=line.data}; append(*out_builder, line_str); if it_index != lines.count - 1 { append(*out_builder, "\n"); } } content := builder_to_string(*out_builder); out_file_name := tprint("%.new", file_name); write_entire_file(out_file_name, content); } handle_window_resizes :: (using state: *State, window: Window_Type) { for Input.get_window_resizes() { Simp.update_window(it.window); if it.window == window { should_reinit := (it.width != window_width) || (it.height != window_height); window_width = it.width; window_height = it.height; if should_reinit { my_init_fonts(state); } } } } handle_enter :: (using state: *State) { cursor_line_index := cursor_y + first_line; current_line := *lines[cursor_line_index]; new_line := [..]u8.{}; array_add(*new_line, array_view(current_line.*, cursor_x)); array_insert_at(*lines, new_line, cursor_line_index + 1); current_line.count = cursor_x; handle_arrow(state, .ARROW_DOWN); cursor_x = 0; } handle_backspace :: (using state: *State) { if cursor_x == 0 && cursor_y == 0 && first_line == 0 { return; } cursor_line_index := cursor_y + first_line; current_line := *lines[cursor_line_index]; if cursor_x == 0 { prev_line := *lines[cursor_line_index - 1]; prev_count := prev_line.count; array_add(prev_line, current_line.*); array_remove_at(*lines, cursor_line_index); handle_arrow(state, .ARROW_UP); cursor_x = prev_count; } else { array_remove_at(current_line, cursor_x - 1); cursor_x -= 1; } } handle_arrow :: (state: *State, key_code: Input.Key_Code) { handle_arrow(state, .{key_code=key_code}); } handle_arrow :: (using state: *State, using event: Input.Event) { visible_lines := get_visible_lines(state); if visible_lines.count == 0 return; if key_code == { case .ARROW_UP; if cursor_y == 0 && first_line == 0 return; if cursor_y == 0 { first_line -= 1; } else { cursor_y -= 1; } case .ARROW_DOWN; if cursor_y == visible_lines.count - 1 { first_line = min(first_line + 1, lines.count); } else if cursor_y + first_line + visible_lines.count >= lines.count { } else { cursor_y += 1; } case .ARROW_LEFT; if cursor_x == 0 && cursor_y != 0 { prev_line := visible_lines[cursor_y - 1]; len := prev_line.count; cursor_y = max(0, cursor_y - 1); cursor_x = len; } else if cursor_x != 0 && ctrl_pressed { } else if cursor_x != 0 { cursor_x -= 1; } case .ARROW_RIGHT; line := visible_lines[cursor_y]; len := line.count; if cursor_x == len { cursor_x = 0; cursor_y += 1; } else if ctrl_pressed { } else { cursor_x += 1; } } } get_visible_lines :: (using state: State) -> [][..]u8 { if line_height <= 1 || window_height == 0 return .[]; num_lines_in_screen := window_height / line_height - 1; return array_view(lines, first_line, num_lines_in_screen); } string_to_line :: (str: string, cursor_char_index := -1) -> string, string { prefix_builder: String_Builder; prefix_builder.allocator = temporary_allocator; suffix_builder: String_Builder; suffix_builder.allocator = temporary_allocator; init_string_builder(*prefix_builder); init_string_builder(*suffix_builder); start := 0; char_index := 0; cursor_index := 0; current_builder := *prefix_builder; while char_index < str.count { defer char_index += 1; c := str.data[char_index]; if c == #char "\t" { // s := string.{data=str.data + start, count=(char_index + 1) - start}; // append(current_builder, s); // append(current_builder, " "); // start = char_index + 1; // cursor_index += 4; } else { cursor_index += 1; } if cursor_index == cursor_char_index { s := string.{data=str.data + start, count=(char_index + 1) - start}; append(current_builder, s); current_builder = *suffix_builder; start = char_index + 1; } } s := string.{data=str.data + start, count=str.count - start}; append(current_builder, s); prefix := builder_to_string(*prefix_builder,, temp); suffix := builder_to_string(*suffix_builder,, temp); return prefix, suffix; } get_time :: () -> s64 { val, ok := to_milliseconds(current_time_monotonic()); assert(ok, "Must be able to convert from apollo to milliseconds"); return val; } read_file_lines :: (using state: *State, file_path: string) -> bool { file_contents, ok := read_entire_file(file_path); if !ok return ok; for lines free(*it); free(lines.data); lines = [..][..]u8.{}; lines_temp := split(file_contents, "\n"); defer free(lines_temp.data); for line: lines_temp { builder: [..]u8; array_add(*builder, array_view(line)); array_add(*lines, builder); } return ok; } my_init_fonts :: (using state: *State) { line_height = 16; base_path := path_strip_filename(get_path_of_running_executable()); my_font = Simp.get_font_at_size("/home/grant/.local/share/fonts/otf/BerkeleyMono/", "BerkeleyMono-Regular.otf", line_height); // my_font = Simp.get_font_at_size(base_path, "Anonymous Pro.ttf", line_height); assert(my_font != null); } array_add :: (xs: *[..]$T, to_append: []T) { array_reserve(xs, xs.count + to_append.count); memcpy(xs.data + xs.count, to_append.data, to_append.count * size_of(T)); xs.count += to_append.count; } array_view :: (s: string) -> []u8 { return []u8.{count=s.count, data=s.data}; } array_view_to_string :: (chars: []u8) -> string { return string.{count=chars.count, data=chars.data}; } array_remove_at :: (xs: *[..]$T, index: int) { xs.count -= 1; memcpy(xs.data + index, xs.data + index + 1, xs.count - index); } #import "Basic"; #import "File"; #import "Math"; #import "String"; #import "System"; #import "Window_Creation"; Input :: #import "Input"; Simp :: #import "Simp";