1/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
2/* If you are missing that file, acquire a complete release at teeworlds.com. */
3#include "lineinput.h"
4
5#include "ui.h"
6
7#include <base/dbg.h>
8#include <base/mem.h>
9#include <base/str.h>
10
11#include <engine/graphics.h>
12#include <engine/keys.h>
13#include <engine/shared/config.h>
14
15IInput *CLineInput::ms_pInput = nullptr;
16ITextRender *CLineInput::ms_pTextRender = nullptr;
17IGraphics *CLineInput::ms_pGraphics = nullptr;
18IClient *CLineInput::ms_pClient = nullptr;
19
20CLineInput *CLineInput::ms_pActiveInput = nullptr;
21EInputPriority CLineInput::ms_ActiveInputPriority = EInputPriority::NONE;
22
23vec2 CLineInput::ms_CompositionWindowPosition = vec2(0.0f, 0.0f);
24float CLineInput::ms_CompositionLineHeight = 0.0f;
25
26char CLineInput::ms_aStars[128] = "";
27
28void CLineInput::SetBuffer(char *pStr, size_t MaxSize, size_t MaxChars)
29{
30 if(m_pStr && m_pStr == pStr)
31 return;
32 const char *pLastStr = m_pStr;
33 m_pStr = pStr;
34 m_MaxSize = MaxSize;
35 m_MaxChars = MaxChars;
36 m_WasChanged = m_pStr && pLastStr && m_WasChanged;
37 m_WasCursorChanged = m_pStr && pLastStr && m_WasCursorChanged;
38 if(!pLastStr)
39 {
40 m_CursorPos = m_SelectionStart = m_SelectionEnd = m_LastCompositionCursorPos = 0;
41 m_ScrollOffset = m_ScrollOffsetChange = 0.0f;
42 m_CaretPosition = vec2(0.0f, 0.0f);
43 m_MouseSelection.m_Selecting = false;
44 m_Hidden = false;
45 m_pEmptyText = nullptr;
46 m_WasRendered = false;
47 }
48 if(m_pStr && m_pStr != pLastStr)
49 UpdateStrData();
50}
51
52void CLineInput::Clear()
53{
54 mem_zero(block: m_pStr, size: m_MaxSize);
55 UpdateStrData();
56}
57
58void CLineInput::Set(const char *pString)
59{
60 str_copy(dst: m_pStr, src: pString, dst_size: m_MaxSize);
61 UpdateStrData();
62 SetCursorOffset(m_Len);
63}
64
65void CLineInput::SetRange(const char *pString, size_t Begin, size_t End)
66{
67 if(Begin > End)
68 std::swap(a&: Begin, b&: End);
69 Begin = std::clamp<size_t>(val: Begin, lo: 0, hi: m_Len);
70 End = std::clamp<size_t>(val: End, lo: 0, hi: m_Len);
71
72 size_t RemovedCharSize, RemovedCharCount;
73 str_utf8_stats(str: m_pStr + Begin, max_size: End - Begin + 1, max_count: m_MaxChars, size: &RemovedCharSize, count: &RemovedCharCount);
74
75 size_t AddedCharSize, AddedCharCount;
76 str_utf8_stats(str: pString, max_size: m_MaxSize - m_Len + RemovedCharSize, max_count: m_MaxChars - m_NumChars + RemovedCharCount, size: &AddedCharSize, count: &AddedCharCount);
77
78 if(RemovedCharSize || AddedCharSize)
79 {
80 if(AddedCharSize < RemovedCharSize)
81 {
82 if(AddedCharSize)
83 mem_copy(dest: m_pStr + Begin, source: pString, size: AddedCharSize);
84 mem_move(dest: m_pStr + Begin + AddedCharSize, source: m_pStr + Begin + RemovedCharSize, size: m_Len - Begin - AddedCharSize);
85 }
86 else if(AddedCharSize > RemovedCharSize)
87 mem_move(dest: m_pStr + End + AddedCharSize - RemovedCharSize, source: m_pStr + End, size: m_Len - End);
88
89 if(AddedCharSize >= RemovedCharSize)
90 mem_copy(dest: m_pStr + Begin, source: pString, size: AddedCharSize);
91
92 m_CursorPos = End - RemovedCharSize + AddedCharSize;
93 m_Len += AddedCharSize - RemovedCharSize;
94 m_NumChars += AddedCharCount - RemovedCharCount;
95 m_WasChanged = true;
96 m_WasCursorChanged = true;
97 m_pStr[m_Len] = '\0';
98 m_SelectionStart = m_SelectionEnd = m_CursorPos;
99 }
100}
101
102void CLineInput::Insert(const char *pString, size_t Begin)
103{
104 SetRange(pString, Begin, End: Begin);
105}
106
107void CLineInput::Append(const char *pString)
108{
109 Insert(pString, Begin: m_Len);
110}
111
112void CLineInput::UpdateStrData()
113{
114 str_utf8_stats(str: m_pStr, max_size: m_MaxSize, max_count: m_MaxChars, size: &m_Len, count: &m_NumChars);
115 if(!in_range<size_t>(a: m_CursorPos, lower: 0, upper: m_Len))
116 SetCursorOffset(m_CursorPos);
117 if(!in_range<size_t>(a: m_SelectionStart, lower: 0, upper: m_Len) || !in_range<size_t>(a: m_SelectionEnd, lower: 0, upper: m_Len))
118 SetSelection(Start: m_SelectionStart, End: m_SelectionEnd);
119}
120
121const char *CLineInput::GetDisplayedString()
122{
123 if(m_pfnDisplayTextCallback)
124 return m_pfnDisplayTextCallback(m_pStr, GetNumChars());
125
126 if(!IsHidden())
127 return m_pStr;
128
129 const size_t NumStars = std::min(a: GetNumChars(), b: sizeof(ms_aStars) - 1);
130 for(size_t i = 0; i < NumStars; ++i)
131 ms_aStars[i] = '*';
132 ms_aStars[NumStars] = '\0';
133 return ms_aStars;
134}
135
136void CLineInput::MoveCursor(EMoveDirection Direction, bool MoveWord, const char *pStr, size_t MaxSize, size_t *pCursorPos)
137{
138 // Check whether cursor position is initially on space or non-space character.
139 // When forwarding, check character to the right of the cursor position.
140 // When rewinding, check character to the left of the cursor position (rewind first).
141 size_t PeekCursorPos = Direction == FORWARD ? *pCursorPos : str_utf8_rewind(str: pStr, cursor: *pCursorPos);
142 const char *pTemp = pStr + PeekCursorPos;
143 bool AnySpace = str_utf8_isspace(code: str_utf8_decode(ptr: &pTemp));
144 bool AnyWord = !AnySpace;
145 while(true)
146 {
147 if(Direction == FORWARD)
148 *pCursorPos = str_utf8_forward(str: pStr, cursor: *pCursorPos);
149 else
150 *pCursorPos = str_utf8_rewind(str: pStr, cursor: *pCursorPos);
151 if(!MoveWord || *pCursorPos <= 0 || *pCursorPos >= MaxSize)
152 break;
153 PeekCursorPos = Direction == FORWARD ? *pCursorPos : str_utf8_rewind(str: pStr, cursor: *pCursorPos);
154 pTemp = pStr + PeekCursorPos;
155 const bool CurrentSpace = str_utf8_isspace(code: str_utf8_decode(ptr: &pTemp));
156 const bool CurrentWord = !CurrentSpace;
157 if(Direction == FORWARD && AnySpace && !CurrentSpace)
158 break; // Forward: Stop when next (right) character is non-space after seeing at least one space character.
159 else if(Direction == REWIND && AnyWord && !CurrentWord)
160 break; // Rewind: Stop when next (left) character is space after seeing at least one non-space character.
161 AnySpace |= CurrentSpace;
162 AnyWord |= CurrentWord;
163 }
164}
165
166void CLineInput::SetCursorOffset(size_t Offset)
167{
168 m_SelectionStart = m_SelectionEnd = m_LastCompositionCursorPos = m_CursorPos = std::clamp<size_t>(val: Offset, lo: 0, hi: m_Len);
169 m_WasCursorChanged = true;
170}
171
172void CLineInput::SetSelection(size_t Start, size_t End)
173{
174 dbg_assert(m_CursorPos == Start || m_CursorPos == End, "Selection and cursor offset got desynchronized");
175 if(Start > End)
176 std::swap(a&: Start, b&: End);
177 m_SelectionStart = std::clamp<size_t>(val: Start, lo: 0, hi: m_Len);
178 m_SelectionEnd = std::clamp<size_t>(val: End, lo: 0, hi: m_Len);
179 m_WasCursorChanged = true;
180}
181
182size_t CLineInput::OffsetFromActualToDisplay(size_t ActualOffset)
183{
184 if(IsHidden() || (m_pfnCalculateOffsetCallback && m_pfnCalculateOffsetCallback()))
185 return str_utf8_offset_bytes_to_chars(str: m_pStr, byte_offset: ActualOffset);
186 return ActualOffset;
187}
188
189size_t CLineInput::OffsetFromDisplayToActual(size_t DisplayOffset)
190{
191 if(IsHidden() || (m_pfnCalculateOffsetCallback && m_pfnCalculateOffsetCallback()))
192 return str_utf8_offset_bytes_to_chars(str: m_pStr, byte_offset: DisplayOffset);
193 return DisplayOffset;
194}
195
196bool CLineInput::ProcessInput(const IInput::CEvent &Event)
197{
198 // update derived attributes to handle external changes to the buffer
199 UpdateStrData();
200
201 const size_t OldCursorPos = m_CursorPos;
202 const bool Selecting = Input()->ShiftIsPressed();
203 const size_t SelectionLength = GetSelectionLength();
204 bool KeyHandled = false;
205
206 if(Event.m_Flags & IInput::FLAG_TEXT)
207 {
208 SetRange(pString: Event.m_aText, Begin: m_SelectionStart, End: m_SelectionEnd);
209 KeyHandled = true;
210 }
211
212 if(Event.m_Flags & IInput::FLAG_PRESS)
213 {
214 const bool ModPressed = Input()->ModifierIsPressed();
215 const bool AltPressed = Input()->AltIsPressed();
216
217#ifdef CONF_PLATFORM_MACOSX
218 const bool MoveWord = AltPressed && !ModPressed;
219#else
220 const bool MoveWord = ModPressed && !AltPressed;
221#endif
222
223 if(Event.m_Key == KEY_BACKSPACE)
224 {
225 if(SelectionLength)
226 {
227 SetRange(pString: "", Begin: m_SelectionStart, End: m_SelectionEnd);
228 }
229 else
230 {
231 // If in MoveWord-mode, backspace will delete the word before the selection
232 if(SelectionLength)
233 m_SelectionEnd = m_CursorPos = m_SelectionStart;
234 if(m_CursorPos > 0)
235 {
236 size_t NewCursorPos = m_CursorPos;
237 MoveCursor(Direction: REWIND, MoveWord, pStr: m_pStr, MaxSize: m_Len, pCursorPos: &NewCursorPos);
238 SetRange(pString: "", Begin: NewCursorPos, End: m_CursorPos);
239 }
240 m_SelectionStart = m_SelectionEnd = m_CursorPos;
241 }
242 KeyHandled = true;
243 }
244 else if(Event.m_Key == KEY_DELETE)
245 {
246 if(SelectionLength)
247 {
248 SetRange(pString: "", Begin: m_SelectionStart, End: m_SelectionEnd);
249 }
250 else
251 {
252 // If in MoveWord-mode, delete will delete the word after the selection
253 if(SelectionLength)
254 m_SelectionStart = m_CursorPos = m_SelectionEnd;
255 if(m_CursorPos < m_Len)
256 {
257 size_t EndCursorPos = m_CursorPos;
258 MoveCursor(Direction: FORWARD, MoveWord, pStr: m_pStr, MaxSize: m_Len, pCursorPos: &EndCursorPos);
259 SetRange(pString: "", Begin: m_CursorPos, End: EndCursorPos);
260 }
261 m_SelectionStart = m_SelectionEnd = m_CursorPos;
262 }
263 KeyHandled = true;
264 }
265 else if(Event.m_Key == KEY_LEFT)
266 {
267 if(SelectionLength && !Selecting)
268 {
269 m_CursorPos = m_SelectionStart;
270 }
271 else if(m_CursorPos > 0)
272 {
273 MoveCursor(Direction: REWIND, MoveWord, pStr: m_pStr, MaxSize: m_Len, pCursorPos: &m_CursorPos);
274 if(Selecting)
275 {
276 if(m_SelectionStart == OldCursorPos) // expand start first
277 m_SelectionStart = m_CursorPos;
278 else if(m_SelectionEnd == OldCursorPos)
279 m_SelectionEnd = m_CursorPos;
280 if(m_SelectionStart > m_SelectionEnd)
281 std::swap(a&: m_SelectionStart, b&: m_SelectionEnd);
282 }
283 }
284
285 if(!Selecting)
286 {
287 m_SelectionStart = m_SelectionEnd = m_CursorPos;
288 }
289 KeyHandled = true;
290 }
291 else if(Event.m_Key == KEY_RIGHT)
292 {
293 if(SelectionLength && !Selecting)
294 {
295 m_CursorPos = m_SelectionEnd;
296 }
297 else if(m_CursorPos < m_Len)
298 {
299 MoveCursor(Direction: FORWARD, MoveWord, pStr: m_pStr, MaxSize: m_Len, pCursorPos: &m_CursorPos);
300 if(Selecting)
301 {
302 if(m_SelectionEnd == OldCursorPos) // expand end first
303 m_SelectionEnd = m_CursorPos;
304 else if(m_SelectionStart == OldCursorPos)
305 m_SelectionStart = m_CursorPos;
306 if(m_SelectionStart > m_SelectionEnd)
307 std::swap(a&: m_SelectionStart, b&: m_SelectionEnd);
308 }
309 }
310
311 if(!Selecting)
312 {
313 m_SelectionStart = m_SelectionEnd = m_CursorPos;
314 }
315 KeyHandled = true;
316 }
317 else if(Event.m_Key == KEY_HOME)
318 {
319 if(Selecting)
320 {
321 if(SelectionLength && m_CursorPos == m_SelectionEnd)
322 m_SelectionEnd = m_SelectionStart;
323 }
324 else
325 m_SelectionEnd = 0;
326 m_CursorPos = 0;
327 m_SelectionStart = 0;
328 KeyHandled = true;
329 }
330 else if(Event.m_Key == KEY_END)
331 {
332 if(Selecting)
333 {
334 if(SelectionLength && m_CursorPos == m_SelectionStart)
335 m_SelectionStart = m_SelectionEnd;
336 }
337 else
338 m_SelectionStart = m_Len;
339 m_CursorPos = m_Len;
340 m_SelectionEnd = m_Len;
341 KeyHandled = true;
342 }
343 else if(ModPressed && !AltPressed && Event.m_Key == KEY_V)
344 {
345 std::string ClipboardText = Input()->GetClipboardText();
346 if(!ClipboardText.empty())
347 {
348 if(m_pfnClipboardLineCallback)
349 {
350 // Split clipboard text into multiple lines. Send all complete lines to callback.
351 // The lineinput is set to the last clipboard line.
352 bool FirstLine = true;
353 size_t i, Begin = 0;
354 for(i = 0; i < ClipboardText.length(); i++)
355 {
356 if(ClipboardText[i] == '\n')
357 {
358 if(i == Begin)
359 {
360 Begin++;
361 continue;
362 }
363 std::string Line = ClipboardText.substr(pos: Begin, n: i - Begin + 1);
364 if(FirstLine)
365 {
366 str_sanitize_cc(str: Line.data());
367 SetRange(pString: Line.c_str(), Begin: m_SelectionStart, End: m_SelectionEnd);
368 FirstLine = false;
369 Line = GetString();
370 }
371 Begin = i + 1;
372 str_sanitize_cc(str: Line.data());
373 m_pfnClipboardLineCallback(Line.c_str());
374 }
375 }
376 std::string Line = ClipboardText.substr(pos: Begin, n: i - Begin + 1);
377 str_sanitize_cc(str: Line.data());
378 if(FirstLine)
379 SetRange(pString: Line.c_str(), Begin: m_SelectionStart, End: m_SelectionEnd);
380 else
381 Set(Line.c_str());
382 }
383 else
384 {
385 str_sanitize_cc(str: ClipboardText.data());
386 SetRange(pString: ClipboardText.c_str(), Begin: m_SelectionStart, End: m_SelectionEnd);
387 }
388 }
389 KeyHandled = true;
390 }
391 else if(ModPressed && !AltPressed && (Event.m_Key == KEY_C || Event.m_Key == KEY_X) && SelectionLength)
392 {
393 char *pSelection = m_pStr + m_SelectionStart;
394 const char TempChar = pSelection[SelectionLength];
395 pSelection[SelectionLength] = '\0';
396 Input()->SetClipboardText(pSelection);
397 pSelection[SelectionLength] = TempChar;
398 if(Event.m_Key == KEY_X)
399 SetRange(pString: "", Begin: m_SelectionStart, End: m_SelectionEnd);
400 KeyHandled = true;
401 }
402 else if(ModPressed && !AltPressed && Event.m_Key == KEY_A)
403 {
404 m_SelectionStart = 0;
405 m_SelectionEnd = m_CursorPos = m_Len;
406 KeyHandled = true;
407 }
408 }
409
410 m_WasCursorChanged |= OldCursorPos != m_CursorPos;
411 m_WasCursorChanged |= SelectionLength != GetSelectionLength();
412 return KeyHandled;
413}
414
415STextBoundingBox CLineInput::Render(const CUIRect *pRect, float FontSize, int Align, bool Changed, float LineWidth, float LineSpacing, const std::vector<STextColorSplit> &vColorSplits)
416{
417 // update derived attributes to handle external changes to the buffer
418 UpdateStrData();
419
420 m_WasRendered = true;
421
422 const char *pDisplayStr = GetDisplayedString();
423 const bool HasComposition = Input()->HasComposition();
424
425 if(pDisplayStr[0] == '\0' && !HasComposition && m_pEmptyText != nullptr)
426 {
427 pDisplayStr = m_pEmptyText;
428 m_MouseSelection.m_Selecting = false;
429 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.75f);
430 }
431
432 CTextCursor Cursor;
433 if(IsActive())
434 {
435 const size_t CursorOffset = GetCursorOffset();
436 const size_t DisplayCursorOffset = OffsetFromActualToDisplay(ActualOffset: CursorOffset);
437 const size_t CompositionStart = CursorOffset + Input()->GetCompositionCursor();
438 const size_t DisplayCompositionStart = OffsetFromActualToDisplay(ActualOffset: CompositionStart);
439 const size_t CaretOffset = HasComposition ? DisplayCompositionStart : DisplayCursorOffset;
440
441 std::string DisplayStrBuffer;
442 if(HasComposition)
443 {
444 const std::string DisplayStr = std::string(pDisplayStr);
445 DisplayStrBuffer = DisplayStr.substr(pos: 0, n: DisplayCursorOffset) + Input()->GetComposition() + DisplayStr.substr(pos: DisplayCursorOffset);
446 pDisplayStr = DisplayStrBuffer.c_str();
447 }
448
449 const STextBoundingBox BoundingBox = TextRender()->TextBoundingBox(Size: FontSize, pText: pDisplayStr, StrLength: -1, LineWidth, LineSpacing);
450 const vec2 CursorPos = CUi::CalcAlignedCursorPos(pRect, TextSize: BoundingBox.Size(), Align);
451
452 Cursor.SetPosition(CursorPos);
453 Cursor.m_FontSize = FontSize;
454 Cursor.m_LineWidth = LineWidth;
455 Cursor.m_ForceCursorRendering = Changed;
456 Cursor.m_LineSpacing = LineSpacing;
457 Cursor.m_PressMouse.x = m_MouseSelection.m_PressMouse.x;
458 Cursor.m_ReleaseMouse.x = m_MouseSelection.m_ReleaseMouse.x;
459 Cursor.m_vColorSplits = vColorSplits;
460 if(LineWidth < 0.0f)
461 {
462 // Using a Y position that's always inside the line input makes it so the selection does not reset when
463 // the mouse is moved outside the line input while selecting, which would otherwise be very inconvenient.
464 // This is a single line cursor, so we don't need the Y position to support selection over multiple lines.
465 Cursor.m_PressMouse.y = CursorPos.y + BoundingBox.m_H / 2.0f;
466 Cursor.m_ReleaseMouse.y = CursorPos.y + BoundingBox.m_H / 2.0f;
467 }
468 else
469 {
470 Cursor.m_PressMouse.y = m_MouseSelection.m_PressMouse.y;
471 Cursor.m_ReleaseMouse.y = m_MouseSelection.m_ReleaseMouse.y;
472 }
473
474 if(HasComposition)
475 {
476 // We need to track the last composition cursor position separately, because the composition
477 // cursor movement does not cause an input event that would set the Changed variable.
478 Cursor.m_ForceCursorRendering |= m_LastCompositionCursorPos != CaretOffset;
479 m_LastCompositionCursorPos = CaretOffset;
480 const size_t DisplayCompositionEnd = DisplayCursorOffset + Input()->GetCompositionLength();
481 Cursor.m_CursorMode = TEXT_CURSOR_CURSOR_MODE_SET;
482 Cursor.m_CursorCharacter = str_utf8_offset_bytes_to_chars(str: pDisplayStr, byte_offset: CaretOffset);
483 Cursor.m_CalculateSelectionMode = TEXT_CURSOR_SELECTION_MODE_SET;
484 Cursor.m_SelectionHeightFactor = 0.1f;
485 Cursor.m_SelectionStart = str_utf8_offset_bytes_to_chars(str: pDisplayStr, byte_offset: DisplayCursorOffset);
486 Cursor.m_SelectionEnd = str_utf8_offset_bytes_to_chars(str: pDisplayStr, byte_offset: DisplayCompositionEnd);
487 TextRender()->TextSelectionColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.8f);
488 TextRender()->TextEx(pCursor: &Cursor, pText: pDisplayStr);
489 TextRender()->TextSelectionColor(Color: TextRender()->DefaultTextSelectionColor());
490 }
491 else if(GetSelectionLength())
492 {
493 const size_t Start = OffsetFromActualToDisplay(ActualOffset: GetSelectionStart());
494 const size_t End = OffsetFromActualToDisplay(ActualOffset: GetSelectionEnd());
495 Cursor.m_CursorMode = m_MouseSelection.m_Selecting ? TEXT_CURSOR_CURSOR_MODE_CALCULATE : TEXT_CURSOR_CURSOR_MODE_SET;
496 Cursor.m_CursorCharacter = str_utf8_offset_bytes_to_chars(str: pDisplayStr, byte_offset: CaretOffset);
497 Cursor.m_CalculateSelectionMode = m_MouseSelection.m_Selecting ? TEXT_CURSOR_SELECTION_MODE_CALCULATE : TEXT_CURSOR_SELECTION_MODE_SET;
498 Cursor.m_SelectionStart = str_utf8_offset_bytes_to_chars(str: pDisplayStr, byte_offset: Start);
499 Cursor.m_SelectionEnd = str_utf8_offset_bytes_to_chars(str: pDisplayStr, byte_offset: End);
500 TextRender()->TextEx(pCursor: &Cursor, pText: pDisplayStr);
501 }
502 else
503 {
504 Cursor.m_CursorMode = m_MouseSelection.m_Selecting ? TEXT_CURSOR_CURSOR_MODE_CALCULATE : TEXT_CURSOR_CURSOR_MODE_SET;
505 Cursor.m_CursorCharacter = str_utf8_offset_bytes_to_chars(str: pDisplayStr, byte_offset: CaretOffset);
506 Cursor.m_CalculateSelectionMode = m_MouseSelection.m_Selecting ? TEXT_CURSOR_SELECTION_MODE_CALCULATE : TEXT_CURSOR_SELECTION_MODE_NONE;
507 TextRender()->TextEx(pCursor: &Cursor, pText: pDisplayStr);
508 }
509
510 if(Cursor.m_CursorMode == TEXT_CURSOR_CURSOR_MODE_CALCULATE && Cursor.m_CursorCharacter >= 0)
511 {
512 const size_t NewCursorOffset = str_utf8_offset_chars_to_bytes(str: pDisplayStr, char_offset: Cursor.m_CursorCharacter);
513 SetCursorOffset(OffsetFromDisplayToActual(DisplayOffset: NewCursorOffset));
514 }
515 if(Cursor.m_CalculateSelectionMode == TEXT_CURSOR_SELECTION_MODE_CALCULATE && Cursor.m_SelectionStart >= 0 && Cursor.m_SelectionEnd >= 0)
516 {
517 const size_t NewSelectionStart = str_utf8_offset_chars_to_bytes(str: pDisplayStr, char_offset: Cursor.m_SelectionStart);
518 const size_t NewSelectionEnd = str_utf8_offset_chars_to_bytes(str: pDisplayStr, char_offset: Cursor.m_SelectionEnd);
519 SetSelection(Start: OffsetFromDisplayToActual(DisplayOffset: NewSelectionStart), End: OffsetFromDisplayToActual(DisplayOffset: NewSelectionEnd));
520 }
521
522 m_CaretPosition = Cursor.m_CursorRenderedPosition;
523
524 CTextCursor CaretCursor;
525 CaretCursor.SetPosition(CursorPos);
526 CaretCursor.m_FontSize = FontSize;
527 CaretCursor.m_Flags = 0;
528 CaretCursor.m_LineWidth = LineWidth;
529 CaretCursor.m_LineSpacing = LineSpacing;
530 CaretCursor.m_CursorMode = TEXT_CURSOR_CURSOR_MODE_SET;
531 CaretCursor.m_CursorCharacter = str_utf8_offset_bytes_to_chars(str: pDisplayStr, byte_offset: DisplayCursorOffset);
532 TextRender()->TextEx(pCursor: &CaretCursor, pText: pDisplayStr);
533 SetCompositionWindowPosition(Anchor: CaretCursor.m_CursorRenderedPosition + vec2(0.0f, CaretCursor.m_AlignedFontSize / 2.0f), LineHeight: CaretCursor.m_AlignedFontSize);
534 }
535 else
536 {
537 const STextBoundingBox BoundingBox = TextRender()->TextBoundingBox(Size: FontSize, pText: pDisplayStr, StrLength: -1, LineWidth, LineSpacing);
538 Cursor.SetPosition(CUi::CalcAlignedCursorPos(pRect, TextSize: BoundingBox.Size(), Align));
539 Cursor.m_FontSize = FontSize;
540 Cursor.m_LineWidth = LineWidth;
541 Cursor.m_LineSpacing = LineSpacing;
542 Cursor.m_vColorSplits = vColorSplits;
543 TextRender()->TextEx(pCursor: &Cursor, pText: pDisplayStr);
544 }
545
546 TextRender()->TextColor(Color: TextRender()->DefaultTextColor());
547
548 return Cursor.BoundingBox();
549}
550
551void CLineInput::RenderCandidates()
552{
553 // Check if the active line input was not rendered and deactivate it in that case.
554 // This can happen e.g. when an input in the ingame menu is active and the menu is
555 // closed or when switching between menu and editor with an active input.
556 CLineInput *pActiveInput = GetActiveInput();
557 if(pActiveInput != nullptr)
558 {
559 if(pActiveInput->m_WasRendered)
560 {
561 pActiveInput->m_WasRendered = false;
562 }
563 else
564 {
565 pActiveInput->Deactivate();
566 return;
567 }
568 }
569
570 if(!Input()->HasComposition() || !Input()->GetCandidateCount())
571 return;
572
573 const float FontSize = 7.0f;
574 const float Padding = 1.0f;
575 const float Margin = 4.0f;
576 const float Height = 300.0f;
577 const float Width = Height * Graphics()->ScreenAspect();
578 const vec2 ScreenSize = Graphics()->ScreenSize();
579
580 Graphics()->MapScreenToSize(Width, Height);
581
582 // Determine longest candidate width
583 float LongestCandidateWidth = 0.0f;
584 for(int i = 0; i < Input()->GetCandidateCount(); ++i)
585 LongestCandidateWidth = std::max(a: LongestCandidateWidth, b: TextRender()->TextWidth(Size: FontSize, pText: Input()->GetCandidate(Index: i)));
586
587 const float NumOffset = 8.0f;
588 const float RectWidth = LongestCandidateWidth + Margin + NumOffset + 2.0f * Padding;
589 const float RectHeight = Input()->GetCandidateCount() * (FontSize + 2.0f * Padding) + Margin;
590
591 vec2 Position = ms_CompositionWindowPosition / ScreenSize * vec2(Width, Height);
592 Position.y += Margin;
593
594 // Move candidate window left if needed
595 if(Position.x + RectWidth + Margin > Width)
596 Position.x -= Position.x + RectWidth + Margin - Width;
597
598 // Move candidate window up if needed
599 if(Position.y + RectHeight + Margin > Height)
600 Position.y -= RectHeight + ms_CompositionLineHeight / ScreenSize.y * Height + 2.0f * Margin;
601
602 Graphics()->TextureClear();
603 Graphics()->QuadsBegin();
604
605 // Draw window shadow
606 Graphics()->SetColor(r: 0.0f, g: 0.0f, b: 0.0f, a: 0.8f);
607 IGraphics::CQuadItem Quad = IGraphics::CQuadItem(Position.x + 0.75f, Position.y + 0.75f, RectWidth, RectHeight);
608 Graphics()->QuadsDrawTL(pArray: &Quad, Num: 1);
609
610 // Draw window background
611 Graphics()->SetColor(r: 0.15f, g: 0.15f, b: 0.15f, a: 1.0f);
612 Quad = IGraphics::CQuadItem(Position.x, Position.y, RectWidth, RectHeight);
613 Graphics()->QuadsDrawTL(pArray: &Quad, Num: 1);
614
615 // Draw selected entry highlight
616 Graphics()->SetColor(r: 0.1f, g: 0.4f, b: 0.8f, a: 1.0f);
617 Quad = IGraphics::CQuadItem(Position.x + Margin / 4.0f, Position.y + Margin / 2.0f + Input()->GetCandidateSelectedIndex() * (FontSize + 2.0f * Padding), RectWidth - Margin / 2.0f, FontSize + 2.0f * Padding);
618 Graphics()->QuadsDrawTL(pArray: &Quad, Num: 1);
619 Graphics()->QuadsEnd();
620
621 // Draw candidates
622 for(int i = 0; i < Input()->GetCandidateCount(); ++i)
623 {
624 char aBuf[3];
625 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d.", (i + 1) % 10);
626
627 const float PosX = Position.x + Margin / 2.0f + Padding;
628 const float PosY = Position.y + Margin / 2.0f + i * (FontSize + 2.0f * Padding) + Padding;
629 TextRender()->TextColor(r: 0.6f, g: 0.6f, b: 0.6f, a: 1.0f);
630 TextRender()->Text(x: PosX, y: PosY, Size: FontSize, pText: aBuf);
631 TextRender()->TextColor(r: 1.0f, g: 1.0f, b: 1.0f, a: 1.0f);
632 TextRender()->Text(x: PosX + NumOffset, y: PosY, Size: FontSize, pText: Input()->GetCandidate(Index: i));
633 }
634}
635
636void CLineInput::SetCompositionWindowPosition(vec2 Anchor, float LineHeight)
637{
638 const vec2 ScreenScale = Graphics()->ScreenSize() / Graphics()->GetScreen().Size();
639 ms_CompositionWindowPosition = Anchor * ScreenScale;
640 ms_CompositionLineHeight = LineHeight * ScreenScale.y;
641 Input()->SetCompositionWindowPosition(X: ms_CompositionWindowPosition.x, Y: ms_CompositionWindowPosition.y, H: ms_CompositionLineHeight);
642}
643
644void CLineInput::Activate(EInputPriority Priority)
645{
646 if(IsActive())
647 return;
648 if(ms_ActiveInputPriority != EInputPriority::NONE && Priority < ms_ActiveInputPriority)
649 return; // do not replace a higher priority input
650 if(ms_pActiveInput)
651 ms_pActiveInput->OnDeactivate();
652 ms_pActiveInput = this;
653 ms_pActiveInput->OnActivate();
654 ms_ActiveInputPriority = Priority;
655}
656
657void CLineInput::Deactivate() const
658{
659 if(!IsActive())
660 return;
661 ms_pActiveInput->OnDeactivate();
662 ms_pActiveInput = nullptr;
663 ms_ActiveInputPriority = EInputPriority::NONE;
664}
665
666void CLineInput::OnActivate()
667{
668 Input()->StartTextInput();
669}
670
671void CLineInput::OnDeactivate()
672{
673 Input()->StopTextInput();
674 m_MouseSelection.m_Selecting = false;
675}
676
677void CLineInputNumber::SetInteger(int Number, int Base, int HexPrefix)
678{
679 char aBuf[32];
680 switch(Base)
681 {
682 case 10:
683 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%d", Number);
684 break;
685 case 16:
686 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%0*X", HexPrefix, Number);
687 break;
688 default:
689 dbg_assert_failed("Base %d unsupported", Base);
690 }
691 if(str_comp(a: aBuf, b: GetString()) != 0)
692 Set(aBuf);
693}
694
695int CLineInputNumber::GetInteger(int Base) const
696{
697 return str_toint_base(str: GetString(), base: Base);
698}
699
700void CLineInputNumber::SetInteger64(int64_t Number, int Base, int HexPrefix)
701{
702 char aBuf[64];
703 switch(Base)
704 {
705 case 10:
706 str_format(aBuf, sizeof(aBuf), "%" PRId64, Number);
707 break;
708 case 16:
709 str_format(aBuf, sizeof(aBuf), "%0*" PRIX64, HexPrefix, Number);
710 break;
711 default:
712 dbg_assert_failed("Base %d unsupported", Base);
713 }
714 if(str_comp(a: aBuf, b: GetString()) != 0)
715 Set(aBuf);
716}
717
718int64_t CLineInputNumber::GetInteger64(int Base) const
719{
720 return str_toint64_base(str: GetString(), base: Base);
721}
722
723void CLineInputNumber::SetFloat(float Number)
724{
725 char aBuf[32];
726 str_format(buffer: aBuf, buffer_size: sizeof(aBuf), format: "%.3f", Number);
727 if(str_comp(a: aBuf, b: GetString()) != 0)
728 Set(aBuf);
729}
730
731float CLineInputNumber::GetFloat() const
732{
733 return str_tofloat(str: GetString());
734}
735