FTXUI  5.0.0
C++ functional terminal UI.
Loading...
Searching...
No Matches
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/component.hpp" // for Make, Input
14#include "ftxui/component/component_base.hpp" // for ComponentBase
15#include "ftxui/component/component_options.hpp" // for InputOption
16#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
17#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Left, Mouse::Pressed
18#include "ftxui/component/screen_interactive.hpp" // for Component
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
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 Render() override {
100 const bool is_focused = Focused();
101 const auto focused = (!is_focused && !hovered_) ? select
102 : insert() ? focusCursorBarBlinking
104
105 auto transform_func =
106 transform ? transform : InputOption::Default().transform;
107
108 // placeholder.
109 if (content->empty()) {
110 auto element = text(placeholder()) | xflex | frame;
111 if (is_focused) {
112 element |= focus;
113 }
114
115 return transform_func({
116 std::move(element), hovered_, is_focused,
117 true // placeholder
118 }) |
119 reflect(box_);
120 }
121
123 const std::vector<std::string> lines = Split(*content);
124
125 cursor_position() = util::clamp(cursor_position(), 0, (int)content->size());
126
127 // Find the line and index of the cursor.
128 int cursor_line = 0;
129 int cursor_char_index = cursor_position();
130 for (const auto& line : lines) {
131 if (cursor_char_index <= (int)line.size()) {
132 break;
133 }
134
135 cursor_char_index -= static_cast<int>(line.size() + 1);
136 cursor_line++;
137 }
138
139 if (lines.empty()) {
140 elements.push_back(text("") | focused);
141 }
142
143 elements.reserve(lines.size());
144 for (size_t i = 0; i < lines.size(); ++i) {
145 const std::string& line = lines[i];
146
147 // This is not the cursor line.
148 if (int(i) != cursor_line) {
149 elements.push_back(Text(line));
150 continue;
151 }
152
153 // The cursor is at the end of the line.
154 if (cursor_char_index >= (int)line.size()) {
155 elements.push_back(hbox({
156 Text(line),
157 text(" ") | focused | reflect(cursor_box_),
158 }) |
159 xflex);
160 continue;
161 }
162
163 // The cursor is on this line.
164 const int glyph_start = cursor_char_index;
165 const int glyph_end = static_cast<int>(GlyphNext(line, glyph_start));
166 const std::string part_before_cursor = line.substr(0, glyph_start);
167 const std::string part_at_cursor =
168 line.substr(glyph_start, glyph_end - glyph_start);
169 const std::string part_after_cursor = line.substr(glyph_end);
170 auto element = hbox({
171 Text(part_before_cursor),
172 Text(part_at_cursor) | focused | reflect(cursor_box_),
173 Text(part_after_cursor),
174 }) |
175 xflex;
176 elements.push_back(element);
177 }
178
179 auto element = vbox(std::move(elements)) | frame;
180 return transform_func({
181 std::move(element), hovered_, is_focused,
182 false // placeholder
183 }) |
184 xflex | reflect(box_);
185 }
186
187 Element Text(const std::string& input) {
188 if (!password()) {
189 return text(input);
190 }
191
192 std::string out;
193 out.reserve(10 + input.size() * 3 / 2);
194 for (size_t i = 0; i < input.size(); ++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 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 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 width += static_cast<int>(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 -= static_cast<int>(GlyphWidth(content(), cursor_position()));
275 cursor_position() =
276 static_cast<int>(GlyphNext(content(), cursor_position()));
277 }
278 }
279
280 bool HandleArrowUp() {
281 if (cursor_position() == 0) {
282 return false;
283 }
284
285 const size_t columns = CursorColumn();
286
287 // Move cursor at the beginning of 2 lines above.
288 while (true) {
289 if (cursor_position() == 0) {
290 return true;
291 }
292 const size_t previous = GlyphPrevious(content(), cursor_position());
293 if (content()[previous] == '\n') {
294 break;
295 }
296 cursor_position() = static_cast<int>(previous);
297 }
298 cursor_position() =
299 static_cast<int>(GlyphPrevious(content(), cursor_position()));
300 while (true) {
301 if (cursor_position() == 0) {
302 break;
303 }
304 const size_t previous = GlyphPrevious(content(), cursor_position());
305 if (content()[previous] == '\n') {
306 break;
307 }
308 cursor_position() = static_cast<int>(previous);
309 }
310
311 MoveCursorColumn(static_cast<int>(columns));
312 return true;
313 }
314
315 bool HandleArrowDown() {
316 if (cursor_position() == (int)content->size()) {
317 return false;
318 }
319
320 const size_t columns = CursorColumn();
321
322 // Move cursor at the beginning of the next line
323 while (true) {
324 if (content()[cursor_position()] == '\n') {
325 break;
326 }
327 cursor_position() =
328 static_cast<int>(GlyphNext(content(), cursor_position()));
329 if (cursor_position() == (int)content().size()) {
330 return true;
331 }
332 }
333 cursor_position() =
334 static_cast<int>(GlyphNext(content(), cursor_position()));
335
336 MoveCursorColumn(static_cast<int>(columns));
337 return true;
338 }
339
340 bool HandleHome() {
341 cursor_position() = 0;
342 return true;
343 }
344
345 bool HandleEnd() {
346 cursor_position() = static_cast<int>(content->size());
347 return true;
348 }
349
350 bool HandleReturn() {
351 if (multiline()) {
352 HandleCharacter("\n");
353 }
354 on_enter();
355 return true;
356 }
357
358 bool HandleCharacter(const std::string& character) {
359 if (!insert() && cursor_position() < (int)content->size() &&
360 content()[cursor_position()] != '\n') {
361 DeleteImpl();
362 }
363 content->insert(cursor_position(), character);
364 cursor_position() += static_cast<int>(character.size());
365 on_change();
366 return true;
367 }
368
369 bool OnEvent(Event event) override {
370 cursor_position() = util::clamp(cursor_position(), 0, (int)content->size());
371
372 if (event == Event::Return) {
373 return HandleReturn();
374 }
375 if (event.is_character()) {
376 return HandleCharacter(event.character());
377 }
378 if (event.is_mouse()) {
379 return HandleMouse(event);
380 }
381 if (event == Event::Backspace) {
382 return HandleBackspace();
383 }
384 if (event == Event::Delete) {
385 return HandleDelete();
386 }
387 if (event == Event::ArrowLeft) {
388 return HandleArrowLeft();
389 }
390 if (event == Event::ArrowRight) {
391 return HandleArrowRight();
392 }
393 if (event == Event::ArrowUp) {
394 return HandleArrowUp();
395 }
396 if (event == Event::ArrowDown) {
397 return HandleArrowDown();
398 }
399 if (event == Event::Home) {
400 return HandleHome();
401 }
402 if (event == Event::End) {
403 return HandleEnd();
404 }
406 return HandleLeftCtrl();
407 }
409 return HandleRightCtrl();
410 }
411 if (event == Event::Insert) {
412 return HandleInsert();
413 }
414 return false;
415 }
416
417 bool HandleLeftCtrl() {
418 if (cursor_position() == 0) {
419 return false;
420 }
421
422 // Move left, as long as left it not a word.
423 while (cursor_position()) {
424 const size_t previous = GlyphPrevious(content(), cursor_position());
425 if (IsWordCharacter(content(), previous)) {
426 break;
427 }
428 cursor_position() = static_cast<int>(previous);
429 }
430 // Move left, as long as left is a word character:
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 return true;
439 }
440
441 bool HandleRightCtrl() {
442 if (cursor_position() == (int)content().size()) {
443 return false;
444 }
445
446 // Move right, until entering a word.
447 while (cursor_position() < (int)content().size()) {
448 cursor_position() =
449 static_cast<int>(GlyphNext(content(), cursor_position()));
450 if (IsWordCharacter(content(), cursor_position())) {
451 break;
452 }
453 }
454 // Move right, as long as right is a word character:
455 while (cursor_position() < (int)content().size()) {
456 const size_t next = GlyphNext(content(), cursor_position());
457 if (!IsWordCharacter(content(), cursor_position())) {
458 break;
459 }
460 cursor_position() = static_cast<int>(next);
461 }
462
463 return true;
464 }
465
466 bool HandleMouse(Event event) {
467 hovered_ = box_.Contain(event.mouse().x, //
468 event.mouse().y) &&
469 CaptureMouse(event);
470 if (!hovered_) {
471 return false;
472 }
473
474 if (event.mouse().button != Mouse::Left) {
475 return false;
476 }
477 if (event.mouse().motion != Mouse::Pressed) {
478 return false;
479 }
480
481 TakeFocus();
482
483 if (content->empty()) {
484 cursor_position() = 0;
485 return true;
486 }
487
488 // Find the line and index of the cursor.
489 std::vector<std::string> lines = Split(*content);
490 int cursor_line = 0;
491 int cursor_char_index = cursor_position();
492 for (const auto& line : lines) {
493 if (cursor_char_index <= (int)line.size()) {
494 break;
495 }
496
497 cursor_char_index -= static_cast<int>(line.size() + 1);
498 cursor_line++;
499 }
500 const int cursor_column =
502
503 int new_cursor_column = cursor_column + event.mouse().x - cursor_box_.x_min;
504 int new_cursor_line = cursor_line + event.mouse().y - cursor_box_.y_min;
505
506 // Fix the new cursor position:
507 new_cursor_line = std::max(std::min(new_cursor_line, (int)lines.size()), 0);
508
509 const std::string empty_string;
510 const std::string& line = new_cursor_line < (int)lines.size()
512 : empty_string;
514
517 return false;
518 }
519
520 // Convert back the new_cursor_{line,column} toward cursor_position:
521 cursor_position() = 0;
522 for (int i = 0; i < new_cursor_line; ++i) {
523 cursor_position() += static_cast<int>(lines[i].size() + 1);
524 }
525 while (new_cursor_column > 0) {
527 static_cast<int>(GlyphWidth(content(), cursor_position()));
528 cursor_position() =
529 static_cast<int>(GlyphNext(content(), cursor_position()));
530 }
531
532 on_change();
533 return true;
534 }
535
536 bool HandleInsert() {
537 insert() = !insert();
538 return true;
539 }
540
541 bool Focusable() const final { return true; }
542
543 bool hovered_ = false;
544
545 Box box_;
546 Box cursor_box_;
547};
548
549} // namespace
550
551/// @brief An input box for editing text.
552/// @param option Additional optional parameters.
553/// @ingroup component
554/// @see InputBase
555///
556/// ### Example
557///
558/// ```cpp
559/// auto screen = ScreenInteractive::FitComponent();
560/// std::string content= "";
561/// std::string placeholder = "placeholder";
562/// Component input = Input({
563/// .content = &content,
564/// .placeholder = &placeholder,
565/// })
566/// screen.Loop(input);
567/// ```
568///
569/// ### Output
570///
571/// ```bash
572/// placeholder
573/// ```
575 return Make<InputBase>(std::move(option));
576}
577
578/// @brief An input box for editing text.
579/// @param content The editable content.
580/// @param option Additional optional parameters.
581/// @ingroup component
582/// @see InputBase
583///
584/// ### Example
585///
586/// ```cpp
587/// auto screen = ScreenInteractive::FitComponent();
588/// std::string content= "";
589/// std::string placeholder = "placeholder";
590/// Component input = Input(content, {
591/// .placeholder = &placeholder,
592/// .password = true,
593/// })
594/// screen.Loop(input);
595/// ```
596///
597/// ### Output
598///
599/// ```bash
600/// placeholder
601/// ```
603 option.content = std::move(content);
604 return Make<InputBase>(std::move(option));
605}
606
607/// @brief An input box for editing text.
608/// @param content The editable content.
609/// @param option Additional optional parameters.
610/// @ingroup component
611/// @see InputBase
612///
613/// ### Example
614///
615/// ```cpp
616/// auto screen = ScreenInteractive::FitComponent();
617/// std::string content= "";
618/// std::string placeholder = "placeholder";
619/// Component input = Input(content, placeholder);
620/// screen.Loop(input);
621/// ```
622///
623/// ### Output
624///
625/// ```bash
626/// placeholder
627/// ```
629 option.content = std::move(content);
630 option.placeholder = std::move(placeholder);
631 return Make<InputBase>(std::move(option));
632}
633
634} // namespace ftxui
An adapter. Own or reference a constant string. For convenience, this class convert multiple mutable ...
Definition ref.hpp:82
constexpr const T & clamp(const T &v, const T &lo, const T &hi)
Definition util.hpp:11
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:237
Element xflex(Element)
Expand/Minimize if possible/needed on the X axis.
Definition flex.cpp:128
WordBreakProperty CodepointToWordBreakProperty(uint32_t codepoint)
Definition string.cpp:1307
Decorator size(WidthOrHeight, Constraint, int value)
Apply a constraint on the size of an element.
Definition size.cpp:89
std::shared_ptr< Node > Element
Definition elements.hpp:22
std::shared_ptr< T > Make(Args &&... args)
Definition component.hpp:26
std::shared_ptr< ComponentBase > Component
int string_width(const std::string &)
Definition string.cpp:1330
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:119
std::vector< Element > Elements
Definition elements.hpp:23
Component Input(InputOption options={})
An input box for editing text.
Definition input.cpp:574
bool EatCodePoint(const std::string &input, size_t start, size_t *end, uint32_t *ucs)
Definition string.cpp:1174
Element select(Element)
Set the child to be the one selected among its siblings.
Definition frame.cpp:149
Element focus(Element)
Set the child to be the one in focus globally.
Definition frame.cpp:156
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:166
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:209
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:43
static const Event Backspace
Definition event.hpp:49
static const Event ArrowUp
Definition event.hpp:40
static const Event ArrowDown
Definition event.hpp:41
static const Event End
Definition event.hpp:59
static const Event Home
Definition event.hpp:58
static const Event Return
Definition event.hpp:51
static const Event ArrowLeft
Definition event.hpp:38
static const Event Delete
Definition event.hpp:50
static const Event Insert
Definition event.hpp:57
static const Event ArrowRightCtrl
Definition event.hpp:44
static const Event ArrowRight
Definition event.hpp:39
Option for the Input component.
static InputOption Default()
Create the default input style:
std::function< Element(InputState)> transform