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