FTXUI 6.1.9
C++ functional terminal UI.
Loading...
Searching...
No Matches
src/ftxui/component/input.cpp
Go to the documentation of this file.
1// Copyright 2022 Arthur Sonzogni. All rights reserved.
2// Use of this source code is governed by the MIT license that can be found in
3// the LICENSE file.
4#include <algorithm> // for max, min
5#include <cstddef> // for size_t
6#include <cstdint> // for uint32_t
7#include <functional> // for function
8#include <sstream> // for basic_istream, stringstream
9#include <string> // for string, basic_string, operator==, getline
10#include <utility> // for move
11#include <vector> // for vector
12
13#include "ftxui/component/app.hpp" // for Component
14#include "ftxui/component/component.hpp" // for Make, Input
15#include "ftxui/component/component_base.hpp" // for ComponentBase
16#include "ftxui/component/component_options.hpp" // for InputOption
17#include "ftxui/component/event.hpp" // for Event, Event::ArrowDown, Event::ArrowLeft, Event::ArrowLeftCtrl, Event::ArrowRight, Event::ArrowRightCtrl, Event::ArrowUp, Event::Backspace, Event::Delete, Event::End, Event::Home, Event::Return
18#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Left, Mouse::Pressed
19#include "ftxui/dom/elements.hpp" // for operator|, reflect, text, Element, xflex, hbox, Elements, frame, operator|=, vbox, focus, focusCursorBarBlinking, select
20#include "ftxui/screen/box.hpp" // for Box
21#include "ftxui/screen/string.hpp" // for string_width
22#include "ftxui/screen/string_internal.hpp" // for GlyphNext, GlyphPrevious, WordBreakProperty, EatCodePoint, CodepointToWordBreakProperty, IsFullWidth, WordBreakProperty::ALetter, WordBreakProperty::CR, WordBreakProperty::Double_Quote, WordBreakProperty::Extend, WordBreakProperty::ExtendNumLet, WordBreakProperty::Format, WordBreakProperty::Hebrew_Letter, WordBreakProperty::Katakana, WordBreakProperty::LF, WordBreakProperty::MidLetter, WordBreakProperty::MidNum, WordBreakProperty::MidNumLet, WordBreakProperty::Newline, WordBreakProperty::Numeric, WordBreakProperty::Regional_Indicator, WordBreakProperty::Single_Quote, WordBreakProperty::WSegSpace, WordBreakProperty::ZWJ
23#include "ftxui/screen/util.hpp" // for clamp
24#include "ftxui/util/ref.hpp" // for StringRef, Ref
25
26namespace ftxui {
27
28namespace {
29
30std::vector<std::string> Split(const std::string& input) {
31 std::vector<std::string> output;
32 std::stringstream ss(input);
33 std::string line;
34 while (std::getline(ss, line)) {
35 output.push_back(line);
36 }
37 if (input.back() == '\n') {
38 output.emplace_back("");
39 }
40 return output;
41}
42
43size_t GlyphWidth(const std::string& input, size_t iter) {
44 uint32_t ucs = 0;
45 if (!EatCodePoint(input, iter, &iter, &ucs)) {
46 return 0;
47 }
48 if (IsFullWidth(ucs)) {
49 return 2;
50 }
51 return 1;
52}
53
54bool IsWordCodePoint(uint32_t codepoint) {
55 switch (CodepointToWordBreakProperty(codepoint)) {
60 return true;
61
71 // Unexpected/Unsure
77 return false;
78 }
79 return false; // NOT_REACHED();
80}
81
82bool IsWordCharacter(const std::string& input, size_t iter) {
83 uint32_t ucs = 0;
84 if (!EatCodePoint(input, iter, &iter, &ucs)) {
85 return false;
86 }
87
88 return IsWordCodePoint(ucs);
89}
90
91// An input box. The user can type text into it.
92class InputBase : public ComponentBase, public InputOption {
93 public:
94 // NOLINTNEXTLINE
95 InputBase(InputOption option) : InputOption(std::move(option)) {}
96
97 private:
98 // Component implementation:
99 Element OnRender() override {
100 const bool is_focused = Focused();
101 const auto focused = (!is_focused && !hovered_) ? focus
102 : insert() ? focusCursorBarBlinking
103 : focusCursorBlockBlinking;
104
105 auto transform_func =
106 transform ? transform : InputOption::Default().transform;
107
108 // placeholder.
109 if (content->empty()) {
110 auto element = text(placeholder()) | focused | xflex | frame;
111
112 return transform_func({
113 std::move(element), hovered_, is_focused,
114 true // placeholder
115 }) |
116 reflect(box_);
117 }
118
119 Elements elements;
120 const std::vector<std::string> lines = Split(*content);
121
122 cursor_position() = util::clamp(cursor_position(), 0, (int)content->size());
123
124 // Find the line and index of the cursor.
125 int cursor_line = 0;
126 int cursor_char_index = cursor_position();
127 for (const auto& line : lines) {
128 if (cursor_char_index <= (int)line.size()) {
129 break;
130 }
131
132 cursor_char_index -= static_cast<int>(line.size() + 1);
133 cursor_line++;
134 }
135
136 if (lines.empty()) {
137 elements.push_back(text("") | focused);
138 }
139
140 elements.reserve(lines.size());
141 for (size_t i = 0; i < lines.size(); ++i) {
142 const std::string& line = lines[i];
143
144 // This is not the cursor line.
145 if (int(i) != cursor_line) {
146 elements.push_back(Text(line));
147 continue;
148 }
149
150 // The cursor is at the end of the line.
151 if (cursor_char_index >= (int)line.size()) {
152 elements.push_back(hbox({
153 Text(line),
154 text(" ") | focused | reflect(cursor_box_),
155 }) |
156 xflex);
157 continue;
158 }
159
160 // The cursor is on this line.
161 const int glyph_start = cursor_char_index;
162 const int glyph_end = static_cast<int>(GlyphNext(line, glyph_start));
163 const std::string part_before_cursor = line.substr(0, glyph_start);
164 const std::string part_at_cursor =
165 line.substr(glyph_start, glyph_end - glyph_start);
166 const std::string part_after_cursor = line.substr(glyph_end);
167 auto element = hbox({
168 Text(part_before_cursor),
169 Text(part_at_cursor) | focused | reflect(cursor_box_),
170 Text(part_after_cursor),
171 }) |
172 xflex;
173 elements.push_back(element);
174 }
175
176 auto element = vbox(std::move(elements)) | frame;
177 return transform_func({
178 std::move(element), hovered_, is_focused,
179 false // placeholder
180 }) |
181 xflex | reflect(box_);
182 }
183
184 Element Text(const std::string& input) {
185 if (!password()) {
186 return text(input);
187 }
188
189 const size_t glyph_count = GlyphCount(input);
190 std::string out;
191 out.reserve(glyph_count * 3);
192 for (size_t i = 0; i < glyph_count; ++i) {
193 out += "•";
194 }
195 return text(out);
196 }
197
198 bool HandleBackspace() {
199 if (cursor_position() == 0) {
200 return false;
201 }
202 const size_t start = GlyphPrevious(content(), cursor_position());
203 const size_t end = cursor_position();
204 content->erase(start, end - start);
205 cursor_position() = static_cast<int>(start);
206 on_change();
207 return true;
208 }
209
210 bool DeleteImpl() {
211 if (cursor_position() == (int)content->size()) {
212 return false;
213 }
214 const size_t start = cursor_position();
215 const size_t end = GlyphNext(content(), cursor_position());
216 content->erase(start, end - start);
217 return true;
218 }
219
220 bool HandleDelete() {
221 if (DeleteImpl()) {
222 on_change();
223 return true;
224 }
225 return false;
226 }
227
228 bool HandleArrowLeft() {
229 if (cursor_position() == 0) {
230 return false;
231 }
232
233 cursor_position() =
234 static_cast<int>(GlyphPrevious(content(), cursor_position()));
235 return true;
236 }
237
238 bool HandleArrowRight() {
239 if (cursor_position() == (int)content->size()) {
240 return false;
241 }
242
243 cursor_position() =
244 static_cast<int>(GlyphNext(content(), cursor_position()));
245 return true;
246 }
247
248 size_t CursorColumn() {
249 size_t iter = cursor_position();
250 int width = 0;
251 while (true) {
252 if (iter == 0) {
253 break;
254 }
255 iter = GlyphPrevious(content(), iter);
256 if (content()[iter] == '\n') {
257 break;
258 }
259 if (password()) {
260 width += 1;
261 } else {
262 width += static_cast<int>(GlyphWidth(content(), iter));
263 }
264 }
265 return width;
266 }
267
268 // Move the cursor `columns` on the right, if possible.
269 void MoveCursorColumn(int columns) {
270 while (columns > 0) {
271 if (cursor_position() == (int)content().size() ||
272 content()[cursor_position()] == '\n') {
273 return;
274 }
275
276 if (password()) {
277 columns -= 1;
278 } else {
279 columns -= static_cast<int>(GlyphWidth(content(), cursor_position()));
280 }
281 cursor_position() =
282 static_cast<int>(GlyphNext(content(), cursor_position()));
283 }
284 }
285
286 bool HandleArrowUp() {
287 if (cursor_position() == 0) {
288 return false;
289 }
290
291 const size_t columns = CursorColumn();
292
293 // Move cursor at the beginning of 2 lines above.
294 while (true) {
295 if (cursor_position() == 0) {
296 return true;
297 }
298 const size_t previous = GlyphPrevious(content(), cursor_position());
299 if (content()[previous] == '\n') {
300 break;
301 }
302 cursor_position() = static_cast<int>(previous);
303 }
304 cursor_position() =
305 static_cast<int>(GlyphPrevious(content(), cursor_position()));
306 while (true) {
307 if (cursor_position() == 0) {
308 break;
309 }
310 const size_t previous = GlyphPrevious(content(), cursor_position());
311 if (content()[previous] == '\n') {
312 break;
313 }
314 cursor_position() = static_cast<int>(previous);
315 }
316
317 MoveCursorColumn(static_cast<int>(columns));
318 return true;
319 }
320
321 bool HandleArrowDown() {
322 if (cursor_position() == (int)content->size()) {
323 return false;
324 }
325
326 const size_t columns = CursorColumn();
327
328 // Move cursor at the beginning of the next line
329 while (true) {
330 if (content()[cursor_position()] == '\n') {
331 break;
332 }
333 cursor_position() =
334 static_cast<int>(GlyphNext(content(), cursor_position()));
335 if (cursor_position() == (int)content().size()) {
336 return true;
337 }
338 }
339 cursor_position() =
340 static_cast<int>(GlyphNext(content(), cursor_position()));
341
342 MoveCursorColumn(static_cast<int>(columns));
343 return true;
344 }
345
346 bool HandleHome() {
347 cursor_position() = 0;
348 return true;
349 }
350
351 bool HandleEnd() {
352 cursor_position() = static_cast<int>(content->size());
353 return true;
354 }
355
356 bool HandleReturn() {
357 if (multiline()) {
358 HandleCharacter("\n");
359 }
360 on_enter();
361 return true;
362 }
363
364 bool HandleCharacter(const std::string& character) {
365 if (!insert() && cursor_position() < (int)content->size() &&
366 content()[cursor_position()] != '\n') {
367 DeleteImpl();
368 }
369 content->insert(cursor_position(), character);
370 cursor_position() += static_cast<int>(character.size());
371 on_change();
372 return true;
373 }
374
375 bool OnEvent(Event event) override {
376 cursor_position() = util::clamp(cursor_position(), 0, (int)content->size());
377
378 if (event == Event::Return) {
379 return HandleReturn();
380 }
381 if (event.is_character()) {
382 return HandleCharacter(event.character());
383 }
384 if (event.is_mouse()) {
385 return HandleMouse(event);
386 }
387 if (event == Event::Backspace) {
388 return HandleBackspace();
389 }
390 if (event == Event::Delete) {
391 return HandleDelete();
392 }
393 if (event == Event::ArrowLeft) {
394 return HandleArrowLeft();
395 }
396 if (event == Event::ArrowRight) {
397 return HandleArrowRight();
398 }
399 if (event == Event::ArrowUp) {
400 return HandleArrowUp();
401 }
402 if (event == Event::ArrowDown) {
403 return HandleArrowDown();
404 }
405 if (event == Event::Home) {
406 return HandleHome();
407 }
408 if (event == Event::End) {
409 return HandleEnd();
410 }
411 if (event == Event::ArrowLeftCtrl) {
412 return HandleLeftCtrl();
413 }
414 if (event == Event::ArrowRightCtrl) {
415 return HandleRightCtrl();
416 }
417 if (event == Event::Insert) {
418 return HandleInsert();
419 }
420 return false;
421 }
422
423 bool HandleLeftCtrl() {
424 if (cursor_position() == 0) {
425 return false;
426 }
427
428 // Move left, as long as left it not a word.
429 while (cursor_position()) {
430 const size_t previous = GlyphPrevious(content(), cursor_position());
431 if (IsWordCharacter(content(), previous)) {
432 break;
433 }
434 cursor_position() = static_cast<int>(previous);
435 }
436 // Move left, as long as left is a word character:
437 while (cursor_position()) {
438 const size_t previous = GlyphPrevious(content(), cursor_position());
439 if (!IsWordCharacter(content(), previous)) {
440 break;
441 }
442 cursor_position() = static_cast<int>(previous);
443 }
444 return true;
445 }
446
447 bool HandleRightCtrl() {
448 if (cursor_position() == (int)content().size()) {
449 return false;
450 }
451
452 // Move right, until entering a word.
453 while (cursor_position() < (int)content().size()) {
454 cursor_position() =
455 static_cast<int>(GlyphNext(content(), cursor_position()));
456 if (IsWordCharacter(content(), cursor_position())) {
457 break;
458 }
459 }
460 // Move right, as long as right is a word character:
461 while (cursor_position() < (int)content().size()) {
462 const size_t next = GlyphNext(content(), cursor_position());
463 if (!IsWordCharacter(content(), cursor_position())) {
464 break;
465 }
466 cursor_position() = static_cast<int>(next);
467 }
468
469 return true;
470 }
471
472 bool HandleMouse(Event event) {
473 hovered_ = box_.Contain(event.mouse().x, //
474 event.mouse().y) &&
475 CaptureMouse(event);
476 if (!hovered_) {
477 return false;
478 }
479
480 if (event.mouse().button != Mouse::Left) {
481 return false;
482 }
483 if (event.mouse().motion != Mouse::Pressed) {
484 return false;
485 }
486
487 TakeFocus();
488
489 if (content->empty()) {
490 cursor_position() = 0;
491 return true;
492 }
493
494 // Find the line and index of the cursor.
495 std::vector<std::string> lines = Split(*content);
496 int cursor_line = 0;
497 int cursor_char_index = cursor_position();
498 for (const auto& line : lines) {
499 if (cursor_char_index <= (int)line.size()) {
500 break;
501 }
502
503 cursor_char_index -= static_cast<int>(line.size() + 1);
504 cursor_line++;
505 }
506 const int cursor_column =
507 password()
508 ? GlyphCount(lines[cursor_line].substr(0, cursor_char_index))
509 : string_width(lines[cursor_line].substr(0, cursor_char_index));
510
511 int new_cursor_column = cursor_column + event.mouse().x - cursor_box_.x_min;
512 int new_cursor_line = cursor_line + event.mouse().y - cursor_box_.y_min;
513
514 // Fix the new cursor position:
515 new_cursor_line = std::max(std::min(new_cursor_line, (int)lines.size()), 0);
516
517 const std::string empty_string;
518 const std::string& line = new_cursor_line < (int)lines.size()
519 ? lines[new_cursor_line]
520 : empty_string;
521 new_cursor_column =
522 util::clamp(new_cursor_column, 0,
523 password() ? GlyphCount(line) : string_width(line));
524
525 if (new_cursor_column == cursor_column && //
526 new_cursor_line == cursor_line) {
527 return false;
528 }
529
530 // Convert back the new_cursor_{line,column} toward cursor_position:
531 cursor_position() = 0;
532 for (int i = 0; i < new_cursor_line; ++i) {
533 cursor_position() += static_cast<int>(lines[i].size() + 1);
534 }
535 while (new_cursor_column > 0) {
536 if (password()) {
537 new_cursor_column -= 1;
538 } else {
539 new_cursor_column -=
540 static_cast<int>(GlyphWidth(content(), cursor_position()));
541 }
542 cursor_position() =
543 static_cast<int>(GlyphNext(content(), cursor_position()));
544 }
545
546 on_change();
547 return true;
548 }
549
550 bool HandleInsert() {
551 insert() = !insert();
552 return true;
553 }
554
555 bool Focusable() const final { return true; }
556
557 bool hovered_ = false;
558
559 Box box_;
560 Box cursor_box_;
561};
562
563} // namespace
564
565/// @brief An input box for editing text.
566/// @param option Additional optional parameters.
567/// @ingroup component
568/// @see InputBase
569///
570/// ### Example
571///
572/// ```cpp
573/// auto screen = App::FitComponent();
574/// std::string content= "";
575/// std::string placeholder = "placeholder";
576/// Component input = Input({
577/// .content = &content,
578/// .placeholder = &placeholder,
579/// })
580/// screen.Loop(input);
581/// ```
582///
583/// ### Output
584///
585/// ```bash
586/// placeholder
587/// ```
589 return Make<InputBase>(std::move(option));
590}
591
592/// @brief An input box for editing text.
593/// @param content The editable content.
594/// @param option Additional optional parameters.
595/// @ingroup component
596/// @see InputBase
597///
598/// ### Example
599///
600/// ```cpp
601/// auto screen = App::FitComponent();
602/// std::string content= "";
603/// std::string placeholder = "placeholder";
604/// Component input = Input(content, {
605/// .placeholder = &placeholder,
606/// .password = true,
607/// })
608/// screen.Loop(input);
609/// ```
610///
611/// ### Output
612///
613/// ```bash
614/// placeholder
615/// ```
617 option.content = std::move(content);
618 return Make<InputBase>(std::move(option));
619}
620
621/// @brief An input box for editing text.
622/// @param content The editable content.
623/// @param placeholder The placeholder text.
624/// @param option Additional optional parameters.
625/// @ingroup component
626/// @see InputBase
627///
628/// ### Example
629///
630/// ```cpp
631/// auto screen = App::FitComponent();
632/// std::string content= "";
633/// std::string placeholder = "placeholder";
634/// Component input = Input(content, placeholder);
635/// screen.Loop(input);
636/// ```
637///
638/// ### Output
639///
640/// ```bash
641/// placeholder
642/// ```
643Component Input(StringRef content, StringRef placeholder, InputOption option) {
644 option.content = std::move(content);
645 option.placeholder = std::move(placeholder);
646 return Make<InputBase>(std::move(option));
647}
648
649} // namespace ftxui
An adapter. Own or reference a constant string. For convenience, this class convert multiple mutable ...
Definition ref.hpp:95
static const Event ArrowLeftCtrl
Definition event.hpp:47
static InputOption Default()
Create the default input style:
static const Event Backspace
Definition event.hpp:53
static const Event ArrowUp
Definition event.hpp:44
std::function< Element(InputState)> transform
static const Event ArrowDown
Definition event.hpp:45
static const Event End
Definition event.hpp:63
StringRef placeholder
The content of the input when it's empty.
static const Event Home
Definition event.hpp:62
StringRef content
The content of the input.
static const Event Return
Definition event.hpp:55
static const Event ArrowLeft
Definition event.hpp:42
static const Event Delete
Definition event.hpp:54
static const Event Insert
Definition event.hpp:61
static const Event ArrowRightCtrl
Definition event.hpp:48
static const Event ArrowRight
Definition event.hpp:43
Component Input(InputOption options={})
An input box for editing text.
Option for the Input component.
Element xflex(Element)
Expand/Minimize if possible/needed on the X axis.
Definition flex.cpp:129
Decorator size(WidthOrHeight, Constraint, int value)
Apply a constraint on the size of an element.
Element text(std::wstring_view text)
Display a piece of unicode text.
Definition text.cpp:164
Element vbox(Elements)
A container displaying elements vertically one by one.
Definition vbox.cpp:96
constexpr const T & clamp(const T &v, const T &lo, const T &hi)
Definition util.hpp:11
The FTXUI ftxui:: namespace.
Definition animation.hpp:10
WordBreakProperty CodepointToWordBreakProperty(uint32_t codepoint)
Definition string.cpp:1308
size_t GlyphPrevious(std::string_view input, size_t start)
Definition string.cpp:1428
std::shared_ptr< T > Make(Args &&... args)
Definition component.hpp:27
std::shared_ptr< Node > Element
Definition elements.hpp:23
Element hbox(Elements)
A container displaying elements horizontally one by one.
Definition hbox.cpp:94
std::vector< Element > Elements
Definition elements.hpp:24
bool EatCodePoint(std::string_view input, size_t start, size_t *end, uint32_t *ucs)
Definition string.cpp:1174
int GlyphCount(std::string_view input)
Definition string.cpp:1534
int string_width(std::string_view)
Definition string.cpp:1336
Decorator reflect(Box &box)
Definition reflect.cpp:43
bool IsFullWidth(uint32_t ucs)
Definition string.cpp:1286
Element frame(Element)
Allow an element to be displayed inside a 'virtual' area. It size can be larger than its container....
Definition frame.cpp:118
std::shared_ptr< ComponentBase > Component
Definition app.hpp:24
size_t GlyphNext(std::string_view input, size_t start)
Definition string.cpp:1453