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 <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 
26 namespace ftxui {
27 
28 namespace {
29 
30 std::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 
43 size_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 
54 bool 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 
82 bool 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.
92 class 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 
122  Elements elements;
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  }
405  if (event == Event::ArrowLeftCtrl) {
406  return HandleLeftCtrl();
407  }
408  if (event == Event::ArrowRightCtrl) {
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 =
501  string_width(lines[cursor_line].substr(0, cursor_char_index));
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()
511  ? lines[new_cursor_line]
512  : empty_string;
513  new_cursor_column = util::clamp(new_cursor_column, 0, string_width(line));
514 
515  if (new_cursor_column == cursor_column && //
516  new_cursor_line == cursor_line) {
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) {
526  new_cursor_column -=
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 /// ```
628 Component Input(StringRef content, StringRef placeholder, InputOption option) {
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< 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
StringRef placeholder
The content of the input when it's empty.
StringRef content
The content of the input.