text_edit.odin 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. package text_edit
  2. /*
  3. Based off the articles by rxi:
  4. * https://rxi.github.io/textbox_behaviour.html
  5. * https://rxi.github.io/a_simple_undo_system.html
  6. */
  7. import "base:runtime"
  8. import "core:time"
  9. import "core:mem"
  10. import "core:strings"
  11. import "core:unicode/utf8"
  12. DEFAULT_UNDO_TIMEOUT :: 300 * time.Millisecond
  13. State :: struct {
  14. selection: [2]int,
  15. line_start, line_end: int,
  16. // initialized each "frame" with `begin`
  17. builder: ^strings.Builder, // let the caller store the text buffer data
  18. up_index, down_index: int, // multi-lines
  19. // undo
  20. undo: [dynamic]^Undo_State,
  21. redo: [dynamic]^Undo_State,
  22. undo_text_allocator: runtime.Allocator,
  23. id: u64, // useful for immediate mode GUIs
  24. // Timeout information
  25. current_time: time.Tick,
  26. last_edit_time: time.Tick,
  27. undo_timeout: time.Duration,
  28. // Set these if you want cut/copy/paste functionality
  29. set_clipboard: proc(user_data: rawptr, text: string) -> (ok: bool),
  30. get_clipboard: proc(user_data: rawptr) -> (text: string, ok: bool),
  31. clipboard_user_data: rawptr,
  32. }
  33. Undo_State :: struct {
  34. selection: [2]int,
  35. len: int,
  36. text: [0]byte, // string(us.text[:us.len]) --- requiring #no_bounds_check
  37. }
  38. Translation :: enum u32 {
  39. Start,
  40. End,
  41. Left,
  42. Right,
  43. Up,
  44. Down,
  45. Word_Left,
  46. Word_Right,
  47. Word_Start,
  48. Word_End,
  49. Soft_Line_Start,
  50. Soft_Line_End,
  51. }
  52. // init the state to some timeout and set the respective allocators
  53. // - undo_state_allocator dictates the dynamic undo|redo arrays allocators
  54. // - undo_text_allocator is the allocator which allocates strings only
  55. init :: proc(s: ^State, undo_text_allocator, undo_state_allocator: runtime.Allocator, undo_timeout := DEFAULT_UNDO_TIMEOUT) {
  56. s.undo_timeout = undo_timeout
  57. // Used for allocating `Undo_State`
  58. s.undo_text_allocator = undo_text_allocator
  59. s.undo.allocator = undo_state_allocator
  60. s.redo.allocator = undo_state_allocator
  61. }
  62. // clear undo|redo strings and delete their stacks
  63. destroy :: proc(s: ^State) {
  64. undo_clear(s, &s.undo)
  65. undo_clear(s, &s.redo)
  66. delete(s.undo)
  67. delete(s.redo)
  68. s.builder = nil
  69. }
  70. // Call at the beginning of each frame
  71. begin :: proc(s: ^State, id: u64, builder: ^strings.Builder) {
  72. assert(builder != nil)
  73. if s.id != 0 {
  74. end(s)
  75. }
  76. s.id = id
  77. s.selection = {len(builder.buf), 0}
  78. s.builder = builder
  79. update_time(s)
  80. undo_clear(s, &s.undo)
  81. undo_clear(s, &s.redo)
  82. }
  83. // Call at the end of each frame
  84. end :: proc(s: ^State) {
  85. s.id = 0
  86. s.builder = nil
  87. }
  88. // update current time so "insert" can check for timeouts
  89. update_time :: proc(s: ^State) {
  90. s.current_time = time.tick_now()
  91. if s.undo_timeout <= 0 {
  92. s.undo_timeout = DEFAULT_UNDO_TIMEOUT
  93. }
  94. }
  95. // setup the builder, selection and undo|redo state once allowing to retain selection
  96. setup_once :: proc(s: ^State, builder: ^strings.Builder) {
  97. s.builder = builder
  98. s.selection = { len(builder.buf), 0 }
  99. undo_clear(s, &s.undo)
  100. undo_clear(s, &s.redo)
  101. }
  102. // returns true when the builder had content to be cleared
  103. // clear builder&selection and the undo|redo stacks
  104. clear_all :: proc(s: ^State) -> (cleared: bool) {
  105. if s.builder != nil && len(s.builder.buf) > 0 {
  106. clear(&s.builder.buf)
  107. s.selection = {}
  108. cleared = true
  109. }
  110. undo_clear(s, &s.undo)
  111. undo_clear(s, &s.redo)
  112. return
  113. }
  114. // push current text state to the wanted undo|redo stack
  115. undo_state_push :: proc(s: ^State, undo: ^[dynamic]^Undo_State) -> mem.Allocator_Error {
  116. text := string(s.builder.buf[:])
  117. item := (^Undo_State)(mem.alloc(size_of(Undo_State) + len(text), align_of(Undo_State), s.undo_text_allocator) or_return)
  118. item.selection = s.selection
  119. item.len = len(text)
  120. #no_bounds_check {
  121. runtime.copy(item.text[:len(text)], text)
  122. }
  123. append(undo, item) or_return
  124. return nil
  125. }
  126. // pop undo|redo state - push to redo|undo - set selection & text
  127. undo :: proc(s: ^State, undo, redo: ^[dynamic]^Undo_State) {
  128. if len(undo) > 0 {
  129. undo_state_push(s, redo)
  130. item := pop(undo)
  131. s.selection = item.selection
  132. #no_bounds_check {
  133. strings.builder_reset(s.builder)
  134. strings.write_string(s.builder, string(item.text[:item.len]))
  135. }
  136. free(item, s.undo_text_allocator)
  137. }
  138. }
  139. // iteratively clearn the undo|redo stack and free each allocated text state
  140. undo_clear :: proc(s: ^State, undo: ^[dynamic]^Undo_State) {
  141. for len(undo) > 0 {
  142. item := pop(undo)
  143. free(item, s.undo_text_allocator)
  144. }
  145. }
  146. // clear redo stack and check if the undo timeout gets hit
  147. undo_check :: proc(s: ^State) {
  148. undo_clear(s, &s.redo)
  149. if time.tick_diff(s.last_edit_time, s.current_time) > s.undo_timeout {
  150. undo_state_push(s, &s.undo)
  151. }
  152. s.last_edit_time = s.current_time
  153. }
  154. // insert text into the edit state - deletes the current selection
  155. input_text :: proc(s: ^State, text: string) {
  156. if len(text) == 0 {
  157. return
  158. }
  159. if has_selection(s) {
  160. selection_delete(s)
  161. }
  162. insert(s, s.selection[0], text)
  163. offset := s.selection[0] + len(text)
  164. s.selection = {offset, offset}
  165. }
  166. // insert slice of runes into the edit state - deletes the current selection
  167. input_runes :: proc(s: ^State, text: []rune) {
  168. if len(text) == 0 {
  169. return
  170. }
  171. if has_selection(s) {
  172. selection_delete(s)
  173. }
  174. offset := s.selection[0]
  175. for r in text {
  176. b, w := utf8.encode_rune(r)
  177. insert(s, offset, string(b[:w]))
  178. offset += w
  179. }
  180. s.selection = {offset, offset}
  181. }
  182. // insert a single rune into the edit state - deletes the current selection
  183. input_rune :: proc(s: ^State, r: rune) {
  184. if has_selection(s) {
  185. selection_delete(s)
  186. }
  187. offset := s.selection[0]
  188. b, w := utf8.encode_rune(r)
  189. insert(s, offset, string(b[:w]))
  190. offset += w
  191. s.selection = {offset, offset}
  192. }
  193. // insert a single rune into the edit state - deletes the current selection
  194. insert :: proc(s: ^State, at: int, text: string) {
  195. undo_check(s)
  196. inject_at(&s.builder.buf, at, text)
  197. }
  198. // remove the wanted range withing, usually the selection within byte indices
  199. remove :: proc(s: ^State, lo, hi: int) {
  200. undo_check(s)
  201. remove_range(&s.builder.buf, lo, hi)
  202. }
  203. // true if selection head and tail dont match and form a selection of multiple characters
  204. has_selection :: proc(s: ^State) -> bool {
  205. return s.selection[0] != s.selection[1]
  206. }
  207. // return the clamped lo/hi of the current selection
  208. // since the selection[0] moves around and could be ahead of selection[1]
  209. // useful when rendering and needing left->right
  210. sorted_selection :: proc(s: ^State) -> (lo, hi: int) {
  211. lo = min(s.selection[0], s.selection[1])
  212. hi = max(s.selection[0], s.selection[1])
  213. lo = clamp(lo, 0, len(s.builder.buf))
  214. hi = clamp(hi, 0, len(s.builder.buf))
  215. return
  216. }
  217. // delete the current selection range and set the proper selection afterwards
  218. selection_delete :: proc(s: ^State) {
  219. lo, hi := sorted_selection(s)
  220. remove(s, lo, hi)
  221. s.selection = {lo, lo}
  222. }
  223. // translates the caret position
  224. translate_position :: proc(s: ^State, t: Translation) -> int {
  225. is_continuation_byte :: proc(b: byte) -> bool {
  226. return b >= 0x80 && b < 0xc0
  227. }
  228. is_space :: proc(b: byte) -> bool {
  229. return b == ' ' || b == '\t' || b == '\n'
  230. }
  231. buf := s.builder.buf[:]
  232. pos := clamp(s.selection[0], 0, len(buf))
  233. switch t {
  234. case .Start:
  235. pos = 0
  236. case .End:
  237. pos = len(buf)
  238. case .Left:
  239. pos -= 1
  240. for pos >= 0 && is_continuation_byte(buf[pos]) {
  241. pos -= 1
  242. }
  243. case .Right:
  244. pos += 1
  245. for pos < len(buf) && is_continuation_byte(buf[pos]) {
  246. pos += 1
  247. }
  248. case .Up:
  249. pos = s.up_index
  250. case .Down:
  251. pos = s.down_index
  252. case .Word_Left:
  253. for pos > 0 && is_space(buf[pos-1]) {
  254. pos -= 1
  255. }
  256. for pos > 0 && !is_space(buf[pos-1]) {
  257. pos -= 1
  258. }
  259. case .Word_Right:
  260. for pos < len(buf) && !is_space(buf[pos]) {
  261. pos += 1
  262. }
  263. for pos < len(buf) && is_space(buf[pos]) {
  264. pos += 1
  265. }
  266. case .Word_Start:
  267. for pos > 0 && !is_space(buf[pos-1]) {
  268. pos -= 1
  269. }
  270. case .Word_End:
  271. for pos < len(buf) && !is_space(buf[pos]) {
  272. pos += 1
  273. }
  274. case .Soft_Line_Start:
  275. pos = s.line_start
  276. case .Soft_Line_End:
  277. pos = s.line_end
  278. }
  279. return clamp(pos, 0, len(buf))
  280. }
  281. // Moves the position of the caret (both sides of the selection)
  282. move_to :: proc(s: ^State, t: Translation) {
  283. if t == .Left && has_selection(s) {
  284. lo, _ := sorted_selection(s)
  285. s.selection = {lo, lo}
  286. } else if t == .Right && has_selection(s) {
  287. _, hi := sorted_selection(s)
  288. s.selection = {hi, hi}
  289. } else {
  290. pos := translate_position(s, t)
  291. s.selection = {pos, pos}
  292. }
  293. }
  294. // Moves only the head of the selection and leaves the tail uneffected
  295. select_to :: proc(s: ^State, t: Translation) {
  296. s.selection[0] = translate_position(s, t)
  297. }
  298. // Deletes everything between the caret and resultant position
  299. delete_to :: proc(s: ^State, t: Translation) {
  300. if has_selection(s) {
  301. selection_delete(s)
  302. } else {
  303. lo := s.selection[0]
  304. hi := translate_position(s, t)
  305. lo, hi = min(lo, hi), max(lo, hi)
  306. remove(s, lo, hi)
  307. s.selection = {lo, lo}
  308. }
  309. }
  310. // return the currently selected text
  311. current_selected_text :: proc(s: ^State) -> string {
  312. lo, hi := sorted_selection(s)
  313. return string(s.builder.buf[lo:hi])
  314. }
  315. // copy & delete the current selection when copy() succeeds
  316. cut :: proc(s: ^State) -> bool {
  317. if copy(s) {
  318. selection_delete(s)
  319. return true
  320. }
  321. return false
  322. }
  323. // try and copy the currently selected text to the clipboard
  324. // State.set_clipboard needs to be assigned
  325. copy :: proc(s: ^State) -> bool {
  326. if s.set_clipboard != nil {
  327. return s.set_clipboard(s.clipboard_user_data, current_selected_text(s))
  328. }
  329. return s.set_clipboard != nil
  330. }
  331. // reinsert whatever the get_clipboard would return
  332. // State.get_clipboard needs to be assigned
  333. paste :: proc(s: ^State) -> bool {
  334. if s.get_clipboard != nil {
  335. input_text(s, s.get_clipboard(s.clipboard_user_data) or_return)
  336. }
  337. return s.get_clipboard != nil
  338. }
  339. Command_Set :: distinct bit_set[Command; u32]
  340. Command :: enum u32 {
  341. None,
  342. Undo,
  343. Redo,
  344. New_Line, // multi-lines
  345. Cut,
  346. Copy,
  347. Paste,
  348. Select_All,
  349. Backspace,
  350. Delete,
  351. Delete_Word_Left,
  352. Delete_Word_Right,
  353. Left,
  354. Right,
  355. Up, // multi-lines
  356. Down, // multi-lines
  357. Word_Left,
  358. Word_Right,
  359. Start,
  360. End,
  361. Line_Start,
  362. Line_End,
  363. Select_Left,
  364. Select_Right,
  365. Select_Up, // multi-lines
  366. Select_Down, // multi-lines
  367. Select_Word_Left,
  368. Select_Word_Right,
  369. Select_Start,
  370. Select_End,
  371. Select_Line_Start,
  372. Select_Line_End,
  373. }
  374. MULTILINE_COMMANDS :: Command_Set{.New_Line, .Up, .Down, .Select_Up, .Select_Down}
  375. perform_command :: proc(s: ^State, cmd: Command) {
  376. switch cmd {
  377. case .None: /**/
  378. case .Undo: undo(s, &s.undo, &s.redo)
  379. case .Redo: undo(s, &s.redo, &s.undo)
  380. case .New_Line: input_text(s, "\n")
  381. case .Cut: cut(s)
  382. case .Copy: copy(s)
  383. case .Paste: paste(s)
  384. case .Select_All: s.selection = {len(s.builder.buf), 0}
  385. case .Backspace: delete_to(s, .Left)
  386. case .Delete: delete_to(s, .Right)
  387. case .Delete_Word_Left: delete_to(s, .Word_Left)
  388. case .Delete_Word_Right: delete_to(s, .Word_Right)
  389. case .Left: move_to(s, .Left)
  390. case .Right: move_to(s, .Right)
  391. case .Up: move_to(s, .Up)
  392. case .Down: move_to(s, .Down)
  393. case .Word_Left: move_to(s, .Word_Left)
  394. case .Word_Right: move_to(s, .Word_Right)
  395. case .Start: move_to(s, .Start)
  396. case .End: move_to(s, .End)
  397. case .Line_Start: move_to(s, .Soft_Line_Start)
  398. case .Line_End: move_to(s, .Soft_Line_End)
  399. case .Select_Left: select_to(s, .Left)
  400. case .Select_Right: select_to(s, .Right)
  401. case .Select_Up: select_to(s, .Up)
  402. case .Select_Down: select_to(s, .Down)
  403. case .Select_Word_Left: select_to(s, .Word_Left)
  404. case .Select_Word_Right: select_to(s, .Word_Right)
  405. case .Select_Start: select_to(s, .Start)
  406. case .Select_End: select_to(s, .End)
  407. case .Select_Line_Start: select_to(s, .Soft_Line_Start)
  408. case .Select_Line_End: select_to(s, .Soft_Line_End)
  409. }
  410. }