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 
28 namespace ftxui {
29 
30 namespace {
31 
32 std::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 
45 size_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 
56 bool 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 
84 bool 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.
94 class 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().button != Mouse::Left) {
470  return false;
471  }
472  if (event.mouse().motion != Mouse::Pressed) {
473  return false;
474  }
475 
476  TakeFocus();
477 
478  if (content->empty()) {
479  cursor_position() = 0;
480  return true;
481  }
482 
483  // Find the line and index of the cursor.
484  std::vector<std::string> lines = Split(*content);
485  int cursor_line = 0;
486  int cursor_char_index = cursor_position();
487  for (const auto& line : lines) {
488  if (cursor_char_index <= (int)line.size()) {
489  break;
490  }
491 
492  cursor_char_index -= line.size() + 1;
493  cursor_line++;
494  }
495  const int cursor_column =
496  string_width(lines[cursor_line].substr(0, cursor_char_index));
497 
498  int new_cursor_column = cursor_column + event.mouse().x - cursor_box_.x_min;
499  int new_cursor_line = cursor_line + event.mouse().y - cursor_box_.y_min;
500 
501  // Fix the new cursor position:
502  new_cursor_line = std::max(std::min(new_cursor_line, (int)lines.size()), 0);
503 
504  const std::string empty_string;
505  const std::string& line = new_cursor_line < (int)lines.size()
506  ? lines[new_cursor_line]
507  : empty_string;
508  new_cursor_column = util::clamp(new_cursor_column, 0, string_width(line));
509 
510  if (new_cursor_column == cursor_column && //
511  new_cursor_line == cursor_line) {
512  return false;
513  }
514 
515  // Convert back the new_cursor_{line,column} toward cursor_position:
516  cursor_position() = 0;
517  for (int i = 0; i < new_cursor_line; ++i) {
518  cursor_position() += lines[i].size() + 1;
519  }
520  while (new_cursor_column > 0) {
521  new_cursor_column -= GlyphWidth(content(), cursor_position());
522  cursor_position() = GlyphNext(content(), cursor_position());
523  }
524 
525  on_change();
526  return true;
527  }
528 
529  bool HandleInsert() {
530  insert() = !insert();
531  return true;
532  }
533 
534  bool Focusable() const final { return true; }
535 
536  bool hovered_ = false;
537 
538  Box box_;
539  Box cursor_box_;
540 };
541 
542 } // namespace
543 
544 /// @brief An input box for editing text.
545 /// @param option Additional optional parameters.
546 /// @ingroup component
547 /// @see InputBase
548 ///
549 /// ### Example
550 ///
551 /// ```cpp
552 /// auto screen = ScreenInteractive::FitComponent();
553 /// std::string content= "";
554 /// std::string placeholder = "placeholder";
555 /// Component input = Input({
556 /// .content = &content,
557 /// .placeholder = &placeholder,
558 /// })
559 /// screen.Loop(input);
560 /// ```
561 ///
562 /// ### Output
563 ///
564 /// ```bash
565 /// placeholder
566 /// ```
568  return Make<InputBase>(std::move(option));
569 }
570 
571 /// @brief An input box for editing text.
572 /// @param content The editable content.
573 /// @param option Additional optional parameters.
574 /// @ingroup component
575 /// @see InputBase
576 ///
577 /// ### Example
578 ///
579 /// ```cpp
580 /// auto screen = ScreenInteractive::FitComponent();
581 /// std::string content= "";
582 /// std::string placeholder = "placeholder";
583 /// Component input = Input(content, {
584 /// .placeholder = &placeholder,
585 /// .password = true,
586 /// })
587 /// screen.Loop(input);
588 /// ```
589 ///
590 /// ### Output
591 ///
592 /// ```bash
593 /// placeholder
594 /// ```
596  option.content = std::move(content);
597  return Make<InputBase>(std::move(option));
598 }
599 
600 /// @brief An input box for editing text.
601 /// @param content The editable content.
602 /// @param option Additional optional parameters.
603 /// @ingroup component
604 /// @see InputBase
605 ///
606 /// ### Example
607 ///
608 /// ```cpp
609 /// auto screen = ScreenInteractive::FitComponent();
610 /// std::string content= "";
611 /// std::string placeholder = "placeholder";
612 /// Component input = Input(content, placeholder);
613 /// screen.Loop(input);
614 /// ```
615 ///
616 /// ### Output
617 ///
618 /// ```bash
619 /// placeholder
620 /// ```
621 Component Input(StringRef content, StringRef placeholder, InputOption option) {
622  option.content = std::move(content);
623  option.placeholder = std::move(placeholder);
624  return Make<InputBase>(std::move(option));
625 }
626 
627 } // 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:567
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.