text_edit

Code

text_edit.odin ¶
227 linesSource

1package text_edit_example
2
3/*********************************************************************
4                        SINGLE LINE TEXT FIELD
5                            (using raylib)
6
7 A minimal editable text field. `core:text/edit` supplies the editing
8 logic -- caret movement, selection, undo/redo, clipboard -- and raylib
9 supplies the window, the input events and the drawing.
10
11 You can
12 * Type to insert text
13 * Click to place the caret
14 * Press <Left>/<Right> to move the caret, hold <Shift> to select
15 * Press <Ctrl+Left>/<Ctrl+Right> to move a whole word
16 * Press <Home>/<End> to jump to either end
17 * Press <Backspace>/<Delete> to erase
18 * Press <Ctrl+A> to select all, <Ctrl+C>/<Ctrl+X>/<Ctrl+V> to copy/cut/paste
19 * Press <Ctrl+Z> to undo, <Ctrl+Y> to redo
20 * Press <Esc> to close the window
21
22 The field moves the caret by grapheme cluster, so a letter followed by
23 a combining mark counts as one character even though it is two runes.
24 Paste something like "e" + U+0301 in to see it: the caret steps over
25 both at once. raylib's default font has no glyph for the combining
26 mark or most non-ASCII letters, so those draw as missing-glyph boxes,
27 but the caret and selection still land in the right place.
28
29**********************************************************************/
30
31import "core:strings"
32import "core:text/edit"
33import "core:unicode/utf8"
34import rl "vendor:raylib"
35
36WINDOW_WIDTH  :: 800
37WINDOW_HEIGHT :: 200
38
39FONT_SIZE      :: 30
40FONT_SPACING   :: 2
41HINT_FONT_SIZE :: 20
42
43FIELD_PADDING :: 10
44FIELD_X       :: 40
45FIELD_Y       :: 80
46FIELD_W       :: WINDOW_WIDTH - 2 * FIELD_X
47FIELD_H       :: FONT_SIZE + 2 * FIELD_PADDING
48
49TEXT_X :: FIELD_X + FIELD_PADDING
50TEXT_Y :: FIELD_Y + FIELD_PADDING
51HINT_Y :: 40
52
53CARET_WIDTH      :: 2
54CARET_BLINK_RATE :: 0.5
55
56COLOR_BACKGROUND :: rl.Color{ 30,  30,  35, 255}
57COLOR_FIELD      :: rl.Color{ 20,  20,  24, 255}
58COLOR_BORDER     :: rl.Color{ 90,  90, 100, 255}
59COLOR_SELECTION  :: rl.Color{ 60,  90, 150, 255}
60COLOR_HINT       :: rl.Color{140, 140, 150, 255}
61
62Key_Binding :: struct {
63	key:   rl.KeyboardKey,
64	ctrl:  bool,
65	shift: bool,
66	cmd:   edit.Command,
67}
68
69// `core:text/edit` has no opinion on which key does what, so the example picks
70// the usual bindings. The modifiers have to match exactly, which is what keeps
71// <Ctrl+Left> from also triggering the plain <Left> binding.
72KEY_BINDINGS :: []Key_Binding{
73	{.LEFT,      false, false, .Left},
74	{.LEFT,      false, true,  .Select_Left},
75	{.LEFT,      true,  false, .Word_Left},
76	{.LEFT,      true,  true,  .Select_Word_Left},
77	{.RIGHT,     false, false, .Right},
78	{.RIGHT,     false, true,  .Select_Right},
79	{.RIGHT,     true,  false, .Word_Right},
80	{.RIGHT,     true,  true,  .Select_Word_Right},
81	{.HOME,      false, false, .Start},
82	{.HOME,      false, true,  .Select_Start},
83	{.END,       false, false, .End},
84	{.END,       false, true,  .Select_End},
85	{.BACKSPACE, false, false, .Backspace},
86	{.BACKSPACE, true,  false, .Delete_Word_Left},
87	{.DELETE,    false, false, .Delete},
88	{.DELETE,    true,  false, .Delete_Word_Right},
89	{.A,         true,  false, .Select_All},
90	{.C,         true,  false, .Copy},
91	{.X,         true,  false, .Cut},
92	{.V,         true,  false, .Paste},
93	{.Z,         true,  false, .Undo},
94	{.Y,         true,  false, .Redo},
95}
96
97main :: proc() {
98	rl.InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "core:text/edit text field")
99	defer rl.CloseWindow()
100	rl.SetTargetFPS(60)
101
102	font := rl.GetFontDefault()
103
104	// `core:text/edit` never owns the text: it edits a `strings.Builder` that
105	// the caller keeps. Anything can read the current contents at any time by
106	// looking at the builder.
107	builder := strings.builder_make()
108	defer strings.builder_destroy(&builder)
109	strings.write_string(&builder, "Edit me!")
110
111	state: edit.State
112	edit.init(&state, context.allocator, context.allocator)
113	defer edit.destroy(&state)
114
115	// `setup_once` binds the builder and keeps the selection across frames.
116	// (`edit.begin`/`edit.end` are for immediate mode GUIs, where a field is
117	// only the focused one for the duration of a frame.)
118	edit.setup_once(&state, &builder)
119
120	// Without this the caret moves by codepoint, which would step into the
121	// middle of a multi-rune character.
122	state.translate_by_grapheme = true
123
124	state.set_clipboard = proc(_: rawptr, text: string) -> bool {
125		rl.SetClipboardText(strings.clone_to_cstring(text, context.temp_allocator))
126		return true
127	}
128	state.get_clipboard = proc(_: rawptr) -> (text: string, ok: bool) {
129		cstr := rl.GetClipboardText()
130		return string(cstr), cstr != nil
131	}
132
133	for !rl.WindowShouldClose() {
134		defer free_all(context.temp_allocator)
135
136		handle_input(&state, font)
137
138		// Read the text only once this frame's editing is done. Reading it any
139		// earlier leaves a slice of the buffer as it used to be, which stops
140		// matching the selection as soon as anything is typed.
141		text := strings.to_string(builder)
142
143		draw(&state, font, text)
144	}
145}
146
147handle_input :: proc(state: ^edit.State, font: rl.Font) {
148	// The undo system groups edits that happen close together in time, so it
149	// needs to be told what "now" is before any editing happens.
150	edit.update_time(state)
151
152	for r := rl.GetCharPressed(); r != 0; r = rl.GetCharPressed() {
153		edit.input_rune(state, r)
154	}
155
156	ctrl  := rl.IsKeyDown(.LEFT_CONTROL) || rl.IsKeyDown(.RIGHT_CONTROL)
157	shift := rl.IsKeyDown(.LEFT_SHIFT)   || rl.IsKeyDown(.RIGHT_SHIFT)
158
159	for binding in KEY_BINDINGS {
160		// `IsKeyPressedRepeat` is what makes a held down key keep firing.
161		pressed := rl.IsKeyPressed(binding.key) || rl.IsKeyPressedRepeat(binding.key)
162		if pressed && binding.ctrl == ctrl && binding.shift == shift {
163			edit.perform_command(state, binding.cmd)
164		}
165	}
166
167	if rl.IsMouseButtonPressed(.LEFT) {
168		caret := caret_at(font, strings.to_string(state.builder^), rl.GetMousePosition().x)
169		state.selection = {caret, caret}
170	}
171}
172
173draw :: proc(state: ^edit.State, font: rl.Font, text: string) {
174	rl.BeginDrawing()
175	defer rl.EndDrawing()
176
177	rl.ClearBackground(COLOR_BACKGROUND)
178
179	field := rl.Rectangle{FIELD_X, FIELD_Y, FIELD_W, FIELD_H}
180	rl.DrawRectangleRec(field, COLOR_FIELD)
181	rl.DrawRectangleLinesEx(field, 2, COLOR_BORDER)
182
183	// The selection is drawn behind the text. `sorted_selection` puts the two
184	// ends in left to right order, because `selection[0]` is the moving end and
185	// can sit on either side of the anchor.
186	if lo, hi := edit.sorted_selection(state); lo != hi {
187		lo_x := TEXT_X + text_width(font, text[:lo])
188		hi_x := TEXT_X + text_width(font, text[:hi])
189		rl.DrawRectangleRec({lo_x, TEXT_Y, hi_x - lo_x, FONT_SIZE}, COLOR_SELECTION)
190	}
191
192	rl.DrawTextEx(font, strings.clone_to_cstring(text, context.temp_allocator),
193	              {TEXT_X, TEXT_Y}, FONT_SIZE, FONT_SPACING, rl.RAYWHITE)
194
195	if int(rl.GetTime() / CARET_BLINK_RATE) % 2 == 0 {
196		caret_x := TEXT_X + text_width(font, text[:state.selection[0]])
197		rl.DrawRectangleRec({caret_x, TEXT_Y, CARET_WIDTH, FONT_SIZE}, rl.RAYWHITE)
198	}
199
200	rl.DrawTextEx(font, "Type to edit. Arrows move, Shift selects, Ctrl+Z undoes.",
201	              {FIELD_X, HINT_Y}, HINT_FONT_SIZE, FONT_SPACING, COLOR_HINT)
202}
203
204text_width :: proc(font: rl.Font, text: string) -> f32 {
205	cstr := strings.clone_to_cstring(text, context.temp_allocator)
206	return rl.MeasureTextEx(font, cstr, FONT_SIZE, FONT_SPACING).x
207}
208
209// Find the grapheme boundary closest to `mouse_x`. Walking the grapheme clusters
210// rather than the bytes is what stops a click from dropping the caret inside a
211// multi-rune character.
212//
213// Every candidate is measured from the start of the text rather than by adding
214// up cluster widths, because the spacing between glyphs belongs to neither of
215// the two clusters it sits between.
216caret_at :: proc(font: rl.Font, text: string, mouse_x: f32) -> (caret: int) {
217	best := abs(TEXT_X - mouse_x)
218
219	it := utf8.decode_grapheme_iterator_make(text)
220	for _, grapheme in utf8.decode_grapheme_iterate(&it) {
221		end := grapheme.byte_index + len(grapheme.text)
222		if distance := abs(TEXT_X + text_width(font, text[:end]) - mouse_x); distance < best {
223			caret, best = end, distance
224		}
225	}
226	return
227}

Declarations Used 42