1 module nodes.text_input;
2 
3 import std.algorithm;
4 
5 import fluid;
6 
7 @safe:
8 
9 Theme testTheme;
10 
11 static this() {
12     testTheme = nullTheme.derive(
13         rule!TextInput(
14             Rule.textColor = color("#000"),
15             Rule.backgroundColor = color("#faf"),
16             Rule.selectionBackgroundColor = color("#02a"),
17             Rule.fontSize = 14.pt,
18         ),
19     );
20 }
21 
22 @("TextInput scrolls when there is too much text to fit in its width")
23 unittest {
24 
25     auto input = textInput(.testTheme);
26     auto root = testSpace(input);
27 
28     input.value = "correct horse battery staple";
29     root.draw();
30     input.caretToEnd();
31 
32     root.drawAndAssert(
33         input.drawsRectangle(0, 0, 200, 27).ofColor("#faf"),
34         input.cropsTo       (0, 0, 200, 27),
35         input.contentLabel.drawsHintedImage().at(-42, 0),
36     );
37 
38 }
39 
40 @("TextInput removes line feeds in single line mode")
41 unittest {
42 
43     auto root = textInput();
44 
45     root.value = "hello wörld!";
46     assert(root.value == "hello wörld!");
47 
48     root.value = "hello wörld!\n";
49     assert(root.value == "hello wörld! ");
50 
51     root.value = "hello wörld!\r\n";
52     assert(root.value == "hello wörld! ");
53 
54     root.value = "hello wörld!\v";
55     assert(root.value == "hello wörld! ");
56 
57 }
58 
59 @("TextInput keeps line feeds in multiline mode")
60 unittest {
61 
62     auto root = textInput(.multiline);
63 
64     root.value = "hello wörld!";
65     assert(root.value == "hello wörld!");
66 
67     root.value = "hello wörld!\n";
68     assert(root.value == "hello wörld!\n");
69 
70     root.value = "hello wörld!\r\n";
71     assert(root.value == "hello wörld!\r\n");
72 
73     root.value = "hello wörld!\v";
74     assert(root.value == "hello wörld!\v");
75 
76 }
77 
78 @("TextInput.selectSlice can be used to change the selection in a consistent manner")
79 unittest {
80 
81     auto root = textInput();
82     root.value = "foo bar baz";
83     root.selectSlice(0, 3);
84     assert(root.selectedValue == "foo");
85 
86     root.caretIndex = 4;
87     root.selectSlice(4, 7);
88     assert(root.selectedValue == "bar");
89 
90     root.caretIndex = 11;
91     root.selectSlice(8, 11);
92     assert(root.selectedValue == "baz");
93 
94 }
95 
96 @("TextInput resizes to fit text")
97 unittest {
98 
99     auto root = textInput(
100         .layout!"fill",
101         .multiline,
102         .nullTheme,
103         "This placeholder exceeds the default size of a text input."
104     );
105 
106     root.draw();
107 
108     Vector2 textSize() {
109         return root.contentLabel.getMinSize;
110     }
111 
112     assert(textSize.x > 200);
113     assert(textSize.x > root.size.x);
114 
115     root.placeholder = "";
116     root.updateSize();
117     root.draw();
118 
119     assert(root.caretPosition.x < 1);
120     assert(textSize.x < 1);
121 
122     root.value = "This value exceeds the default size of a text input.";
123     root.updateSize();
124     root.caretToEnd();
125     root.draw();
126 
127     assert(root.caretPosition.x > 200);
128     assert(textSize.x > 200);
129     assert(textSize.x > root.size.x);
130 
131     root.value = "This value is long enough to start a new line in the output. To make sure of it, here's "
132         ~ "some more text. And more.";
133     root.updateSize();
134     root.draw();
135 
136     assert(textSize.x > root.size.x);
137     assert(textSize.x <= 800);
138     assert(textSize.y >= root.style.getTypeface.lineHeight * 2);
139     assert(root.getMinSize.y >= textSize.y);
140 
141 }
142 
143 @("TextInput accepts text when focused")
144 unittest {
145 
146     auto input = textInput("placeholder");
147     auto focus = focusChain();
148     auto root = chain(focus, input);
149 
150     // Empty text
151     {
152         root.draw();
153 
154         assert(input.value == "");
155         assert(input.contentLabel.text == "placeholder");
156         assert(input.isEmpty);
157     }
158 
159     // Focus the box and input stuff
160     {
161         focus.typeText("¡Hola, mundo!");
162         focus.currentFocus = input;
163         root.draw();
164 
165         assert(input.value == "¡Hola, mundo!");
166     }
167 
168     // The text will be displayed the next frame
169     {
170         root.draw();
171 
172         assert(input.contentLabel.text == "¡Hola, mundo!");
173         assert(input.isFocused);
174     }
175 
176 }
177 
178 @("breakLine does nothing in single line TextInput")
179 unittest {
180 
181     auto root = textInput();
182 
183     root.push("hello");
184     root.runInputAction!(FluidInputAction.breakLine);
185 
186     assert(root.value == "hello");
187 
188 }
189 
190 @("breakLine creates a new TextInput history entry")
191 unittest {
192 
193     auto root = textInput(.multiline);
194 
195     root.push("hello");
196     root.runInputAction!(FluidInputAction.breakLine);
197     assert(root.value == "hello\n");
198 
199     root.undo();
200     assert(root.value == "hello");
201     root.redo();
202     assert(root.value == "hello\n");
203 
204     root.undo();
205     assert(root.value == "hello");
206     root.undo();
207     assert(root.value == "");
208     root.redo();
209     assert(root.value == "hello");
210     root.redo();
211     assert(root.value == "hello\n");
212 
213 }
214 
215 @("breakLine interacts well with Unicode text")
216 unittest {
217 
218     auto root = textInput(.nullTheme, .multiline);
219 
220     root.push("Привет, мир!");
221     root.runInputAction!(FluidInputAction.breakLine);
222 
223     assert(root.value == "Привет, мир!\n");
224     assert(root.caretIndex == root.value.length);
225 
226     root.push("Это пример текста для тестирования поддержки Unicode во Fluid.");
227     root.runInputAction!(FluidInputAction.breakLine);
228 
229     assert(root.value == "Привет, мир!\nЭто пример текста для тестирования поддержки Unicode во Fluid.\n");
230     assert(root.caretIndex == root.value.length);
231 
232 }
233 
234 @("breakLine creates history entries")
235 unittest {
236 
237     auto root = textInput(.multiline);
238     root.push("first line");
239     root.breakLine();
240     root.push("second line");
241     root.breakLine();
242     assert(root.value == "first line\nsecond line\n");
243 
244     root.undo();
245     assert(root.value == "first line\nsecond line");
246     root.undo();
247     assert(root.value == "first line\n");
248     root.undo();
249     assert(root.value == "first line");
250     root.undo();
251     assert(root.value == "");
252     root.redo();
253     assert(root.value == "first line");
254     root.redo();
255     assert(root.value == "first line\n");
256     root.redo();
257     assert(root.value == "first line\nsecond line");
258     root.redo();
259     assert(root.value == "first line\nsecond line\n");
260 
261 }
262 
263 @("Single line TextInput submits text when pressing Enter")
264 unittest {
265 
266     int submitted;
267 
268     auto map = InputMapping();
269     map.bindNew!(FluidInputAction.breakLine)(KeyboardIO.codes.enter);
270     map.bindNew!(FluidInputAction.submit)(KeyboardIO.codes.enter);
271     map.bindNew!(FluidInputAction.breakLine)(KeyboardIO.codes.enter);
272 
273     TextInput input;
274     input = textInput("placeholder", delegate {
275         submitted++;
276         assert(input.value == "Hello World");
277     });
278 
279     auto focus = focusChain();
280     auto root = chain(
281         inputMapChain(map),
282         focus,
283         input,
284     );
285 
286     // Type stuff
287     focus.currentFocus = input;
288     input.value = "Hello World";
289     root.draw();
290     assert(submitted == 0);
291     assert(input.value == "Hello World");
292     assert(input.contentLabel.text == "Hello World");
293 
294     // Submit
295     focus.emitEvent(KeyboardIO.press.enter);
296     root.draw();
297     assert(submitted == 1);
298 
299 }
300 
301 @("Ctrl+Enter submits, while Enter creates a line feed")
302 unittest {
303 
304     auto map = InputMapping();
305     map.bindNew!(FluidInputAction.breakLine)(KeyboardIO.codes.enter);
306     map.bindNew!(FluidInputAction.submit)(KeyboardIO.codes.enter);
307     map.bindNew!(FluidInputAction.breakLine)(KeyboardIO.codes.enter);
308     map.bindNew!(FluidInputAction.submit)(KeyboardIO.codes.leftControl, KeyboardIO.codes.enter);
309 
310     int submitted;
311     auto input = multilineInput("", delegate { submitted++; });
312     auto focus = focusChain();
313     auto root = chain(
314         inputMapChain(map),
315         focus,
316         input,
317     );
318 
319     // Type text
320     focus.currentFocus = input;
321     input.push("Hello, World!");
322     root.draw();
323 
324     // Press enter to create a line feed
325     focus.emitEvent(KeyboardIO.press.enter);
326     root.draw();
327     assert(input.value == "Hello, World!\n");
328     assert(submitted == 0);
329 
330     // TextInput should ignore typed line feeds in this scenario
331     focus.typeText("\n");
332     focus.emitEvent(KeyboardIO.press.enter);
333     root.draw();
334     assert(input.value == "Hello, World!\n\n");
335     assert(submitted == 0);
336 
337     // Press Ctrl+Enter
338     focus.emitEvent(KeyboardIO.press.leftControl);
339     focus.emitEvent(KeyboardIO.press.enter);
340     root.draw();
341 
342     // Input should be submitted
343     assert(input.value == "Hello, World!\n\n");
344     assert(submitted == 1);
345 
346 }
347 
348 @("TextInput.chopWord removes last word and chopWord(true) removes next word; chopWord supports Unicode")
349 unittest {
350 
351     auto root = textInput();
352 
353     root.push("Это пример текста для тестирования поддержки Unicode во Fluid.");
354     root.chopWord;
355     assert(root.value == "Это пример текста для тестирования поддержки Unicode во Fluid");
356 
357     root.chopWord;
358     assert(root.value == "Это пример текста для тестирования поддержки Unicode во ");
359 
360     root.chopWord;
361     assert(root.value == "Это пример текста для тестирования поддержки Unicode ");
362 
363     root.chopWord;
364     assert(root.value == "Это пример текста для тестирования поддержки ");
365 
366     root.chopWord;
367     assert(root.value == "Это пример текста для тестирования ");
368 
369     root.caretToStart();
370     root.chopWord(true);
371     assert(root.value == "пример текста для тестирования ");
372 
373     root.chopWord(true);
374     assert(root.value == "текста для тестирования ");
375 
376 }
377 
378 
379 @("Cannot type while invoking a keyboard shortcut action")
380 unittest {
381 
382     auto map = InputMapping();
383     map.bindNew!(FluidInputAction.backspaceWord)(KeyboardIO.codes.w);
384 
385     auto input = textInput();
386     auto focus = focusChain();
387     auto root = chain(
388         inputMapChain(map),
389         focus,
390         input,
391     );
392 
393     // Type stuff
394     focus.currentFocus = input;
395     input.value = "Hello ";
396     input.caretToEnd();
397     root.draw();
398     assert(input.value == "Hello ");
399 
400     // Typing should be disabled while erasing
401     focus.emitEvent(KeyboardIO.press.w);
402     focus.typeText("w");
403     root.draw();
404 
405     assert(input.value == "");
406     assert(input.isEmpty);
407 
408 }
409 
410 @("FluidInputAction.deleteWord deletes the next word in TextInput")
411 unittest {
412 
413     auto root = textInput();
414 
415     // deleteWord should do nothing, because the caret is at the end
416     root.push("Hello, Wörld");
417     root.runInputAction!(FluidInputAction.deleteWord);
418 
419     assert(!root.isSelecting);
420     assert(root.value == "Hello, Wörld");
421     assert(root.caretIndex == "Hello, Wörld".length);
422 
423     // Move it to the previous word
424     root.runInputAction!(FluidInputAction.previousWord);
425 
426     assert(!root.isSelecting);
427     assert(root.value == "Hello, Wörld");
428     assert(root.caretIndex == "Hello, ".length);
429 
430     // Delete the next word
431     root.runInputAction!(FluidInputAction.deleteWord);
432 
433     assert(!root.isSelecting);
434     assert(root.value == "Hello, ");
435     assert(root.caretIndex == "Hello, ".length);
436 
437     // Move to the start
438     root.runInputAction!(FluidInputAction.toStart);
439 
440     assert(!root.isSelecting);
441     assert(root.value == "Hello, ");
442     assert(root.caretIndex == 0);
443 
444     // Delete the next word
445     root.runInputAction!(FluidInputAction.deleteWord);
446 
447     assert(!root.isSelecting);
448     assert(root.value == ", ");
449     assert(root.caretIndex == 0);
450 
451     // Delete the next word
452     root.runInputAction!(FluidInputAction.deleteWord);
453 
454     assert(!root.isSelecting);
455     assert(root.value == "");
456     assert(root.caretIndex == 0);
457 
458 }
459 
460 @("FluidInputAction.chop removes last word and chop(true) removes next, supports Unicode")
461 unittest {
462 
463     auto root = textInput();
464 
465     root.push("поддержки во Fluid.");
466     root.chop;
467     assert(root.value == "поддержки во Fluid");
468 
469     root.chop;
470     assert(root.value == "поддержки во Flui");
471 
472     root.chop;
473     assert(root.value == "поддержки во Flu");
474 
475     root.chopWord;
476     assert(root.value == "поддержки во ");
477 
478     root.chop;
479     assert(root.value == "поддержки во");
480 
481     root.chop;
482     assert(root.value == "поддержки в");
483 
484     root.chop;
485     assert(root.value == "поддержки ");
486 
487     root.caretToStart();
488     root.chop(true);
489     assert(root.value == "оддержки ");
490 
491     root.chop(true);
492     assert(root.value == "ддержки ");
493 
494     root.chop(true);
495     assert(root.value == "держки ");
496 
497 }
498 
499 @("TextInput.lineByIndex can be used to replace lines")
500 unittest {
501 
502     auto root = textInput(.multiline);
503     root.push("foo");
504     root.lineByIndex(0, "foobar");
505     assert(root.value == "foobar");
506     assert(root.valueBeforeCaret == "foobar");
507 
508     root.push("\nąąąźź");
509     root.lineByIndex(6, "~");
510     root.caretIndex = root.caretIndex - 2;
511     assert(root.value == "~\nąąąźź");
512     assert(root.valueBeforeCaret == "~\nąąąź");
513 
514     root.push("\n\nstuff");
515     assert(root.value == "~\nąąąź\n\nstuffź");
516 
517     root.lineByIndex(11, "");
518     assert(root.value == "~\nąąąź\n\nstuffź");
519 
520     root.lineByIndex(11, "*");
521     assert(root.value == "~\nąąąź\n*\nstuffź");
522 
523 }
524 
525 @("TextInput.lineByIndex works well with Unicode")
526 unittest {
527 
528     auto root = textInput(.multiline);
529     root.push("óne\nßwo\nßhree");
530     root.selectionStart = 5;
531     root.selectionEnd = 14;
532     root.lineByIndex(5, "[REDACTED]");
533     assert(root.value[root.selectionEnd] == 'e');
534     assert(root.value == "óne\n[REDACTED]\nßhree");
535 
536     assert(root.value[root.selectionEnd] == 'e');
537     assert(root.selectionStart == 5);
538     assert(root.selectionEnd == 20);
539 
540 }
541 
542 @("TextInput.caretLine returns the current line")
543 unittest {
544 
545     auto root = textInput(.multiline);
546     assert(root.caretLine == "");
547     root.push("aąaa");
548     assert(root.caretLine == root.value);
549     root.caretIndex = 0;
550     assert(root.caretLine == root.value);
551     root.push("bbb");
552     assert(root.caretLine == root.value);
553     assert(root.value == "bbbaąaa");
554     root.push("\n");
555     assert(root.value == "bbb\naąaa");
556     assert(root.caretLine == "aąaa");
557     root.caretToEnd();
558     root.push("xx");
559     assert(root.caretLine == "aąaaxx");
560     root.push("\n");
561     assert(root.caretLine == "");
562     root.push("\n");
563     assert(root.caretLine == "");
564     root.caretIndex = root.caretIndex - 1;
565     assert(root.caretLine == "");
566     root.caretToStart();
567     assert(root.caretLine == "bbb");
568 
569 }
570 
571 @("TextInput.caretLine can be set to change the current line's content")
572 unittest {
573 
574     auto root = textInput(.multiline);
575     root.push("a\nbb\nccc\n");
576     assert(root.caretLine == "");
577 
578     root.caretIndex = root.caretIndex - 1;
579     assert(root.caretLine == "ccc");
580 
581     root.caretLine = "hi";
582     assert(root.value == "a\nbb\nhi\n");
583 
584     assert(!root.isSelecting);
585     assert(root.valueBeforeCaret == "a\nbb\nhi");
586 
587     root.caretLine = "";
588     assert(root.value == "a\nbb\n\n");
589     assert(root.valueBeforeCaret == "a\nbb\n");
590 
591     root.caretLine = "new value";
592     assert(root.value == "a\nbb\nnew value\n");
593     assert(root.valueBeforeCaret == "a\nbb\nnew value");
594 
595     root.caretIndex = 0;
596     root.caretLine = "insert";
597     assert(root.value == "insert\nbb\nnew value\n");
598     assert(root.valueBeforeCaret == "insert");
599     assert(root.caretLine == "insert");
600 
601 }
602 
603 @("TextInput.column can be used to get distance from line start, either in characters or bytes")
604 unittest {
605 
606     auto root = textInput(.multiline);
607     assert(root.column!dchar == 0);
608     root.push(" ");
609     assert(root.column!dchar == 1);
610     root.push("a");
611     assert(root.column!dchar == 2);
612     root.push("ąąą");
613     assert(root.column!dchar == 5);
614     assert(root.column!char == 8);
615     root.push("O\n");
616     assert(root.column!dchar == 0);
617     root.push(" ");
618     assert(root.column!dchar == 1);
619     root.push("HHH");
620     assert(root.column!dchar == 4);
621 
622 }
623 
624 @("Parts of TextInput text can be iterated with eachLineByIndex")
625 unittest {
626 
627     auto root = textInput(.multiline);
628     root.push("aaaąąą@\r\n#\n##ąąśðą\nĄŚ®ŒĘ¥Ę®\n");
629 
630     size_t i;
631     foreach (line; root.eachLineByIndex(4, 18)) {
632 
633         if (i == 0) assert(line == "aaaąąą@");
634         if (i == 1) assert(line == "#");
635         if (i == 2) assert(line == "##ąąśðą");
636         assert(i.among(0, 1, 2));
637         i++;
638 
639     }
640     assert(i == 3);
641 
642     i = 0;
643     foreach (line; root.eachLineByIndex(22, 27)) {
644 
645         if (i == 0) assert(line == "##ąąśðą");
646         if (i == 1) assert(line == "ĄŚ®ŒĘ¥Ę®");
647         assert(i.among(0, 1));
648         i++;
649 
650     }
651     assert(i == 2);
652 
653     i = 0;
654     foreach (line; root.eachLineByIndex(44, 44)) {
655 
656         assert(i == 0);
657         assert(line == "");
658         i++;
659 
660     }
661     assert(i == 1);
662 
663     i = 0;
664     foreach (line; root.eachLineByIndex(1, 1)) {
665 
666         assert(i == 0);
667         assert(line == "aaaąąą@");
668         i++;
669 
670     }
671     assert(i == 1);
672 
673 }
674 
675 @("TextInput.eachLineByIndex works with single lines of text")
676 unittest {
677 
678     auto root = textInput();
679     root.value = "test";
680 
681     size_t i;
682     foreach (line; root.eachLineByIndex(1, 4)) {
683 
684         assert(i++ == 0);
685         assert(line == "test");
686 
687     }
688 
689 }
690 
691 @("TextInput.eachSelectedLine works with empty text")
692 unittest {
693 
694     bool done;
695     auto root = textInput();
696 
697     foreach (line; root.eachSelectedLine) {
698         done = true;
699         assert(line == "");
700     }
701 
702     assert(done);
703 
704 }
705 
706 @("TextInput.selectWord can be used to select whatever word the caret is touching")
707 unittest {
708 
709     auto root = textInput();
710     root.push("Привет, мир! Это пример текста для тестирования поддержки Unicode во Fluid.");
711 
712     // Select word the caret is touching
713     root.selectWord();
714     assert(root.selectedValue == ".");
715 
716     // Expand
717     root.selectWord();
718     assert(root.selectedValue == "Fluid.");
719 
720     // Go to start
721     root.caretToStart();
722     assert(!root.isSelecting);
723     assert(root.caretIndex == 0);
724     assert(root.selectedValue == "");
725 
726     root.selectWord();
727     assert(root.selectedValue == "Привет");
728 
729     root.selectWord();
730     assert(root.selectedValue == "Привет,");
731 
732     root.selectWord();
733     assert(root.selectedValue == "Привет,");
734 
735     root.runInputAction!(FluidInputAction.nextChar);
736     assert(root.caretIndex == 13);  // Before space
737 
738     root.runInputAction!(FluidInputAction.nextChar);  // After space
739     root.runInputAction!(FluidInputAction.nextChar);  // Inside "мир"
740     assert(!root.isSelecting);
741     assert(root.caretIndex == 16);
742 
743     root.selectWord();
744     assert(root.selectedValue == "мир");
745 
746     root.selectWord();
747     assert(root.selectedValue == "мир!");
748 
749 }
750 
751 @("TextInput.selectLine selects the whole text in single line text inputs")
752 unittest {
753 
754     auto root = textInput();
755 
756     root.push("ąąąą ąąą ąąąąąąą ąą\nąąą ąąą");
757     assert(root.caretIndex == 49);
758 
759     root.selectLine();
760     assert(root.selectedValue == root.value);
761     assert(root.selectedValue.length == 49);
762     assert(root.value.length == 49);
763 
764 }
765 
766 @("TextInput.selectLine selects the line the caret is on")
767 unittest {
768 
769     auto root = textInput(.multiline);
770 
771     root.push("ąąą ąąą ąąąąąąą ąą\nąąą ąąą");
772     root.draw();
773     assert(root.caretIndex == 47);
774 
775     root.selectLine();
776     assert(root.selectedValue == "ąąą ąąą");
777     assert(root.selectionStart == 34);
778     assert(root.selectionEnd == 47);
779 
780     root.runInputAction!(FluidInputAction.selectPreviousLine);
781     assert(root.selectionStart == 34);
782     assert(root.selectionEnd == 13);
783     assert(root.selectedValue == " ąąąąąąą ąą\n");
784 
785     root.selectLine();
786     assert(root.selectedValue == root.value);
787 
788 }
789 
790 @("TextInput.previousWord moves the caret to the previous word")
791 unittest {
792 
793     auto root = textInput();
794     root.push("Привет, мир! Это пример текста для тестирования поддержки Unicode во Fluid.");
795 
796     assert(root.caretIndex == root.value.length);
797 
798     root.runInputAction!(FluidInputAction.previousWord);
799     assert(root.caretIndex == root.value.length - ".".length);
800 
801     root.runInputAction!(FluidInputAction.previousWord);
802     assert(root.caretIndex == root.value.length - "Fluid.".length);
803 
804     root.runInputAction!(FluidInputAction.previousChar);
805     assert(root.caretIndex == root.value.length - " Fluid.".length);
806 
807     root.runInputAction!(FluidInputAction.previousChar);
808     assert(root.caretIndex == root.value.length - "о Fluid.".length);
809 
810     root.runInputAction!(FluidInputAction.previousChar);
811     assert(root.caretIndex == root.value.length - "во Fluid.".length);
812 
813     root.runInputAction!(FluidInputAction.previousWord);
814     assert(root.caretIndex == root.value.length - "Unicode во Fluid.".length);
815 
816     root.runInputAction!(FluidInputAction.previousWord);
817     assert(root.caretIndex == root.value.length - "поддержки Unicode во Fluid.".length);
818 
819     root.runInputAction!(FluidInputAction.nextChar);
820     assert(root.caretIndex == root.value.length - "оддержки Unicode во Fluid.".length);
821 
822     root.runInputAction!(FluidInputAction.nextWord);
823     assert(root.caretIndex == root.value.length - "Unicode во Fluid.".length);
824 
825 }
826 
827 @("previousLine/nextLine keeps the current column in TextInput")
828 unittest {
829 
830     auto root = textInput(.multiline);
831 
832     // 5 en dashes, 3 then 4; starting at last line
833     root.push("–––––\n–––\n––––");
834     root.draw();
835 
836     assert(root.caretIndex == root.value.length);
837 
838     // From last line to second line — caret should be at its end
839     root.runInputAction!(FluidInputAction.previousLine);
840     assert(root.valueBeforeCaret == "–––––\n–––");
841 
842     // First line, move to 4th dash (same as third line)
843     root.runInputAction!(FluidInputAction.previousLine);
844     assert(root.valueBeforeCaret == "––––");
845 
846     // Next line — end
847     root.runInputAction!(FluidInputAction.nextLine);
848     assert(root.valueBeforeCaret == "–––––\n–––");
849 
850     // Update anchor to match second line
851     root.runInputAction!(FluidInputAction.toLineEnd);
852     assert(root.valueBeforeCaret == "–––––\n–––");
853 
854     // First line again, should be 3rd dash now (same as second line)
855     root.runInputAction!(FluidInputAction.previousLine);
856     assert(root.valueBeforeCaret == "–––");
857 
858     // Last line, 3rd dash too
859     root.runInputAction!(FluidInputAction.nextLine);
860     root.runInputAction!(FluidInputAction.nextLine);
861     assert(root.valueBeforeCaret == "–––––\n–––\n–––");
862 
863 }
864 
865 @("TextInput.push replaces and clears selection")
866 unittest {
867 
868     auto root = textInput();
869 
870     root.draw();
871     root.selectAll();
872 
873     assert(root.selectionStart == 0);
874     assert(root.selectionEnd == 0);
875 
876     root.push("foo bar ");
877 
878     assert(!root.isSelecting);
879 
880     root.push("baz");
881 
882     assert(root.value == "foo bar baz");
883 
884     auto value1 = root.value;
885 
886     root.selectAll();
887 
888     assert(root.selectionStart == 0);
889     assert(root.selectionEnd == root.value.length);
890 
891     root.push("replaced");
892 
893     assert(root.value == "replaced");
894 
895 }
896 
897 @("Inserts can be undone with TextInput.undo, and redone with TextInput.redo")
898 unittest {
899 
900     auto root = textInput(.multiline);
901     root.push("Hello, ");
902     root.runInputAction!(FluidInputAction.breakLine);
903     root.push("new");
904     root.runInputAction!(FluidInputAction.breakLine);
905     root.push("line");
906     root.chop;
907     root.chopWord;
908     root.push("few");
909     root.push(" lines");
910     assert(root.value == "Hello, \nnew\nfew lines");
911 
912     // Move back to last chop
913     root.undo();
914     assert(root.value == "Hello, \nnew\n");
915 
916     // Test redo
917     root.redo();
918     assert(root.value == "Hello, \nnew\nfew lines");
919     root.undo();
920     assert(root.value == "Hello, \nnew\n");
921 
922     // Move back through isnerts
923     root.undo();
924     assert(root.value == "Hello, \nnew\nline");
925     root.undo();
926     assert(root.value == "Hello, \nnew\n");
927     root.undo();
928     assert(root.value == "Hello, \nnew");
929     root.undo();
930     assert(root.value == "Hello, \n");
931     root.undo();
932     assert(root.value == "Hello, ");
933     root.undo();
934     assert(root.value == "");
935     root.redo();
936     assert(root.value == "Hello, ");
937     root.redo();
938     assert(root.value == "Hello, \n");
939     root.redo();
940     assert(root.value == "Hello, \nnew");
941     root.redo();
942     assert(root.value == "Hello, \nnew\n");
943     root.redo();
944     assert(root.value == "Hello, \nnew\nline");
945     root.redo();
946     assert(root.value == "Hello, \nnew\n");
947     root.redo();
948     assert(root.value == "Hello, \nnew\nfew lines");
949 
950     // Navigate and replace "Hello"
951     root.caretIndex = 5;
952     root.runInputAction!(FluidInputAction.selectPreviousWord);
953     root.push("Hi");
954     assert(root.value == "Hi, \nnew\nfew lines");
955     assert(root.valueBeforeCaret == "Hi");
956 
957     root.undo();
958     assert(root.value == "Hello, \nnew\nfew lines");
959     assert(root.selectedValue == "Hello");
960 
961     root.undo();
962     assert(root.value == "Hello, \nnew\n");
963     assert(root.valueAfterCaret == "");
964 
965 }
966 
967 @("Movement breaks up inserts into separate TextInput history entries")
968 unittest {
969 
970     auto root = textInput();
971 
972     foreach (i; 0..4) {
973         root.caretToStart();
974         root.push("a");
975     }
976 
977     assert(root.value == "aaaa");
978     assert(root.valueBeforeCaret == "a");
979     root.undo();
980     assert(root.value == "aaa");
981     assert(root.valueBeforeCaret == "");
982     root.undo();
983     assert(root.value == "aa");
984     assert(root.valueBeforeCaret == "");
985     root.undo();
986     assert(root.value == "a");
987     assert(root.valueBeforeCaret == "");
988     root.undo();
989     assert(root.value == "");
990 
991 }
992 
993 @("TextInput.selectToEnd selects until a linea break")
994 unittest {
995 
996     auto root = textInput(.nullTheme, .multiline);
997     auto lineHeight = root.style.getTypeface.lineHeight;
998 
999     root.value = "First one\nSecond two";
1000     root.draw();
1001 
1002     // Navigate to the start and select the whole line
1003     root.caretToStart();
1004     root.runInputAction!(FluidInputAction.selectToLineEnd);
1005 
1006     assert(root.selectedValue == "First one");
1007     assert(root.caretPosition.y < lineHeight);
1008 
1009 }
1010 
1011 @("wordFront returns the next word in text and wordBack returns the last word")
1012 unittest {
1013 
1014     assert("hello world!".wordFront == "hello ");
1015     assert("hello, world!".wordFront == "hello");
1016     assert("hello world!".wordBack == "!");
1017     assert("hello world".wordBack == "world");
1018     assert("hello ".wordBack == "hello ");
1019 
1020     assert("witaj świecie!".wordFront == "witaj ");
1021     assert(" świecie!".wordFront == " ");
1022     assert("świecie!".wordFront == "świecie");
1023     assert("witaj świecie!".wordBack == "!");
1024     assert("witaj świecie".wordBack == "świecie");
1025     assert("witaj ".wordBack == "witaj ");
1026 
1027     assert("Всем привет!".wordFront == "Всем ");
1028     assert("привет!".wordFront == "привет");
1029     assert("!".wordFront == "!");
1030 
1031     // dstring
1032     assert("Всем привет!"d.wordFront == "Всем "d);
1033     assert("привет!"d.wordFront == "привет"d);
1034     assert("!"d.wordFront == "!"d);
1035 
1036     assert("Всем привет!"d.wordBack == "!"d);
1037     assert("Всем привет"d.wordBack == "привет"d);
1038     assert("Всем "d.wordBack == "Всем "d);
1039 
1040     // Whitespace exclusion
1041     assert("witaj świecie!".wordFront(true) == "witaj");
1042     assert(" świecie!".wordFront(true) == "");
1043     assert("witaj świecie".wordBack(true) == "świecie");
1044     assert("witaj ".wordBack(true) == "");
1045 
1046 }
1047 
1048 @("wordFront and wordBack select words, and can select line feeds")
1049 unittest {
1050 
1051     assert("\nabc\n".wordFront == "\n");
1052     assert("\n  abc\n".wordFront == "\n  ");
1053     assert("abc\n".wordFront == "abc");
1054     assert("abc  \n".wordFront == "abc  ");
1055     assert("  \n".wordFront == "  ");
1056     assert("\n     abc".wordFront == "\n     ");
1057 
1058     assert("\nabc\n".wordBack == "\n");
1059     assert("\nabc".wordBack == "abc");
1060     assert("abc  \n".wordBack == "\n");
1061     assert("abc  ".wordFront == "abc  ");
1062     assert("\nabc\n  ".wordBack == "\n  ");
1063     assert("\nabc\n  a".wordBack == "a");
1064 
1065     assert("\r\nabc\r\n".wordFront == "\r\n");
1066     assert("\r\n  abc\r\n".wordFront == "\r\n  ");
1067     assert("abc\r\n".wordFront == "abc");
1068     assert("abc  \r\n".wordFront == "abc  ");
1069     assert("  \r\n".wordFront == "  ");
1070     assert("\r\n     abc".wordFront == "\r\n     ");
1071 
1072     assert("\r\nabc\r\n".wordBack == "\r\n");
1073     assert("\r\nabc".wordBack == "abc");
1074     assert("abc  \r\n".wordBack == "\r\n");
1075     assert("abc  ".wordFront == "abc  ");
1076     assert("\r\nabc\r\n  ".wordBack == "\r\n  ");
1077     assert("\r\nabc\r\n  a".wordBack == "a");
1078 
1079 }
1080 
1081 @("TextInput.chop supports unicode")
1082 unittest {
1083 
1084     auto root = textInput();
1085 
1086     // Type stuff
1087     root.value = "hello‽";
1088     root.caretToEnd();
1089     root.draw();
1090 
1091     assert(root.value == "hello‽");
1092     assert(root.contentLabel.text == "hello‽");
1093 
1094     // Erase a letter
1095     root.chop;
1096     root.draw();
1097     assert(root.value == "hello");
1098     assert(root.contentLabel.text == "hello");
1099 
1100     // Erase a letter
1101     root.chop;
1102     root.draw();
1103     assert(root.value == "hell");
1104     assert(root.contentLabel.text == "hell");
1105 
1106 }
1107 
1108 @("TextInput.chop/chopWord/clear don't affect extracted ropes")
1109 unittest {
1110 
1111     auto root = textInput();
1112 
1113     root.push("Hello, World!");
1114     auto value1 = root.value;
1115 
1116     root.chop();
1117     assert(root.value == "Hello, World");
1118 
1119     auto value2 = root.value;
1120     root.chopWord();
1121 
1122     assert(root.value == "Hello, ");
1123     assert(value1  == "Hello, World!");
1124 
1125     auto value3 = root.value;
1126     root.clear();
1127 
1128     assert(root.value == "");
1129     assert(value3  == "Hello, ");
1130     assert(value2  == "Hello, World");
1131     assert(value1  == "Hello, World!");
1132 
1133 }
1134 
1135 @("TextInput.chopWord/push doesn't affect extracted ropes")
1136 unittest {
1137 
1138     auto root = textInput();
1139 
1140     root.push("Hello, World");
1141     root.draw();
1142 
1143     auto value1 = root.value;
1144     root.chopWord();
1145     assert(root.value == "Hello, ");
1146 
1147     auto value2 = root.value;
1148     root.push("Moon");
1149     assert(root.value == "Hello, Moon");
1150 
1151     auto value3 = root.value;
1152     root.clear();
1153 
1154     assert(root.value == "");
1155     assert(value3 == "Hello, Moon");
1156     assert(value2 == "Hello, ");
1157     assert(value1 == "Hello, World");
1158 
1159 }
1160 
1161 @("TextInput.caretTo works")
1162 unittest {
1163 
1164     // Note: This test depends on parameters specific to the default typeface.
1165 
1166     import std.math : isClose;
1167 
1168     auto root = textInput(.nullTheme, .multiline);
1169     root.size = Vector2(200, 0);
1170     root.value = "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp, make it long enough to cross over";
1171     root.draw();
1172 
1173     // Move the caret to different points on the canvas
1174 
1175     // Left side of the second "l" in "Hello", first line
1176     root.caretTo(Vector2(30, 10));
1177     assert(root.caretIndex == "Hel".length);
1178 
1179     // Right side of the same "l"
1180     root.caretTo(Vector2(33, 10));
1181     assert(root.caretIndex == "Hell".length);
1182 
1183     // Comma, right side, close to the second line
1184     root.caretTo(Vector2(50, 24));
1185     assert(root.caretIndex == "Hello,".length);
1186 
1187     // End of the line, far right
1188     root.caretTo(Vector2(200, 10));
1189     assert(root.caretIndex == "Hello, World!".length);
1190 
1191     // Start of the next line
1192     root.caretTo(Vector2(0, 30));
1193     assert(root.caretIndex == "Hello, World!\n".length);
1194 
1195     // Space, right between "Hello," and "Moon"
1196     root.caretTo(Vector2(54, 40));
1197     assert(root.caretIndex == "Hello, World!\nHello, ".length);
1198 
1199     // Empty line
1200     root.caretTo(Vector2(54, 60));
1201     assert(root.caretIndex == "Hello, World!\nHello, Moon\n".length);
1202 
1203     // Beginning of the next line; left side of the "H"
1204     root.caretTo(Vector2(4, 85));
1205     assert(root.caretIndex == "Hello, World!\nHello, Moon\n\n".length);
1206 
1207     // Wrapped line, the bottom of letter "p" in "up"
1208     root.caretTo(Vector2(142, 128));
1209     assert(root.caretIndex == "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp".length);
1210 
1211     // End of line
1212     root.caretTo(Vector2(160, 128));
1213     assert(root.caretIndex == "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp, ".length);
1214 
1215     // Beginning of the next line; result should be the same
1216     root.caretTo(Vector2(2, 148));
1217     assert(root.caretIndex == "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp, ".length);
1218 
1219     // Just by the way, check if the caret position is correct
1220     root.updateCaretPosition(true);
1221     assert(root.caretPosition.x.isClose(0));
1222     assert(root.caretPosition.y.isClose(135));
1223 
1224     root.updateCaretPosition(false);
1225     assert(root.caretPosition.x.isClose(153));
1226     assert(root.caretPosition.y.isClose(108));
1227 
1228     // Try the same with the third line
1229     root.caretTo(Vector2(200, 148));
1230     assert(root.caretIndex
1231         == "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp, make it long enough ".length);
1232     root.caretTo(Vector2(2, 168));
1233     assert(root.caretIndex
1234         == "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp, make it long enough ".length);
1235 
1236 }
1237 
1238 @("previousLine/nextLine keep visual column in TextInput")
1239 unittest {
1240 
1241     // Note: This test depends on parameters specific to the default typeface.
1242 
1243     import std.math : isClose;
1244 
1245     auto root = textInput(.nullTheme, .multiline);
1246     root.size = Vector2(200, 0);
1247     root.value = "Hello, World!\nHello, Moon\n\nHello, Sun\nWrap this line µp, make it long enough to cross over";
1248     root.draw();
1249 
1250     root.caretIndex = 0;
1251     root.updateCaretPosition();
1252     root.runInputAction!(FluidInputAction.toLineEnd);
1253 
1254     assert(root.caretIndex == "Hello, World!".length);
1255 
1256     // Move to the next line, should be at the end
1257     root.runInputAction!(FluidInputAction.nextLine);
1258 
1259     assert(root.valueBeforeCaret.wordBack == "Moon");
1260     assert(root.valueAfterCaret.wordFront == "\n");
1261 
1262     // Move to the blank line
1263     root.runInputAction!(FluidInputAction.nextLine);
1264 
1265     const blankLine = root.caretIndex;
1266     assert(root.valueBeforeCaret.wordBack == "\n");
1267     assert(root.valueAfterCaret.wordFront == "\n");
1268 
1269     // toLineEnd and toLineStart should have no effect
1270     root.runInputAction!(FluidInputAction.toLineStart);
1271     assert(root.caretIndex == blankLine);
1272     root.runInputAction!(FluidInputAction.toLineEnd);
1273     assert(root.caretIndex == blankLine);
1274 
1275     // Next line again
1276     // The anchor has been reset to the beginning
1277     root.runInputAction!(FluidInputAction.nextLine);
1278 
1279     assert(root.valueBeforeCaret.wordBack == "\n");
1280     assert(root.valueAfterCaret.wordFront == "Hello");
1281 
1282     // Move to the very end
1283     root.runInputAction!(FluidInputAction.toEnd);
1284 
1285     assert(root.valueBeforeCaret.wordBack == "over");
1286     assert(root.valueAfterCaret.wordFront == "");
1287 
1288     // Move to start of the line
1289     root.runInputAction!(FluidInputAction.toLineStart);
1290 
1291     assert(root.valueBeforeCaret.wordBack == "enough ");
1292     assert(root.valueAfterCaret.wordFront == "to ");
1293     assert(root.caretPosition.x.isClose(0));
1294 
1295     // Move to the previous line
1296     root.runInputAction!(FluidInputAction.previousLine);
1297 
1298     assert(root.valueBeforeCaret.wordBack == ", ");
1299     assert(root.valueAfterCaret.wordFront == "make ");
1300     assert(root.caretPosition.x.isClose(0));
1301 
1302     // Move to its end — position should be the same as earlier, but the caret should be on the same line
1303     root.runInputAction!(FluidInputAction.toLineEnd);
1304 
1305     assert(root.valueBeforeCaret.wordBack == "enough ");
1306     assert(root.valueAfterCaret.wordFront == "to ");
1307     assert(root.caretPosition.x.isClose(181));
1308 
1309     // Move to the previous line — again
1310     root.runInputAction!(FluidInputAction.previousLine);
1311 
1312     assert(root.valueBeforeCaret.wordBack == ", ");
1313     assert(root.valueAfterCaret.wordFront == "make ");
1314     assert(root.caretPosition.x.isClose(153));
1315 
1316 }
1317 
1318 @("TextInput automatically updates scrolling ancestors")
1319 unittest {
1320 
1321     // Note: This theme relies on properties of the default typeface
1322 
1323     import fluid.scroll;
1324 
1325     const viewportWidth = 200;
1326     const viewportHeight = 50;
1327 
1328     auto theme = nullTheme.derive(
1329         rule!Node(
1330             Rule.typeface = Style.defaultTypeface,
1331             Rule.fontSize = 20.pt,
1332             Rule.textColor = color("#fff"),
1333             Rule.backgroundColor = color("#000"),
1334         ),
1335     );
1336     auto input = multilineInput();
1337     auto root = sizeLock!vscrollFrame(
1338         .sizeLimit(viewportWidth, viewportHeight),
1339         theme,
1340         input
1341     );
1342 
1343     root.draw();
1344     assert(root.scroll == 0);
1345 
1346     // Begin typing
1347     input.push("FLUID\nIS\nAWESOME");
1348     input.caretToStart();
1349     input.push("FLUID\nIS\nAWESOME\n");
1350     root.draw();
1351     root.draw();
1352 
1353     const focusBox = input.focusBoxImpl(Rectangle(0, 0, viewportWidth, viewportHeight));
1354 
1355     assert(focusBox.start == input.caretPosition);
1356     assert(focusBox.end.y - viewportHeight == root.scroll);
1357 
1358 }
1359 
1360 @("TextInput text can be selected with mouse")
1361 unittest {
1362 
1363     // This test relies on properties of the default typeface
1364 
1365     import std.math : isClose;
1366 
1367     auto input = textInput();
1368     auto hover = hoverChain();
1369     auto root = testSpace(
1370         .testTheme,
1371         chain(inputMapChain(), hover, input)
1372     );
1373     input.value = "Hello, World! Foo, bar, scroll this input";
1374     input.caretToEnd();
1375     root.draw();
1376 
1377     assert(input.scroll.isClose(127));
1378 
1379     // Select some stuff
1380     hover.point(150, 10)
1381         .then((a) {
1382             a.press(false);
1383             return a.move(65, 10);
1384         })
1385         .then((a) {
1386             a.press(false);
1387             assert(input.selectedValue == "scroll this");
1388         })
1389         .runWhileDrawing(root);
1390 
1391     // Match the selection box
1392     root.drawAndAssert(
1393         input.drawsRectangle(64, 0, 86, 27).ofColor("#02a")
1394     );
1395 
1396 }
1397 
1398 @("Double-click selects words, and triple-click selects lines")
1399 unittest {
1400 
1401     // This test relies on properties of the default typeface
1402 
1403     import std.math : isClose;
1404 
1405     auto input = textInput(nullTheme);
1406     auto hover = hoverChain();
1407     auto root = chain(hover, input);
1408     input.value = "Hello, World! Foo, bar, scroll this input";
1409     input.caretToEnd();
1410     root.draw();
1411 
1412     hover.point(150, 10)
1413         .then((action) {
1414 
1415             // Double- and triple-click
1416             foreach (i; 0..3) {
1417 
1418                 assert(action.isHovered(input));
1419 
1420                 action.press(true, i+1);
1421                 root.draw();
1422 
1423                 // Double-clicked
1424                 if (i == 1) {
1425                     assert(input.selectedValue == "this");
1426                 }
1427 
1428                 // Triple-clicked
1429                 if (i == 2) {
1430                     assert(input.selectedValue == input.value);
1431                 }
1432 
1433             }
1434 
1435         })
1436         .runWhileDrawing(root);
1437 
1438     assert(input.selectedValue == input.value);
1439 
1440 }
1441 
1442 @("caretToPointer correctly maps mouse coordinates to internal")
1443 unittest {
1444 
1445     import std.math : isClose;
1446 
1447     // caretToMouse is a just a wrapper over caretTo, enabling mouse input
1448     // This test checks if it correctly maps mouse coordinates to internal coordinates
1449 
1450     auto theme = nullTheme.derive(
1451         rule!TextInput(
1452             Rule.margin = 40,
1453             Rule.padding = 40,
1454         )
1455     );
1456     auto input = textInput(.multiline, theme);
1457     auto hover = hoverChain();
1458     auto root = chain(hover, input);
1459     input.size = Vector2(200, 0);
1460     input.value = "123\n456\n789";
1461     root.draw();
1462 
1463     assert(input.caretIndex == 0);
1464 
1465     hover.point(140, 90)
1466         .then((a) {
1467              input.caretToPointer(a.pointer);
1468         })
1469         .runWhileDrawing(root);
1470 
1471     assert(input.caretIndex == 3);
1472 
1473 }
1474 
1475 @("TextInput.cut removes text and puts it in the clipboard")
1476 unittest {
1477 
1478     auto input = textInput();
1479     auto root = clipboardChain(input);
1480 
1481     root.draw();
1482     input.push("Foo Bar Baz Ban");
1483 
1484     // Move cursor to "Bar"
1485     input.runInputAction!(FluidInputAction.toStart);
1486     input.runInputAction!(FluidInputAction.nextWord);
1487 
1488     // Select "Bar Baz "
1489     input.runInputAction!(FluidInputAction.selectNextWord);
1490     input.runInputAction!(FluidInputAction.selectNextWord);
1491 
1492     assert(root.value == "");
1493     assert(input.selectedValue == "Bar Baz ");
1494 
1495     // Cut the text
1496     input.cut();
1497 
1498     assert(root.value == "Bar Baz ");
1499     assert(input.value == "Foo Ban");
1500 
1501 }
1502 
1503 @("TextInput.cut works with Unicode")
1504 unittest {
1505 
1506     auto input = textInput();
1507     auto clipboard = clipboardChain(input);
1508     auto root = clipboard;
1509 
1510     input.push("Привет, мир! Это пример текста для тестирования поддержки Unicode во Fluid.");
1511     root.draw();
1512     clipboard.value = "ą";
1513 
1514     input.runInputAction!(FluidInputAction.previousChar);
1515     input.selectionStart = 106;  // Before "Unicode"
1516     input.cut();
1517 
1518     assert(input.value == "Привет, мир! Это пример текста для тестирования поддержки .");
1519     assert(clipboard.value == "Unicode во Fluid");
1520 
1521     input.caretIndex = 14;
1522     input.runInputAction!(FluidInputAction.selectNextWord);  // мир
1523     input.paste();
1524 
1525     assert(input.value == "Привет, Unicode во Fluid! Это пример текста для тестирования поддержки .");
1526 
1527 }
1528 
1529 @("TextInput.copy copies text without editing")
1530 unittest {
1531 
1532     auto input = textInput();
1533     auto clipboard = clipboardChain(input);
1534     auto root = clipboard;
1535 
1536     root.draw();
1537     input.push("Foo Bar Baz Ban");
1538     input.selectAll();
1539     assert(clipboard.value == "");
1540 
1541     input.copy();
1542     assert(clipboard.value == "Foo Bar Baz Ban");
1543 
1544     // Reduce selection by a word
1545     input.runInputAction!(FluidInputAction.selectPreviousWord);
1546     input.copy();
1547 
1548     assert(clipboard.value == "Foo Bar Baz ");
1549     assert(input.value == "Foo Bar Baz Ban");
1550 
1551 }
1552 
1553 @("TextInput.paste inserts text from the clipboard")
1554 unittest {
1555 
1556     auto input = textInput();
1557     auto clipboard = clipboardChain(input);
1558     auto root = clipboard;
1559 
1560     input.value = "Foo ";
1561     root.draw();
1562     input.caretToEnd();
1563     clipboard.value = "Bar";
1564     assert(input.caretIndex == 4);
1565     assert(input.value == "Foo ");
1566 
1567     input.paste();
1568     assert(input.caretIndex == 7);
1569     assert(input.value == "Foo Bar");
1570 
1571     input.caretToStart();
1572     input.paste();
1573     assert(input.caretIndex == 3);
1574     assert(input.value == "BarFoo Bar");
1575 
1576 }
1577 
1578 @("TextInput read large amounts of text at once")
1579 unittest {
1580 
1581     import std.array;
1582     import std.range : repeat;
1583 
1584     immutable(char)[4096] content = 'a';
1585 
1586     auto input = textInput();
1587     auto focus = focusChain();
1588     auto root = chain(focus, input);
1589     root.draw();
1590 
1591     focus.currentFocus = input;
1592     focus.typeText(content[]);
1593     root.draw();
1594 
1595     assert(input.value == content);
1596 
1597 }
1598 
1599 @("TextInput.paste supports clipboard with lots of content")
1600 unittest {
1601 
1602     import std.array;
1603     import std.range : repeat;
1604 
1605     immutable(char)[4096] content = 'a';
1606 
1607     auto input = textInput();
1608     auto clipboard = clipboardChain();
1609     auto root = chain(clipboard, input);
1610     clipboard.value = content[];
1611     root.draw();
1612 
1613     input.paste();
1614     assert(input.value == content);
1615 
1616 }
1617 
1618 @("TextInput: Mouse selections works correctly across lines")
1619 unittest {
1620 
1621     import std.math : isClose;
1622 
1623     auto input = textInput(.multiline, .testTheme);
1624     auto hover = hoverChain();
1625     auto root = chain(inputMapChain(), hover, input);
1626 
1627     input.value = "Line one\nLine two\n\nLine four";
1628     root.draw();
1629 
1630     auto lineHeight = input.style.getTypeface.lineHeight;
1631 
1632     // Move the caret to second line
1633     input.caretIndex = "Line one\nLin".length;
1634     input.updateCaretPosition();
1635 
1636     const middle = input.caretPosition;
1637     const top    = middle - Vector2(0, lineHeight);
1638     const blank  = middle + Vector2(0, lineHeight);
1639     const bottom = middle + Vector2(0, lineHeight * 2);
1640 
1641     // Press in the middle and drag to the top
1642     hover.point(middle)
1643         .then((a) {
1644             a.press(false);
1645             return a.move(top);
1646         })
1647 
1648         // Check results; move to bottom
1649         .then((a) {
1650             a.press(false);
1651             assert(input.selectedValue == "e one\nLin");
1652             assert(input.selectionStart > input.selectionEnd);
1653             return a.move(bottom);
1654         })
1655 
1656         // Now move to the blank line
1657         .then((a) {
1658             a.press(false);
1659             assert(input.selectedValue == "e two\n\nLin");
1660             assert(input.selectionStart < input.selectionEnd);
1661             return a.move(blank);
1662         })
1663         .then((a) {
1664             a.press(true);
1665             assert(input.selectedValue == "e two\n");
1666             assert(input.selectionStart < input.selectionEnd);
1667         })
1668         .runWhileDrawing(root);
1669 
1670 }
1671 
1672 @("TextInput: Mouse selections can select words by double clicking")
1673 unittest {
1674 
1675     auto input = textInput(.multiline, .testTheme);
1676     auto hover = hoverChain();
1677     auto root = chain(inputMapChain(), hover, input);
1678 
1679     input.value = "Line one\nLine two\n\nLine four";
1680     root.draw();
1681 
1682     auto lineHeight = input.style.getTypeface.lineHeight;
1683 
1684     // Move the caret to second line
1685     input.caretIndex = "Line one\nLin".length;
1686     input.updateCaretPosition();
1687 
1688     const middle = input.caretPosition;
1689     const top    = middle - Vector2(0, lineHeight);
1690     const blank  = middle + Vector2(0, lineHeight);
1691     const bottom = middle + Vector2(0, lineHeight * 2);
1692 
1693     // Double click in the middle
1694     hover.point(middle)
1695         .then((a) {
1696             a.doubleClick(false);
1697             assert(input.selectedValue == "Line");
1698             assert(input.selectionStart < input.selectionEnd);
1699 
1700             // Drag the pointer to top row
1701             return a.move(top);
1702         })
1703         .then((a) {
1704             a.doubleClick(false);
1705             assert(input.selectedValue == "Line one\nLine");
1706             assert(input.selectionStart > input.selectionEnd);
1707 
1708             // Bottom row
1709             return a.move(bottom);
1710         })
1711         .then((a) {
1712             a.doubleClick(false);
1713             assert(input.selectedValue == "Line two\n\nLine");
1714             assert(input.selectionStart < input.selectionEnd);
1715 
1716             // And now drag the pointer to the blank line
1717             return a.move(blank);
1718         })
1719         .then((a) {
1720             a.doubleClick(true);
1721         })
1722         .runWhileDrawing(root);
1723 
1724     assert(input.selectedValue == "Line two\n");
1725     assert(input.selectionStart < input.selectionEnd);
1726 
1727 }
1728 
1729 @("TextInput: Mouse selections can select words by triple clicking")
1730 unittest {
1731 
1732     auto input = textInput(.multiline, .testTheme);
1733     auto hover = hoverChain();
1734     auto root = chain(inputMapChain(), hover, input);
1735 
1736     input.value = "Line one\nLine two\n\nLine four";
1737     root.draw();
1738 
1739     auto lineHeight = input.style.getTypeface.lineHeight;
1740 
1741     // Move the caret to second line
1742     input.caretIndex = "Line one\nLin".length;
1743     input.updateCaretPosition();
1744 
1745     const middle = input.caretPosition;
1746     const top    = middle - Vector2(0, lineHeight);
1747     const blank  = middle + Vector2(0, lineHeight);
1748     const bottom = middle + Vector2(0, lineHeight * 2);
1749 
1750     hover.point(middle)
1751         .then((a) {
1752             a.tripleClick(false);
1753             assert(input.selectedValue == "Line two");
1754             assert(input.selectionStart < input.selectionEnd);
1755 
1756             return a.move(top);
1757         })
1758         .then((a) {
1759             a.tripleClick(false);
1760             assert(input.selectedValue == "Line one\nLine two");
1761             assert(input.selectionStart > input.selectionEnd);
1762 
1763             return a.move(bottom);
1764         })
1765         .then((a) {
1766             a.tripleClick(false);
1767             assert(input.selectedValue == "Line two\n\nLine four");
1768             assert(input.selectionStart < input.selectionEnd);
1769 
1770             return a.move(blank);
1771         })
1772         .then((a) {
1773             a.tripleClick(true);
1774         })
1775         .runWhileDrawing(root);
1776 
1777     assert(input.selectedValue == "Line two\n");
1778     assert(input.selectionStart < input.selectionEnd);
1779 
1780 }
1781 
1782 @("TextInput selection displays correctly in HiDPI")
1783 unittest {
1784 
1785     auto node = multilineInput(.testTheme);
1786     auto root = testSpace(node);
1787 
1788     // Matsuo Bashō "The Old Pond"
1789     node.value = "Old pond...\n"
1790         ~ "a frog jumps in\n"
1791         ~ "water's sound\n";
1792     node.selectSlice(4, 33);
1793 
1794     // 100% scale
1795     root.drawAndAssert(
1796         node.drawsRectangle(0, 0, 200, 108).ofColor("#ffaaff"),
1797         node.cropsTo(0, 0, 200, 108),
1798 
1799         // Selection
1800         node.drawsRectangle(33, 0, 59, 27).ofColor("#0022aa"),
1801         node.drawsRectangle(0, 27, 128, 27).ofColor("#0022aa"),
1802         node.drawsRectangle(0, 54, 50, 27).ofColor("#0022aa"),
1803 
1804         node.contentLabel.isDrawn().at(0, 0, 200, 108),
1805         node.contentLabel.drawsHintedImage().at(0, 0, 1024, 1024).ofColor("#ffffff")
1806             .sha256("7033f92fce5cf825ab357b1514628504361399d20ce47e2966ed86cacc45cf3a"),
1807     );
1808 
1809     // 125% scale
1810     root.setScale(1.25);
1811     root.drawAndAssert(
1812 
1813         // Selection
1814         node.drawsRectangle(33.6, 0, 57.6, 26.4).ofColor("#0022aa"),
1815         node.drawsRectangle(0, 26.4, 128.8, 26.4).ofColor("#0022aa"),
1816         node.drawsRectangle(0, 52.8, 48.8, 26.4).ofColor("#0022aa"),
1817 
1818         node.contentLabel.isDrawn().at(0, 0, 200, 106),
1819         node.contentLabel.drawsHintedImage().at(0, 0, 819.2, 819.2).ofColor("#ffffff")
1820             .sha256("2c72029c85ba28479d2089456261828dfb046c1be134b46408740b853e352b90"),
1821     );
1822 
1823 }
1824 
1825 @("TextInput pointer position is correctly recognized in HiDPI")
1826 unittest {
1827 
1828     auto node = multilineInput(.testTheme);
1829     auto focus = focusChain(node);
1830     auto root = testSpace(focus);
1831 
1832     focus.currentFocus = node;
1833 
1834     // Matsuo Bashō "The Old Pond"
1835     node.value = "Old pond...\n"
1836         ~ "a frog jumps in\n"
1837         ~ "water's sound\n";
1838 
1839     // Warning: There is some kind of precision loss going on here
1840     foreach (i, scale; [1.00, 1.25]) {
1841         root.setScale(scale);
1842         root.draw();
1843 
1844         node.caretTo(Vector2(36, 10));
1845         node.updateCaretPosition();
1846         assert(node.caretIndex == 4);
1847         root.drawAndAssert(
1848             i == 0
1849                 ? node.drawsLine().from(33.0, 2.70).to(33.0, 24.30).ofWidth(1).ofColor("#000000")
1850                 : node.drawsLine().from(33.6, 2.64).to(33.6, 23.76).ofWidth(1).ofColor("#000000"),
1851         );
1852 
1853         node.caretTo(Vector2(47, 66));
1854         node.updateCaretPosition();
1855         assert(node.caretIndex == 33);
1856         root.drawAndAssert(
1857             i == 0
1858                 ? node.drawsLine().from(50.0, 56.70).to(50.0, 78.30).ofWidth(1).ofColor("#000000")
1859                 : node.drawsLine().from(48.8, 55.44).to(48.8, 76.56).ofWidth(1).ofColor("#000000"),
1860         );
1861     }
1862 
1863 }
1864 
1865 @("TextInput scrolling works correctly in HiDPI")
1866 unittest {
1867 
1868     enum textConstant = " one two three four";
1869 
1870     auto node = lineInput();
1871     auto root = testSpace(.testTheme, node);
1872     root.setScale(1.25);
1873     root.draw();
1874 
1875     node.push(textConstant);
1876     root.drawAndAssert(
1877         node.isDrawn().at(0, 0, 200, 27),
1878         node.drawsRectangle(0, 0, 200, 27).ofColor("#ffaaff"),
1879         node.cropsTo(0, 0, 200, 27),
1880         node.contentLabel.drawsHintedImage().at(0, 0, 819.2, 819.2).ofColor("#ffffff")
1881             .sha256("f8e7558a9641e24bb5cb8bb49c27284d87436789114e2f875e2736b521fe170e"),
1882         node.contentLabel.doesNotDraw(),
1883     );
1884 
1885     foreach (_; 0..5) {
1886         node.push(textConstant);
1887     }
1888     root.drawAndAssert(
1889         node.cropsTo(0, 0, 200, 27),
1890         node.contentLabel.isDrawn().at(-784, 0, 984, 27),
1891         node.contentLabel.drawsHintedImage().at(-784, 0, 819.2, 819.2).ofColor("#ffffff")
1892             .sha256("01f6ca34c8a7cda32d38daac9938031a5b16020e8fed3aca0f4748582c787de8"),
1893         node.contentLabel.drawsHintedImage().at(35.2, 0, 819.2, 819.2).ofColor("#ffffff")
1894             .sha256("9fa7e5f27e1ad1d7c21efa837f94ab241b3f4b4401c61841720eb40c5ff859cc"),
1895     );
1896 
1897     foreach (_; 0..4) {
1898         node.push(textConstant);
1899     }
1900     root.drawAndAssert(
1901         node.cropsTo(0, 0, 200, 27),
1902         node.contentLabel.isDrawn().at(-1440, 0, 1640, 27),
1903         node.contentLabel.drawsHintedImage().at(-620.8, 0, 819.2, 819.2).ofColor("#ffffff")
1904             .sha256("e4910bc3700d464f172425e266ea918ec88f6a6c0d42b6cbeed396e9f22fb5df"),
1905         node.contentLabel.drawsHintedImage().at(198.4, 0, 819.2, 819.2).ofColor("#ffffff")
1906             .sha256("bb017d2518a0b78fe37ba7aa231553806dbb9f6a8aaff8a84fedb8b4b704025d"),
1907     );
1908 
1909 }