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