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