Browse Source

Merge pull request #2969 from Skytrias/Skytrias-text-edit-additions

`core:text/edit` Add setup_once and clear_all, clean up old code and add a few comments
gingerBill 1 year ago
parent
commit
34065865a0
1 changed files with 75 additions and 29 deletions
  1. 75 29
      core/text/edit/text_edit.odin

+ 75 - 29
core/text/edit/text_edit.odin

@@ -63,7 +63,9 @@ Translation :: enum u32 {
 	Soft_Line_End,
 	Soft_Line_End,
 }
 }
 
 
-
+// init the state to some timeout and set the respective allocators
+// - undo_state_allocator dictates the dynamic undo|redo arrays allocators
+// - undo_text_allocator is the allocator which allocates strings only
 init :: proc(s: ^State, undo_text_allocator, undo_state_allocator: runtime.Allocator, undo_timeout := DEFAULT_UNDO_TIMEOUT) {
 init :: proc(s: ^State, undo_text_allocator, undo_state_allocator: runtime.Allocator, undo_timeout := DEFAULT_UNDO_TIMEOUT) {
 	s.undo_timeout = undo_timeout
 	s.undo_timeout = undo_timeout
 
 
@@ -74,6 +76,7 @@ init :: proc(s: ^State, undo_text_allocator, undo_state_allocator: runtime.Alloc
 	s.redo.allocator = undo_state_allocator
 	s.redo.allocator = undo_state_allocator
 }
 }
 
 
+// clear undo|redo strings and delete their stacks
 destroy :: proc(s: ^State) {
 destroy :: proc(s: ^State) {
 	undo_clear(s, &s.undo)
 	undo_clear(s, &s.undo)
 	undo_clear(s, &s.redo)
 	undo_clear(s, &s.redo)
@@ -82,7 +85,6 @@ destroy :: proc(s: ^State) {
 	s.builder = nil
 	s.builder = nil
 }
 }
 
 
-
 // Call at the beginning of each frame
 // Call at the beginning of each frame
 begin :: proc(s: ^State, id: u64, builder: ^strings.Builder) {
 begin :: proc(s: ^State, id: u64, builder: ^strings.Builder) {
 	assert(builder != nil)
 	assert(builder != nil)
@@ -92,11 +94,7 @@ begin :: proc(s: ^State, id: u64, builder: ^strings.Builder) {
 	s.id = id
 	s.id = id
 	s.selection = {len(builder.buf), 0}
 	s.selection = {len(builder.buf), 0}
 	s.builder = builder
 	s.builder = builder
-	s.current_time = time.tick_now()
-	if s.undo_timeout <= 0 {
-		s.undo_timeout = DEFAULT_UNDO_TIMEOUT
-	}
-	set_text(s, string(s.builder.buf[:]))
+	update_time(s)
 	undo_clear(s, &s.undo)
 	undo_clear(s, &s.undo)
 	undo_clear(s, &s.redo)
 	undo_clear(s, &s.redo)
 }
 }
@@ -107,12 +105,37 @@ end :: proc(s: ^State) {
 	s.builder = nil
 	s.builder = nil
 }
 }
 
 
-set_text :: proc(s: ^State, text: string) {
-	strings.builder_reset(s.builder)
-	strings.write_string(s.builder, text)
+// update current time so "insert" can check for timeouts
+update_time :: proc(s: ^State) {
+	s.current_time = time.tick_now()
+	if s.undo_timeout <= 0 {
+		s.undo_timeout = DEFAULT_UNDO_TIMEOUT
+	}
+}
+
+// setup the builder, selection and undo|redo state once allowing to retain selection
+setup_once :: proc(s: ^State, builder: ^strings.Builder) {
+	s.builder = builder
+	s.selection = { len(builder.buf), 0 }
+	undo_clear(s, &s.undo)
+	undo_clear(s, &s.redo)
 }
 }
 
 
+// returns true when the builder had content to be cleared
+// clear builder&selection and the undo|redo stacks
+clear_all :: proc(s: ^State) -> (cleared: bool) {
+	if s.builder != nil && len(s.builder.buf) > 0 {
+		clear(&s.builder.buf)
+		s.selection = {}
+		cleared = true
+	}
 
 
+	undo_clear(s, &s.undo)
+	undo_clear(s, &s.redo)
+	return
+}
+
+// push current text state to the wanted undo|redo stack
 undo_state_push :: proc(s: ^State, undo: ^[dynamic]^Undo_State) -> mem.Allocator_Error {
 undo_state_push :: proc(s: ^State, undo: ^[dynamic]^Undo_State) -> mem.Allocator_Error {
 	text := string(s.builder.buf[:])
 	text := string(s.builder.buf[:])
 	item := (^Undo_State)(mem.alloc(size_of(Undo_State) + len(text), align_of(Undo_State), s.undo_text_allocator) or_return)
 	item := (^Undo_State)(mem.alloc(size_of(Undo_State) + len(text), align_of(Undo_State), s.undo_text_allocator) or_return)
@@ -125,18 +148,21 @@ undo_state_push :: proc(s: ^State, undo: ^[dynamic]^Undo_State) -> mem.Allocator
 	return nil
 	return nil
 }
 }
 
 
+// pop undo|redo state - push to redo|undo - set selection & text
 undo :: proc(s: ^State, undo, redo: ^[dynamic]^Undo_State) {
 undo :: proc(s: ^State, undo, redo: ^[dynamic]^Undo_State) {
 	if len(undo) > 0 {
 	if len(undo) > 0 {
 		undo_state_push(s, redo)
 		undo_state_push(s, redo)
 		item := pop(undo)
 		item := pop(undo)
 		s.selection = item.selection
 		s.selection = item.selection
 		#no_bounds_check {
 		#no_bounds_check {
-			set_text(s, string(item.text[:item.len]))
+			strings.builder_reset(s.builder)
+			strings.write_string(s.builder, string(item.text[:item.len]))
 		}
 		}
 		free(item, s.undo_text_allocator)
 		free(item, s.undo_text_allocator)
 	}
 	}
 }
 }
 
 
