package main import "core:c" import "core:fmt" import "core:os" import "base:runtime" import "core:slice" import "core:strings" import "core:sync" import "core:time" import json "core:encoding/json" import SDL "vendor:sdl3" SDL_View :: struct { first_line: int, tree_first: int, mouse_x: f32, mouse_y: f32, mouse_selecting: bool, tab_dragging: bool, tab_drag_index: int, resizing_panel: Resize_Panel, show_metrics: bool, window_width: int, window_height: int, sidebar_width: int, right_sidebar_width: int, explorer_visible: bool, left_tab: Left_Tab, gradle_sidebar_visible: bool, terminal_visible: bool, terminal_height: int, cached_workspace: string, saved_active_file: int, saved_open_files: [dynamic]Saved_Open_File, } Saved_Open_File :: struct { path: string, cursor: int, } Left_Tab :: enum { Files, Git, } Resize_Panel :: enum { None, Left_Sidebar, Right_Sidebar, Bottom_Panel, } Project_File :: struct { path: string, name: string, depth: int, is_dir: bool, expanded: bool, } Project_Tree :: struct { workspace: string, files: [dynamic]Project_File, expanded: map[string]bool, truncated: bool, } // State for the system folder-selection dialog. SDL may invoke the dialog // callback on a different thread, so the selection is handed to the main // loop under a mutex. Folder_Dialog_State :: struct { window: ^SDL.Window, mutex: sync.Mutex, active: bool, pending: bool, path: [dynamic]u8, } Context_Menu :: struct { open: bool, // Daemon state snapshotted when the menu opens, so rendering and click // handling grey out the same items. daemon_ok: bool, x: f32, y: f32, } Context_Menu_Action :: enum { Cut, Copy, Paste, Definition, References, Rename, } Context_Menu_Item :: struct { label: string, action: Context_Menu_Action, separator_before: bool, } @(rodata) context_menu_items := [?]Context_Menu_Item{ {label = "Cut", action = .Cut}, {label = "Copy", action = .Copy}, {label = "Paste", action = .Paste}, {label = "Go to Definition", action = .Definition, separator_before = true}, {label = "Find References", action = .References}, {label = "Rename...", action = .Rename}, } Completion_Popup :: struct { open: bool, selected: int, line: int, column: int, pending_id: int, items: [dynamic]Completion_Item, } Completion_Item :: struct { label: string, kind: string, } Hover_Tooltip :: struct { open: bool, pending_id: int, line: int, column: int, contents: string, definition_path: string, definition_line: int, definition_column: int, has_definition: bool, x, y: f32, width, height: f32, selecting: bool, selection_active: bool, selection_anchor: int, selection_cursor: int, // Mouse-dwell hovers anchor to the hovered identifier and dismiss when // the mouse leaves its column span; keyboard hovers anchor to the cursor. from_mouse: bool, word_start: int, word_end: int, } Definition_Jump :: struct { pending_id: int, origin_path: string, origin_offset: int, } Nav_Location :: struct { path: string, offset: int, } Navigation_History :: struct { back: [dynamic]Nav_Location, forward: [dynamic]Nav_Location, } Reference_Item :: struct { path: string, line: int, column: int, label: string, } References_Panel :: struct { open: bool, pending_id: int, selected: int, items: [dynamic]Reference_Item, } Search_Panel :: struct { open: bool, input: [dynamic]u8, message: string, } Diagnostics_Panel :: struct { open: bool, selected: int, } Command_Palette :: struct { open: bool, selected: int, input: [dynamic]u8, } Command_Id :: enum { Open_Folder, New_File, Reload_Workspace, Save, Save_All, Close_Tab, Find, Diagnostics, Gradle_Tasks, Explorer, Metrics, Completion, Hover, Definition, References, Rename, } Command_Item :: struct { id: Command_Id, label: string, } COMMAND_ITEMS := [?]Command_Item{ {.Open_Folder, "Open Folder"}, {.New_File, "New File"}, {.Reload_Workspace, "Reload Workspace"}, {.Save, "Save File"}, {.Save_All, "Save All"}, {.Close_Tab, "Close Tab"}, {.Find, "Find in File"}, {.Diagnostics, "Toggle Diagnostics"}, {.Gradle_Tasks, "Toggle Gradle Tasks"}, {.Explorer, "Toggle Explorer"}, {.Metrics, "Toggle Metrics Overlay"}, {.Completion, "Show Completions"}, {.Hover, "Show Hover"}, {.Definition, "Go to Definition"}, {.References, "Find References"}, {.Rename, "Rename Preview"}, } Gradle_Task_Item :: struct { path: string, name: string, description: string, group: string, } Gradle_Tasks_Panel :: struct { open: bool, pending_id: int, pending_run: bool, selected: int, scroll: int, items: [dynamic]Gradle_Task_Item, collapsed: map[string]bool, message: string, output: [dynamic]string, } // One visible row of the Gradle tree: project -> group -> task. Gradle_Row_Kind :: enum { Project, Group, Task, } Gradle_Row :: struct { kind: Gradle_Row_Kind, label: string, key: string, // collapse-state key for project and group rows item_index: int, } Syntax_Kind :: enum { Plain, Keyword, String, Comment, Number, Type, } Syntax_Span :: struct { start: int, end: int, kind: Syntax_Kind, } Syntax_State :: struct { in_block_comment: bool, in_triple_string: bool, } SDL_WINDOW_WIDTH :: 1100 SDL_WINDOW_HEIGHT :: 760 SDL_TOP_BAR_HEIGHT :: 32 SDL_TAB_BAR_HEIGHT :: 30 SDL_STATUS_BAR_HEIGHT :: 24 SDL_LINE_HEIGHT :: 17 SDL_TREE_ROW_HEIGHT :: 17 SDL_SIDEBAR_DEFAULT_WIDTH :: 260 SDL_SIDEBAR_MIN_WIDTH :: 180 SDL_SIDEBAR_MAX_WIDTH :: 520 SDL_RIGHT_SIDEBAR_DEFAULT_WIDTH :: 340 SDL_RIGHT_SIDEBAR_MIN_WIDTH :: 220 SDL_RIGHT_SIDEBAR_MAX_WIDTH :: 620 SDL_RESIZE_HANDLE_WIDTH :: 6 SDL_TOOL_STRIP_WIDTH :: 26 SDL_HOVER_DWELL_MS :: 450 SDL_GUTTER_WIDTH :: 72 SDL_EDITOR_TOP :: SDL_TOP_BAR_HEIGHT + SDL_TAB_BAR_HEIGHT SDL_EDITOR_TEXT_Y :: SDL_EDITOR_TOP + 14 SDL_TREE_HEADER_Y :: SDL_TOP_BAR_HEIGHT + 12 SDL_TREE_FIRST_Y :: SDL_TOP_BAR_HEIGHT + 42 ui_state_load :: proc() -> SDL_View { view := SDL_View{ window_width = SDL_WINDOW_WIDTH, window_height = SDL_WINDOW_HEIGHT, sidebar_width = SDL_SIDEBAR_DEFAULT_WIDTH, right_sidebar_width = SDL_RIGHT_SIDEBAR_DEFAULT_WIDTH, terminal_height = SDL_TERMINAL_DEFAULT_HEIGHT, explorer_visible = true, } path, ok := ui_state_path(context.temp_allocator) if !ok do return view data, err := os.read_entire_file(path, context.allocator) if err != nil do return view defer delete(data) value, parse_err := json.parse_string(string(data), .JSON, true) if parse_err != nil do return view defer json.destroy_value(value) if width, has_width := json_get_int(value, "sidebarWidth"); has_width { view.sidebar_width = clamp_int(width, SDL_SIDEBAR_MIN_WIDTH, SDL_SIDEBAR_MAX_WIDTH) } if width, has_width := json_get_int(value, "rightSidebarWidth"); has_width { view.right_sidebar_width = clamp_int(width, SDL_RIGHT_SIDEBAR_MIN_WIDTH, SDL_RIGHT_SIDEBAR_MAX_WIDTH) } if width, has_width := json_get_int(value, "windowWidth"); has_width { view.window_width = clamp_int(width, 640, 4096) } if height, has_height := json_get_int(value, "windowHeight"); has_height { view.window_height = clamp_int(height, 420, 4096) } if visible, has_visible := json_get_bool(value, "explorerVisible"); has_visible { view.explorer_visible = visible } if visible, has_visible := json_get_bool(value, "gradleSidebarVisible"); has_visible { view.gradle_sidebar_visible = visible } if visible, has_visible := json_get_bool(value, "terminalVisible"); has_visible { view.terminal_visible = visible } if height, has_height := json_get_int(value, "terminalHeight"); has_height { view.terminal_height = clamp_int(height, SDL_TERMINAL_MIN_HEIGHT, 2000) } if tab, has_tab := json_get_int(value, "leftTab"); has_tab { view.left_tab = Left_Tab(clamp_int(tab, 0, len(Left_Tab) - 1)) } if workspace, has_workspace := json_get_string(value, "workspace"); has_workspace { view.cached_workspace = strings.clone(workspace) } if active_file, has_active_file := json_get_int(value, "activeFile"); has_active_file { view.saved_active_file = active_file } if open_files_value, has_open_files := json_object_get(value, "openFiles"); has_open_files { #partial switch open_files in open_files_value { case json.Array: for item in open_files { path, has_path := json_get_string(item, "path") if !has_path || len(path) == 0 do continue cursor, _ := json_get_int(item, "cursor") append(&view.saved_open_files, Saved_Open_File{path = strings.clone(path), cursor = max_int(cursor, 0)}) } } } return view } ui_state_save :: proc(view: ^SDL_View) { path, ok := ui_state_path(context.temp_allocator) if !ok do return dir, dir_ok := ui_state_dir(context.temp_allocator) if dir_ok { _ = os.make_directory_all(dir) } open_files_json: [dynamic]u8 defer delete(open_files_json) append(&open_files_json, '[') for file, index in view.saved_open_files { if index > 0 do append(&open_files_json, ',') item := fmt.tprintf("{{\"path\":%s,\"cursor\":%d}}", json_quote(file.path), file.cursor) for b in transmute([]u8)item do append(&open_files_json, b) } append(&open_files_json, ']') text := fmt.tprintf("{{\n \"windowWidth\": %d,\n \"windowHeight\": %d,\n \"sidebarWidth\": %d,\n \"rightSidebarWidth\": %d,\n \"explorerVisible\": %v,\n \"gradleSidebarVisible\": %v,\n \"terminalVisible\": %v,\n \"terminalHeight\": %d,\n \"leftTab\": %d,\n \"workspace\": %s,\n \"activeFile\": %d,\n \"openFiles\": %s\n}}\n", view.window_width, view.window_height, view.sidebar_width, view.right_sidebar_width, view.explorer_visible, view.gradle_sidebar_visible, view.terminal_visible, view.terminal_height, int(view.left_tab), json_quote(view.cached_workspace), view.saved_active_file, string(open_files_json[:])) _ = os.write_entire_file(path, transmute([]byte)text) } ui_state_destroy :: proc(view: ^SDL_View) { delete(view.cached_workspace) ui_state_clear_open_files(view) delete(view.saved_open_files) } ui_state_clear_open_files :: proc(view: ^SDL_View) { for file in view.saved_open_files { delete(file.path) } clear(&view.saved_open_files) } ui_state_capture_editor :: proc(view: ^SDL_View, editor: ^Editor, workspace: string) { delete(view.cached_workspace) view.cached_workspace = strings.clone(workspace) view.saved_active_file = editor.active ui_state_clear_open_files(view) for &buffer in editor.buffers { append(&view.saved_open_files, Saved_Open_File{path = strings.clone(buffer.path), cursor = buffer.cursor.offset}) } } ui_state_path :: proc(allocator: runtime.Allocator) -> (string, bool) { dir, ok := ui_state_dir(allocator) if !ok do return "", false return fmt.aprintf("%s/ui.json", dir), true } ui_state_dir :: proc(allocator: runtime.Allocator) -> (string, bool) { cache_dir, err := os.user_cache_dir(allocator) if err != nil do return "", false return fmt.aprintf("%s/native-kotlin-editor", cache_dir), true } // The left tool strip owns the leftmost pixels below the top bar; sidebar // content starts after it. content_left_edge :: proc() -> int { return SDL_TOOL_STRIP_WIDTH } left_sidebar_width :: proc(view: ^SDL_View) -> int { width := SDL_TOOL_STRIP_WIDTH if view.explorer_visible { width += clamp_int(view.sidebar_width, SDL_SIDEBAR_MIN_WIDTH, SDL_SIDEBAR_MAX_WIDTH) } return width } // The tool strip owns the rightmost pixels below the top bar, so panels and // editor content end at this edge instead of the window edge. content_right_edge :: proc(window_width: int) -> int { return window_width - SDL_TOOL_STRIP_WIDTH } // Effective bottom-panel height: user-resizable, kept within the window. bottom_panel_height :: proc(view: ^SDL_View) -> int { max_height := max_int(view.window_height - SDL_STATUS_BAR_HEIGHT - SDL_EDITOR_TOP - 120, SDL_TERMINAL_MIN_HEIGHT) return clamp_int(view.terminal_height, SDL_TERMINAL_MIN_HEIGHT, max_height) } // Bottom edge of the editor area: the bottom panel takes space above the // status bar when visible. editor_content_bottom :: proc(view: ^SDL_View) -> int { bottom := view.window_height - SDL_STATUS_BAR_HEIGHT if view.terminal_visible do bottom -= bottom_panel_height(view) return bottom } right_sidebar_width_view :: proc(view: ^SDL_View, window_width: int) -> int { max_available := max_int(window_width - left_sidebar_width(view) - 220, SDL_RIGHT_SIDEBAR_MIN_WIDTH) return min_int(clamp_int(view.right_sidebar_width, SDL_RIGHT_SIDEBAR_MIN_WIDTH, SDL_RIGHT_SIDEBAR_MAX_WIDTH), max_available) } editor_text_x :: proc(view: ^SDL_View) -> int { return left_sidebar_width(view) + SDL_GUTTER_WIDTH + 20 } daemon_begin_owned_start :: proc(workspace: string, owned_daemon: ^Daemon_Process, port_line: ^[dynamic]u8, editor: ^Editor, start_pending: ^bool) -> bool { daemon_stop_process(owned_daemon) clear(port_line) started_daemon, ok := daemon_start_process_begin(workspace) if ok { owned_daemon^ = started_daemon start_pending^ = true editor_set_status(editor, "Starting Kotlin daemon...") return true } editor_set_status(editor, "Kotlin daemon failed to start") return false } editor_restore_initial_files :: proc(editor: ^Editor, view: ^SDL_View, workspace, file_path: string) -> bool { if len(file_path) > 0 { return editor_open_file(editor, file_path) } restored := false if view.cached_workspace == workspace && len(view.saved_open_files) > 0 { for file in view.saved_open_files { if !os.is_file(file.path) do continue if editor_open_file(editor, file.path) { active := editor_active_buffer(editor) if active != nil { active.cursor.offset = clamp_int(file.cursor, 0, buffer_len(&active.buffer)) _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) } restored = true } } if restored { editor.active = clamp_int(view.saved_active_file, 0, len(editor.buffers) - 1) return true } } first_file, found := workspace_find_first_file(workspace, 0) if !found do return false defer delete(first_file) return editor_open_file(editor, first_file) } run_sdl_editor :: proc(initial_workspace: string, daemon_port: int, file_path: string) { view := ui_state_load() defer ui_state_destroy(&view) // No workspace argument means reopen the last workspace, falling back to // the current directory on first launch or if it no longer exists. resolved_workspace := initial_workspace if len(resolved_workspace) == 0 { if len(view.cached_workspace) > 0 && os.is_dir(view.cached_workspace) { resolved_workspace = view.cached_workspace } else { resolved_workspace = "." } } workspace := strings.clone(resolved_workspace) defer delete(workspace) editor := Editor{} defer editor_destroy(&editor) if !editor_restore_initial_files(&editor, &view, workspace, file_path) { return } tree := project_tree_load(workspace) defer project_tree_destroy(&tree) if !SDL.Init(SDL.INIT_VIDEO) { fmt.println("SDL init failed:", SDL.GetError()) return } defer SDL.Quit() window := SDL.CreateWindow("Native Kotlin Editor", i32(view.window_width), i32(view.window_height), {.RESIZABLE}) if window == nil { fmt.println("SDL window failed:", SDL.GetError()) return } defer SDL.DestroyWindow(window) gpu := gpu_renderer_make(window) defer gpu_renderer_destroy(&gpu) renderer: ^SDL.Renderer if !gpu.available { renderer = SDL.CreateRenderer(window, nil) if renderer == nil { fmt.println("SDL renderer failed:", SDL.GetError()) return } } defer { if renderer != nil { SDL.DestroyRenderer(renderer) } } _ = SDL.StartTextInput(window) defer { _ = SDL.StopTextInput(window) } default_cursor := SDL.GetDefaultCursor() text_cursor := SDL.CreateSystemCursor(.TEXT) resize_cursor := SDL.CreateSystemCursor(.EW_RESIZE) ns_resize_cursor := SDL.CreateSystemCursor(.NS_RESIZE) pointer_cursor := SDL.CreateSystemCursor(.POINTER) defer { if text_cursor != nil do SDL.DestroyCursor(text_cursor) if resize_cursor != nil do SDL.DestroyCursor(resize_cursor) if ns_resize_cursor != nil do SDL.DestroyCursor(ns_resize_cursor) if pointer_cursor != nil do SDL.DestroyCursor(pointer_cursor) } refresh_view_size(window, &view) defer { ui_state_capture_editor(&view, &editor, workspace) ui_state_save(&view) } completion := completion_popup_make() defer completion_popup_destroy(&completion) hover := Hover_Tooltip{} defer hover_tooltip_destroy(&hover) definition := Definition_Jump{} defer definition_jump_destroy(&definition) navigation := Navigation_History{} defer navigation_history_destroy(&navigation) references := References_Panel{} defer references_panel_destroy(&references) rename := Rename_Panel{} defer rename_panel_destroy(&rename) search := Search_Panel{} defer search_panel_destroy(&search) diagnostics_panel := Diagnostics_Panel{} command_palette := Command_Palette{} defer command_palette_destroy(&command_palette) context_menu := Context_Menu{} terminal := Terminal_Panel{} defer terminal_destroy(&terminal) git_panel := Git_Panel{} defer git_panel_destroy(&git_panel) git_status := Git_Status_Panel{} defer git_status_destroy(&git_status) terminal.open = view.terminal_visible if terminal.open { _ = terminal_start(&terminal, workspace) } dialog := Folder_Dialog_State{window = window} defer folder_dialog_destroy(&dialog) tasks_panel := Gradle_Tasks_Panel{} tasks_panel.open = view.gradle_sidebar_visible if tasks_panel.open { tasks_panel.message = strings.clone("Waiting for Kotlin daemon...") } defer gradle_tasks_panel_destroy(&tasks_panel) daemon := Daemon_Client{} defer daemon_close(&daemon) owned_daemon := Daemon_Process{} defer daemon_stop_process(&owned_daemon) daemon_sync_state := Daemon_Sync_State{} daemon_port_line: [dynamic]u8 defer delete(daemon_port_line) daemon_start_pending := false daemon_connect_pending := false daemon_initialized := false owned_daemon_enabled := daemon_port <= 0 daemon_restart_cooldown_frames := 0 port := daemon_port if port > 0 { daemon_connect_pending = true } else { _ = daemon_begin_owned_start(workspace, &owned_daemon, &daemon_port_line, &editor, &daemon_start_pending) } mouse_moved_at := time.now() mouse_dwell_handled := false running := true for running { event: SDL.Event for SDL.PollEvent(&event) { #partial switch event.type { case .QUIT, .WINDOW_CLOSE_REQUESTED: running = false case .KEY_DOWN: if context_menu.open { context_menu.open = false if event.key.key == SDL.K_ESCAPE do continue } if event.key.key == SDL.K_GRAVE && (event.key.mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { terminal_toggle(&terminal, &git_panel, &view, workspace) continue } if handle_git_status_key(&git_status, &view, workspace, event.key.key, event.key.mod) { continue } if terminal_handle_key(&terminal, event.key.key, event.key.mod) { continue } if handle_sdl_key(&editor, &view, &tree, workspace, &dialog,&completion, &hover, &definition, &navigation, &references, &rename, &search, &diagnostics_panel, &command_palette, &tasks_panel, &daemon, &daemon_sync_state, event.key.key, event.key.mod) { daemon_sync_schedule(&daemon_sync_state) } case .TEXT_INPUT: text := strings.truncate_to_byte(string(event.text.text), 0) if len(text) > 0 { if git_status.input_focused && git_status_panel_active(&view) { git_status_input_text(&git_status, text) continue } if terminal.open && terminal.focused { terminal_send(&terminal, text) continue } if rename.open { rename_panel_insert(&rename, text) continue } if search.open { search_panel_insert(&search, text) continue } if command_palette.open { command_palette_insert(&command_palette, text) continue } active := editor_active_buffer(&editor) if active != nil && active.diff == nil { editor_insert_text(active, text) close_stale_edit_overlays(&completion, &hover, &references, &rename) ensure_cursor_visible(&editor, &view) daemon_sync_schedule(&daemon_sync_state) } } case .MOUSE_WHEEL: view.mouse_x = event.wheel.mouse_x view.mouse_y = event.wheel.mouse_y mouse_moved_at = time.now() mouse_dwell_handled = false if hover.open && hover.from_mouse { hover.open = false } if handle_overlay_wheel(&editor, &view, &gpu, &command_palette, &completion, &references, &diagnostics_panel, event.wheel.mouse_x, event.wheel.mouse_y, int(event.wheel.integer_y)) { // Routed to the topmost hovered overlay. } else if handle_terminal_wheel(&terminal, &git_panel, &view, &tasks_panel, event.wheel.mouse_x, event.wheel.mouse_y, int(event.wheel.integer_y)) { // Scrolled the terminal history. } else if handle_gradle_tasks_wheel(&view, &tasks_panel, event.wheel.mouse_x, event.wheel.mouse_y, int(event.wheel.integer_y)) { // Routed to the right sidebar. } else if handle_git_status_wheel(&git_status, &view, event.wheel.mouse_x, event.wheel.mouse_y, int(event.wheel.integer_y)) { // Scrolled the git status list. } else if event.wheel.mouse_x < f32(left_sidebar_width(&view)) { scroll_project_tree(&tree, &view, -int(event.wheel.integer_y) * 3) } else { scroll_sdl_view(&editor, &view, -int(event.wheel.integer_y) * 3) } case .MOUSE_BUTTON_DOWN: if event.button.button == SDL.BUTTON_RIGHT { if gpu.available { _ = handle_editor_right_click(&context_menu, &editor, &view, &gpu, &tasks_panel, &daemon, event.button.x, event.button.y) } continue } if event.button.button != SDL.BUTTON_LEFT do continue if handle_context_menu_click(&context_menu, &editor, &view, &completion, &hover, &definition, &references, &rename, &daemon, &daemon_sync_state, event.button.x, event.button.y) { view.mouse_selecting = false } else if handle_panel_resize_down(&view, &tasks_panel, event.button.x, event.button.y) { view.mouse_selecting = false set_editor_cursor(&view, &editor, &tree, &tasks_panel, &command_palette, &context_menu, &references, &diagnostics_panel, event.button.x, event.button.y, default_cursor, text_cursor, resize_cursor, ns_resize_cursor, pointer_cursor) } else if gpu.available && handle_toolbar_click(&dialog, workspace, event.button.x, event.button.y) { view.mouse_selecting = false } else if gpu.available && handle_bottom_panel_click(&terminal, &git_panel, &view, &tasks_panel, workspace, event.button.x, event.button.y) { view.mouse_selecting = false } else if gpu.available && handle_tool_strip_click(&view, &tasks_panel, &terminal, &git_panel, workspace, &editor, &daemon, &daemon_sync_state, event.button.x, event.button.y) { view.mouse_selecting = false } else if gpu.available && handle_left_strip_click(&view, &git_status, workspace, event.button.x, event.button.y) { view.mouse_selecting = false } else if gpu.available && handle_git_status_click(&git_status, &view, workspace, event.button.x, event.button.y) { view.mouse_selecting = false } else if handle_command_palette_click(&command_palette, &editor, &view, &tree, workspace, &dialog,&completion, &hover, &definition, &references, &rename, &search, &diagnostics_panel, &tasks_panel, &daemon, &daemon_sync_state, event.button.x, event.button.y) { view.mouse_selecting = false } else if handle_completion_click(&editor, &view, &gpu, &completion, &hover, &references, &rename, &search, &daemon_sync_state, event.button.x, event.button.y) { view.mouse_selecting = false } else if handle_hover_tooltip_click(&hover, &navigation, &editor, &view, &gpu, event.button.x, event.button.y) { view.mouse_selecting = false } else if handle_references_click(&references, &navigation, &editor, &view, event.button.x, event.button.y) { view.mouse_selecting = false } else if handle_diagnostics_click(&diagnostics_panel, &editor, &view, event.button.x, event.button.y) { view.mouse_selecting = false } else if handle_gradle_tasks_click(&view, &tasks_panel, &daemon, event.button.x, event.button.y, int(event.button.clicks)) { view.mouse_selecting = false } else if handle_overlay_outside_click(&command_palette, &completion, &references, &diagnostics_panel) { view.mouse_selecting = false } else if handle_editor_tab_click(&editor, &view, event.button.x, event.button.y) { daemon_sync_now(&daemon_sync_state, &daemon, &editor, true) } else if handle_project_tree_click(&editor, &tree, &view, event.button.x, event.button.y) { daemon_sync_now(&daemon_sync_state, &daemon, &editor, true) } else { if hover.open && len(hover.contents) > 0 { hover.open = false } terminal.focused = false git_status.input_focused = false view.mouse_selecting = handle_editor_text_mouse(&editor, &view, &gpu, event.button.x, event.button.y, false) } case .MOUSE_BUTTON_UP: if event.button.button == SDL.BUTTON_LEFT { if view.resizing_panel != .None { view.resizing_panel = .None ui_state_save(&view) } view.tab_dragging = false view.tab_drag_index = 0 view.mouse_selecting = false hover.selecting = false } case .MOUSE_MOTION: view.mouse_x = event.motion.x view.mouse_y = event.motion.y mouse_moved_at = time.now() mouse_dwell_handled = false if handle_hover_tooltip_motion(&hover, &gpu, event.motion.x, event.motion.y) { // Keep routing drag selection to the hover text. } else if hover.open && hover.from_mouse && len(hover.contents) > 0 && !hover_tooltip_motion_safe(&hover, &editor, &view, &gpu, event.motion.x, event.motion.y) { hover.open = false } if view.resizing_panel == .Bottom_Panel && (event.motion.state & SDL.BUTTON_LMASK) != {} { handle_bottom_panel_resize_motion(&view, event.motion.y) } else if view.resizing_panel != .None && (event.motion.state & SDL.BUTTON_LMASK) != {} { handle_panel_resize_motion(&view, event.motion.x) } else if view.tab_dragging && (event.motion.state & SDL.BUTTON_LMASK) != {} { handle_editor_tab_drag(&editor, &view, event.motion.x, event.motion.y) } else if view.mouse_selecting && (event.motion.state & SDL.BUTTON_LMASK) != {} { handle_editor_text_mouse(&editor, &view, &gpu, event.motion.x, event.motion.y, true) } set_editor_cursor(&view, &editor, &tree, &tasks_panel, &command_palette, &context_menu, &references, &diagnostics_panel, event.motion.x, event.motion.y, default_cursor, text_cursor, resize_cursor, ns_resize_cursor, pointer_cursor) } } refresh_view_size(window, &view) if gpu.available && daemon.connected && !mouse_dwell_handled && !hover.open && !completion.open && !context_menu.open && !command_palette.open && !references.open && !rename.open && !search.open && time.diff(mouse_moved_at, time.now()) >= SDL_HOVER_DWELL_MS * time.Millisecond && editor_text_hit(&view, &tasks_panel, view.mouse_x, view.mouse_y) { mouse_dwell_handled = true if daemon_sync_state.pending { daemon_sync_now(&daemon_sync_state, &daemon, &editor, false) } _ = hover_tooltip_open_at_mouse(&hover, &editor, &view, &gpu, &daemon, view.mouse_x, view.mouse_y) } if selected, has_selected := folder_dialog_take(&dialog); has_selected { attempt_open_folder(&editor, &view, &tree, &navigation, &workspace, selected, &daemon, &daemon_sync_state, &owned_daemon, &daemon_port_line, &daemon_start_pending, owned_daemon_enabled) delete(selected) } if owned_daemon_enabled && !daemon.connected && !daemon_start_pending && !daemon_connect_pending { if daemon_initialized { daemon_initialized = false daemon_close(&daemon) editor_set_status(&editor, "Kotlin daemon disconnected; restarting...") daemon_restart_cooldown_frames = 30 } if daemon_restart_cooldown_frames > 0 { daemon_restart_cooldown_frames -= 1 } else { _ = daemon_begin_owned_start(workspace, &owned_daemon, &daemon_port_line, &editor, &daemon_start_pending) daemon_restart_cooldown_frames = 30 } } if !daemon.connected { if daemon_start_pending { started_port, ready, failed := daemon_poll_port(&owned_daemon, &daemon_port_line) if ready { port = started_port daemon_start_pending = false daemon_connect_pending = true } else if failed { daemon_start_pending = false daemon_stop_process(&owned_daemon) daemon_restart_cooldown_frames = 60 editor_set_status(&editor, "Kotlin daemon failed to announce a port") } } if daemon_connect_pending { daemon = daemon_connect(port) daemon_connect_pending = false if daemon.connected { daemon_start_reader(&daemon) daemon_send_workspace_open(&daemon, workspace) editor_set_status(&editor, "Opening Kotlin workspace...") daemon_sync_now(&daemon_sync_state, &daemon, &editor, true) if tasks_panel.open && tasks_panel.pending_id == 0 && len(tasks_panel.items) == 0 { gradle_tasks_panel_request(&tasks_panel, &daemon) } daemon_initialized = true editor_set_status(&editor, "Kotlin daemon ready") } else { if owned_daemon_enabled { daemon_stop_process(&owned_daemon) daemon_restart_cooldown_frames = 60 } editor_set_status(&editor, "Kotlin daemon connection failed") } } } else if !daemon_initialized { daemon_start_reader(&daemon) daemon_send_workspace_open(&daemon, workspace) editor_set_status(&editor, "Opening Kotlin workspace...") daemon_sync_now(&daemon_sync_state, &daemon, &editor, true) if tasks_panel.open && tasks_panel.pending_id == 0 && len(tasks_panel.items) == 0 { gradle_tasks_panel_request(&tasks_panel, &daemon) } daemon_initialized = true } daemon_sync_flush_if_due(&daemon_sync_state, &daemon, &editor) daemon_apply_workspace_response(&daemon, &editor) daemon_apply_latest_diagnostics(&daemon, &editor) completion_popup_apply_response(&completion, &daemon) hover_tooltip_apply_response(&hover, &daemon) definition_jump_apply_response(&definition, &navigation, &editor, &view, &daemon) references_panel_apply_response(&references, &daemon) rename_panel_apply_response(&rename, &daemon) gradle_tasks_panel_apply_event(&tasks_panel, &daemon) gradle_tasks_panel_apply_response(&tasks_panel, &daemon) terminal_pump(&terminal) git_panel_pump(&git_panel) git_status_pump(&git_status) if git_status_panel_active(&view) && git_status.loaded_root != workspace { git_status_request(&git_status, workspace) } if git_status.committed { // A new commit exists: force the history panel to reload when // (or while) it is visible. git_status.committed = false delete(git_panel.loaded_root) git_panel.loaded_root = strings.clone("") } if git_panel.diff_ready { git_panel.diff_ready = false editor_open_diff(&editor, git_panel_diff_tab_name(&git_panel), git_panel.diff_text) } if terminal.open && terminal.active_tab == .Git { git_panel_activate(&git_panel, workspace) } if gpu.available { render_sdl_editor_gpu(&gpu, &editor, &view, &tree, &completion, &hover, &references, &rename, &search, &diagnostics_panel, &command_palette, &context_menu, &tasks_panel, &terminal, &git_panel, &git_status) } else { render_sdl_editor(renderer, &editor, &view, &tree, &completion, &hover, &references, &rename) } SDL.Delay(16) } } completion_popup_make :: proc() -> Completion_Popup { popup := Completion_Popup{} completion_popup_set_message(&popup, "No completions yet") return popup } completion_popup_destroy :: proc(popup: ^Completion_Popup) { for item in popup.items { delete(item.label) delete(item.kind) } delete(popup.items) } close_stale_edit_overlays :: proc(completion: ^Completion_Popup, hover: ^Hover_Tooltip, references: ^References_Panel, rename: ^Rename_Panel) { completion.open = false hover.open = false references.open = false rename.open = false } close_completion_accept_overlays :: proc(completion: ^Completion_Popup, hover: ^Hover_Tooltip, references: ^References_Panel, rename: ^Rename_Panel, search: ^Search_Panel) { close_stale_edit_overlays(completion, hover, references, rename) search.open = false } completion_popup_open :: proc(popup: ^Completion_Popup, editor: ^Editor, daemon: ^Daemon_Client) { active := editor_active_buffer(editor) if active == nil do return line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) popup.open = true popup.selected = 0 popup.line = line popup.column = column popup.pending_id = daemon_send_completion(daemon, active.path, line, column) completion_popup_set_message(popup, "Loading completions...") } hover_tooltip_destroy :: proc(hover: ^Hover_Tooltip) { delete(hover.contents) delete(hover.definition_path) } hover_tooltip_open :: proc(hover: ^Hover_Tooltip, editor: ^Editor, daemon: ^Daemon_Client) { active := editor_active_buffer(editor) if active == nil do return line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) hover.open = true hover.from_mouse = false hover.line = line hover.column = column hover.has_definition = false hover.pending_id = daemon_send_hover(daemon, active.path, line, column) hover_tooltip_set_contents(hover, "Loading hover...") } buffer_word_span_at :: proc(buffer: ^Buffer, line, column: int) -> (start_col, end_col: int, ok: bool) { bytes := buffer_line_bytes(buffer, line) defer delete(bytes) if column < 0 || column >= len(bytes) do return 0, 0, false if !is_identifier_byte(bytes[column]) do return 0, 0, false start := column for start > 0 && is_identifier_byte(bytes[start - 1]) { start -= 1 } end := column for end < len(bytes) && is_identifier_byte(bytes[end]) { end += 1 } return start, end, true } // Opens a hover request for the identifier under the mouse; returns false // when the mouse is not over an identifier. hover_tooltip_open_at_mouse :: proc(hover: ^Hover_Tooltip, editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, daemon: ^Daemon_Client, x, y: f32) -> bool { active := editor_active_buffer(editor) if active == nil || active.scratch do return false line, column, ok := editor_line_col_from_mouse(editor, view, gpu, x, y) if !ok do return false word_start, word_end, has_word := buffer_word_span_at(&active.buffer, line, column) if !has_word do return false hover.open = true hover.from_mouse = true hover.line = line hover.column = column hover.word_start = word_start hover.word_end = word_end hover.has_definition = false hover.pending_id = daemon_send_hover(daemon, active.path, line, column) // No "loading" placeholder for dwell hovers: the tooltip stays invisible // until real contents arrive, so hovering plain code shows nothing. hover_tooltip_set_contents(hover, "") return true } definition_jump_request :: proc(definition: ^Definition_Jump, editor: ^Editor, daemon: ^Daemon_Client) { active := editor_active_buffer(editor) if active == nil do return delete(definition.origin_path) definition.origin_path = strings.clone(active.path) definition.origin_offset = active.cursor.offset line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) definition.pending_id = daemon_send_definition(daemon, active.path, line, column) } definition_jump_destroy :: proc(definition: ^Definition_Jump) { delete(definition.origin_path) } definition_jump_apply_response :: proc(definition: ^Definition_Jump, navigation: ^Navigation_History, editor: ^Editor, view: ^SDL_View, daemon: ^Daemon_Client) { if definition.pending_id == 0 do return response := daemon_take_response(daemon, definition.pending_id) defer delete(response) if len(response) == 0 do return path, line, column, ok := parse_definition_response(string(response[:]), definition.pending_id) defer delete(path) if !ok do return definition.pending_id = 0 if len(path) == 0 do return if len(definition.origin_path) > 0 { navigation_push(&navigation.back, definition.origin_path, definition.origin_offset) navigation_clear_stack(&navigation.forward) } if !editor_jump_current_tab_to_location(editor, view, path, line, column) do return daemon_sync_active_buffer(daemon, editor, true) } editor_jump_to_location :: proc(editor: ^Editor, view: ^SDL_View, path: string, line, column: int) -> bool { if !editor_open_or_focus_file(editor, path) do return false active := editor_active_buffer(editor) if active == nil do return false cursor_move_to_line_col(&active.buffer, &active.cursor, max_int(line - 1, 0), max_int(column - 1, 0)) ensure_cursor_visible(editor, view) return true } editor_jump_current_tab_to_location :: proc(editor: ^Editor, view: ^SDL_View, path: string, line, column: int) -> bool { if !editor_replace_active_file(editor, path) do return false active := editor_active_buffer(editor) if active == nil do return false cursor_move_to_line_col(&active.buffer, &active.cursor, max_int(line - 1, 0), max_int(column - 1, 0)) ensure_cursor_visible(editor, view) return true } parse_definition_response :: proc(response: string, expected_id: int) -> (string, int, int, bool) { message, value, ok := parse_protocol_message(response) if !ok do return "", 0, 0, false defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id || !message.ok do return "", 0, 0, false result, has_result := json_object_get(value, "result") if !has_result do return "", 0, 0, false locations_value, has_locations := json_object_get(result, "locations") if !has_locations do return "", 0, 0, false #partial switch locations in locations_value { case json.Array: if len(locations) == 0 do return "", 0, 0, true location := locations[0] path, has_path := json_get_string(location, "path") if !has_path do return "", 0, 0, false line, _ := json_get_int(location, "line") column, _ := json_get_int(location, "column") return strings.clone(path), line, column, true } return "", 0, 0, false } navigation_history_destroy :: proc(navigation: ^Navigation_History) { navigation_clear_stack(&navigation.back) navigation_clear_stack(&navigation.forward) delete(navigation.back) delete(navigation.forward) } navigation_clear_stack :: proc(stack: ^[dynamic]Nav_Location) { for location in stack^ { delete(location.path) } clear(stack) } navigation_push :: proc(stack: ^[dynamic]Nav_Location, path: string, offset: int) { append(stack, Nav_Location{path = strings.clone(path), offset = offset}) } navigation_pop :: proc(stack: ^[dynamic]Nav_Location) -> (Nav_Location, bool) { if len(stack^) == 0 do return {}, false index := len(stack^) - 1 location := stack^[index] resize(stack, index) return location, true } navigation_current_push :: proc(stack: ^[dynamic]Nav_Location, editor: ^Editor) { active := editor_active_buffer(editor) if active == nil do return navigation_push(stack, active.path, active.cursor.offset) } navigation_go_back :: proc(navigation: ^Navigation_History, editor: ^Editor, view: ^SDL_View) { location, ok := navigation_pop(&navigation.back) if !ok do return defer delete(location.path) navigation_current_push(&navigation.forward, editor) if !editor_open_or_focus_file(editor, location.path) do return active := editor_active_buffer(editor) if active == nil do return active.cursor.offset = clamp_int(location.offset, 0, buffer_len(&active.buffer)) _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) ensure_cursor_visible(editor, view) } navigation_go_forward :: proc(navigation: ^Navigation_History, editor: ^Editor, view: ^SDL_View) { location, ok := navigation_pop(&navigation.forward) if !ok do return defer delete(location.path) navigation_current_push(&navigation.back, editor) if !editor_open_or_focus_file(editor, location.path) do return active := editor_active_buffer(editor) if active == nil do return active.cursor.offset = clamp_int(location.offset, 0, buffer_len(&active.buffer)) _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) ensure_cursor_visible(editor, view) } references_panel_destroy :: proc(panel: ^References_Panel) { references_panel_clear(panel) } references_panel_clear :: proc(panel: ^References_Panel) { for item in panel.items { delete(item.path) delete(item.label) } clear(&panel.items) } references_panel_request :: proc(panel: ^References_Panel, editor: ^Editor, daemon: ^Daemon_Client) { active := editor_active_buffer(editor) if active == nil do return line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) panel.open = true panel.selected = 0 panel.pending_id = daemon_send_references(daemon, active.path, line, column) references_panel_clear(panel) append(&panel.items, Reference_Item{label = strings.clone("Loading references...")}) } references_panel_apply_response :: proc(panel: ^References_Panel, daemon: ^Daemon_Client) { if !panel.open || panel.pending_id == 0 do return response := daemon_take_response(daemon, panel.pending_id) defer delete(response) if len(response) == 0 do return items, ok := parse_references_response(string(response[:]), panel.pending_id) defer { for item in items { delete(item.path) delete(item.label) } delete(items) } if !ok do return panel.pending_id = 0 references_panel_clear(panel) if len(items) == 0 { append(&panel.items, Reference_Item{label = strings.clone("No references")}) } else { for item in items { append(&panel.items, Reference_Item{ path = strings.clone(item.path), line = item.line, column = item.column, label = strings.clone(item.label), }) } } panel.selected = 0 } parse_references_response :: proc(response: string, expected_id: int) -> ([dynamic]Reference_Item, bool) { items: [dynamic]Reference_Item message, value, ok := parse_protocol_message(response) if !ok do return items, false defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id || !message.ok do return items, false result, has_result := json_object_get(value, "result") if !has_result do return items, false locations_value, has_locations := json_object_get(result, "locations") if !has_locations do return items, false #partial switch locations in locations_value { case json.Array: for location in locations { path, has_path := json_get_string(location, "path") if !has_path do continue line, _ := json_get_int(location, "line") column, _ := json_get_int(location, "column") _, file := os.split_path(path) label := fmt.tprintf("%s:%d:%d", file, line, column) append(&items, Reference_Item{ path = strings.clone(path), line = line, column = column, label = strings.clone(label), }) } } return items, true } references_panel_accept :: proc(panel: ^References_Panel, navigation: ^Navigation_History, editor: ^Editor, view: ^SDL_View) { if !panel.open || len(panel.items) == 0 do return index := clamp_int(panel.selected, 0, len(panel.items) - 1) item := panel.items[index] if len(item.path) == 0 do return navigation_current_push(&navigation.back, editor) navigation_clear_stack(&navigation.forward) _ = editor_jump_to_location(editor, view, item.path, item.line, item.column) panel.open = false } search_panel_destroy :: proc(panel: ^Search_Panel) { delete(panel.input) delete(panel.message) } command_palette_destroy :: proc(palette: ^Command_Palette) { delete(palette.input) } command_palette_open :: proc(palette: ^Command_Palette) { palette.open = true palette.selected = 0 clear(&palette.input) } command_palette_insert :: proc(palette: ^Command_Palette, text: string) { for b in transmute([]u8)text { if b >= 32 && b < 127 { append(&palette.input, b) } } palette.selected = min_int(palette.selected, max_int(command_palette_match_count(palette) - 1, 0)) } command_palette_backspace :: proc(palette: ^Command_Palette) { if len(palette.input) > 0 { resize(&palette.input, len(palette.input) - 1) palette.selected = min_int(palette.selected, max_int(command_palette_match_count(palette) - 1, 0)) } } command_palette_match_count :: proc(palette: ^Command_Palette) -> int { count := 0 for item in COMMAND_ITEMS { if command_palette_matches(palette, item.label) do count += 1 } return count } command_palette_selected_item :: proc(palette: ^Command_Palette) -> (Command_Item, bool) { seen := 0 for item in COMMAND_ITEMS { if !command_palette_matches(palette, item.label) do continue if seen == palette.selected do return item, true seen += 1 } return {}, false } command_palette_matches :: proc(palette: ^Command_Palette, label: string) -> bool { if len(palette.input) == 0 do return true return ascii_contains_fold(label, string(palette.input[:])) } ascii_contains_fold :: proc(haystack, needle: string) -> bool { if len(needle) == 0 do return true if len(needle) > len(haystack) do return false for start := 0; start <= len(haystack) - len(needle); start += 1 { match := true for i := 0; i < len(needle); i += 1 { if ascii_lower(haystack[start + i]) != ascii_lower(needle[i]) { match = false break } } if match do return true } return false } ascii_lower :: proc(b: u8) -> u8 { if b >= 'A' && b <= 'Z' do return b + ('a' - 'A') return b } command_palette_accept :: proc(palette: ^Command_Palette, editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, workspace: string, dialog: ^Folder_Dialog_State,completion: ^Completion_Popup, hover: ^Hover_Tooltip, definition: ^Definition_Jump, references: ^References_Panel, rename: ^Rename_Panel, search: ^Search_Panel, diagnostics_panel: ^Diagnostics_Panel, tasks_panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State) { item, ok := command_palette_selected_item(palette) if !ok do return palette.open = false switch item.id { case .Open_Folder: folder_dialog_show(dialog, workspace) case .New_File: project_tree_create_new_file(editor, view, tree, workspace, daemon, sync) case .Close_Tab: if editor_close_active(editor) { scroll_sdl_view(editor, view, 0) } case .Reload_Workspace: workspace_reload(editor, view, tree, workspace, daemon, sync) case .Save: _ = editor_save_active(editor) daemon_sync_now(sync, daemon, editor, false) case .Save_All: _ = editor_save_all(editor) daemon_sync_now(sync, daemon, editor, false) case .Find: search_panel_open(search) case .Diagnostics: diagnostics_panel.open = !diagnostics_panel.open diagnostics_panel.selected = 0 case .Gradle_Tasks: gradle_panel_toggle(view, tasks_panel, editor, daemon, sync) case .Explorer: view.explorer_visible = !view.explorer_visible view.tree_first = 0 ui_state_save(view) case .Metrics: view.show_metrics = !view.show_metrics case .Completion: daemon_sync_now(sync, daemon, editor, false) completion_popup_open(completion, editor, daemon) case .Hover: daemon_sync_now(sync, daemon, editor, false) hover_tooltip_open(hover, editor, daemon) case .Definition: daemon_sync_now(sync, daemon, editor, false) definition_jump_request(definition, editor, daemon) case .References: daemon_sync_now(sync, daemon, editor, false) references_panel_request(references, editor, daemon) case .Rename: rename_panel_open(rename) } } project_tree_create_new_file :: proc(editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, workspace: string, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State) -> bool { for index := 1; index < 1000; index += 1 { name := "untitled.txt" if index == 1 else fmt.tprintf("untitled-%d.txt", index) path := fmt.tprintf("%s/%s", workspace, name) if os.is_file(path) do continue err := os.write_entire_file(path, []u8{}) if err != nil { editor_set_status(editor, fmt.tprintf("Could not create file: %s", path)) return false } if !editor_open_or_focus_file(editor, path) { editor_set_status(editor, fmt.tprintf("Created file but could not open it: %s", path)) return false } project_tree_rebuild(tree) scroll_project_tree(tree, view, 0) daemon_sync_now(sync, daemon, editor, true) editor_set_status(editor, fmt.tprintf("Created file: %s", path)) return true } editor_set_status(editor, "Could not choose a new file name") return false } gradle_panel_toggle :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, editor: ^Editor, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State) { if tasks_panel.open { tasks_panel.open = false view.gradle_sidebar_visible = false } else { view.gradle_sidebar_visible = true daemon_sync_now(sync, daemon, editor, false) gradle_tasks_panel_request(tasks_panel, daemon) } ui_state_save(view) } gradle_tasks_panel_destroy :: proc(panel: ^Gradle_Tasks_Panel) { gradle_tasks_panel_clear(panel) delete(panel.items) delete(panel.collapsed) delete(panel.message) gradle_tasks_panel_clear_output(panel) delete(panel.output) } gradle_tasks_panel_clear :: proc(panel: ^Gradle_Tasks_Panel) { for item in panel.items { delete(item.path) delete(item.name) delete(item.description) delete(item.group) } clear(&panel.items) for key in panel.collapsed { delete(key) } clear(&panel.collapsed) panel.scroll = 0 } gradle_tasks_panel_clear_output :: proc(panel: ^Gradle_Tasks_Panel) { for line in panel.output { delete(line) } clear(&panel.output) } gradle_tasks_panel_add_output :: proc(panel: ^Gradle_Tasks_Panel, stream, text: string) { label := fmt.tprintf("%s: %s", stream, text) append(&panel.output, strings.clone(label)) for len(panel.output) > 80 { delete(panel.output[0]) ordered_remove(&panel.output, 0) } } gradle_tasks_panel_request :: proc(panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client) { panel.open = true panel.selected = 0 panel.pending_id = daemon_send_gradle_tasks(daemon) panel.pending_run = false gradle_tasks_panel_clear(panel) delete(panel.message) panel.message = strings.clone("Loading Gradle tasks...") } gradle_tasks_panel_apply_response :: proc(panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client) { if !panel.open || panel.pending_id == 0 do return response := daemon_take_response(daemon, panel.pending_id) defer delete(response) if len(response) == 0 do return if panel.pending_run { message, ok := parse_gradle_run_response(string(response[:]), panel.pending_id) defer delete(message) if !ok do return panel.pending_id = 0 panel.pending_run = false delete(panel.message) panel.message = strings.clone(message) return } items, ok := parse_gradle_tasks_response(string(response[:]), panel.pending_id) defer { for item in items { delete(item.path) delete(item.name) delete(item.description) } delete(items) } if !ok do return panel.pending_id = 0 gradle_tasks_panel_clear(panel) for item in items { append(&panel.items, Gradle_Task_Item{path = strings.clone(item.path), name = strings.clone(item.name), description = strings.clone(item.description)}) } delete(panel.message) if len(panel.items) == 0 { panel.message = strings.clone("No Gradle tasks") } else { panel.message = strings.clone("Enter will run tasks in a later slice") } } gradle_tasks_panel_apply_event :: proc(panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client) { if !panel.open do return for { event := daemon_take_gradle_event(daemon) if len(event) == 0 { delete(event) return } task, state, stream, text, ok := parse_gradle_run_event(string(event[:])) delete(event) if ok { if state == "output" { gradle_tasks_panel_add_output(panel, stream, text) } else { delete(panel.message) panel.message = strings.clone(fmt.tprintf("Gradle %s: %s", state, task)) } } delete(task) delete(state) delete(stream) delete(text) } } parse_gradle_run_event :: proc(event: string) -> (string, string, string, string, bool) { message, value, ok := parse_protocol_message(event) if !ok do return "", "", "", "", false defer json.destroy_value(value) if message.kind != .Event || message.event != "gradle/run" do return "", "", "", "", false params, has_params := json_object_get(value, "params") if !has_params do return "", "", "", "", false task, _ := json_get_string(params, "task") state, _ := json_get_string(params, "state") stream, _ := json_get_string(params, "stream") text, _ := json_get_string(params, "text") return strings.clone(task), strings.clone(state), strings.clone(stream), strings.clone(text), true } gradle_tasks_panel_run :: proc(panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client, index: int) { if index < 0 || index >= len(panel.items) || panel.pending_id != 0 do return task := panel.items[index].path if len(task) == 0 do task = panel.items[index].name if len(task) == 0 do return panel.pending_id = daemon_send_gradle_run(daemon, task) panel.pending_run = true gradle_tasks_panel_clear_output(panel) delete(panel.message) panel.message = strings.clone(fmt.tprintf("Running %s...", task)) } parse_gradle_run_response :: proc(response: string, expected_id: int) -> (string, bool) { message, value, ok := parse_protocol_message(response) if !ok do return "", false defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id { return "", false } if !message.ok { error_message := protocol_error_message(value) defer delete(error_message) if len(error_message) > 0 { return strings.clone(fmt.tprintf("Gradle failed: %s", error_message)), true } return strings.clone("Gradle failed"), true } result, has_result := json_object_get(value, "result") task := "task" if has_result { task, _ = json_get_string(result, "task") } return strings.clone(fmt.tprintf("Gradle finished: %s", task)), true } parse_gradle_tasks_response :: proc(response: string, expected_id: int) -> ([dynamic]Gradle_Task_Item, bool) { items: [dynamic]Gradle_Task_Item message, value, ok := parse_protocol_message(response) if !ok do return items, false defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id || !message.ok do return items, false result, has_result := json_object_get(value, "result") if !has_result do return items, false tasks_value, has_tasks := json_object_get(result, "tasks") if !has_tasks do return items, false #partial switch tasks in tasks_value { case json.Array: for task in tasks { path, _ := json_get_string(task, "path") name, _ := json_get_string(task, "name") description, _ := json_get_string(task, "description") group, _ := json_get_string(task, "group") append(&items, Gradle_Task_Item{path = strings.clone(path), name = strings.clone(name), description = strings.clone(description), group = strings.clone(group)}) } } return items, true } search_panel_open :: proc(panel: ^Search_Panel) { panel.open = true delete(panel.message) panel.message = strings.clone("Enter search text, then Enter") } search_panel_insert :: proc(panel: ^Search_Panel, text: string) { for b in transmute([]u8)text { if b >= 32 && b < 127 { append(&panel.input, b) } } } search_panel_backspace :: proc(panel: ^Search_Panel) { if len(panel.input) > 0 { resize(&panel.input, len(panel.input) - 1) } } search_panel_find_next :: proc(panel: ^Search_Panel, editor: ^Editor, view: ^SDL_View) { search_panel_find(panel, editor, view, 1) } search_panel_find_previous :: proc(panel: ^Search_Panel, editor: ^Editor, view: ^SDL_View) { search_panel_find(panel, editor, view, -1) } search_panel_find :: proc(panel: ^Search_Panel, editor: ^Editor, view: ^SDL_View, direction: int) { active := editor_active_buffer(editor) if active == nil do return if len(panel.input) == 0 { search_panel_set_message(panel, "Search text is empty") return } text := buffer_bytes(&active.buffer) defer delete(text) query := panel.input[:] found: int ok: bool if direction >= 0 { start := min_int(active.cursor.offset + 1, len(text)) found, ok = find_bytes_from(text[:], query, start) if !ok && start > 0 { found, ok = find_bytes_from(text[:], query, 0) } } else { start := max_int(active.cursor.offset - len(query) - 1, 0) found, ok = find_bytes_reverse_from(text[:], query, start) if !ok && start < len(text) { found, ok = find_bytes_reverse_from(text[:], query, len(text) - len(query)) } } if !ok { search_panel_set_message(panel, "No matches") return } active.selection_active = true active.selection_anchor = found active.cursor.offset = found + len(query) _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) ensure_cursor_visible(editor, view) line, column := buffer_offset_to_line_col(&active.buffer, found) search_panel_set_message(panel, fmt.tprintf("Found at %d:%d", line + 1, column + 1)) } search_panel_set_message :: proc(panel: ^Search_Panel, message: string) { delete(panel.message) panel.message = strings.clone(message) } find_bytes_from :: proc(text: []u8, query: []u8, start: int) -> (int, bool) { if len(query) == 0 || len(query) > len(text) do return 0, false i := clamp_int(start, 0, len(text) - len(query)) for i <= len(text) - len(query) { match := true for j := 0; j < len(query); j += 1 { if text[i + j] != query[j] { match = false break } } if match do return i, true i += 1 } return 0, false } find_bytes_reverse_from :: proc(text: []u8, query: []u8, start: int) -> (int, bool) { if len(query) == 0 || len(query) > len(text) do return 0, false i := clamp_int(start, 0, len(text) - len(query)) for i >= 0 { match := true for j := 0; j < len(query); j += 1 { if text[i + j] != query[j] { match = false break } } if match do return i, true i -= 1 } return 0, false } folder_dialog_destroy :: proc(dialog: ^Folder_Dialog_State) { delete(dialog.path) } folder_dialog_show :: proc(dialog: ^Folder_Dialog_State, workspace: string) { if sync.mutex_guard(&dialog.mutex) { if dialog.active do return dialog.active = true } location: cstring if absolute, err := os.get_absolute_path(workspace, context.temp_allocator); err == nil { location = strings.clone_to_cstring(absolute, context.temp_allocator) } SDL.ShowOpenFolderDialog(folder_dialog_callback, rawptr(dialog), dialog.window, location, false) } folder_dialog_callback :: proc "c" (userdata: rawptr, filelist: [^]cstring, filter: c.int) { context = runtime.default_context() dialog := (^Folder_Dialog_State)(userdata) if sync.mutex_guard(&dialog.mutex) { dialog.active = false dialog.pending = false clear(&dialog.path) // A nil list is a dialog error; an empty list means the user canceled. if filelist != nil && filelist[0] != nil { for b in transmute([]u8)string(filelist[0]) { append(&dialog.path, b) } dialog.pending = true } } } folder_dialog_take :: proc(dialog: ^Folder_Dialog_State) -> (string, bool) { if sync.mutex_guard(&dialog.mutex) { if dialog.pending { dialog.pending = false return strings.clone(string(dialog.path[:])), true } } return "", false } workspace_find_first_file :: proc(dir: string, depth: int) -> (string, bool) { if depth > 6 do return "", false entries, err := os.read_all_directory_by_path(dir, context.allocator) if err != nil do return "", false defer os.file_info_slice_delete(entries, context.allocator) slice.sort_by(entries, proc(a, b: os.File_Info) -> bool { a_dir := a.type == .Directory b_dir := b.type == .Directory if a_dir != b_dir do return !a_dir return a.name < b.name }) for entry in entries { if project_tree_skip(entry.name) do continue if entry.type != .Directory { return strings.clone(fmt.tprintf("%s/%s", dir, entry.name)), true } } for entry in entries { if project_tree_skip(entry.name) do continue if entry.type == .Directory { path, ok := workspace_find_first_file(fmt.tprintf("%s/%s", dir, entry.name), depth + 1) if ok do return path, true } } return "", false } attempt_open_folder :: proc( editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, navigation: ^Navigation_History, workspace: ^string, selected: string, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State, owned_daemon: ^Daemon_Process, port_line: ^[dynamic]u8, start_pending: ^bool, owned_enabled: bool, ) { resolved := strings.trim_space(selected) if len(resolved) == 0 || !os.is_dir(resolved) { editor_set_status(editor, "Not a directory") return } for &buffer in editor.buffers { if buffer.dirty { editor_set_status(editor, "Save all buffers before switching folders") return } } first_file, found := workspace_find_first_file(resolved, 0) if !found { editor_set_status(editor, "Folder has no openable files") return } defer delete(first_file) // Persist the old workspace's session before tearing it down. ui_state_capture_editor(view, editor, workspace^) ui_state_save(view) // Open the new buffer first so the editor never ends up with zero buffers. old_count := len(editor.buffers) if !editor_open_file(editor, first_file) { editor_set_status(editor, "Failed to open a file in that folder") return } for _ in 0 ..< old_count { editor_buffer_destroy(&editor.buffers[0]) ordered_remove(&editor.buffers, 0) } editor.active = 0 view.first_line = 0 navigation_clear_stack(&navigation.back) navigation_clear_stack(&navigation.forward) delete(workspace^) workspace^ = strings.clone(resolved) project_tree_destroy(tree) tree^ = project_tree_load(workspace^) view.tree_first = 0 // The daemon runs independently of the workspace, so an open connection // just needs the new root; only start a daemon if none is running. if daemon.connected { daemon_send_workspace_open(daemon, workspace^) editor_set_status(editor, "Opening Kotlin workspace...") daemon_sync_now(sync, daemon, editor, true) } else if owned_enabled { _ = daemon_begin_owned_start(workspace^, owned_daemon, port_line, editor, start_pending) } editor_set_status(editor, fmt.tprintf("Opened folder: %s", workspace^)) } hover_tooltip_apply_response :: proc(hover: ^Hover_Tooltip, daemon: ^Daemon_Client) { if !hover.open || hover.pending_id == 0 do return response := daemon_take_response(daemon, hover.pending_id) defer delete(response) if len(response) == 0 do return contents, definition_path, definition_line, definition_column, has_definition, ok := parse_hover_response(string(response[:]), hover.pending_id) defer { delete(contents) delete(definition_path) } if !ok do return hover.pending_id = 0 if len(contents) == 0 { if hover.from_mouse { hover.open = false } else { hover_tooltip_set_contents(hover, "No hover information") } } else { hover_tooltip_set_contents(hover, contents) } hover_tooltip_set_definition(hover, definition_path, definition_line, definition_column, has_definition) } hover_tooltip_set_contents :: proc(hover: ^Hover_Tooltip, contents: string) { delete(hover.contents) hover.contents = strings.clone(contents) hover.selection_active = false hover.selecting = false hover.selection_anchor = 0 hover.selection_cursor = 0 } hover_tooltip_set_definition :: proc(hover: ^Hover_Tooltip, path: string, line, column: int, has_definition: bool) { delete(hover.definition_path) hover.definition_path = strings.clone(path) hover.definition_line = line hover.definition_column = column hover.has_definition = has_definition && len(path) > 0 } parse_hover_response :: proc(response: string, expected_id: int) -> (contents: string, definition_path: string, definition_line: int, definition_column: int, has_definition: bool, ok: bool) { message, value, parsed := parse_protocol_message(response) if !parsed do return defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id || !message.ok do return result, has_result := json_object_get(value, "result") if !has_result do return parsed_contents, has_contents := json_get_string(result, "contents") if has_contents { contents = strings.clone(parsed_contents) } if definition, has_def := json_object_get(result, "definition"); has_def { if path, has_path := json_get_string(definition, "path"); has_path { definition_path = strings.clone(path) definition_line, _ = json_get_int(definition, "line") definition_column, _ = json_get_int(definition, "column") has_definition = true } } ok = true return } hover_tooltip_hit :: proc(hover: ^Hover_Tooltip, x, y: f32) -> bool { return hover.open && len(hover.contents) > 0 && x >= hover.x && x < hover.x + hover.width && y >= hover.y && y < hover.y + hover.height } hover_tooltip_motion_safe :: proc(hover: ^Hover_Tooltip, editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, x, y: f32) -> bool { if hover_tooltip_hit(hover, x, y) do return true active := editor_active_buffer(editor) if active == nil do return false if hover.line < 0 || hover.line >= buffer_line_count(&active.buffer) do return false word_x1 := editor_column_pixel_x_gpu(gpu, view, &active.buffer, hover.line, hover.word_start) word_x2 := editor_column_pixel_x_gpu(gpu, view, &active.buffer, hover.line, hover.word_end) word_y := f32(SDL_EDITOR_TEXT_Y + (hover.line - view.first_line) * SDL_LINE_HEIGHT) if x >= word_x1 && x < word_x2 && y >= word_y - 3 && y < word_y + SDL_LINE_HEIGHT { return true } // Let the pointer travel from the source word to the tooltip without the // dwell hover disappearing in the gap between them. left := min_f32(word_x1, hover.x) - 10 right := max_f32(word_x2, hover.x + hover.width) + 10 top := min_f32(word_y - 3, hover.y) - 10 bottom := max_f32(word_y + SDL_LINE_HEIGHT, hover.y + hover.height) + 10 return x >= left && x < right && y >= top && y < bottom } hover_tooltip_action_hit :: proc(hover: ^Hover_Tooltip, x, y: f32) -> bool { if !hover.has_definition do return false action_y := hover.y + hover.height - 25 return x >= hover.x + 8 && x < hover.x + 142 && y >= action_y && y < action_y + 18 } hover_visible_lines :: proc(hover: ^Hover_Tooltip) -> []string { all_lines, _ := strings.split_lines(hover.contents, context.temp_allocator) line_count := min_int(len(all_lines), 12) lines := all_lines[:line_count] for len(lines) > 0 && len(strings.trim_space(lines[len(lines) - 1])) == 0 { lines = lines[:len(lines) - 1] } return lines } hover_line_start_offset :: proc(hover: ^Hover_Tooltip, line_index: int) -> int { offset := 0 current := 0 for i := 0; i < len(hover.contents) && current < line_index; i += 1 { offset += 1 if hover.contents[i] == '\n' { current += 1 } } return offset } hover_text_offset_at :: proc(hover: ^Hover_Tooltip, gpu: ^GPU_Renderer, x, y: f32) -> int { lines := hover_visible_lines(hover) if len(lines) == 0 do return 0 line_index := clamp_int(int((y - (hover.y + 10)) / 16), 0, len(lines) - 1) line := lines[line_index] rel_x := max_f32(x - (hover.x + 10), 0) column := int((rel_x + max_f32(gpu.font_advance, 1) * 0.5) / max_f32(gpu.font_advance, 1)) column = clamp_int(column, 0, len(line)) return min_int(hover_line_start_offset(hover, line_index) + column, len(hover.contents)) } hover_tooltip_copy_selection :: proc(hover: ^Hover_Tooltip) -> bool { if !hover.selection_active do return false start := min_int(hover.selection_anchor, hover.selection_cursor) end := max_int(hover.selection_anchor, hover.selection_cursor) if start == end do return false c_text, err := strings.clone_to_cstring(hover.contents[start:end], context.temp_allocator) if err != nil do return false return SDL.SetClipboardText(c_text) } handle_hover_tooltip_click :: proc(hover: ^Hover_Tooltip, navigation: ^Navigation_History, editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, x, y: f32) -> bool { if !hover_tooltip_hit(hover, x, y) do return false if hover_tooltip_action_hit(hover, x, y) { active := editor_active_buffer(editor) if active != nil { navigation_push(&navigation.back, active.path, active.cursor.offset) navigation_clear_stack(&navigation.forward) } _ = editor_jump_current_tab_to_location(editor, view, hover.definition_path, hover.definition_line, hover.definition_column) hover.open = false return true } offset := hover_text_offset_at(hover, gpu, x, y) hover.selecting = true hover.selection_active = true hover.selection_anchor = offset hover.selection_cursor = offset return true } handle_hover_tooltip_motion :: proc(hover: ^Hover_Tooltip, gpu: ^GPU_Renderer, x, y: f32) -> bool { if !hover.selecting do return false hover.selection_cursor = hover_text_offset_at(hover, gpu, x, y) return true } completion_popup_apply_response :: proc(popup: ^Completion_Popup, daemon: ^Daemon_Client) { if !popup.open || popup.pending_id == 0 do return response := daemon_take_response(daemon, popup.pending_id) defer delete(response) if len(response) == 0 do return items, ok := parse_completion_response(string(response[:]), popup.pending_id) defer { for item in items { delete(item.label) delete(item.kind) } delete(items) } if !ok do return popup.pending_id = 0 if len(items) == 0 { completion_popup_set_message(popup, "No completions") } else { completion_popup_set_items(popup, items[:]) } } completion_popup_set_message :: proc(popup: ^Completion_Popup, message: string) { completion_popup_set_items(popup, []Completion_Item{{label = message, kind = "status"}}) } completion_popup_set_items :: proc(popup: ^Completion_Popup, items: []Completion_Item) { for item in popup.items { delete(item.label) delete(item.kind) } clear(&popup.items) for item in items { append(&popup.items, Completion_Item{label = strings.clone(item.label), kind = strings.clone(item.kind)}) } popup.selected = 0 } completion_popup_accept :: proc(popup: ^Completion_Popup, editor: ^Editor) -> bool { if !popup.open || len(popup.items) == 0 do return false active := editor_active_buffer(editor) if active == nil do return false index := clamp_int(popup.selected, 0, len(popup.items) - 1) item := popup.items[index] if item.kind == "status" || len(item.label) == 0 do return false selection_start, selection_end, has_selection := editor_selection_range(active) if has_selection { buffer_replace_range(&active.buffer, selection_start, selection_end - selection_start, item.label) active.cursor.offset = selection_start + len(item.label) _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) editor_update_dirty(active) editor_clear_selection(active) popup.open = false return true } start := active.cursor.offset for start > 0 { bytes := buffer_range_bytes(&active.buffer, start - 1, 1) if len(bytes) == 0 { delete(bytes) break } ch := bytes[0] delete(bytes) if !is_identifier_part_byte(ch) do break start -= 1 } if start < active.cursor.offset { buffer_replace_range(&active.buffer, start, active.cursor.offset - start, item.label) active.cursor.offset = start active.cursor.offset += len(item.label) } else { buffer_replace_range(&active.buffer, active.cursor.offset, 0, item.label) active.cursor.offset += len(item.label) } _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) editor_update_dirty(active) editor_clear_selection(active) popup.open = false return true } parse_completion_response :: proc(response: string, expected_id: int) -> ([dynamic]Completion_Item, bool) { labels: [dynamic]Completion_Item message, value, ok := parse_protocol_message(response) if !ok do return labels, false defer json.destroy_value(value) if message.kind != .Response || message.id != expected_id || !message.ok do return labels, false result, has_result := json_object_get(value, "result") if !has_result do return labels, false items_value, has_items := json_object_get(result, "items") if !has_items do return labels, false #partial switch items in items_value { case json.Array: for item in items { label := "" kind := "" #partial switch value in item { case json.String: label = string(value) case json.Object: label, _ = json_get_string(item, "label") kind, _ = json_get_string(item, "kind") } if len(label) > 0 { append(&labels, Completion_Item{label = strings.clone(label), kind = strings.clone(kind)}) } } } return labels, true } project_tree_load :: proc(workspace: string) -> Project_Tree { tree := Project_Tree{workspace = strings.clone(workspace)} project_tree_rebuild(&tree) return tree } project_tree_clear_files :: proc(tree: ^Project_Tree) { for file in tree.files { delete(file.path) delete(file.name) } clear(&tree.files) } project_tree_rebuild :: proc(tree: ^Project_Tree) { project_tree_clear_files(tree) tree.truncated = false project_tree_append_dir(tree, tree.workspace, 0) } project_tree_destroy :: proc(tree: ^Project_Tree) { project_tree_clear_files(tree) delete(tree.files) tree.files = nil for key, _ in tree.expanded { delete(key) } delete(tree.expanded) tree.expanded = nil delete(tree.workspace) tree.workspace = "" } project_tree_toggle_dir :: proc(tree: ^Project_Tree, index: int) { if index < 0 || index >= len(tree.files) do return item := tree.files[index] if !item.is_dir do return if _, found := tree.expanded[item.path]; found { tree.expanded[item.path] = !item.expanded } else { tree.expanded[strings.clone(item.path)] = true } project_tree_rebuild(tree) } workspace_reload :: proc(editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, workspace: string, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State) { project_tree_rebuild(tree) scroll_project_tree(tree, view, 0) editor_clear_all_diagnostics(editor) daemon_send_workspace_open(daemon, workspace) editor_set_status(editor, "Opening Kotlin workspace...") daemon_sync_now(sync, daemon, editor, true) } project_tree_append_dir :: proc(tree: ^Project_Tree, dir: string, depth: int) { if depth > 12 do return if len(tree.files) >= 5000 { tree.truncated = true return } entries, err := os.read_all_directory_by_path(dir, context.allocator) if err != nil do return defer os.file_info_slice_delete(entries, context.allocator) slice.sort_by(entries, proc(a, b: os.File_Info) -> bool { a_dir := a.type == .Directory b_dir := b.type == .Directory if a_dir != b_dir do return a_dir return a.name < b.name }) for entry in entries { if len(tree.files) >= 5000 { tree.truncated = true return } if project_tree_skip(entry.name) do continue path := fmt.tprintf("%s/%s", dir, entry.name) is_dir := entry.type == .Directory expanded := is_dir && tree.expanded[path] append(&tree.files, Project_File{ path = strings.clone(path), name = strings.clone(entry.name), depth = depth, is_dir = is_dir, expanded = expanded, }) if expanded { child_path := tree.files[len(tree.files) - 1].path project_tree_append_dir(tree, child_path, depth + 1) } } } project_tree_skip :: proc(name: string) -> bool { return name == ".git" || name == ".gradle" || name == "build" || name == ".idea" } set_editor_cursor :: proc(view: ^SDL_View, editor: ^Editor, tree: ^Project_Tree, tasks_panel: ^Gradle_Tasks_Panel, command_palette: ^Command_Palette, context_menu: ^Context_Menu, references: ^References_Panel, diagnostics_panel: ^Diagnostics_Panel, x, y: f32, default_cursor, text_cursor, resize_cursor, ns_resize_cursor, pointer_cursor: ^SDL.Cursor) { if view.resizing_panel == .Bottom_Panel || (view.resizing_panel == .None && bottom_panel_resize_hit(view, tasks_panel, x, y)) { if ns_resize_cursor != nil do _ = SDL.SetCursor(ns_resize_cursor) return } if view.resizing_panel != .None || panel_resize_hit(view, tasks_panel, x, y) { if resize_cursor != nil do _ = SDL.SetCursor(resize_cursor) return } if ui_pointer_hit(view, editor, tree, tasks_panel, command_palette, context_menu, references, diagnostics_panel, x, y) { if pointer_cursor != nil do _ = SDL.SetCursor(pointer_cursor) return } if editor_text_hit(view, tasks_panel, x, y) { if text_cursor != nil do _ = SDL.SetCursor(text_cursor) return } if default_cursor != nil do _ = SDL.SetCursor(default_cursor) } // True when the mouse is over something that reacts to a click: tree rows, // editor tabs, overlay list rows, or Gradle task rows. ui_pointer_hit :: proc(view: ^SDL_View, editor: ^Editor, tree: ^Project_Tree, tasks_panel: ^Gradle_Tasks_Panel, command_palette: ^Command_Palette, context_menu: ^Context_Menu, references: ^References_Panel, diagnostics_panel: ^Diagnostics_Panel, x, y: f32) -> bool { if toolbar_open_folder_hit(x, y) { return true } if context_menu.open { if index, on_item := context_menu_item_at(context_menu, x, y); on_item { return context_menu_item_enabled(context_menu, editor, context_menu_items[index].action) } } if tool_strip_gradle_hit(view.window_width, x, y) || tool_strip_terminal_hit(view.window_width, x, y) { return true } if left_strip_hit(.Files, x, y) || left_strip_hit(.Git, x, y) { return true } if command_palette.open { width := f32(min_int(max_int(view.window_width - 280, 420), 720)) palette_x := f32(view.window_width) * 0.5 - width * 0.5 palette_y: f32 = SDL_TOP_BAR_HEIGHT + 48 visible := min_int(max_int(command_palette_match_count(command_palette), 1), 8) height := f32(58 + visible * 24) if x >= palette_x && x < palette_x + width && y >= palette_y + 60 && y < palette_y + height { return true } } if references.open { visible := min_int(max_int(len(references.items), 1), 14) height := f32(30 + visible * 16) if x >= 700 && x < 700 + 380 && y >= 100 && y < 72 + height { return true } } if diagnostics_panel.open { active := editor_active_buffer(editor) if active != nil && len(active.diagnostics) > 0 { height: f32 = 168 sidebar_width := left_sidebar_width(view) panel_y := f32(editor_content_bottom(view)) - height if x >= f32(sidebar_width) && y >= panel_y + 34 && y < panel_y + height { return true } } } if _, _, _, tab_ok := editor_tab_hit(editor, view, x, y); tab_ok { return true } if _, tree_ok := project_tree_row_at(tree, view, x, y); tree_ok { return true } if tasks_panel.open && len(tasks_panel.items) > 0 { rows := gradle_panel_rows(tasks_panel) if _, on_row := gradle_panel_row_at(tasks_panel, view, rows, x, y); on_row { return true } } return false } panel_resize_hit :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, x, y: f32) -> bool { if view.explorer_visible { left_edge := f32(left_sidebar_width(view)) if abs_f32(x - left_edge) <= SDL_RESIZE_HANDLE_WIDTH && y >= SDL_TOP_BAR_HEIGHT { return true } } if tasks_panel != nil && tasks_panel.open { right_edge := f32(content_right_edge(view.window_width) - right_sidebar_width_view(view, view.window_width)) if abs_f32(x - right_edge) <= SDL_RESIZE_HANDLE_WIDTH && y >= SDL_TOP_BAR_HEIGHT { return true } } return false } // The bottom panel resizes vertically by dragging its top edge. bottom_panel_resize_hit :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, x, y: f32) -> bool { if !view.terminal_visible do return false right := f32(content_right_edge(view.window_width)) if tasks_panel != nil && tasks_panel.open { right -= f32(right_sidebar_width_view(view, view.window_width)) } top := f32(view.window_height - SDL_STATUS_BAR_HEIGHT - bottom_panel_height(view)) return x >= f32(left_sidebar_width(view)) && x < right && abs_f32(y - top) <= SDL_RESIZE_HANDLE_WIDTH } handle_panel_resize_down :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, x, y: f32) -> bool { if bottom_panel_resize_hit(view, tasks_panel, x, y) { view.resizing_panel = .Bottom_Panel return true } if view.explorer_visible { left_edge := f32(left_sidebar_width(view)) if abs_f32(x - left_edge) <= SDL_RESIZE_HANDLE_WIDTH && y >= SDL_TOP_BAR_HEIGHT { view.resizing_panel = .Left_Sidebar return true } } if tasks_panel != nil && tasks_panel.open { right_edge := f32(content_right_edge(view.window_width) - right_sidebar_width_view(view, view.window_width)) if abs_f32(x - right_edge) <= SDL_RESIZE_HANDLE_WIDTH && y >= SDL_TOP_BAR_HEIGHT { view.resizing_panel = .Right_Sidebar return true } } return false } editor_text_hit :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, x, y: f32) -> bool { if y < SDL_EDITOR_TEXT_Y || y >= f32(editor_content_bottom(view)) do return false if x < f32(editor_text_x(view)) do return false right_limit := f32(content_right_edge(view.window_width) - 8) if tasks_panel != nil && tasks_panel.open { right_limit = f32(content_right_edge(view.window_width) - right_sidebar_width_view(view, view.window_width) - 8) } return x < right_limit } handle_panel_resize_motion :: proc(view: ^SDL_View, x: f32) { #partial switch view.resizing_panel { case .Left_Sidebar: max_width := min_int(SDL_SIDEBAR_MAX_WIDTH, max_int(view.window_width - view.right_sidebar_width - 260, SDL_SIDEBAR_MIN_WIDTH)) view.sidebar_width = clamp_int(int(x) - content_left_edge(), SDL_SIDEBAR_MIN_WIDTH, max_width) case .Right_Sidebar: available := view.window_width - left_sidebar_width(view) - 220 max_width := min_int(SDL_RIGHT_SIDEBAR_MAX_WIDTH, max_int(available, SDL_RIGHT_SIDEBAR_MIN_WIDTH)) view.right_sidebar_width = clamp_int(content_right_edge(view.window_width) - int(x), SDL_RIGHT_SIDEBAR_MIN_WIDTH, max_width) } } handle_bottom_panel_resize_motion :: proc(view: ^SDL_View, y: f32) { max_height := max_int(view.window_height - SDL_STATUS_BAR_HEIGHT - SDL_EDITOR_TOP - 120, SDL_TERMINAL_MIN_HEIGHT) view.terminal_height = clamp_int(view.window_height - SDL_STATUS_BAR_HEIGHT - int(y), SDL_TERMINAL_MIN_HEIGHT, max_height) } project_tree_row_at :: proc(tree: ^Project_Tree, view: ^SDL_View, x, y: f32) -> (int, bool) { if !view.explorer_visible || view.left_tab != .Files do return 0, false if x < f32(content_left_edge()) || x >= f32(left_sidebar_width(view)) do return 0, false if y < SDL_TREE_FIRST_Y do return 0, false index := view.tree_first + int((y - SDL_TREE_FIRST_Y) / SDL_TREE_ROW_HEIGHT) if index < 0 || index >= len(tree.files) do return 0, false return index, true } handle_project_tree_click :: proc(editor: ^Editor, tree: ^Project_Tree, view: ^SDL_View, x, y: f32) -> bool { if x >= f32(left_sidebar_width(view)) do return false if y < SDL_TREE_FIRST_Y do return false index, ok := project_tree_row_at(tree, view, x, y) if !ok do return false item := tree.files[index] if item.is_dir { project_tree_toggle_dir(tree, index) scroll_project_tree(tree, view, 0) return false } if !editor_open_or_focus_file(editor, item.path) do return false view.first_line = 0 return true } handle_editor_tab_click :: proc(editor: ^Editor, view: ^SDL_View, x, y: f32) -> bool { index, tab_x, width, ok := editor_tab_hit(editor, view, x, y) if !ok do return false if x >= tab_x + width - 28 { _ = editor_close_buffer(editor, index) view.tab_dragging = false return true } editor.active = index view.tab_dragging = true view.tab_drag_index = index return true } handle_editor_tab_drag :: proc(editor: ^Editor, view: ^SDL_View, x, y: f32) { if !view.tab_dragging || len(editor.buffers) <= 1 do return target, _, _, ok := editor_tab_hit(editor, view, x, y) if !ok || target == view.tab_drag_index do return editor_move_buffer(editor, view.tab_drag_index, target) view.tab_drag_index = target } handle_command_palette_click :: proc(palette: ^Command_Palette, editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, workspace: string, dialog: ^Folder_Dialog_State,completion: ^Completion_Popup, hover: ^Hover_Tooltip, definition: ^Definition_Jump, references: ^References_Panel, rename: ^Rename_Panel, search: ^Search_Panel, diagnostics_panel: ^Diagnostics_Panel, tasks_panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State, x, y: f32) -> bool { if !palette.open do return false width := f32(min_int(max_int(view.window_width - 280, 420), 720)) palette_x := f32(view.window_width) * 0.5 - width * 0.5 palette_y: f32 = SDL_TOP_BAR_HEIGHT + 48 match_count := command_palette_match_count(palette) visible := min_int(max_int(match_count, 1), 8) height := f32(58 + visible * 24) if x < palette_x || x >= palette_x + width || y < palette_y || y >= palette_y + height do return false if y < palette_y + 68 do return true if match_count == 0 do return true first_seen := 0 if palette.selected >= 8 { first_seen = palette.selected - 7 } row := int((y - (palette_y + 68)) / 24) target_seen := first_seen + row if target_seen >= 0 && target_seen < match_count { palette.selected = target_seen command_palette_accept(palette, editor, view, tree, workspace, dialog, completion, hover, definition, references, rename, search, diagnostics_panel, tasks_panel, daemon, sync) } return true } handle_completion_click :: proc(editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, popup: ^Completion_Popup, hover: ^Hover_Tooltip, references: ^References_Panel, rename: ^Rename_Panel, search: ^Search_Panel, sync: ^Daemon_Sync_State, x, y: f32) -> bool { if !popup.open do return false active := editor_active_buffer(editor) if active == nil do return false cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) popup_x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, cursor_line, cursor_col) popup_y := f32(SDL_EDITOR_TEXT_Y + (cursor_line - view.first_line + 1) * SDL_LINE_HEIGHT) if popup_y > f32(view.window_height - 110) do popup_y = f32(view.window_height - 110) visible_items := min_int(len(popup.items), 8) if visible_items <= 0 do return x >= popup_x && x < popup_x + 260 && y >= popup_y && y < popup_y + 26 height := f32(26 + visible_items * 16) if x < popup_x || x >= popup_x + 260 || y < popup_y || y >= popup_y + height do return false if y < popup_y + 22 do return true first_index := 0 if popup.selected >= 8 { first_index = popup.selected - 7 } row := int((y - (popup_y + 22)) / 16) index := first_index + row if index >= 0 && index < len(popup.items) { popup.selected = index if completion_popup_accept(popup, editor) { close_completion_accept_overlays(popup, hover, references, rename, search) ensure_cursor_visible(editor, view) daemon_sync_schedule(sync) } } return true } handle_references_click :: proc(panel: ^References_Panel, navigation: ^Navigation_History, editor: ^Editor, view: ^SDL_View, x, y: f32) -> bool { if !panel.open do return false popup_x: f32 = 700 popup_y: f32 = 72 visible := min_int(max_int(len(panel.items), 1), 14) height := f32(30 + visible * 16) if x < popup_x || x >= popup_x + 380 || y < popup_y || y >= popup_y + height do return false if y < popup_y + 28 do return true first_index := 0 if panel.selected >= 14 { first_index = panel.selected - 13 } row := int((y - (popup_y + 28)) / 16) index := first_index + row if index >= 0 && index < len(panel.items) { panel.selected = index references_panel_accept(panel, navigation, editor, view) } return true } handle_diagnostics_click :: proc(panel: ^Diagnostics_Panel, editor: ^Editor, view: ^SDL_View, x, y: f32) -> bool { if !panel.open do return false active := editor_active_buffer(editor) if active == nil do return false height: f32 = 168 sidebar_width := left_sidebar_width(view) panel_x := f32(sidebar_width) panel_y := f32(editor_content_bottom(view)) - height width := f32(view.window_width - sidebar_width) if x < panel_x || x >= panel_x + width || y < panel_y || y >= panel_y + height do return false if y < panel_y + 38 do return true if len(active.diagnostics) == 0 do return true panel.selected = clamp_int(panel.selected, 0, len(active.diagnostics) - 1) visible_items := 7 first_index := 0 if panel.selected >= visible_items { first_index = panel.selected - visible_items + 1 } row := int((y - (panel_y + 38)) / 18) index := first_index + row if index >= 0 && index < len(active.diagnostics) && index < first_index + visible_items { panel.selected = index diagnostics_panel_accept(panel, editor, view) } return true } // Task-list layout shared by rendering and click handling: rows are 18px, // or 30px when the task has a description line, and the list area sits // between the panel header (92px) and the output section. gradle_panel_output_height :: proc(panel: ^Gradle_Tasks_Panel, panel_height: f32) -> f32 { if len(panel.output) == 0 do return 0 return min_f32(160, max_f32(72, panel_height * 0.32)) } // The task list is an IntelliJ-style tree: one collapsible node per Gradle // project, with that project's tasks as children. SDL_GRADLE_ROW_HEIGHT :: 17 SDL_GRADLE_LIST_TOP :: 52 gradle_task_project :: proc(path: string) -> string { last := strings.last_index_byte(path, ':') if last <= 0 do return ":" return path[:last] } gradle_task_group :: proc(item: Gradle_Task_Item) -> string { if len(item.group) == 0 do return "other" return item.group } // Looks up a node's collapse state, seeding the IntelliJ-like default on // first sight: projects start expanded, groups start collapsed. gradle_panel_node_collapsed :: proc(panel: ^Gradle_Tasks_Panel, key: string, default_collapsed: bool) -> bool { if key not_in panel.collapsed { panel.collapsed[strings.clone(key)] = default_collapsed } return panel.collapsed[key] } append_unique_string :: proc(list: ^[dynamic]string, value: string) { for existing in list { if existing == value do return } append(list, value) } // Flattens the visible tree rows. Slices point into panel.items strings and // the result is temp-allocated, so use it within the same frame only. gradle_panel_rows :: proc(panel: ^Gradle_Tasks_Panel) -> []Gradle_Row { rows := make([dynamic]Gradle_Row, context.temp_allocator) projects := make([dynamic]string, context.temp_allocator) for item in panel.items { append_unique_string(&projects, gradle_task_project(item.path)) } slice.sort(projects[:]) for project in projects { append(&rows, Gradle_Row{kind = .Project, label = project, key = project}) if gradle_panel_node_collapsed(panel, project, false) do continue groups := make([dynamic]string, context.temp_allocator) for item in panel.items { if gradle_task_project(item.path) != project do continue append_unique_string(&groups, gradle_task_group(item)) } slice.sort(groups[:]) for group in groups { group_key := fmt.tprintf("%s|%s", project, group) append(&rows, Gradle_Row{kind = .Group, label = group, key = group_key}) if gradle_panel_node_collapsed(panel, group_key, true) do continue for item, index in panel.items { if gradle_task_project(item.path) != project || gradle_task_group(item) != group do continue label := item.name if len(label) == 0 do label = item.path append(&rows, Gradle_Row{kind = .Task, label = label, item_index = index}) } } } return rows[:] } gradle_panel_toggle_node :: proc(panel: ^Gradle_Tasks_Panel, key: string) { // Rows are built before any click lands on them, so the key is present. if key in panel.collapsed { panel.collapsed[key] = !panel.collapsed[key] } } gradle_panel_geometry :: proc(view: ^SDL_View, window_width, window_height: int) -> (x, y, width, height: f32) { width = f32(right_sidebar_width_view(view, window_width)) height = f32(window_height - SDL_TOP_BAR_HEIGHT - SDL_STATUS_BAR_HEIGHT) x = f32(content_right_edge(window_width)) - width y = SDL_TOP_BAR_HEIGHT return } gradle_panel_visible_rows :: proc(panel: ^Gradle_Tasks_Panel, panel_height: f32) -> int { list_height := panel_height - SDL_GRADLE_LIST_TOP - gradle_panel_output_height(panel, panel_height) - 6 return max_int(int(list_height / SDL_GRADLE_ROW_HEIGHT), 1) } // Maps a point to a row of the flattened tree; rows must come from // gradle_panel_rows this frame. gradle_panel_row_at :: proc(panel: ^Gradle_Tasks_Panel, view: ^SDL_View, rows: []Gradle_Row, x, y: f32) -> (int, bool) { panel_x, panel_y, width, height := gradle_panel_geometry(view, view.window_width, view.window_height) if x < panel_x || x >= panel_x + width do return 0, false list_top := panel_y + SDL_GRADLE_LIST_TOP if y < list_top do return 0, false visible := gradle_panel_visible_rows(panel, height) row := int((y - list_top) / SDL_GRADLE_ROW_HEIGHT) if row >= visible do return 0, false index := panel.scroll + row if index < 0 || index >= len(rows) do return 0, false return index, true } handle_gradle_tasks_click :: proc(view: ^SDL_View, panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client, x, y: f32, clicks: int) -> bool { if !panel.open do return false panel_x, panel_y, width, height := gradle_panel_geometry(view, view.window_width, view.window_height) if x < panel_x || x >= panel_x + width || y < panel_y || y >= panel_y + height do return false rows := gradle_panel_rows(panel) index, on_row := gradle_panel_row_at(panel, view, rows, x, y) if !on_row do return true row := rows[index] if row.kind == .Task { panel.selected = row.item_index if clicks >= 2 { gradle_tasks_panel_run(panel, daemon, row.item_index) } } else { gradle_panel_toggle_node(panel, row.key) } return true } handle_gradle_tasks_wheel :: proc(view: ^SDL_View, panel: ^Gradle_Tasks_Panel, x, y: f32, wheel_y: int) -> bool { if !panel.open do return false panel_x, panel_y, width, height := gradle_panel_geometry(view, view.window_width, view.window_height) if x < panel_x || x >= panel_x + width || y < panel_y || y >= panel_y + height do return false rows := gradle_panel_rows(panel) max_scroll := max_int(len(rows) - gradle_panel_visible_rows(panel, height), 0) panel.scroll = clamp_int(panel.scroll - wheel_y * 3, 0, max_scroll) return true } handle_overlay_outside_click :: proc(command_palette: ^Command_Palette, completion: ^Completion_Popup, references: ^References_Panel, diagnostics_panel: ^Diagnostics_Panel) -> bool { if command_palette.open { command_palette.open = false return true } if completion.open { completion.open = false return true } if references.open { references.open = false return true } if diagnostics_panel.open { diagnostics_panel.open = false return true } return false } handle_overlay_wheel :: proc(editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, command_palette: ^Command_Palette, completion: ^Completion_Popup, references: ^References_Panel, diagnostics_panel: ^Diagnostics_Panel, x, y: f32, wheel_y: int) -> bool { if command_palette.open { width := f32(min_int(max_int(view.window_width - 280, 420), 720)) palette_x := f32(view.window_width) * 0.5 - width * 0.5 palette_y: f32 = SDL_TOP_BAR_HEIGHT + 48 visible := min_int(max_int(command_palette_match_count(command_palette), 1), 8) height := f32(58 + visible * 24) if x >= palette_x && x < palette_x + width && y >= palette_y && y < palette_y + height { max_selected := max_int(command_palette_match_count(command_palette) - 1, 0) command_palette.selected = clamp_int(command_palette.selected - wheel_y * 3, 0, max_selected) return true } } if completion.open { active := editor_active_buffer(editor) if active != nil { cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) popup_x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, cursor_line, cursor_col) popup_y := f32(SDL_EDITOR_TEXT_Y + (cursor_line - view.first_line + 1) * SDL_LINE_HEIGHT) if popup_y > f32(view.window_height - 110) do popup_y = f32(view.window_height - 110) visible_items := min_int(len(completion.items), 8) height := f32(26 + visible_items * 16) if x >= popup_x && x < popup_x + 260 && y >= popup_y && y < popup_y + height { completion.selected = clamp_int(completion.selected - wheel_y * 3, 0, max_int(len(completion.items) - 1, 0)) return true } } } if references.open { popup_x: f32 = 700 popup_y: f32 = 72 visible := min_int(max_int(len(references.items), 1), 14) height := f32(30 + visible * 16) if x >= popup_x && x < popup_x + 380 && y >= popup_y && y < popup_y + height { references.selected = clamp_int(references.selected - wheel_y * 3, 0, max_int(len(references.items) - 1, 0)) return true } } if diagnostics_panel.open { active := editor_active_buffer(editor) if active != nil { height: f32 = 168 sidebar_width := left_sidebar_width(view) panel_x := f32(sidebar_width) panel_y := f32(editor_content_bottom(view)) - height width := f32(view.window_width - sidebar_width) if x >= panel_x && x < panel_x + width && y >= panel_y && y < panel_y + height { diagnostics_panel.selected = clamp_int(diagnostics_panel.selected - wheel_y * 3, 0, max_int(len(active.diagnostics) - 1, 0)) return true } } } return false } editor_tab_hit :: proc(editor: ^Editor, view: ^SDL_View, x, y: f32) -> (int, f32, f32, bool) { sidebar_width := left_sidebar_width(view) if x < f32(sidebar_width) || y < SDL_TOP_BAR_HEIGHT || y >= SDL_EDITOR_TOP do return 0, 0, 0, false tab_x := f32(sidebar_width + 12) max_x := f32(view.window_width - 12) for &buffer, index in editor.buffers { if tab_x >= max_x do break _, file_name := os.split_path(buffer.path) width := f32(tab_width_for_label(file_name)) if tab_x + width > max_x { width = max_x - tab_x } if x >= tab_x && x < tab_x + width { return index, tab_x, width, true } tab_x += width + 4 } return 0, 0, 0, false } editor_move_buffer :: proc(editor: ^Editor, from, to: int) -> bool { if from < 0 || from >= len(editor.buffers) || to < 0 || to >= len(editor.buffers) || from == to do return false buffer := editor.buffers[from] ordered_remove(&editor.buffers, from) inject_at(&editor.buffers, to, buffer) editor.active = to return true } tab_width_for_label :: proc(label: string) -> int { return max_int(150, min_int(280, gpu_text_width(label) + 54)) } handle_editor_text_mouse :: proc(editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, x, y: f32, extend: bool) -> bool { active := editor_active_buffer(editor) if active == nil || active.diff != nil do return false text_x := f32(editor_text_x(view)) text_y: f32 = SDL_EDITOR_TEXT_Y if x < text_x || y < text_y do return false line := view.first_line + int((y - text_y) / SDL_LINE_HEIGHT) if line < 0 || line >= buffer_line_count(&active.buffer) do return false column := text_column_from_pixel_x_gpu(gpu, &active.buffer, line, x - text_x) if extend { editor_start_selection(active) } else { editor_clear_selection(active) } cursor_move_to_line_col(&active.buffer, &active.cursor, line, max_int(column, 0)) if !extend { active.selection_anchor = active.cursor.offset } ensure_cursor_visible(editor, view) return true } SDL_CONTEXT_MENU_WIDTH :: 190 SDL_CONTEXT_MENU_ROW_HEIGHT :: 22 SDL_CONTEXT_MENU_PADDING :: 6 SDL_CONTEXT_MENU_SEPARATOR :: 7 context_menu_height :: proc() -> f32 { height := f32(2 * SDL_CONTEXT_MENU_PADDING) for item in context_menu_items { if item.separator_before do height += SDL_CONTEXT_MENU_SEPARATOR height += SDL_CONTEXT_MENU_ROW_HEIGHT } return height } context_menu_open_at :: proc(menu: ^Context_Menu, view: ^SDL_View, daemon: ^Daemon_Client, x, y: f32) { menu.x = max_f32(min_f32(x, f32(view.window_width) - SDL_CONTEXT_MENU_WIDTH - 4), 0) menu.y = max_f32(min_f32(y, f32(editor_content_bottom(view)) - context_menu_height() - 4), SDL_TOP_BAR_HEIGHT) menu.daemon_ok = daemon.connected menu.open = true } context_menu_hit :: proc(menu: ^Context_Menu, x, y: f32) -> bool { return x >= menu.x && x < menu.x + SDL_CONTEXT_MENU_WIDTH && y >= menu.y && y < menu.y + context_menu_height() } context_menu_item_at :: proc(menu: ^Context_Menu, x, y: f32) -> (int, bool) { if x < menu.x || x >= menu.x + SDL_CONTEXT_MENU_WIDTH do return 0, false row_y := menu.y + SDL_CONTEXT_MENU_PADDING for item, index in context_menu_items { if item.separator_before do row_y += SDL_CONTEXT_MENU_SEPARATOR if y >= row_y && y < row_y + SDL_CONTEXT_MENU_ROW_HEIGHT do return index, true row_y += SDL_CONTEXT_MENU_ROW_HEIGHT } return 0, false } context_menu_item_enabled :: proc(menu: ^Context_Menu, editor: ^Editor, action: Context_Menu_Action) -> bool { active := editor_active_buffer(editor) if active == nil do return false switch action { case .Cut, .Copy: _, _, has_selection := editor_selection_range(active) return has_selection case .Paste: return bool(SDL.HasClipboardText()) case .Definition, .References, .Rename: return menu.daemon_ok } return false } context_menu_perform :: proc(action: Context_Menu_Action, editor: ^Editor, view: ^SDL_View, completion: ^Completion_Popup, hover: ^Hover_Tooltip, definition: ^Definition_Jump, references: ^References_Panel, rename: ^Rename_Panel, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State) { active := editor_active_buffer(editor) if active == nil do return switch action { case .Cut: if editor_copy_selection_to_clipboard(active) && editor_delete_selection(active) { editor_update_dirty(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) daemon_sync_schedule(sync) } case .Copy: _ = editor_copy_selection_to_clipboard(active) case .Paste: if editor_paste_clipboard(active) { close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) daemon_sync_schedule(sync) } case .Definition: daemon_sync_now(sync, daemon, editor, false) definition_jump_request(definition, editor, daemon) case .References: daemon_sync_now(sync, daemon, editor, false) references_panel_request(references, editor, daemon) case .Rename: rename_panel_open(rename) } } handle_context_menu_click :: proc(menu: ^Context_Menu, editor: ^Editor, view: ^SDL_View, completion: ^Completion_Popup, hover: ^Hover_Tooltip, definition: ^Definition_Jump, references: ^References_Panel, rename: ^Rename_Panel, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State, x, y: f32) -> bool { if !menu.open do return false if !context_menu_hit(menu, x, y) { menu.open = false return true } index, on_item := context_menu_item_at(menu, x, y) if !on_item do return true item := context_menu_items[index] if !context_menu_item_enabled(menu, editor, item.action) do return true menu.open = false context_menu_perform(item.action, editor, view, completion, hover, definition, references, rename, daemon, sync) return true } editor_line_col_from_mouse :: proc(editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, x, y: f32) -> (int, int, bool) { active := editor_active_buffer(editor) if active == nil do return 0, 0, false text_x := f32(editor_text_x(view)) if x < text_x || y < SDL_EDITOR_TEXT_Y do return 0, 0, false line := view.first_line + int((y - SDL_EDITOR_TEXT_Y) / SDL_LINE_HEIGHT) if line < 0 || line >= buffer_line_count(&active.buffer) do return 0, 0, false column := text_column_from_pixel_x_gpu(gpu, &active.buffer, line, x - text_x) return line, max_int(column, 0), true } editor_offset_from_mouse :: proc(editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, x, y: f32) -> (int, bool) { active := editor_active_buffer(editor) if active == nil do return 0, false line, column, ok := editor_line_col_from_mouse(editor, view, gpu, x, y) if !ok do return 0, false return buffer_line_col_to_offset(&active.buffer, line, column), true } handle_editor_right_click :: proc(menu: ^Context_Menu, editor: ^Editor, view: ^SDL_View, gpu: ^GPU_Renderer, tasks_panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client, x, y: f32) -> bool { if !editor_text_hit(view, tasks_panel, x, y) { menu.open = false return false } active := editor_active_buffer(editor) if active == nil || active.diff != nil do return false // Keep the selection when right-clicking inside it; otherwise move the // caret to the click, matching IDE behavior. offset, has_offset := editor_offset_from_mouse(editor, view, gpu, x, y) start, end, has_selection := editor_selection_range(active) if !(has_selection && has_offset && offset >= start && offset < end) { _ = handle_editor_text_mouse(editor, view, gpu, x, y, false) } context_menu_open_at(menu, view, daemon, x, y) return true } render_context_menu_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, menu: ^Context_Menu, editor: ^Editor) { if !menu.open do return height := context_menu_height() gpu_rect(gpu, menu.x, menu.y, SDL_CONTEXT_MENU_WIDTH, height, 25, 28, 36, 255) gpu_rect_outline(gpu, menu.x, menu.y, SDL_CONTEXT_MENU_WIDTH, height, 58, 62, 72, 255) hovered_index, has_hover := context_menu_item_at(menu, view.mouse_x, view.mouse_y) row_y := menu.y + SDL_CONTEXT_MENU_PADDING for item, index in context_menu_items { if item.separator_before { gpu_rect(gpu, menu.x + 8, row_y + 3, SDL_CONTEXT_MENU_WIDTH - 16, 1, 52, 55, 63, 255) row_y += SDL_CONTEXT_MENU_SEPARATOR } enabled := context_menu_item_enabled(menu, editor, item.action) if enabled && has_hover && hovered_index == index { gpu_rect(gpu, menu.x + 4, row_y, SDL_CONTEXT_MENU_WIDTH - 8, SDL_CONTEXT_MENU_ROW_HEIGHT, 49, 56, 70, 255) } if enabled { gpu_text(gpu, menu.x + 14, row_y + 4, item.label, 220, 223, 228, 255) } else { gpu_text(gpu, menu.x + 14, row_y + 4, item.label, 120, 124, 132, 255) } row_y += SDL_CONTEXT_MENU_ROW_HEIGHT } } scroll_project_tree :: proc(tree: ^Project_Tree, view: ^SDL_View, delta: int) { max_first := max_int(len(tree.files) - visible_tree_rows(view), 0) view.tree_first = clamp_int(view.tree_first + delta, 0, max_first) } handle_sdl_key :: proc(editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, workspace: string, dialog: ^Folder_Dialog_State,completion: ^Completion_Popup, hover: ^Hover_Tooltip, definition: ^Definition_Jump, navigation: ^Navigation_History, references: ^References_Panel, rename: ^Rename_Panel, search: ^Search_Panel, diagnostics_panel: ^Diagnostics_Panel, command_palette: ^Command_Palette, tasks_panel: ^Gradle_Tasks_Panel, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State, key: SDL.Keycode, mod: SDL.Keymod) -> bool { active := editor_active_buffer(editor) if active == nil do return false if key == SDL.K_C && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && hover_tooltip_copy_selection(hover) { return false } if command_palette.open { switch key { case SDL.K_ESCAPE: command_palette.open = false return false case SDL.K_BACKSPACE: command_palette_backspace(command_palette) return false case SDL.K_UP: command_palette.selected = max_int(command_palette.selected - 1, 0) return false case SDL.K_DOWN: command_palette.selected = min_int(command_palette.selected + 1, max_int(command_palette_match_count(command_palette) - 1, 0)) return false case SDL.K_PAGEUP: command_palette.selected = max_int(command_palette.selected - 8, 0) return false case SDL.K_PAGEDOWN: command_palette.selected = min_int(command_palette.selected + 8, max_int(command_palette_match_count(command_palette) - 1, 0)) return false case SDL.K_HOME: command_palette.selected = 0 return false case SDL.K_END: command_palette.selected = max_int(command_palette_match_count(command_palette) - 1, 0) return false case SDL.K_RETURN: command_palette_accept(command_palette, editor, view, tree, workspace, dialog, completion, hover, definition, references, rename, search, diagnostics_panel, tasks_panel, daemon, sync) return false } } if rename.open { switch key { case SDL.K_ESCAPE: rename.open = false return false case SDL.K_BACKSPACE: rename_panel_backspace(rename) return false case SDL.K_RETURN: if len(rename.edits) > 0 { _ = rename_panel_apply_edits(rename, editor, sync) } else { daemon_sync_now(sync, daemon, editor, false) rename_panel_request(rename, editor, daemon) } return false } } if search.open { switch key { case SDL.K_ESCAPE: search.open = false return false case SDL.K_BACKSPACE: search_panel_backspace(search) return false case SDL.K_RETURN: if (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { search_panel_find_previous(search, editor, view) } else { search_panel_find_next(search, editor, view) } return false } } if diagnostics_panel.open { active_for_diagnostics := editor_active_buffer(editor) diagnostic_count := 0 if active_for_diagnostics == nil else len(active_for_diagnostics.diagnostics) switch key { case SDL.K_ESCAPE: diagnostics_panel.open = false return false case SDL.K_UP: diagnostics_panel.selected = max_int(diagnostics_panel.selected - 1, 0) return false case SDL.K_DOWN: diagnostics_panel.selected = min_int(diagnostics_panel.selected + 1, max_int(diagnostic_count - 1, 0)) return false case SDL.K_PAGEUP: diagnostics_panel.selected = max_int(diagnostics_panel.selected - 7, 0) return false case SDL.K_PAGEDOWN: diagnostics_panel.selected = min_int(diagnostics_panel.selected + 7, max_int(diagnostic_count - 1, 0)) return false case SDL.K_HOME: diagnostics_panel.selected = 0 return false case SDL.K_END: diagnostics_panel.selected = max_int(diagnostic_count - 1, 0) return false case SDL.K_RETURN: diagnostics_panel_accept(diagnostics_panel, editor, view) return false } } if tasks_panel.open && key == SDL.K_ESCAPE { tasks_panel.open = false view.gradle_sidebar_visible = false ui_state_save(view) return false } // Diff tabs are read-only: allow closing, the palette, and scrolling; // swallow everything else so nothing edits the hidden buffer. if active.diff != nil { ctrl := (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE shift := (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE switch { case key == SDL.K_W && ctrl: if editor_close_active(editor) { scroll_sdl_view(editor, view, 0) } case key == SDL.K_P && ctrl && shift: command_palette_open(command_palette) case key == SDL.K_UP: diff_view_scroll(active, view, -1) case key == SDL.K_DOWN: diff_view_scroll(active, view, 1) case key == SDL.K_PAGEUP: diff_view_scroll(active, view, -visible_editor_lines(view)) case key == SDL.K_PAGEDOWN: diff_view_scroll(active, view, visible_editor_lines(view)) case key == SDL.K_HOME && ctrl: active.diff.scroll = 0 case key == SDL.K_END && ctrl: diff_view_scroll(active, view, len(active.diff.rows)) } return false } if key == SDL.K_LEFT && (mod & SDL.KMOD_ALT) != SDL.KMOD_NONE { navigation_go_back(navigation, editor, view) return false } if key == SDL.K_RIGHT && (mod & SDL.KMOD_ALT) != SDL.KMOD_NONE { navigation_go_forward(navigation, editor, view) return false } if key == SDL.K_SPACE && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { daemon_sync_now(sync, daemon, editor, false) completion_popup_open(completion, editor, daemon) return false } if key == SDL.K_H && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { daemon_sync_now(sync, daemon, editor, false) hover_tooltip_open(hover, editor, daemon) return false } if key == SDL.K_B && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { daemon_sync_now(sync, daemon, editor, false) definition_jump_request(definition, editor, daemon) return false } if key == SDL.K_R && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { rename_panel_open(rename) return false } if key == SDL.K_R && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { daemon_sync_now(sync, daemon, editor, false) references_panel_request(references, editor, daemon) return false } if key == SDL.K_F && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { search_panel_open(search) return false } if key == SDL.K_O && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { folder_dialog_show(dialog, workspace) return false } if key == SDL.K_P && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { command_palette_open(command_palette) return false } if key == SDL.K_E && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { view.explorer_visible = !view.explorer_visible view.tree_first = 0 ui_state_save(view) return false } if key == SDL.K_T && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { if tasks_panel.open { tasks_panel.open = false view.gradle_sidebar_visible = false } else { view.gradle_sidebar_visible = true daemon_sync_now(sync, daemon, editor, false) gradle_tasks_panel_request(tasks_panel, daemon) } ui_state_save(view) return false } if key == SDL.K_M && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { diagnostics_panel.open = !diagnostics_panel.open diagnostics_panel.selected = 0 return false } if key == SDL.K_M && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { view.show_metrics = !view.show_metrics return false } if key == SDL.K_F8 && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { editor_jump_diagnostic(editor, view, -1) return false } if key == SDL.K_F8 { editor_jump_diagnostic(editor, view, 1) return false } if references.open { switch key { case SDL.K_ESCAPE: references.open = false return false case SDL.K_UP: references.selected = max_int(references.selected - 1, 0) return false case SDL.K_DOWN: references.selected = min_int(references.selected + 1, max_int(len(references.items) - 1, 0)) return false case SDL.K_PAGEUP: references.selected = max_int(references.selected - 14, 0) return false case SDL.K_PAGEDOWN: references.selected = min_int(references.selected + 14, max_int(len(references.items) - 1, 0)) return false case SDL.K_HOME: references.selected = 0 return false case SDL.K_END: references.selected = max_int(len(references.items) - 1, 0) return false case SDL.K_RETURN: references_panel_accept(references, navigation, editor, view) return false } } if completion.open { switch key { case SDL.K_ESCAPE: completion.open = false return false case SDL.K_UP: completion.selected = max_int(completion.selected - 1, 0) return false case SDL.K_DOWN: completion.selected = min_int(completion.selected + 1, max_int(len(completion.items) - 1, 0)) return false case SDL.K_PAGEUP: completion.selected = max_int(completion.selected - 8, 0) return false case SDL.K_PAGEDOWN: completion.selected = min_int(completion.selected + 8, max_int(len(completion.items) - 1, 0)) return false case SDL.K_HOME: completion.selected = 0 return false case SDL.K_END: completion.selected = max_int(len(completion.items) - 1, 0) return false case SDL.K_RETURN: if completion_popup_accept(completion, editor) { close_completion_accept_overlays(completion, hover, references, rename, search) ensure_cursor_visible(editor, view) daemon_sync_schedule(sync) return true } completion.open = false return false } } if key == SDL.K_S && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE && (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { _ = editor_save_all(editor) daemon_sync_now(sync, daemon, editor, false) return false } if key == SDL.K_S && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { _ = editor_save_active(editor) daemon_sync_now(sync, daemon, editor, false) return false } if key == SDL.K_W && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { if editor_close_active(editor) { scroll_sdl_view(editor, view, 0) } return false } if key == SDL.K_Z && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { if buffer_undo(&active.buffer, &active.cursor) { editor_update_dirty(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true } return false } if key == SDL.K_Y && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { if buffer_redo(&active.buffer, &active.cursor) { editor_update_dirty(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true } return false } if key == SDL.K_C && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { editor_copy_selection_to_clipboard(active) return false } if key == SDL.K_X && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { if editor_copy_selection_to_clipboard(active) && editor_delete_selection(active) { editor_update_dirty(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true } return false } if key == SDL.K_V && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { if editor_paste_clipboard(active) { close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true } return false } if key == SDL.K_LEFT && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { prepare_selection_for_keyboard_move(active, mod) cursor_move_word_left(&active.buffer, &active.cursor) finish_selection_for_keyboard_move(active, mod) ensure_cursor_visible(editor, view) return false } if key == SDL.K_RIGHT && (mod & SDL.KMOD_CTRL) != SDL.KMOD_NONE { prepare_selection_for_keyboard_move(active, mod) cursor_move_word_right(&active.buffer, &active.cursor) finish_selection_for_keyboard_move(active, mod) ensure_cursor_visible(editor, view) return false } switch key { case SDL.K_ESCAPE: hover.open = false return false case SDL.K_RETURN: editor_insert_newline_auto_indent(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true case SDL.K_TAB: if (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { if editor_outdent(active) { close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true } return false } editor_insert_text(active, " ") close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true case SDL.K_BACKSPACE: if !editor_delete_selection(active) { cursor_backspace(&active.buffer, &active.cursor) } editor_clear_selection(active) editor_update_dirty(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true case SDL.K_DELETE: if editor_delete_selection(active) || cursor_delete_forward(&active.buffer, &active.cursor) { editor_clear_selection(active) editor_update_dirty(active) close_stale_edit_overlays(completion, hover, references, rename) ensure_cursor_visible(editor, view) return true } case SDL.K_UP: prepare_selection_for_keyboard_move(active, mod) cursor_move_vertical(&active.buffer, &active.cursor, -1) finish_selection_for_keyboard_move(active, mod) ensure_cursor_visible(editor, view) case SDL.K_DOWN: prepare_selection_for_keyboard_move(active, mod) cursor_move_vertical(&active.buffer, &active.cursor, 1) finish_selection_for_keyboard_move(active, mod) ensure_cursor_visible(editor, view) case SDL.K_LEFT: prepare_selection_for_keyboard_move(active, mod) if active.cursor.offset > 0 { active.cursor.offset -= 1 _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) } finish_selection_for_keyboard_move(active, mod) case SDL.K_RIGHT: prepare_selection_for_keyboard_move(active, mod) if active.cursor.offset < buffer_len(&active.buffer) { active.cursor.offset += 1 _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) } finish_selection_for_keyboard_move(active, mod) case SDL.K_HOME: prepare_selection_for_keyboard_move(active, mod) line, _ := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) active.cursor.offset = buffer_line_start(&active.buffer, line) active.cursor.wanted_column = 0 finish_selection_for_keyboard_move(active, mod) ensure_cursor_visible(editor, view) case SDL.K_END: prepare_selection_for_keyboard_move(active, mod) line, _ := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) active.cursor.offset = buffer_line_end(&active.buffer, line) _, active.cursor.wanted_column = buffer_offset_to_line_col(&active.buffer, active.cursor.offset) finish_selection_for_keyboard_move(active, mod) ensure_cursor_visible(editor, view) case SDL.K_PAGEUP: prepare_selection_for_keyboard_move(active, mod) page_delta := max_int(visible_editor_lines(view) - 2, 1) cursor_move_vertical(&active.buffer, &active.cursor, -page_delta) finish_selection_for_keyboard_move(active, mod) scroll_sdl_view(editor, view, -page_delta) ensure_cursor_visible(editor, view) case SDL.K_PAGEDOWN: prepare_selection_for_keyboard_move(active, mod) page_delta := max_int(visible_editor_lines(view) - 2, 1) cursor_move_vertical(&active.buffer, &active.cursor, page_delta) finish_selection_for_keyboard_move(active, mod) scroll_sdl_view(editor, view, page_delta) ensure_cursor_visible(editor, view) } return false } editor_copy_selection_to_clipboard :: proc(active: ^Editor_Buffer) -> bool { start, end, ok := editor_selection_range(active) if !ok do return false bytes := buffer_range_bytes(&active.buffer, start, end - start) defer delete(bytes) text := string(bytes[:]) c_text, err := strings.clone_to_cstring(text, context.temp_allocator) if err != nil do return false return SDL.SetClipboardText(c_text) } editor_paste_clipboard :: proc(active: ^Editor_Buffer) -> bool { if !SDL.HasClipboardText() do return false raw := SDL.GetClipboardText() if raw == nil do return false defer SDL.free(raw) text := string(cstring(raw)) if len(text) == 0 do return false editor_insert_text(active, text) return true } editor_jump_diagnostic :: proc(editor: ^Editor, view: ^SDL_View, direction: int) { active := editor_active_buffer(editor) if active == nil || len(active.diagnostics) == 0 do return cursor_line, cursor_column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) best_index := -1 best_distance := 1 << 30 for diagnostic, index in active.diagnostics { distance: int if direction >= 0 { if diagnostic.line > cursor_line || (diagnostic.line == cursor_line && diagnostic.column > cursor_column) { distance = (diagnostic.line - cursor_line) * 100000 + diagnostic.column - cursor_column } else { distance = (buffer_line_count(&active.buffer) + diagnostic.line - cursor_line) * 100000 + diagnostic.column } } else { if diagnostic.line < cursor_line || (diagnostic.line == cursor_line && diagnostic.column < cursor_column) { distance = (cursor_line - diagnostic.line) * 100000 + cursor_column - diagnostic.column } else { distance = (buffer_line_count(&active.buffer) + cursor_line - diagnostic.line) * 100000 + cursor_column } } if distance < best_distance { best_distance = distance best_index = index } } if best_index < 0 do return target := active.diagnostics[best_index] cursor_move_to_line_col(&active.buffer, &active.cursor, target.line, target.column) editor_clear_selection(active) ensure_cursor_visible(editor, view) } diagnostics_panel_accept :: proc(panel: ^Diagnostics_Panel, editor: ^Editor, view: ^SDL_View) { active := editor_active_buffer(editor) if active == nil || len(active.diagnostics) == 0 do return index := clamp_int(panel.selected, 0, len(active.diagnostics) - 1) diagnostic := active.diagnostics[index] cursor_move_to_line_col(&active.buffer, &active.cursor, diagnostic.line, diagnostic.column) editor_clear_selection(active) ensure_cursor_visible(editor, view) } prepare_selection_for_keyboard_move :: proc(active: ^Editor_Buffer, mod: SDL.Keymod) { if (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { editor_start_selection(active) } } finish_selection_for_keyboard_move :: proc(active: ^Editor_Buffer, mod: SDL.Keymod) { if (mod & SDL.KMOD_SHIFT) != SDL.KMOD_NONE { start, end, ok := editor_selection_range(active) if !ok || start == end { editor_clear_selection(active) } } else { editor_clear_selection(active) } } scroll_sdl_view :: proc(editor: ^Editor, view: ^SDL_View, delta: int) { active := editor_active_buffer(editor) if active == nil do return if active.diff != nil { diff_view_scroll(active, view, delta) return } max_first := max_int(buffer_line_count(&active.buffer) - visible_editor_lines(view), 0) view.first_line = clamp_int(view.first_line + delta, 0, max_first) } ensure_cursor_visible :: proc(editor: ^Editor, view: ^SDL_View) { active := editor_active_buffer(editor) if active == nil do return line, _ := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) visible_lines := visible_editor_lines(view) if line < view.first_line { view.first_line = line } else if line >= view.first_line + visible_lines { view.first_line = line - visible_lines + 1 } scroll_sdl_view(editor, view, 0) } refresh_view_size :: proc(window: ^SDL.Window, view: ^SDL_View) { w, h: i32 if SDL.GetWindowSize(window, &w, &h) { view.window_width = max_int(int(w), 1) view.window_height = max_int(int(h), 1) } } visible_editor_lines :: proc(view: ^SDL_View) -> int { available := editor_content_bottom(view) - SDL_EDITOR_TEXT_Y - 8 return max_int(available / SDL_LINE_HEIGHT, 1) } visible_tree_rows :: proc(view: ^SDL_View) -> int { available := view.window_height - SDL_TREE_FIRST_Y - SDL_STATUS_BAR_HEIGHT - 8 return max_int(available / SDL_TREE_ROW_HEIGHT, 1) } render_sdl_editor :: proc(renderer: ^SDL.Renderer, editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, completion: ^Completion_Popup, hover: ^Hover_Tooltip, references: ^References_Panel, rename: ^Rename_Panel) { active := editor_active_buffer(editor) if active == nil do return _ = SDL.SetRenderDrawColor(renderer, 15, 17, 22, 255) _ = SDL.RenderClear(renderer) render_project_tree(renderer, tree, view, active.path) _ = SDL.SetRenderDrawColor(renderer, 37, 41, 54, 255) sidebar_width := left_sidebar_width(view) text_x := editor_text_x(view) gutter := SDL.FRect{f32(sidebar_width), 0, 72, f32(view.window_height)} _ = SDL.RenderFillRect(renderer, &gutter) cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) line_height: f32 = 16 y: f32 = 18 dirty_marker := "" if !active.dirty else "*" _ = SDL.SetRenderDrawColor(renderer, 180, 190, 210, 255) title := fmt.tprintf("%s%s v%d", active.path, dirty_marker, active.buffer.version) render_debug_text_limited(renderer, f32(sidebar_width + 84), 4, title, 92) for screen_line := 0; screen_line < visible_editor_lines(view); screen_line += 1 { line_index := view.first_line + screen_line if line_index >= buffer_line_count(&active.buffer) do break bytes := buffer_line_bytes(&active.buffer, line_index) line_text := string(bytes[:]) gutter_text := fmt.ctprintf("%4d", line_index + 1) diagnostic, has_diagnostic := editor_diagnostic_on_line(editor, line_index) if line_index == cursor_line { marker := SDL.FRect{f32(sidebar_width), y - 2, f32(view.window_width - sidebar_width), line_height} _ = SDL.SetRenderDrawColor(renderer, 29, 34, 48, 255) _ = SDL.RenderFillRect(renderer, &marker) _ = SDL.SetRenderDrawColor(renderer, 138, 180, 248, 255) } else if has_diagnostic { _ = SDL.SetRenderDrawColor(renderer, 235, 95, 95, 255) } else { _ = SDL.SetRenderDrawColor(renderer, 103, 111, 135, 255) } _ = SDL.RenderDebugText(renderer, f32(sidebar_width + 20), y, gutter_text) if has_diagnostic { _ = SDL.RenderDebugText(renderer, f32(sidebar_width + 62), y, "!") } render_selection_for_line(renderer, view, active, line_index, y) _ = SDL.SetRenderDrawColor(renderer, 220, 225, 235, 255) render_debug_text_limited(renderer, f32(text_x), y, line_text, visible_text_columns(view)) if line_index == cursor_line { cursor_x := f32(text_x + cursor_col * SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE) _ = SDL.SetRenderDrawColor(renderer, 255, 210, 120, 255) _ = SDL.RenderLine(renderer, cursor_x, y - 2, cursor_x, y + line_height - 2) } delete(bytes) y += line_height } if len(editor.status) > 0 { _ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255) status := fmt.ctprintf("%s", editor.status) _ = SDL.RenderDebugText(renderer, f32(sidebar_width + 84), 734, status) } else if len(active.diagnostics) > 0 { first := active.diagnostics[0] errors, warnings, infos := editor_diagnostic_counts(active) status := fmt.tprintf("%d errors %d warnings %d info | %s %d:%d %s", errors, warnings, infos, first.severity, first.line + 1, first.column + 1, first.message) _ = SDL.SetRenderDrawColor(renderer, 235, 95, 95, 255) render_debug_text_limited(renderer, f32(sidebar_width + 84), 734, status, 92) } else if active.dirty { _ = SDL.SetRenderDrawColor(renderer, 255, 210, 120, 255) _ = SDL.RenderDebugText(renderer, f32(sidebar_width + 84), 734, "modified - Ctrl+S to save") } else { _ = SDL.SetRenderDrawColor(renderer, 120, 210, 150, 255) _ = SDL.RenderDebugText(renderer, f32(sidebar_width + 84), 734, "saved") } render_completion_popup(renderer, editor, view, completion) render_hover_tooltip(renderer, editor, view, hover) render_references_panel(renderer, references) render_rename_panel(renderer, rename) _ = SDL.RenderPresent(renderer) } render_sdl_editor_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL_View, tree: ^Project_Tree, completion: ^Completion_Popup, hover: ^Hover_Tooltip, references: ^References_Panel, rename: ^Rename_Panel, search: ^Search_Panel, diagnostics_panel: ^Diagnostics_Panel, command_palette: ^Command_Palette, context_menu: ^Context_Menu, tasks_panel: ^Gradle_Tasks_Panel, terminal: ^Terminal_Panel, git: ^Git_Panel, git_status: ^Git_Status_Panel) { active := editor_active_buffer(editor) if active == nil do return gpu_begin(gpu) gpu_rect(gpu, 0, 0, f32(gpu.width), f32(gpu.height), 30, 31, 34, 255) render_top_bar_gpu(gpu, active) sidebar_width := left_sidebar_width(view) text_x := editor_text_x(view) render_left_sidebar_gpu(gpu, view, tree, git_status, active.path) editor_height := f32(editor_content_bottom(view) - SDL_EDITOR_TOP) gpu_rect(gpu, f32(sidebar_width), SDL_TOP_BAR_HEIGHT, f32(gpu.width - sidebar_width), SDL_TAB_BAR_HEIGHT, 37, 38, 43, 255) gpu_rect(gpu, f32(sidebar_width), SDL_EDITOR_TOP, f32(SDL_GUTTER_WIDTH), editor_height, 32, 33, 36, 255) gpu_rect(gpu, f32(sidebar_width + SDL_GUTTER_WIDTH), SDL_EDITOR_TOP, f32(gpu.width - sidebar_width - SDL_GUTTER_WIDTH), editor_height, 27, 28, 32, 255) gpu_rect(gpu, f32(sidebar_width - 1), SDL_TOP_BAR_HEIGHT, 1, f32(gpu.height - SDL_TOP_BAR_HEIGHT), 48, 50, 56, 255) gpu_rect(gpu, f32(sidebar_width + SDL_GUTTER_WIDTH - 1), SDL_EDITOR_TOP, 1, editor_height, 43, 45, 51, 255) render_editor_tabs_gpu(gpu, editor, view) if active.diff != nil { render_diff_view_gpu(gpu, view, active, tasks_panel) } else { cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) line_height: f32 = SDL_LINE_HEIGHT y: f32 = SDL_EDITOR_TEXT_Y syntax_state := kotlin_syntax_state_before_line(&active.buffer, view.first_line) for screen_line := 0; screen_line < visible_editor_lines(view); screen_line += 1 { line_index := view.first_line + screen_line if line_index >= buffer_line_count(&active.buffer) do break bytes := buffer_line_bytes(&active.buffer, line_index) line_text := string(bytes[:]) gutter_text := fmt.tprintf("%4d", line_index + 1) diagnostic, has_diagnostic := editor_diagnostic_on_line(editor, line_index) gutter_color := [4]u8{116, 120, 128, 255} if line_index == cursor_line { gpu_rect(gpu, f32(sidebar_width), y - 3, f32(gpu.width - sidebar_width), line_height + 1, 38, 40, 46, 255) gpu_rect(gpu, f32(sidebar_width + SDL_GUTTER_WIDTH), y - 3, 2, line_height + 1, 77, 155, 230, 255) gutter_color = {169, 174, 184, 255} } else if has_diagnostic { gutter_color = {244, 113, 116, 255} } gpu_text(gpu, f32(sidebar_width + 18), y, gutter_text, gutter_color[0], gutter_color[1], gutter_color[2], gutter_color[3]) if has_diagnostic { gpu_text(gpu, f32(sidebar_width + 58), y, "!", 244, 113, 116, 255) } render_selection_for_line_gpu(gpu, view, active, line_index, y) render_search_matches_for_line_gpu(gpu, view, active, search, line_index, line_text, y) syntax_state = render_syntax_line_gpu(gpu, f32(text_x), y, line_text, visible_gpu_text_columns(gpu, view, tasks_panel), syntax_state) if has_diagnostic { render_diagnostic_underline_gpu(gpu, view, &active.buffer, line_index, diagnostic.column, y) } if line_index == cursor_line { cursor_x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, cursor_line, cursor_col) gpu_line(gpu, cursor_x, y - 3, cursor_x, y + line_height - 1, 230, 230, 230, 255) } delete(bytes) y += line_height } } if len(editor.status) > 0 { render_status_bar_gpu(gpu, editor.status, false) } else if len(active.diagnostics) > 0 { first := active.diagnostics[0] errors, warnings, infos := editor_diagnostic_counts(active) status := fmt.tprintf("%d errors %d warnings %d info | %s %d:%d %s", errors, warnings, infos, first.severity, first.line + 1, first.column + 1, first.message) render_status_bar_gpu(gpu, status, true) } else if active.dirty { render_status_bar_gpu(gpu, "modified - Ctrl+S to save", false) } else { render_status_bar_gpu(gpu, "saved", false) } render_terminal_panel_gpu(gpu, view, terminal, git, tasks_panel) render_completion_popup_gpu(gpu, editor, view, completion) render_hover_tooltip_gpu(gpu, editor, view, hover) render_references_panel_gpu(gpu, references) render_rename_panel_gpu(gpu, rename) render_search_panel_gpu(gpu, search) render_diagnostics_panel_gpu(gpu, editor, view, diagnostics_panel) render_gradle_tasks_panel_gpu(gpu, view, tasks_panel) render_tool_strip_gpu(gpu, tasks_panel, terminal) render_command_palette_gpu(gpu, view, command_palette) render_context_menu_gpu(gpu, view, context_menu, editor) render_metrics_overlay_gpu(gpu, editor, view) gpu_present(gpu) } visible_text_columns :: proc(view: ^SDL_View) -> int { return max_int((view.window_width - editor_text_x(view) - 8) / SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE, 1) } visible_gpu_text_columns :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel) -> int { right_limit := content_right_edge(gpu.width) - 16 if tasks_panel != nil && tasks_panel.open { right_limit = content_right_edge(gpu.width) - right_sidebar_width_view(view, gpu.width) - 16 } return max_int(int(f32(right_limit - editor_text_x(view)) / max_f32(gpu.font_advance, 1)), 1) } editor_column_pixel_x :: proc(view: ^SDL_View, buffer: ^Buffer, line, column: int) -> f32 { return f32(editor_text_x(view) + line_prefix_pixel_width(buffer, line, column)) } editor_column_pixel_x_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, buffer: ^Buffer, line, column: int) -> f32 { return round_f32(f32(editor_text_x(view)) + line_prefix_pixel_advance_gpu(gpu, buffer, line, column)) } line_prefix_pixel_width :: proc(buffer: ^Buffer, line, column: int) -> int { bytes := buffer_line_bytes(buffer, line) defer delete(bytes) prefix_len := clamp_int(column, 0, len(bytes)) return gpu_text_width(string(bytes[:prefix_len])) } line_prefix_pixel_width_gpu :: proc(gpu: ^GPU_Renderer, buffer: ^Buffer, line, column: int) -> int { return int(line_prefix_pixel_advance_gpu(gpu, buffer, line, column) + 0.5) } line_prefix_pixel_advance_gpu :: proc(gpu: ^GPU_Renderer, buffer: ^Buffer, line, column: int) -> f32 { bytes := buffer_line_bytes(buffer, line) defer delete(bytes) prefix_len := clamp_int(column, 0, len(bytes)) return gpu_font_text_advance(gpu, string(bytes[:prefix_len])) } text_column_from_pixel_x :: proc(buffer: ^Buffer, line: int, x: f32) -> int { bytes := buffer_line_bytes(buffer, line) defer delete(bytes) if len(bytes) == 0 do return 0 target := max_int(int(x), 0) best := 0 best_distance := 1 << 30 for column := 0; column <= len(bytes); column += 1 { width := gpu_text_width(string(bytes[:column])) distance := abs_int(width - target) if distance < best_distance { best = column best_distance = distance } if width > target && column > 0 do break } return best } text_column_from_pixel_x_gpu :: proc(gpu: ^GPU_Renderer, buffer: ^Buffer, line: int, x: f32) -> int { if gpu == nil || !gpu.available || gpu.font_advance <= 0 { return text_column_from_pixel_x(buffer, line, x) } bytes := buffer_line_bytes(buffer, line) defer delete(bytes) if len(bytes) == 0 do return 0 column := int((x + gpu.font_advance * 0.5) / gpu.font_advance) return clamp_int(column, 0, len(bytes)) } abs_int :: proc(v: int) -> int { if v < 0 do return -v return v } render_syntax_line_gpu :: proc(gpu: ^GPU_Renderer, x, y: f32, line: string, max_chars: int, state: Syntax_State) -> Syntax_State { if len(line) == 0 do return state limit := min_int(len(line), max_chars) spans, next_state := tokenize_kotlin_line(line[:limit], state) defer delete(spans) for span in spans { if span.start >= span.end do continue text := line[span.start:span.end] offset := gpu_font_text_advance(gpu, line[:span.start]) r, g, b := syntax_color(span.kind) gpu_text(gpu, round_f32(x + offset), y, text, r, g, b, 255) } return next_state } tokenize_kotlin_line :: proc(line: string, initial_state: Syntax_State) -> ([dynamic]Syntax_Span, Syntax_State) { spans: [dynamic]Syntax_Span state := initial_state i := 0 for i < len(line) { start := i ch := line[i] if state.in_block_comment { for i < len(line) { if line[i] == '*' && i + 1 < len(line) && line[i + 1] == '/' { i += 2 state.in_block_comment = false break } i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .Comment}) continue } if state.in_triple_string { for i < len(line) { if starts_with_at(line, i, "\"\"\"") { i += 3 state.in_triple_string = false break } i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .String}) continue } if ch == '/' && i + 1 < len(line) && line[i + 1] == '/' { append(&spans, Syntax_Span{start = i, end = len(line), kind = .Comment}) break } if ch == '/' && i + 1 < len(line) && line[i + 1] == '*' { i += 2 state.in_block_comment = true for i < len(line) { if line[i] == '*' && i + 1 < len(line) && line[i + 1] == '/' { i += 2 state.in_block_comment = false break } i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .Comment}) continue } if starts_with_at(line, i, "\"\"\"") { i += 3 state.in_triple_string = true for i < len(line) { if starts_with_at(line, i, "\"\"\"") { i += 3 state.in_triple_string = false break } i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .String}) continue } if ch == '"' { i += 1 escaped := false for i < len(line) { if line[i] == '"' && !escaped { i += 1 break } escaped = line[i] == '\\' && !escaped if line[i] != '\\' do escaped = false i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .String}) continue } if ch == '\'' { i += 1 escaped := false for i < len(line) { if line[i] == '\'' && !escaped { i += 1 break } escaped = line[i] == '\\' && !escaped if line[i] != '\\' do escaped = false i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .String}) continue } if is_digit_byte(ch) { i += 1 for i < len(line) && (is_digit_byte(line[i]) || line[i] == '.') { i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .Number}) continue } if is_identifier_start_byte(ch) { i += 1 for i < len(line) && is_identifier_part_byte(line[i]) { i += 1 } word := line[start:i] kind := Syntax_Kind.Keyword if is_kotlin_or_java_keyword(word) else Syntax_Kind.Plain if kind == .Plain && is_type_like_identifier(word) { kind = .Type } append(&spans, Syntax_Span{start = start, end = i, kind = kind}) continue } i += 1 for i < len(line) && !is_identifier_start_byte(line[i]) && !is_digit_byte(line[i]) && line[i] != '"' && line[i] != '\'' { if line[i] == '/' && i + 1 < len(line) && line[i + 1] == '/' do break i += 1 } append(&spans, Syntax_Span{start = start, end = i, kind = .Plain}) } return spans, state } kotlin_syntax_state_before_line :: proc(buffer: ^Buffer, line: int) -> Syntax_State { state := Syntax_State{} for index := 0; index < line; index += 1 { bytes := buffer_line_bytes(buffer, index) spans: [dynamic]Syntax_Span spans, state = tokenize_kotlin_line(string(bytes[:]), state) delete(spans) delete(bytes) } return state } starts_with_at :: proc(text: string, at: int, prefix: string) -> bool { if at < 0 || at + len(prefix) > len(text) do return false return text[at:at + len(prefix)] == prefix } syntax_color :: proc(kind: Syntax_Kind) -> (u8, u8, u8) { switch kind { case .Keyword: return 197, 134, 192 case .String: return 206, 145, 120 case .Comment: return 106, 153, 85 case .Number: return 181, 206, 168 case .Type: return 78, 201, 176 case .Plain: return 214, 217, 223 } return 214, 217, 223 } is_kotlin_or_java_keyword :: proc(word: string) -> bool { switch word { case "as", "break", "class", "continue", "do", "else", "false", "for", "fun", "if", "in", "interface", "is", "null", "object", "package", "return", "super", "this", "throw", "true", "try", "typealias", "typeof", "val", "var", "when", "while", "by", "catch", "constructor", "delegate", "dynamic", "field", "file", "finally", "get", "import", "init", "param", "property", "receiver", "set", "setparam", "where", "actual", "abstract", "annotation", "companion", "const", "crossinline", "data", "enum", "expect", "external", "final", "infix", "inline", "inner", "internal", "lateinit", "noinline", "open", "operator", "out", "override", "private", "protected", "public", "reified", "sealed", "suspend", "tailrec", "vararg": return true case "assert", "boolean", "byte", "case", "char", "default", "double", "extends", "float", "goto", "implements", "instanceof", "int", "long", "native", "new", "record", "short", "static", "strictfp", "switch", "synchronized", "throws", "transient", "void", "volatile": return true } return false } is_type_like_identifier :: proc(word: string) -> bool { if len(word) == 0 do return false return word[0] >= 'A' && word[0] <= 'Z' } is_identifier_start_byte :: proc(ch: u8) -> bool { return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' } is_identifier_part_byte :: proc(ch: u8) -> bool { return is_identifier_start_byte(ch) || is_digit_byte(ch) } is_digit_byte :: proc(ch: u8) -> bool { return ch >= '0' && ch <= '9' } render_debug_text_limited :: proc(renderer: ^SDL.Renderer, x, y: f32, text: string, max_chars: int) { if max_chars <= 0 do return if len(text) <= max_chars { text_c := fmt.ctprintf("%s", text) _ = SDL.RenderDebugText(renderer, x, y, text_c) return } if max_chars <= 3 { text_c := fmt.ctprintf("%s", text[:max_chars]) _ = SDL.RenderDebugText(renderer, x, y, text_c) return } text_c := fmt.ctprintf("%s...", text[:max_chars - 3]) _ = SDL.RenderDebugText(renderer, x, y, text_c) } render_rename_panel :: proc(renderer: ^SDL.Renderer, panel: ^Rename_Panel) { if !panel.open do return x: f32 = 620 y: f32 = 420 width: f32 = 460 visible := min_int(max_int(len(panel.edits), 1), 8) height := f32(76 + visible * 16) _ = SDL.SetRenderDrawColor(renderer, 24, 28, 38, 245) rect := SDL.FRect{x, y, width, height} _ = SDL.RenderFillRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 110, 135, 180, 255) _ = SDL.RenderRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255) _ = SDL.RenderDebugText(renderer, x + 10, y + 8, "Rename Preview") input := fmt.ctprintf("New name: %s", string(panel.input[:])) _ = SDL.SetRenderDrawColor(renderer, 255, 210, 120, 255) _ = SDL.RenderDebugText(renderer, x + 10, y + 28, input) summary := fmt.ctprintf("%s", panel.summary) _ = SDL.SetRenderDrawColor(renderer, 220, 225, 235, 255) _ = SDL.RenderDebugText(renderer, x + 10, y + 48, summary) item_y := y + 68 for edit, index in panel.edits { if index >= 8 do break label := fmt.ctprintf("%s", edit.label) _ = SDL.RenderDebugText(renderer, x + 10, item_y, label) item_y += 16 } } render_rename_panel_gpu :: proc(gpu: ^GPU_Renderer, panel: ^Rename_Panel) { if !panel.open do return x: f32 = 620 y: f32 = 420 width: f32 = 460 visible := min_int(max_int(len(panel.edits), 1), 8) height := f32(76 + visible * 16) gpu_rect(gpu, x, y, width, height, 24, 28, 38, 245) gpu_rect_outline(gpu, x, y, width, height, 110, 135, 180, 255) gpu_text(gpu, x + 10, y + 8, "Rename Preview", 145, 165, 205, 255) input := fmt.tprintf("New name: %s", string(panel.input[:])) gpu_text_limited(gpu, x + 10, y + 28, input, 56, 255, 210, 120, 255) gpu_text_limited(gpu, x + 10, y + 48, panel.summary, 56, 220, 225, 235, 255) item_y := y + 68 for edit, index in panel.edits { if index >= 8 do break gpu_text_limited(gpu, x + 10, item_y, edit.label, 56, 220, 225, 235, 255) item_y += 16 } } render_search_panel_gpu :: proc(gpu: ^GPU_Renderer, panel: ^Search_Panel) { if !panel.open do return width: f32 = 440 height: f32 = 72 x := f32(gpu.width) - width - 24 y: f32 = SDL_TOP_BAR_HEIGHT + SDL_TAB_BAR_HEIGHT + 12 gpu_rect(gpu, x, y, width, height, 31, 34, 41, 245) gpu_rect_outline(gpu, x, y, width, height, 77, 155, 230, 255) gpu_text(gpu, x + 12, y + 10, "Find", 145, 190, 255, 255) query := fmt.tprintf("%s", string(panel.input[:])) gpu_rect(gpu, x + 54, y + 8, width - 66, 24, 24, 26, 31, 255) gpu_rect_outline(gpu, x + 54, y + 8, width - 66, 24, 64, 68, 78, 255) gpu_text_limited(gpu, x + 62, y + 15, query, 48, 230, 233, 238, 255) if len(panel.message) > 0 { gpu_text_limited(gpu, x + 12, y + 46, panel.message, 58, 170, 176, 186, 255) } } SDL_TOOLBAR_OPEN_FOLDER_LABEL :: "Open Folder" // Geometry is shared by rendering, click handling, and hover cursor so the // three always agree. toolbar_open_folder_button :: proc() -> (x, y, width, height: f32) { x = 14 + f32(gpu_text_width("Native Kotlin Editor")) + 26 y = 5 width = f32(gpu_text_width(SDL_TOOLBAR_OPEN_FOLDER_LABEL)) + 20 height = SDL_TOP_BAR_HEIGHT - 10 return } toolbar_open_folder_hit :: proc(x, y: f32) -> bool { button_x, button_y, width, height := toolbar_open_folder_button() return x >= button_x && x < button_x + width && y >= button_y && y < button_y + height } handle_toolbar_click :: proc(dialog: ^Folder_Dialog_State, workspace: string, x, y: f32) -> bool { if !toolbar_open_folder_hit(x, y) do return false folder_dialog_show(dialog, workspace) return true } SDL_TOOL_STRIP_GRADLE_LABEL :: "Gradle" tool_strip_gradle_button :: proc(window_width: int) -> (x, y, width, height: f32) { x = f32(window_width - SDL_TOOL_STRIP_WIDTH) y = SDL_TOP_BAR_HEIGHT + 8 width = SDL_TOOL_STRIP_WIDTH height = f32(len(SDL_TOOL_STRIP_GRADLE_LABEL)) * 14 + 16 return } tool_strip_gradle_hit :: proc(window_width: int, x, y: f32) -> bool { button_x, button_y, width, height := tool_strip_gradle_button(window_width) return x >= button_x && x < button_x + width && y >= button_y && y < button_y + height } SDL_TOOL_STRIP_TERMINAL_LABEL :: "Term" // Left tool strip: switches the sidebar between file navigation and the git // staging view. left_tab_label :: proc(tab: Left_Tab) -> string { switch tab { case .Files: return "Files" case .Git: return "Git" } return "" } left_strip_button :: proc(tab: Left_Tab) -> (x, y, width, height: f32) { x = 0 y = SDL_TOP_BAR_HEIGHT + 8 width = SDL_TOOL_STRIP_WIDTH for t in Left_Tab { height = f32(len(left_tab_label(t))) * 14 + 16 if t == tab do break y += height + 8 } return } left_strip_hit :: proc(tab: Left_Tab, x, y: f32) -> bool { button_x, button_y, width, height := left_strip_button(tab) return x >= button_x && x < button_x + width && y >= button_y && y < button_y + height } left_tab_activate :: proc(view: ^SDL_View, tab: Left_Tab) { if view.left_tab == tab { view.explorer_visible = !view.explorer_visible } else { view.left_tab = tab view.explorer_visible = true } } handle_left_strip_click :: proc(view: ^SDL_View, git_status: ^Git_Status_Panel, workspace: string, x, y: f32) -> bool { if x >= f32(SDL_TOOL_STRIP_WIDTH) do return false if y < SDL_TOP_BAR_HEIGHT || y >= f32(view.window_height - SDL_STATUS_BAR_HEIGHT) do return false if left_strip_hit(.Files, x, y) { left_tab_activate(view, .Files) ui_state_save(view) } else if left_strip_hit(.Git, x, y) { left_tab_activate(view, .Git) if git_status_panel_active(view) { git_status_request(git_status, workspace) } ui_state_save(view) } // Clicks on the strip never fall through. return true } // The whole left column: strip background, its buttons, and the active // sidebar panel (file tree or git status). render_left_sidebar_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, tree: ^Project_Tree, git_status: ^Git_Status_Panel, active_path: string) { height := f32(gpu.height - SDL_TOP_BAR_HEIGHT - SDL_STATUS_BAR_HEIGHT) gpu_rect(gpu, 0, SDL_TOP_BAR_HEIGHT, SDL_TOOL_STRIP_WIDTH, height, 37, 38, 43, 255) gpu_rect(gpu, SDL_TOOL_STRIP_WIDTH - 1, SDL_TOP_BAR_HEIGHT, 1, height, 56, 58, 64, 255) tool_strip_button_render(gpu, left_strip_button(.Files), left_tab_label(.Files), view.explorer_visible && view.left_tab == .Files) tool_strip_button_render(gpu, left_strip_button(.Git), left_tab_label(.Git), view.explorer_visible && view.left_tab == .Git) if !view.explorer_visible do return left := f32(content_left_edge()) sidebar_width := f32(left_sidebar_width(view)) gpu_rect(gpu, left, SDL_TOP_BAR_HEIGHT, sidebar_width - left, height, 35, 36, 40, 255) gpu_rect(gpu, left, SDL_TOP_BAR_HEIGHT, sidebar_width - left, 1, 55, 57, 64, 255) gpu_rect(gpu, sidebar_width - 2, SDL_TOP_BAR_HEIGHT, 4, height, 57, 60, 68, 255) if view.left_tab == .Files { render_project_tree_gpu(gpu, tree, view, active_path) } else { render_git_status_panel_gpu(gpu, view, git_status) } } tool_strip_terminal_button :: proc(window_width: int) -> (x, y, width, height: f32) { _, gradle_y, _, gradle_height := tool_strip_gradle_button(window_width) x = f32(window_width - SDL_TOOL_STRIP_WIDTH) y = gradle_y + gradle_height + 8 width = SDL_TOOL_STRIP_WIDTH height = f32(len(SDL_TOOL_STRIP_TERMINAL_LABEL)) * 14 + 16 return } tool_strip_terminal_hit :: proc(window_width: int, x, y: f32) -> bool { button_x, button_y, width, height := tool_strip_terminal_button(window_width) return x >= button_x && x < button_x + width && y >= button_y && y < button_y + height } handle_tool_strip_click :: proc(view: ^SDL_View, tasks_panel: ^Gradle_Tasks_Panel, terminal: ^Terminal_Panel, git: ^Git_Panel, workspace: string, editor: ^Editor, daemon: ^Daemon_Client, sync: ^Daemon_Sync_State, x, y: f32) -> bool { if x < f32(content_right_edge(view.window_width)) do return false if y < SDL_TOP_BAR_HEIGHT || y >= f32(view.window_height - SDL_STATUS_BAR_HEIGHT) do return false if tool_strip_gradle_hit(view.window_width, x, y) { gradle_panel_toggle(view, tasks_panel, editor, daemon, sync) } else if tool_strip_terminal_hit(view.window_width, x, y) { terminal_toggle(terminal, git, view, workspace) } // Clicks on the strip never fall through to the editor behind it. return true } render_tool_strip_gpu :: proc(gpu: ^GPU_Renderer, tasks_panel: ^Gradle_Tasks_Panel, terminal: ^Terminal_Panel) { x := f32(content_right_edge(gpu.width)) y: f32 = SDL_TOP_BAR_HEIGHT height := f32(gpu.height - SDL_TOP_BAR_HEIGHT - SDL_STATUS_BAR_HEIGHT) gpu_rect(gpu, x, y, SDL_TOOL_STRIP_WIDTH, height, 37, 38, 43, 255) gpu_rect(gpu, x, y, 1, height, 56, 58, 64, 255) tool_strip_button_render(gpu, tool_strip_gradle_button(gpu.width), SDL_TOOL_STRIP_GRADLE_LABEL, tasks_panel.open) tool_strip_button_render(gpu, tool_strip_terminal_button(gpu.width), SDL_TOOL_STRIP_TERMINAL_LABEL, terminal.open) } tool_strip_button_render :: proc(gpu: ^GPU_Renderer, button_x, button_y, button_width, button_height: f32, label: string, active: bool) { if active { gpu_rect(gpu, button_x + 2, button_y, button_width - 3, button_height, 58, 64, 82, 255) } char_x := button_x + (button_width - 8) * 0.5 for i in 0 ..< len(label) { gpu_text(gpu, char_x, button_y + 8 + f32(i) * 14, label[i:i + 1], 200, 206, 216, 255) } } render_command_palette_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, palette: ^Command_Palette) { if !palette.open do return width := f32(min_int(max_int(gpu.width - 280, 420), 720)) x := f32(gpu.width) * 0.5 - width * 0.5 y: f32 = SDL_TOP_BAR_HEIGHT + 48 visible := min_int(max_int(command_palette_match_count(palette), 1), 8) height := f32(58 + visible * 24) gpu_rect(gpu, x, y, width, height, 25, 28, 36, 248) gpu_rect_outline(gpu, x, y, width, height, 77, 155, 230, 255) gpu_text(gpu, x + 16, y + 13, "Command Palette", 145, 190, 255, 255) query := fmt.tprintf(">%s", string(palette.input[:])) gpu_rect(gpu, x + 14, y + 34, width - 28, 24, 18, 20, 25, 255) gpu_rect_outline(gpu, x + 14, y + 34, width - 28, 24, 58, 62, 72, 255) gpu_text_limited(gpu, x + 24, y + 41, query, max_int(int((width - 48) / max_f32(gpu.font_advance, 1)), 1), 230, 233, 238, 255) item_y := y + 68 seen := 0 rendered := 0 first_seen := 0 if palette.selected >= 8 { first_seen = palette.selected - 7 } for item in COMMAND_ITEMS { if !command_palette_matches(palette, item.label) do continue if seen < first_seen { seen += 1 continue } if rendered >= 8 do break row_hovered := view.mouse_x >= x + 8 && view.mouse_x < x + width - 8 && view.mouse_y >= item_y - 5 && view.mouse_y < item_y + 17 if seen == palette.selected { gpu_rect(gpu, x + 8, item_y - 5, width - 16, 22, 49, 56, 70, 255) gpu_rect(gpu, x + 8, item_y - 5, 2, 22, 77, 155, 230, 255) } else if row_hovered { gpu_rect(gpu, x + 8, item_y - 5, width - 16, 22, 38, 42, 52, 255) } color := [3]u8{218, 222, 230} if seen != palette.selected { color = {166, 172, 184} } if row_hovered && seen != palette.selected { color = {200, 205, 215} } gpu_text_limited(gpu, x + 20, item_y, item.label, max_int(int((width - 40) / max_f32(gpu.font_advance, 1)), 1), color[0], color[1], color[2], 255) item_y += 24 seen += 1 rendered += 1 } if rendered == 0 { gpu_text(gpu, x + 20, item_y, "No matching commands", 150, 154, 162, 255) } } render_diagnostics_panel_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL_View, panel: ^Diagnostics_Panel) { if !panel.open do return active := editor_active_buffer(editor) if active == nil do return height: f32 = 168 sidebar_width := left_sidebar_width(view) x := f32(sidebar_width) y := f32(editor_content_bottom(view)) - height width := f32(gpu.width - sidebar_width) gpu_rect(gpu, x, y, width, height, 30, 32, 38, 245) gpu_rect(gpu, x, y, width, 1, 64, 68, 78, 255) gpu_text(gpu, x + 14, y + 10, "Diagnostics", 220, 223, 228, 255) errors, warnings, infos := editor_diagnostic_counts(active) count_label := fmt.tprintf("%dE %dW %dI", errors, warnings, infos) gpu_text(gpu, x + width - 120, y + 10, count_label, 150, 154, 162, 255) if len(active.diagnostics) == 0 { gpu_text(gpu, x + 14, y + 42, "No diagnostics", 150, 154, 162, 255) return } panel.selected = clamp_int(panel.selected, 0, len(active.diagnostics) - 1) visible_items := 7 first_index := 0 if panel.selected >= visible_items { first_index = panel.selected - visible_items + 1 } item_y := y + 38 for index := first_index; index < len(active.diagnostics) && index < first_index + visible_items; index += 1 { diagnostic := active.diagnostics[index] row_hovered := view.mouse_x >= x + 8 && view.mouse_x < x + width - 8 && view.mouse_y >= item_y - 4 && view.mouse_y < item_y + 14 if index == panel.selected { gpu_rect(gpu, x + 8, item_y - 4, width - 16, 20, 49, 56, 70, 255) gpu_rect(gpu, x + 8, item_y - 4, 2, 20, 77, 155, 230, 255) } else if row_hovered { gpu_rect(gpu, x + 8, item_y - 4, width - 16, 20, 40, 43, 50, 255) } label := fmt.tprintf("%s %d:%d %s", diagnostic.severity, diagnostic.line + 1, diagnostic.column + 1, diagnostic.message) color := [3]u8{218, 222, 230} if diagnostic.severity == "error" { color = {244, 113, 116} } gpu_text_limited(gpu, x + 18, item_y, label, max_int((gpu.width - sidebar_width - 40) / int(max_f32(gpu.font_advance, 1)), 1), color[0], color[1], color[2], 255) item_y += 18 } } render_gradle_tasks_panel_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, panel: ^Gradle_Tasks_Panel) { if !panel.open do return x, y, width, height := gradle_panel_geometry(view, gpu.width, gpu.height) gpu_rect(gpu, x, y, width, height, 35, 36, 40, 255) gpu_rect(gpu, x, y, 1, height, 56, 58, 64, 255) gpu_text(gpu, x + 14, y + 12, "GRADLE", 150, 154, 162, 255) task_count := fmt.tprintf("%d", len(panel.items)) gpu_text_limited(gpu, x + width - 44, y + 12, task_count, 5, 150, 154, 162, 255) if len(panel.message) > 0 { gpu_text_limited(gpu, x + 14, y + 32, panel.message, int((width - 28) / 8), 150, 154, 162, 255) } if len(panel.items) == 0 { gpu_text_limited(gpu, x + 14, y + SDL_GRADLE_LIST_TOP, "No Gradle tasks loaded", 34, 150, 154, 162, 255) } output_height := gradle_panel_output_height(panel, height) rows := gradle_panel_rows(panel) visible := gradle_panel_visible_rows(panel, height) panel.scroll = clamp_int(panel.scroll, 0, max_int(len(rows) - visible, 0)) max_columns := max_int(int((width - 44) / max_f32(gpu.font_advance, 1)), 1) row_y := y + SDL_GRADLE_LIST_TOP for offset in 0 ..< visible { index := panel.scroll + offset if index >= len(rows) do break row := rows[index] row_hovered := view.mouse_x >= x + 4 && view.mouse_x < x + width - 4 && view.mouse_y >= row_y && view.mouse_y < row_y + SDL_GRADLE_ROW_HEIGHT if row.kind == .Task && row.item_index == panel.selected { gpu_rect(gpu, x + 4, row_y, width - 8, SDL_GRADLE_ROW_HEIGHT, 49, 56, 70, 255) gpu_rect(gpu, x + 4, row_y, 2, SDL_GRADLE_ROW_HEIGHT, 77, 155, 230, 255) } else if row_hovered { gpu_rect(gpu, x + 4, row_y, width - 8, SDL_GRADLE_ROW_HEIGHT, 42, 44, 50, 255) } text_y := row_y + 2 switch row.kind { case .Project: arrow := ">" if panel.collapsed[row.key] else "v" gpu_text(gpu, x + 12, text_y, arrow, 140, 146, 158, 255) gpu_text_limited(gpu, x + 24, text_y, row.label, max_columns, 200, 205, 215, 255) case .Group: arrow := ">" if panel.collapsed[row.key] else "v" gpu_text(gpu, x + 26, text_y, arrow, 140, 146, 158, 255) gpu_text_limited(gpu, x + 38, text_y, row.label, max_columns, 170, 176, 188, 255) case .Task: gpu_text_limited(gpu, x + 52, text_y, row.label, max_columns, 218, 222, 230, 255) } row_y += SDL_GRADLE_ROW_HEIGHT } if len(panel.output) > 0 { output_y := y + height - output_height gpu_rect(gpu, x, output_y, width, output_height, 28, 29, 34, 255) gpu_rect(gpu, x, output_y, width, 1, 58, 61, 68, 255) gpu_text(gpu, x + 14, output_y + 10, "Output", 150, 154, 162, 255) line_y := output_y + 34 max_lines := max_int(int((output_height - 42) / 16), 1) first := max_int(len(panel.output) - max_lines, 0) max_columns := max_int(int((width - 28) / max_f32(gpu.font_advance, 1)), 1) for index := first; index < len(panel.output); index += 1 { line := panel.output[index] color := [3]u8{176, 182, 192} if strings.has_prefix(line, "stderr:") { color = {244, 113, 116} } gpu_text_limited(gpu, x + 14, line_y, line, max_columns, color[0], color[1], color[2], 255) line_y += 16 } } } render_references_panel :: proc(renderer: ^SDL.Renderer, panel: ^References_Panel) { if !panel.open do return x: f32 = 700 y: f32 = 72 width: f32 = 380 visible := min_int(max_int(len(panel.items), 1), 14) height := f32(30 + visible * 16) _ = SDL.SetRenderDrawColor(renderer, 24, 28, 38, 245) rect := SDL.FRect{x, y, width, height} _ = SDL.RenderFillRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 95, 120, 165, 255) _ = SDL.RenderRect(renderer, &rect) title := fmt.ctprintf("References (%d)", len(panel.items) if panel.pending_id == 0 else 0) _ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255) _ = SDL.RenderDebugText(renderer, x + 10, y + 8, title) item_y := y + 28 first_index := 0 if panel.selected >= 14 { first_index = panel.selected - 13 } for index := first_index; index < len(panel.items) && index < first_index + 14; index += 1 { item := panel.items[index] if index == panel.selected { _ = SDL.SetRenderDrawColor(renderer, 52, 62, 86, 255) row := SDL.FRect{x + 4, item_y - 2, width - 8, 16} _ = SDL.RenderFillRect(renderer, &row) _ = SDL.SetRenderDrawColor(renderer, 255, 210, 120, 255) } else { _ = SDL.SetRenderDrawColor(renderer, 220, 225, 235, 255) } label := fmt.ctprintf("%s", item.label) _ = SDL.RenderDebugText(renderer, x + 10, item_y, label) item_y += 16 } } render_references_panel_gpu :: proc(gpu: ^GPU_Renderer, panel: ^References_Panel) { if !panel.open do return x: f32 = 700 y: f32 = 72 width: f32 = 380 visible := min_int(max_int(len(panel.items), 1), 14) height := f32(30 + visible * 16) gpu_rect(gpu, x, y, width, height, 24, 28, 38, 245) gpu_rect_outline(gpu, x, y, width, height, 95, 120, 165, 255) title := fmt.tprintf("References (%d)", len(panel.items) if panel.pending_id == 0 else 0) gpu_text(gpu, x + 10, y + 8, title, 145, 165, 205, 255) item_y := y + 28 first_index := 0 if panel.selected >= 14 { first_index = panel.selected - 13 } for index := first_index; index < len(panel.items) && index < first_index + 14; index += 1 { item := panel.items[index] if index == panel.selected { gpu_rect(gpu, x + 4, item_y - 2, width - 8, 16, 52, 62, 86, 255) gpu_text_limited(gpu, x + 10, item_y, item.label, 46, 255, 210, 120, 255) } else { gpu_text_limited(gpu, x + 10, item_y, item.label, 46, 220, 225, 235, 255) } item_y += 16 } } render_hover_tooltip :: proc(renderer: ^SDL.Renderer, editor: ^Editor, view: ^SDL_View, hover: ^Hover_Tooltip) { if !hover.open do return if len(hover.contents) == 0 do return active := editor_active_buffer(editor) if active == nil do return cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) x := f32(editor_text_x(view) + cursor_col * SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE) y := f32(SDL_EDITOR_TEXT_Y + (cursor_line - view.first_line + 2) * SDL_LINE_HEIGHT) if y > 690 do y = 690 width := f32(max_int(180, min_int(520, len(hover.contents) * SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE + 20))) height: f32 = 34 _ = SDL.SetRenderDrawColor(renderer, 30, 34, 45, 245) rect := SDL.FRect{x, y, width, height} _ = SDL.RenderFillRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 100, 125, 165, 255) _ = SDL.RenderRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 220, 225, 235, 255) first_line := hover.contents if newline := strings.index_byte(first_line, '\n'); newline >= 0 { first_line = first_line[:newline] } text := fmt.ctprintf("%s", first_line) _ = SDL.RenderDebugText(renderer, x + 10, y + 10, text) } render_hover_tooltip_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL_View, hover: ^Hover_Tooltip) { if !hover.open || len(hover.contents) == 0 do return active := editor_active_buffer(editor) if active == nil do return // Hover contents can span several lines (signatures, docs), so the box // is sized to the visible lines and each line is drawn separately — // gpu_text would otherwise continue multi-line text below the box. lines := hover_visible_lines(hover) if len(lines) == 0 do return max_line_width := 0 for line in lines { max_line_width = max_int(max_line_width, gpu_font_text_width(gpu, line)) } width := f32(max_int(180, min_int(560, max_line_width + 20))) action_height := 26 if hover.has_definition else 0 height := f32(18 + len(lines) * 16 + action_height) anchor_line := min_int(hover.line, buffer_line_count(&active.buffer) - 1) anchor_col := hover.word_start if hover.from_mouse else hover.column x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, anchor_line, anchor_col) y := f32(SDL_EDITOR_TEXT_Y + (anchor_line - view.first_line + 2) * SDL_LINE_HEIGHT) x = max_f32(min_f32(x, f32(content_right_edge(gpu.width)) - width - 4), 0) y = max_f32(min_f32(y, f32(editor_content_bottom(view)) - height - 4), SDL_EDITOR_TOP) hover.x = x hover.y = y hover.width = width hover.height = height gpu_rect(gpu, x, y, width, height, 30, 34, 45, 255) gpu_rect_outline(gpu, x, y, width, height, 100, 125, 165, 255) max_columns := max_int(int((width - 20) / max_f32(gpu.font_advance, 1)), 1) line_y := y + 10 selection_start := min_int(hover.selection_anchor, hover.selection_cursor) selection_end := max_int(hover.selection_anchor, hover.selection_cursor) content_offset := 0 syntax_state := Syntax_State{} for line in lines { visible_len := min_int(len(line), max_columns) if hover.selection_active && selection_start != selection_end { line_start := content_offset line_end := content_offset + visible_len if selection_end > line_start && selection_start < line_end { start_col := clamp_int(selection_start - line_start, 0, visible_len) end_col := clamp_int(selection_end - line_start, start_col, visible_len) if end_col > start_col { gpu_rect(gpu, x + 10 + f32(start_col) * gpu.font_advance, line_y - 2, f32(end_col - start_col) * gpu.font_advance, 16, 62, 83, 125, 190) } } } syntax_state = render_syntax_line_gpu(gpu, x + 10, line_y, line, max_columns, syntax_state) content_offset += len(line) + 1 line_y += 16 } if hover.has_definition { action_y := y + height - 25 gpu_rect(gpu, x + 8, action_y, 134, 18, 43, 54, 72, 255) gpu_rect_outline(gpu, x + 8, action_y, 134, 18, 77, 155, 230, 255) gpu_text(gpu, x + 16, action_y + 4, "Go to definition", 160, 205, 255, 255) } } render_completion_popup :: proc(renderer: ^SDL.Renderer, editor: ^Editor, view: ^SDL_View, popup: ^Completion_Popup) { if !popup.open do return active := editor_active_buffer(editor) if active == nil do return cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) x := f32(editor_text_x(view) + cursor_col * SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE) y := f32(SDL_EDITOR_TEXT_Y + (cursor_line - view.first_line + 1) * SDL_LINE_HEIGHT) if y > 650 do y = 650 width: f32 = 260 height := f32(26 + min_int(len(popup.items), 8) * 16) _ = SDL.SetRenderDrawColor(renderer, 28, 32, 43, 245) rect := SDL.FRect{x, y, width, height} _ = SDL.RenderFillRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 90, 110, 150, 255) _ = SDL.RenderRect(renderer, &rect) _ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255) _ = SDL.RenderDebugText(renderer, x + 8, y + 6, "Completions") item_y := y + 22 first_index := 0 if popup.selected >= 8 { first_index = popup.selected - 7 } for index := first_index; index < len(popup.items) && index < first_index + 8; index += 1 { item := popup.items[index] if index == popup.selected { _ = SDL.SetRenderDrawColor(renderer, 52, 62, 86, 255) row := SDL.FRect{x + 4, item_y - 2, width - 8, 16} _ = SDL.RenderFillRect(renderer, &row) _ = SDL.SetRenderDrawColor(renderer, 255, 210, 120, 255) } else { _ = SDL.SetRenderDrawColor(renderer, 220, 225, 235, 255) } item_c := fmt.ctprintf("%s", item.label) _ = SDL.RenderDebugText(renderer, x + 10, item_y, item_c) if len(item.kind) > 0 && item.kind != "status" { kind_c := fmt.ctprintf("%s", item.kind) _ = SDL.RenderDebugText(renderer, x + width - 78, item_y, kind_c) } item_y += 16 } } render_completion_popup_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL_View, popup: ^Completion_Popup) { if !popup.open do return active := editor_active_buffer(editor) if active == nil do return cursor_line, cursor_col := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, cursor_line, cursor_col) y := f32(SDL_EDITOR_TEXT_Y + (cursor_line - view.first_line + 1) * SDL_LINE_HEIGHT) if y > f32(gpu.height - 110) do y = f32(gpu.height - 110) width: f32 = 260 height := f32(26 + min_int(len(popup.items), 8) * 16) gpu_rect(gpu, x, y, width, height, 28, 32, 43, 245) gpu_rect_outline(gpu, x, y, width, height, 90, 110, 150, 255) gpu_text(gpu, x + 8, y + 6, "Completions", 145, 165, 205, 255) item_y := y + 22 first_index := 0 if popup.selected >= 8 { first_index = popup.selected - 7 } for index := first_index; index < len(popup.items) && index < first_index + 8; index += 1 { item := popup.items[index] if index == popup.selected { gpu_rect(gpu, x + 4, item_y - 2, width - 8, 16, 52, 62, 86, 255) gpu_text_limited(gpu, x + 10, item_y, item.label, 24, 255, 210, 120, 255) } else { gpu_text_limited(gpu, x + 10, item_y, item.label, 24, 220, 225, 235, 255) } if len(item.kind) > 0 && item.kind != "status" { gpu_text_limited(gpu, x + width - 78, item_y, item.kind, 10, 140, 146, 158, 255) } item_y += 16 } } render_selection_for_line :: proc(renderer: ^SDL.Renderer, view: ^SDL_View, active: ^Editor_Buffer, line_index: int, y: f32) { selection_start, selection_end, ok := editor_selection_range(active) if !ok do return line_start := buffer_line_start(&active.buffer, line_index) line_end := buffer_line_end(&active.buffer, line_index) if selection_end < line_start || selection_start > line_end do return start := max_int(selection_start, line_start) end := min_int(selection_end, line_end) if start == end { if selection_end != line_start do return end = min_int(line_end, start + 1) } start_col := start - line_start end_col := max_int(end - line_start, start_col + 1) x := f32(editor_text_x(view) + start_col * SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE) width := f32((end_col - start_col) * SDL.DEBUG_TEXT_FONT_CHARACTER_SIZE) rect := SDL.FRect{x, y - 2, width, 16} _ = SDL.SetRenderDrawColor(renderer, 62, 83, 125, 180) _ = SDL.RenderFillRect(renderer, &rect) } render_selection_for_line_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, active: ^Editor_Buffer, line_index: int, y: f32) { selection_start, selection_end, ok := editor_selection_range(active) if !ok do return line_start := buffer_line_start(&active.buffer, line_index) line_end := buffer_line_end(&active.buffer, line_index) if selection_end < line_start || selection_start > line_end do return start := max_int(selection_start, line_start) end := min_int(selection_end, line_end) if start == end { if selection_end != line_start do return end = min_int(line_end, start + 1) } start_col := start - line_start end_col := max_int(end - line_start, start_col + 1) x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, line_index, start_col) width := round_f32(line_prefix_pixel_advance_gpu(gpu, &active.buffer, line_index, end_col) - line_prefix_pixel_advance_gpu(gpu, &active.buffer, line_index, start_col)) gpu_rect(gpu, x, y - 3, width, SDL_LINE_HEIGHT + 1, 58, 91, 140, 170) } render_search_matches_for_line_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, active: ^Editor_Buffer, panel: ^Search_Panel, line_index: int, line: string, y: f32) { if !panel.open || len(panel.input) == 0 || len(line) == 0 do return query := panel.input[:] if len(query) > len(line) do return at := 0 for at <= len(line) - len(query) { match := true for i := 0; i < len(query); i += 1 { if line[at + i] != query[i] { match = false break } } if match { x := editor_column_pixel_x_gpu(gpu, view, &active.buffer, line_index, at) width := round_f32(line_prefix_pixel_advance_gpu(gpu, &active.buffer, line_index, at + len(query)) - line_prefix_pixel_advance_gpu(gpu, &active.buffer, line_index, at)) gpu_rect(gpu, x, y - 3, width, SDL_LINE_HEIGHT + 1, 94, 75, 31, 190) gpu_rect_outline(gpu, x, y - 3, width, SDL_LINE_HEIGHT + 1, 181, 137, 45, 220) at += max_int(len(query), 1) } else { at += 1 } } } render_diagnostic_underline_gpu :: proc(gpu: ^GPU_Renderer, view: ^SDL_View, buffer: ^Buffer, line_index, column: int, y: f32) { start_col := max_int(column, 0) end_col := start_col + 6 line_end_col := buffer_line_end(buffer, line_index) - buffer_line_start(buffer, line_index) end_col = clamp_int(end_col, start_col + 1, max_int(line_end_col, start_col + 1)) x0 := editor_column_pixel_x_gpu(gpu, view, buffer, line_index, start_col) x1 := editor_column_pixel_x_gpu(gpu, view, buffer, line_index, end_col) underline_y := y + SDL_LINE_HEIGHT - 2 segment: f32 = 3 x := x0 for x < x1 { gpu_rect(gpu, x, underline_y, min_f32(segment, x1 - x), 1, 244, 113, 116, 255) x += segment * 2 } } render_project_tree :: proc(renderer: ^SDL.Renderer, tree: ^Project_Tree, view: ^SDL_View, active_path: string) { if !view.explorer_visible do return sidebar_width := left_sidebar_width(view) _ = SDL.SetRenderDrawColor(renderer, 20, 23, 31, 255) sidebar := SDL.FRect{0, 0, f32(sidebar_width), f32(view.window_height)} _ = SDL.RenderFillRect(renderer, &sidebar) _ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255) title := "Project (truncated)" if tree.truncated else "Project" _ = SDL.RenderDebugText(renderer, 12, 8, fmt.ctprintf("%s", title)) y: f32 = 28 end := min_int(view.tree_first + visible_tree_rows(view), len(tree.files)) for index := view.tree_first; index < end; index += 1 { item := tree.files[index] if item.path == active_path { _ = SDL.SetRenderDrawColor(renderer, 37, 45, 63, 255) row := SDL.FRect{0, y - 2, f32(sidebar_width), 16} _ = SDL.RenderFillRect(renderer, &row) } marker := " " if item.is_dir { marker = "v " if item.expanded else "> " } label := fmt.ctprintf("%s%s", marker, item.name) if item.is_dir { _ = SDL.SetRenderDrawColor(renderer, 145, 165, 205, 255) } else if item.path == active_path { _ = SDL.SetRenderDrawColor(renderer, 255, 210, 120, 255) } else { _ = SDL.SetRenderDrawColor(renderer, 190, 198, 215, 255) } _ = SDL.RenderDebugText(renderer, 12 + f32(item.depth * 12), y, label) y += 16 } } render_project_tree_gpu :: proc(gpu: ^GPU_Renderer, tree: ^Project_Tree, view: ^SDL_View, active_path: string) { if !view.explorer_visible || view.left_tab != .Files do return left := f32(content_left_edge()) sidebar_width := left_sidebar_width(view) title := "EXPLORER (TRUNCATED)" if tree.truncated else "EXPLORER" gpu_text(gpu, left + 14, SDL_TREE_HEADER_Y, title, 150, 154, 162, 255) gpu_rect(gpu, left + 14, SDL_TREE_HEADER_Y + 18, f32(sidebar_width) - left - 28, 1, 52, 54, 60, 255) hovered_index, has_hover := project_tree_row_at(tree, view, view.mouse_x, view.mouse_y) y: f32 = SDL_TREE_FIRST_Y end := min_int(view.tree_first + visible_tree_rows(view), len(tree.files)) for index := view.tree_first; index < end; index += 1 { item := tree.files[index] is_active := item.path == active_path if is_active { gpu_rect(gpu, left + 8, y - 3, f32(sidebar_width) - left - 16, SDL_TREE_ROW_HEIGHT + 1, 49, 51, 58, 255) gpu_rect(gpu, left + 8, y - 3, 2, SDL_TREE_ROW_HEIGHT + 1, 77, 155, 230, 255) } else if has_hover && index == hovered_index { gpu_rect(gpu, left + 8, y - 3, f32(sidebar_width) - left - 16, SDL_TREE_ROW_HEIGHT + 1, 42, 44, 50, 255) } indent := left + f32(16 + item.depth * 12) text_budget := f32(sidebar_width) - indent - 24 max_columns := max_int(int(text_budget / max_f32(gpu.font_advance, 1)), 1) if item.is_dir { arrow := "v" if item.expanded else ">" gpu_text(gpu, indent, y, arrow, 140, 146, 158, 255) gpu_text_limited(gpu, indent + gpu.font_advance * 2, y, item.name, max_int(max_columns - 2, 1), 200, 205, 214, 255) } else if is_active { gpu_text_limited(gpu, indent + gpu.font_advance * 2, y, item.name, max_int(max_columns - 2, 1), 235, 237, 241, 255) } else { gpu_text_limited(gpu, indent + gpu.font_advance * 2, y, item.name, max_int(max_columns - 2, 1), 185, 189, 198, 255) } y += SDL_TREE_ROW_HEIGHT } // Scrollbar when the tree overflows the sidebar. total := len(tree.files) visible := visible_tree_rows(view) if total > visible { track_top := f32(SDL_TREE_FIRST_Y - 4) track_height := f32(visible * SDL_TREE_ROW_HEIGHT) track_x := f32(sidebar_width - 8) gpu_rect(gpu, track_x, track_top, 4, track_height, 40, 42, 48, 255) thumb_height := max_f32(track_height * f32(visible) / f32(total), 24) max_first := max_int(total - visible, 1) thumb_y := track_top + (track_height - thumb_height) * f32(view.tree_first) / f32(max_first) gpu_rect(gpu, track_x, thumb_y, 4, thumb_height, 92, 96, 106, 255) } } render_editor_tabs_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL_View) { tab_x := f32(left_sidebar_width(view) + 12) max_x := f32(gpu.width - 12) mouse_in_bar := view.mouse_y >= SDL_TOP_BAR_HEIGHT && view.mouse_y < SDL_EDITOR_TOP for &buffer, index in editor.buffers { if tab_x >= max_x do break _, file_name := os.split_path(buffer.path) width := f32(tab_width_for_label(file_name)) if tab_x + width > max_x { width = max_x - tab_x } active := index == editor.active hovered := mouse_in_bar && view.mouse_x >= tab_x && view.mouse_x < tab_x + width close_hovered := hovered && view.mouse_x >= tab_x + width - 28 if active { gpu_rect(gpu, tab_x, SDL_TOP_BAR_HEIGHT + 5, width, SDL_TAB_BAR_HEIGHT - 5, 27, 28, 32, 255) gpu_rect(gpu, tab_x, SDL_TOP_BAR_HEIGHT + SDL_TAB_BAR_HEIGHT - 2, width, 2, 77, 155, 230, 255) } else if hovered { gpu_rect(gpu, tab_x, SDL_TOP_BAR_HEIGHT + 6, width, SDL_TAB_BAR_HEIGHT - 6, 48, 50, 56, 255) } else { gpu_rect(gpu, tab_x, SDL_TOP_BAR_HEIGHT + 6, width, SDL_TAB_BAR_HEIGHT - 6, 41, 43, 48, 255) } dirty_marker := " *" if buffer.dirty else "" tab_title := fmt.tprintf("%s%s", file_name, dirty_marker) title_color := [3]u8{220, 223, 228} if active else [3]u8{168, 172, 182} if hovered && !active { title_color = {198, 202, 211} } gpu_text_limited(gpu, tab_x + 16, SDL_TOP_BAR_HEIGHT + 12, tab_title, 34, title_color[0], title_color[1], title_color[2], 255) if close_hovered { gpu_rect(gpu, tab_x + width - 27, SDL_TOP_BAR_HEIGHT + 8, 15, SDL_TAB_BAR_HEIGHT - 14, 62, 65, 73, 255) gpu_text(gpu, tab_x + width - 22, SDL_TOP_BAR_HEIGHT + 12, "x", 235, 237, 241, 255) } else { close_color := [3]u8{168, 172, 182} if active else [3]u8{124, 128, 138} gpu_text(gpu, tab_x + width - 22, SDL_TOP_BAR_HEIGHT + 12, "x", close_color[0], close_color[1], close_color[2], 255) } gpu_rect(gpu, tab_x + width, SDL_TOP_BAR_HEIGHT + 8, 1, SDL_TAB_BAR_HEIGHT - 12, 56, 58, 64, 255) tab_x += width + 4 } } render_metrics_overlay_gpu :: proc(gpu: ^GPU_Renderer, editor: ^Editor, view: ^SDL_View) { if !view.show_metrics do return active := editor_active_buffer(editor) if active == nil do return line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) prefix_width := line_prefix_pixel_advance_gpu(gpu, &active.buffer, line, column) text := fmt.tprintf("metrics: advance=%.2f line=%d col=%d prefix=%.2f x=%.2f", gpu.font_advance, line + 1, column + 1, prefix_width, editor_column_pixel_x_gpu(gpu, view, &active.buffer, line, column)) width := f32(max_int(gpu_font_text_width(gpu, text) + 20, 420)) x := f32(left_sidebar_width(view) + 16) y := f32(SDL_EDITOR_TOP + 10) gpu_rect(gpu, x, y, width, 34, 22, 24, 29, 235) gpu_rect_outline(gpu, x, y, width, 34, 77, 155, 230, 255) gpu_text(gpu, x + 10, y + 10, text, 220, 230, 245, 255) } render_top_bar_gpu :: proc(gpu: ^GPU_Renderer, active: ^Editor_Buffer) { gpu_rect(gpu, 0, 0, f32(gpu.width), SDL_TOP_BAR_HEIGHT, 43, 45, 50, 255) gpu_rect(gpu, 0, SDL_TOP_BAR_HEIGHT - 1, f32(gpu.width), 1, 56, 58, 64, 255) gpu_text(gpu, 14, 10, "Native Kotlin Editor", 220, 223, 228, 255) button_x, button_y, button_width, button_height := toolbar_open_folder_button() gpu_rect(gpu, button_x, button_y, button_width, button_height, 56, 58, 66, 255) gpu_rect_outline(gpu, button_x, button_y, button_width, button_height, 72, 76, 86, 255) gpu_text(gpu, button_x + 10, button_y + 5, SDL_TOOLBAR_OPEN_FOLDER_LABEL, 200, 206, 216, 255) line, column := buffer_offset_to_line_col(&active.buffer, active.cursor.offset) right := fmt.tprintf("Ln %d, Col %d", line + 1, column + 1) gpu_text_limited(gpu, f32(gpu.width - 150), 10, right, 20, 172, 176, 186, 255) } render_status_bar_gpu :: proc(gpu: ^GPU_Renderer, message: string, diagnostic: bool) { y := f32(gpu.height - SDL_STATUS_BAR_HEIGHT) if diagnostic { gpu_rect(gpu, 0, y, f32(gpu.width), SDL_STATUS_BAR_HEIGHT, 122, 50, 54, 255) gpu_text_limited(gpu, 14, y + 7, message, 120, 255, 235, 235, 255) } else { gpu_rect(gpu, 0, y, f32(gpu.width), SDL_STATUS_BAR_HEIGHT, 34, 92, 143, 255) gpu_text_limited(gpu, 14, y + 7, message, 120, 232, 242, 255, 255) } gpu_text(gpu, f32(gpu.width - 156), y + 7, "Odin + SDL3 GPU", 218, 232, 246, 255) } gpu_rect_outline :: proc(gpu: ^GPU_Renderer, x, y, w, h: f32, r, g, b, a: u8) { gpu_rect(gpu, x, y, w, 1, r, g, b, a) gpu_rect(gpu, x, y + h - 1, w, 1, r, g, b, a) gpu_rect(gpu, x, y, 1, h, r, g, b, a) gpu_rect(gpu, x + w - 1, y, 1, h, r, g, b, a) }