Created
November 23, 2023 02:00
-
-
Save pJotoro/fae7bc4ea3c551d40d2e8d5b67c119d4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import "jo:app" | |
import "core:mem" | |
import "core:os" | |
import "core:strings" | |
import "core:text/edit" | |
import stbtt "vendor:stb/truetype" | |
Glyph :: struct { | |
data: [^]byte, | |
x, y, width, height: i32, | |
} | |
Font :: struct { | |
buf: []byte, | |
info: stbtt.fontinfo, | |
glyphs: map[rune]Glyph, | |
allocator: mem.Allocator, | |
} | |
font_create :: proc(file_path: string, allocator := context.allocator) -> (font: Font, ok: bool) { | |
font.allocator = allocator | |
context.allocator = font.allocator | |
font.buf, ok = os.read_entire_file(file_path) | |
if !ok do return | |
if !stbtt.InitFont(&font.info, raw_data(font.buf), stbtt.GetFontOffsetForIndex(raw_data(font.buf), 0)) do return | |
font.glyphs = make(map[rune]Glyph) | |
for r in '!'..='z' { | |
glyph_x, glyph_y, glyph_width, glyph_height: i32 | |
glyph_data := stbtt.GetCodepointBitmap(&font.info, 0, stbtt.ScaleForPixelHeight(&font.info, 50), r, &glyph_width, &glyph_height, &glyph_x, &glyph_y) | |
if glyph_data == nil do continue | |
font.glyphs[r] = Glyph{glyph_data, glyph_x, glyph_y, glyph_width, glyph_height} | |
} | |
ok = true | |
return | |
} | |
text_edit_state: edit.State | |
event_callback :: proc(event: app.Event, user_data: rawptr) { | |
#partial switch e in event { | |
case app.Event_Key_Down: | |
if app.key_down(.Control) { | |
if app.key_down(.Shift) { | |
#partial switch e.key { | |
case .Left: | |
edit.perform_command(&text_edit_state, .Select_Word_Left) | |
case .Right: | |
edit.perform_command(&text_edit_state, .Select_Word_Right) | |
} | |
} else { | |
#partial switch e.key { | |
case .Z: | |
edit.perform_command(&text_edit_state, .Undo) | |
case .Y: | |
edit.perform_command(&text_edit_state, .Redo) | |
case .X: | |
edit.perform_command(&text_edit_state, .Cut) | |
case .C: | |
edit.perform_command(&text_edit_state, .Copy) | |
case .V: | |
edit.perform_command(&text_edit_state, .Paste) | |
case .A: | |
edit.perform_command(&text_edit_state, .Select_All) | |
case .Backspace: | |
edit.perform_command(&text_edit_state, .Delete_Word_Left) | |
case .Delete: | |
edit.perform_command(&text_edit_state, .Delete_Word_Right) | |
case .Left: | |
edit.perform_command(&text_edit_state, .Word_Left) | |
case .Right: | |
edit.perform_command(&text_edit_state, .Word_Right) | |
} | |
} | |
} else { | |
if app.key_down(.Shift) { | |
#partial switch e.key { | |
case .Left: | |
edit.perform_command(&text_edit_state, .Select_Left) | |
case .Right: | |
edit.perform_command(&text_edit_state, .Select_Right) | |
case .Up: | |
edit.perform_command(&text_edit_state, .Select_Up) | |
case .Down: | |
edit.perform_command(&text_edit_state, .Select_Down) | |
} | |
} else { | |
#partial switch e.key { | |
case .Enter: | |
edit.perform_command(&text_edit_state, .New_Line) | |
case .Backspace: | |
edit.perform_command(&text_edit_state, .Backspace) | |
case .Delete: | |
edit.perform_command(&text_edit_state, .Delete) | |
case .Left: | |
edit.perform_command(&text_edit_state, .Left) | |
case .Right: | |
edit.perform_command(&text_edit_state, .Right) | |
case .Up: | |
edit.perform_command(&text_edit_state, .Up) | |
case .Down: | |
edit.perform_command(&text_edit_state, .Down) | |
} | |
} | |
} | |
case app.Event_Char: | |
switch e.char { | |
case 0x0..<0x20, 0x7F, 0x81, 0x8D, 0x8F, 0x90, 0x9D: | |
return | |
case: | |
runes := [?]rune{e.char} | |
edit.input_runes(&text_edit_state, runes[:]) | |
} | |
} | |
} | |
clipboard: []byte | |
set_clipboard :: proc(user_data: rawptr, text: string) -> (ok: bool) { | |
if clipboard != nil do delete(clipboard) | |
clipboard = make([]byte, len(text)) | |
copy(clipboard, text) | |
ok = true | |
return | |
} | |
get_clipboard :: proc(user_data: rawptr) -> (text: string, ok: bool) { | |
text = string(clipboard) | |
ok = true | |
return | |
} | |
main :: proc() { | |
assert(len(os.args) == 2, "must take the form: text_editor [font path]") | |
edit.init(&text_edit_state, context.allocator, context.allocator) | |
text_edit_state.set_clipboard = set_clipboard | |
text_edit_state.get_clipboard = get_clipboard | |
builder: strings.Builder | |
strings.builder_init(&builder) | |
edit.begin(&text_edit_state, 1, &builder) | |
app.init(event_callback = event_callback) | |
bitmap := make([]u32, app.width() * app.height()) | |
font, ok := font_create(os.args[1]) | |
assert(ok, "invalid font") | |
for !app.should_close() { | |
if app.key_pressed(.Escape) do return | |
mem.set(raw_data(bitmap), 0xDD, len(bitmap) * size_of(u32)) | |
draw_text(bitmap, font, 50, 500, (128 << 16) | (234 << 8) | 255, &text_edit_state, 5) | |
app.render(bitmap) | |
} | |
} | |
draw_rectangle :: proc "contextless" (bitmap: []u32, x, y, width, height: int, color: u32) #no_bounds_check { | |
for bitmap_y := max(y, 0); bitmap_y < y + height && bitmap_y < app.height(); bitmap_y += 1 { | |
for bitmap_x := max(x, 0); bitmap_x < x + width && bitmap_x < app.width(); bitmap_x += 1 { | |
bitmap[bitmap_y*app.width() + bitmap_x] = color | |
} | |
} | |
} | |
draw_glyph :: proc "contextless" (bitmap: []u32, font: Font, x, y: int, color: u32, glyph: rune) #no_bounds_check { | |
glyph_bitmap := font.glyphs[glyph] | |
glyph_x, glyph_y: int | |
glyph_width := int(glyph_bitmap.width) | |
glyph_height := int(glyph_bitmap.height) | |
for bitmap_y := max(y, 0); bitmap_y < y + int(glyph_bitmap.height) && bitmap_y < app.height(); bitmap_y += 1 { | |
for bitmap_x := max(x, 0); bitmap_x < x + int(glyph_bitmap.width) && bitmap_x < app.width(); bitmap_x += 1 { | |
pixel := glyph_bitmap.data[(glyph_height-glyph_y-1)*glyph_width + glyph_x] | |
if pixel != 0 do bitmap[bitmap_y*app.width() + bitmap_x] = color | |
glyph_x += 1 | |
} | |
glyph_x = 0 | |
glyph_y += 1 | |
} | |
return | |
} | |
draw_text :: proc(bitmap: []u32, font: Font, x, y: int, color: u32, text: ^edit.State, spacing: int) { | |
x_offset, y_offset: int | |
str := string(text.builder.buf[:]) | |
height := 50 | |
selection_color := u32((255 << 16) | (149 << 8) | 43) | |
drew_cursor := false | |
for glyph, glyph_index in str { | |
draw_highlight := (glyph_index >= text.selection[0] && glyph_index < text.selection[1]) || (glyph_index >= text.selection[1] && glyph_index < text.selection[0]) | |
if glyph_index == text.selection[0] { | |
draw_rectangle(bitmap, x + x_offset - 4, y + y_offset, 2, height, 0) | |
drew_cursor = true | |
} | |
switch glyph { | |
case ' ': | |
width := spacing * 4 | |
if draw_highlight { | |
draw_rectangle(bitmap, x + x_offset, y + y_offset, width, height, selection_color) | |
} | |
x_offset += width | |
case '\t': | |
width := spacing * 16 | |
if draw_highlight { | |
draw_rectangle(bitmap, x + x_offset, y + y_offset, width, height, selection_color) | |
} | |
x_offset += width | |
case '\n': | |
x_offset = 0 | |
y_offset -= 50 | |
case: | |
glyph_y_offset: int | |
switch glyph { | |
case 'g', 'p', 'q': | |
glyph_y_offset = -7 | |
case 'j', 'y': | |
glyph_y_offset = -10 | |
case 'Q': | |
glyph_y_offset = -5 | |
case '\'': | |
glyph_y_offset = 20 | |
} | |
glyph_bitmap := font.glyphs[glyph] | |
glyph_width := int(glyph_bitmap.width) | |
width := glyph_width + spacing | |
if draw_highlight { | |
draw_rectangle(bitmap, x + x_offset, y + y_offset, width, height, selection_color) | |
} | |
draw_glyph(bitmap, font, x + x_offset, y + y_offset + glyph_y_offset, color, glyph) | |
x_offset += width | |
if x_offset > app.width() - 100 { | |
x_offset = 0 | |
y_offset -= height | |
} | |
} | |
} | |
if !drew_cursor { | |
draw_rectangle(bitmap, x + x_offset - 4, y + y_offset, 2, height, 0) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment