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> SplitLines(std::string_view input) {
31 std::vector<std::string> output;
32 size_t start = 0;
33 size_t end = input.find('\n');
34 while (end != std::string_view::npos) {
35 output.push_back(std::string(input.substr(start, end - start)));
36 start = end + 1;
37 end = input.find('\n', start);
38 }
39 output.push_back(std::string(input.substr(start)));
40 return output;
41}
42
43size_t GlyphWidth(std::string_view 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(std::string_view 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 = SplitLines(*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 const std::string cursor_cell = is_focused ? " " : "";
152 if (cursor_char_index >= (int)line.size()) {
153 elements.push_back(
154 hbox({
155 Text(line),
156 text(cursor_cell) | focused | reflect(cursor_box_),
157 }) |
158 xflex);
159 continue;
160 }
161
162 // The cursor is on this line.
163 const int glyph_start = cursor_char_index;
164 const int glyph_end = static_cast<int>(GlyphNext(line, glyph_start));
165 const std::string part_before_cursor = line.substr(0, glyph_start);
166 const std::string part_at_cursor =
167 line.substr(glyph_start, glyph_end - glyph_start);
168 const std::string part_after_cursor = line.substr(glyph_end);
169 auto element = hbox({
170 Text(part_before_cursor),
171 Text(part_at_cursor) | focused | reflect(cursor_box_),
172 Text(part_after_cursor),
173 }) |
174 xflex;
175 elements.push_back(element);
176 }
177
178 auto element = vbox(std::move(elements)) | frame;
179 return transform_func({
180 std::move(element), hovered_, is_focused,
181 false // placeholder
182 }) |
183 xflex | reflect(box_);
184 }
185
186 Element Text(const std::string& input) {
187 if (!password()) {
188 return text(input);
189 }
190
191 const size_t glyph_count = GlyphCount(input);
192 std::string out;
193 out.reserve(glyph_count * 3);
194 for (size_t i = 0; i < glyph_count; ++i) {
195 out += "•";
196 }
197 return text(out);
198 }
199
200 bool HandleBackspace() {
201 if (cursor_position() == 0) {
202 return false;
203 }
204 const size_t start = GlyphPrevious(content(), cursor_position());
205 const size_t end = cursor_position();
206 content->erase(start, end - start);
207 cursor_position() = static_cast<int>(start);
208 App::PostEventOrExecute(on_change);
209 return true;
210 }
211
212 bool DeleteImpl() {
213 if (cursor_position() == (int)content->size()) {
214 return false;
215 }
216 const size_t start = cursor_position();
217 const size_t end = GlyphNext(content(), cursor_position());
218 content->erase(start, end - start);
219 return true;
220 }
221
222 bool HandleDelete() {
223 if (DeleteImpl()) {
224 App::PostEventOrExecute(on_change);
225 return true;
226 }
227 return false;
228 }
229
230 bool HandleArrowLeft() {
231 if (cursor_position() == 0) {
232 return false;
233 }
234
235 cursor_position() =
236 static_cast<int>(GlyphPrevious(content(), cursor_position()));
237 return true;
238 }
239
240 bool HandleArrowRight() {
241 if (cursor_position() == (int)content->size()) {
242 return false;
243 }
244
245 cursor_position() =
246 static_cast<int>(GlyphNext(content(), cursor_position()));
247 return true;
248 }
249
250 size_t CursorColumn() {
251 size_t iter = cursor_position();
252 int width = 0;
253 while (true) {
254 if (iter == 0) {
255 break;
256 }
257 iter = GlyphPrevious(content(), iter);
258 if (content()[iter] == '\n') {
259 break;
260 }
261 if (password()) {
262 width += 1;
263 } else {
264 width += static_cast<int>(GlyphWidth(content(), iter));
265 }
266 }
267 return width;
268 }
269
270 // Move the cursor `columns` on the right, if possible.
271 void MoveCursorColumn(int columns) {
272 while (columns > 0) {
273 if (cursor_position() == (int)content().size() ||
274 content()[cursor_position()] == '\n') {
275 return;
276 }
277
278 if (password()) {
279 columns -= 1;
280 } else {
281 columns -= static_cast<int>(GlyphWidth(content(), cursor_position()));
282 }
283 cursor_position() =
284 static_cast<int>(GlyphNext(content(), cursor_position()));
285 }
286 }
287
288 bool HandleArrowUp() {
289 if (cursor_position() == 0) {
290 return false;
291 }
292
293 const size_t columns = CursorColumn();
294
295 // Move cursor at the beginning of 2 lines above.
296 while (true) {
297 if (cursor_position() == 0) {
298 return true;
299 }
300 const size_t previous = GlyphPrevious(content(), cursor_position());
301 if (content()[previous] == '\n') {
302 break;
303 }
304 cursor_position() = static_cast<int>(previous);
305 }
306 cursor_position() =
307 static_cast<int>(GlyphPrevious(content(), cursor_position()));
308 while (true) {
309 if (cursor_position() == 0) {
310 break;
311 }
312 const size_t previous = GlyphPrevious(content(), cursor_position());
313 if (content()[previous] == '\n') {
314 break;
315 }
316 cursor_position() = static_cast<int>(previous);
317 }
318
319 MoveCursorColumn(static_cast<int>(columns));
320 return true;
321 }
322
323 bool HandleArrowDown() {
324 if (cursor_position() == (int)content->size()) {
325 return false;
326 }
327
328 const size_t columns = CursorColumn();
329
330 // Move cursor at the beginning of the next line
331 while (true) {
332 if (content()[cursor_position()] == '\n') {
333 break;
334 }
335 cursor_position() =
336 static_cast<int>(GlyphNext(content(), cursor_position()));
337 if (cursor_position() == (int)content().size()) {
338 return true;
339 }
340 }
341 cursor_position() =
342 static_cast<int>(GlyphNext(content(), cursor_position()));
343
344 MoveCursorColumn(static_cast<int>(columns));
345 return true;
346 }
347
348 bool HandleHome() {
349 cursor_position() = 0;
350 return true;
351 }
352
353 bool HandleEnd() {
354 cursor_position() = static_cast<int>(content->size());
355 return true;
356 }
357
358 bool HandleReturn() {
359 if (multiline()) {
360 HandleCharacter("\n");
361 }
362 App::PostEventOrExecute(on_enter);
363 return true;
364 }
365
366 bool HandleCharacter(const std::string& character) {
367 if (!insert() && cursor_position() < (int)content->size() &&
368 content()[cursor_position()] != '\n') {
369 DeleteImpl();
370 }
371 content->insert(cursor_position(), character);
372 cursor_position() += static_cast<int>(character.size());
373 App::PostEventOrExecute(on_change);
374 return true;
375 }
376
377 bool OnEvent(Event event) override {
378 cursor_position() = util::clamp(cursor_position(), 0, (int)content->size());
379
380 if (event == Event::Return) {
381 return HandleReturn();
382 }
383 if (event.is_character()) {
384 return HandleCharacter(event.character());
385 }
386 if (event.is_mouse()) {
387 return HandleMouse(event);
388 }
389 if (event == Event::Backspace) {
390 return HandleBackspace();
391 }
392 if (event == Event::Delete) {
393 return HandleDelete();
394 }
395 if (event == Event::ArrowLeft) {
396 return HandleArrowLeft();
397 }
398 if (event == Event::ArrowRight) {
399 return HandleArrowRight();
400 }
401 if (event == Event::ArrowUp) {
402 return HandleArrowUp();
403 }
404 if (event == Event::ArrowDown) {
405 return HandleArrowDown();
406 }
407 if (event == Event::Home) {
408 return HandleHome();
409 }
410 if (event == Event::End) {
411 return HandleEnd();
412 }
413 if (event == Event::ArrowLeftCtrl) {
414 return HandleLeftCtrl();
415 }
416 if (event == Event::ArrowRightCtrl) {
417 return HandleRightCtrl();
418 }
419 if (event == Event::Insert) {
420 return HandleInsert();
421 }
422 return false;
423 }
424
425 bool HandleLeftCtrl() {
426 if (cursor_position() == 0) {
427 return false;
428 }
429
430 // Move left, as long as left it not a word.
431 while (cursor_position()) {
432 const size_t previous = GlyphPrevious(content(), cursor_position());
433 if (IsWordCharacter(content(), previous)) {
434 break;
435 }
436 cursor_position() = static_cast<int>(previous);
437 }
438 // Move left, as long as left is a word character:
439 while (cursor_position()) {
440 const size_t previous = GlyphPrevious(content(), cursor_position());
441 if (!IsWordCharacter(content(), previous)) {
442 break;
443 }
444 cursor_position() = static_cast<int>(previous);
445 }
446 return true;
447 }
448
449 bool HandleRightCtrl() {
450 if (cursor_position() == (int)content().size()) {
451 return false;
452 }
453
454 // Move right, until entering a word.
455 while (cursor_position() < (int)content().size()) {
456 cursor_position() =
457 static_cast<int>(GlyphNext(content(), cursor_position()));
458 if (IsWordCharacter(content(), cursor_position())) {
459 break;
460 }
461 }
462 // Move right, as long as right is a word character:
463 while (cursor_position() < (int)content().size()) {
464 const size_t next = GlyphNext(content(), cursor_position());
465 if (!IsWordCharacter(content(), cursor_position())) {
466 break;
467 }
468 cursor_position() = static_cast<int>(next);
469 }
470
471 return true;
472 }
473
474 bool HandleMouse(Event event) {
475 hovered_ = box_.Contain(event.mouse().x, //
476 event.mouse().y) &&
477 CaptureMouse(event);
478 if (!hovered_) {
479 return false;
480 }
481
482 if (event.mouse().button != Mouse::Left) {
483 return false;
484 }
485 if (event.mouse().motion != Mouse::Pressed) {
486 return false;
487 }
488
489 TakeFocus();
490
491 if (content->empty()) {
492 cursor_position() = 0;
493 return true;
494 }
495
496 // Find the line and index of the cursor.
497 std::vector<std::string> lines = SplitLines(*content);
498 int cursor_line = 0;
499 int cursor_char_index = cursor_position();
500 for (const auto& line : lines) {
501 if (cursor_char_index <= (int)line.size()) {
502 break;
503 }
504
505 cursor_char_index -= static_cast<int>(line.size() + 1);
506 cursor_line++;
507 }
508 const int cursor_column =
509 password()
510 ? GlyphCount(lines[cursor_line].substr(0, cursor_char_index))
511 : string_width(lines[cursor_line].substr(0, cursor_char_index));
512
513 int new_cursor_column = cursor_column + event.mouse().x - cursor_box_.x_min;
514 int new_cursor_line = cursor_line + event.mouse().y - cursor_box_.y_min;
515
516 // Fix the new cursor position:
517 new_cursor_line = std::max(std::min(new_cursor_line, (int)lines.size()), 0);
518
519 const std::string empty_string;
520 const std::string& line = new_cursor_line < (int)lines.size()
521 ? lines[new_cursor_line]
522 : empty_string;
523 new_cursor_column =
524 util::clamp(new_cursor_column, 0,
525 password() ? GlyphCount(line) : string_width(line));
526
527 if (new_cursor_column == cursor_column && //
528 new_cursor_line == cursor_line) {
529 return false;
530 }
531
532 // Convert back the new_cursor_{line,column} toward cursor_position:
533 cursor_position() = 0;
534 for (int i = 0; i < new_cursor_line; ++i) {
535 cursor_position() += static_cast<int>(lines[i].size() + 1);
536 }
537 while (new_cursor_column > 0) {
538 if (password()) {
539 new_cursor_column -= 1;
540 } else {
541 new_cursor_column -=
542 static_cast<int>(GlyphWidth(content(), cursor_position()));
543 }
544 cursor_position() =
545 static_cast<int>(GlyphNext(content(), cursor_position()));
546 }
547
548 App::PostEventOrExecute(on_change);
549 return true;
550 }
551
552 bool HandleInsert() {
553 insert() = !insert();
554 return true;
555 }
556
557 bool Focusable() const final { return true; }
558
559 bool hovered_ = false;
560
561 Box box_;
562 Box cursor_box_;
563};
564
565} // namespace
566
567/// @brief An input box for editing text.
568/// @param option Additional optional parameters.
569/// @ingroup component
570/// @see InputBase
571///
572/// ### Example
573///
574/// ```cpp
575/// auto screen = App::FitComponent();
576/// std::string content= "";
577/// std::string placeholder = "placeholder";
578/// Component input = Input({
579/// .content = &content,
580/// .placeholder = &placeholder,
581/// })
582/// screen.Loop(input);
583/// ```
584///
585/// ### Output
586///
587/// ```bash
588/// placeholder
589/// ```
590Component Input(InputOption option) {
591 return Make<InputBase>(std::move(option));
592}
593
594/// @brief An input box for editing text.
595/// @param content The editable content.
596/// @param option Additional optional parameters.
597/// @ingroup component
598/// @see InputBase
599///
600/// ### Example
601///
602/// ```cpp
603/// auto screen = App::FitComponent();
604/// std::string content= "";
605/// std::string placeholder = "placeholder";
606/// Component input = Input(content, {
607/// .placeholder = &placeholder,
608/// .password = true,
609/// })
610/// screen.Loop(input);
611/// ```
612///
613/// ### Output
614///
615/// ```bash
616/// placeholder
617/// ```
618Component Input(StringRef content, InputOption option) {
619 option.content = std::move(content);
620 return Make<InputBase>(std::move(option));
621}
622
623/// @brief An input box for editing text.
624/// @param content The editable content.
625/// @param placeholder The placeholder text.
626/// @param option Additional optional parameters.
627/// @ingroup component
628/// @see InputBase
629///
630/// ### Example
631///
632/// ```cpp
633/// auto screen = App::FitComponent();
634/// std::string content= "";
635/// std::string placeholder = "placeholder";
636/// Component input = Input(content, placeholder);
637/// screen.Loop(input);
638/// ```
639///
640/// ### Output
641///
642/// ```bash
643/// placeholder
644/// ```
645Component Input(StringRef content, StringRef placeholder, InputOption option) {
646 option.content = std::move(content);
647 option.placeholder = std::move(placeholder);
648 return Make<InputBase>(std::move(option));
649}
650
651} // namespace ftxui
Component Input(StringRef content, InputOption options={})
An input box for editing text.
Element text(std::string_view text)
Display a piece of UTF8 encoded unicode text.
Element xflex(Element)
Expand/Minimize if possible/needed on the X axis.
Definition flex.cpp:129
Decorator size(WidthOrHeight direction, Constraint constraint, int value)
Apply a constraint on the size of an element.
Element vbox(Elements children)
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:11
WordBreakProperty CodepointToWordBreakProperty(uint32_t codepoint)
Definition string.cpp:1309
size_t GlyphPrevious(std::string_view input, size_t start)
Definition string.cpp:1429
std::shared_ptr< T > Make(Args &&... args)
Definition component.hpp:28
std::shared_ptr< Node > Element
Definition elements.hpp:24
int string_width(std::string_view input)
Definition string.cpp:1337
Element hbox(Elements children)
A container displaying elements horizontally one by one.
Definition hbox.cpp:94
std::vector< Element > Elements
Definition elements.hpp:25
bool EatCodePoint(std::string_view input, size_t start, size_t *end, uint32_t *ucs)
Definition string.cpp:1175
int GlyphCount(std::string_view input)
Definition string.cpp:1535
const Element & element
Definition node.hpp:95
Decorator reflect(Box &box)
Definition reflect.cpp:43
bool IsFullWidth(uint32_t ucs)
Definition string.cpp:1287
Element frame(Element child)
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:23
size_t GlyphNext(std::string_view input, size_t start)
Definition string.cpp:1454