package main import "core:c" import "core:os" import "core:path/filepath" import "core:math" import "core:strings" import "base:runtime" import "core:fmt" import "core:log" import "core:mem" import "core:slice" import "vendor:sdl2" import "vendor:sdl2/ttf" import "core" import "theme" import "ui" HardcodedFontPath :: "bin/JetBrainsMono-Regular.ttf"; State :: core.State; FileBuffer :: core.FileBuffer; // TODO: should probably go into state scratch: mem.Scratch; scratch_alloc: runtime.Allocator; state := core.State {}; do_normal_mode :: proc(state: ^State, buffer: ^FileBuffer) { } do_insert_mode :: proc(state: ^State, buffer: ^FileBuffer) { key := 0; for key > 0 { if key >= 32 && key <= 125 && len(buffer.input_buffer) < 1024-1 { append(&buffer.input_buffer, u8(key)); } key = 0; } } do_visual_mode :: proc(state: ^State, buffer: ^FileBuffer) { } register_default_leader_actions :: proc(input_map: ^core.InputActions) { core.register_key_action(input_map, .Q, proc(state: ^State) { state.current_input_map = &state.input_map.mode[state.mode]; }, "close this help"); } register_default_go_actions :: proc(input_map: ^core.InputActions) { core.register_key_action(input_map, .H, proc(state: ^State) { core.move_cursor_start_of_line(core.current_buffer(state)); state.current_input_map = &state.input_map.mode[state.mode]; }, "move to beginning of line"); core.register_key_action(input_map, .L, proc(state: ^State) { core.move_cursor_end_of_line(core.current_buffer(state)); state.current_input_map = &state.input_map.mode[state.mode]; }, "move to end of line"); } register_default_input_actions :: proc(input_map: ^core.InputActions) { // Cursor Movement { core.register_key_action(input_map, .W, proc(state: ^State) { core.move_cursor_forward_start_of_word(core.current_buffer(state)); }, "move forward one word"); core.register_key_action(input_map, .E, proc(state: ^State) { core.move_cursor_forward_end_of_word(core.current_buffer(state)); }, "move forward to end of word"); core.register_key_action(input_map, .B, proc(state: ^State) { core.move_cursor_backward_start_of_word(core.current_buffer(state)); }, "move backward one word"); core.register_key_action(input_map, .K, proc(state: ^State) { core.move_cursor_up(core.current_buffer(state)); }, "move up one line"); core.register_key_action(input_map, .J, proc(state: ^State) { core.move_cursor_down(core.current_buffer(state)); }, "move down one line"); core.register_key_action(input_map, .H, proc(state: ^State) { core.move_cursor_left(core.current_buffer(state)); }, "move left one char"); core.register_key_action(input_map, .L, proc(state: ^State) { core.move_cursor_right(core.current_buffer(state)); }, "move right one char"); core.register_ctrl_key_action(input_map, .U, proc(state: ^State) { core.scroll_file_buffer(core.current_buffer(state), .Up); }, "scroll buffer up"); core.register_ctrl_key_action(input_map, .D, proc(state: ^State) { core.scroll_file_buffer(core.current_buffer(state), .Down); }, "scroll buffer up"); } // Scale font size { core.register_ctrl_key_action(input_map, .MINUS, proc(state: ^State) { if state.source_font_height > 16 { state.source_font_height -= 2; state.source_font_width = state.source_font_height / 2; state.font_atlas = core.gen_font_atlas(state, HardcodedFontPath); } log.debug(state.source_font_height); }, "increase font size"); core.register_ctrl_key_action(input_map, .EQUAL, proc(state: ^State) { state.source_font_height += 2; state.source_font_width = state.source_font_height / 2; state.font_atlas = core.gen_font_atlas(state, HardcodedFontPath); }, "decrease font size"); } core.register_key_action(input_map, .SPACE, core.new_input_actions(), "leader commands"); register_default_leader_actions(&(&input_map.key_actions[.SPACE]).action.(core.InputActions)); core.register_key_action(input_map, .G, core.new_input_actions(), "Go commands"); register_default_go_actions(&(&input_map.key_actions[.G]).action.(core.InputActions)); core.register_key_action(&state.input_map.mode[.Normal], .V, proc(state: ^State) { state.mode = .Visual; state.current_input_map = &state.input_map.mode[.Visual]; core.current_buffer(state).selection = core.new_selection(core.current_buffer(state).cursor); }, "enter visual mode"); } register_default_visual_actions :: proc(input_map: ^core.InputActions) { core.register_key_action(input_map, .ESCAPE, proc(state: ^State) { state.mode = .Normal; state.current_input_map = &state.input_map.mode[.Normal]; core.current_buffer(state).selection = nil; core.update_file_buffer_scroll(core.current_buffer(state)) }, "exit visual mode"); // Cursor Movement { core.register_key_action(input_map, .W, proc(state: ^State) { sel_cur := &(core.current_buffer(state).selection.?); core.move_cursor_forward_start_of_word(core.current_buffer(state), cursor = &sel_cur.end); }, "move forward one word"); core.register_key_action(input_map, .E, proc(state: ^State) { sel_cur := &(core.current_buffer(state).selection.?); core.move_cursor_forward_end_of_word(core.current_buffer(state), cursor = &sel_cur.end); }, "move forward to end of word"); core.register_key_action(input_map, .B, proc(state: ^State) { sel_cur := &(core.current_buffer(state).selection.?); core.move_cursor_backward_start_of_word(core.current_buffer(state), cursor = &sel_cur.end); }, "move backward one word"); core.register_key_action(input_map, .K, proc(state: ^State) { sel_cur := &(core.current_buffer(state).selection.?); core.move_cursor_up(core.current_buffer(state), cursor = &sel_cur.end); }, "move up one line"); core.register_key_action(input_map, .J, proc(state: ^State) { sel_cur := &(core.current_buffer(state).selection.?); core.move_cursor_down(core.current_buffer(state), cursor = &sel_cur.end); }, "move down one line"); core.register_key_action(input_map, .H, proc(state: ^State) { sel_cur := &(core.current_buffer(state).selection.?); core.move_cursor_left(core.current_buffer(state), cursor = &sel_cur.end); }, "move left one char"); core.register_key_action(input_map, .L, proc(state: ^State) { sel_cur := &(core.current_buffer(state).selection.?); core.move_cursor_right(core.current_buffer(state), cursor = &sel_cur.end); }, "move right one char"); core.register_ctrl_key_action(input_map, .U, proc(state: ^State) { sel_cur := &(core.current_buffer(state).selection.?); core.scroll_file_buffer(core.current_buffer(state), .Up, cursor = &sel_cur.end); }, "scroll buffer up"); core.register_ctrl_key_action(input_map, .D, proc(state: ^State) { sel_cur := &(core.current_buffer(state).selection.?); core.scroll_file_buffer(core.current_buffer(state), .Down, cursor = &sel_cur.end); }, "scroll buffer up"); } // Text Modification { core.register_key_action(input_map, .D, proc(state: ^State) { sel_cur := &(core.current_buffer(state).selection.?); core.delete_content(core.current_buffer(state), sel_cur); core.current_buffer(state).selection = nil; core.update_file_buffer_scroll(core.current_buffer(state)) state.mode = .Normal state.current_input_map = &state.input_map.mode[.Normal]; }, "delete selection"); core.register_key_action(input_map, .C, proc(state: ^State) { sel_cur := &(core.current_buffer(state).selection.?); core.delete_content(core.current_buffer(state), sel_cur); core.current_buffer(state).selection = nil; core.update_file_buffer_scroll(core.current_buffer(state)) state.mode = .Insert state.current_input_map = &state.input_map.mode[.Normal]; sdl2.StartTextInput(); }, "change selection"); } } register_default_text_input_actions :: proc(input_map: ^core.InputActions) { core.register_key_action(input_map, .I, proc(state: ^State) { state.mode = .Insert; sdl2.StartTextInput(); }, "enter insert mode"); core.register_key_action(input_map, .A, proc(state: ^State) { core.move_cursor_right(core.current_buffer(state), false); state.mode = .Insert; sdl2.StartTextInput(); }, "enter insert mode after character (append)"); // TODO: add shift+o to insert newline above current one core.register_key_action(input_map, .O, proc(state: ^State) { core.move_cursor_end_of_line(core.current_buffer(state), false); core.insert_content(core.current_buffer(state), []u8{'\n'}); state.mode = .Insert; sdl2.StartTextInput(); }, "insert mode on newline"); } ui_font_width :: proc() -> i32 { return i32(state.source_font_width); } ui_font_height :: proc() -> i32 { return i32(state.source_font_height); } draw :: proc(state: ^State) { if buffer := core.current_buffer(state); buffer != nil { buffer.glyph_buffer_height = math.min(256, int((state.screen_height - state.source_font_height*2) / state.source_font_height)) + 1; buffer.glyph_buffer_width = math.min(256, int((state.screen_width - state.source_font_width) / state.source_font_width)); } render_color := theme.get_palette_color(.Background); sdl2.SetRenderDrawColor(state.sdl_renderer, render_color.r, render_color.g, render_color.b, render_color.a); sdl2.RenderClear(state.sdl_renderer); new_ui := transmute(^ui.State)state.ui new_ui.max_size.x = state.screen_width new_ui.max_size.y = state.screen_height // TODO: use the new panels stuff if file_buffer := core.current_buffer(state); file_buffer != nil { ui_file_buffer(new_ui, file_buffer) } ui.compute_layout_2(new_ui) ui.draw(new_ui, state) if state.mode != .Insert && state.current_input_map != &state.input_map.mode[state.mode] { longest_description := 0; for key, action in state.current_input_map.key_actions { if len(action.description) > longest_description { longest_description = len(action.description); } } for key, action in state.current_input_map.ctrl_key_actions { if len(action.description) > longest_description { longest_description = len(action.description); } } longest_description += 8; helper_height := state.source_font_height * (len(state.current_input_map.key_actions) + len(state.current_input_map.ctrl_key_actions)); offset_from_bottom := state.source_font_height * 2; core.draw_rect( state, state.screen_width - longest_description * state.source_font_width, state.screen_height - helper_height - offset_from_bottom, longest_description*state.source_font_width, helper_height, .Background2 ); index := 0; for key, action in state.current_input_map.key_actions { core.draw_text( state, fmt.tprintf("%s - %s", key, action.description), state.screen_width - longest_description * state.source_font_width, state.screen_height - helper_height + index * state.source_font_height - offset_from_bottom ); index += 1; } for key, action in state.current_input_map.ctrl_key_actions { core.draw_text( state, fmt.tprintf("-%s - %s", key, action.description), state.screen_width - longest_description * state.source_font_width, state.screen_height - helper_height + index * state.source_font_height - offset_from_bottom ); index += 1; } } sdl2.RenderPresent(state.sdl_renderer); } expose_event_watcher :: proc "c" (state: rawptr, event: ^sdl2.Event) -> i32 { if event.type == .WINDOWEVENT { state := transmute(^State)state; context = state.ctx; if event.window.event == .EXPOSED { //draw(state); } else if event.window.event == .SIZE_CHANGED { w,h: i32; sdl2.GetRendererOutputSize(state.sdl_renderer, &w, &h); state.screen_width = int(w); state.screen_height = int(h); state.width_dpi_ratio = f32(w) / f32(event.window.data1); state.height_dpi_ratio = f32(h) / f32(event.window.data2); // KDE resizes very slowly on linux if you trigger a re-render when ODIN_OS != .Linux { draw(state); } } } return 0; } ui_file_buffer :: proc(s: ^ui.State, buffer: ^FileBuffer) { draw_func := proc(state: ^State, e: ui.UI_Element, user_data: rawptr) { buffer := transmute(^FileBuffer)user_data; if buffer != nil { buffer.glyph_buffer_width = e.layout.size.x / state.source_font_width; buffer.glyph_buffer_height = e.layout.size.y / state.source_font_height + 1; core.draw_file_buffer(state, buffer, e.layout.pos.x, e.layout.pos.y); } }; relative_file_path, _ := filepath.rel(state.directory, buffer.file_path, context.temp_allocator) ui.open_element(s, nil, { dir = .TopToBottom, kind = {ui.Grow{}, ui.Grow{}}, }) { ui.open_element(s, ui.UI_Element_Kind_Custom{fn = draw_func, user_data = transmute(rawptr)buffer}, { kind = {ui.Grow{}, ui.Grow{}} }) ui.close_element(s) ui.open_element(s, nil, { kind = {ui.Grow{}, ui.Exact(state.source_font_height)} }) { ui.open_element(s, fmt.tprintf("%s", state.mode), {}) ui.close_element(s) } ui.close_element(s) } ui.close_element(s) } main :: proc() { _command_arena: mem.Arena mem.arena_init(&_command_arena, make([]u8, 1024*1024)); state = State { ctx = context, screen_width = 640, screen_height = 480, source_font_width = 8, source_font_height = 16, input_map = core.new_input_map(), commands = make(core.EditorCommandList), command_arena = mem.arena_allocator(&_command_arena), panel_catalog = make([dynamic]core.PanelId), directory = os.get_current_directory(), log_buffer = core.new_virtual_file_buffer(context.allocator), }; // context.logger = core.new_logger(&state.log_buffer); context.logger = log.create_console_logger(); state.ctx = context; state.ui = &ui.State { curr_elements = make([]ui.UI_Element, 8192), prev_elements = make([]ui.UI_Element, 8192), } // TODO: don't use this mem.scratch_allocator_init(&scratch, 1024*1024); scratch_alloc = mem.scratch_allocator(&scratch); state.current_input_map = &state.input_map.mode[.Normal]; register_default_input_actions(&state.input_map.mode[.Normal]); register_default_visual_actions(&state.input_map.mode[.Visual]); register_default_text_input_actions(&state.input_map.mode[.Normal]); // core.register_editor_command( // &state.commands, // "nl.spacegirl.editor.core", // "Open New Panel", // "Opens a new panel", // proc(state: ^State) { // Args :: struct { // panel_id: string // } // if args, ok := core.attempt_read_command_args(Args, state.command_args[:]); ok { // log.info("maybe going to open panel with id", args.panel_id) // for p in state.panel_catalog { // switch v in p { // case core.LuaPanelId: // { // if v.id == args.panel_id { // if index, ok := lua.add_panel(state, v); ok { // for i in 0.. 1 { for arg in os.args[1:] { buffer, err := core.new_file_buffer(context.allocator, arg, state.directory); if err.type != .None { log.error("Failed to create file buffer:", err); continue; } runtime.append(&state.buffers, buffer); } } else { buffer := core.new_virtual_file_buffer(context.allocator); runtime.append(&state.buffers, buffer); } if sdl2.Init({.VIDEO}) < 0 { log.error("SDL failed to initialize:", sdl2.GetError()); return; } if ttf.Init() < 0 { log.error("SDL_TTF failed to initialize:", ttf.GetError()); return; } defer ttf.Quit(); sdl_window := sdl2.CreateWindow( "odin_editor - [now with more ui]", sdl2.WINDOWPOS_UNDEFINED, sdl2.WINDOWPOS_UNDEFINED, 640, 480, {.SHOWN, .RESIZABLE, .ALLOW_HIGHDPI} ); defer if sdl_window != nil { sdl2.DestroyWindow(sdl_window); } if sdl_window == nil { log.error("Failed to create window:", sdl2.GetError()); return; } state.sdl_renderer = sdl2.CreateRenderer(sdl_window, -1, {.ACCELERATED, .PRESENTVSYNC}); defer if state.sdl_renderer != nil { sdl2.DestroyRenderer(state.sdl_renderer); } if state.sdl_renderer == nil { log.error("Failed to create renderer:", sdl2.GetError()); return; } state.font_atlas = core.gen_font_atlas(&state, HardcodedFontPath); defer { if state.font_atlas.font != nil { ttf.CloseFont(state.font_atlas.font); } if state.font_atlas.texture != nil { sdl2.DestroyTexture(state.font_atlas.texture); } } { w,h: i32; sdl2.GetRendererOutputSize(state.sdl_renderer, &w, &h); state.width_dpi_ratio = f32(w) / f32(state.screen_width); state.height_dpi_ratio = f32(h) / f32(state.screen_height); state.screen_width = int(w); state.screen_height = int(h); } sdl2.SetRenderDrawBlendMode(state.sdl_renderer, .BLEND); // Done to clear the buffer sdl2.StartTextInput(); sdl2.StopTextInput(); sdl2.AddEventWatch(expose_event_watcher, &state); control_key_pressed: bool; for !state.should_close { { // ui_context.last_mouse_left_down = ui_context.mouse_left_down; // ui_context.last_mouse_right_down = ui_context.mouse_right_down; // ui_context.last_mouse_x = ui_context.mouse_x; // ui_context.last_mouse_y = ui_context.mouse_y; sdl_event: sdl2.Event; for(sdl2.PollEvent(&sdl_event)) { if sdl_event.type == .QUIT { state.should_close = true; } if sdl_event.type == .MOUSEMOTION { // ui_context.mouse_x = int(f32(sdl_event.motion.x) * state.width_dpi_ratio); // ui_context.mouse_y = int(f32(sdl_event.motion.y) * state.height_dpi_ratio); } if sdl_event.type == .MOUSEBUTTONDOWN || sdl_event.type == .MOUSEBUTTONUP { event := sdl_event.button; if event.button == sdl2.BUTTON_LEFT { // ui_context.mouse_left_down = sdl_event.type == .MOUSEBUTTONDOWN; } if event.button == sdl2.BUTTON_RIGHT { // ui_context.mouse_left_down = sdl_event.type == .MOUSEBUTTONDOWN; } } switch state.mode { case .Visual: fallthrough case .Normal: { if sdl_event.type == .KEYDOWN { key := core.Key(sdl_event.key.keysym.sym); if key == .LCTRL { control_key_pressed = true; } else if state.current_input_map != nil { if control_key_pressed { if action, exists := state.current_input_map.ctrl_key_actions[key]; exists { switch value in action.action { case core.EditorAction: value(&state); case core.InputActions: state.current_input_map = &(&state.current_input_map.ctrl_key_actions[key]).action.(core.InputActions) } } } else { if action, exists := state.current_input_map.key_actions[key]; exists { switch value in action.action { case core.EditorAction: value(&state); case core.InputActions: state.current_input_map = &(&state.current_input_map.key_actions[key]).action.(core.InputActions) } } } } } if sdl_event.type == .KEYUP { key := core.Key(sdl_event.key.keysym.sym); if key == .LCTRL { control_key_pressed = false; } } } case .Insert: { buffer := core.current_buffer(&state); if sdl_event.type == .KEYDOWN { key := core.Key(sdl_event.key.keysym.sym); #partial switch key { case .ESCAPE: { state.mode = .Normal; core.insert_content(buffer, buffer.input_buffer[:]); runtime.clear(&buffer.input_buffer); sdl2.StopTextInput(); } case .TAB: { // TODO: change this to insert a tab character for _ in 0..<4 { append(&buffer.input_buffer, ' '); } } case .BACKSPACE: { core.delete_content(buffer, 1); } case .ENTER: { append(&buffer.input_buffer, '\n'); } } } if sdl_event.type == .TEXTINPUT { for char in sdl_event.text.text { if char < 1 { break; } if char >= 32 && char <= 125 && len(buffer.input_buffer) < 1024-1 { append(&buffer.input_buffer, u8(char)); } } } } } } } draw(&state); switch state.mode { case .Normal: buffer := core.current_buffer(&state); do_normal_mode(&state, buffer); case .Insert: buffer := core.current_buffer(&state); do_insert_mode(&state, buffer); case .Visual: buffer := core.current_buffer(&state); do_visual_mode(&state, buffer); } runtime.free_all(context.temp_allocator); } sdl2.Quit(); }