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.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.
notepad/main.odin

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