1// Aseprite UI Library
2// Copyright (C) 2018-2022 Igara Studio S.A.
3// Copyright (C) 2001-2018 David Capello
4//
5// This file is released under the terms of the MIT license.
6// Read LICENSE.txt for more information.
7
8#ifdef HAVE_CONFIG_H
9#include "config.h"
10#endif
11
12#include "ui/entry.h"
13
14#include "base/string.h"
15#include "os/draw_text.h"
16#include "os/font.h"
17#include "os/system.h"
18#include "ui/display.h"
19#include "ui/menu.h"
20#include "ui/message.h"
21#include "ui/scale.h"
22#include "ui/size_hint_event.h"
23#include "ui/system.h"
24#include "ui/theme.h"
25#include "ui/timer.h"
26#include "ui/widget.h"
27
28#include <algorithm>
29#include <cctype>
30#include <cstdarg>
31#include <cstdio>
32#include <memory>
33
34namespace ui {
35
36// Shared timer between all entries.
37static std::unique_ptr<Timer> s_timer;
38
39static inline bool is_word_char(int ch) {
40 return (ch && !std::isspace(ch) && !std::ispunct(ch));
41}
42
43Entry::Entry(const int maxsize, const char* format, ...)
44 : Widget(kEntryWidget)
45 , m_maxsize(maxsize)
46 , m_caret(0)
47 , m_scroll(0)
48 , m_select(0)
49 , m_hidden(false)
50 , m_state(false)
51 , m_readonly(false)
52 , m_recent_focused(false)
53 , m_lock_selection(false)
54 , m_translate_dead_keys(true)
55{
56 enableFlags(CTRL_RIGHT_CLICK);
57
58 // formatted string
59 char buf[4096]; // TODO buffer overflow
60 if (format) {
61 va_list ap;
62 va_start(ap, format);
63 vsprintf(buf, format, ap);
64 va_end(ap);
65 }
66 // empty string
67 else {
68 buf[0] = 0;
69 }
70
71 // TODO support for text alignment and multi-line
72 // widget->align = LEFT | MIDDLE;
73 setText(buf);
74
75 setFocusStop(true);
76 initTheme();
77}
78
79Entry::~Entry()
80{
81 stopTimer();
82}
83
84void Entry::setMaxTextLength(const int maxsize)
85{
86 m_maxsize = maxsize;
87}
88
89bool Entry::isReadOnly() const
90{
91 return m_readonly;
92}
93
94void Entry::setReadOnly(bool state)
95{
96 m_readonly = state;
97}
98
99void Entry::showCaret()
100{
101 m_hidden = false;
102 if (shouldStartTimer(hasFocus()))
103 startTimer();
104 invalidate();
105}
106
107void Entry::hideCaret()
108{
109 m_hidden = true;
110 stopTimer();
111 invalidate();
112}
113
114int Entry::lastCaretPos() const
115{
116 return int(m_boxes.size()-1);
117}
118
119void Entry::setCaretPos(int pos)
120{
121 gfx::Size caretSize = theme()->getEntryCaretSize(this);
122 int textlen = lastCaretPos();
123 m_caret = std::clamp(pos, 0, textlen);
124 m_scroll = std::clamp(m_scroll, 0, textlen);
125
126 // Backward scroll
127 if (m_caret < m_scroll)
128 m_scroll = m_caret;
129 // Forward scroll
130 else if (m_caret > m_scroll) {
131 int xLimit = bounds().x2() - border().right();
132 while (m_caret > m_scroll) {
133 int segmentWidth = 0;
134 for (int j=m_scroll; j<m_caret; ++j)
135 segmentWidth += m_boxes[j].width;
136
137 int x = bounds().x + border().left() + segmentWidth + caretSize.w;
138 if (x < xLimit)
139 break;
140 else
141 ++m_scroll;
142 }
143 }
144
145 if (shouldStartTimer(hasFocus()))
146 startTimer();
147 m_state = true;
148
149 invalidate();
150}
151
152void Entry::setCaretToEnd()
153{
154 int end = lastCaretPos();
155 selectText(end, end);
156}
157
158void Entry::selectText(int from, int to)
159{
160 int end = lastCaretPos();
161
162 m_select = from;
163 setCaretPos(from); // to move scroll
164 setCaretPos((to >= 0)? to: end+to+1);
165
166 invalidate();
167}
168
169void Entry::selectAllText()
170{
171 selectText(0, -1);
172}
173
174void Entry::deselectText()
175{
176 m_select = -1;
177 invalidate();
178}
179
180std::string Entry::selectedText() const
181{
182 Range range = selectedRange();
183 if (!range.isEmpty())
184 return text().substr(m_boxes[range.from].from,
185 m_boxes[range.to-1].to - m_boxes[range.from].from);
186 else
187 return std::string();
188}
189
190Entry::Range Entry::selectedRange() const
191{
192 Range range;
193 if ((m_select >= 0) &&
194 (m_caret != m_select)) {
195 range.from = std::min(m_caret, m_select);
196 range.to = std::max(m_caret, m_select);
197
198 ASSERT(range.from >= 0 && range.from < int(m_boxes.size()));
199 ASSERT(range.to >= 0 && range.to <= int(m_boxes.size()));
200 }
201 return range;
202}
203
204void Entry::setSuffix(const std::string& suffix)
205{
206 // No-op cases
207 if ((!m_suffix && suffix.empty()) ||
208 (m_suffix && *m_suffix == suffix))
209 return;
210
211 m_suffix = std::make_unique<std::string>(suffix);
212 invalidate();
213}
214
215std::string Entry::getSuffix()
216{
217 return (m_suffix ? *m_suffix: std::string());
218}
219
220void Entry::setTranslateDeadKeys(bool state)
221{
222 m_translate_dead_keys = state;
223}
224
225void Entry::getEntryThemeInfo(int* scroll, int* caret, int* state, Range* range) const
226{
227 if (scroll) *scroll = m_scroll;
228 if (caret) *caret = m_caret;
229 if (state) *state = !m_hidden && m_state;
230 if (range) *range = selectedRange();
231}
232
233gfx::Rect Entry::getEntryTextBounds() const
234{
235 return onGetEntryTextBounds();
236}
237
238bool Entry::onProcessMessage(Message* msg)
239{
240 switch (msg->type()) {
241
242 case kTimerMessage:
243 if (hasFocus() && static_cast<TimerMessage*>(msg)->timer() == s_timer.get()) {
244 // Blinking caret
245 m_state = (m_state ? false: true);
246 invalidate();
247 }
248 break;
249
250 case kFocusEnterMessage:
251 if (shouldStartTimer(true))
252 startTimer();
253
254 m_state = true;
255 invalidate();
256
257 if (m_lock_selection) {
258 m_lock_selection = false;
259 }
260 else {
261 selectAllText();
262 m_recent_focused = true;
263 }
264
265 // Start processing dead keys
266 if (m_translate_dead_keys)
267 os::instance()->setTranslateDeadKeys(true);
268 break;
269
270 case kFocusLeaveMessage:
271 invalidate();
272
273 stopTimer();
274
275 if (!m_lock_selection)
276 deselectText();
277
278 m_recent_focused = false;
279
280 // Stop processing dead keys
281 if (m_translate_dead_keys)
282 os::instance()->setTranslateDeadKeys(false);
283 break;
284
285 case kKeyDownMessage:
286 if (hasFocus() && !isReadOnly()) {
287 // Command to execute
288 EntryCmd cmd = EntryCmd::NoOp;
289 KeyMessage* keymsg = static_cast<KeyMessage*>(msg);
290 KeyScancode scancode = keymsg->scancode();
291
292 switch (scancode) {
293
294 case kKeyLeft:
295 if (msg->ctrlPressed() || msg->altPressed())
296 cmd = EntryCmd::BackwardWord;
297 else if (msg->cmdPressed())
298 cmd = EntryCmd::BeginningOfLine;
299 else
300 cmd = EntryCmd::BackwardChar;
301 break;
302
303 case kKeyRight:
304 if (msg->ctrlPressed() || msg->altPressed())
305 cmd = EntryCmd::ForwardWord;
306 else if (msg->cmdPressed())
307 cmd = EntryCmd::EndOfLine;
308 else
309 cmd = EntryCmd::ForwardChar;
310 break;
311
312 case kKeyHome:
313 cmd = EntryCmd::BeginningOfLine;
314 break;
315
316 case kKeyEnd:
317 cmd = EntryCmd::EndOfLine;
318 break;
319
320 case kKeyDel:
321 if (msg->shiftPressed())
322 cmd = EntryCmd::Cut;
323 else if (msg->ctrlPressed())
324 cmd = EntryCmd::DeleteForwardToEndOfLine;
325 else
326 cmd = EntryCmd::DeleteForward;
327 break;
328
329 case kKeyInsert:
330 if (msg->shiftPressed())
331 cmd = EntryCmd::Paste;
332 else if (msg->ctrlPressed())
333 cmd = EntryCmd::Copy;
334 break;
335
336 case kKeyBackspace:
337 if (msg->ctrlPressed() || msg->altPressed())
338 cmd = EntryCmd::DeleteBackwardWord;
339 else
340 cmd = EntryCmd::DeleteBackward;
341 break;
342
343 default:
344 // Map common macOS/Windows shortcuts for Cut/Copy/Paste/Select all
345#if defined __APPLE__
346 if (msg->onlyCmdPressed())
347#else
348 if (msg->onlyCtrlPressed())
349#endif
350 {
351 switch (scancode) {
352 case kKeyX: cmd = EntryCmd::Cut; break;
353 case kKeyC: cmd = EntryCmd::Copy; break;
354 case kKeyV: cmd = EntryCmd::Paste; break;
355 case kKeyA: cmd = EntryCmd::SelectAll; break;
356 }
357 }
358 break;
359 }
360
361 if (cmd == EntryCmd::NoOp) {
362 if (keymsg->unicodeChar() >= 32) {
363 executeCmd(EntryCmd::InsertChar, keymsg->unicodeChar(),
364 (msg->shiftPressed()) ? true: false);
365
366 // Select dead-key
367 if (keymsg->isDeadKey()) {
368 if (lastCaretPos() < m_maxsize)
369 selectText(m_caret-1, m_caret);
370 }
371 return true;
372 }
373 // Consume all key down of modifiers only, e.g. so the user
374 // can press first "Ctrl" key, and then "Ctrl+C"
375 // combination.
376 else if (keymsg->scancode() >= kKeyFirstModifierScancode) {
377 return true;
378 }
379 else {
380 break; // Propagate to manager
381 }
382 }
383
384 executeCmd(cmd, keymsg->unicodeChar(),
385 (msg->shiftPressed()) ? true: false);
386 return true;
387 }
388 break;
389
390 case kMouseDownMessage:
391 captureMouse();
392
393 // Disable selecting words if we click again (only
394 // double-clicking enables selecting words again).
395 if (!m_selecting_words.isEmpty())
396 m_selecting_words.reset();
397
398 [[fallthrough]];
399
400 case kMouseMoveMessage:
401 if (hasCapture()) {
402 bool is_dirty = false;
403 int c = getCaretFromMouse(static_cast<MouseMessage*>(msg));
404
405 if (static_cast<MouseMessage*>(msg)->left() || !isPosInSelection(c)) {
406 // Move caret
407 if (m_caret != c) {
408 setCaretPos(c);
409 is_dirty = true;
410 invalidate();
411 }
412
413 // Move selection
414 if (m_recent_focused) {
415 m_recent_focused = false;
416 m_select = m_caret;
417 }
418 // Deselect
419 else if (msg->type() == kMouseDownMessage) {
420 m_select = m_caret;
421 }
422 // Continue selecting words
423 else if (!m_selecting_words.isEmpty()) {
424 Range toWord = wordRange(m_caret);
425 if (toWord.from < m_selecting_words.from) {
426 m_select = std::max(m_selecting_words.to, toWord.to);
427 setCaretPos(std::min(m_selecting_words.from, toWord.from));
428 }
429 else {
430 m_select = std::min(m_selecting_words.from, toWord.from);
431 setCaretPos(std::max(m_selecting_words.to, toWord.to));
432 }
433 }
434 }
435
436 // Show the caret
437 if (is_dirty) {
438 if (shouldStartTimer(true))
439 startTimer();
440 m_state = true;
441 }
442
443 return true;
444 }
445 break;
446
447 case kMouseUpMessage:
448 if (hasCapture()) {
449 releaseMouse();
450
451 if (!m_selecting_words.isEmpty())
452 m_selecting_words.reset();
453
454 MouseMessage* mouseMsg = static_cast<MouseMessage*>(msg);
455 if (mouseMsg->right()) {
456 // This flag is disabled in kFocusEnterMessage message handler.
457 m_lock_selection = true;
458
459 showEditPopupMenu(mouseMsg->position());
460 requestFocus();
461 }
462 }
463 return true;
464
465 case kDoubleClickMessage:
466 if (!hasFocus())
467 requestFocus();
468
469 m_selecting_words = wordRange(m_caret);
470 selectText(m_selecting_words.from, m_selecting_words.to);
471
472 // Capture mouse to continue selecting words on kMouseMoveMessage
473 captureMouse();
474 return true;
475
476 case kMouseEnterMessage:
477 case kMouseLeaveMessage:
478 // TODO theme stuff
479 if (isEnabled())
480 invalidate();
481 break;
482 }
483
484 return Widget::onProcessMessage(msg);
485}
486
487// static
488gfx::Size Entry::sizeHintWithText(Entry* entry,
489 const std::string& text)
490{
491 int w =
492 entry->font()->textLength(text) +
493 + 2*entry->theme()->getEntryCaretSize(entry).w
494 + entry->border().width();
495
496 w = std::min(w, entry->display()->workareaSizeUIScale().w/2);
497
498 int h =
499 + entry->font()->height()
500 + entry->border().height();
501
502 return gfx::Size(w, h);
503}
504
505void Entry::onSizeHint(SizeHintEvent& ev)
506{
507 int trailing = font()->textLength(getSuffix());
508 trailing = std::max(trailing, 2*theme()->getEntryCaretSize(this).w);
509
510 int w =
511 font()->textLength("w") * std::min(m_maxsize, 6) +
512 + trailing
513 + border().width();
514
515 w = std::min(w, display()->workareaSizeUIScale().w/2);
516
517 int h =
518 + font()->height()
519 + border().height();
520
521 ev.setSizeHint(w, h);
522}
523
524void Entry::onPaint(PaintEvent& ev)
525{
526 theme()->paintEntry(ev);
527}
528
529void Entry::onSetText()
530{
531 Widget::onSetText();
532 recalcCharBoxes(text());
533
534 int textlen = lastCaretPos();
535 if (m_caret >= 0 && m_caret > textlen)
536 m_caret = textlen;
537}
538
539void Entry::onChange()
540{
541 Change();
542}
543
544gfx::Rect Entry::onGetEntryTextBounds() const
545{
546 gfx::Rect bounds = clientBounds();
547 bounds.x += border().left();
548 bounds.y += bounds.h/2 - textHeight()/2;
549 bounds.w -= border().width();
550 bounds.h = textHeight();
551 return bounds;
552}
553
554int Entry::getCaretFromMouse(MouseMessage* mousemsg)
555{
556 int mouseX = mousemsg->position().x;
557 if (mouseX < bounds().x+border().left()) {
558 // Scroll to the left
559 return std::max(0, m_scroll-1);
560 }
561
562 int lastPos = lastCaretPos();
563 int i = std::min(m_scroll, lastPos);
564 for (; i<lastPos; ++i) {
565 int segmentWidth = 0;
566 int indexBox = 0;
567 for (int j=m_scroll; j<i; ++j) {
568 segmentWidth += m_boxes[j].width;
569 indexBox = j+1;
570 }
571
572 int x = bounds().x + border().left() + segmentWidth + m_boxes[indexBox].width / 2;
573
574 if (mouseX > bounds().x2() - border().right()) {
575 if (x >= bounds().x2() - border().right()) {
576 // Scroll to the right
577 break;
578 }
579 }
580 else if (x > mouseX)
581 break;
582 }
583
584 return std::clamp(i, 0, lastPos);
585}
586
587void Entry::executeCmd(EntryCmd cmd, int unicodeChar, bool shift_pressed)
588{
589 std::string text = this->text();
590 const Range range = selectedRange();
591
592 switch (cmd) {
593
594 case EntryCmd::NoOp:
595 break;
596
597 case EntryCmd::InsertChar:
598 // delete the entire selection
599 if (!range.isEmpty()) {
600 deleteRange(range, text);
601
602 // We set the caret to the beginning of the erased selection,
603 // needed to show the first inserted character in case
604 // m_scroll > m_caret. E.g. we select all text and insert a
605 // new character to replace the whole text, the new inserted
606 // character makes m_caret=1, so m_scroll will be 1 too, but
607 // we need to make m_scroll=0 to show the new inserted char.)
608 // In this way, we first ensure a m_scroll value enough to
609 // show the new inserted character.
610 recalcCharBoxes(text);
611 setCaretPos(m_caret);
612 }
613
614 // Convert the unicode character -> wstring -> utf-8 string -> insert the utf-8 string
615 if (lastCaretPos() < m_maxsize) {
616 ASSERT(m_caret <= lastCaretPos());
617
618 std::wstring unicodeStr;
619 unicodeStr.push_back(unicodeChar);
620
621 text.insert(m_boxes[m_caret].from,
622 base::to_utf8(unicodeStr));
623 recalcCharBoxes(text);
624 ++m_caret;
625 }
626
627 m_select = -1;
628 break;
629
630 case EntryCmd::BackwardChar:
631 case EntryCmd::BackwardWord:
632 // selection
633 if (shift_pressed) {
634 if (m_select < 0)
635 m_select = m_caret;
636 }
637 else
638 m_select = -1;
639
640 // backward word
641 if (cmd == EntryCmd::BackwardWord) {
642 backwardWord();
643 }
644 // backward char
645 else if (m_caret > 0) {
646 m_caret--;
647 }
648 break;
649
650 case EntryCmd::ForwardChar:
651 case EntryCmd::ForwardWord:
652 // selection
653 if (shift_pressed) {
654 if (m_select < 0)
655 m_select = m_caret;
656 }
657 else
658 m_select = -1;
659
660 // forward word
661 if (cmd == EntryCmd::ForwardWord) {
662 forwardWord();
663 }
664 // forward char
665 else if (m_caret < (int)text.size()) {
666 m_caret++;
667 }
668 break;
669
670 case EntryCmd::BeginningOfLine:
671 // selection
672 if (shift_pressed) {
673 if (m_select < 0)
674 m_select = m_caret;
675 }
676 else
677 m_select = -1;
678
679 m_caret = 0;
680 break;
681
682 case EntryCmd::EndOfLine:
683 // selection
684 if (shift_pressed) {
685 if (m_select < 0)
686 m_select = m_caret;
687 }
688 else
689 m_select = -1;
690
691 m_caret = lastCaretPos();
692 break;
693
694 case EntryCmd::DeleteForward:
695 case EntryCmd::Cut:
696 // delete the entire selection
697 if (!range.isEmpty()) {
698 // *cut* text!
699 if (cmd == EntryCmd::Cut)
700 set_clipboard_text(selectedText());
701
702 // remove text
703 deleteRange(range, text);
704 }
705 // delete the next character
706 else {
707 if (m_caret < (int)text.size())
708 text.erase(m_boxes[m_caret].from,
709 m_boxes[m_caret].to - m_boxes[m_caret].from);
710 }
711
712 m_select = -1;
713 break;
714
715 case EntryCmd::Paste: {
716 std::string clipboard;
717 if (get_clipboard_text(clipboard)) {
718 // delete the entire selection
719 if (!range.isEmpty()) {
720 deleteRange(range, text);
721 m_select = -1;
722 }
723
724 // Paste text
725 recalcCharBoxes(text);
726 int oldBoxes = m_boxes.size();
727
728 text.insert(m_boxes[m_caret].from, clipboard);
729
730 // Remove extra chars that do not fit in m_maxsize
731 recalcCharBoxes(text);
732 if (lastCaretPos() > m_maxsize) {
733 text.erase(m_boxes[m_maxsize].from,
734 text.size() - m_boxes[m_maxsize].from);
735 recalcCharBoxes(text);
736 }
737
738 int newBoxes = m_boxes.size();
739 setCaretPos(m_caret+(newBoxes - oldBoxes));
740 }
741 break;
742 }
743
744 case EntryCmd::Copy:
745 if (!range.isEmpty())
746 set_clipboard_text(selectedText());
747 break;
748
749 case EntryCmd::DeleteBackward:
750 // delete the entire selection
751 if (!range.isEmpty()) {
752 deleteRange(range, text);
753 }
754 // delete the previous character
755 else {
756 if (m_caret > 0) {
757 --m_caret;
758 text.erase(m_boxes[m_caret].from,
759 m_boxes[m_caret].to - m_boxes[m_caret].from);
760 }
761 }
762
763 m_select = -1;
764 break;
765
766 case EntryCmd::DeleteBackwardWord:
767 m_select = m_caret;
768 backwardWord();
769 if (m_caret < m_select) {
770 text.erase(m_boxes[m_caret].from,
771 m_boxes[m_select-1].to - m_boxes[m_caret].from);
772 }
773 m_select = -1;
774 break;
775
776 case EntryCmd::DeleteForwardToEndOfLine:
777 text.erase(m_boxes[m_caret].from);
778 break;
779
780 case EntryCmd::SelectAll:
781 selectAllText();
782 break;
783 }
784
785 if (text != this->text()) {
786 setText(text);
787 onChange();
788 }
789
790 setCaretPos(m_caret);
791 invalidate();
792}
793
794void Entry::forwardWord()
795{
796 int textlen = lastCaretPos();
797
798 for (; m_caret < textlen; ++m_caret) {
799 if (is_word_char(m_boxes[m_caret].codepoint))
800 break;
801 }
802
803 for (; m_caret < textlen; ++m_caret) {
804 if (!is_word_char(m_boxes[m_caret].codepoint))
805 break;
806 }
807}
808
809void Entry::backwardWord()
810{
811 for (--m_caret; m_caret >= 0; --m_caret) {
812 if (is_word_char(m_boxes[m_caret].codepoint))
813 break;
814 }
815
816 for (; m_caret >= 0; --m_caret) {
817 if (!is_word_char(m_boxes[m_caret].codepoint)) {
818 ++m_caret;
819 break;
820 }
821 }
822
823 if (m_caret < 0)
824 m_caret = 0;
825}
826
827Entry::Range Entry::wordRange(int pos)
828{
829 const int last = lastCaretPos();
830 pos = std::clamp(pos, 0, last);
831
832 int i, j;
833 i = j = pos;
834
835 // Select word space
836 if (is_word_char(m_boxes[pos].codepoint)) {
837 for (; i>=0; --i) {
838 if (!is_word_char(m_boxes[i].codepoint))
839 break;
840 }
841 ++i;
842 for (; j<=last; ++j) {
843 if (!is_word_char(m_boxes[j].codepoint))
844 break;
845 }
846 }
847 // Select punctuation space
848 else {
849 for (; i>=0; --i) {
850 if (is_word_char(m_boxes[i].codepoint))
851 break;
852 }
853 ++i;
854 for (; j<=last; ++j) {
855 if (is_word_char(m_boxes[j].codepoint))
856 break;
857 }
858 }
859 return Range(i, j);
860}
861
862bool Entry::isPosInSelection(int pos)
863{
864 return (pos >= std::min(m_caret, m_select) &&
865 pos <= std::max(m_caret, m_select));
866}
867
868void Entry::showEditPopupMenu(const gfx::Point& pt)
869{
870 Menu menu;
871 MenuItem cut("Cut");
872 MenuItem copy("Copy");
873 MenuItem paste("Paste");
874 menu.addChild(&cut);
875 menu.addChild(&copy);
876 menu.addChild(&paste);
877 cut.Click.connect([this]{ executeCmd(EntryCmd::Cut, 0, false); });
878 copy.Click.connect([this]{ executeCmd(EntryCmd::Copy, 0, false); });
879 paste.Click.connect([this]{ executeCmd(EntryCmd::Paste, 0, false); });
880
881 if (isReadOnly()) {
882 cut.setEnabled(false);
883 paste.setEnabled(false);
884 }
885
886 menu.showPopup(pt, display());
887}
888
889class Entry::CalcBoxesTextDelegate : public os::DrawTextDelegate {
890public:
891 CalcBoxesTextDelegate(const int end) : m_end(end) {
892 }
893
894 const Entry::CharBoxes& boxes() const { return m_boxes; }
895
896 void preProcessChar(const int index,
897 const int codepoint,
898 gfx::Color& fg,
899 gfx::Color& bg,
900 const gfx::Rect& charBounds) override {
901 if (!m_boxes.empty())
902 m_boxes.back().to = index;
903
904 m_box = CharBox();
905 m_box.codepoint = codepoint;
906 m_box.from = index;
907 m_box.to = m_end;
908 }
909
910 bool preDrawChar(const gfx::Rect& charBounds) override {
911 m_box.width = charBounds.w;
912 return true;
913 }
914
915 void postDrawChar(const gfx::Rect& charBounds) override {
916 m_boxes.push_back(m_box);
917 }
918
919private:
920 Entry::CharBox m_box;
921 Entry::CharBoxes m_boxes;
922 int m_end;
923};
924
925void Entry::recalcCharBoxes(const std::string& text)
926{
927 int lastTextIndex = int(text.size());
928 CalcBoxesTextDelegate delegate(lastTextIndex);
929 os::draw_text(nullptr, font(), text,
930 gfx::ColorNone, gfx::ColorNone, 0, 0, &delegate);
931 m_boxes = delegate.boxes();
932
933 if (!m_boxes.empty()) {
934 m_boxes.back().to = lastTextIndex;
935 }
936
937 // A last box for the last position
938 CharBox box;
939 box.codepoint = 0;
940 box.from = box.to = lastTextIndex;
941 m_boxes.push_back(box);
942}
943
944bool Entry::shouldStartTimer(bool hasFocus)
945{
946 return (!m_hidden && hasFocus && isEnabled());
947}
948
949void Entry::deleteRange(const Range& range, std::string& text)
950{
951 text.erase(m_boxes[range.from].from,
952 m_boxes[range.to-1].to - m_boxes[range.from].from);
953 m_caret = range.from;
954}
955
956void Entry::startTimer()
957{
958 if (s_timer)
959 s_timer->stop();
960 s_timer = std::make_unique<Timer>(500, this);
961 s_timer->start();
962}
963
964void Entry::stopTimer()
965{
966 if (s_timer) {
967 s_timer->stop();
968 s_timer.reset();
969 }
970}
971
972} // namespace ui
973