text_input.ui.tests.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #include <planet/debug/ui.hpp> #include <planet/log.hpp> #include <planet/ui/baseplate.hpp> #include <planet/widget/text_input.hpp> #include <felspar/test.hpp> #include <vector> namespace { constexpr planet::ui::reflowable::constrained_type screen{ {40, 0, 40}, {30, 0, 30}}; constexpr planet::affine::rectangle2d field_area{ {15, 20}, planet::affine::extents2d{4, 3}}; using field_type = planet::widget::text_input<std::string>; |
The usual keys, which is what every field here is driven by
23 | auto const &keys = planet::text::default_bindings; |
Lay the field out over a box away from the origin and draw it so it joins the frame's live list. Only a drawn widget can be routed an event. The shell has no graphic to size it, so the rectangle it covers is whatever it is handed — here the box a presentation would have laid its own text out over.
33 34 35 36 37 38 | template<typename F> void lay_out(F &f) { f.reflow({.screen = screen}, screen); f.move_to({.screen = screen}, field_area); f.draw(); } |
Bind the field straight to the baseplate and lay it out.
40 41 42 43 44 | template<typename F> void place(F &f, planet::ui::baseplate &bp, planet::ui::panel &panel) { f.add_to(bp, panel); lay_out(f); } |
Bind the field above another widget — a modal's screen — and lay it out.
46 47 48 49 | void place_in(field_type &f, planet::ui::widget &parent) { f.add_to(parent); lay_out(f); } |
Put a button on the display well away from the field, so that a click over it is unambiguously a click aimed at something other than the field being edited.
57 58 59 60 61 62 63 64 65 66 | void place_away( planet::debug::button<> &b, planet::ui::baseplate &bp, planet::ui::panel &panel) { b.add_to(bp, panel); b.reflow({.screen = screen}, screen); b.move_to( {.screen = screen}, {{1, 1}, planet::affine::extents2d{4, 3}}); b.draw(); } |
Draw a frame. A widget only joins the routing by drawing itself, so the field's dismissal screen is not there to take a click until a frame has been drawn with the edit already running.
74 75 76 77 78 79 | void frame(planet::ui::baseplate &bp, std::initializer_list<planet::ui::widget *> const ws) { bp.start_frame_reset(); for (auto *const w : ws) { w->draw(); } } |
Put the pointer over the field without pressing anything.
83 84 85 | void hover(planet::ui::baseplate &bp) { bp.events.mouse.push({.location = {16, 21}}); } |
Move the pointer off the field. Nothing is under it there, so the soft
focus is dropped and whatever has_focus still reports can only be the
hard focus.
91 92 93 | void pointer_away(planet::ui::baseplate &bp) { bp.events.mouse.push({.location = {1, 1}}); } |
Left click at a location.
95 96 97 98 99 100 101 102 103 104 105 | void click_at(planet::ui::baseplate &bp, planet::affine::point2d const l) { bp.events.mouse.push( {.button = planet::events::button::left, .action = planet::events::action::down, .location = l}); bp.events.mouse.push( {.button = planet::events::button::left, .action = planet::events::action::up, .location = l, .clicks = 1}); } |
Left click over the field.
107 | void click(planet::ui::baseplate &bp) { click_at(bp, {16, 21}); } |
Left click over the far corner of the field.
109 | void click_far_end(planet::ui::baseplate &bp) { click_at(bp, {18, 22}); } |
Left click over the button placed away from the field.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | void click_elsewhere(planet::ui::baseplate &bp) { click_at(bp, {2, 2}); } void type(planet::ui::baseplate &bp, std::string s) { bp.events.text.push({.utf8 = std::move(s)}); } void press(planet::ui::baseplate &bp, planet::events::scancode const sc) { bp.events.key.push( {.scancode = sc, .action = planet::events::action::down}); } auto const suite = felspar::testsuite("text_input", []() { planet::log::active = planet::log::level::error; }); auto const subscriptions = suite.test("subscriptions", [](auto check) { |
Delivery tests consumer_count() on the queue for the kind, so the
shell has to hold all three open at rest or the kinds it is not
currently blocked on fall through to whatever it covers.
132 133 134 135 136 137 138 139 140 141 142 143 144 | planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; field.add_to(bp, panel); check(field.events.mouse.consumer_count()) == 1u; check(field.events.key.consumer_count()) == 1u; check(field.events.text.consumer_count()) == 1u; }); auto const layout = suite.test("layout", [](auto check) { |
The shell draws nothing, so it has nothing to size itself from: the presentation lays its own graphic out and then puts the shell over the same rectangle. All the shell does is take both as given, which is what keeps the hit area and the visible field in agreement.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); check(field.constraints().extents()) == screen.extents(); check(field.position()) == field_area; }); auto const activation = suite.test("activation", [](auto check) { planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); check(field.is_editing()) == false; click(bp); check(field.is_editing()) == true; |
An edit corrects rather than retypes, so the buffer starts as the value with the caret at the end of it.
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | check(field.editor().value()) == "Nomad"; check(field.editor().cursor()) == 5u; pointer_away(bp); check(bp.has_focus(field)) == true; }); auto const typing = suite.test("typing", [](auto check) { planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); click(bp); type(bp, " I"); type(bp, "I"); check(field.editor().value()) == "Nomad II"; |
Nothing reaches the output until the edit is committed
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 | check(output) == "Nomad"; }); auto const committing = suite.test("return commits", [](auto check) { planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); click(bp); type(bp, " II"); press(bp, planet::events::scancode::return_key); check(output) == "Nomad II"; check(field.editor().value()) == "Nomad II"; check(field.is_editing()) == false; |
The hard focus was given up, so normal routing is restored
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | pointer_away(bp); check(bp.has_focus(field)) == false; }); auto const cancelling = suite.test("escape cancels", [](auto check) { planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); click(bp); type(bp, " II"); press(bp, planet::events::scancode::escape_key); check(output) == "Nomad"; check(field.editor().value()) == "Nomad"; check(field.is_editing()) == false; pointer_away(bp); check(bp.has_focus(field)) == false; }); auto const ignored_at_rest = suite.test("text ignored at rest", [](auto check) { planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); hover(bp); type(bp, "x"); |
The shell is subscribed, so the text really was delivered to it — it is the editor that discards it rather than the routing never reaching the field.
260 261 262 263 264 265 266 267 268 | check(field.events.text.values_pushed()) == 1u; check(field.editor().value()) == "Nomad"; check(output) == "Nomad"; }); auto const click_away = suite.test( "click away", [](auto check, auto &log) { |
The shell holds the hard focus while an edit is running, so it is handed every click in the window whatever it was aimed at, and passes each one down to its dismissal screen. The screen ends the edit, keeping what was typed.
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 | planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); planet::debug::button<> elsewhere{log}; place_away(elsewhere, bp, panel); click(bp); type(bp, " II"); frame(bp, {&field, &elsewhere}); click_elsewhere(bp); check(field.is_editing()) == false; check(output) == "Nomad II"; check(field.editor().value()) == "Nomad II"; check(bp.has_focus(field)) == false; }, [](auto check, auto &log) { |
The screen is a hover boundary, so the click that ends the edit stops there: the button under the pointer is dismissed past, not pressed. Pressing it takes a second click, once the edit is over.
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); planet::debug::button<> elsewhere{log}; place_away(elsewhere, bp, panel); click(bp); frame(bp, {&field, &elsewhere}); click_elsewhere(bp); check(elsewhere.clicks) == 0u; frame(bp, {&field, &elsewhere}); click_elsewhere(bp); check(elsewhere.clicks) == 1u; }, [](auto check, auto &log) { |
Ending the edit by clicking away is an end-edit like any other, so the platform's text input — and with it a mobile on-screen keyboard — is switched off.
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 | planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); planet::debug::button<> elsewhere{log}; place_away(elsewhere, bp, panel); std::string transitions; field.editing_changed = [&](bool const e) { transitions += (e ? '+' : '-'); }; click(bp); frame(bp, {&field, &elsewhere}); click_elsewhere(bp); check(transitions) == "+-"; }, [](auto check, auto &log) { |
At rest the screen is not drawn, so it is not in the routing at all and a click on another widget reaches it as usual.
348 349 350 351 352 353 354 355 356 357 358 359 360 361 | planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); planet::debug::button<> elsewhere{log}; place_away(elsewhere, bp, panel); click_elsewhere(bp); check(field.is_editing()) == false; check(elsewhere.clicks) == 1u; }, [](auto check) { |
A field inside a modal is attached above the modal's own screen, so its dismissal screen has to be placed above that one too. Were it at a fixed layer instead the click that ends an edit would walk past it to the modal's screen, which would close the whole modal and leave the edit running.
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 | planet::ui::baseplate bp; planet::ui::screen modal{"modal", 100.0f}; modal.add_to(bp); modal.draw(); std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place_in(field, modal); click(bp); check(field.is_editing()) == true; type(bp, " II"); frame(bp, {&modal, &field}); click_elsewhere(bp); check(field.is_editing()) == false; check(output) == "Nomad II"; |
The modal screen is a hover boundary, so anything that got as far as it would have been pushed to its queue whether it subscribes or not. Nothing did.
392 393 394 395 396 397 398 399 400 401 402 403 | check(modal.events.mouse.values_pushed()) == 0u; }); auto const edit_state = suite.test( "edit state callback", [](auto check) { planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); |
+ for a begin-edit, - for an end-edit
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 | std::string transitions; field.editing_changed = [&](bool const e) { transitions += (e ? '+' : '-'); }; click(bp); check(transitions) == "+"; press(bp, planet::events::scancode::return_key); check(transitions) == "+-"; }, [](auto check) { planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); std::string transitions; field.editing_changed = [&](bool const e) { transitions += (e ? '+' : '-'); }; click(bp); press(bp, planet::events::scancode::escape_key); |
Cancelling ends the edit just as committing does
431 432 433 434 435 436 437 | check(transitions) == "+-"; }); auto const click_within = suite.test( "click within a running edit", [](auto check, auto &log) { |
The hard focus does not capture the mouse, so while an edit runs mouse routing is still positional: a click over the field reaches the field, and only a click somewhere else reaches the dismissal screen that ends the edit. That layering is the whole of how the two are told apart — the field never asks where the click was.
446 447 448 449 450 451 452 453 454 455 456 457 458 459 | planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); planet::debug::button<> elsewhere{log}; place_away(elsewhere, bp, panel); click(bp); frame(bp, {&field, &elsewhere}); click_far_end(bp); check(field.is_editing()) == true; check(bp.has_focus(field)) == true; |
And a click away from it still ends the edit
462 463 464 465 466 467 468 | frame(bp, {&field, &elsewhere}); click_elsewhere(bp); check(field.is_editing()) == false; check(output) == "Nomad"; }, [](auto check) { |
Where the click landed goes to the presentation, which is the only layer with a font and so the only one that can say where in the text that is. The shell hands over the point and makes nothing of it itself.
The click that begins an edit is handed over like any other, so clicking into the middle of the text starts editing there rather than at the end of it.
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 | planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); std::vector<planet::affine::point2d> landed; field.clicked = [&](planet::affine::point2d const p) { landed.push_back(p); }; click(bp); frame(bp, {&field}); check(landed.size()) == 1u; check(landed.at(0).x()) == 16.0f; check(landed.at(0).y()) == 21.0f; click_far_end(bp); check(landed.size()) == 2u; check(landed.at(1).x()) == 18.0f; check(landed.at(1).y()) == 22.0f; }); auto const typing_follows_focus = suite.test( "typing follows the focus rather than the pointer", [](auto check, auto &log) { |
Key and text events have no position of their own, and the hard focus is what stands in for one. Giving up the mouse capture must not cost the field that: with the pointer parked over another widget entirely, what is typed still goes into the field being edited.
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 | planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{"field", output, keys, "Nomad"}; place(field, bp, panel); planet::debug::button<> elsewhere{log}; place_away(elsewhere, bp, panel); click(bp); frame(bp, {&field, &elsewhere}); pointer_away(bp); type(bp, " II"); press(bp, planet::events::scancode::return_key); check(output) == "Nomad II"; check(field.is_editing()) == false; |
Moving the pointer over it is not clicking it
531 532 533 534 535 536 | check(elsewhere.clicks) == 0u; }); auto const refused_commit = suite.test("a commit the editor refuses", [](auto check) { |
What the value is allowed to be is the editor's to say, and it says so by refusing the commit. The shell's part is to end the edit anyway — the focus goes back, the keyboard is put away — while writing nothing out, because what is being held is the value from before the edit and the call site already has that.
545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 | planet::ui::baseplate bp; planet::ui::panel panel; std::string output{"Nomad"}; field_type field{ "field", output, keys, [](std::string_view const v) { return not v.empty(); }, "Nomad"}; std::vector<bool> edit_states; field.editing_changed = [&edit_states](bool const editing) { edit_states.push_back(editing); }; place(field, bp, panel); click(bp); type(bp, " II"); while (not field.editor().value().empty()) { press(bp, planet::events::scancode::backspace_key); } press(bp, planet::events::scancode::return_key); check(field.editor().value()) == "Nomad"; check(output) == "Nomad"; check(field.is_editing()) == false; |
The hard focus went back whether the value was kept or not
569 570 | pointer_away(bp); check(bp.has_focus(field)) == false; |
and so did the platform's keyboard
572 573 574 575 576 577 578 579 | check(edit_states.size()) == 2u; check(edit_states.at(1)) == false; }); auto const owned_callback = suite.test( "a commit callback the field owns", [](auto check) { |
A commit that does something rather than lands somewhere is
a callback, and the call site that hands one over usually has
nowhere to keep it — the builder that made the field is a
temporary. So a callable output is owned by the field, the
way planet::widget::button owns one, while a string or a
queue to write into still belongs to the call site and is
held by reference.
589 590 591 592 593 | planet::ui::baseplate bp; planet::ui::panel panel; std::string committed; using callback_field = planet::widget::text_input< std::function<void(std::string const &)>>; |
Built and then moved, because that is what a builder handing a field back by value does to it, and the output has to come along.
599 600 601 602 603 604 605 606 607 608 609 610 611 612 | callback_field built{ "field", [&committed](std::string const &v) { committed = v; }, keys, "Nomad"}; callback_field field{std::move(built)}; place(field, bp, panel); click(bp); type(bp, " II"); press(bp, planet::events::scancode::return_key); check(committed) == "Nomad II"; }, [](auto check) { |
Nothing is called for an edit that was abandoned
614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 | planet::ui::baseplate bp; planet::ui::panel panel; std::string committed; planet::widget::text_input< std::function<void(std::string const &)>> field{"field", [&committed](std::string const &v) { committed = v; }, keys, "Nomad"}; place(field, bp, panel); click(bp); type(bp, " II"); press(bp, planet::events::scancode::escape_key); check(committed) == ""; }); } |