From a7b0073b31ef0211858fcaab0a9b8f1c3e94bd83 Mon Sep 17 00:00:00 2001 From: KVBA-dev Date: Sun, 22 Feb 2026 17:20:28 +0100 Subject: [PATCH 1/4] core:text/edit example --- .github/workflows/check.yml | 2 + text_edit/main.odin | 191 ++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 text_edit/main.odin diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 73b767d..af09535 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -68,6 +68,8 @@ jobs: odin check math/noise/draw_texture $FLAGS odin check math/rand/markov $FLAGS + odin check text_edit $FLAGS + odin check raylib/game_of_life $FLAGS odin check raylib/log $FLAGS odin check raylib/microui $FLAGS diff --git a/text_edit/main.odin b/text_edit/main.odin new file mode 100644 index 0000000..b8cb00a --- /dev/null +++ b/text_edit/main.odin @@ -0,0 +1,191 @@ +package main + +/* + core:text/edit example + + Ths core package provides procedures for implementing + famously difficult text input fields. This is a simple example + showcasing how to create a single line text edit field. +*/ + + // Using the packages below for: +import rl "vendor:raylib" // - rendering and keyboard input +import "core:text/edit" // - text editing logic +import "core:strings" // - string builder and string to cstring conversion + +FONT_SIZE :: 50 + +/* HELPER PROCEDURES */ + +/* NOT the same as IsKeyDown */ +IsKeyHeld :: proc(key: rl.KeyboardKey) -> bool { + return rl.IsKeyPressed(key) || rl.IsKeyPressedRepeat(key) +} + +IsControlDown :: proc() -> bool { + return rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) +} + +IsShiftDown :: proc() -> bool { + return rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) +} + +main :: proc() { + /* Creating a window. + Using "defer" just after that to not forget + to close the window! */ + rl.InitWindow(1024, 800, "core:text/edit example") + defer rl.CloseWindow() + + /* Initialising string builder + for dynamically creating strings + in runtime. */ + builder: strings.Builder + strings.builder_init(&builder) + + /* This is the edit state. + It stores information about current selection, caret position, + undo and redo lists, and clipboard interface. */ + state: edit.State + + /* Initialise the edit state, we pass default allocator + for undo data */ + edit.init(&state, context.allocator, context.allocator) + defer edit.destroy(&state) // don't forget to destroy at the end! + + /* Before editing we need to tell the state that this is what we're editing. + Note that we're NOT calling it every frame (as in every loop iteration), + although the documentation might suggest otherwise. */ + edit.begin(&state, 1, &builder) + defer edit.end(&state) // again, don't forget to end! + + /* Main loop */ + for !rl.WindowShouldClose() { + /* Update the time for the state. + Useful with undo operation. + The state struct contains the information about the time since the last edit. + If it exceeds the timeout time, it is then pushed to the undo list. + Default timeout is 300 ms. */ + edit.update_time(&state) + + if !IsControlDown() { + /* Normal input */ + c := rl.GetCharPressed() + + /* NOTE: this skips the newline characters. + Depending on your goals (e.g. multiline field), + you might want to change this condition to something else */ + if c >= rune(' ') { + /* Input the rune into the state */ + edit.input_rune(&state, c) + } + } else { // Handle Ctrl+key combinations + switch { + case rl.IsKeyPressed(.Z): // undo + edit.perform_command(&state, .Undo) + case rl.IsKeyPressed(.Y): // redo + edit.perform_command(&state, .Redo) + /* WARN: the following commands require that you set + the clipboard procedures in the state struct. + In current implementation they don't work. */ + case rl.IsKeyPressed(.C): // copy + edit.perform_command(&state, .Copy) + case rl.IsKeyPressed(.V): // paste + edit.perform_command(&state, .Paste) + case rl.IsKeyPressed(.X): // cut + edit.perform_command(&state, .Cut) + } + } + + /* Movement in the text field + - Shift + arrow expands the selection in the direction of the arrow + - Ctrl + arrow moves the caret to the next word + - Shift + Ctrl + arrow combines both operations */ + if IsKeyHeld(.LEFT) { + cmd := edit.Command.Left + if (rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT)) && (rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL)) { + cmd = .Select_Word_Left + } else if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) { + cmd = .Word_Left + } else if rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) { + cmd = .Select_Left + } + edit.perform_command(&state, cmd) + } + + if IsKeyHeld(.RIGHT) { + cmd := edit.Command.Right + if (rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT)) && (rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL)) { + cmd = .Select_Word_Right + } else if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) { + cmd = .Word_Right + } else if rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) { + cmd = .Select_Right + } + edit.perform_command(&state, cmd) + } + + /* Deleting characters. `Backspace` deletes before the caret, `Delete` after + - Ctrl + key deletes entire word in the respective direction */ + if IsKeyHeld(.BACKSPACE) { + cmd := edit.Command.Backspace + if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) { + cmd = .Delete_Word_Left + } + edit.perform_command(&state, cmd) + } + + if IsKeyHeld(.DELETE) { + cmd := edit.Command.Delete + if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) { + cmd = .Delete_Word_Right + } + edit.perform_command(&state, cmd) + } + + /* Our result string */ + str := strings.to_string(builder) + + /* raylib operates on cstrings, so we need to convert + our string from builder to a cstring */ + cstr := strings.to_cstring(&builder) + + /* This will be used for determining the position of the caret on screen */ + substr := strings.clone_to_cstring(str[:state.selection[0]], context.temp_allocator) + caret_x := rl.MeasureText(substr, FONT_SIZE) + + /* Calculate selection size */ + selection_str := str[:state.selection[1]] + selection_cstr := strings.clone_to_cstring(selection_str, context.temp_allocator) + selection_x := rl.MeasureText(selection_cstr, FONT_SIZE) + + /* The edit state stores the selection information as a 2-element array. + 1st element (index 0) is the current caret position. + 2nd element (index 1) marks the end of the selection. + If both elements are equal, then there's no selection. + Note that either element can be bigger than the other. This means that + we need to choose the lower element to start from, and then calculate + the width as the absolute value of the difference. */ + + /* Main drawing section + Without it your program won't run! Keep that in mind if you're learning raylib. */ + rl.BeginDrawing() + rl.ClearBackground(rl.WHITE) + + rl.DrawText("core:text/edit example", 20, 20, 20, rl.RED) + + rl.DrawRectangle(0, 340, 1024, 70, rl.LIGHTGRAY) // background + if edit.has_selection(&state) { // selection + rl.DrawRectangle(caret_x if caret_x < selection_x else selection_x, + 340, + abs(selection_x - caret_x), + 70, + rl.SKYBLUE, + ) + } + rl.DrawText(cstr, 0, 350, FONT_SIZE, rl.RED) // text + rl.DrawLine(caret_x, 350, caret_x, 400, rl.RED) // caret + + rl.EndDrawing() + } +} From a9a6eca7fc3c267795eec806cedad9e724e6f3c1 Mon Sep 17 00:00:00 2001 From: KVBA-dev Date: Sun, 22 Feb 2026 17:37:26 +0100 Subject: [PATCH 2/4] fixed typos, used the helper procedures whenever applicable --- text_edit/main.odin | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/text_edit/main.odin b/text_edit/main.odin index b8cb00a..10bdfd4 100644 --- a/text_edit/main.odin +++ b/text_edit/main.odin @@ -3,7 +3,7 @@ package main /* core:text/edit example - Ths core package provides procedures for implementing + This core package provides procedures for implementing famously difficult text input fields. This is a simple example showcasing how to create a single line text edit field. */ @@ -32,7 +32,7 @@ IsShiftDown :: proc() -> bool { main :: proc() { /* Creating a window. - Using "defer" just after that to not forget + Using "defer" just after that to remember to close the window! */ rl.InitWindow(1024, 800, "core:text/edit example") defer rl.CloseWindow() @@ -45,7 +45,7 @@ main :: proc() { /* This is the edit state. It stores information about current selection, caret position, - undo and redo lists, and clipboard interface. */ + undo and redo arrays, and clipboard interface. */ state: edit.State /* Initialise the edit state, we pass default allocator @@ -61,10 +61,9 @@ main :: proc() { /* Main loop */ for !rl.WindowShouldClose() { - /* Update the time for the state. - Useful with undo operation. + /* Update the time for the state. Useful with undo operation. The state struct contains the information about the time since the last edit. - If it exceeds the timeout time, it is then pushed to the undo list. + If it exceeds the timeout, it is then pushed to the undo array. Default timeout is 300 ms. */ edit.update_time(&state) @@ -103,11 +102,11 @@ main :: proc() { - Shift + Ctrl + arrow combines both operations */ if IsKeyHeld(.LEFT) { cmd := edit.Command.Left - if (rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT)) && (rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL)) { + if IsShiftDown() && IsControlDown() { cmd = .Select_Word_Left - } else if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) { + } else if IsControlDown() { cmd = .Word_Left - } else if rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) { + } else if IsShiftDown() { cmd = .Select_Left } edit.perform_command(&state, cmd) @@ -115,11 +114,11 @@ main :: proc() { if IsKeyHeld(.RIGHT) { cmd := edit.Command.Right - if (rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT)) && (rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL)) { + if IsShiftDown() && IsControlDown() { cmd = .Select_Word_Right - } else if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) { + } else if IsControlDown() { cmd = .Word_Right - } else if rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) { + } else if IsShiftDown() { cmd = .Select_Right } edit.perform_command(&state, cmd) @@ -129,7 +128,7 @@ main :: proc() { - Ctrl + key deletes entire word in the respective direction */ if IsKeyHeld(.BACKSPACE) { cmd := edit.Command.Backspace - if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) { + if IsControlDown() { cmd = .Delete_Word_Left } edit.perform_command(&state, cmd) @@ -137,7 +136,7 @@ main :: proc() { if IsKeyHeld(.DELETE) { cmd := edit.Command.Delete - if rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) { + if IsControlDown() { cmd = .Delete_Word_Right } edit.perform_command(&state, cmd) @@ -150,7 +149,9 @@ main :: proc() { our string from builder to a cstring */ cstr := strings.to_cstring(&builder) - /* This will be used for determining the position of the caret on screen */ + /* This will be used for determining the position of the caret on screen. + Note that for this demo we're using MeasureText, because we use the default + raylib font. If you want to use a different font, look into MeasureTextEx.*/ substr := strings.clone_to_cstring(str[:state.selection[0]], context.temp_allocator) caret_x := rl.MeasureText(substr, FONT_SIZE) @@ -176,12 +177,11 @@ main :: proc() { rl.DrawRectangle(0, 340, 1024, 70, rl.LIGHTGRAY) // background if edit.has_selection(&state) { // selection - rl.DrawRectangle(caret_x if caret_x < selection_x else selection_x, - 340, + rl.DrawRectangle(caret_x if caret_x < selection_x else selection_x, // ternary expression! equivalent to + 340, // caret_x < selection_x ? caret_x : selection_x abs(selection_x - caret_x), 70, - rl.SKYBLUE, - ) + rl.SKYBLUE) } rl.DrawText(cstr, 0, 350, FONT_SIZE, rl.RED) // text rl.DrawLine(caret_x, 350, caret_x, 400, rl.RED) // caret From 4ac446cf249de9ea5f85a086ae65132d71ab8035 Mon Sep 17 00:00:00 2001 From: KVBA-dev Date: Thu, 16 Apr 2026 12:38:12 +0200 Subject: [PATCH 3/4] fixed comments, removed all occurences of defer --- text_edit/main.odin | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/text_edit/main.odin b/text_edit/main.odin index 10bdfd4..4982d05 100644 --- a/text_edit/main.odin +++ b/text_edit/main.odin @@ -4,8 +4,8 @@ package main core:text/edit example This core package provides procedures for implementing - famously difficult text input fields. This is a simple example - showcasing how to create a single line text edit field. + text input fields. This is a simple example showcasing + how to create a single line text edit field. */ // Using the packages below for: @@ -31,11 +31,8 @@ IsShiftDown :: proc() -> bool { } main :: proc() { - /* Creating a window. - Using "defer" just after that to remember - to close the window! */ + /* Creating a window. */ rl.InitWindow(1024, 800, "core:text/edit example") - defer rl.CloseWindow() /* Initialising string builder for dynamically creating strings @@ -51,13 +48,11 @@ main :: proc() { /* Initialise the edit state, we pass default allocator for undo data */ edit.init(&state, context.allocator, context.allocator) - defer edit.destroy(&state) // don't forget to destroy at the end! /* Before editing we need to tell the state that this is what we're editing. Note that we're NOT calling it every frame (as in every loop iteration), although the documentation might suggest otherwise. */ edit.begin(&state, 1, &builder) - defer edit.end(&state) // again, don't forget to end! /* Main loop */ for !rl.WindowShouldClose() { @@ -169,7 +164,7 @@ main :: proc() { the width as the absolute value of the difference. */ /* Main drawing section - Without it your program won't run! Keep that in mind if you're learning raylib. */ + Keep that in mind if you're learning raylib, because without it, your program will freeze! */ rl.BeginDrawing() rl.ClearBackground(rl.WHITE) @@ -188,4 +183,13 @@ main :: proc() { rl.EndDrawing() } + /* We call this procedure to signal that we're done editing + that input field */ + edit.end(&state) + + /* At the end of the program, release the resources allocated at the start */ + edit.destroy(&state) + + /* Finally, close window */ + rl.CloseWindow() } From fe24e0ce66768a51e644513c60455bc16de0178c Mon Sep 17 00:00:00 2001 From: KVBA-dev Date: Thu, 16 Apr 2026 12:45:20 +0200 Subject: [PATCH 4/4] updated helper procedure names to match the style convention --- text_edit/main.odin | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/text_edit/main.odin b/text_edit/main.odin index 4982d05..aff591f 100644 --- a/text_edit/main.odin +++ b/text_edit/main.odin @@ -18,15 +18,15 @@ FONT_SIZE :: 50 /* HELPER PROCEDURES */ /* NOT the same as IsKeyDown */ -IsKeyHeld :: proc(key: rl.KeyboardKey) -> bool { +is_key_held :: proc(key: rl.KeyboardKey) -> bool { return rl.IsKeyPressed(key) || rl.IsKeyPressedRepeat(key) } -IsControlDown :: proc() -> bool { +is_ctrl_down :: proc() -> bool { return rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL) } -IsShiftDown :: proc() -> bool { +is_shift_down :: proc() -> bool { return rl.IsKeyDown(.LEFT_SHIFT) || rl.IsKeyDown(.RIGHT_SHIFT) } @@ -62,7 +62,7 @@ main :: proc() { Default timeout is 300 ms. */ edit.update_time(&state) - if !IsControlDown() { + if !is_ctrl_down() { /* Normal input */ c := rl.GetCharPressed() @@ -95,25 +95,25 @@ main :: proc() { - Shift + arrow expands the selection in the direction of the arrow - Ctrl + arrow moves the caret to the next word - Shift + Ctrl + arrow combines both operations */ - if IsKeyHeld(.LEFT) { + if is_key_held(.LEFT) { cmd := edit.Command.Left - if IsShiftDown() && IsControlDown() { + if is_shift_down() && is_ctrl_down() { cmd = .Select_Word_Left - } else if IsControlDown() { + } else if is_ctrl_down() { cmd = .Word_Left - } else if IsShiftDown() { + } else if is_shift_down() { cmd = .Select_Left } edit.perform_command(&state, cmd) } - if IsKeyHeld(.RIGHT) { + if is_key_held(.RIGHT) { cmd := edit.Command.Right - if IsShiftDown() && IsControlDown() { + if is_shift_down() && is_ctrl_down() { cmd = .Select_Word_Right - } else if IsControlDown() { + } else if is_ctrl_down() { cmd = .Word_Right - } else if IsShiftDown() { + } else if is_shift_down() { cmd = .Select_Right } edit.perform_command(&state, cmd) @@ -121,17 +121,17 @@ main :: proc() { /* Deleting characters. `Backspace` deletes before the caret, `Delete` after - Ctrl + key deletes entire word in the respective direction */ - if IsKeyHeld(.BACKSPACE) { + if is_key_held(.BACKSPACE) { cmd := edit.Command.Backspace - if IsControlDown() { + if is_ctrl_down() { cmd = .Delete_Word_Left } edit.perform_command(&state, cmd) } - if IsKeyHeld(.DELETE) { + if is_key_held(.DELETE) { cmd := edit.Command.Delete - if IsControlDown() { + if is_ctrl_down() { cmd = .Delete_Word_Right } edit.perform_command(&state, cmd)