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/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
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// 入力ボックス。ユーザーはテキストを入力できます。
92class InputBase : public ComponentBase, public InputOption {
93 public:
94 // NOLINTNEXTLINE
95 InputBase(InputOption option) : InputOption(std::move(option)) {}
96
97 private:
98 // コンポーネントの実装:
99 Element OnRender() override {
100 const bool is_focused = Focused();
101 const auto focused = (!is_focused && !hovered_) ? nothing
102 : insert() ? focusCursorBarBlinking
103 : focusCursorBlockBlinking;
104
105 auto transform_func =
106 transform ? transform : InputOption::Default().transform;
107
108 // プレースホルダー。
109 if (content->empty()) {
110 auto element = text(placeholder()) | xflex | frame;
111
112 return transform_func({
113 std::move(element), hovered_, is_focused,
114 true // placeholder
115 }) |
116 focus | 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 // カーソルの行とインデックスを検索します。
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 // これはカーソル行ではありません。
145 if (int(i) != cursor_line) {
146 elements.push_back(Text(line));
147 continue;
148 }
149
150 // カーソルが行末にあります。
151 if (cursor_char_index >= (int)line.size()) {
152 elements.push_back(hbox({
153 Text(line),
154 text(" ") | focused | reflect(cursor_box_),
155 }) |
156 xflex);
157 continue;
158 }
159
160 // カーソルはこの行にあります。
161 const int glyph_start = cursor_char_index;
162 const int glyph_end = static_cast<int>(GlyphNext(line, glyph_start));
163 const std::string part_before_cursor = line.substr(0, glyph_start);
164 const std::string part_at_cursor =
165 line.substr(glyph_start, glyph_end - glyph_start);
166 const std::string part_after_cursor = line.substr(glyph_end);
167 auto element = hbox({
168 Text(part_before_cursor),
169 Text(part_at_cursor) | focused | reflect(cursor_box_),
170 Text(part_after_cursor),
171 }) |
172 xflex;
173 elements.push_back(element);
174 }
175
176 auto element = vbox(std::move(elements)) | frame;
177 return transform_func({
178 std::move(element), hovered_, is_focused,
179 false // placeholder
180 }) |
181 xflex | reflect(box_);
182 }
183
184 Element Text(const std::string& input) {
185 if (!password()) {
186 return text(input);
187 }
188
189 std::string out;
190 out.reserve(10 + input.size() * 3 / 2);
191 for (size_t i = 0; i < input.size(); ++i) {
192 out += "•";
193 }
194 return text(out);
195 }
196
197 bool HandleBackspace() {
198 if (cursor_position() == 0) {
199 return false;
200 }
201 const size_t start = GlyphPrevious(content(), cursor_position());
202 const size_t end = cursor_position();
203 content->erase(start, end - start);
204 cursor_position() = static_cast<int>(start);
205 on_change();
206 return true;
207 }
208
209 bool DeleteImpl() {
210 if (cursor_position() == (int)content->size()) {
211 return false;
212 }
213 const size_t start = cursor_position();
214 const size_t end = GlyphNext(content(), cursor_position());
215 content->erase(start, end - start);
216 return true;
217 }
218
219 bool HandleDelete() {
220 if (DeleteImpl()) {
221 on_change();
222 return true;
223 }
224 return false;
225 }
226
227 bool HandleArrowLeft() {
228 if (cursor_position() == 0) {
229 return false;
230 }
231
232 cursor_position() =
233 static_cast<int>(GlyphPrevious(content(), cursor_position()));
234 return true;
235 }
236
237 bool HandleArrowRight() {
238 if (cursor_position() == (int)content->size()) {
239 return false;
240 }
241
242 cursor_position() =
243 static_cast<int>(GlyphNext(content(), cursor_position()));
244 return true;
245 }
246
247 size_t CursorColumn() {
248 size_t iter = cursor_position();
249 int width = 0;
250 while (true) {
251 if (iter == 0) {
252 break;
253 }
254 iter = GlyphPrevious(content(), iter);
255 if (content()[iter] == '\n') {
256 break;
257 }
258 width += static_cast<int>(GlyphWidth(content(), iter));
259 }
260 return width;
261 }
262
263 // 可能であれば、カーソルを右に `columns` 移動します。
264 void MoveCursorColumn(int columns) {
265 while (columns > 0) {
266 if (cursor_position() == (int)content().size() ||
267 content()[cursor_position()] == '\n') {
268 return;
269 }
270
271 columns -= static_cast<int>(GlyphWidth(content(), cursor_position()));
272 cursor_position() =
273 static_cast<int>(GlyphNext(content(), cursor_position()));
274 }
275 }
276
277 bool HandleArrowUp() {
278 if (cursor_position() == 0) {
279 return false;
280 }
281
282 const size_t columns = CursorColumn();
283
284 // カーソルを2行上の先頭に移動します。
285 while (true) {
286 if (cursor_position() == 0) {
287 return true;
288 }
289 const size_t previous = GlyphPrevious(content(), cursor_position());
290 if (content()[previous] == '\n') {
291 break;
292 }
293 cursor_position() = static_cast<int>(previous);
294 }
295 cursor_position() =
296 static_cast<int>(GlyphPrevious(content(), cursor_position()));
297 while (true) {
298 if (cursor_position() == 0) {
299 break;
300 }
301 const size_t previous = GlyphPrevious(content(), cursor_position());
302 if (content()[previous] == '\n') {
303 break;
304 }
305 cursor_position() = static_cast<int>(previous);
306 }
307
308 MoveCursorColumn(static_cast<int>(columns));
309 return true;
310 }
311
312 bool HandleArrowDown() {
313 if (cursor_position() == (int)content->size()) {
314 return false;
315 }
316
317 const size_t columns = CursorColumn();
318
319 // カーソルを次の行の先頭に移動します
320 while (true) {
321 if (content()[cursor_position()] == '\n') {
322 break;
323 }
324 cursor_position() =
325 static_cast<int>(GlyphNext(content(), cursor_position()));
326 if (cursor_position() == (int)content().size()) {
327 return true;
328 }
329 }
330 cursor_position() =
331 static_cast<int>(GlyphNext(content(), cursor_position()));
332
333 MoveCursorColumn(static_cast<int>(columns));
334 return true;
335 }
336
337 bool HandleHome() {
338 cursor_position() = 0;
339 return true;
340 }
341
342 bool HandleEnd() {
343 cursor_position() = static_cast<int>(content->size());
344 return true;
345 }
346
347 bool HandleReturn() {
348 if (multiline()) {
349 HandleCharacter("\n");
350 }
351 on_enter();
352 return true;
353 }
354
355 bool HandleCharacter(const std::string& character) {
356 if (!insert() && cursor_position() < (int)content->size() &&
357 content()[cursor_position()] != '\n') {
358 DeleteImpl();
359 }
360 content->insert(cursor_position(), character);
361 cursor_position() += static_cast<int>(character.size());
362 on_change();
363 return true;
364 }
365
366 bool OnEvent(Event event) override {
367 cursor_position() = util::clamp(cursor_position(), 0, (int)content->size());
368
369 if (event == Event::Return) {
370 return HandleReturn();
371 }
372 if (event.is_character()) {
373 return HandleCharacter(event.character());
374 }
375 if (event.is_mouse()) {
376 return HandleMouse(event);
377 }
378 if (event == Event::Backspace) {
379 return HandleBackspace();
380 }
381 if (event == Event::Delete) {
382 return HandleDelete();
383 }
384 if (event == Event::ArrowLeft) {
385 return HandleArrowLeft();
386 }
387 if (event == Event::ArrowRight) {
388 return HandleArrowRight();
389 }
390 if (event == Event::ArrowUp) {
391 return HandleArrowUp();
392 }
393 if (event == Event::ArrowDown) {
394 return HandleArrowDown();
395 }
396 if (event == Event::Home) {
397 return HandleHome();
398 }
399 if (event == Event::End) {
400 return HandleEnd();
401 }
402 if (event == Event::ArrowLeftCtrl) {
403 return HandleLeftCtrl();
404 }
405 if (event == Event::ArrowRightCtrl) {
406 return HandleRightCtrl();
407 }
408 if (event == Event::Insert) {
409 return HandleInsert();
410 }
411 return false;
412 }
413
414 bool HandleLeftCtrl() {
415 if (cursor_position() == 0) {
416 return false;
417 }
418
419 // 左が単語ではない限り、左に移動します。
420 while (cursor_position()) {
421 const size_t previous = GlyphPrevious(content(), cursor_position());
422 if (IsWordCharacter(content(), previous)) {
423 break;
424 }
425 cursor_position() = static_cast<int>(previous);
426 }
427 // 左が単語文字である限り、左に移動します:
428 while (cursor_position()) {
429 const size_t previous = GlyphPrevious(content(), cursor_position());
430 if (!IsWordCharacter(content(), previous)) {
431 break;
432 }
433 cursor_position() = static_cast<int>(previous);
434 }
435 return true;
436 }
437
438 bool HandleRightCtrl() {
439 if (cursor_position() == (int)content().size()) {
440 return false;
441 }
442
443 // 単語に入るまで右に移動します。
444 while (cursor_position() < (int)content().size()) {
445 cursor_position() =
446 static_cast<int>(GlyphNext(content(), cursor_position()));
447 if (IsWordCharacter(content(), cursor_position())) {
448 break;
449 }
450 }
451 // 右が単語文字である限り、右に移動します:
452 while (cursor_position() < (int)content().size()) {
453 const size_t next = GlyphNext(content(), cursor_position());
454 if (!IsWordCharacter(content(), cursor_position())) {
455 break;
456 }
457 cursor_position() = static_cast<int>(next);
458 }
459
460 return true;
461 }
462
463 bool HandleMouse(Event event) {
464 hovered_ = box_.Contain(event.mouse().x, //
465 event.mouse().y) &&
466 CaptureMouse(event);
467 if (!hovered_) {
468 return false;
469 }
470
471 if (event.mouse().button != Mouse::Left) {
472 return false;
473 }
474 if (event.mouse().motion != Mouse::Pressed) {
475 return false;
476 }
477
478 TakeFocus();
479
480 if (content->empty()) {
481 cursor_position() = 0;
482 return true;
483// カーソルの行とインデックスを検索します。
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 -= static_cast<int>(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 // new_cursor_{line,column} を cursor_position に戻します:
516 cursor_position() = 0;
517 for (int i = 0; i < new_cursor_line; ++i) {
518 cursor_position() += static_cast<int>(lines[i].size() + 1);
519 }
520 while (new_cursor_column > 0) {
521 new_cursor_column -=
522 static_cast<int>(GlyphWidth(content(), cursor_position()));
523 cursor_position() =
524 static_cast<int>(GlyphNext(content(), cursor_position()));
525 }
526
527 on_change();
528 return true;
529 }
530
531 bool HandleInsert() {
532 insert() = !insert();
533 return true;
534 }
535
536 bool Focusable() const final { return true; }
537
538 bool hovered_ = false;
539
540 Box box_;
541 Box cursor_box_;
542};
543
544} // namespace
545
546/// @brief テキストを編集するための入力ボックス。
547/// @param option 追加のオプションパラメータ。
548/// @ingroup component
549/// @see InputBase
550///
551/// ### 例
552///
553/// ```cpp
554/// auto screen = ScreenInteractive::FitComponent();
555/// std::string content= "";
556/// std::string placeholder = "placeholder";
557/// Component input = Input({
558/// .content = &content,
559/// .placeholder = &placeholder,
560/// })
561/// screen.Loop(input);
562/// ```
563///
564/// ### 出力
565///
566/// ```bash
567/// placeholder
568/// ```
569Component Input(InputOption option) {
570 return Make<InputBase>(std::move(option));
571}
572
573/// @brief テキストを編集するための入力ボックス。
574/// @param content 編集可能なコンテンツ。
575/// @param option 追加のオプションパラメータ。
576/// @ingroup component
577/// @see InputBase
578///
579/// ### 例
580///
581/// ```cpp
582/// auto screen = ScreenInteractive::FitComponent();
583/// std::string content= "";
584/// std::string placeholder = "placeholder";
585/// Component input = Input(content, {
586/// .placeholder = &placeholder,
587/// .password = true,
588/// })
589/// screen.Loop(input);
590/// ```
591///
592/// ### 出力
593///
594/// ```bash
595/// placeholder
596/// ```
597Component Input(StringRef content, InputOption option) {
598 option.content = std::move(content);
599 return Make<InputBase>(std::move(option));
600}
601
602/// @brief テキストを編集するための入力ボックス。
603/// @param content 編集可能なコンテンツ。
604/// @param placeholder プレースホルダーテキスト。
605/// @param option 追加のオプションパラメータ。
606/// @ingroup component
607/// @see InputBase
608///
609/// ### 例
610///
611/// ```cpp
612/// auto screen = ScreenInteractive::FitComponent();
613/// std::string content= "";
614/// std::string placeholder = "placeholder";
615/// Component input = Input(content, placeholder);
616/// screen.Loop(input);
617/// ```
618///
619/// ### 出力
620///
621/// ```bash
622/// placeholder
623/// ```
624Component Input(StringRef content, StringRef placeholder, InputOption option) {
625 option.content = std::move(content);
626 option.placeholder = std::move(placeholder);
627 return Make<InputBase>(std::move(option));
628}
629
630} // namespace ftxui
FTXUI ftxui:: 名前空間
Definition animation.hpp:9
size_t GlyphNext(const std::string &input, size_t start)
Definition string.cpp:1414
WordBreakProperty CodepointToWordBreakProperty(uint32_t codepoint)
Definition string.cpp:1301
std::shared_ptr< T > Make(Args &&... args)
Definition component.hpp:26
std::shared_ptr< Node > Element
Definition elements.hpp:21
std::vector< Element > Elements
Definition elements.hpp:22
bool EatCodePoint(const std::string &input, size_t start, size_t *end, uint32_t *ucs)
Definition string.cpp:1170
Component Input(InputOption options={})
bool IsFullWidth(uint32_t ucs)
Definition string.cpp:1279
size_t GlyphPrevious(const std::string &input, size_t start)
Definition string.cpp:1392
std::shared_ptr< ComponentBase > Component
return size
Definition string.cpp:1516