1 ///
2 module fluid.text_input;
3 
4 import std.uni;
5 import std.utf;
6 import std.range;
7 import std.string;
8 import std.traits;
9 import std.datetime;
10 import std.algorithm;
11 import std.container.dlist;
12 
13 import fluid.node;
14 import fluid.text;
15 import fluid.input;
16 import fluid.label;
17 import fluid.style;
18 import fluid.utils;
19 import fluid.scroll;
20 import fluid.actions;
21 import fluid.backend;
22 import fluid.structs;
23 import fluid.typeface;
24 import fluid.popup_frame;
25 
26 
27 @safe:
28 
29 
30 /// Constructor parameter, enables multiline input in `TextInput`.
31 auto multiline(bool value = true) {
32 
33     struct Multiline {
34 
35         bool value;
36 
37         void apply(TextInput node) {
38             node.multiline = value;
39         }
40 
41     }
42 
43     return Multiline(value);
44 
45 }
46 
47 
48 /// Text input field.
49 alias textInput = nodeBuilder!TextInput;
50 alias lineInput = nodeBuilder!TextInput;
51 alias multilineInput = nodeBuilder!(TextInput, (a) {
52     a.multiline = true;
53 });
54 
55 /// ditto
56 class TextInput : InputNode!Node, FluidScrollable {
57 
58     mixin enableInputActions;
59 
60     public {
61 
62         /// Size of the field.
63         auto size = Vector2(200, 0);
64 
65         /// A placeholder text for the field, displayed when the field is empty. Style using `emptyStyle`.
66         Rope placeholder;
67 
68         /// Time of the last interaction with the input.
69         SysTime lastTouch;
70 
71         /// Reference horizontal (X) position for vertical movement. Relative to the input's top-left corner.
72         ///
73         /// To make sure that vertical navigation via up/down arrows stays in the same column as it traverses lines of
74         /// varying width, a reference position is saved to match the visual position of the last column. This is then
75         /// used to match against characters on adjacent lines to find the one that is the closest. This ensures that
76         /// even if cursor travels a large vertical distance, it stays close to the original horizontal position,
77         /// without sliding to the left or right in the process.
78         ///
79         /// `horizontalAnchor` is updated any time the cursor moves horizontally, including mouse navigation.
80         float horizontalAnchor;
81 
82         /// Context menu for this input.
83         PopupFrame contextMenu;
84 
85         /// Underlying label controlling the content.
86         ContentLabel contentLabel;
87 
88         /// Maximum entries in the history.
89         int maxHistorySize = 256;
90 
91     }
92 
93     protected {
94 
95         static struct HistoryEntry {
96 
97             Rope value;
98             size_t selectionStart;
99             size_t selectionEnd;
100 
101             /// If true, the entry results from an action that was executed immediately after the last action, without
102             /// changing caret position in the meantime.
103             bool isContinuous;
104 
105             /// Change made by this entry.
106             ///
107             /// `first` and `second` should represent the old and new value respectively; `second` is effectively a
108             /// substring of `value`.
109             Rope.DiffRegion diff;
110 
111             /// A history entry is "additive" if it adds any new content to the input. An entry is "subtractive" if it
112             /// removes any part of the input. An entry that replaces content is simultaneously additive and
113             /// subtractive.
114             ///
115             /// See_Also: `setPreviousEntry`, `canMergeWith`
116             bool isAdditive;
117 
118             /// ditto.
119             bool isSubtractive;
120 
121             /// Set `isAdditive` and `isSubtractive` based on the given text representing the last input.
122             void setPreviousEntry(HistoryEntry entry) {
123 
124                 setPreviousEntry(entry.value);
125 
126             }
127 
128             /// ditto
129             void setPreviousEntry(Rope previousValue) {
130 
131                 this.diff = previousValue.diff(value);
132                 this.isAdditive = diff.second.length != 0;
133                 this.isSubtractive = diff.first.length != 0;
134 
135             }
136 
137             /// Check if this entry can be merged with (newer) entry given its text content. This is used to combine
138             /// runs of similar actions together, for example when typing a word, the whole word will form a single
139             /// entry, instead of creating separate entries per character.
140             ///
141             /// Two entries can be combined if they are:
142             ///
143             /// 1. Both additive, and the latter is not subtractive. This combines runs input, including if the first
144             ///    item in the run replaces some text. However, replacing text will break an existing chain of actions.
145             /// 2. Both subtractive, and neither is additive.
146             ///
147             /// See_Also: `isAdditive`
148             bool canMergeWith(Rope nextValue) const {
149 
150                 // Create a dummy entry based on the text
151                 auto nextEntry = HistoryEntry(nextValue, 0, 0);
152                 nextEntry.setPreviousEntry(value);
153 
154                 return canMergeWith(nextEntry);
155 
156             }
157 
158             /// ditto
159             bool canMergeWith(HistoryEntry nextEntry) const {
160 
161                 const mergeAdditive = this.isAdditive
162                     && nextEntry.isAdditive
163                     && !nextEntry.isSubtractive;
164 
165                 if (mergeAdditive) return true;
166 
167                 const mergeSubtractive = !this.isAdditive
168                     && this.isSubtractive
169                     && !nextEntry.isAdditive
170                     && nextEntry.isSubtractive;
171 
172                 return mergeSubtractive;
173 
174             }
175 
176         }
177 
178         /// If true, current movement action is performed while selecting.
179         bool selectionMovement;
180 
181         /// Last padding box assigned to this node, with scroll applied.
182         Rectangle _inner = Rectangle(0, 0, 0, 0);
183 
184         /// If true, the caret index has not changed since last `pushSnapshot`.
185         bool _isContinuous;
186 
187         /// Current action history, expressed as two stacks, indicating undoable and redoable actions, controllable via
188         /// `snapshot`, `pushSnapshot`, `undo` and `redo`.
189         DList!HistoryEntry _undoStack;
190 
191         /// ditto
192         DList!HistoryEntry _redoStack;
193 
194     }
195 
196     private {
197 
198         /// Action used to keep the text input in view.
199         ScrollIntoViewAction _scrollAction;
200 
201         /// Buffer used to store recently inserted text.
202         /// See_Also: `buffer`
203         char[] _buffer;
204 
205         /// Number of bytes stored in the bufer.
206         size_t _usedBufferSize;
207 
208         /// Node the buffer is stored in.
209         RopeNode* _bufferNode;
210         invariant(_bufferNode is null || _bufferNode.value.sameTail(_buffer[0 .. _usedBufferSize]),
211             "_bufferNode must be in sync with _buffer");
212 
213         /// Value of the text input.
214         Rope _value;
215 
216         /// Available horizontal space.
217         float _availableWidth = float.nan;
218 
219         /// Visual position of the caret.
220         Vector2 _caretPosition;
221 
222         /// Index of the caret.
223         ptrdiff_t _caretIndex;
224 
225         /// Reference point; beginning of selection. Set to -1 if there is no start.
226         ptrdiff_t _selectionStart;
227 
228         /// Current horizontal visual offset of the label.
229         float _scroll = 0;
230 
231     }
232 
233     /// Create a text input.
234     /// Params:
235     ///     placeholder = Placeholder text for the field.
236     ///     submitted   = Callback for when the field is submitted.
237     this(string placeholder = "", void delegate() @trusted submitted = null) {
238 
239         import fluid.button;
240 
241         this.placeholder = placeholder;
242         this.submitted = submitted;
243         this.lastTouch = Clock.currTime;
244         this.contentLabel = new typeof(contentLabel);
245 
246         // Make single line the default
247         contentLabel.isWrapDisabled = true;
248 
249         // Enable edit mode
250         contentLabel.text.hasFastEdits = true;
251 
252         // Create the context menu
253         this.contextMenu = popupFrame(
254             button(
255                 .layout!"fill",
256                 "Cut",
257                 delegate {
258                     cut();
259                     contextMenu.hide();
260                 }
261             ),
262             button(
263                 .layout!"fill",
264                 "Copy",
265                 delegate {
266                     copy();
267                     contextMenu.hide();
268                 }
269             ),
270             button(
271                 .layout!"fill",
272                 "Paste",
273                 delegate {
274                     paste();
275                     contextMenu.hide();
276                 }
277             ),
278         );
279 
280     }
281 
282     static class ContentLabel : Label {
283 
284         this() {
285 
286             super("");
287 
288         }
289 
290         override bool hoveredImpl(Rectangle, Vector2) const {
291 
292             return false;
293 
294         }
295 
296         override void drawImpl(Rectangle outer, Rectangle inner) {
297 
298             // Don't draw background
299             const style = pickStyle();
300             text.draw(style, inner.start);
301 
302         }
303 
304         override void reloadStyles() {
305 
306             // Do not load styles
307 
308         }
309 
310         override Style pickStyle() {
311 
312             // Always use default style
313             return style;
314 
315         }
316 
317     }
318 
319     /// Mark the text input as modified.
320     void touch() {
321 
322         lastTouch = Clock.currTime;
323         scrollIntoView();
324 
325     }
326 
327     /// Mark the text input as modified and fire the "changed" event.
328     void touchText() {
329 
330         touch();
331         if (changed) changed();
332 
333     }
334 
335     /// Scroll ancestors so the text input becomes visible.
336     ///
337     /// `TextInput` keeps its own instance of `ScrollIntoViewAction`, reusing it every time it is needed.
338     ///
339     /// Params:
340     ///     alignToTop = If true, the top of the element will be aligned to the top of the scrollable area.
341     ScrollIntoViewAction scrollIntoView(bool alignToTop = false) {
342 
343         // Create the action
344         if (!_scrollAction) {
345             _scrollAction = .scrollIntoView(this, alignToTop);
346         }
347         else {
348             _scrollAction.reset(alignToTop);
349             queueAction(_scrollAction);
350         }
351 
352         return _scrollAction;
353 
354     }
355 
356     /// Value written in the input.
357     inout(Rope) value() inout {
358 
359         return _value;
360 
361     }
362 
363     /// ditto
364     Rope value(Rope newValue) {
365 
366         auto withoutLineFeeds = Typeface.lineSplitter(newValue).joiner;
367 
368         // Single line mode — filter vertical space out
369         if (!multiline && !newValue.equal(withoutLineFeeds)) {
370 
371             newValue = Typeface.lineSplitter(newValue).join(' ');
372 
373         }
374 
375         _value = newValue;
376         _bufferNode = null;
377         updateSize();
378         return value;
379 
380     }
381 
382     /// ditto
383     Rope value(const(char)[] value) {
384 
385         return this.value(Rope(value));
386 
387     }
388 
389     /// If true, this input is currently empty.
390     bool isEmpty() const {
391 
392         return value == "";
393 
394     }
395 
396     /// If true, this input accepts multiple inputs in the input; pressing "enter" will start a new line.
397     ///
398     /// Even if multiline is off, the value may still contain line feeds if inserted from code.
399     bool multiline() const {
400 
401         return !contentLabel.isWrapDisabled;
402 
403     }
404 
405     /// ditto
406     bool multiline(bool value) {
407 
408         contentLabel.isWrapDisabled = !value;
409         return value;
410 
411     }
412 
413     /// Current horizontal visual offset of the label.
414     float scroll() const {
415 
416         return _scroll;
417 
418     }
419 
420     /// Set scroll value.
421     float scroll(float value) {
422 
423         const limit = max(0, contentLabel.minSize.x - _inner.w);
424 
425         return _scroll = value.clamp(0, limit);
426 
427     }
428 
429     ///
430     bool canScroll(Vector2 value) const {
431 
432         return clamp(scroll + value.x, 0, _availableWidth) != scroll;
433 
434     }
435 
436     /// Get the current style for the label.
437     /// Params:
438     ///     style = Current style of the TextInput.
439     Style pickLabelStyle(Style style) {
440 
441         // Pick style from the input
442         auto result = style;
443 
444         // Remove spacing
445         result.margin = 0;
446         result.padding = 0;
447         result.border = 0;
448 
449         return result;
450 
451     }
452 
453     final Style pickLabelStyle() {
454 
455         return pickLabelStyle(pickStyle);
456 
457     }
458 
459     /// Get or set text preceding the caret.
460     Rope valueBeforeCaret() const {
461 
462         return value[0 .. caretIndex];
463 
464     }
465 
466     /// ditto
467     Rope valueBeforeCaret(Rope newValue) {
468 
469         // Replace the data
470         if (valueAfterCaret.empty)
471             value = newValue;
472         else
473             value = newValue ~ valueAfterCaret;
474 
475         caretIndex = newValue.length;
476         updateSize();
477 
478         return value[0 .. caretIndex];
479 
480     }
481 
482     /// ditto
483     Rope valueBeforeCaret(const(char)[] newValue) {
484 
485         return valueBeforeCaret(Rope(newValue));
486 
487     }
488 
489     /// Get or set currently selected text.
490     Rope selectedValue() inout {
491 
492         return value[selectionLowIndex .. selectionHighIndex];
493 
494     }
495 
496     /// ditto
497     Rope selectedValue(Rope newValue) {
498 
499         const isLow = caretIndex == selectionStart;
500         const low = selectionLowIndex;
501         const high = selectionHighIndex;
502 
503         value = value.replace(low, high, newValue);
504         caretIndex = low + newValue.length;
505         updateSize();
506         clearSelection();
507 
508         return value[low .. low + newValue.length];
509 
510     }
511 
512     /// ditto
513     Rope selectedValue(const(char)[] newValue) {
514 
515         return selectedValue(Rope(newValue));
516 
517     }
518 
519     /// Get or set text following the caret.
520     Rope valueAfterCaret() inout {
521 
522         return value[caretIndex .. $];
523 
524     }
525 
526     /// ditto
527     Rope valueAfterCaret(Rope newValue) {
528 
529         // Replace the data
530         if (valueBeforeCaret.empty)
531             value = newValue;
532         else
533             value = valueBeforeCaret ~ newValue;
534 
535         updateSize();
536 
537         return value[caretIndex .. $];
538 
539     }
540 
541     /// ditto
542     Rope valueAfterCaret(const(char)[] value) {
543 
544         return valueAfterCaret(Rope(value));
545 
546     }
547 
548     unittest {
549 
550         auto root = textInput();
551 
552         root.value = "hello wörld!";
553         assert(root.value == "hello wörld!");
554 
555         root.value = "hello wörld!\n";
556         assert(root.value == "hello wörld! ");
557 
558         root.value = "hello wörld!\r\n";
559         assert(root.value == "hello wörld! ");
560 
561         root.value = "hello wörld!\v";
562         assert(root.value == "hello wörld! ");
563 
564     }
565 
566     unittest {
567 
568         auto root = textInput(.multiline);
569 
570         root.value = "hello wörld!";
571         assert(root.value == "hello wörld!");
572 
573         root.value = "hello wörld!\n";
574         assert(root.value == "hello wörld!\n");
575 
576         root.value = "hello wörld!\r\n";
577         assert(root.value == "hello wörld!\r\n");
578 
579         root.value = "hello wörld!\v";
580         assert(root.value == "hello wörld!\v");
581 
582     }
583 
584     /// Visual position of the caret, relative to the top-left corner of the input.
585     Vector2 caretPosition() const {
586 
587         // Calculated in caretPositionImpl
588         return _caretPosition;
589 
590     }
591 
592     /// Index of the character, byte-wise.
593     ptrdiff_t caretIndex() const {
594 
595         return _caretIndex.clamp(0, value.length);
596 
597     }
598 
599     /// ditto
600     ptrdiff_t caretIndex(ptrdiff_t index) {
601 
602         if (!isSelecting) {
603             _selectionStart = index;
604         }
605 
606         touch();
607         _bufferNode = null;
608         _isContinuous = false;
609         return _caretIndex = index;
610 
611     }
612 
613     /// If true, there's an active selection.
614     bool isSelecting() const {
615 
616         return selectionStart != caretIndex || selectionMovement;
617 
618     }
619 
620     /// Low index of the selection, left boundary, first index.
621     ptrdiff_t selectionLowIndex() const {
622 
623         return min(selectionStart, selectionEnd);
624 
625     }
626 
627     /// High index of the selection, right boundary, second index.
628     ptrdiff_t selectionHighIndex() const {
629 
630         return max(selectionStart, selectionEnd);
631 
632     }
633 
634     /// Point where selection begins. Caret is the other end of the selection.
635     ///
636     /// Note that `selectionStart` may be greater than `selectionEnd`. If you need them in order, use
637     /// `selectionLowIndex` and `selectionHighIndex`.
638     ptrdiff_t selectionStart() const {
639 
640         // Selection is present
641         return _selectionStart.clamp(0, value.length);
642 
643     }
644 
645     /// ditto
646     ptrdiff_t selectionStart(ptrdiff_t value) {
647 
648         return _selectionStart = value;
649 
650     }
651 
652     /// Point where selection ends. Corresponds to caret position.
653     alias selectionEnd = caretIndex;
654 
655     /// Select a part of text. This is preferred to setting `selectionStart` & `selectionEnd` directly, since the two
656     /// properties are synchronized together and a change might be ignored.
657     void selectSlice(size_t start, size_t end)
658     in (end <= value.length, format!"Slice [%s .. %s] exceeds textInput value length of %s"(start, end, value.length))
659     do {
660 
661         selectionEnd = end;
662         selectionStart = start;
663 
664     }
665 
666     unittest {
667 
668         auto root = textInput();
669         root.value = "foo bar baz";
670         root.selectSlice(0, 3);
671         assert(root.selectedValue == "foo");
672 
673         root.caretIndex = 4;
674         root.selectSlice(4, 7);
675         assert(root.selectedValue == "bar");
676 
677         root.caretIndex = 11;
678         root.selectSlice(8, 11);
679         assert(root.selectedValue == "baz");
680 
681     }
682 
683     ///
684     void clearSelection() {
685 
686         _selectionStart = _caretIndex;
687 
688     }
689 
690     /// Clear selection if selection movement is disabled.
691     protected void moveOrClearSelection() {
692 
693         if (!selectionMovement) {
694 
695             clearSelection();
696 
697         }
698 
699     }
700 
701     protected override void resizeImpl(Vector2 area) {
702 
703         import std.math : isNaN;
704 
705         // Set the size
706         minSize = size;
707 
708         // Set the label text
709         contentLabel.text = value == "" ? placeholder : value;
710 
711         const isFill = layout.nodeAlign[0] == NodeAlign.fill;
712 
713         _availableWidth = isFill
714             ? area.x
715             : size.x;
716 
717         const textArea = multiline
718             ? Vector2(_availableWidth, area.y)
719             : Vector2(0, size.y);
720 
721         // Resize the label, and remove the spacing
722         contentLabel.style = pickLabelStyle(style);
723         contentLabel.resize(tree, theme, textArea);
724 
725         const minLines = multiline ? 3 : 1;
726 
727         // Set height to at least the font size, or total text size
728         minSize.y = max(minSize.y, style.getTypeface.lineHeight * minLines, contentLabel.minSize.y);
729 
730         // Locate the cursor
731         updateCaretPosition();
732 
733         // Horizontal anchor is not set, update it
734         if (horizontalAnchor.isNaN)
735             horizontalAnchor = caretPosition.x;
736 
737     }
738 
739     unittest {
740 
741         auto io = new HeadlessBackend(Vector2(800, 600));
742         auto root = textInput(
743             .layout!"fill",
744             .multiline,
745             .nullTheme,
746             "This placeholder exceeds the default size of a text input."
747         );
748 
749         root.io = io;
750         root.draw();
751 
752         Vector2 textSize() {
753 
754             return root.contentLabel.minSize;
755 
756         }
757 
758         assert(textSize.x > 200);
759         assert(textSize.x > root.size.x);
760 
761         io.nextFrame;
762         root.placeholder = "";
763         root.updateSize();
764         root.draw();
765 
766         assert(root.caretPosition.x < 1);
767         assert(textSize.x < 1);
768 
769         io.nextFrame;
770         root.value = "This value exceeds the default size of a text input.";
771         root.updateSize();
772         root.caretToEnd();
773         root.draw();
774 
775         assert(root.caretPosition.x > 200);
776         assert(textSize.x > 200);
777         assert(textSize.x > root.size.x);
778 
779         io.nextFrame;
780         root.value = ("This value is long enough to start a new line in the output. To make sure of it, here's "
781             ~ "some more text. And more.");
782         root.updateSize();
783         root.draw();
784 
785         assert(textSize.x > root.size.x);
786         assert(textSize.x <= 800);
787         assert(textSize.y >= root.style.getTypeface.lineHeight * 2);
788         assert(root.minSize.y >= textSize.y);
789 
790     }
791 
792     /// Update the caret position to match the caret index.
793     ///
794     /// ## preferNextLine
795     ///
796     /// Determines if, in case text wraps over the new line, and the caret is in an ambiguous position, the caret
797     /// will move to the next line, or stay on the previous one. Usually `false`, except for arrow keys and the
798     /// "home" key.
799     ///
800     /// In cases where the text wraps over to the new line due to lack of space, the implied line break creates an
801     /// ambiguous position for the caret. The caret may be placed either at the end of the original line, or be put
802     /// on the newly created line:
803     ///
804     /// ---
805     /// Lorem ipsum dolor sit amet, consectetur |
806     /// |adipiscing elit, sed do eiusmod tempor
807     /// ---
808     ///
809     /// Depending on the situation, either position may be preferable. Keep in mind that the caret position influences
810     /// further movement, particularly when navigating using the down and up arrows. In case the caret is at the
811     /// end of the line, it should stay close to the end, but when it's at the beginning, it should stay close to the
812     /// start.
813     ///
814     /// This is not an issue at all on explicitly created lines, since the caret position is easily decided upon
815     /// depending if it is preceding the line break, or if it's following one. This property is only used for implicitly
816     /// created lines.
817     void updateCaretPosition(bool preferNextLine = false) {
818 
819         import std.math : isNaN;
820 
821         // No available width, waiting for resize
822         if (_availableWidth.isNaN) {
823             _caretPosition.x = float.nan;
824             return;
825         }
826 
827         _caretPosition = caretPositionImpl(_availableWidth, preferNextLine);
828 
829         const scrolledCaret = caretPosition.x - scroll;
830 
831         // Scroll to make sure the caret is always in view
832         const scrollOffset
833             = scrolledCaret > _inner.width ? scrolledCaret - _inner.width
834             : scrolledCaret < 0            ? scrolledCaret
835             : 0;
836 
837         // Set the scroll
838         scroll = multiline
839             ? 0
840             : scroll + scrollOffset;
841 
842     }
843 
844     /// Find the closest index to the given position.
845     /// Returns: Index of the character. The index may be equal to character length.
846     size_t nearestCharacter(Vector2 needle) {
847 
848         import std.math : abs;
849 
850         auto ruler = textRuler();
851         auto typeface = ruler.typeface;
852 
853         struct Position {
854             size_t index;
855             Vector2 position;
856         }
857 
858         /// Returns the position (inside the word) of the character that is the closest to the needle.
859         Position closest(Vector2 startPosition, Vector2 endPosition, const Rope word) {
860 
861             // Needle is before or after the word
862             if (needle.x <= startPosition.x) return Position(0, startPosition);
863             if (needle.x >= endPosition.x) return Position(word.length, endPosition);
864 
865             size_t index;
866             auto match = Position(0, startPosition);
867 
868             // Search inside the word
869             while (index < word.length) {
870 
871                 decode(word[], index);  // index by reference
872 
873                 auto size = typeface.measure(word[0..index]);
874                 auto end = startPosition.x + size.x;
875                 auto half = (match.position.x + end)/2;
876 
877                 // Hit left side of this character, or right side of the previous, return the previous character
878                 if (needle.x < half) break;
879 
880                 match.index = index;
881                 match.position.x = startPosition.x + size.x;
882 
883             }
884 
885             return match;
886 
887         }
888 
889         auto result = Position(0, Vector2(float.infinity, float.infinity));
890 
891         // Search for a matching character on adjacent lines
892         search: foreach (index, line; typeface.lineSplitterIndex(value[])) {
893 
894             ruler.startLine();
895 
896             // Each word is a single, unbreakable unit
897             foreach (word, penPosition; typeface.eachWord(ruler, line, multiline)) {
898 
899                 scope (exit) index += word.length;
900 
901                 // Find the middle of the word to use as a reference for vertical search
902                 const middleY = ruler.caret.center.y;
903 
904                 // Skip this word if the closest match is closer vertically
905                 if (abs(result.position.y - needle.y) < abs(middleY - needle.y)) continue;
906 
907                 // Find the words' closest horizontal position
908                 const newLine = ruler.wordLineIndex == 1;
909                 const startPosition = Vector2(penPosition.x, middleY);
910                 const endPosition = Vector2(ruler.penPosition.x, middleY);
911                 const reference = closest(startPosition, endPosition, word);
912 
913                 // Skip if the closest match is still closer than the chosen reference
914                 if (!newLine && abs(result.position.x - needle.x) < abs(reference.position.x - needle.x)) continue;
915 
916                 // Save the result if it's better
917                 result = reference;
918                 result.index += index;
919 
920             }
921 
922         }
923 
924         return result.index;
925 
926     }
927 
928     /// Get the current buffer.
929     ///
930     /// The buffer is used to store all content inserted through `push`.
931     protected inout(char)[] buffer() inout {
932 
933         return _buffer;
934 
935     }
936 
937     /// Get the used size of the buffer.
938     protected ref inout(size_t) usedBufferSize() inout {
939 
940         return _usedBufferSize;
941 
942     }
943 
944     /// Get the filled part of the buffer.
945     protected inout(char)[] usedBuffer() inout {
946 
947         return _buffer[0 .. _usedBufferSize];
948 
949     }
950 
951     /// Get the empty part of the buffer.
952     protected inout(char)[] freeBuffer() inout {
953 
954         return _buffer[_usedBufferSize .. $];
955 
956     }
957 
958     /// Request a new or a larger buffer.
959     /// Params:
960     ///     minimumSize = Minimum size to allocate for the buffer.
961     protected void newBuffer(size_t minimumSize = 64) {
962 
963         const newSize = max(minimumSize, 64);
964 
965         _buffer = new char[newSize];
966         usedBufferSize = 0;
967 
968     }
969 
970     protected Vector2 caretPositionImpl(float textWidth, bool preferNextLine) {
971 
972         Rope unbreakableChars(Rope value) {
973 
974             // Split on lines
975             auto lines = Typeface.lineSplitter(value);
976             if (lines.empty) return value.init;
977 
978             // Split on words
979             auto chunks = Typeface.defaultWordChunks(lines.front);
980             if (chunks.empty) return value.init;
981 
982             // Return empty string if the result starts with whitespace
983             if (chunks.front.byDchar.front.isWhite) return value.init;
984 
985             // Return first word only
986             return chunks.front;
987 
988         }
989 
990         // Check if the caret follows unbreakable characters
991         const head = unbreakableChars(
992             valueBeforeCaret.wordBack(true)
993         );
994 
995         // If the caret is surrounded by unbreakable characters, include them in the output to make sure the
996         // word is wrapped correctly
997         const tail = preferNextLine || !head.empty
998             ? unbreakableChars(valueAfterCaret)
999             : Rope.init;
1000 
1001         auto typeface = style.getTypeface;
1002         auto ruler = textRuler();
1003         auto slice = value[0 .. caretIndex + tail.length];
1004 
1005         // Measure text until the caret; include the word that follows to keep proper wrapping
1006         typeface.measure(ruler, slice, multiline);
1007 
1008         auto caretPosition = ruler.caret.start;
1009 
1010         // Measure the word itself, and remove it
1011         caretPosition.x -= typeface.measure(tail[]).x;
1012 
1013         return caretPosition;
1014 
1015     }
1016 
1017     protected override void drawImpl(Rectangle outer, Rectangle inner) @trusted {
1018 
1019         auto style = pickStyle();
1020 
1021         // Fill the background
1022         style.drawBackground(tree.io, outer);
1023 
1024         // Copy style to the label
1025         contentLabel.style = pickLabelStyle(style);
1026 
1027         // Scroll the inner rectangle
1028         auto scrolledInner = inner;
1029         scrolledInner.x -= scroll;
1030 
1031         // Save the inner box
1032         _inner = scrolledInner;
1033 
1034         // Increase the size of the inner box so that tree doesn't turn on scissors mode on its own
1035         scrolledInner.w += scroll;
1036 
1037         const lastScissors = tree.pushScissors(outer);
1038         scope (exit) tree.popScissors(lastScissors);
1039 
1040         // Draw the contents
1041         drawContents(inner, scrolledInner);
1042 
1043     }
1044 
1045     protected void drawContents(Rectangle inner, Rectangle scrolledInner) {
1046 
1047         // Draw selection
1048         drawSelection(scrolledInner);
1049 
1050         // Draw the text
1051         contentLabel.draw(scrolledInner);
1052 
1053         // Draw the caret
1054         drawCaret(scrolledInner);
1055 
1056     }
1057 
1058     protected void drawCaret(Rectangle inner) {
1059 
1060         // Ignore the rest if the node isn't focused
1061         if (!isFocused || isDisabledInherited) return;
1062 
1063         // Add a blinking caret
1064         if (showCaret) {
1065 
1066             const lineHeight = style.getTypeface.lineHeight;
1067             const margin = lineHeight / 10f;
1068             const relativeCaretPosition = this.caretPosition();
1069             const caretPosition = start(inner) + relativeCaretPosition;
1070 
1071             // Draw the caret
1072             io.drawLine(
1073                 caretPosition + Vector2(0, margin),
1074                 caretPosition - Vector2(0, margin - lineHeight),
1075                 style.textColor,
1076             );
1077 
1078         }
1079 
1080     }
1081 
1082     override Rectangle focusBoxImpl(Rectangle inner) const {
1083 
1084         const lineHeight = style.getTypeface.lineHeight;
1085         const position = inner.start + caretPosition;
1086 
1087         return Rectangle(
1088             position.tupleof,
1089             1, lineHeight
1090         );
1091 
1092     }
1093 
1094     /// Get an appropriate text ruler for this input.
1095     protected TextRuler textRuler() {
1096 
1097         return TextRuler(style.getTypeface, multiline ? _availableWidth : float.nan);
1098 
1099     }
1100 
1101     /// Draw selection, if applicable.
1102     protected void drawSelection(Rectangle inner) {
1103 
1104         // Ignore if selection is empty
1105         if (selectionStart == selectionEnd) return;
1106 
1107         const low = selectionLowIndex;
1108         const high = selectionHighIndex;
1109 
1110         auto style = pickStyle();
1111         auto typeface = style.getTypeface;
1112         auto ruler = textRuler();
1113 
1114         Vector2 lineStart;
1115         Vector2 lineEnd;
1116 
1117         // Run through the text
1118         foreach (index, line; typeface.lineSplitterIndex(value)) {
1119 
1120             ruler.startLine();
1121 
1122             // Each word is a single, unbreakable unit
1123             foreach (word, penPosition; typeface.eachWord(ruler, line, multiline)) {
1124 
1125                 const caret = ruler.caret(penPosition);
1126                 const startIndex = index;
1127                 const endIndex = index = index + word.length;
1128 
1129                 const newLine = ruler.wordLineIndex == 1;
1130 
1131                 scope (exit) lineEnd = ruler.caret.end;
1132 
1133                 // New line started, flush the line
1134                 if (newLine && startIndex > low) {
1135 
1136                     const rect = Rectangle(
1137                         (inner.start + lineStart).tupleof,
1138                         (lineEnd - lineStart).tupleof
1139                     );
1140 
1141                     lineStart = caret.start;
1142                     io.drawRectangle(rect, style.selectionBackgroundColor);
1143 
1144                 }
1145 
1146                 // Selection starts here
1147                 if (startIndex <= low && low <= endIndex) {
1148 
1149                     const dent = typeface.measure(word[0 .. low - startIndex]);
1150 
1151                     lineStart = caret.start + Vector2(dent.x, 0);
1152 
1153                 }
1154 
1155                 // Selection ends here
1156                 if (startIndex <= high && high <= endIndex) {
1157 
1158                     const dent = typeface.measure(word[0 .. high - startIndex]);
1159                     const lineEnd = caret.end + Vector2(dent.x, 0);
1160                     const rect = Rectangle(
1161                         (inner.start + lineStart).tupleof,
1162                         (lineEnd - lineStart).tupleof
1163                     );
1164 
1165                     io.drawRectangle(rect, style.selectionBackgroundColor);
1166                     return;
1167 
1168                 }
1169 
1170             }
1171 
1172         }
1173 
1174     }
1175 
1176     protected bool showCaret() {
1177 
1178         auto timeSecs = (Clock.currTime - lastTouch).total!"seconds";
1179 
1180         // Add a blinking caret if there is no selection
1181         return selectionStart == selectionEnd && timeSecs % 2 == 0;
1182 
1183     }
1184 
1185     protected override bool keyboardImpl() {
1186 
1187         import std.uni : isAlpha, isWhite;
1188         import std.range : back;
1189 
1190         bool changed;
1191 
1192         // Get pressed key
1193         while (true) {
1194 
1195             // Read text
1196             if (const key = io.inputCharacter) {
1197 
1198                 // Append to char arrays
1199                 push(key);
1200                 changed = true;
1201 
1202             }
1203 
1204             // Stop if there's nothing left
1205             else break;
1206 
1207         }
1208 
1209         // Typed something
1210         if (changed) {
1211 
1212             // Trigger the callback
1213             touchText();
1214 
1215             return true;
1216 
1217         }
1218 
1219         return true;
1220 
1221     }
1222 
1223     unittest {
1224 
1225         auto root = textInput();
1226         root.value = "Ho";
1227 
1228         root.caretIndex = 1;
1229         root.push("e");
1230         assert(root.value.byNode.equal(["H", "e", "o"]));
1231 
1232         root.push("n");
1233 
1234         assert(root.value.byNode.equal(["H", "en", "o"]));
1235 
1236         root.push("l");
1237         assert(root.value.byNode.equal(["H", "enl", "o"]));
1238 
1239         // Create enough text to fill the buffer
1240         // A new node should be created as a result
1241         auto bufferFiller = 'A'.repeat(root.freeBuffer.length).array;
1242 
1243         root.push(bufferFiller);
1244         assert(root.value.byNode.equal(["H", "enl", bufferFiller, "o"]));
1245 
1246         // Undo all pushes until the initial fill
1247         root.undo();
1248         assert(root.value == "Ho");
1249         assert(root.valueBeforeCaret == "H");
1250 
1251         // Undo will not clear the initial value
1252         root.undo();
1253         assert(root.value == "Ho");
1254         assert(root.valueBeforeCaret == "H");
1255 
1256         // The above undo does not add a new redo stack entry; effectively, this redo cancels both undo actions above
1257         root.redo();
1258         assert(root.value.byNode.equal(["H", "enl", bufferFiller, "o"]));
1259 
1260     }
1261 
1262     unittest {
1263 
1264         auto io = new HeadlessBackend;
1265         auto root = textInput("placeholder");
1266 
1267         root.io = io;
1268 
1269         // Empty text
1270         {
1271             root.draw();
1272 
1273             assert(root.value == "");
1274             assert(root.contentLabel.text == "placeholder");
1275             assert(root.isEmpty);
1276         }
1277 
1278         // Focus the box and input stuff
1279         {
1280             io.nextFrame;
1281             io.inputCharacter("¡Hola, mundo!");
1282             root.focus();
1283             root.draw();
1284 
1285             assert(root.value == "¡Hola, mundo!");
1286         }
1287 
1288         // The text will be displayed the next frame
1289         {
1290             io.nextFrame;
1291             root.draw();
1292 
1293             assert(root.contentLabel.text == "¡Hola, mundo!");
1294             assert(root.isFocused);
1295         }
1296 
1297     }
1298 
1299     /// Push a character or string to the input.
1300     void push(dchar character) {
1301 
1302         char[4] buffer;
1303 
1304         auto size = buffer.encode(character);
1305         push(buffer[0..size]);
1306 
1307     }
1308 
1309     /// ditto
1310     void push(scope const(char)[] ch)
1311     out (; _bufferNode, "_bufferNode must exist after pushing to buffer")
1312     do {
1313 
1314         // Move the buffer node into here; move it back when done
1315         auto bufferNode = _bufferNode;
1316         _bufferNode = null;
1317         scope (exit) _bufferNode = bufferNode;
1318 
1319         // Not enough space in the buffer, allocate more
1320         if (freeBuffer.length <= ch.length) {
1321 
1322             newBuffer(ch.length);
1323             bufferNode = null;
1324 
1325         }
1326 
1327         auto slice = freeBuffer[0 .. ch.length];
1328 
1329         // Save the data in the buffer
1330         slice[] = ch;
1331         _usedBufferSize += ch.length;
1332 
1333         // Selection is active, overwrite it
1334         if (isSelecting) {
1335 
1336             bufferNode = new RopeNode(slice);
1337             push(Rope(bufferNode));
1338             return;
1339 
1340         }
1341 
1342         // The above `if` handles the one case where `push` doesn't directly add new characters to the text.
1343         // From here, appending can be optimized by memorizing the node we create to add the text, and reusing it
1344         // afterwards. This way, we avoid creating many one element nodes.
1345 
1346         size_t originalLength;
1347 
1348         // Make sure there is a node to write to
1349         if (!bufferNode)
1350             bufferNode = new RopeNode(slice);
1351 
1352         // If writing in a single sequence, reuse the last inserted node
1353         else {
1354 
1355             originalLength = bufferNode.length;
1356 
1357             // Append the character to its value
1358             // The bufferNode will always share tail with the buffer
1359             bufferNode.value = usedBuffer[$ - originalLength - ch.length .. $];
1360 
1361         }
1362 
1363         // Save previous value in undo stack
1364         const previousState = snapshot();
1365         scope (success) pushSnapshot(previousState);
1366 
1367         // Insert the text by replacing the old node, if present
1368         value = value.replace(caretIndex - originalLength, caretIndex, Rope(bufferNode));
1369         assert(value.isBalanced);
1370 
1371         // Move the caret
1372         caretIndex = caretIndex + ch.length;
1373         updateCaretPosition();
1374         horizontalAnchor = caretPosition.x;
1375 
1376     }
1377 
1378     /// ditto
1379     void push(Rope text) {
1380 
1381         // Save previous value in undo stack
1382         const previousState = snapshot();
1383         scope (success) pushSnapshot(previousState);
1384 
1385         // If selection is active, overwrite the selection
1386         if (isSelecting) {
1387 
1388             // Override with the character
1389             selectedValue = text;
1390             clearSelection();
1391 
1392         }
1393 
1394         // Insert the character before caret
1395         else if (valueBeforeCaret.length) {
1396 
1397             valueBeforeCaret = valueBeforeCaret ~ text;
1398             touch();
1399 
1400         }
1401 
1402         else {
1403 
1404             valueBeforeCaret = text;
1405             touch();
1406 
1407         }
1408 
1409         updateCaretPosition();
1410         horizontalAnchor = caretPosition.x;
1411 
1412     }
1413 
1414     /// Start a new line
1415     @(FluidInputAction.breakLine)
1416     protected bool breakLine() {
1417 
1418         if (!multiline) return false;
1419 
1420         auto snap = snapshot();
1421         push('\n');
1422         forcePushSnapshot(snap);
1423 
1424         return true;
1425 
1426     }
1427 
1428     unittest {
1429 
1430         auto root = textInput();
1431 
1432         root.push("hello");
1433         root.runInputAction!(FluidInputAction.breakLine);
1434 
1435         assert(root.value == "hello");
1436 
1437     }
1438 
1439     unittest {
1440 
1441         auto root = textInput(.multiline);
1442 
1443         root.push("hello");
1444         root.runInputAction!(FluidInputAction.breakLine);
1445         assert(root.value == "hello\n");
1446 
1447         root.undo();
1448         assert(root.value == "hello");
1449         root.redo();
1450         assert(root.value == "hello\n");
1451 
1452         root.undo();
1453         assert(root.value == "hello");
1454         root.undo();
1455         assert(root.value == "");
1456         root.redo();
1457         assert(root.value == "hello");
1458         root.redo();
1459         assert(root.value == "hello\n");
1460 
1461     }
1462 
1463     unittest {
1464 
1465         auto root = textInput(.nullTheme, .multiline);
1466 
1467         root.push("Привет, мир!");
1468         root.runInputAction!(FluidInputAction.breakLine);
1469 
1470         assert(root.value == "Привет, мир!\n");
1471         assert(root.caretIndex == root.value.length);
1472 
1473         root.push("Это пример текста для тестирования поддержки Unicode во Fluid.");
1474         root.runInputAction!(FluidInputAction.breakLine);
1475 
1476         assert(root.value == "Привет, мир!\nЭто пример текста для тестирования поддержки Unicode во Fluid.\n");
1477         assert(root.caretIndex == root.value.length);
1478 
1479     }
1480 
1481     unittest {
1482 
1483         auto root = textInput(.multiline);
1484         root.push("first line");
1485         root.breakLine();
1486         root.push("second line");
1487         root.breakLine();
1488         assert(root.value == "first line\nsecond line\n");
1489 
1490         root.undo();
1491         assert(root.value == "first line\nsecond line");
1492         root.undo();
1493         assert(root.value == "first line\n");
1494         root.undo();
1495         assert(root.value == "first line");
1496         root.undo();
1497         assert(root.value == "");
1498         root.redo();
1499         assert(root.value == "first line");
1500         root.redo();
1501         assert(root.value == "first line\n");
1502         root.redo();
1503         assert(root.value == "first line\nsecond line");
1504         root.redo();
1505         assert(root.value == "first line\nsecond line\n");
1506 
1507     }
1508 
1509     /// Submit the input.
1510     @(FluidInputAction.submit)
1511     void submit() {
1512 
1513         import std.sumtype : match;
1514 
1515         // breakLine has higher priority, stop if it's active
1516         if (multiline && tree.isActive!(FluidInputAction.breakLine)) return;
1517 
1518         // Run the callback
1519         if (submitted) submitted();
1520 
1521     }
1522 
1523     unittest {
1524 
1525         int submitted;
1526 
1527         auto io = new HeadlessBackend;
1528         TextInput root;
1529 
1530         root = textInput("placeholder", delegate {
1531             submitted++;
1532             assert(root.value == "Hello World");
1533         });
1534 
1535         root.io = io;
1536 
1537         // Type stuff
1538         {
1539             root.value = "Hello World";
1540             root.focus();
1541             root.updateSize();
1542             root.draw();
1543 
1544             assert(submitted == 0);
1545             assert(root.value == "Hello World");
1546             assert(root.contentLabel.text == "Hello World");
1547         }
1548 
1549         // Submit
1550         {
1551             io.press(KeyboardKey.enter);
1552             root.draw();
1553 
1554             assert(submitted == 1);
1555         }
1556 
1557     }
1558 
1559     unittest {
1560 
1561         int submitted;
1562         auto io = new HeadlessBackend;
1563         auto root = textInput(
1564             .multiline,
1565             "",
1566             delegate {
1567                 submitted++;
1568             }
1569         );
1570 
1571         root.io = io;
1572         root.push("Hello, World!");
1573 
1574         // Press enter (not focused)
1575         io.press(KeyboardKey.enter);
1576         root.draw();
1577 
1578         // No effect
1579         assert(root.value == "Hello, World!");
1580         assert(submitted == 0);
1581 
1582         // Focus for the next frame
1583         io.nextFrame();
1584         root.focus();
1585 
1586         // Press enter
1587         io.press(KeyboardKey.enter);
1588         root.draw();
1589 
1590         // A new line should be added
1591         assert(root.value == "Hello, World!\n");
1592         assert(submitted == 0);
1593 
1594         // Press ctrl+enter
1595         io.nextFrame();
1596         version (OSX)
1597             io.press(KeyboardKey.leftCommand);
1598         else 
1599             io.press(KeyboardKey.leftControl);
1600         io.press(KeyboardKey.enter);
1601         root.draw();
1602 
1603         // Input should be submitted
1604         assert(root.value == "Hello, World!\n");
1605         assert(submitted == 1);
1606 
1607     }
1608 
1609     /// Erase last word before the caret, or the first word after.
1610     ///
1611     /// Parms:
1612     ///     forward = If true, delete the next word. If false, delete the previous.
1613     void chopWord(bool forward = false) {
1614 
1615         import std.uni;
1616         import std.range;
1617 
1618         // Save previous value in undo stack
1619         const previousState = snapshot();
1620         scope (success) pushSnapshot(previousState);
1621 
1622         // Selection active, delete it
1623         if (isSelecting) {
1624 
1625             selectedValue = null;
1626 
1627         }
1628 
1629         // Remove next word
1630         else if (forward) {
1631 
1632             // Find the word to delete
1633             const erasedWord = valueAfterCaret.wordFront;
1634 
1635             // Remove the word
1636             valueAfterCaret = valueAfterCaret[erasedWord.length .. $];
1637 
1638         }
1639 
1640         // Remove previous word
1641         else {
1642 
1643             // Find the word to delete
1644             const erasedWord = valueBeforeCaret.wordBack;
1645 
1646             // Remove the word
1647             valueBeforeCaret = valueBeforeCaret[0 .. $ - erasedWord.length];
1648 
1649         }
1650 
1651         // Update the size of the box
1652         updateSize();
1653         updateCaretPosition();
1654         horizontalAnchor = caretPosition.x;
1655 
1656     }
1657 
1658     unittest {
1659 
1660         auto root = textInput();
1661 
1662         root.push("Это пример текста для тестирования поддержки Unicode во Fluid.");
1663         root.chopWord;
1664         assert(root.value == "Это пример текста для тестирования поддержки Unicode во Fluid");
1665 
1666         root.chopWord;
1667         assert(root.value == "Это пример текста для тестирования поддержки Unicode во ");
1668 
1669         root.chopWord;
1670         assert(root.value == "Это пример текста для тестирования поддержки Unicode ");
1671 
1672         root.chopWord;
1673         assert(root.value == "Это пример текста для тестирования поддержки ");
1674 
1675         root.chopWord;
1676         assert(root.value == "Это пример текста для тестирования ");
1677 
1678         root.caretToStart();
1679         root.chopWord(true);
1680         assert(root.value == "пример текста для тестирования ");
1681 
1682         root.chopWord(true);
1683         assert(root.value == "текста для тестирования ");
1684 
1685     }
1686 
1687     /// Remove a word before the caret.
1688     @(FluidInputAction.backspaceWord)
1689     void backspaceWord() {
1690 
1691         chopWord();
1692         touchText();
1693 
1694     }
1695 
1696     unittest {
1697 
1698         auto io = new HeadlessBackend;
1699         auto root = textInput();
1700 
1701         root.io = io;
1702 
1703         // Type stuff
1704         {
1705             root.value = "Hello World";
1706             root.focus();
1707             root.caretToEnd();
1708             root.draw();
1709 
1710             assert(root.value == "Hello World");
1711             assert(root.contentLabel.text == "Hello World");
1712         }
1713 
1714         // Erase a word
1715         {
1716             io.nextFrame;
1717             root.chopWord;
1718             root.draw();
1719 
1720             assert(root.value == "Hello ");
1721             assert(root.contentLabel.text == "Hello ");
1722             assert(root.isFocused);
1723         }
1724 
1725         // Erase a word
1726         {
1727             io.nextFrame;
1728             root.chopWord;
1729             root.draw();
1730 
1731             assert(root.value == "");
1732             assert(root.contentLabel.text == "");
1733             assert(root.isEmpty);
1734         }
1735 
1736         // Typing should be disabled while erasing
1737         {
1738             io.press(KeyboardKey.leftControl);
1739             io.press(KeyboardKey.w);
1740             io.inputCharacter('w');
1741 
1742             root.draw();
1743 
1744             assert(root.value == "");
1745             assert(root.contentLabel.text == "");
1746             assert(root.isEmpty);
1747         }
1748 
1749     }
1750 
1751     /// Delete a word in front of the caret.
1752     @(FluidInputAction.deleteWord)
1753     void deleteWord() {
1754 
1755         chopWord(true);
1756         touchText();
1757 
1758     }
1759 
1760     unittest {
1761 
1762         auto root = textInput();
1763 
1764         // deleteWord should do nothing, because the caret is at the end
1765         root.push("Hello, Wörld");
1766         root.runInputAction!(FluidInputAction.deleteWord);
1767 
1768         assert(!root.isSelecting);
1769         assert(root.value == "Hello, Wörld");
1770         assert(root.caretIndex == "Hello, Wörld".length);
1771 
1772         // Move it to the previous word
1773         root.runInputAction!(FluidInputAction.previousWord);
1774 
1775         assert(!root.isSelecting);
1776         assert(root.value == "Hello, Wörld");
1777         assert(root.caretIndex == "Hello, ".length);
1778 
1779         // Delete the next word
1780         root.runInputAction!(FluidInputAction.deleteWord);
1781 
1782         assert(!root.isSelecting);
1783         assert(root.value == "Hello, ");
1784         assert(root.caretIndex == "Hello, ".length);
1785 
1786         // Move to the start
1787         root.runInputAction!(FluidInputAction.toStart);
1788 
1789         assert(!root.isSelecting);
1790         assert(root.value == "Hello, ");
1791         assert(root.caretIndex == 0);
1792 
1793         // Delete the next word
1794         root.runInputAction!(FluidInputAction.deleteWord);
1795 
1796         assert(!root.isSelecting);
1797         assert(root.value == ", ");
1798         assert(root.caretIndex == 0);
1799 
1800         // Delete the next word
1801         root.runInputAction!(FluidInputAction.deleteWord);
1802 
1803         assert(!root.isSelecting);
1804         assert(root.value == "");
1805         assert(root.caretIndex == 0);
1806 
1807     }
1808 
1809     /// Erase any character preceding the caret, or the next one.
1810     /// Params:
1811     ///     forward = If true, removes character after the caret, otherwise removes the one before.
1812     void chop(bool forward = false) {
1813 
1814         // Save previous value in undo stack
1815         const previousState = snapshot();
1816         scope (success) pushSnapshot(previousState);
1817 
1818         // Selection active
1819         if (isSelecting) {
1820 
1821             selectedValue = null;
1822 
1823         }
1824 
1825         // Remove next character
1826         else if (forward) {
1827 
1828             if (valueAfterCaret == "") return;
1829 
1830             const length = valueAfterCaret.decodeFrontStatic.codeLength!char;
1831 
1832             valueAfterCaret = valueAfterCaret[length..$];
1833 
1834         }
1835 
1836         // Remove previous character
1837         else {
1838 
1839             if (valueBeforeCaret == "") return;
1840 
1841             const length = valueBeforeCaret.decodeBackStatic.codeLength!char;
1842 
1843             valueBeforeCaret = valueBeforeCaret[0..$-length];
1844 
1845         }
1846 
1847         // Trigger the callback
1848         touchText();
1849 
1850         // Update the size of the box
1851         updateSize();
1852         updateCaretPosition();
1853         horizontalAnchor = caretPosition.x;
1854 
1855     }
1856 
1857     unittest {
1858 
1859         auto root = textInput();
1860 
1861         root.push("поддержки во Fluid.");
1862         root.chop;
1863         assert(root.value == "поддержки во Fluid");
1864 
1865         root.chop;
1866         assert(root.value == "поддержки во Flui");
1867 
1868         root.chop;
1869         assert(root.value == "поддержки во Flu");
1870 
1871         root.chopWord;
1872         assert(root.value == "поддержки во ");
1873 
1874         root.chop;
1875         assert(root.value == "поддержки во");
1876 
1877         root.chop;
1878         assert(root.value == "поддержки в");
1879 
1880         root.chop;
1881         assert(root.value == "поддержки ");
1882 
1883         root.caretToStart();
1884         root.chop(true);
1885         assert(root.value == "оддержки ");
1886 
1887         root.chop(true);
1888         assert(root.value == "ддержки ");
1889 
1890         root.chop(true);
1891         assert(root.value == "держки ");
1892 
1893     }
1894 
1895     private {
1896 
1897         bool _pressed;
1898 
1899         /// Number of clicks performed within short enough time from each other. First click is number 0.
1900         int _clickCount;
1901 
1902         /// Time of the last `press` event, used to enable double click and triple click selection.
1903         SysTime _lastClick;
1904 
1905         /// Position of the last click.
1906         Vector2 _lastClickPosition;
1907 
1908     }
1909 
1910     protected override void mouseImpl() {
1911 
1912         enum maxDistance = 5;
1913 
1914         // Pressing with the mouse
1915         if (!tree.isMouseDown!(FluidInputAction.press)) return;
1916 
1917         const justPressed = !_pressed;
1918 
1919         // Just pressed
1920         if (justPressed) {
1921 
1922             const clickPosition = io.mousePosition;
1923 
1924             // To count as repeated, the click must be within the specified double click time, and close enough
1925             // to the original location
1926             const isRepeated = Clock.currTime - _lastClick < io.doubleClickTime
1927                 && distance(clickPosition, _lastClickPosition) < maxDistance;
1928 
1929             // Count repeated clicks
1930             _clickCount = isRepeated
1931                 ? _clickCount + 1
1932                 : 0;
1933 
1934             // Register the click
1935             _lastClick = Clock.currTime;
1936             _lastClickPosition = clickPosition;
1937 
1938         }
1939 
1940         // Move the caret with the mouse
1941         caretToMouse();
1942         moveOrClearSelection();
1943 
1944         final switch (_clickCount % 3) {
1945 
1946             // First click, merely move the caret while selecting
1947             case 0: break;
1948 
1949             // Second click, select the word surrounding the caret
1950             case 1:
1951                 selectWord();
1952                 break;
1953 
1954             // Third click, select whole line
1955             case 2:
1956                 selectLine();
1957                 break;
1958 
1959         }
1960 
1961         // Enable selection mode
1962         // Disable it when releasing
1963         _pressed = selectionMovement = !tree.isMouseActive!(FluidInputAction.press);
1964 
1965     }
1966 
1967     unittest {
1968 
1969         // This test relies on properties of the default typeface
1970 
1971         import std.math : isClose;
1972 
1973         auto io = new HeadlessBackend;
1974         auto root = textInput(nullTheme.derive(
1975             rule!TextInput(
1976                 Rule.selectionBackgroundColor = color("#02a"),
1977             ),
1978         ));
1979 
1980         root.io = io;
1981         root.value = "Hello, World! Foo, bar, scroll this input";
1982         root.focus();
1983         root.caretToEnd();
1984         root.draw();
1985 
1986         assert(root.scroll.isClose(127));
1987 
1988         // Select some stuff
1989         io.nextFrame;
1990         io.mousePosition = Vector2(150, 10);
1991         io.press();
1992         root.draw();
1993 
1994         io.nextFrame;
1995         io.mousePosition = Vector2(65, 10);
1996         root.draw();
1997 
1998         assert(root.selectedValue == "scroll this");
1999 
2000         io.nextFrame;
2001         root.draw();
2002 
2003         // Match the selection box
2004         io.assertRectangle(
2005             Rectangle(64, 0, 86, 27),
2006             color("#02a")
2007         );
2008 
2009     }
2010 
2011     unittest {
2012 
2013         // This test relies on properties of the default typeface
2014 
2015         import std.math : isClose;
2016 
2017         auto io = new HeadlessBackend;
2018         auto root = textInput(nullTheme.derive(
2019             rule!TextInput(
2020                 Rule.selectionBackgroundColor = color("#02a"),
2021             ),
2022         ));
2023 
2024         root.io = io;
2025         root.value = "Hello, World! Foo, bar, scroll this input";
2026         root.focus();
2027         root.caretToEnd();
2028         root.draw();
2029 
2030         io.mousePosition = Vector2(150, 10);
2031 
2032         // Double- and triple-click
2033         foreach (i; 0..3) {
2034 
2035             io.nextFrame;
2036             io.press();
2037             root.draw();
2038 
2039             io.nextFrame;
2040             io.release();
2041             root.draw();
2042 
2043             // Double-clicked
2044             if (i == 1) {
2045                 assert(root.selectedValue == "this");
2046             }
2047 
2048             // Triple-clicked
2049             if (i == 2) {
2050                 assert(root.selectedValue == root.value);
2051             }
2052 
2053         }
2054 
2055         io.nextFrame;
2056         root.draw();
2057 
2058     }
2059 
2060     unittest {
2061 
2062         import std.math : isClose;
2063 
2064         auto io = new HeadlessBackend;
2065         auto theme = nullTheme.derive(
2066             rule!TextInput(
2067                 Rule.selectionBackgroundColor = color("#02a"),
2068             ),
2069         );
2070         auto root = textInput(.multiline, theme);
2071         auto lineHeight = root.style.getTypeface.lineHeight;
2072 
2073         root.io = io;
2074         root.value = "Line one\nLine two\n\nLine four";
2075         root.focus();
2076         root.draw();
2077 
2078         // Move the caret to second line
2079         root.caretIndex = "Line one\nLin".length;
2080         root.updateCaretPosition();
2081 
2082         const middle = root._inner.start + root.caretPosition;
2083         const top    = middle - Vector2(0, lineHeight);
2084         const blank  = middle + Vector2(0, lineHeight);
2085         const bottom = middle + Vector2(0, lineHeight * 2);
2086 
2087         {
2088 
2089             // Press, and move the mouse around
2090             io.nextFrame();
2091             io.mousePosition = middle;
2092             io.press();
2093             root.draw();
2094 
2095             // Move it to top row
2096             io.nextFrame();
2097             io.mousePosition = top;
2098             root.draw();
2099 
2100             assert(root.selectedValue == "e one\nLin");
2101             assert(root.selectionStart > root.selectionEnd);
2102 
2103             // Move it to bottom row
2104             io.nextFrame();
2105             io.mousePosition = bottom;
2106             root.draw();
2107 
2108             assert(root.selectedValue == "e two\n\nLin");
2109             assert(root.selectionStart < root.selectionEnd);
2110 
2111             // And to the blank line
2112             io.nextFrame();
2113             io.mousePosition = blank;
2114             root.draw();
2115 
2116             assert(root.selectedValue == "e two\n");
2117             assert(root.selectionStart < root.selectionEnd);
2118 
2119         }
2120 
2121         {
2122 
2123             // Double click
2124             io.mousePosition = middle;
2125             root._lastClick = SysTime.init;
2126 
2127             foreach (i; 0..2) {
2128 
2129                 io.nextFrame();
2130                 io.release();
2131                 root.draw();
2132 
2133                 io.nextFrame();
2134                 io.press();
2135                 root.draw();
2136 
2137             }
2138 
2139             assert(root.selectedValue == "Line");
2140             assert(root.selectionStart < root.selectionEnd);
2141 
2142             // Move it to top row
2143             io.nextFrame();
2144             io.mousePosition = top;
2145             root.draw();
2146 
2147             assert(root.selectedValue == "Line one\nLine");
2148             assert(root.selectionStart > root.selectionEnd);
2149 
2150             // Move it to bottom row
2151             io.nextFrame();
2152             io.mousePosition = bottom;
2153             root.draw();
2154 
2155             assert(root.selectedValue == "Line two\n\nLine");
2156             assert(root.selectionStart < root.selectionEnd);
2157 
2158             // And to the blank line
2159             io.nextFrame();
2160             io.mousePosition = blank;
2161             root.draw();
2162 
2163             assert(root.selectedValue == "Line two\n");
2164             assert(root.selectionStart < root.selectionEnd);
2165 
2166         }
2167 
2168         {
2169 
2170             // Triple
2171             io.mousePosition = middle;
2172             root._lastClick = SysTime.init;
2173 
2174             foreach (i; 0..3) {
2175 
2176                 io.nextFrame();
2177                 io.release();
2178                 root.draw();
2179 
2180                 io.nextFrame();
2181                 io.press();
2182                 root.draw();
2183 
2184             }
2185 
2186             assert(root.selectedValue == "Line two");
2187             assert(root.selectionStart < root.selectionEnd);
2188 
2189             // Move it to top row
2190             io.nextFrame();
2191             io.mousePosition = top;
2192             root.draw();
2193 
2194             assert(root.selectedValue == "Line one\nLine two");
2195             assert(root.selectionStart > root.selectionEnd);
2196 
2197             // Move it to bottom row
2198             io.nextFrame();
2199             io.mousePosition = bottom;
2200             root.draw();
2201 
2202             assert(root.selectedValue == "Line two\n\nLine four");
2203             assert(root.selectionStart < root.selectionEnd);
2204 
2205             // And to the blank line
2206             io.nextFrame();
2207             io.mousePosition = blank;
2208             root.draw();
2209 
2210             assert(root.selectedValue == "Line two\n");
2211             assert(root.selectionStart < root.selectionEnd);
2212 
2213         }
2214 
2215     }
2216 
2217     protected override void scrollImpl(Vector2 value) {
2218 
2219         const speed = ScrollInput.scrollSpeed;
2220         const move = speed * value.x;
2221 
2222         scroll = scroll + move;
2223 
2224     }
2225 
2226     Rectangle shallowScrollTo(const(Node) child, Rectangle parentBox, Rectangle childBox) {
2227 
2228         return childBox;
2229 
2230     }
2231 
2232     /// Get line in the input by a byte index.
2233     /// Returns: A rope slice with the line containing the given index.
2234     Rope lineByIndex(KeepTerminator keepTerminator = No.keepTerminator)(size_t index) const {
2235 
2236         return value.lineByIndex!keepTerminator(index);
2237 
2238     }
2239 
2240     /// Update a line with given byte index.
2241     const(char)[] lineByIndex(size_t index, const(char)[] value) {
2242 
2243         lineByIndex(index, Rope(value));
2244         return value;
2245 
2246     }
2247 
2248     /// ditto
2249     Rope lineByIndex(size_t index, Rope newValue) {
2250 
2251         import std.utf;
2252         import fluid.typeface;
2253 
2254         const backLength = Typeface.lineSplitter(value[0..index].retro).front.byChar.walkLength;
2255         const frontLength = Typeface.lineSplitter(value[index..$]).front.byChar.walkLength;
2256         const start = index - backLength;
2257         const end = index + frontLength;
2258         size_t[2] selection = [selectionStart, selectionEnd];
2259 
2260         // Combine everything on the same line, before and after the caret
2261         value = value.replace(start, end, newValue);
2262 
2263         // Caret/selection needs updating
2264         foreach (i, ref caret; selection) {
2265 
2266             const diff = newValue.length - end + start;
2267 
2268             if (caret < start) continue;
2269 
2270             // Move the caret to the start or end, depending on its type
2271             if (caret <= end) {
2272 
2273                 // Selection start moves to the beginning
2274                 // But only if selection is active
2275                 if (i == 0 && isSelecting)
2276                     caret = start;
2277 
2278                 // End moves to the end
2279                 else
2280                     caret = start + newValue.length;
2281 
2282             }
2283 
2284             // Offset the caret
2285             else caret += diff;
2286 
2287         }
2288 
2289         // Update the carets
2290         selectionStart = selection[0];
2291         selectionEnd = selection[1];
2292 
2293         return newValue;
2294 
2295     }
2296 
2297     unittest {
2298 
2299         auto root = textInput(.multiline);
2300         root.push("foo");
2301         root.lineByIndex(0, "foobar");
2302         assert(root.value == "foobar");
2303         assert(root.valueBeforeCaret == "foobar");
2304 
2305         root.push("\nąąąźź");
2306         root.lineByIndex(6, "~");
2307         root.caretIndex = root.caretIndex - 2;
2308         assert(root.value == "~\nąąąźź");
2309         assert(root.valueBeforeCaret == "~\nąąąź");
2310 
2311         root.push("\n\nstuff");
2312         assert(root.value == "~\nąąąź\n\nstuffź");
2313 
2314         root.lineByIndex(11, "");
2315         assert(root.value == "~\nąąąź\n\nstuffź");
2316 
2317         root.lineByIndex(11, "*");
2318         assert(root.value == "~\nąąąź\n*\nstuffź");
2319 
2320     }
2321 
2322     unittest {
2323 
2324         auto root = textInput(.multiline);
2325         root.push("óne\nßwo\nßhree");
2326         root.selectionStart = 5;
2327         root.selectionEnd = 14;
2328         root.lineByIndex(5, "[REDACTED]");
2329         assert(root.value[root.selectionEnd] == 'e');
2330         assert(root.value == "óne\n[REDACTED]\nßhree");
2331 
2332         assert(root.value[root.selectionEnd] == 'e');
2333         assert(root.selectionStart == 5);
2334         assert(root.selectionEnd == 20);
2335 
2336     }
2337 
2338     /// Get the index of the start or end of the line — from index of any character on the same line.
2339     size_t lineStartByIndex(size_t index) {
2340 
2341         return value.lineStartByIndex(index);
2342 
2343     }
2344 
2345     /// ditto
2346     size_t lineEndByIndex(size_t index) {
2347 
2348         return value.lineEndByIndex(index);
2349 
2350     }
2351 
2352     /// Get the current line
2353     Rope caretLine() {
2354 
2355         return value.lineByIndex(caretIndex);
2356 
2357     }
2358 
2359     unittest {
2360 
2361         auto root = textInput(.multiline);
2362         assert(root.caretLine == "");
2363         root.push("aąaa");
2364         assert(root.caretLine == root.value);
2365         root.caretIndex = 0;
2366         assert(root.caretLine == root.value);
2367         root.push("bbb");
2368         assert(root.caretLine == root.value);
2369         assert(root.value == "bbbaąaa");
2370         root.push("\n");
2371         assert(root.value == "bbb\naąaa");
2372         assert(root.caretLine == "aąaa");
2373         root.caretToEnd();
2374         root.push("xx");
2375         assert(root.caretLine == "aąaaxx");
2376         root.push("\n");
2377         assert(root.caretLine == "");
2378         root.push("\n");
2379         assert(root.caretLine == "");
2380         root.caretIndex = root.caretIndex - 1;
2381         assert(root.caretLine == "");
2382         root.caretToStart();
2383         assert(root.caretLine == "bbb");
2384 
2385     }
2386 
2387     /// Change the current line. Moves the cursor to the end of the newly created line.
2388     const(char)[] caretLine(const(char)[] newValue) {
2389 
2390         return lineByIndex(caretIndex, newValue);
2391 
2392     }
2393 
2394     /// ditto
2395     Rope caretLine(Rope newValue) {
2396 
2397         return lineByIndex(caretIndex, newValue);
2398 
2399     }
2400 
2401     unittest {
2402 
2403         auto root = textInput(.multiline);
2404         root.push("a\nbb\nccc\n");
2405         assert(root.caretLine == "");
2406 
2407         root.caretIndex = root.caretIndex - 1;
2408         assert(root.caretLine == "ccc");
2409 
2410         root.caretLine = "hi";
2411         assert(root.value == "a\nbb\nhi\n");
2412 
2413         assert(!root.isSelecting);
2414         assert(root.valueBeforeCaret == "a\nbb\nhi");
2415 
2416         root.caretLine = "";
2417         assert(root.value == "a\nbb\n\n");
2418         assert(root.valueBeforeCaret == "a\nbb\n");
2419 
2420         root.caretLine = "new value";
2421         assert(root.value == "a\nbb\nnew value\n");
2422         assert(root.valueBeforeCaret == "a\nbb\nnew value");
2423 
2424         root.caretIndex = 0;
2425         root.caretLine = "insert";
2426         assert(root.value == "insert\nbb\nnew value\n");
2427         assert(root.valueBeforeCaret == "insert");
2428         assert(root.caretLine == "insert");
2429 
2430     }
2431 
2432     /// Get the column the given index (or the cursor, if omitted) is on.
2433     /// Returns:
2434     ///     Return value depends on the type fed into the function. `column!dchar` will use characters and `column!char`
2435     ///     will use bytes. The type does not have effect on the input index.
2436     ptrdiff_t column(Chartype)(ptrdiff_t index) {
2437 
2438         return value.column!Chartype(index);
2439 
2440     }
2441 
2442     /// ditto
2443     ptrdiff_t column(Chartype)() {
2444 
2445         return column!Chartype(caretIndex);
2446 
2447     }
2448 
2449     unittest {
2450 
2451         auto root = textInput(.multiline);
2452         assert(root.column!dchar == 0);
2453         root.push(" ");
2454         assert(root.column!dchar == 1);
2455         root.push("a");
2456         assert(root.column!dchar == 2);
2457         root.push("ąąą");
2458         assert(root.column!dchar == 5);
2459         assert(root.column!char == 8);
2460         root.push("O\n");
2461         assert(root.column!dchar == 0);
2462         root.push(" ");
2463         assert(root.column!dchar == 1);
2464         root.push("HHH");
2465         assert(root.column!dchar == 4);
2466 
2467     }
2468 
2469     /// Iterate on each line in an interval.
2470     auto eachLineByIndex(ptrdiff_t start, ptrdiff_t end) {
2471 
2472         struct LineIterator {
2473 
2474             TextInput input;
2475             ptrdiff_t index;
2476             ptrdiff_t end;
2477 
2478             private Rope front;
2479             private ptrdiff_t nextLine;
2480 
2481             alias SetLine = void delegate(Rope line) @safe;
2482 
2483             int opApply(scope int delegate(size_t startIndex, ref Rope line) @safe yield) {
2484 
2485                 while (index <= end) {
2486 
2487                     const line = input.value.lineByIndex!(Yes.keepTerminator)(index);
2488 
2489                     // Get index of the next line
2490                     const lineStart = index - input.column!char(index);
2491                     nextLine = lineStart + line.length;
2492 
2493                     // Output the line
2494                     const originalFront = front = line[].chomp;
2495                     auto stop = yield(lineStart, front);
2496 
2497                     // Update indices in case the line has changed
2498                     if (front !is originalFront) {
2499                         setLine(originalFront, front);
2500                     }
2501 
2502                     // Stop if requested
2503                     if (stop) return stop;
2504 
2505                     // Stop if reached the end of string
2506                     if (index == nextLine) return 0;
2507                     if (line.length == originalFront.length) return 0;
2508 
2509                     // Move to the next line
2510                     index = nextLine;
2511 
2512                 }
2513 
2514                 return 0;
2515 
2516             }
2517 
2518             int opApply(scope int delegate(ref Rope line) @safe yield) {
2519 
2520                 foreach (index, ref line; this) {
2521 
2522                     if (auto stop = yield(line)) return stop;
2523 
2524                 }
2525 
2526                 return 0;
2527 
2528             }
2529 
2530             /// Replace the current line with a new one.
2531             private void setLine(Rope oldLine, Rope line) @safe {
2532 
2533                 const lineStart = index - input.column!char(index);
2534 
2535                 // Get the size of the line terminator
2536                 const lineTerminatorLength = nextLine - lineStart - oldLine.length;
2537 
2538                 // Update the line
2539                 input.lineByIndex(index, line);
2540                 index = lineStart + line.length;
2541                 end += line.length - oldLine.length;
2542 
2543                 // Add the terminator
2544                 nextLine = index + lineTerminatorLength;
2545 
2546                 assert(line == front);
2547                 assert(nextLine >= index);
2548                 assert(nextLine <= input.value.length);
2549 
2550             }
2551 
2552         }
2553 
2554         return LineIterator(this, start, end);
2555 
2556     }
2557 
2558     unittest {
2559 
2560         auto root = textInput(.multiline);
2561         root.push("aaaąąą@\r\n#\n##ąąśðą\nĄŚ®ŒĘ¥Ę®\n");
2562 
2563         size_t i;
2564         foreach (line; root.eachLineByIndex(4, 18)) {
2565 
2566             if (i == 0) assert(line == "aaaąąą@");
2567             if (i == 1) assert(line == "#");
2568             if (i == 2) assert(line == "##ąąśðą");
2569             assert(i.among(0, 1, 2));
2570             i++;
2571 
2572         }
2573         assert(i == 3);
2574 
2575         i = 0;
2576         foreach (line; root.eachLineByIndex(22, 27)) {
2577 
2578             if (i == 0) assert(line == "##ąąśðą");
2579             if (i == 1) assert(line == "ĄŚ®ŒĘ¥Ę®");
2580             assert(i.among(0, 1));
2581             i++;
2582 
2583         }
2584         assert(i == 2);
2585 
2586         i = 0;
2587         foreach (line; root.eachLineByIndex(44, 44)) {
2588 
2589             assert(i == 0);
2590             assert(line == "");
2591             i++;
2592 
2593         }
2594         assert(i == 1);
2595 
2596         i = 0;
2597         foreach (line; root.eachLineByIndex(1, 1)) {
2598 
2599             assert(i == 0);
2600             assert(line == "aaaąąą@");
2601             i++;
2602 
2603         }
2604         assert(i == 1);
2605 
2606     }
2607 
2608     unittest {
2609 
2610         auto root = textInput(.multiline);
2611         root.push("skip\nonë\r\ntwo\r\nthree\n");
2612 
2613         assert(root.lineByIndex(4) == "skip");
2614         assert(root.lineByIndex(8) == "onë");
2615         assert(root.lineByIndex(12) == "two");
2616 
2617         size_t i;
2618         foreach (lineStart, ref line; root.eachLineByIndex(5, root.value.length)) {
2619 
2620             if (i == 0) {
2621                 assert(line == "onë");
2622                 assert(lineStart == 5);
2623                 line = Rope("value");
2624             }
2625             else if (i == 1) {
2626                 assert(root.value == "skip\nvalue\r\ntwo\r\nthree\n");
2627                 assert(lineStart == 12);
2628                 assert(line == "two");
2629                 line = Rope("\nbar-bar-bar-bar-bar");
2630             }
2631             else if (i == 2) {
2632                 assert(root.value == "skip\nvalue\r\n\nbar-bar-bar-bar-bar\r\nthree\n");
2633                 assert(lineStart == 34);
2634                 assert(line == "three");
2635                 line = Rope.init;
2636             }
2637             else if (i == 3) {
2638                 assert(root.value == "skip\nvalue\r\n\nbar-bar-bar-bar-bar\r\n\n");
2639                 assert(lineStart == root.value.length);
2640                 assert(line == "");
2641             }
2642             else assert(false);
2643 
2644             i++;
2645 
2646         }
2647 
2648         assert(i == 4);
2649 
2650     }
2651 
2652     unittest {
2653 
2654         auto root = textInput(.multiline);
2655         root.push("Fïrst line\nSëcond line\r\n Third line\n    Fourth line\rFifth line");
2656 
2657         size_t i = 0;
2658         foreach (ref line; root.eachLineByIndex(19, 49)) {
2659 
2660             if (i == 0) assert(line == "Sëcond line");
2661             else if (i == 1) assert(line == " Third line");
2662             else if (i == 2) assert(line == "    Fourth line");
2663             else assert(false);
2664             i++;
2665 
2666             line = "    " ~ line;
2667 
2668         }
2669         assert(i == 3);
2670         root.selectionStart = 19;
2671         root.selectionEnd = 49;
2672 
2673     }
2674 
2675     unittest {
2676 
2677         auto root = textInput();
2678         root.value = "some text, some line, some stuff\ntext";
2679 
2680         foreach (ref line; root.eachLineByIndex(root.value.length, root.value.length)) {
2681 
2682             line = Rope("");
2683             line = Rope("woo");
2684             line = Rope("n");
2685             line = Rope(" ąąą ");
2686             line = Rope("");
2687 
2688         }
2689 
2690         assert(root.value == "");
2691 
2692     }
2693 
2694     unittest {
2695 
2696         auto root = textInput();
2697         root.value = "test";
2698 
2699         {
2700             size_t i;
2701             foreach (line; root.eachLineByIndex(1, 4)) {
2702 
2703                 assert(i++ == 0);
2704                 assert(line == "test");
2705 
2706             }
2707         }
2708 
2709         {
2710             size_t i;
2711             foreach (ref line; root.eachLineByIndex(1, 4)) {
2712 
2713                 assert(i++ == 0);
2714                 assert(line == "test");
2715                 line = "tested";
2716 
2717             }
2718             assert(root.value == "tested");
2719         }
2720 
2721     }
2722 
2723     /// Return each line containing the selection.
2724     auto eachSelectedLine() {
2725 
2726         return eachLineByIndex(selectionLowIndex, selectionHighIndex);
2727 
2728     }
2729 
2730     unittest {
2731 
2732         auto root = textInput();
2733 
2734         foreach (ref line; root.eachSelectedLine) {
2735 
2736             line = Rope("value");
2737 
2738         }
2739 
2740         assert(root.value == "value");
2741 
2742     }
2743 
2744     /// Open the input's context menu.
2745     @(FluidInputAction.contextMenu)
2746     void openContextMenu() {
2747 
2748         // Move the caret
2749         if (!isSelecting)
2750             caretToMouse();
2751 
2752         // Spawn the popup
2753         tree.spawnPopup(contextMenu);
2754 
2755         // Anchor to caret position
2756         contextMenu.anchor = _inner.start + caretPosition;
2757 
2758     }
2759 
2760     /// Remove a character before the caret. Same as `chop`.
2761     @(FluidInputAction.backspace)
2762     void backspace() {
2763 
2764         chop();
2765 
2766     }
2767 
2768     /// Delete one character in front of the cursor.
2769     @(FluidInputAction.deleteChar)
2770     void deleteChar() {
2771 
2772         chop(true);
2773 
2774     }
2775 
2776     unittest {
2777 
2778         auto io = new HeadlessBackend;
2779         auto root = textInput();
2780 
2781         root.io = io;
2782 
2783         // Type stuff
2784         {
2785             root.value = "hello‽";
2786             root.focus();
2787             root.caretToEnd();
2788             root.draw();
2789 
2790             assert(root.value == "hello‽");
2791             assert(root.contentLabel.text == "hello‽");
2792         }
2793 
2794         // Erase a letter
2795         {
2796             io.nextFrame;
2797             root.chop;
2798             root.draw();
2799 
2800             assert(root.value == "hello");
2801             assert(root.contentLabel.text == "hello");
2802             assert(root.isFocused);
2803         }
2804 
2805         // Erase a letter
2806         {
2807             io.nextFrame;
2808             root.chop;
2809             root.draw();
2810 
2811             assert(root.value == "hell");
2812             assert(root.contentLabel.text == "hell");
2813             assert(root.isFocused);
2814         }
2815 
2816         // Typing should be disabled while erasing
2817         {
2818             io.press(KeyboardKey.backspace);
2819             io.inputCharacter("o, world");
2820 
2821             root.draw();
2822 
2823             assert(root.value == "hel");
2824             assert(root.isFocused);
2825         }
2826 
2827     }
2828 
2829     /// Clear the value of this input field, making it empty.
2830     void clear()
2831     out(; isEmpty)
2832     do {
2833 
2834         // Remove the value
2835         value = null;
2836 
2837         clearSelection();
2838         updateCaretPosition();
2839         horizontalAnchor = caretPosition.x;
2840 
2841     }
2842 
2843     unittest {
2844 
2845         auto io = new HeadlessBackend;
2846         auto root = textInput();
2847 
2848         io.inputCharacter("Hello, World!");
2849         root.io = io;
2850         root.focus();
2851         root.draw();
2852 
2853         auto value1 = root.value;
2854 
2855         root.chop();
2856 
2857         assert(root.value == "Hello, World");
2858 
2859         auto value2 = root.value;
2860         root.chopWord();
2861 
2862         assert(root.value == "Hello, ");
2863 
2864         auto value3 = root.value;
2865         root.clear();
2866 
2867         assert(root.value == "");
2868 
2869     }
2870 
2871     unittest {
2872 
2873         auto io = new HeadlessBackend;
2874         auto root = textInput();
2875 
2876         io.inputCharacter("Hello, World");
2877         root.io = io;
2878         root.focus();
2879         root.draw();
2880 
2881         auto value1 = root.value;
2882 
2883         root.chopWord();
2884 
2885         assert(root.value == "Hello, ");
2886 
2887         auto value2 = root.value;
2888 
2889         root.push("Moon");
2890 
2891         assert(root.value == "Hello, Moon");
2892 
2893         auto value3 = root.value;
2894 
2895         root.clear();
2896 
2897         assert(root.value == "");
2898 
2899     }
2900 
2901     /// Select the word surrounding the cursor. If selection is active, expands selection to cover words.
2902     void selectWord() {
2903 
2904         enum excludeWhite = true;
2905 
2906         const isLow = selectionStart <= selectionEnd;
2907         const low = selectionLowIndex;
2908         const high = selectionHighIndex;
2909 
2910         const head = value[0 .. low].wordBack(excludeWhite);
2911         const tail = value[high .. $].wordFront(excludeWhite);
2912 
2913         // Move the caret to the end of the word
2914         caretIndex = high + tail.length;
2915 
2916         // Set selection to the start of the word
2917         selectionStart = low - head.length;
2918 
2919         // Swap them if order is reversed
2920         if (!isLow) swap(_selectionStart, _caretIndex);
2921 
2922         touch();
2923         updateCaretPosition(false);
2924 
2925     }
2926 
2927     unittest {
2928 
2929         auto root = textInput();
2930         root.push("Привет, мир! Это пример текста для тестирования поддержки Unicode во Fluid.");
2931 
2932         // Select word the caret is touching
2933         root.selectWord();
2934         assert(root.selectedValue == ".");
2935 
2936         // Expand
2937         root.selectWord();
2938         assert(root.selectedValue == "Fluid.");
2939 
2940         // Go to start
2941         root.caretToStart();
2942         assert(!root.isSelecting);
2943         assert(root.caretIndex == 0);
2944         assert(root.selectedValue == "");
2945 
2946         root.selectWord();
2947         assert(root.selectedValue == "Привет");
2948 
2949         root.selectWord();
2950         assert(root.selectedValue == "Привет,");
2951 
2952         root.selectWord();
2953         assert(root.selectedValue == "Привет,");
2954 
2955         root.runInputAction!(FluidInputAction.nextChar);
2956         assert(root.caretIndex == 13);  // Before space
2957 
2958         root.runInputAction!(FluidInputAction.nextChar);  // After space
2959         root.runInputAction!(FluidInputAction.nextChar);  // Inside "мир"
2960         assert(!root.isSelecting);
2961         assert(root.caretIndex == 16);
2962 
2963         root.selectWord();
2964         assert(root.selectedValue == "мир");
2965 
2966         root.selectWord();
2967         assert(root.selectedValue == "мир!");
2968 
2969     }
2970 
2971     /// Select the whole line the cursor is.
2972     void selectLine() {
2973 
2974         const isLow = selectionStart <= selectionEnd;
2975 
2976         foreach (index, line; Typeface.lineSplitterIndex(value)) {
2977 
2978             const lineStart = index;
2979             const lineEnd = index + line.length;
2980 
2981             // Found selection start
2982             if (lineStart <= selectionStart && selectionStart <= lineEnd) {
2983 
2984                 selectionStart = isLow
2985                     ? lineStart
2986                     : lineEnd;
2987 
2988             }
2989 
2990             // Found selection end
2991             if (lineStart <= selectionEnd && selectionEnd <= lineEnd) {
2992 
2993                 selectionEnd = isLow
2994                     ? lineEnd
2995                     : lineStart;
2996 
2997 
2998             }
2999 
3000         }
3001 
3002         updateCaretPosition(false);
3003         horizontalAnchor = caretPosition.x;
3004 
3005     }
3006 
3007     unittest {
3008 
3009         auto root = textInput();
3010 
3011         root.push("ąąąą ąąą ąąąąąąą ąą\nąąą ąąą");
3012         assert(root.caretIndex == 49);
3013 
3014         root.selectLine();
3015         assert(root.selectedValue == root.value);
3016         assert(root.selectedValue.length == 49);
3017         assert(root.value.length == 49);
3018 
3019     }
3020 
3021     unittest {
3022 
3023         auto root = textInput(.multiline);
3024 
3025         root.push("ąąą ąąą ąąąąąąą ąą\nąąą ąąą");
3026         root.draw();
3027         assert(root.caretIndex == 47);
3028 
3029         root.selectLine();
3030         assert(root.selectedValue == "ąąą ąąą");
3031         assert(root.selectionStart == 34);
3032         assert(root.selectionEnd == 47);
3033 
3034         root.runInputAction!(FluidInputAction.selectPreviousLine);
3035         assert(root.selectionStart == 34);
3036         assert(root.selectionEnd == 13);
3037         assert(root.selectedValue == " ąąąąąąą ąą\n");
3038 
3039         root.selectLine();
3040         assert(root.selectedValue == root.value);
3041 
3042     }
3043 
3044     /// Move caret to the previous or next character.
3045     @(FluidInputAction.previousChar, FluidInputAction.nextChar)
3046     protected void previousOrNextChar(FluidInputAction action) {
3047 
3048         const forward = action == FluidInputAction.nextChar;
3049 
3050         // Terminating selection
3051         if (isSelecting && !selectionMovement) {
3052 
3053             // Move to either end of the selection
3054             caretIndex = forward
3055                 ? selectionHighIndex
3056                 : selectionLowIndex;
3057             clearSelection();
3058 
3059         }
3060 
3061         // Move to next character
3062         else if (forward) {
3063 
3064             if (valueAfterCaret == "") return;
3065 
3066             const length = valueAfterCaret.decodeFrontStatic.codeLength!char;
3067 
3068             caretIndex = caretIndex + length;
3069 
3070         }
3071 
3072         // Move to previous character
3073         else {
3074 
3075             if (valueBeforeCaret == "") return;
3076 
3077             const length = valueBeforeCaret.decodeBackStatic.codeLength!char;
3078 
3079             caretIndex = caretIndex - length;
3080 
3081         }
3082 
3083         updateCaretPosition(true);
3084         horizontalAnchor = caretPosition.x;
3085 
3086     }
3087 
3088     unittest {
3089 
3090         auto root = textInput();
3091         root.push("Привет, мир! Это пример текста для тестирования поддержки Unicode во Fluid.");
3092 
3093         assert(root.caretIndex == root.value.length);
3094 
3095         root.runInputAction!(FluidInputAction.previousWord);
3096         assert(root.caretIndex == root.value.length - ".".length);
3097 
3098         root.runInputAction!(FluidInputAction.previousWord);
3099         assert(root.caretIndex == root.value.length - "Fluid.".length);
3100 
3101         root.runInputAction!(FluidInputAction.previousChar);
3102         assert(root.caretIndex == root.value.length - " Fluid.".length);
3103 
3104         root.runInputAction!(FluidInputAction.previousChar);
3105         assert(root.caretIndex == root.value.length - "о Fluid.".length);
3106 
3107         root.runInputAction!(FluidInputAction.previousChar);
3108         assert(root.caretIndex == root.value.length - "во Fluid.".length);
3109 
3110         root.runInputAction!(FluidInputAction.previousWord);
3111         assert(root.caretIndex == root.value.length - "Unicode во Fluid.".length);
3112 
3113         root.runInputAction!(FluidInputAction.previousWord);
3114         assert(root.caretIndex == root.value.length - "поддержки Unicode во Fluid.".length);
3115 
3116         root.runInputAction!(FluidInputAction.nextChar);
3117         assert(root.caretIndex == root.value.length - "оддержки Unicode во Fluid.".length);
3118 
3119         root.runInputAction!(FluidInputAction.nextWord);
3120         assert(root.caretIndex == root.value.length - "Unicode во Fluid.".length);
3121 
3122     }
3123 
3124     /// Move caret to the previous or next word.
3125     @(FluidInputAction.previousWord, FluidInputAction.nextWord)
3126     protected void previousOrNextWord(FluidInputAction action) {
3127 
3128         // Previous word
3129         if (action == FluidInputAction.previousWord) {
3130 
3131             caretIndex = caretIndex - valueBeforeCaret.wordBack.length;
3132 
3133         }
3134 
3135         // Next word
3136         else {
3137 
3138             caretIndex = caretIndex + valueAfterCaret.wordFront.length;
3139 
3140         }
3141 
3142         updateCaretPosition(true);
3143         moveOrClearSelection();
3144         horizontalAnchor = caretPosition.x;
3145 
3146     }
3147 
3148     /// Move the caret to the previous or next line.
3149     @(FluidInputAction.previousLine, FluidInputAction.nextLine)
3150     protected void previousOrNextLine(FluidInputAction action) {
3151 
3152         auto typeface = style.getTypeface;
3153         auto search = Vector2(horizontalAnchor, caretPosition.y);
3154 
3155         // Next line
3156         if (action == FluidInputAction.nextLine) {
3157 
3158             search.y += typeface.lineHeight;
3159 
3160         }
3161 
3162         // Previous line
3163         else {
3164 
3165             search.y -= typeface.lineHeight;
3166 
3167         }
3168 
3169         caretTo(search);
3170         updateCaretPosition(horizontalAnchor < 1);
3171         moveOrClearSelection();
3172 
3173     }
3174 
3175     unittest {
3176 
3177         auto root = textInput(.multiline);
3178 
3179         // 5 en dashes, 3 then 4; starting at last line
3180         root.push("–––––\n–––\n––––");
3181         root.draw();
3182 
3183         assert(root.caretIndex == root.value.length);
3184 
3185         // From last line to second line — caret should be at its end
3186         root.runInputAction!(FluidInputAction.previousLine);
3187         assert(root.valueBeforeCaret == "–––––\n–––");
3188 
3189         // First line, move to 4th dash (same as third line)
3190         root.runInputAction!(FluidInputAction.previousLine);
3191         assert(root.valueBeforeCaret == "––––");
3192 
3193         // Next line — end
3194         root.runInputAction!(FluidInputAction.nextLine);
3195         assert(root.valueBeforeCaret == "–––––\n–––");
3196 
3197         // Update anchor to match second line
3198         root.runInputAction!(FluidInputAction.toLineEnd);
3199         assert(root.valueBeforeCaret == "–––––\n–––");
3200 
3201         // First line again, should be 3rd dash now (same as second line)
3202         root.runInputAction!(FluidInputAction.previousLine);
3203         assert(root.valueBeforeCaret == "–––");
3204 
3205         // Last line, 3rd dash too
3206         root.runInputAction!(FluidInputAction.nextLine);
3207         root.runInputAction!(FluidInputAction.nextLine);
3208         assert(root.valueBeforeCaret == "–––––\n–––\n–––");
3209 
3210     }
3211 
3212     /// Move the caret to the given screen position (viewport space).
3213     /// Params:
3214     ///     position = Position in the screen to move the cursor to.
3215     void caretTo(Vector2 position) {
3216 
3217         caretIndex = nearestCharacter(position);
3218 
3219     }
3220 
3221     @("TextInput.caretTo works")
3222     unittest {
3223 
3224         // Note: This test depends on parameters specific to the default typeface.
3225 
3226         import std.math : isClose;
3227 
3228         auto io = new HeadlessBackend;
3229         auto root = textInput(.nullTheme, .multiline);
3230 
3231         root.io = io;
3232         root.size = Vector2(200, 0);
3233         root.value = "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp, make it long enough to cross over";
3234         root.draw();
3235 
3236         // Move the caret to different points on the canvas
3237 
3238         // Left side of the second "l" in "Hello", first line
3239         root.caretTo(Vector2(30, 10));
3240         assert(root.caretIndex == "Hel".length);
3241 
3242         // Right side of the same "l"
3243         root.caretTo(Vector2(33, 10));
3244         assert(root.caretIndex == "Hell".length);
3245 
3246         // Comma, right side, close to the second line
3247         root.caretTo(Vector2(50, 24));
3248         assert(root.caretIndex == "Hello,".length);
3249 
3250         // End of the line, far right
3251         root.caretTo(Vector2(200, 10));
3252         assert(root.caretIndex == "Hello, World!".length);
3253 
3254         // Start of the next line
3255         root.caretTo(Vector2(0, 30));
3256         assert(root.caretIndex == "Hello, World!\n".length);
3257 
3258         // Space, right between "Hello," and "Moon"
3259         root.caretTo(Vector2(54, 40));
3260         assert(root.caretIndex == "Hello, World!\nHello, ".length);
3261 
3262         // Empty line
3263         root.caretTo(Vector2(54, 60));
3264         assert(root.caretIndex == "Hello, World!\nHello, Moon\n".length);
3265 
3266         // Beginning of the next line; left side of the "H"
3267         root.caretTo(Vector2(4, 85));
3268         assert(root.caretIndex == "Hello, World!\nHello, Moon\n\n".length);
3269 
3270         // Wrapped line, the bottom of letter "p" in "up"
3271         root.caretTo(Vector2(142, 128));
3272         assert(root.caretIndex == "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp".length);
3273 
3274         // End of line
3275         root.caretTo(Vector2(160, 128));
3276         assert(root.caretIndex == "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp, ".length);
3277 
3278         // Beginning of the next line; result should be the same
3279         root.caretTo(Vector2(2, 148));
3280         assert(root.caretIndex == "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp, ".length);
3281 
3282         // Just by the way, check if the caret position is correct
3283         root.updateCaretPosition(true);
3284         assert(root.caretPosition.x.isClose(0));
3285         assert(root.caretPosition.y.isClose(135));
3286 
3287         root.updateCaretPosition(false);
3288         assert(root.caretPosition.x.isClose(153));
3289         assert(root.caretPosition.y.isClose(108));
3290 
3291         // Try the same with the third line
3292         root.caretTo(Vector2(200, 148));
3293         assert(root.caretIndex
3294             == "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp, make it long enough ".length);
3295         root.caretTo(Vector2(2, 168));
3296         assert(root.caretIndex
3297             == "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp, make it long enough ".length);
3298 
3299     }
3300 
3301     /// Move the caret to mouse position.
3302     void caretToMouse() {
3303 
3304         caretTo(io.mousePosition - _inner.start);
3305         updateCaretPosition(false);
3306         horizontalAnchor = caretPosition.x;
3307 
3308     }
3309 
3310     unittest {
3311 
3312         import std.math : isClose;
3313 
3314         // caretToMouse is a just a wrapper over caretTo, enabling mouse input
3315         // This test checks if it correctly maps mouse coordinates to internal coordinates
3316 
3317         auto io = new HeadlessBackend;
3318         auto theme = nullTheme.derive(
3319             rule!TextInput(
3320                 Rule.margin = 40,
3321                 Rule.padding = 40,
3322             )
3323         );
3324         auto root = textInput(.multiline, theme);
3325 
3326         root.io = io;
3327         root.size = Vector2(200, 0);
3328         root.value = "123\n456\n789";
3329         root.draw();
3330 
3331         io.nextFrame();
3332         io.mousePosition = Vector2(140, 90);
3333         root.caretToMouse();
3334 
3335         assert(root.caretIndex == 3);
3336 
3337     }
3338 
3339     /// Move the caret to the beginning of the line. This function perceives the line visually, so if the text wraps, it
3340     /// will go to the beginning of the visible line, instead of the hard line break.
3341     @(FluidInputAction.toLineStart)
3342     void caretToLineStart() {
3343 
3344         const search = Vector2(0, caretPosition.y);
3345 
3346         caretTo(search);
3347         updateCaretPosition(true);
3348         moveOrClearSelection();
3349         horizontalAnchor = caretPosition.x;
3350 
3351     }
3352 
3353     /// Move the caret to the end of the line.
3354     @(FluidInputAction.toLineEnd)
3355     void caretToLineEnd() {
3356 
3357         const search = Vector2(float.infinity, caretPosition.y);
3358 
3359         caretTo(search);
3360         updateCaretPosition(false);
3361         moveOrClearSelection();
3362         horizontalAnchor = caretPosition.x;
3363 
3364     }
3365 
3366     unittest {
3367 
3368         // Note: This test depends on parameters specific to the default typeface.
3369 
3370         import std.math : isClose;
3371 
3372         auto io = new HeadlessBackend;
3373         auto root = textInput(.nullTheme, .multiline);
3374 
3375         root.io = io;
3376         root.size = Vector2(200, 0);
3377         root.value = "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp, make it long enough to cross over";
3378         root.focus();
3379         root.draw();
3380 
3381         root.caretIndex = 0;
3382         root.updateCaretPosition();
3383         root.runInputAction!(FluidInputAction.toLineEnd);
3384 
3385         assert(root.caretIndex == "Hello, World!".length);
3386 
3387         // Move to the next line, should be at the end
3388         root.runInputAction!(FluidInputAction.nextLine);
3389 
3390         assert(root.valueBeforeCaret.wordBack == "Moon");
3391         assert(root.valueAfterCaret.wordFront == "\n");
3392 
3393         // Move to the blank line
3394         root.runInputAction!(FluidInputAction.nextLine);
3395 
3396         const blankLine = root.caretIndex;
3397         assert(root.valueBeforeCaret.wordBack == "\n");
3398         assert(root.valueAfterCaret.wordFront == "\n");
3399 
3400         // toLineEnd and toLineStart should have no effect
3401         root.runInputAction!(FluidInputAction.toLineStart);
3402         assert(root.caretIndex == blankLine);
3403         root.runInputAction!(FluidInputAction.toLineEnd);
3404         assert(root.caretIndex == blankLine);
3405 
3406         // Next line again
3407         // The anchor has been reset to the beginning
3408         root.runInputAction!(FluidInputAction.nextLine);
3409 
3410         assert(root.valueBeforeCaret.wordBack == "\n");
3411         assert(root.valueAfterCaret.wordFront == "Hello");
3412 
3413         // Move to the very end
3414         root.runInputAction!(FluidInputAction.toEnd);
3415 
3416         assert(root.valueBeforeCaret.wordBack == "over");
3417         assert(root.valueAfterCaret.wordFront == "");
3418 
3419         // Move to start of the line
3420         root.runInputAction!(FluidInputAction.toLineStart);
3421 
3422         assert(root.valueBeforeCaret.wordBack == "enough ");
3423         assert(root.valueAfterCaret.wordFront == "to ");
3424         assert(root.caretPosition.x.isClose(0));
3425 
3426         // Move to the previous line
3427         root.runInputAction!(FluidInputAction.previousLine);
3428 
3429         assert(root.valueBeforeCaret.wordBack == ", ");
3430         assert(root.valueAfterCaret.wordFront == "make ");
3431         assert(root.caretPosition.x.isClose(0));
3432 
3433         // Move to its end — position should be the same as earlier, but the caret should be on the same line
3434         root.runInputAction!(FluidInputAction.toLineEnd);
3435 
3436         assert(root.valueBeforeCaret.wordBack == "enough ");
3437         assert(root.valueAfterCaret.wordFront == "to ");
3438         assert(root.caretPosition.x.isClose(181));
3439 
3440         // Move to the previous line — again
3441         root.runInputAction!(FluidInputAction.previousLine);
3442 
3443         assert(root.valueBeforeCaret.wordBack == ", ");
3444         assert(root.valueAfterCaret.wordFront == "make ");
3445         assert(root.caretPosition.x.isClose(153));
3446 
3447     }
3448 
3449     /// Move the caret to the beginning of the input
3450     @(FluidInputAction.toStart)
3451     void caretToStart() {
3452 
3453         caretIndex = 0;
3454         updateCaretPosition(true);
3455         moveOrClearSelection();
3456         horizontalAnchor = caretPosition.x;
3457 
3458     }
3459 
3460     /// Move the caret to the end of the input
3461     @(FluidInputAction.toEnd)
3462     void caretToEnd() {
3463 
3464         caretIndex = value.length;
3465         updateCaretPosition(false);
3466         moveOrClearSelection();
3467         horizontalAnchor = caretPosition.x;
3468 
3469     }
3470 
3471     /// Select all text
3472     @(FluidInputAction.selectAll)
3473     void selectAll() {
3474 
3475         selectionMovement = true;
3476         scope (exit) selectionMovement = false;
3477 
3478         _selectionStart = 0;
3479         caretToEnd();
3480 
3481     }
3482 
3483     unittest {
3484 
3485         auto root = textInput();
3486 
3487         root.draw();
3488         root.selectAll();
3489 
3490         assert(root.selectionStart == 0);
3491         assert(root.selectionEnd == 0);
3492 
3493         root.push("foo bar ");
3494 
3495         assert(!root.isSelecting);
3496 
3497         root.push("baz");
3498 
3499         assert(root.value == "foo bar baz");
3500 
3501         auto value1 = root.value;
3502 
3503         root.selectAll();
3504 
3505         assert(root.selectionStart == 0);
3506         assert(root.selectionEnd == root.value.length);
3507 
3508         root.push("replaced");
3509 
3510         assert(root.value == "replaced");
3511 
3512     }
3513 
3514     /// Begin or continue selection using given movement action.
3515     ///
3516     /// Use `selectionStart` and `selectionEnd` to define selection boundaries manually.
3517     @(
3518         FluidInputAction.selectPreviousChar,
3519         FluidInputAction.selectNextChar,
3520         FluidInputAction.selectPreviousWord,
3521         FluidInputAction.selectNextWord,
3522         FluidInputAction.selectPreviousLine,
3523         FluidInputAction.selectNextLine,
3524         FluidInputAction.selectToLineStart,
3525         FluidInputAction.selectToLineEnd,
3526         FluidInputAction.selectToStart,
3527         FluidInputAction.selectToEnd,
3528     )
3529     protected void select(FluidInputAction action) {
3530 
3531         selectionMovement = true;
3532         scope (exit) selectionMovement = false;
3533 
3534         // Start selection
3535         if (!isSelecting)
3536         selectionStart = caretIndex;
3537 
3538         with (FluidInputAction) switch (action) {
3539             case selectPreviousChar:
3540                 runInputAction!previousChar;
3541                 break;
3542             case selectNextChar:
3543                 runInputAction!nextChar;
3544                 break;
3545             case selectPreviousWord:
3546                 runInputAction!previousWord;
3547                 break;
3548             case selectNextWord:
3549                 runInputAction!nextWord;
3550                 break;
3551             case selectPreviousLine:
3552                 runInputAction!previousLine;
3553                 break;
3554             case selectNextLine:
3555                 runInputAction!nextLine;
3556                 break;
3557             case selectToLineStart:
3558                 runInputAction!toLineStart;
3559                 break;
3560             case selectToLineEnd:
3561                 runInputAction!toLineEnd;
3562                 break;
3563             case selectToStart:
3564                 runInputAction!toStart;
3565                 break;
3566             case selectToEnd:
3567                 runInputAction!toEnd;
3568                 break;
3569             default:
3570                 assert(false, "Invalid action");
3571         }
3572 
3573     }
3574 
3575     /// Cut selected text to clipboard, clearing the selection.
3576     @(FluidInputAction.cut)
3577     void cut() {
3578 
3579         auto snap = snapshot();
3580         copy();
3581         pushSnapshot(snap);
3582         selectedValue = null;
3583 
3584     }
3585 
3586     unittest {
3587 
3588         auto root = textInput();
3589 
3590         root.draw();
3591         root.push("Foo Bar Baz Ban");
3592 
3593         // Move cursor to "Bar"
3594         root.runInputAction!(FluidInputAction.toStart);
3595         root.runInputAction!(FluidInputAction.nextWord);
3596 
3597         // Select "Bar Baz "
3598         root.runInputAction!(FluidInputAction.selectNextWord);
3599         root.runInputAction!(FluidInputAction.selectNextWord);
3600 
3601         assert(root.io.clipboard == "");
3602         assert(root.selectedValue == "Bar Baz ");
3603 
3604         // Cut the text
3605         root.cut();
3606 
3607         assert(root.io.clipboard == "Bar Baz ");
3608         assert(root.value == "Foo Ban");
3609 
3610     }
3611 
3612     unittest {
3613 
3614         auto root = textInput();
3615 
3616         root.push("Привет, мир! Это пример текста для тестирования поддержки Unicode во Fluid.");
3617         root.draw();
3618         root.io.clipboard = "ą";
3619 
3620         root.runInputAction!(FluidInputAction.previousChar);
3621         root.selectionStart = 106;  // Before "Unicode"
3622         root.cut();
3623 
3624         assert(root.value == "Привет, мир! Это пример текста для тестирования поддержки .");
3625         assert(root.io.clipboard == "Unicode во Fluid");
3626 
3627         root.caretIndex = 14;
3628         root.runInputAction!(FluidInputAction.selectNextWord);  // мир
3629         root.paste();
3630 
3631         assert(root.value == "Привет, Unicode во Fluid! Это пример текста для тестирования поддержки .");
3632 
3633     }
3634 
3635     /// Copy selected text to clipboard.
3636     @(FluidInputAction.copy)
3637     void copy() {
3638 
3639         import std.conv : text;
3640 
3641         if (isSelecting)
3642             io.clipboard = text(selectedValue);
3643 
3644     }
3645 
3646     unittest {
3647 
3648         auto root = textInput();
3649 
3650         root.draw();
3651         root.push("Foo Bar Baz Ban");
3652 
3653         // Select all
3654         root.selectAll();
3655 
3656         assert(root.io.clipboard == "");
3657 
3658         root.copy();
3659 
3660         assert(root.io.clipboard == "Foo Bar Baz Ban");
3661 
3662         // Reduce selection by a word
3663         root.runInputAction!(FluidInputAction.selectPreviousWord);
3664         root.copy();
3665 
3666         assert(root.io.clipboard == "Foo Bar Baz ");
3667         assert(root.value == "Foo Bar Baz Ban");
3668 
3669     }
3670 
3671     /// Paste text from clipboard.
3672     @(FluidInputAction.paste)
3673     void paste() {
3674 
3675         auto snap = snapshot();
3676         push(io.clipboard);
3677         forcePushSnapshot(snap);
3678 
3679     }
3680 
3681     unittest {
3682 
3683         auto root = textInput();
3684 
3685         root.value = "Foo ";
3686         root.draw();
3687         root.caretToEnd();
3688         root.io.clipboard = "Bar";
3689 
3690         assert(root.caretIndex == 4);
3691         assert(root.value == "Foo ");
3692 
3693         root.paste();
3694 
3695         assert(root.caretIndex == 7);
3696         assert(root.value == "Foo Bar");
3697 
3698         root.caretToStart();
3699         root.paste();
3700 
3701         assert(root.caretIndex == 3);
3702         assert(root.value == "BarFoo Bar");
3703 
3704     }
3705 
3706     /// Clear the undo/redo action history.
3707     ///
3708     /// Calling this will erase both the undo and redo stack, making it impossible to restore any changes made in prior,
3709     /// through means such as Ctrl+Z and Ctrl+Y.
3710     void clearHistory() {
3711 
3712         _undoStack.clear();
3713         _redoStack.clear();
3714 
3715     }
3716 
3717     /// Push the given state snapshot (value, caret & selection) into the undo stack. Refuses to push if the current
3718     /// state can be merged with it, unless `forcePushSnapshot` is used.
3719     ///
3720     /// A snapshot pushed through `forcePushSnapshot` will break continuity — it will not be merged with any other
3721     /// snapshot.
3722     void pushSnapshot(HistoryEntry entry) {
3723 
3724         // Compare against current state, so it can be dismissed if it's too similar
3725         auto currentState = snapshot();
3726         currentState.setPreviousEntry(entry);
3727 
3728         // Mark as continuous, so runs of similar characters can be merged together
3729         scope (success) _isContinuous = true;
3730 
3731         // No change was made, ignore
3732         if (currentState.diff.isSame()) return;
3733 
3734         // Current state is compatible, ignore
3735         if (entry.isContinuous && entry.canMergeWith(currentState)) return;
3736 
3737         // Push state
3738         forcePushSnapshot(entry);
3739 
3740     }
3741 
3742     /// ditto
3743     void forcePushSnapshot(HistoryEntry entry) {
3744 
3745         // Break continuity
3746         _isContinuous = false;
3747 
3748         // Ignore if the last entry is identical
3749         if (!_undoStack.empty && _undoStack.back == entry) return;
3750 
3751         // Truncate the history to match the index, insert the current value.
3752         _undoStack.insertBack(entry);
3753 
3754         // Clear the redo stack
3755         _redoStack.clear();
3756 
3757     }
3758 
3759     unittest {
3760 
3761         auto root = textInput(.multiline);
3762         root.push("Hello, ");
3763         root.runInputAction!(FluidInputAction.breakLine);
3764         root.push("new");
3765         root.runInputAction!(FluidInputAction.breakLine);
3766         root.push("line");
3767         root.chop;
3768         root.chopWord;
3769         root.push("few");
3770         root.push(" lines");
3771         assert(root.value == "Hello, \nnew\nfew lines");
3772 
3773         // Move back to last chop
3774         root.undo();
3775         assert(root.value == "Hello, \nnew\n");
3776 
3777         // Test redo
3778         root.redo();
3779         assert(root.value == "Hello, \nnew\nfew lines");
3780         root.undo();
3781         assert(root.value == "Hello, \nnew\n");
3782 
3783         // Move back through isnerts
3784         root.undo();
3785         assert(root.value == "Hello, \nnew\nline");
3786         root.undo();
3787         assert(root.value == "Hello, \nnew\n");
3788         root.undo();
3789         assert(root.value == "Hello, \nnew");
3790         root.undo();
3791         assert(root.value == "Hello, \n");
3792         root.undo();
3793         assert(root.value == "Hello, ");
3794         root.undo();
3795         assert(root.value == "");
3796         root.redo();
3797         assert(root.value == "Hello, ");
3798         root.redo();
3799         assert(root.value == "Hello, \n");
3800         root.redo();
3801         assert(root.value == "Hello, \nnew");
3802         root.redo();
3803         assert(root.value == "Hello, \nnew\n");
3804         root.redo();
3805         assert(root.value == "Hello, \nnew\nline");
3806         root.redo();
3807         assert(root.value == "Hello, \nnew\n");
3808         root.redo();
3809         assert(root.value == "Hello, \nnew\nfew lines");
3810 
3811         // Navigate and replace "Hello"
3812         root.caretIndex = 5;
3813         root.runInputAction!(FluidInputAction.selectPreviousWord);
3814         root.push("Hi");
3815         assert(root.value == "Hi, \nnew\nfew lines");
3816         assert(root.valueBeforeCaret == "Hi");
3817 
3818         root.undo();
3819         assert(root.value == "Hello, \nnew\nfew lines");
3820         assert(root.selectedValue == "Hello");
3821 
3822         root.undo();
3823         assert(root.value == "Hello, \nnew\n");
3824         assert(root.valueAfterCaret == "");
3825 
3826     }
3827 
3828     unittest {
3829 
3830         auto root = textInput();
3831 
3832         foreach (i; 0..4) {
3833             root.caretToStart();
3834             root.push("a");
3835         }
3836 
3837         assert(root.value == "aaaa");
3838         assert(root.valueBeforeCaret == "a");
3839         root.undo();
3840         assert(root.value == "aaa");
3841         assert(root.valueBeforeCaret == "");
3842         root.undo();
3843         assert(root.value == "aa");
3844         assert(root.valueBeforeCaret == "");
3845         root.undo();
3846         assert(root.value == "a");
3847         assert(root.valueBeforeCaret == "");
3848         root.undo();
3849         assert(root.value == "");
3850 
3851     }
3852 
3853     /// Produce a snapshot for the current state. Returns the snapshot.
3854     protected HistoryEntry snapshot() const {
3855 
3856         auto entry = HistoryEntry(value, selectionStart, selectionEnd, _isContinuous);
3857 
3858         // Get previous entry in the history
3859         if (!_undoStack.empty)
3860             entry.setPreviousEntry(_undoStack.back.value);
3861 
3862         return entry;
3863 
3864     }
3865 
3866     /// Restore state from snapshot
3867     protected HistoryEntry snapshot(HistoryEntry entry) {
3868 
3869         value = entry.value;
3870         selectSlice(entry.selectionStart, entry.selectionEnd);
3871 
3872         return entry;
3873 
3874     }
3875 
3876     /// Restore the last value in history.
3877     @(FluidInputAction.undo)
3878     void undo() {
3879 
3880         // Nothing to undo
3881         if (_undoStack.empty) return;
3882 
3883         // Push the current state to redo stack
3884         _redoStack.insertBack(snapshot);
3885 
3886         // Restore the value
3887         this.snapshot = _undoStack.back;
3888         _undoStack.removeBack;
3889 
3890     }
3891 
3892     /// Perform the last undone action again.
3893     @(FluidInputAction.redo)
3894     void redo() {
3895 
3896         // Nothing to redo
3897         if (_redoStack.empty) return;
3898 
3899         // Push the current state to undo stack
3900         _undoStack.insertBack(snapshot);
3901 
3902         // Restore the value
3903         this.snapshot = _redoStack.back;
3904         _redoStack.removeBack;
3905 
3906     }
3907 
3908 }
3909 
3910 unittest {
3911 
3912     auto root = textInput(.nullTheme, .multiline);
3913     auto lineHeight = root.style.getTypeface.lineHeight;
3914 
3915     root.value = "First one\nSecond two";
3916     root.draw();
3917 
3918     // Navigate to the start and select the whole line
3919     root.caretToStart();
3920     root.runInputAction!(FluidInputAction.selectToLineEnd);
3921 
3922     assert(root.selectedValue == "First one");
3923     assert(root.caretPosition.y < lineHeight);
3924 
3925 }
3926 
3927 /// `wordFront` and `wordBack` get the word at the beginning or end of given string, respectively.
3928 ///
3929 /// A word is a streak of consecutive characters — non-whitespace, either all alphanumeric or all not — followed by any
3930 /// number of whitespace.
3931 ///
3932 /// Params:
3933 ///     text = Text to scan for the word.
3934 ///     excludeWhite = If true, whitespace will not be included in the word.
3935 T wordFront(T)(T text, bool excludeWhite = false) {
3936 
3937     size_t length;
3938 
3939     T result() { return text[0..length]; }
3940     T remaining() { return text[length..$]; }
3941 
3942     while (remaining != "") {
3943 
3944         // Get the first character
3945         const lastChar = remaining.decodeFrontStatic;
3946 
3947         // Exclude white characters if enabled
3948         if (excludeWhite && lastChar.isWhite) break;
3949 
3950         length += lastChar.codeLength!(typeof(text[0]));
3951 
3952         // Stop if empty
3953         if (remaining == "") break;
3954 
3955         const nextChar = remaining.decodeFrontStatic;
3956 
3957         // Stop if the next character is a line feed
3958         if (nextChar.only.chomp.empty && !only(lastChar, nextChar).equal("\r\n")) break;
3959 
3960         // Continue if the next character is whitespace
3961         // Includes any case where the previous character is followed by whitespace
3962         else if (nextChar.isWhite) continue;
3963 
3964         // Stop if whitespace follows a non-white character
3965         else if (lastChar.isWhite) break;
3966 
3967         // Stop if the next character has different type
3968         else if (lastChar.isAlphaNum != nextChar.isAlphaNum) break;
3969 
3970     }
3971 
3972     return result;
3973 
3974 }
3975 
3976 /// ditto
3977 T wordBack(T)(T text, bool excludeWhite = false) {
3978 
3979     size_t length = text.length;
3980 
3981     T result() { return text[length..$]; }
3982     T remaining() { return text[0..length]; }
3983 
3984     while (remaining != "") {
3985 
3986         // Get the first character
3987         const lastChar = remaining.decodeBackStatic;
3988 
3989         // Exclude white characters if enabled
3990         if (excludeWhite && lastChar.isWhite) break;
3991 
3992         length -= lastChar.codeLength!(typeof(text[0]));
3993 
3994         // Stop if empty
3995         if (remaining == "") break;
3996 
3997         const nextChar = remaining.decodeBackStatic;
3998 
3999         // Stop if the character is a line feed
4000         if (lastChar.only.chomp.empty && !only(nextChar, lastChar).equal("\r\n")) break;
4001 
4002         // Continue if the current character is whitespace
4003         // Inverse to `wordFront`
4004         else if (lastChar.isWhite) continue;
4005 
4006         // Stop if whitespace follows a non-white character
4007         else if (nextChar.isWhite) break;
4008 
4009         // Stop if the next character has different type
4010         else if (lastChar.isAlphaNum != nextChar.isAlphaNum) break;
4011 
4012     }
4013 
4014     return result;
4015 
4016 }
4017 
4018 /// `decodeFront` and `decodeBack` variants that do not mutate the range
4019 private dchar decodeFrontStatic(T)(T range) @trusted {
4020 
4021     return range.decodeFront;
4022 
4023 }
4024 
4025 /// ditto
4026 private dchar decodeBackStatic(T)(T range) @trusted {
4027 
4028     return range.decodeBack;
4029 
4030 }
4031 
4032 unittest {
4033 
4034     assert("hello world!".wordFront == "hello ");
4035     assert("hello, world!".wordFront == "hello");
4036     assert("hello world!".wordBack == "!");
4037     assert("hello world".wordBack == "world");
4038     assert("hello ".wordBack == "hello ");
4039 
4040     assert("witaj świecie!".wordFront == "witaj ");
4041     assert(" świecie!".wordFront == " ");
4042     assert("świecie!".wordFront == "świecie");
4043     assert("witaj świecie!".wordBack == "!");
4044     assert("witaj świecie".wordBack == "świecie");
4045     assert("witaj ".wordBack == "witaj ");
4046 
4047     assert("Всем привет!".wordFront == "Всем ");
4048     assert("привет!".wordFront == "привет");
4049     assert("!".wordFront == "!");
4050 
4051     // dstring
4052     assert("Всем привет!"d.wordFront == "Всем "d);
4053     assert("привет!"d.wordFront == "привет"d);
4054     assert("!"d.wordFront == "!"d);
4055 
4056     assert("Всем привет!"d.wordBack == "!"d);
4057     assert("Всем привет"d.wordBack == "привет"d);
4058     assert("Всем "d.wordBack == "Всем "d);
4059 
4060     // Whitespace exclusion
4061     assert("witaj świecie!".wordFront(true) == "witaj");
4062     assert(" świecie!".wordFront(true) == "");
4063     assert("witaj świecie".wordBack(true) == "świecie");
4064     assert("witaj ".wordBack(true) == "");
4065 
4066 }
4067 
4068 unittest {
4069 
4070     assert("\nabc\n".wordFront == "\n");
4071     assert("\n  abc\n".wordFront == "\n  ");
4072     assert("abc\n".wordFront == "abc");
4073     assert("abc  \n".wordFront == "abc  ");
4074     assert("  \n".wordFront == "  ");
4075     assert("\n     abc".wordFront == "\n     ");
4076 
4077     assert("\nabc\n".wordBack == "\n");
4078     assert("\nabc".wordBack == "abc");
4079     assert("abc  \n".wordBack == "\n");
4080     assert("abc  ".wordFront == "abc  ");
4081     assert("\nabc\n  ".wordBack == "\n  ");
4082     assert("\nabc\n  a".wordBack == "a");
4083 
4084     assert("\r\nabc\r\n".wordFront == "\r\n");
4085     assert("\r\n  abc\r\n".wordFront == "\r\n  ");
4086     assert("abc\r\n".wordFront == "abc");
4087     assert("abc  \r\n".wordFront == "abc  ");
4088     assert("  \r\n".wordFront == "  ");
4089     assert("\r\n     abc".wordFront == "\r\n     ");
4090 
4091     assert("\r\nabc\r\n".wordBack == "\r\n");
4092     assert("\r\nabc".wordBack == "abc");
4093     assert("abc  \r\n".wordBack == "\r\n");
4094     assert("abc  ".wordFront == "abc  ");
4095     assert("\r\nabc\r\n  ".wordBack == "\r\n  ");
4096     assert("\r\nabc\r\n  a".wordBack == "a");
4097 
4098 }
4099 
4100 @("TextInput automatically updates scrolling ancestors")
4101 unittest {
4102 
4103     // Note: This theme relies on properties of the default typeface
4104 
4105     import fluid.scroll;
4106 
4107     const viewportHeight = 50;
4108     
4109     auto theme = nullTheme.derive(
4110         rule!Node(
4111             Rule.typeface = Style.defaultTypeface,
4112             Rule.fontSize = 20.pt,
4113             Rule.textColor = color("#fff"),
4114             Rule.backgroundColor = color("#000"),
4115         ),
4116     );
4117     auto input = multilineInput();
4118     auto root = vscrollFrame(theme, input);
4119     auto io = new HeadlessBackend(Vector2(200, viewportHeight));
4120     root.io = io;
4121 
4122     root.draw();
4123     assert(root.scroll == 0);
4124 
4125     // Begin typing
4126     input.push("FLUID\nIS\nAWESOME");
4127     input.caretToStart();
4128     input.push("FLUID\nIS\nAWESOME\n");
4129     root.draw();
4130     root.draw();
4131 
4132     const focusBox = input.focusBoxImpl(Rectangle(0, 0, 200, 50));
4133 
4134     assert(focusBox.start == input.caretPosition);
4135     assert(focusBox.end.y - viewportHeight == root.scroll);
4136     
4137     
4138 }