You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
910 lines
25 KiB
Plaintext
910 lines
25 KiB
Plaintext
package main
|
|
|
|
import "core:fmt"
|
|
import "core:log"
|
|
import "core:math"
|
|
import "core:os"
|
|
import "core:slice"
|
|
import "core:strings"
|
|
import "core:thread"
|
|
import "core:time"
|
|
|
|
import rl "vendor:raylib"
|
|
|
|
import "file_dialog"
|
|
|
|
main :: proc() {
|
|
context.logger = log.create_console_logger(.Debug)
|
|
defer log.destroy_console_logger(context.logger)
|
|
|
|
args := os.args[1:]
|
|
|
|
config, ok := parse_config("config.ini")
|
|
if !ok {
|
|
log.fatal("failed to parse config file")
|
|
return
|
|
}
|
|
|
|
// Initialize application
|
|
app := App{
|
|
files = make([dynamic]File_Buffer),
|
|
fonts = make([dynamic]FontConfig),
|
|
active_tab = 0,
|
|
current_font_index = 0,
|
|
line_height = 24,
|
|
tab_height = 40,
|
|
window_width = 1200,
|
|
window_height = 800,
|
|
line_number_width = 30,
|
|
mouse_drag_start = -1,
|
|
is_dragging = false,
|
|
copied_text = "",
|
|
config = config
|
|
}
|
|
app.text_area_y = app.tab_height + 10
|
|
|
|
// Initialize Raylib
|
|
rl.InitWindow(app.window_width, app.window_height, "notepad_squared")
|
|
rl.SetWindowState(rl.ConfigFlags{.WINDOW_RESIZABLE})
|
|
rl.SetWindowMinSize(DEFAULT_WINDOW_MINIMUM_WINDOW_WIDTH, DEFAULT_WINDOW_MINIMUM_WINDOW_HEIGHT)
|
|
rl.SetTargetFPS(60)
|
|
|
|
// Load fonts
|
|
load_fonts(&app)
|
|
|
|
// Load files from command line arguments
|
|
if len(args) > 1 {
|
|
for filename in args {
|
|
create_file_buffer(&app, filename)
|
|
}
|
|
} else {
|
|
create_file_buffer(&app)
|
|
}
|
|
|
|
if len(app.files) == 0 {
|
|
log.error("no files could be loaded")
|
|
rl.CloseWindow()
|
|
return
|
|
}
|
|
|
|
defer {
|
|
rl.CloseWindow()
|
|
delete(app.files)
|
|
delete(app.fonts)
|
|
}
|
|
|
|
// Main loop
|
|
for !rl.WindowShouldClose() {
|
|
rl.BeginDrawing()
|
|
rl.ClearBackground(app.config.colours.background)
|
|
|
|
for file in app.files {
|
|
if app.thread_finished && thread.is_done(app.thread) {
|
|
thread.destroy(app.thread)
|
|
}
|
|
}
|
|
|
|
// Handle window resize
|
|
if rl.IsWindowResized() {
|
|
app.window_width = rl.GetScreenWidth()
|
|
app.window_height = rl.GetScreenHeight()
|
|
}
|
|
|
|
// Handle tab switching with mouse
|
|
if rl.IsMouseButtonPressed(.LEFT) {
|
|
mouse_pos := rl.GetMousePosition()
|
|
if mouse_pos.y <= app.tab_height {
|
|
tab_width := f32(app.window_width) / f32(len(app.files))
|
|
clicked_tab := int(mouse_pos.x / tab_width)
|
|
if clicked_tab >= 0 && clicked_tab < len(app.files) {
|
|
app.active_tab = clicked_tab
|
|
}
|
|
}
|
|
}
|
|
|
|
if !app.file_dialog_open {
|
|
// Handle text selection with mouse
|
|
if len(app.files) > 0 && app.active_tab < len(app.files) {
|
|
current_file := &app.files[app.active_tab]
|
|
mouse_pos := rl.GetMousePosition()
|
|
|
|
if rl.IsMouseButtonPressed(.LEFT) {
|
|
if mouse_pos.y > app.text_area_y {
|
|
character_pos := get_character_at_position(&app, mouse_pos)
|
|
current_file.cursor_pos = character_pos
|
|
current_file.selection.start = character_pos
|
|
current_file.selection.end = character_pos
|
|
current_file.selection.active = false
|
|
app.mouse_drag_start = character_pos
|
|
app.is_dragging = true
|
|
}
|
|
}
|
|
|
|
if app.is_dragging && rl.IsMouseButtonDown(.LEFT) {
|
|
if mouse_pos.y > app.text_area_y {
|
|
character_pos := get_character_at_position(&app, mouse_pos)
|
|
current_file.cursor_pos = character_pos
|
|
if character_pos != app.mouse_drag_start {
|
|
current_file.selection.start = min(app.mouse_drag_start, character_pos)
|
|
current_file.selection.end = max(app.mouse_drag_start, character_pos)
|
|
current_file.selection.active = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if rl.IsMouseButtonReleased(.LEFT) {
|
|
app.is_dragging = false
|
|
}
|
|
}
|
|
|
|
// Handle keyboard input for active file
|
|
if len(app.files) > 0 && app.active_tab < len(app.files) {
|
|
current_file := &app.files[app.active_tab]
|
|
|
|
// Handle text input
|
|
key := rl.GetCharPressed()
|
|
for key != 0 {
|
|
if key >= 32 && key <= 126 { // Printable ASCII
|
|
if current_file.selection.active {
|
|
delete_selection(current_file)
|
|
}
|
|
insert_character(current_file, rune(key))
|
|
}
|
|
key = rl.GetCharPressed()
|
|
}
|
|
|
|
// Handle special keys
|
|
if rl.IsKeyDown(.ENTER) {
|
|
if current_file.selection.active {
|
|
delete_selection(current_file)
|
|
}
|
|
insert_character(current_file, '\n')
|
|
time.sleep(DEFAULT_DELAY)
|
|
}
|
|
if rl.IsKeyDown(.BACKSPACE) {
|
|
if current_file.selection.active {
|
|
delete_selection(current_file)
|
|
}
|
|
delete_character(current_file)
|
|
time.sleep(DEFAULT_DELAY)
|
|
}
|
|
if rl.IsKeyDown(.TAB) {
|
|
if current_file.selection.active {
|
|
delete_selection(current_file)
|
|
}
|
|
insert_character(current_file, '\t')
|
|
time.sleep(DEFAULT_DELAY)
|
|
}
|
|
if rl.IsKeyDown(.DELETE) {
|
|
if current_file.selection.active {
|
|
delete_selection(current_file)
|
|
}
|
|
delete_character(current_file, delete_backwards = true)
|
|
time.sleep(DEFAULT_DELAY)
|
|
}
|
|
|
|
// Handle cursor movement
|
|
shift_held := rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT)
|
|
|
|
if rl.IsKeyDown(.LEFT) {
|
|
if !shift_held && current_file.selection.active {
|
|
current_file.cursor_pos = current_file.selection.start
|
|
current_file.selection.active = false
|
|
} else {
|
|
if !current_file.selection.active && shift_held {
|
|
current_file.selection.start = current_file.cursor_pos
|
|
current_file.selection.active = true
|
|
}
|
|
if current_file.cursor_pos > 0 {
|
|
current_file.cursor_pos -= 1
|
|
}
|
|
if shift_held {
|
|
current_file.selection.end = current_file.cursor_pos
|
|
}
|
|
}
|
|
time.sleep(DEFAULT_DELAY)
|
|
}
|
|
if rl.IsKeyDown(.RIGHT) {
|
|
if !shift_held && current_file.selection.active {
|
|
current_file.cursor_pos = current_file.selection.end
|
|
current_file.selection.active = false
|
|
} else {
|
|
if !current_file.selection.active && shift_held {
|
|
current_file.selection.start = current_file.cursor_pos
|
|
current_file.selection.active = true
|
|
}
|
|
if current_file.cursor_pos < len(current_file.content) {
|
|
current_file.cursor_pos += 1
|
|
}
|
|
if shift_held {
|
|
current_file.selection.end = current_file.cursor_pos
|
|
}
|
|
}
|
|
time.sleep(DEFAULT_DELAY)
|
|
}
|
|
if rl.IsKeyDown(.UP) {
|
|
if !shift_held && current_file.selection.active {
|
|
current_file.selection.active = false
|
|
} else if !current_file.selection.active && shift_held {
|
|
current_file.selection.start = current_file.cursor_pos
|
|
current_file.selection.active = true
|
|
}
|
|
move_cursor_up(current_file)
|
|
if shift_held {
|
|
current_file.selection.end = current_file.cursor_pos
|
|
}
|
|
time.sleep(DEFAULT_DELAY)
|
|
}
|
|
if rl.IsKeyDown(.DOWN) {
|
|
if !shift_held && current_file.selection.active {
|
|
current_file.selection.active = false
|
|
} else if !current_file.selection.active && shift_held {
|
|
current_file.selection.start = current_file.cursor_pos
|
|
current_file.selection.active = true
|
|
}
|
|
move_cursor_down(current_file)
|
|
if shift_held {
|
|
current_file.selection.end = current_file.cursor_pos
|
|
}
|
|
time.sleep(DEFAULT_DELAY)
|
|
}
|
|
if rl.IsKeyDown(.HOME) {
|
|
move_cursor_to_start_of_line(current_file)
|
|
}
|
|
if rl.IsKeyDown(.END) {
|
|
move_cursor_to_end_of_line(current_file)
|
|
}
|
|
|
|
// Handle scrolling
|
|
wheel := rl.GetMouseWheelMove()
|
|
current_file.scroll_y -= wheel * app.line_height * 3
|
|
current_file.scroll_y = max(0, current_file.scroll_y)
|
|
|
|
// Handle keyboard shortcuts
|
|
if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) {
|
|
if rl.IsKeyPressed(.S) {
|
|
app.thread = thread.create_and_start_with_poly_data2(&app, current_file, save_file)
|
|
}
|
|
if rl.IsKeyPressed(.O) {
|
|
app.thread = thread.create_and_start_with_poly_data(&app, open_file)
|
|
}
|
|
if rl.IsKeyPressed(.A) {
|
|
select_all(current_file)
|
|
}
|
|
if rl.IsKeyPressed(.C) {
|
|
copy_selection(&app, current_file)
|
|
}
|
|
if rl.IsKeyPressed(.V) {
|
|
paste_text(current_file, app.copied_text)
|
|
}
|
|
if rl.IsKeyPressed(.TAB) {
|
|
if rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) {
|
|
app.active_tab = (app.active_tab - 1) % len(app.files)
|
|
} else {
|
|
app.active_tab = (app.active_tab + 1) % len(app.files)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle font switching (F1-F9)
|
|
for i in 0..<min(9, len(app.fonts)) {
|
|
if rl.IsKeyPressed(rl.KeyboardKey(int(rl.KeyboardKey.F1) + i)) {
|
|
app.current_font_index = i
|
|
app.line_height = app.fonts[app.current_font_index].size + 4
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Draw tabs
|
|
draw_tabs(&app)
|
|
|
|
// Draw active file content
|
|
if len(app.files) > 0 && app.active_tab < len(app.files) {
|
|
draw_file_content(&app, &app.files[app.active_tab])
|
|
}
|
|
|
|
// Draw status bar
|
|
draw_status_bar(&app)
|
|
|
|
rl.EndDrawing()
|
|
}
|
|
}
|
|
|
|
load_fonts :: proc(app: ^App) {
|
|
// TODO: Support loading multiple fonts
|
|
// TODO: Allow searching for a font based on it's name and search fc-list database
|
|
font := FontConfig{
|
|
font = rl.LoadFont(strings.clone_to_cstring(app.config.font)),
|
|
size = f32(app.config.font_size),
|
|
name = "Default",
|
|
}
|
|
append(&app.fonts, font)
|
|
|
|
app.line_height = app.fonts[app.current_font_index].size + 4
|
|
}
|
|
|
|
create_file_buffer :: proc(app: ^App, filename: string = "") -> (ok: bool) {
|
|
ok = false
|
|
|
|
buffer: File_Buffer
|
|
if len(filename) == 0 {
|
|
buffer = File_Buffer{
|
|
filename = "",
|
|
content = "",
|
|
modified = false,
|
|
cursor_pos = 0,
|
|
scroll_y = 0,
|
|
selection = Selection{start = 0, end = 0, active = false},
|
|
}
|
|
} else {
|
|
data, ok := os.read_entire_file(filename)
|
|
if !ok {
|
|
fmt.printf("Failed to read file: %s\n", filename)
|
|
return
|
|
}
|
|
|
|
buffer = File_Buffer{
|
|
filename = strings.clone(filename),
|
|
content = string(data),
|
|
modified = false,
|
|
cursor_pos = 0,
|
|
scroll_y = 0,
|
|
selection = Selection{start = 0, end = 0, active = false},
|
|
}
|
|
}
|
|
|
|
append(&app.files, buffer)
|
|
|
|
ok = true
|
|
|
|
return
|
|
}
|
|
|
|
open_file :: proc(app: ^App) {
|
|
app.file_dialog_open = true
|
|
result: file_dialog.Result
|
|
result = file_dialog.show_open_file_dialog()
|
|
app.file_dialog_open = false
|
|
if !result.success {
|
|
log.error(result.error_message)
|
|
return
|
|
}
|
|
defer file_dialog.destroy_result(&result)
|
|
|
|
ok := create_file_buffer(app, result.file_path)
|
|
if !ok {
|
|
log.error("failed ot create file buffer")
|
|
return
|
|
}
|
|
app.active_tab+=1
|
|
|
|
return
|
|
}
|
|
|
|
get_wrapped_lines :: proc(content: string, font: ^FontConfig, max_width: f32) -> []WrappedLine {
|
|
lines := strings.split(content, "\n")
|
|
defer delete(lines)
|
|
|
|
wrapped_lines := make([dynamic]WrappedLine)
|
|
character_offset := 0
|
|
|
|
for line, line_index in lines {
|
|
if len(line) == 0 {
|
|
// Empty line
|
|
append(&wrapped_lines, WrappedLine{
|
|
text = "",
|
|
original_line = line_index,
|
|
character_offset = character_offset,
|
|
})
|
|
} else {
|
|
// Wrap long lines
|
|
current_pos := 0
|
|
for current_pos < len(line) {
|
|
// Find how many characters fit in the width
|
|
fit_characters := 0
|
|
current_width: f32 = 0
|
|
|
|
for i in current_pos..<len(line) {
|
|
character_width := rl.MeasureTextEx(font.font, strings.clone_to_cstring(fmt.tprint(rune(line[i]))), font.size, 1).x
|
|
if current_width + character_width > max_width && fit_characters > 0 {
|
|
break
|
|
}
|
|
current_width += character_width
|
|
fit_characters += 1
|
|
}
|
|
|
|
// If we couldn't fit even one character, force at least one
|
|
if fit_characters == 0 {
|
|
fit_characters = 1
|
|
}
|
|
|
|
// Extract the text that fits
|
|
end_pos := min(current_pos + fit_characters, len(line))
|
|
wrapped_text := line[current_pos:end_pos]
|
|
|
|
append(&wrapped_lines, WrappedLine{
|
|
text = wrapped_text,
|
|
original_line = line_index,
|
|
character_offset = character_offset + current_pos,
|
|
})
|
|
|
|
current_pos = end_pos
|
|
}
|
|
}
|
|
character_offset += len(line) + 1 // +1 for newline
|
|
}
|
|
|
|
return wrapped_lines[:]
|
|
}
|
|
|
|
get_character_at_position :: proc(app: ^App, mouse_pos: rl.Vector2) -> int {
|
|
if len(app.files) == 0 || app.active_tab >= len(app.files) do return 0
|
|
|
|
file := &app.files[app.active_tab]
|
|
font := &app.fonts[app.current_font_index]
|
|
|
|
content_area_x := 10 + app.line_number_width
|
|
content_area_y := app.text_area_y
|
|
content_width := f32(app.window_width - 20) - app.line_number_width - 10
|
|
|
|
if mouse_pos.x < content_area_x || mouse_pos.y < content_area_y do return 0
|
|
|
|
wrapped_lines := get_wrapped_lines(file.content, font, content_width)
|
|
defer delete(wrapped_lines)
|
|
|
|
y_pos := content_area_y - file.scroll_y + 5
|
|
|
|
for wrapped_line, i in wrapped_lines {
|
|
if mouse_pos.y >= y_pos && mouse_pos.y < y_pos + app.line_height {
|
|
// Found the line, now find the character
|
|
x_offset := mouse_pos.x - content_area_x - 5
|
|
|
|
if x_offset <= 0 {
|
|
return wrapped_line.character_offset
|
|
}
|
|
|
|
for j in 0..=len(wrapped_line.text) {
|
|
text_slice := wrapped_line.text[:j]
|
|
text_width := rl.MeasureTextEx(font.font, strings.clone_to_cstring(text_slice), font.size, 1).x
|
|
if x_offset <= text_width {
|
|
return wrapped_line.character_offset + j
|
|
}
|
|
}
|
|
return wrapped_line.character_offset + len(wrapped_line.text)
|
|
}
|
|
y_pos += app.line_height
|
|
}
|
|
|
|
return len(file.content)
|
|
}
|
|
|
|
draw :: proc(app: ^App) {
|
|
}
|
|
|
|
draw_tabs :: proc(app: ^App) {
|
|
if len(app.files) == 0 do return
|
|
|
|
tab_width := f32(app.window_width) / f32(len(app.files))
|
|
|
|
for i in 0..<len(app.files) {
|
|
x := f32(i) * tab_width
|
|
|
|
colours := app.config.colours
|
|
|
|
// Tab background
|
|
colour := colours.active_tab if i == app.active_tab else colours.inactive_tab
|
|
rl.DrawRectangle(i32(x), 0, i32(tab_width), i32(app.tab_height), colour)
|
|
|
|
// Tab border
|
|
rl.DrawRectangleLines(i32(x), 0, i32(tab_width), i32(app.tab_height), colour)
|
|
|
|
// Tab text
|
|
filename := app.files[i].filename
|
|
if len(filename) > 20 {
|
|
filename = fmt.tprintf("...%s", filename[len(filename)-17:])
|
|
}
|
|
|
|
// Add asterisk if modified
|
|
display_name := filename
|
|
if app.files[i].modified {
|
|
display_name = fmt.tprintf("%s*", filename)
|
|
}
|
|
|
|
font := &app.fonts[app.current_font_index]
|
|
text_size := rl.MeasureTextEx(font.font, strings.clone_to_cstring(display_name), font.size, 1)
|
|
text_x := x + tab_width/2 - text_size.x/2
|
|
text_y := app.tab_height/2 - text_size.y/2
|
|
|
|
rl.DrawTextEx(
|
|
font.font,
|
|
strings.clone_to_cstring(display_name),
|
|
{text_x, text_y},
|
|
font.size,
|
|
1,
|
|
colours.text
|
|
)
|
|
}
|
|
}
|
|
|
|
draw_file_content :: proc(app: ^App, file: ^File_Buffer) {
|
|
font := &app.fonts[app.current_font_index]
|
|
|
|
// Content area (excluding line numbers)
|
|
content_area := rl.Rectangle{
|
|
x = 10 + app.line_number_width,
|
|
y = app.text_area_y,
|
|
width = f32(app.window_width - 20) - app.line_number_width,
|
|
height = f32(app.window_height) - app.text_area_y - 30,
|
|
}
|
|
|
|
// Line number area
|
|
line_num_area := rl.Rectangle{
|
|
x = 10,
|
|
y = app.text_area_y,
|
|
width = app.line_number_width,
|
|
height = f32(app.window_height) - app.text_area_y - 30,
|
|
}
|
|
|
|
// Background
|
|
colours := app.config.colours
|
|
|
|
rl.DrawRectangleRec(line_num_area, colours.line_numbers_background)
|
|
rl.DrawRectangleLinesEx(line_num_area, 2, colours.line_numbers_background)
|
|
|
|
rl.DrawRectangleRec(content_area, colours.background)
|
|
rl.DrawRectangleLinesEx(content_area, 2, colours.background)
|
|
|
|
// Get wrapped lines
|
|
content_width := content_area.width
|
|
wrapped_lines := get_wrapped_lines(file.content, font, content_width)
|
|
defer delete(wrapped_lines)
|
|
|
|
// Enable scissor test for scrolling
|
|
rl.BeginScissorMode(
|
|
i32(content_area.x),
|
|
i32(content_area.y),
|
|
i32(content_area.width),
|
|
i32(content_area.height)
|
|
)
|
|
|
|
// Draw text content and selection
|
|
y_pos := content_area.y - file.scroll_y + 5
|
|
cursor_y := y_pos
|
|
cursor_x := content_area.x + 5
|
|
|
|
for wrapped_line, i in wrapped_lines {
|
|
if y_pos > content_area.y + content_area.height {
|
|
break
|
|
}
|
|
|
|
if y_pos + app.line_height > content_area.y {
|
|
// Draw selection background for this line
|
|
if file.selection.active {
|
|
line_start := wrapped_line.character_offset
|
|
line_end := wrapped_line.character_offset + len(wrapped_line.text)
|
|
|
|
if file.selection.start <= line_end && file.selection.end >= line_start {
|
|
sel_start_in_line := max(0, file.selection.start - line_start)
|
|
sel_end_in_line := min(len(wrapped_line.text), file.selection.end - line_start)
|
|
|
|
if sel_start_in_line < sel_end_in_line {
|
|
before_sel := wrapped_line.text[:sel_start_in_line]
|
|
sel_x := content_area.x + 5 +
|
|
rl.MeasureTextEx(font.font, strings.clone_to_cstring(before_sel), font.size, 1).x
|
|
|
|
selected_text := wrapped_line.text[sel_start_in_line:sel_end_in_line]
|
|
sel_width := rl.MeasureTextEx(font.font, strings.clone_to_cstring(selected_text), font.size, 1).x
|
|
|
|
rl.DrawRectangle(
|
|
i32(sel_x),
|
|
i32(y_pos),
|
|
i32(sel_width),
|
|
i32(app.line_height),
|
|
app.config.colours.highlight
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Draw text
|
|
rl.DrawTextEx(
|
|
font.font,
|
|
strings.clone_to_cstring(wrapped_line.text),
|
|
{content_area.x + 5, y_pos},
|
|
font.size,
|
|
1,
|
|
app.config.colours.text
|
|
)
|
|
}
|
|
|
|
// Calculate cursor position
|
|
if wrapped_line.character_offset <= file.cursor_pos &&
|
|
file.cursor_pos <= wrapped_line.character_offset + len(wrapped_line.text) {
|
|
cursor_y = y_pos
|
|
cursor_character_pos := file.cursor_pos - wrapped_line.character_offset
|
|
cursor_text := wrapped_line.text[:cursor_character_pos] if cursor_character_pos <= len(wrapped_line.text) else wrapped_line.text
|
|
cursor_x = content_area.x + 5 + rl.MeasureTextEx(font.font, strings.clone_to_cstring(cursor_text), font.size, 1).x
|
|
}
|
|
|
|
y_pos += app.line_height
|
|
}
|
|
|
|
// Draw cursor
|
|
if !file.selection.active && int(rl.GetTime() * 2) % 2 == 0 { // Blinking cursor
|
|
rl.DrawLine(i32(cursor_x), i32(cursor_y), i32(cursor_x), i32(cursor_y + app.line_height), rl.RED)
|
|
}
|
|
|
|
rl.EndScissorMode()
|
|
|
|
// Draw line numbers (show original line numbers)
|
|
rl.BeginScissorMode(i32(line_num_area.x), i32(line_num_area.y),
|
|
i32(line_num_area.width), i32(line_num_area.height))
|
|
|
|
y_pos = line_num_area.y - file.scroll_y + 5
|
|
current_original_line := -1
|
|
|
|
for wrapped_line, i in wrapped_lines {
|
|
if y_pos > line_num_area.y + line_num_area.height {
|
|
break
|
|
}
|
|
|
|
if y_pos + app.line_height > line_num_area.y {
|
|
// Only show line number for the first wrapped line of each original line
|
|
if wrapped_line.original_line != current_original_line {
|
|
current_original_line = wrapped_line.original_line
|
|
line_num_text := fmt.tprintf("%d", wrapped_line.original_line + 1)
|
|
text_size := rl.MeasureTextEx(font.font, strings.clone_to_cstring(line_num_text), font.size, 1)
|
|
text_x := line_num_area.x + line_num_area.width - text_size.x - 5
|
|
rl.DrawTextEx(
|
|
font.font,
|
|
strings.clone_to_cstring(line_num_text),
|
|
{text_x, y_pos},
|
|
font.size,
|
|
1,
|
|
app.config.colours.line_numbers
|
|
)
|
|
}
|
|
}
|
|
|
|
y_pos += app.line_height
|
|
}
|
|
|
|
rl.EndScissorMode()
|
|
}
|
|
|
|
draw_status_bar :: proc(app: ^App) {
|
|
status_y := app.window_height - 25
|
|
rl.DrawRectangle(0, status_y, app.window_width, 25, app.config.colours.status_bar)
|
|
|
|
if len(app.files) > 0 && app.active_tab < len(app.files) {
|
|
file := &app.files[app.active_tab]
|
|
font_name := app.fonts[app.current_font_index].name
|
|
selection_info := ""
|
|
if file.selection.active {
|
|
selection_info = fmt.tprintf(" | Selected: %d characters", file.selection.end - file.selection.start)
|
|
}
|
|
status_text := fmt.tprintf(
|
|
"File: %s | Font: %s (F1-F%d) | Cursor: %d%s | Ctrl+S: Save | Ctrl+A: Select All | Ctrl+C/V: Copy/Paste",
|
|
file.filename,
|
|
font_name,
|
|
len(app.fonts),
|
|
file.cursor_pos,
|
|
selection_info
|
|
)
|
|
|
|
font := &app.fonts[app.current_font_index]
|
|
rl.DrawTextEx(
|
|
font.font,
|
|
strings.clone_to_cstring(status_text),
|
|
{f32(5), f32(status_y + 5)},
|
|
font.size,
|
|
1,
|
|
app.config.colours.status_bar_text
|
|
)
|
|
}
|
|
}
|
|
|
|
insert_character :: proc(file: ^File_Buffer, character: rune) {
|
|
if file.cursor_pos <= len(file.content) {
|
|
before := file.content[:file.cursor_pos]
|
|
after := file.content[file.cursor_pos:]
|
|
file.content = fmt.aprintf("%s%c%s", before, character, after)
|
|
file.cursor_pos += 1
|
|
file.modified = true
|
|
clear_selection(file)
|
|
}
|
|
}
|
|
|
|
delete_character :: proc(file: ^File_Buffer, delete_backwards: bool = false) {
|
|
if file.cursor_pos > 0 && !delete_backwards {
|
|
before := file.content[:file.cursor_pos-1]
|
|
after := file.content[file.cursor_pos:]
|
|
file.content = fmt.aprintf("%s%s", before, after)
|
|
file.cursor_pos -= 1
|
|
file.modified = true
|
|
clear_selection(file)
|
|
} else if file.cursor_pos < len(file.content) && delete_backwards {
|
|
before := file.content[:file.cursor_pos]
|
|
after := file.content[file.cursor_pos+1:]
|
|
file.content = fmt.aprintf("%s%s", before, after)
|
|
file.modified = true
|
|
clear_selection(file)
|
|
}
|
|
}
|
|
|
|
delete_selection :: proc(file: ^File_Buffer) {
|
|
if !file.selection.active do return
|
|
|
|
start := min(file.selection.start, file.selection.end)
|
|
end := max(file.selection.start, file.selection.end)
|
|
|
|
before := file.content[:start]
|
|
after := file.content[end:]
|
|
file.content = fmt.aprintf("%s%s", before, after)
|
|
file.cursor_pos = start
|
|
file.modified = true
|
|
clear_selection(file)
|
|
}
|
|
|
|
clear_selection :: proc(file: ^File_Buffer) {
|
|
file.selection.active = false
|
|
file.selection.start = 0
|
|
file.selection.end = 0
|
|
}
|
|
|
|
select_all :: proc(file: ^File_Buffer) {
|
|
file.selection.start = 0
|
|
file.selection.end = len(file.content)
|
|
file.selection.active = true
|
|
file.cursor_pos = len(file.content)
|
|
}
|
|
|
|
copy_selection :: proc(app: ^App, file: ^File_Buffer) {
|
|
if !file.selection.active do return
|
|
|
|
start := min(file.selection.start, file.selection.end)
|
|
end := max(file.selection.start, file.selection.end)
|
|
app.copied_text = file.content[start:end]
|
|
|
|
// Set clipboard (Raylib function)
|
|
rl.SetClipboardText(strings.clone_to_cstring(app.copied_text))
|
|
}
|
|
|
|
paste_text :: proc(file: ^File_Buffer, text: string) {
|
|
if file.selection.active {
|
|
delete_selection(file)
|
|
}
|
|
|
|
// Get clipboard text
|
|
clipboard := rl.GetClipboardText()
|
|
if clipboard != nil {
|
|
text_to_paste := string(clipboard)
|
|
before := file.content[:file.cursor_pos]
|
|
after := file.content[file.cursor_pos:]
|
|
file.content = fmt.aprintf("%s%s%s", before, text_to_paste, after)
|
|
file.cursor_pos += len(text_to_paste)
|
|
file.modified = true
|
|
}
|
|
}
|
|
|
|
move_cursor_up :: proc(file: ^File_Buffer) {
|
|
// For wrapped text, we need to handle cursor movement differently
|
|
if file.cursor_pos > 0 {
|
|
// Find the previous newline
|
|
for i := file.cursor_pos - 1; i >= 0; i -= 1 {
|
|
if file.content[i] == '\n' {
|
|
// Move to the previous line
|
|
prev_newline := -1
|
|
for j := i - 1; j >= 0; j -= 1 {
|
|
if file.content[j] == '\n' {
|
|
prev_newline = j
|
|
break
|
|
}
|
|
}
|
|
|
|
current_pos_in_line := file.cursor_pos - i - 1
|
|
prev_line_length := i - prev_newline - 1
|
|
new_pos_in_line := min(current_pos_in_line, prev_line_length)
|
|
file.cursor_pos = prev_newline + 1 + new_pos_in_line
|
|
return
|
|
}
|
|
}
|
|
// If no newline found, go to beginning
|
|
file.cursor_pos = 0
|
|
}
|
|
}
|
|
|
|
move_cursor_down :: proc(file: ^File_Buffer) {
|
|
// For wrapped text, we need to handle cursor movement differently
|
|
if file.cursor_pos < len(file.content) {
|
|
// Find the current line start
|
|
current_line_start := 0
|
|
for i := file.cursor_pos - 1; i >= 0; i -= 1 {
|
|
if file.content[i] == '\n' {
|
|
current_line_start = i + 1
|
|
break
|
|
}
|
|
}
|
|
|
|
// Find the next newline
|
|
for i := file.cursor_pos; i < len(file.content); i += 1 {
|
|
if file.content[i] == '\n' {
|
|
// Move to the next line
|
|
next_newline := len(file.content)
|
|
for j := i + 1; j < len(file.content); j += 1 {
|
|
if file.content[j] == '\n' {
|
|
next_newline = j
|
|
break
|
|
}
|
|
}
|
|
|
|
current_pos_in_line := file.cursor_pos - current_line_start
|
|
next_line_length := next_newline - i - 1
|
|
new_pos_in_line := min(current_pos_in_line, next_line_length)
|
|
file.cursor_pos = i + 1 + new_pos_in_line
|
|
return
|
|
}
|
|
}
|
|
// If no newline found, go to end
|
|
file.cursor_pos = len(file.content)
|
|
}
|
|
}
|
|
|
|
move_cursor_to_start_of_line :: proc(file: ^File_Buffer) {
|
|
// For wrapped text, we need to handle cursor movement differently
|
|
if file.cursor_pos < len(file.content) {
|
|
// Find the current line start
|
|
for i := file.cursor_pos - 1; i >= 0; i -= 1 {
|
|
if file.content[i] == '\n' {
|
|
file.cursor_pos = i + 1
|
|
return
|
|
}
|
|
}
|
|
|
|
// If no newline found, go to start
|
|
file.cursor_pos = 0
|
|
}
|
|
}
|
|
|
|
move_cursor_to_end_of_line :: proc(file: ^File_Buffer) {
|
|
// For wrapped text, we need to handle cursor movement differently
|
|
if file.cursor_pos < len(file.content) {
|
|
// Find the next newline
|
|
for i := file.cursor_pos; i < len(file.content); i += 1 {
|
|
if file.content[i] == '\n' {
|
|
file.cursor_pos = i
|
|
return
|
|
}
|
|
}
|
|
|
|
// If no newline found, go to end
|
|
file.cursor_pos = len(file.content)
|
|
}
|
|
}
|
|
|
|
save_file :: proc(app: ^App, file: ^File_Buffer) {
|
|
app.thread_finished = false
|
|
if len(file.filename) == 0 {
|
|
app.file_dialog_open = true
|
|
result := file_dialog.show_save_file_dialog()
|
|
app.file_dialog_open = false
|
|
if !result.success {
|
|
log.error(result.error_message)
|
|
}
|
|
defer file_dialog.destroy_result(&result)
|
|
|
|
file.filename = strings.clone(result.file_path)
|
|
}
|
|
|
|
ok := os.write_entire_file(file.filename, transmute([]u8)file.content)
|
|
file.modified = !ok
|
|
if !ok {
|
|
log.error("failed to save file, \"", file.filename, "\"", sep="") // TODO: Show popup window that displays this to the user
|
|
return
|
|
}
|
|
app.thread_finished = true
|
|
}
|
|
|