+// iteratively clearn the undo|redo stack and free each allocated text state
 undo_clear :: proc(s: ^State, undo: ^[dynamic]^Undo_State) {
 undo_clear :: proc(s: ^State, undo: ^[dynamic]^Undo_State) {
 	for len(undo) > 0 {
 	for len(undo) > 0 {
 		item := pop(undo)
 		item := pop(undo)
@@ -144,6 +170,7 @@ undo_clear :: proc(s: ^State, undo: ^[dynamic]^Undo_State) {
 	}
 	}
 }
 }
 
 
+// clear redo stack and check if the undo timeout gets hit
 undo_check :: proc(s: ^State) {
 undo_check :: proc(s: ^State) {
 	undo_clear(s, &s.redo)
 	undo_clear(s, &s.redo)
 	if time.tick_diff(s.last_edit_time, s.current_time) > s.undo_timeout {
 	if time.tick_diff(s.last_edit_time, s.current_time) > s.undo_timeout {
@@ -152,8 +179,7 @@ undo_check :: proc(s: ^State) {
 	s.last_edit_time = s.current_time
 	s.last_edit_time = s.current_time
 }
 }
 
 
-
-
+// insert text into the edit state - deletes the current selection
 input_text :: proc(s: ^State, text: string) {
 input_text :: proc(s: ^State, text: string) {
 	if len(text) == 0 {
 	if len(text) == 0 {
 		return
 		return
@@ -166,6 +192,7 @@ input_text :: proc(s: ^State, text: string) {
 	s.selection = {offset, offset}
 	s.selection = {offset, offset}
 }
 }
 
 
+// insert slice of runes into the edit state - deletes the current selection
 input_runes :: proc(s: ^State, text: []rune) {
 input_runes :: proc(s: ^State, text: []rune) {
 	if len(text) == 0 {
 	if len(text) == 0 {
 		return
 		return
@@ -182,43 +209,55 @@ input_runes :: proc(s: ^State, text: []rune) {
 	s.selection = {offset, offset}
 	s.selection = {offset, offset}
 }
 }
 
 
+// insert a single rune into the edit state - deletes the current selection
+input_rune :: proc(s: ^State, r: rune) {
+	if has_selection(s) {
+		selection_delete(s)
+	}
+	offset := s.selection[0]
+	b, w := utf8.encode_rune(r)
+	insert(s, offset, string(b[:w]))
+	offset += w
+	s.selection = {offset, offset}
+}
 
 
+// insert a single rune into the edit state - deletes the current selection
 insert :: proc(s: ^State, at: int, text: string) {
 insert :: proc(s: ^State, at: int, text: string) {
 	undo_check(s)
 	undo_check(s)
 	inject_at(&s.builder.buf, at, text)
 	inject_at(&s.builder.buf, at, text)
 }
 }
 
 
+// remove the wanted range withing, usually the selection within byte indices
 remove :: proc(s: ^State, lo, hi: int) {
 remove :: proc(s: ^State, lo, hi: int) {
 	undo_check(s)
 	undo_check(s)
 	remove_range(&s.builder.buf, lo, hi)
 	remove_range(&s.builder.buf, lo, hi)
 }
 }
 
 
-
-
+// true if selection head and tail dont match and form a selection of multiple characters
 has_selection :: proc(s: ^State) -> bool {
 has_selection :: proc(s: ^State) -> bool {
 	return s.selection[0] != s.selection[1]
 	return s.selection[0] != s.selection[1]
 }
 }
 
 
+// return the clamped lo/hi of the current selection
+// since the selection[0] moves around and could be ahead of selection[1]
+// useful when rendering and needing left->right
 sorted_selection :: proc(s: ^State) -> (lo, hi: int) {
 sorted_selection :: proc(s: ^State) -> (lo, hi: int) {
 	lo = min(s.selection[0], s.selection[1])
 	lo = min(s.selection[0], s.selection[1])
 	hi = max(s.selection[0], s.selection[1])
 	hi = max(s.selection[0], s.selection[1])
 	lo = clamp(lo, 0, len(s.builder.buf))
 	lo = clamp(lo, 0, len(s.builder.buf))
 	hi = clamp(hi, 0, len(s.builder.buf))
 	hi = clamp(hi, 0, len(s.builder.buf))
-	s.selection[0] = lo
-	s.selection[1] = hi
 	return
 	return
 }
 }
 
 
-
+// delete the current selection range and set the proper selection afterwards
 selection_delete :: proc(s: ^State) {
 selection_delete :: proc(s: ^State) {
 	lo, hi := sorted_selection(s)
 	lo, hi := sorted_selection(s)
 	remove(s, lo, hi)
 	remove(s, lo, hi)
 	s.selection = {lo, lo}
 	s.selection = {lo, lo}
 }
 }
 
 
-
-
-translate_position :: proc(s: ^State, pos: int, t: Translation) -> int {
+// translates the caret position 
+translate_position :: proc(s: ^State, t: Translation) -> int {
 	is_continuation_byte :: proc(b: byte) -> bool {
 	is_continuation_byte :: proc(b: byte) -> bool {
 		return b >= 0x80 && b < 0xc0
 		return b >= 0x80 && b < 0xc0
 	}
 	}
@@ -227,9 +266,7 @@ translate_position :: proc(s: ^State, pos: int, t: Translation) -> int {
 	}
 	}
 
 
 	buf := s.builder.buf[:]
 	buf := s.builder.buf[:]
-
-	pos := pos
-	pos = clamp(pos, 0, len(buf))
+	pos := clamp(s.selection[0], 0, len(buf))
 
 
 	switch t {
 	switch t {
 	case .Start:
 	case .Start:
@@ -280,6 +317,7 @@ translate_position :: proc(s: ^State, pos: int, t: Translation) -> int {
 	return clamp(pos, 0, len(buf))
 	return clamp(pos, 0, len(buf))
 }
 }
 
 
+// Moves the position of the caret (both sides of the selection)
 move_to :: proc(s: ^State, t: Translation) {
 move_to :: proc(s: ^State, t: Translation) {
 	if t == .Left && has_selection(s) {
 	if t == .Left && has_selection(s) {
 		lo, _ := sorted_selection(s)
 		lo, _ := sorted_selection(s)
@@ -288,32 +326,36 @@ move_to :: proc(s: ^State, t: Translation) {
 		_, hi := sorted_selection(s)
 		_, hi := sorted_selection(s)
 		s.selection = {hi, hi}
 		s.selection = {hi, hi}
 	} else {
 	} else {
-		pos := translate_position(s, s.selection[0], t)
+		pos := translate_position(s, t)
 		s.selection = {pos, pos}
 		s.selection = {pos, pos}
 	}
 	}
 }
 }
+
+// Moves only the head of the selection and leaves the tail uneffected
 select_to :: proc(s: ^State, t: Translation) {
 select_to :: proc(s: ^State, t: Translation) {
-	s.selection[0] = translate_position(s, s.selection[0], t)
+	s.selection[0] = translate_position(s, t)
 }
 }
+
+// Deletes everything between the caret and resultant position
 delete_to :: proc(s: ^State, t: Translation) {
 delete_to :: proc(s: ^State, t: Translation) {
 	if has_selection(s) {
 	if has_selection(s) {
 		selection_delete(s)
 		selection_delete(s)
 	} else {
 	} else {
 		lo := s.selection[0]
 		lo := s.selection[0]
-		hi := translate_position(s, lo, t)
+		hi := translate_position(s, t)
 		lo, hi = min(lo, hi), max(lo, hi)
 		lo, hi = min(lo, hi), max(lo, hi)
 		remove(s, lo, hi)
 		remove(s, lo, hi)
 		s.selection = {lo, lo}
 		s.selection = {lo, lo}
 	}
 	}
 }
 }
 
 
-
+// return the currently selected text
 current_selected_text :: proc(s: ^State) -> string {
 current_selected_text :: proc(s: ^State) -> string {
 	lo, hi := sorted_selection(s)
 	lo, hi := sorted_selection(s)
 	return string(s.builder.buf[lo:hi])
 	return string(s.builder.buf[lo:hi])
 }
 }
 
 
-
+// copy & delete the current selection when copy() succeeds
 cut :: proc(s: ^State) -> bool {
 cut :: proc(s: ^State) -> bool {
 	if copy(s) {
 	if copy(s) {
 		selection_delete(s)
 		selection_delete(s)
@@ -322,6 +364,8 @@ cut :: proc(s: ^State) -> bool {
 	return false
 	return false
 }
 }
 
 
+// try and copy the currently selected text to the clipboard
+// State.set_clipboard needs to be assigned
 copy :: proc(s: ^State) -> bool {
 copy :: proc(s: ^State) -> bool {
 	if s.set_clipboard != nil {
 	if s.set_clipboard != nil {
 		return s.set_clipboard(s.clipboard_user_data, current_selected_text(s))
 		return s.set_clipboard(s.clipboard_user_data, current_selected_text(s))
@@ -329,6 +373,8 @@ copy :: proc(s: ^State) -> bool {
 	return s.set_clipboard != nil
 	return s.set_clipboard != nil
 }
 }
 
 
+// reinsert whatever the get_clipboard would return
+// State.get_clipboard needs to be assigned
 paste :: proc(s: ^State) -> bool {
 paste :: proc(s: ^State) -> bool {
 	if s.get_clipboard != nil {
 	if s.get_clipboard != nil {
 		input_text(s, s.get_clipboard(s.clipboard_user_data) or_return)
 		input_text(s, s.get_clipboard(s.clipboard_user_data) or_return)