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