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.
587 lines
15 KiB
Plaintext
587 lines
15 KiB
Plaintext
package main
|
|
|
|
import "core:fmt"
|
|
import "core:log"
|
|
import "core:math"
|
|
import "core:mem"
|
|
import "core:os"
|
|
import "core:slice"
|
|
import "core:strconv"
|
|
import "core:strings"
|
|
import "core:unicode/utf8"
|
|
|
|
import sdl "vendor:sdl2"
|
|
import ttf "vendor:sdl2/ttf"
|
|
|
|
// Application constants
|
|
WINDOW_WIDTH :: 1200
|
|
WINDOW_HEIGHT :: 800
|
|
TAB_HEIGHT :: 30
|
|
LINE_NUMBER_WIDTH :: 60
|
|
FONT_SIZE :: 14
|
|
TEXT_PADDING :: 10
|
|
|
|
app: App
|
|
|
|
main :: proc() {
|
|
context.logger = log.create_console_logger(.Debug)
|
|
defer log.destroy_console_logger(context.logger)
|
|
|
|
// Get command line arguments
|
|
args := os.args[1:]
|
|
|
|
ok := false
|
|
app.config, ok = parse_config("config.ini")
|
|
if !ok {
|
|
log.fatal("failed to parse config file")
|
|
return
|
|
}
|
|
defer config_destroy(app.config)
|
|
|
|
log.debug("app config:")
|
|
log.debug(app.config)
|
|
|
|
if init_sdl() != 0 {
|
|
log.fatal("failed to initialize sdl")
|
|
return
|
|
}
|
|
defer cleanup_sdl()
|
|
|
|
sdl.SetWindowMinimumSize(app.window, 300, 400)
|
|
|
|
// Create tabs from command line arguments
|
|
if len(args) == 0 {
|
|
// Create empty tab if no files specified
|
|
create_tab("Untitled", "")
|
|
} else {
|
|
for filename in args {
|
|
content := ""
|
|
if data, ok := os.read_entire_file(filename); ok {
|
|
content = string(data)
|
|
} else {
|
|
fmt.printf("Warning: Could not read file '%s'\n", filename)
|
|
content = fmt.tprintf("Error: Could not read file '%s'", filename)
|
|
}
|
|
|
|
// Extract filename for tab
|
|
tab_name := filename
|
|
if idx := strings.last_index(filename, "/"); idx != -1 {
|
|
tab_name = filename[idx+1:]
|
|
}
|
|
if idx := strings.last_index(tab_name, "\\"); idx != -1 {
|
|
tab_name = tab_name[idx+1:]
|
|
}
|
|
|
|
create_tab(tab_name, content)
|
|
}
|
|
}
|
|
|
|
app.active_tab = 0
|
|
calculate_layout()
|
|
|
|
// Main loop
|
|
app.running = true
|
|
for app.running {
|
|
handle_events()
|
|
render()
|
|
sdl.Delay(16) // ~60 FPS
|
|
}
|
|
}
|
|
|
|
init_sdl :: proc() -> int {
|
|
if sdl.Init(sdl.INIT_VIDEO) < 0 {
|
|
fmt.printf("SDL could not initialize: %s\n", sdl.GetError())
|
|
return -1
|
|
}
|
|
|
|
if ttf.Init() < 0 {
|
|
fmt.printf("TTF could not initialize: %s\n", ttf.GetError())
|
|
return -1
|
|
}
|
|
|
|
app.window = sdl.CreateWindow(
|
|
"notepad",
|
|
sdl.WINDOWPOS_CENTERED, sdl.WINDOWPOS_CENTERED,
|
|
WINDOW_WIDTH, WINDOW_HEIGHT,
|
|
sdl.WINDOW_SHOWN | sdl.WINDOW_RESIZABLE
|
|
)
|
|
|
|
if app.window == nil {
|
|
fmt.printf("Window could not be created: %s\n", sdl.GetError())
|
|
return -1
|
|
}
|
|
|
|
app.renderer = sdl.CreateRenderer(app.window, -1, sdl.RENDERER_ACCELERATED)
|
|
if app.renderer == nil {
|
|
fmt.printf("Renderer could not be created: %s\n", sdl.GetError())
|
|
return -1
|
|
}
|
|
|
|
font_cstring := strings.clone_to_cstring(app.config.font)
|
|
defer delete(font_cstring)
|
|
app.font = ttf.OpenFont(font_cstring, i32(app.config.font_size))
|
|
|
|
if app.font == nil {
|
|
log.fatal("Could not load font")
|
|
return -1
|
|
}
|
|
|
|
// Calculate character dimensions
|
|
w, h: i32
|
|
if ttf.SizeText(app.font, "W", &w, &h) == 0 {
|
|
app.char_width = w
|
|
app.char_height = h
|
|
} else {
|
|
app.char_width = 8
|
|
app.char_height = 16
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
cleanup_sdl :: proc() {
|
|
if app.font != nil do ttf.CloseFont(app.font)
|
|
if app.renderer != nil do sdl.DestroyRenderer(app.renderer)
|
|
if app.window != nil do sdl.DestroyWindow(app.window)
|
|
|
|
for &tab in app.tabs {
|
|
delete(tab.buffer.lines)
|
|
}
|
|
delete(app.tabs)
|
|
|
|
ttf.Quit()
|
|
sdl.Quit()
|
|
}
|
|
|
|
create_tab :: proc(name: string, content: string) {
|
|
tab := Tab{
|
|
name = strings.clone(name),
|
|
buffer = Text_Buffer{},
|
|
}
|
|
|
|
tab.buffer.filename = strings.clone(name)
|
|
tab.buffer.lines = make([dynamic]string)
|
|
|
|
// Split content into lines
|
|
if len(content) > 0 {
|
|
lines := strings.split(content, "\n")
|
|
defer delete(lines)
|
|
for line in lines {
|
|
append(&tab.buffer.lines, strings.clone(line))
|
|
}
|
|
} else {
|
|
append(&tab.buffer.lines, "")
|
|
}
|
|
|
|
append(&app.tabs, tab)
|
|
}
|
|
|
|
calculate_layout :: proc() {
|
|
w, h: i32
|
|
sdl.GetWindowSize(app.window, &w, &h)
|
|
|
|
app.text_area_width = w - LINE_NUMBER_WIDTH
|
|
app.visible_lines = int((h - TAB_HEIGHT) / app.char_height) - 1
|
|
|
|
// Calculate tab rectangles
|
|
if len(app.tabs) > 0 {
|
|
tab_width := w / i32(len(app.tabs))
|
|
for i in 0..<len(app.tabs) {
|
|
app.tabs[i].rect = {
|
|
x = i32(i) * tab_width,
|
|
y = 0,
|
|
w = tab_width,
|
|
h = TAB_HEIGHT,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
handle_events :: proc() {
|
|
e: sdl.Event
|
|
for sdl.PollEvent(&e) {
|
|
#partial switch e.type {
|
|
case sdl.EventType.QUIT:
|
|
app.running = false
|
|
|
|
case sdl.EventType.WINDOWEVENT:
|
|
calculate_layout()
|
|
|
|
case sdl.EventType.KEYDOWN:
|
|
handle_key_down(e.key.keysym.scancode, e.key.keysym.mod)
|
|
|
|
case sdl.EventType.TEXTINPUT:
|
|
handle_text_input(string(cstring(&e.text.text[0])))
|
|
|
|
case sdl.EventType.MOUSEBUTTONDOWN:
|
|
handle_mouse_click(e.button.x, e.button.y)
|
|
|
|
case sdl.EventType.MOUSEWHEEL:
|
|
handle_scroll(e.wheel.y)
|
|
}
|
|
}
|
|
|
|
// Handle cursor blinking
|
|
current_time := sdl.GetTicks()
|
|
if current_time - app.cursor_blink_timer > 500 {
|
|
app.show_cursor = !app.show_cursor
|
|
app.cursor_blink_timer = current_time
|
|
}
|
|
}
|
|
|
|
handle_key_down :: proc(scancode: sdl.Scancode, modifiers: sdl.Keymod) {
|
|
if len(app.tabs) == 0 do return
|
|
|
|
buffer := &app.tabs[app.active_tab].buffer
|
|
|
|
left_ctrl_pressed := (modifiers & sdl.KMOD_CTRL) == sdl.KMOD_LCTRL
|
|
right_ctrl_pressed := (modifiers & sdl.KMOD_CTRL) == sdl.KMOD_RCTRL
|
|
|
|
#partial switch scancode {
|
|
case .S:
|
|
if left_ctrl_pressed || right_ctrl_pressed {
|
|
// TODO: Tidy up this code and optimize it for big files
|
|
filename := buffer.filename
|
|
|
|
builder := strings.builder_make()
|
|
defer strings.builder_destroy(&builder)
|
|
|
|
for line in buffer.lines {
|
|
strings.write_string(&builder, line)
|
|
strings.write_rune(&builder, '\n')
|
|
}
|
|
str := strings.to_string(builder)
|
|
data := transmute([]u8)str
|
|
|
|
ok := os.write_entire_file(filename, data)
|
|
}
|
|
case .TAB:
|
|
// Switch tabs
|
|
app.active_tab = (app.active_tab + 1) % len(app.tabs)
|
|
|
|
case .RETURN:
|
|
// Insert new line
|
|
if buffer.cursor_line < len(buffer.lines) {
|
|
current_line := buffer.lines[buffer.cursor_line]
|
|
left_part := current_line[:buffer.cursor_col] if buffer.cursor_col <= len(current_line) else current_line
|
|
right_part := current_line[buffer.cursor_col:] if buffer.cursor_col <= len(current_line) else ""
|
|
|
|
buffer.lines[buffer.cursor_line] = strings.clone(left_part)
|
|
inject_at(&buffer.lines, buffer.cursor_line + 1, strings.clone(right_part))
|
|
|
|
buffer.cursor_line += 1
|
|
buffer.cursor_col = 0
|
|
buffer.modified = true
|
|
}
|
|
|
|
case .BACKSPACE:
|
|
if buffer.cursor_col > 0 {
|
|
// Remove character
|
|
line := &buffer.lines[buffer.cursor_line]
|
|
if buffer.cursor_col <= len(line^) {
|
|
new_line := strings.concatenate({line^[:buffer.cursor_col-1], line^[buffer.cursor_col:]})
|
|
delete(line^)
|
|
line^ = new_line
|
|
buffer.cursor_col -= 1
|
|
buffer.modified = true
|
|
}
|
|
} else if buffer.cursor_line > 0 {
|
|
// Join with previous line
|
|
current_line := buffer.lines[buffer.cursor_line]
|
|
prev_line := &buffer.lines[buffer.cursor_line - 1]
|
|
buffer.cursor_col = len(prev_line^)
|
|
|
|
new_line := strings.concatenate({prev_line^, current_line})
|
|
delete(prev_line^)
|
|
prev_line^ = new_line
|
|
|
|
ordered_remove(&buffer.lines, buffer.cursor_line)
|
|
buffer.cursor_line -= 1
|
|
buffer.modified = true
|
|
}
|
|
|
|
case .DELETE:
|
|
if buffer.cursor_line < len(buffer.lines) {
|
|
line := &buffer.lines[buffer.cursor_line]
|
|
if buffer.cursor_col < len(line^) {
|
|
new_line := strings.concatenate({line^[:buffer.cursor_col], line^[buffer.cursor_col+1:]})
|
|
delete(line^)
|
|
line^ = new_line
|
|
buffer.modified = true
|
|
}
|
|
}
|
|
|
|
case .LEFT:
|
|
if buffer.cursor_col > 0 {
|
|
buffer.cursor_col -= 1
|
|
} else if buffer.cursor_line > 0 {
|
|
buffer.cursor_line -= 1
|
|
buffer.cursor_col = len(buffer.lines[buffer.cursor_line])
|
|
}
|
|
|
|
case .RIGHT:
|
|
if buffer.cursor_line < len(buffer.lines) {
|
|
if buffer.cursor_col < len(buffer.lines[buffer.cursor_line]) {
|
|
buffer.cursor_col += 1
|
|
} else if buffer.cursor_line < len(buffer.lines) - 1 {
|
|
buffer.cursor_line += 1
|
|
buffer.cursor_col = 0
|
|
}
|
|
}
|
|
|
|
case .UP:
|
|
if buffer.cursor_line > 0 {
|
|
buffer.cursor_line -= 1
|
|
line_len := len(buffer.lines[buffer.cursor_line])
|
|
if buffer.cursor_col > line_len {
|
|
buffer.cursor_col = line_len
|
|
}
|
|
}
|
|
|
|
case .DOWN:
|
|
if buffer.cursor_line < len(buffer.lines) - 1 {
|
|
buffer.cursor_line += 1
|
|
line_len := len(buffer.lines[buffer.cursor_line])
|
|
if buffer.cursor_col > line_len {
|
|
buffer.cursor_col = line_len
|
|
}
|
|
}
|
|
|
|
case .HOME:
|
|
buffer.cursor_col = 0
|
|
|
|
case .END:
|
|
if buffer.cursor_line < len(buffer.lines) {
|
|
buffer.cursor_col = len(buffer.lines[buffer.cursor_line])
|
|
}
|
|
|
|
case .PAGEUP:
|
|
buffer.cursor_line = max(0, buffer.cursor_line - app.visible_lines)
|
|
buffer.scroll_y = max(0, buffer.scroll_y - app.visible_lines)
|
|
|
|
case .PAGEDOWN:
|
|
buffer.cursor_line = min(len(buffer.lines) - 1, buffer.cursor_line + app.visible_lines)
|
|
buffer.scroll_y = min(len(buffer.lines) - app.visible_lines, buffer.scroll_y + app.visible_lines)
|
|
}
|
|
|
|
// Reset cursor blink
|
|
app.cursor_blink_timer = sdl.GetTicks()
|
|
app.show_cursor = true
|
|
}
|
|
|
|
handle_text_input :: proc(text: string) {
|
|
if len(app.tabs) == 0 do return
|
|
|
|
buffer := &app.tabs[app.active_tab].buffer
|
|
|
|
if buffer.cursor_line < len(buffer.lines) {
|
|
line := &buffer.lines[buffer.cursor_line]
|
|
if buffer.cursor_col <= len(line^) {
|
|
new_line := strings.concatenate({
|
|
line^[:buffer.cursor_col],
|
|
text,
|
|
line^[buffer.cursor_col:]
|
|
})
|
|
delete(line^)
|
|
line^ = new_line
|
|
buffer.cursor_col += len(text)
|
|
buffer.modified = true
|
|
}
|
|
}
|
|
|
|
// Reset cursor blink
|
|
app.cursor_blink_timer = sdl.GetTicks()
|
|
app.show_cursor = true
|
|
}
|
|
|
|
handle_mouse_click :: proc(x, y: i32) {
|
|
// Check if click is on tabs
|
|
if y <= TAB_HEIGHT {
|
|
for i in 0..<len(app.tabs) {
|
|
tab := &app.tabs[i]
|
|
if x >= tab.rect.x && x < tab.rect.x + tab.rect.w {
|
|
app.active_tab = i
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Click in text area
|
|
if len(app.tabs) > 0 && y > TAB_HEIGHT {
|
|
buffer := &app.tabs[app.active_tab].buffer
|
|
|
|
// Calculate line and column
|
|
line := int((y - TAB_HEIGHT) / app.char_height) + buffer.scroll_y
|
|
col := int((x - LINE_NUMBER_WIDTH) / app.char_width)
|
|
|
|
if line >= 0 && line < len(buffer.lines) {
|
|
buffer.cursor_line = line
|
|
line_len := len(buffer.lines[line])
|
|
buffer.cursor_col = min(col, line_len)
|
|
}
|
|
|
|
// Reset cursor blink
|
|
app.cursor_blink_timer = sdl.GetTicks()
|
|
app.show_cursor = true
|
|
}
|
|
}
|
|
|
|
handle_scroll :: proc(y: i32) {
|
|
if len(app.tabs) == 0 do return
|
|
|
|
buffer := &app.tabs[app.active_tab].buffer
|
|
buffer.scroll_y = max(0, min(len(buffer.lines) - app.visible_lines, buffer.scroll_y - int(y * 3)))
|
|
}
|
|
|
|
render :: proc() {
|
|
sdl.SetRenderDrawColor(app.renderer, app.config.colours.background.r, app.config.colours.background.g, app.config.colours.background.b, app.config.colours.background.a)
|
|
sdl.RenderClear(app.renderer)
|
|
|
|
render_tabs()
|
|
|
|
if len(app.tabs) > 0 {
|
|
render_text_area()
|
|
}
|
|
|
|
sdl.RenderPresent(app.renderer)
|
|
}
|
|
|
|
render_tabs :: proc() {
|
|
for i in 0..<len(app.tabs) {
|
|
tab := &app.tabs[i]
|
|
color := app.config.colours.inactive_tab if i != app.active_tab else app.config.colours.active_tab
|
|
|
|
sdl.SetRenderDrawColor(app.renderer, color.r, color.g, color.b, color.a)
|
|
sdl.RenderFillRect(app.renderer, &tab.rect)
|
|
|
|
sdl.SetRenderDrawColor(app.renderer, app.config.colours.tab_border.r, app.config.colours.tab_border.g, app.config.colours.tab_border.b, app.config.colours.tab_border.a)
|
|
sdl.RenderDrawRect(app.renderer, &tab.rect)
|
|
|
|
// Render tab text
|
|
render_text(tab.name, tab.rect.x + 10, tab.rect.y + 5, app.config.colours.text)
|
|
}
|
|
}
|
|
|
|
render_text_area :: proc() {
|
|
buffer := &app.tabs[app.active_tab].buffer
|
|
|
|
// Render line number background
|
|
line_bg_rect := sdl.Rect{0, TAB_HEIGHT, LINE_NUMBER_WIDTH, WINDOW_HEIGHT - TAB_HEIGHT}
|
|
sdl.SetRenderDrawColor(
|
|
app.renderer,
|
|
app.config.colours.line_numbers_background.r,
|
|
app.config.colours.line_numbers_background.g,
|
|
app.config.colours.line_numbers_background.b,
|
|
app.config.colours.line_numbers_background.a
|
|
)
|
|
sdl.RenderFillRect(app.renderer, &line_bg_rect)
|
|
|
|
// Render separator line
|
|
sdl.SetRenderDrawColor(
|
|
app.renderer,
|
|
app.config.colours.tab_border.r,
|
|
app.config.colours.tab_border.g,
|
|
app.config.colours.tab_border.b,
|
|
app.config.colours.tab_border.a
|
|
)
|
|
sdl.RenderDrawLine(app.renderer, LINE_NUMBER_WIDTH, TAB_HEIGHT, LINE_NUMBER_WIDTH, WINDOW_HEIGHT)
|
|
|
|
// Adjust scroll if cursor is outside visible area
|
|
if buffer.cursor_line < buffer.scroll_y {
|
|
buffer.scroll_y = buffer.cursor_line
|
|
} else if buffer.cursor_line >= buffer.scroll_y + app.visible_lines {
|
|
buffer.scroll_y = buffer.cursor_line - app.visible_lines + 1
|
|
}
|
|
|
|
// Render visible lines
|
|
start_line := max(0, buffer.scroll_y)
|
|
end_line := min(len(buffer.lines), start_line + app.visible_lines)
|
|
|
|
for i in start_line..<end_line {
|
|
y := TAB_HEIGHT + i32(i - start_line) * app.char_height
|
|
|
|
// Render line number
|
|
line_num_str := fmt.tprintf("%d", i + 1)
|
|
render_text(line_num_str, 5, y, app.config.colours.line_numbers)
|
|
|
|
// Render line text with wrapping
|
|
if i < len(buffer.lines) {
|
|
render_wrapped_line(buffer.lines[i], LINE_NUMBER_WIDTH + TEXT_PADDING, y, app.text_area_width - TEXT_PADDING * 2)
|
|
}
|
|
|
|
// Render cursor
|
|
if i == buffer.cursor_line && app.show_cursor {
|
|
cursor_x := LINE_NUMBER_WIDTH + TEXT_PADDING + i32(buffer.cursor_col) * app.char_width
|
|
cursor_y := y
|
|
|
|
sdl.SetRenderDrawColor(
|
|
app.renderer,
|
|
app.config.colours.cursor.r,
|
|
app.config.colours.cursor.g,
|
|
app.config.colours.cursor.b,
|
|
app.config.colours.cursor.a
|
|
)
|
|
cursor_rect := sdl.Rect{cursor_x, cursor_y, 2, app.char_height}
|
|
sdl.RenderFillRect(app.renderer, &cursor_rect)
|
|
}
|
|
}
|
|
}
|
|
|
|
render_text :: proc(text: string, x, y: i32, color: sdl.Color) {
|
|
if app.font == nil || len(text) == 0 do return
|
|
|
|
text_cstr := strings.clone_to_cstring(text)
|
|
defer delete(text_cstr)
|
|
|
|
surface := ttf.RenderText_Solid(app.font, text_cstr, color)
|
|
if surface == nil do return
|
|
defer sdl.FreeSurface(surface)
|
|
|
|
texture := sdl.CreateTextureFromSurface(app.renderer, surface)
|
|
if texture == nil do return
|
|
defer sdl.DestroyTexture(texture)
|
|
|
|
dst_rect := sdl.Rect{x, y, surface.w, surface.h}
|
|
sdl.RenderCopy(app.renderer, texture, nil, &dst_rect)
|
|
}
|
|
|
|
render_wrapped_line :: proc(line: string, x, y: i32, max_width: i32) {
|
|
if len(line) == 0 do return
|
|
|
|
chars_per_line := int(max_width / app.char_width)
|
|
if chars_per_line <= 0 do return
|
|
|
|
// Simple word wrapping - break at word boundaries when possible
|
|
words := strings.split(line, " ")
|
|
defer delete(words)
|
|
|
|
current_line := ""
|
|
line_y := y
|
|
|
|
for word in words {
|
|
test_line := current_line
|
|
if len(current_line) > 0 {
|
|
test_line = strings.concatenate({current_line, " ", word})
|
|
} else {
|
|
test_line = word
|
|
}
|
|
|
|
if len(test_line) <= chars_per_line {
|
|
current_line = test_line
|
|
} else {
|
|
// Render current line and start new one
|
|
if len(current_line) > 0 {
|
|
render_text(current_line, x, line_y, app.config.colours.text)
|
|
line_y += app.char_height
|
|
}
|
|
current_line = word
|
|
}
|
|
}
|
|
|
|
// Render final line
|
|
if len(current_line) > 0 {
|
|
render_text(current_line, x, line_y, app.config.colours.text)
|
|
}
|
|
}
|
|
|