editor/client/odin/sdl_app.odin
2026-07-09 08:27:13 +02:00

5062 lines
198 KiB
Odin

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)
}