Skip to content

Instantly share code, notes, and snippets.

@pJotoro
Created November 23, 2023 02:00
Show Gist options
  • Save pJotoro/fae7bc4ea3c551d40d2e8d5b67c119d4 to your computer and use it in GitHub Desktop.
Save pJotoro/fae7bc4ea3c551d40d2e8d5b67c119d4 to your computer and use it in GitHub Desktop.
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