2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
8 /** @file textbuf.cpp Textbuffer handling. */
12 #include "textbuf_type.h"
13 #include "string_func.h"
14 #include "strings_func.h"
17 #include "gfx_layout.h"
18 #include "window_func.h"
19 #include "core/alloc_func.hpp"
21 #include "safeguards.h"
24 * Try to retrieve the current clipboard contents.
26 * @note OS-specific function.
27 * @return The (optional) clipboard contents.
29 std::optional
<std::string
> GetClipboardContents();
35 * Checks if it is possible to delete a character.
36 * @param backspace if set, delete the character before the caret,
37 * otherwise, delete the character after it.
38 * @return true if a character can be deleted in the given direction.
40 bool Textbuf::CanDelChar(bool backspace
)
42 return backspace
? this->caretpos
!= 0 : this->caretpos
< this->bytes
- 1;
46 * Delete a character from a textbuffer, either with 'Delete' or 'Backspace'
47 * The character is delete from the position the caret is at
48 * @param keycode Type of deletion, either WKC_BACKSPACE or WKC_DELETE
49 * @return Return true on successful change of Textbuf, or false otherwise
51 bool Textbuf::DeleteChar(uint16_t keycode
)
53 bool word
= (keycode
& WKC_CTRL
) != 0;
55 keycode
&= ~WKC_SPECIAL_KEYS
;
56 if (keycode
!= WKC_BACKSPACE
&& keycode
!= WKC_DELETE
) return false;
58 bool backspace
= keycode
== WKC_BACKSPACE
;
60 if (!CanDelChar(backspace
)) return false;
62 char *s
= this->buf
+ this->caretpos
;
66 /* Delete a complete word. */
68 /* Delete whitespace and word in front of the caret. */
69 len
= this->caretpos
- (uint16_t)this->char_iter
->Prev(StringIterator::ITER_WORD
);
72 /* Delete word and following whitespace following the caret. */
73 len
= (uint16_t)this->char_iter
->Next(StringIterator::ITER_WORD
) - this->caretpos
;
75 /* Update character count. */
76 for (const char *ss
= s
; ss
< s
+ len
; Utf8Consume(&ss
)) {
80 /* Delete a single character. */
82 /* Delete the last code point in front of the caret. */
85 len
= (uint16_t)Utf8Decode(&c
, s
);
88 /* Delete the complete character following the caret. */
89 len
= (uint16_t)this->char_iter
->Next(StringIterator::ITER_CHARACTER
) - this->caretpos
;
90 /* Update character count. */
91 for (const char *ss
= s
; ss
< s
+ len
; Utf8Consume(&ss
)) {
97 /* Move the remaining characters over the marker */
98 memmove(s
, s
+ len
, this->bytes
- (s
- this->buf
) - len
);
101 if (backspace
) this->caretpos
-= len
;
103 this->UpdateStringIter();
105 this->UpdateCaretPosition();
106 this->UpdateMarkedText();
112 * Delete every character in the textbuffer
114 void Textbuf::DeleteAll()
116 memset(this->buf
, 0, this->max_bytes
);
117 this->bytes
= this->chars
= 1;
118 this->pixels
= this->caretpos
= this->caretxoffs
= 0;
119 this->markpos
= this->markend
= this->markxoffs
= this->marklength
= 0;
120 this->UpdateStringIter();
124 * Insert a character to a textbuffer. If maxwidth of the Textbuf is zero,
125 * we don't care about the visual-length but only about the physical
126 * length of the string
127 * @param key Character to be inserted
128 * @return Return true on successful change of Textbuf, or false otherwise
130 bool Textbuf::InsertChar(char32_t key
)
132 uint16_t len
= (uint16_t)Utf8CharLen(key
);
133 if (this->bytes
+ len
<= this->max_bytes
&& this->chars
+ 1 <= this->max_chars
) {
134 memmove(this->buf
+ this->caretpos
+ len
, this->buf
+ this->caretpos
, this->bytes
- this->caretpos
);
135 Utf8Encode(this->buf
+ this->caretpos
, key
);
138 this->caretpos
+= len
;
140 this->UpdateStringIter();
142 this->UpdateCaretPosition();
143 this->UpdateMarkedText();
150 * Insert a string into the text buffer. If maxwidth of the Textbuf is zero,
151 * we don't care about the visual-length but only about the physical
152 * length of the string.
153 * @param str String to insert.
154 * @param marked Replace the currently marked text with the new text.
155 * @param caret Move the caret to this point in the insertion string.
156 * @param insert_location Position at which to insert the string.
157 * @param replacement_end Replace all characters from #insert_location up to this location with the new string.
158 * @return True on successful change of Textbuf, or false otherwise.
160 bool Textbuf::InsertString(const char *str
, bool marked
, const char *caret
, const char *insert_location
, const char *replacement_end
)
162 uint16_t insertpos
= (marked
&& this->marklength
!= 0) ? this->markpos
: this->caretpos
;
163 if (insert_location
!= nullptr) {
164 insertpos
= insert_location
- this->buf
;
165 if (insertpos
> this->bytes
) return false;
167 if (replacement_end
!= nullptr) {
168 this->DeleteText(insertpos
, replacement_end
- this->buf
, str
== nullptr);
171 if (marked
) this->DiscardMarkedText(str
== nullptr);
174 if (str
== nullptr) return false;
176 uint16_t bytes
= 0, chars
= 0;
178 for (const char *ptr
= str
; (c
= Utf8Consume(&ptr
)) != '\0';) {
179 if (!IsValidChar(c
, this->afilter
)) break;
181 uint8_t len
= Utf8CharLen(c
);
182 if (this->bytes
+ bytes
+ len
> this->max_bytes
) break;
183 if (this->chars
+ chars
+ 1 > this->max_chars
) break;
188 /* Move caret if needed. */
189 if (ptr
== caret
) this->caretpos
= insertpos
+ bytes
;
192 if (bytes
== 0) return false;
195 this->markpos
= insertpos
;
196 this->markend
= insertpos
+ bytes
;
199 memmove(this->buf
+ insertpos
+ bytes
, this->buf
+ insertpos
, this->bytes
- insertpos
);
200 memcpy(this->buf
+ insertpos
, str
, bytes
);
202 this->bytes
+= bytes
;
203 this->chars
+= chars
;
204 if (!marked
&& caret
== nullptr) this->caretpos
+= bytes
;
205 assert(this->bytes
<= this->max_bytes
);
206 assert(this->chars
<= this->max_chars
);
207 this->buf
[this->bytes
- 1] = '\0'; // terminating zero
209 this->UpdateStringIter();
211 this->UpdateCaretPosition();
212 this->UpdateMarkedText();
218 * Insert a chunk of text from the clipboard onto the textbuffer. Get TEXT clipboard
219 * and append this up to the maximum length (either absolute or screenlength). If maxlength
220 * is zero, we don't care about the screenlength but only about the physical length of the string
221 * @return true on successful change of Textbuf, or false otherwise
223 bool Textbuf::InsertClipboard()
225 auto contents
= GetClipboardContents();
226 if (!contents
.has_value()) return false;
228 return this->InsertString(contents
.value().c_str(), false);
232 * Delete a part of the text.
233 * @param from Start of the text to delete.
234 * @param to End of the text to delete.
235 * @param update Set to true if the internal state should be updated.
237 void Textbuf::DeleteText(uint16_t from
, uint16_t to
, bool update
)
240 const char *s
= this->buf
+ from
;
241 while (s
< this->buf
+ to
) {
246 /* Strip marked characters from buffer. */
247 memmove(this->buf
+ from
, this->buf
+ to
, this->bytes
- to
);
248 this->bytes
-= to
- from
;
251 auto fixup
= [&](uint16_t &pos
) {
252 if (pos
<= from
) return;
260 /* Fixup caret if needed. */
261 fixup(this->caretpos
);
263 /* Fixup marked text if needed. */
264 fixup(this->markpos
);
265 fixup(this->markend
);
268 this->UpdateStringIter();
269 this->UpdateCaretPosition();
270 this->UpdateMarkedText();
275 * Discard any marked text.
276 * @param update Set to true if the internal state should be updated.
278 void Textbuf::DiscardMarkedText(bool update
)
280 if (this->markend
== 0) return;
282 this->DeleteText(this->markpos
, this->markend
, update
);
283 this->markpos
= this->markend
= this->markxoffs
= this->marklength
= 0;
287 * Get the current text.
288 * @return Current text.
290 const char *Textbuf::GetText() const
295 /** Update the character iter after the text has changed. */
296 void Textbuf::UpdateStringIter()
298 this->char_iter
->SetString(this->buf
);
299 size_t pos
= this->char_iter
->SetCurPosition(this->caretpos
);
300 this->caretpos
= pos
== StringIterator::END
? 0 : (uint16_t)pos
;
303 /** Update pixel width of the text. */
304 void Textbuf::UpdateWidth()
306 this->pixels
= GetStringBoundingBox(this->buf
, FS_NORMAL
).width
;
309 /** Update pixel position of the caret. */
310 void Textbuf::UpdateCaretPosition()
312 const auto pos
= GetCharPosInString(this->buf
, this->buf
+ this->caretpos
, FS_NORMAL
);
313 this->caretxoffs
= _current_text_dir
== TD_LTR
? pos
.left
: pos
.right
;
316 /** Update pixel positions of the marked text area. */
317 void Textbuf::UpdateMarkedText()
319 if (this->markend
!= 0) {
320 const auto pos
= GetCharPosInString(this->buf
, this->buf
+ this->markpos
, FS_NORMAL
);
321 const auto end
= GetCharPosInString(this->buf
, this->buf
+ this->markend
, FS_NORMAL
);
322 this->markxoffs
= std::min(pos
.left
, end
.left
);
323 this->marklength
= std::max(pos
.right
, end
.right
) - this->markxoffs
;
325 this->markxoffs
= this->marklength
= 0;
330 * Move to previous character position.
331 * @param what Move ITER_CHARACTER or ITER_WORD.
332 * @return true iff able to move.
334 bool Textbuf::MovePrev(StringIterator::IterType what
)
336 if (this->caretpos
== 0) return false;
338 size_t pos
= this->char_iter
->Prev(what
);
339 if (pos
== StringIterator::END
) return true;
341 this->caretpos
= static_cast<uint16_t>(pos
);
342 this->UpdateCaretPosition();
347 * Move to next character position.
348 * @param what Move ITER_CHARACTER or ITER_WORD.
349 * @return true iff able to move.
351 bool Textbuf::MoveNext(StringIterator::IterType what
)
353 if (this->caretpos
>= this->bytes
- 1) return false;
355 size_t pos
= this->char_iter
->Next(what
);
356 if (pos
== StringIterator::END
) return true;
358 this->caretpos
= static_cast<uint16_t>(pos
);
359 this->UpdateCaretPosition();
364 * Handle text navigation with arrow keys left/right.
365 * This defines where the caret will blink and the next character interaction will occur
366 * @param keycode Direction in which navigation occurs (WKC_CTRL |) WKC_LEFT, (WKC_CTRL |) WKC_RIGHT, WKC_END, WKC_HOME
367 * @return Return true on successful change of Textbuf, or false otherwise
369 bool Textbuf::MovePos(uint16_t keycode
)
373 case WKC_CTRL
| WKC_LEFT
: {
374 auto move_type
= (keycode
& WKC_CTRL
) != 0 ? StringIterator::ITER_WORD
: StringIterator::ITER_CHARACTER
;
375 return (_current_text_dir
== TD_LTR
) ? this->MovePrev(move_type
) : this->MoveNext(move_type
);
379 case WKC_CTRL
| WKC_RIGHT
: {
380 auto move_type
= (keycode
& WKC_CTRL
) != 0 ? StringIterator::ITER_WORD
: StringIterator::ITER_CHARACTER
;
381 return (_current_text_dir
== TD_LTR
) ? this->MoveNext(move_type
) : this->MovePrev(move_type
);
386 this->char_iter
->SetCurPosition(this->caretpos
);
387 this->UpdateCaretPosition();
391 this->caretpos
= this->bytes
- 1;
392 this->char_iter
->SetCurPosition(this->caretpos
);
393 this->UpdateCaretPosition();
404 * Initialize the textbuffer by supplying it the buffer to write into
405 * and the maximum length of this buffer
406 * @param max_bytes maximum size in bytes, including terminating '\0'
407 * @param max_chars maximum size in chars, including terminating '\0'
409 Textbuf::Textbuf(uint16_t max_bytes
, uint16_t max_chars
)
410 : buf(MallocT
<char>(max_bytes
)), char_iter(StringIterator::Create())
412 assert(max_bytes
!= 0);
413 assert(max_chars
!= 0);
415 this->afilter
= CS_ALPHANUMERAL
;
416 this->max_bytes
= max_bytes
;
417 this->max_chars
= max_chars
== UINT16_MAX
? max_bytes
: max_chars
;
428 * Render a string into the textbuffer.
429 * @param string String
431 void Textbuf::Assign(StringID string
)
433 this->Assign(GetString(string
));
437 * Copy a string into the textbuffer.
438 * @param text Source.
440 void Textbuf::Assign(const std::string_view text
)
442 size_t bytes
= std::min
<size_t>(this->max_bytes
- 1, text
.size());
443 memcpy(this->buf
, text
.data(), bytes
);
444 this->buf
[bytes
] = '\0';
446 StrMakeValidInPlace(this->buf
, &this->buf
[bytes
], SVS_NONE
);
448 /* Make sure the name isn't too long for the text buffer in the number of
449 * characters (not bytes). max_chars also counts the '\0' characters. */
450 while (Utf8StringLength(this->buf
) + 1 > this->max_chars
) {
451 *Utf8PrevChar(this->buf
+ strlen(this->buf
)) = '\0';
459 * Update Textbuf type with its actual physical character and screenlength
460 * Get the count of characters in the string as well as the width in pixels.
461 * Useful when copying in a larger amount of text at once
463 void Textbuf::UpdateSize()
465 const char *buf
= this->buf
;
467 this->chars
= this->bytes
= 1; // terminating zero
470 while ((c
= Utf8Consume(&buf
)) != '\0') {
471 this->bytes
+= Utf8CharLen(c
);
474 assert(this->bytes
<= this->max_bytes
);
475 assert(this->chars
<= this->max_chars
);
477 this->caretpos
= this->bytes
- 1;
478 this->UpdateStringIter();
480 this->UpdateMarkedText();
482 this->UpdateCaretPosition();
486 * Handle the flashing of the caret.
487 * @return True if the caret state changes.
489 bool Textbuf::HandleCaret()
492 bool b
= !!(_caret_timer
& 0x20);
494 if (b
!= this->caret
) {
501 HandleKeyPressResult
Textbuf::HandleKeyPress(char32_t key
, uint16_t keycode
)
506 case WKC_ESC
: return HKPR_CANCEL
;
508 case WKC_RETURN
: case WKC_NUM_ENTER
: return HKPR_CONFIRM
;
510 case (WKC_CTRL
| 'V'):
511 case (WKC_SHIFT
| WKC_INSERT
):
512 edited
= this->InsertClipboard();
515 case (WKC_CTRL
| 'U'):
520 case WKC_BACKSPACE
: case WKC_DELETE
:
521 case WKC_CTRL
| WKC_BACKSPACE
: case WKC_CTRL
| WKC_DELETE
:
522 edited
= this->DeleteChar(keycode
);
525 case WKC_LEFT
: case WKC_RIGHT
: case WKC_END
: case WKC_HOME
:
526 case WKC_CTRL
| WKC_LEFT
: case WKC_CTRL
| WKC_RIGHT
:
527 this->MovePos(keycode
);
531 if (IsValidChar(key
, this->afilter
)) {
532 edited
= this->InsertChar(key
);
534 return HKPR_NOT_HANDLED
;
539 return edited
? HKPR_EDITING
: HKPR_CURSOR
;