editor.text.tests.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | #include <planet/text/editor.hpp>
#include <felspar/test.hpp>
namespace {
planet::events::text typed(std::string s) { return {.utf8 = std::move(s)}; }
planet::events::key down(planet::events::scancode const sc) {
return {.scancode = sc, .action = planet::events::action::down};
}
planet::events::key
down(planet::events::scancode const sc,
planet::events::modifiers const m) {
return {.scancode = sc,
.action = planet::events::action::down,
.modifiers = m};
}
planet::events::key up(planet::events::scancode const sc) {
return {.scancode = sc, .action = planet::events::action::up};
}
|
The usual keys, which is what everything but the rebinding tests uses
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 | auto const &keys = planet::text::default_bindings;
auto const suite = felspar::testsuite("text.editor");
auto const at_rest = suite.test("at rest", [](auto check) {
planet::text::editor ed{keys, "Nomad"};
check(ed.is_editing()) == false;
check(ed.value()) == "Nomad";
});
auto const resting_value = suite.test(
"resting value",
[](auto check) {
|
A resting value handed in from outside takes the place of
what was there — the seam a field mirroring a number some
other control is changing keeps its text current through.
| planet::text::editor ed{keys, "Nomad"};
ed.value("Zeta");
check(ed.value()) == "Zeta";
check(ed.is_editing()) == false;
},
[](auto check) {
|
An edit in flight is left untouched: the buffer being typed
is never overwritten by what the field is told it now reads,
so the mirror only moves the value while the field is at rest.
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 | planet::text::editor ed{keys, "Nomad"};
ed.begin();
ed.handle(typed(" II"));
ed.value("Zeta");
check(ed.value()) == "Nomad II";
check(ed.is_editing()) == true;
});
auto const beginning = suite.test("begin", [](auto check) {
planet::text::editor ed{keys, "Nomad"};
ed.begin();
check(ed.is_editing()) == true;
|
An edit corrects rather than retypes: what is there is what is being
edited, with the caret at the end of it ready for more.
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100 | check(ed.value()) == "Nomad";
check(ed.cursor()) == 5u;
});
auto const typing = suite.test(
"typing",
[](auto check) {
planet::text::editor ed{keys, "Nomad"};
ed.begin();
check(ed.handle(typed(" Ze")))
== planet::text::outcome::changed;
check(ed.handle(typed("ta"))) == planet::text::outcome::changed;
check(ed.value()) == "Nomad Zeta";
check(ed.cursor()) == 10u;
},
[](auto check) {
|
Text arrives at a field whenever it is subscribed, which is
all of the time, so an editor at rest has to say that it
wanted none of it.
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125 | planet::text::editor ed{keys, "Nomad"};
check(ed.handle(typed("x"))) == planet::text::outcome::ignored;
check(ed.value()) == "Nomad";
});
auto const control_keys = suite.test(
"control keys",
[](auto check) {
planet::text::editor ed{keys, "Nomad"};
ed.begin();
check(ed.handle(down(planet::events::scancode::return_key)))
== planet::text::outcome::commit;
check(ed.handle(down(planet::events::scancode::escape_key)))
== planet::text::outcome::cancel;
check(ed.handle(down(planet::events::scancode::letter_a)))
== planet::text::outcome::ignored;
|
A single line has nowhere for up and down to go either
| check(ed.handle(down(planet::events::scancode::up_key)))
== planet::text::outcome::ignored;
check(ed.handle(down(planet::events::scancode::down_key)))
== planet::text::outcome::ignored;
|
Reporting an outcome is not acting on it: the side effects of
ending an edit need a baseplate, so they belong to the shell,
and the editor is still editing until it is told otherwise.
| check(ed.is_editing()) == true;
},
[](auto check) {
|
Only the key going down ends an edit. A key still held from
before the edit began releases into it, and that release must
not be what finishes it.
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166 | planet::text::editor ed{keys, "Nomad"};
ed.begin();
check(ed.handle(up(planet::events::scancode::return_key)))
== planet::text::outcome::ignored;
check(ed.handle(up(planet::events::scancode::escape_key)))
== planet::text::outcome::ignored;
});
auto const keys_at_rest = suite.test("keys at rest", [](auto check) {
planet::text::editor ed{keys, "Nomad"};
check(ed.handle(down(planet::events::scancode::return_key)))
== planet::text::outcome::ignored;
check(ed.handle(down(planet::events::scancode::escape_key)))
== planet::text::outcome::ignored;
check(ed.handle(down(planet::events::scancode::letter_a)))
== planet::text::outcome::ignored;
check(ed.handle(down(planet::events::scancode::backspace_key)))
== planet::text::outcome::ignored;
check(ed.handle(down(planet::events::scancode::delete_key)))
== planet::text::outcome::ignored;
|
Navigation is ignored at rest as well, caret and all: a field is
subscribed to keys whether or not it is being edited, and the arrow
keys belong to whatever else is listening for them until an edit
takes them.
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197 | check(ed.handle(down(planet::events::scancode::left_key)))
== planet::text::outcome::ignored;
check(ed.handle(down(planet::events::scancode::right_key)))
== planet::text::outcome::ignored;
check(ed.handle(down(planet::events::scancode::home_key)))
== planet::text::outcome::ignored;
check(ed.handle(down(planet::events::scancode::end_key)))
== planet::text::outcome::ignored;
check(ed.is_editing()) == false;
check(ed.value()) == "Nomad";
check(ed.cursor()) == 0u;
});
auto const backspace = suite.test(
"backspace",
[](auto check) {
planet::text::editor ed{keys, "Zeta"};
ed.begin();
check(ed.handle(down(planet::events::scancode::backspace_key)))
== planet::text::outcome::changed;
check(ed.value()) == "Zet";
|
The caret follows the text it took out
| check(ed.cursor()) == 3u;
},
[](auto check) {
|
Nothing to remove is nothing changed
203
204
205
206
207
208
209
210
211 | planet::text::editor ed{keys};
ed.begin();
check(ed.handle(down(planet::events::scancode::backspace_key)))
== planet::text::outcome::ignored;
check(ed.value()) == "";
},
[](auto check) {
|
Nor is there anything before a caret at the start of the text
213
214
215
216
217
218
219
220
221
222
223 | planet::text::editor ed{keys, "Zeta"};
ed.begin();
ed.handle(down(planet::events::scancode::home_key));
check(ed.handle(down(planet::events::scancode::backspace_key)))
== planet::text::outcome::ignored;
check(ed.value()) == "Zeta";
check(ed.cursor()) == 0u;
},
[](auto check) {
|
Correcting a typo and carrying on
225
226
227
228
229
230
231
232
233
234 | planet::text::editor ed{keys, "Zetz"};
ed.begin();
ed.handle(down(planet::events::scancode::backspace_key));
ed.handle(typed("a"));
ed.commit();
check(ed.value()) == "Zeta";
},
[](auto check) {
|
A whole character goes, never the tail byte of one: what is
left has to still be text.
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257 | planet::text::editor ed{keys, "Zeté"};
ed.begin();
check(ed.handle(down(planet::events::scancode::backspace_key)))
== planet::text::outcome::changed;
check(ed.value()) == "Zet";
check(ed.cursor()) == 3u;
},
[](auto check) {
planet::text::editor ed{keys, "Ze🚀"};
ed.begin();
ed.handle(down(planet::events::scancode::backspace_key));
check(ed.value()) == "Ze";
check(ed.cursor()) == 2u;
},
[](auto check) {
|
Backspace acts at the caret, not at the end of the text
259
260
261
262
263
264
265
266
267
268 | planet::text::editor ed{keys, "Zeta"};
ed.begin();
ed.handle(down(planet::events::scancode::left_key));
ed.handle(down(planet::events::scancode::backspace_key));
check(ed.value()) == "Zea";
check(ed.cursor()) == 2u;
},
[](auto check) {
|
A key coming back up is not a key press, backspace included
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291 | planet::text::editor ed{keys, "Zeta"};
ed.begin();
check(ed.handle(up(planet::events::scancode::backspace_key)))
== planet::text::outcome::ignored;
check(ed.value()) == "Zeta";
});
auto const navigation = suite.test(
"navigation",
[](auto check) {
planet::text::editor ed{keys, "Nomad"};
ed.begin();
check(ed.handle(down(planet::events::scancode::left_key)))
== planet::text::outcome::changed;
check(ed.cursor()) == 4u;
check(ed.handle(down(planet::events::scancode::right_key)))
== planet::text::outcome::changed;
check(ed.cursor()) == 5u;
|
Neither arrow runs off the end of the buffer
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313 | check(ed.handle(down(planet::events::scancode::right_key)))
== planet::text::outcome::ignored;
check(ed.cursor()) == 5u;
check(ed.value()) == "Nomad";
},
[](auto check) {
planet::text::editor ed{keys, "Nomad"};
ed.begin();
check(ed.handle(down(planet::events::scancode::home_key)))
== planet::text::outcome::changed;
check(ed.cursor()) == 0u;
check(ed.handle(down(planet::events::scancode::left_key)))
== planet::text::outcome::ignored;
check(ed.cursor()) == 0u;
check(ed.handle(down(planet::events::scancode::end_key)))
== planet::text::outcome::changed;
check(ed.cursor()) == 5u;
|
Already there is nothing changed
| check(ed.handle(down(planet::events::scancode::end_key)))
== planet::text::outcome::ignored;
},
[](auto check) {
|
The caret steps over whole characters, so it can never land
inside one: → is three bytes, between two of one.
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343 | planet::text::editor ed{keys, "a→b"};
ed.begin();
check(ed.cursor()) == 5u;
ed.handle(down(planet::events::scancode::left_key));
check(ed.cursor()) == 4u;
ed.handle(down(planet::events::scancode::left_key));
check(ed.cursor()) == 1u;
ed.handle(down(planet::events::scancode::left_key));
check(ed.cursor()) == 0u;
ed.handle(down(planet::events::scancode::right_key));
check(ed.cursor()) == 1u;
ed.handle(down(planet::events::scancode::right_key));
check(ed.cursor()) == 4u;
});
auto const insertion = suite.test(
"insertion",
[](auto check) {
|
Typing goes in at the caret and the caret follows it along
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360 | planet::text::editor ed{keys, "Nomad"};
ed.begin();
ed.handle(down(planet::events::scancode::home_key));
check(ed.handle(typed("The ")))
== planet::text::outcome::changed;
check(ed.value()) == "The Nomad";
check(ed.cursor()) == 4u;
ed.handle(typed("Old "));
check(ed.value()) == "The Old Nomad";
check(ed.cursor()) == 8u;
},
[](auto check) {
|
The caret advances by the bytes inserted, not by characters
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382 | planet::text::editor ed{keys, "Zet"};
ed.begin();
ed.handle(typed("é"));
check(ed.value()) == "Zeté";
check(ed.cursor()) == 5u;
});
auto const forward_delete = suite.test(
"delete",
[](auto check) {
planet::text::editor ed{keys, "Nomad"};
ed.begin();
ed.handle(down(planet::events::scancode::home_key));
check(ed.handle(down(planet::events::scancode::delete_key)))
== planet::text::outcome::changed;
check(ed.value()) == "omad";
|
The text moves back to the caret, so the caret stays put
| check(ed.cursor()) == 0u;
},
[](auto check) {
|
Nothing follows a caret at the end of the text
388
389
390
391
392
393
394
395
396 | planet::text::editor ed{keys, "Nomad"};
ed.begin();
check(ed.handle(down(planet::events::scancode::delete_key)))
== planet::text::outcome::ignored;
check(ed.value()) == "Nomad";
},
[](auto check) {
|
A whole character goes here too
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418 | planet::text::editor ed{keys, "→b"};
ed.begin();
ed.handle(down(planet::events::scancode::home_key));
ed.handle(down(planet::events::scancode::delete_key));
check(ed.value()) == "b";
check(ed.cursor()) == 0u;
});
auto const cursor_from_outside = suite.test(
"set_cursor",
[](auto check) {
planet::text::editor ed{keys, "Nomad"};
ed.begin();
ed.set_cursor(2);
check(ed.cursor()) == 2u;
ed.set_cursor(0);
check(ed.cursor()) == 0u;
|
Past the end is the end, not the boundary before it — a
click past the last character puts the caret after it.
| ed.set_cursor(99);
check(ed.cursor()) == 5u;
},
[](auto check) {
|
A presentation measures its way to an offset and can land
inside a character. What it gets is the boundary at or before
where it asked, never somewhere illegal.
432
433
434
435
436
437
438
439
440
441
442 | planet::text::editor ed{keys, "a→b"};
ed.begin();
ed.set_cursor(2);
check(ed.cursor()) == 1u;
ed.set_cursor(3);
check(ed.cursor()) == 1u;
ed.set_cursor(4);
check(ed.cursor()) == 4u;
},
[](auto check) {
|
So what is typed after one is still valid text
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474 | planet::text::editor ed{keys, "a→b"};
ed.begin();
ed.set_cursor(3);
ed.handle(typed("x"));
check(ed.value()) == "ax→b";
});
auto const ending = suite.test(
"ending an edit",
[](auto check) {
planet::text::editor ed{keys, "Nomad"};
ed.begin();
ed.handle(typed(" II"));
ed.commit();
check(ed.value()) == "Nomad II";
check(ed.is_editing()) == false;
},
[](auto check) {
planet::text::editor ed{keys, "Nomad"};
ed.begin();
ed.handle(typed(" II"));
ed.cancel();
check(ed.value()) == "Nomad";
check(ed.is_editing()) == false;
|
The caret comes back inside the value that was put back: it
was past the end of it a moment ago, and every offset the
editor holds has to be one into the text it holds now.
480
481
482
483
484
485
486 | check(ed.cursor()) == 5u;
});
auto const filtering = suite.test(
"what the value is allowed to be",
[](auto check) {
|
A cap on how long the value may grow. The filter is handed
the value as it would be, so a rule about the whole of it
is written as a rule about the whole of it.
492
493
494
495
496
497
498
499 | planet::text::editor ed{
keys,
[](std::string_view const v) { return v.size() <= 7; },
"Nomad"};
ed.begin();
check(ed.handle(typed(" I"))) == planet::text::outcome::changed;
check(ed.value()) == "Nomad I";
|
The insertion that would take it past the cap does not happen
at all: the buffer and the caret are exactly as they were,
and the editor reports that nothing changed so no redraw is
charged for a keystroke that did nothing.
| check(ed.handle(typed("I"))) == planet::text::outcome::ignored;
check(ed.value()) == "Nomad I";
check(ed.cursor()) == 7u;
},
[](auto check) {
|
A refused insertion at the caret leaves what is on either
side of it alone, not just what is on the end.
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529 | planet::text::editor ed{
keys,
[](std::string_view const v) {
return v.find('x') == std::string_view::npos;
},
"Nomad"};
ed.begin();
ed.handle(down(planet::events::scancode::home_key));
check(ed.handle(typed("x"))) == planet::text::outcome::ignored;
check(ed.value()) == "Nomad";
check(ed.cursor()) == 0u;
},
[](auto check) {
|
The whole of what was typed goes back, however many bytes it
took: what a refused insertion leaves behind has to still be
the text that was there before it.
535
536
537
538
539
540
541
542
543
544
545
546 | planet::text::editor ed{
keys,
[](std::string_view const v) { return v.size() < 4; },
"a"};
ed.begin();
check(ed.handle(typed("→"))) == planet::text::outcome::ignored;
check(ed.value()) == "a";
check(ed.cursor()) == 1u;
},
[](auto check) {
|
A removal is consulted but never obeyed, so a value can
always be cut down to nothing — a cap that stopped it being
cut back would be no use, and a rule that it may not be blank
has to leave a way to clear it and start again.
553
554
555
556
557
558
559
560
561 | planet::text::editor ed{
keys,
[](std::string_view const v) { return not v.empty(); },
"Nomad"};
ed.begin();
while (not ed.value().empty()) {
ed.handle(down(planet::events::scancode::backspace_key));
}
check(ed.value()) == "";
|
What it will not do is let the edit end on one
564
565
566
567
568
569
570 | check(ed.commit()) == false;
check(ed.value()) == "Nomad";
check(ed.is_editing()) == false;
check(ed.cursor()) == 5u;
},
[](auto check) {
|
A value the filter is happy with commits as it always did
572
573
574
575
576
577
578
579
580
581
582
583
584 | planet::text::editor ed{
keys,
[](std::string_view const v) { return not v.empty(); },
"Nomad"};
ed.begin();
ed.handle(typed(" II"));
check(ed.commit()) == true;
check(ed.value()) == "Nomad II";
check(ed.is_editing()) == false;
},
[](auto check) {
|
A removal is a change like any other, so a call site that
reacts to the value as it is edited is told of it: the filter
is consulted after a deletion — backwards and forwards —
with the value the buffer has become, though the deletion
itself is never blocked.
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613 | std::string consulted;
planet::text::editor ed{
keys,
[&consulted](std::string_view const v) {
consulted += v;
consulted += ';';
return true;
},
"Zeta"};
ed.begin();
ed.handle(down(planet::events::scancode::backspace_key));
ed.handle(down(planet::events::scancode::home_key));
ed.handle(down(planet::events::scancode::delete_key));
check(consulted) == "Zet;et;";
});
auto const rebinding = suite.test(
"rebinding",
[](auto check) {
|
A game that has loaded the player's own choices hands them
over, and from then on those are the keys the edit answers
to. A default stops meaning anything the moment something
else is bound in its place.
620
621
622
623
624
625
626
627
628
629
630 | planet::text::configuration const bound{
.commit = {planet::events::scancode::tab_key, {}}};
planet::text::editor ed{bound, "Nomad"};
ed.begin();
check(ed.handle(down(planet::events::scancode::tab_key)))
== planet::text::outcome::commit;
check(ed.handle(down(planet::events::scancode::return_key)))
== planet::text::outcome::ignored;
},
[](auto check) {
|
A binding that asks for a modifier needs it held
632
633
634
635
636
637
638
639
640
641
642 | planet::text::configuration const bound{
.caret_to_start = {
planet::events::scancode::letter_a,
{.ctrl = true}}};
planet::text::editor ed{bound, "Nomad"};
ed.begin();
check(ed.handle(down(
planet::events::scancode::letter_a, {.ctrl = true})))
== planet::text::outcome::changed;
check(ed.cursor()) == 0u;
|
Unmodified the same key is a letter to type rather than a
control, and a key event carrying a letter is not what puts
one in the buffer — the character arrives as text.
| check(ed.handle(down(planet::events::scancode::letter_a)))
== planet::text::outcome::ignored;
check(ed.cursor()) == 0u;
|
Home has nothing bound to it any more
| ed.handle(down(planet::events::scancode::end_key));
check(ed.handle(down(planet::events::scancode::home_key)))
== planet::text::outcome::ignored;
check(ed.cursor()) == 5u;
},
[](auto check) {
|
One key bound to two things is answered rather than
refused: the bindings are tried in the order
text::configuration declares them, so the first of the two
is what happens.
665
666
667
668
669
670
671
672
673 | planet::text::configuration bound;
bound.cancel = bound.commit;
planet::text::editor ed{bound, "Nomad"};
ed.begin();
check(ed.handle(down(planet::events::scancode::return_key)))
== planet::text::outcome::commit;
},
[](auto check) {
|
The configuration is held by reference, so a key the player
rebinds is at once the key every editor reading that
configuration answers to — an edit already running
included. Nothing has to be reached into and told.
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694 | planet::text::configuration bound;
planet::text::editor ed{bound, "Nomad"};
ed.begin();
bound.commit = {planet::events::scancode::tab_key, {}};
check(ed.handle(down(planet::events::scancode::tab_key)))
== planet::text::outcome::commit;
check(ed.handle(down(planet::events::scancode::return_key)))
== planet::text::outcome::ignored;
});
auto const exact_modifiers =
suite.test("modifiers are exact", [](auto check) {
|
The defaults are the bare keys, so a modifier held with one
leaves the edit alone: shift-Left belongs to whatever the
game means by it rather than moving the caret.
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716 | planet::text::editor ed{keys, "Nomad"};
ed.begin();
check(ed.handle(down(
planet::events::scancode::left_key, {.shift = true})))
== planet::text::outcome::ignored;
check(ed.cursor()) == 5u;
check(ed.handle(
down(planet::events::scancode::backspace_key,
{.ctrl = true})))
== planet::text::outcome::ignored;
check(ed.value()) == "Nomad";
});
}
|