FTXUI/src/ftxui/component/input.cpp

581 lines
17 KiB
C++
Raw Normal View History

#include <algorithm> // for max, min
2022-03-31 08:17:43 +08:00
#include <cstddef> // for size_t
#include <cstdint> // for uint32_t
2021-07-10 19:20:43 +08:00
#include <functional> // for function
2023-05-02 19:32:37 +08:00
#include <memory> // for allocator, shared_ptr, allocator_traits<>::value_type
#include <sstream> // for basic_istream, stringstream
#include <string> // for string, basic_string, operator==, getline
#include <utility> // for move
#include <vector> // for vector
2021-05-10 02:32:27 +08:00
2021-07-10 19:20:43 +08:00
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
#include "ftxui/component/component.hpp" // for Make, Input
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/component_options.hpp" // for InputOption
2023-05-02 19:32:37 +08:00
#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
2021-05-10 02:32:27 +08:00
#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Left, Mouse::Pressed
#include "ftxui/component/screen_interactive.hpp" // for Component
2023-05-02 19:32:37 +08:00
#include "ftxui/dom/elements.hpp" // for operator|, reflect, text, Element, xflex, hbox, Elements, frame, operator|=, vbox, focus, focusCursorBarBlinking, select
2021-07-10 19:20:43 +08:00
#include "ftxui/screen/box.hpp" // for Box
2023-05-02 19:32:37 +08:00
#include "ftxui/screen/string.hpp" // for string_width
#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
#include "ftxui/screen/util.hpp" // for clamp
#include "ftxui/util/ref.hpp" // for StringRef, Ref
2021-05-10 02:32:27 +08:00
namespace ftxui {
namespace {
2023-05-02 19:32:37 +08:00
std::vector<std::string> Split(const std::string& input) {
std::vector<std::string> output;
std::stringstream ss(input);
std::string line;
while (std::getline(ss, line)) {
output.push_back(line);
}
if (input.back() == '\n') {
output.push_back("");
}
return output;
}
size_t GlyphWidth(const std::string& input, size_t iter) {
uint32_t ucs = 0;
if (!EatCodePoint(input, iter, &iter, &ucs))
return 0;
if (IsFullWidth(ucs))
return 2;
return 1;
}
bool IsWordCodePoint(uint32_t codepoint) {
switch (CodepointToWordBreakProperty(codepoint)) {
case WordBreakProperty::ALetter:
case WordBreakProperty::Hebrew_Letter:
case WordBreakProperty::Katakana:
case WordBreakProperty::Numeric:
return true;
case WordBreakProperty::CR:
case WordBreakProperty::Double_Quote:
case WordBreakProperty::LF:
case WordBreakProperty::MidLetter:
case WordBreakProperty::MidNum:
case WordBreakProperty::MidNumLet:
case WordBreakProperty::Newline:
case WordBreakProperty::Single_Quote:
case WordBreakProperty::WSegSpace:
2023-05-02 19:32:37 +08:00
// Unexpected/Unsure
case WordBreakProperty::Extend:
case WordBreakProperty::ExtendNumLet:
case WordBreakProperty::Format:
case WordBreakProperty::Regional_Indicator:
case WordBreakProperty::ZWJ:
return false;
}
2023-05-02 19:32:37 +08:00
return false; // NOT_REACHED();
2022-10-16 16:45:11 +08:00
}
2023-05-02 19:32:37 +08:00
bool IsWordCharacter(const std::string& input, size_t iter) {
uint32_t ucs = 0;
if (!EatCodePoint(input, iter, &iter, &ucs)) {
return false;
2022-03-31 08:17:43 +08:00
}
2023-05-02 19:32:37 +08:00
return IsWordCodePoint(ucs);
}
2021-07-10 20:23:46 +08:00
// An input box. The user can type text into it.
class InputBase : public ComponentBase {
2021-07-10 18:29:39 +08:00
public:
2023-05-02 19:32:37 +08:00
// NOLINTNEXTLINE
InputBase(StringRef content, Ref<InputOption> option)
: content_(std::move(content)), option_(std::move(option)) {}
2021-07-10 18:29:39 +08:00
2023-05-02 19:32:37 +08:00
private:
2021-07-10 18:29:39 +08:00
// Component implementation:
Element Render() override {
2022-12-20 01:51:25 +08:00
const bool is_focused = Focused();
2023-05-02 19:32:37 +08:00
const auto focused =
(is_focused || hovered_) ? focusCursorBarBlinking : select;
auto transform = option_->transform ? option_->transform
: InputOption::Default().transform;
2021-07-10 18:29:39 +08:00
// placeholder.
2023-05-02 19:32:37 +08:00
if (content_->empty()) {
auto element = text(option_->placeholder()) | xflex | frame;
2022-03-31 08:17:43 +08:00
if (is_focused) {
element |= focus;
2022-03-31 08:17:43 +08:00
}
2023-05-02 19:32:37 +08:00
return transform({
std::move(element), hovered_, is_focused,
true // placeholder
}) |
reflect(box_);
}
Elements elements;
const std::vector<std::string> lines = Split(*content_);
2023-05-02 19:32:37 +08:00
int& cursor_position = option_->cursor_position();
cursor_position = util::clamp(cursor_position, 0, (int)content_->size());
// Find the line and index of the cursor.
int cursor_line = 0;
int cursor_char_index = cursor_position;
for (const auto& line : lines) {
if (cursor_char_index <= (int)line.size()) {
break;
2022-03-31 08:17:43 +08:00
}
2023-05-02 19:32:37 +08:00
cursor_char_index -= line.size() + 1;
cursor_line++;
2021-07-10 18:29:39 +08:00
}
2019-01-20 05:06:05 +08:00
2023-05-02 19:32:37 +08:00
if (lines.empty()) {
elements.push_back(text("") | focused);
}
elements.reserve(lines.size());
2023-05-02 19:32:37 +08:00
for (size_t i = 0; i < lines.size(); ++i) {
const std::string& line = lines[i];
// This is not the cursor line.
if (int(i) != cursor_line) {
elements.push_back(Text(line));
continue;
2022-12-20 01:51:25 +08:00
}
2023-05-02 19:32:37 +08:00
// The cursor is at the end of the line.
if (cursor_char_index >= (int)line.size()) {
elements.push_back(hbox({
Text(line),
text(" ") | focused | reflect(cursor_box_),
}) |
xflex);
continue;
}
// The cursor is on this line.
const int glyph_start = cursor_char_index;
const int glyph_end = GlyphNext(line, glyph_start);
const std::string part_before_cursor = line.substr(0, glyph_start);
const std::string part_at_cursor =
line.substr(glyph_start, glyph_end - glyph_start);
const std::string part_after_cursor = line.substr(glyph_end);
auto element = hbox({
Text(part_before_cursor),
Text(part_at_cursor) | focused | reflect(cursor_box_),
Text(part_after_cursor),
}) |
xflex;
elements.push_back(element);
}
auto element = vbox(std::move(elements)) | frame;
return transform({
std::move(element), hovered_, is_focused,
false // placeholder
}) |
2023-05-02 19:32:37 +08:00
xflex | reflect(box_);
2021-07-10 18:29:39 +08:00
}
2023-05-02 19:32:37 +08:00
Element Text(const std::string& input) {
if (!option_->password()) {
return text(input);
}
2023-05-02 19:32:37 +08:00
std::string out;
out.reserve(10 + input.size() * 3 / 2);
for (size_t i = 0; i < input.size(); ++i) {
out += "";
2022-03-31 08:17:43 +08:00
}
2023-05-02 19:32:37 +08:00
return text(out);
}
2018-10-19 04:58:38 +08:00
2023-05-02 19:32:37 +08:00
bool HandleBackspace() {
int& cursor_position = option_->cursor_position();
if (cursor_position == 0) {
return false;
2021-07-10 18:29:39 +08:00
}
2023-05-02 19:32:37 +08:00
const size_t start = GlyphPrevious(content_(), cursor_position);
const size_t end = cursor_position;
content_->erase(start, end - start);
cursor_position = start;
return true;
}
2021-07-10 18:29:39 +08:00
2023-05-02 19:32:37 +08:00
bool HandleDelete() {
int& cursor_position = option_->cursor_position();
if (cursor_position == (int)content_->size()) {
return false;
2021-07-10 18:29:39 +08:00
}
2023-05-02 19:32:37 +08:00
const size_t start = cursor_position;
const size_t end = GlyphNext(content_(), cursor_position);
content_->erase(start, end - start);
return true;
}
2021-07-10 18:29:39 +08:00
2023-05-02 19:32:37 +08:00
bool HandleArrowLeft() {
int& cursor_position = option_->cursor_position();
if (cursor_position == 0) {
return false;
2021-07-10 18:29:39 +08:00
}
2018-10-19 04:58:38 +08:00
2023-05-02 19:32:37 +08:00
cursor_position = GlyphPrevious(content_(), cursor_position);
return true;
}
bool HandleArrowRight() {
int& cursor_position = option_->cursor_position();
if (cursor_position == (int)content_->size()) {
return false;
2021-07-10 18:29:39 +08:00
}
2023-05-02 19:32:37 +08:00
cursor_position = GlyphNext(content_(), cursor_position);
return true;
}
size_t CursorColumn() {
int& cursor_position = option_->cursor_position();
size_t iter = cursor_position;
int width = 0;
while (true) {
if (iter == 0) {
break;
}
iter = GlyphPrevious(content_(), iter);
if (content_()[iter] == '\n') {
break;
}
width += GlyphWidth(content_(), iter);
2021-07-10 18:29:39 +08:00
}
2023-05-02 19:32:37 +08:00
return width;
}
2018-10-19 04:58:38 +08:00
2023-05-02 19:32:37 +08:00
// Move the cursor `columns` on the right, if possible.
void MoveCursorColumn(int columns) {
int& cursor_position = option_->cursor_position();
while (columns > 0) {
if (cursor_position == (int)content_().size() ||
content_()[cursor_position] == '\n') {
return;
}
columns -= GlyphWidth(content_(), cursor_position);
cursor_position = GlyphNext(content_(), cursor_position);
2021-07-10 18:29:39 +08:00
}
2023-05-02 19:32:37 +08:00
}
2019-01-27 09:33:06 +08:00
2023-05-02 19:32:37 +08:00
bool HandleArrowUp() {
int& cursor_position = option_->cursor_position();
if (cursor_position == 0) {
return false;
}
2023-05-02 19:32:37 +08:00
size_t columns = CursorColumn();
// Move cursor at the beginning of 2 lines above.
while (true) {
if (cursor_position == 0) {
return true;
}
size_t previous = GlyphPrevious(content_(), cursor_position);
if (content_()[previous] == '\n') {
break;
}
cursor_position = previous;
}
cursor_position = GlyphPrevious(content_(), cursor_position);
while (true) {
if (cursor_position == 0) {
break;
}
size_t previous = GlyphPrevious(content_(), cursor_position);
if (content_()[previous] == '\n') {
break;
}
cursor_position = previous;
}
2023-05-02 19:32:37 +08:00
MoveCursorColumn(columns);
return true;
}
bool HandleArrowDown() {
int& cursor_position = option_->cursor_position();
if (cursor_position == (int)content_->size()) {
return false;
2021-07-10 18:29:39 +08:00
}
2018-10-21 20:18:11 +08:00
2023-05-02 19:32:37 +08:00
size_t columns = CursorColumn();
// Move cursor at the beginning of the next line
while (true) {
if (content_()[cursor_position] == '\n') {
break;
}
cursor_position = GlyphNext(content_(), cursor_position);
if (cursor_position == (int)content_().size()) {
return true;
}
}
cursor_position = GlyphNext(content_(), cursor_position);
MoveCursorColumn(columns);
return true;
}
bool HandleHome() {
int& cursor_position = option_->cursor_position();
cursor_position = 0;
return true;
}
bool HandleEnd() {
int& cursor_position = option_->cursor_position();
cursor_position = content_->size();
return true;
}
bool HandleReturn() {
int& cursor_position = option_->cursor_position();
content_->insert(cursor_position, "\n");
cursor_position++;
option_->on_change();
return true;
}
bool HandleCharacter(const std::string& character) {
if (character == "\n" && !option_->multiline()) {
option_->on_enter();
return false;
2021-07-10 18:29:39 +08:00
}
2018-10-21 20:18:11 +08:00
2023-05-02 19:32:37 +08:00
int& cursor_position = option_->cursor_position();
content_->insert(cursor_position, character);
cursor_position += character.size();
option_->on_change();
if (character == "\n") {
option_->on_enter();
}
return true;
}
bool OnEvent(Event event) override {
int& cursor_position = option_->cursor_position();
cursor_position = util::clamp(cursor_position, 0, (int)content_->size());
2021-07-10 18:29:39 +08:00
if (event.is_character()) {
2023-05-02 19:32:37 +08:00
return HandleCharacter(event.character());
}
if (event.is_mouse()) {
return HandleMouse(event);
}
if (event == Event::Backspace) {
return HandleBackspace();
}
if (event == Event::Delete) {
return HandleDelete();
}
if (event == Event::ArrowLeft) {
return HandleArrowLeft();
}
if (event == Event::ArrowRight) {
return HandleArrowRight();
}
if (event == Event::ArrowUp) {
return HandleArrowUp();
2021-07-10 18:29:39 +08:00
}
2023-05-02 19:32:37 +08:00
if (event == Event::ArrowDown) {
return HandleArrowDown();
}
if (event == Event::Home) {
return HandleHome();
}
if (event == Event::End) {
return HandleEnd();
}
if (event == Event::ArrowLeftCtrl) {
return HandleLeftCtrl();
}
if (event == Event::ArrowRightCtrl) {
return HandleRightCtrl();
}
if (event == Event::Return) {
return HandleReturn();
}
2021-07-10 18:29:39 +08:00
return false;
}
2023-05-02 19:32:37 +08:00
bool HandleLeftCtrl() {
int& cursor_position = option_->cursor_position();
if (cursor_position == 0) {
return false;
}
2023-05-02 19:32:37 +08:00
// Move left, as long as left it not a word.
while (cursor_position) {
size_t previous = GlyphPrevious(content_(), cursor_position);
if (IsWordCharacter(content_(), previous)) {
break;
}
cursor_position = previous;
}
// Move left, as long as left is a word character:
2023-05-02 19:32:37 +08:00
while (cursor_position) {
size_t previous = GlyphPrevious(content_(), cursor_position);
if (!IsWordCharacter(content_(), previous)) {
break;
}
cursor_position = previous;
}
2023-05-02 19:32:37 +08:00
return true;
}
2023-05-02 19:32:37 +08:00
bool HandleRightCtrl() {
int& cursor_position = option_->cursor_position();
if (cursor_position == (int)content_().size()) {
return false;
}
2023-05-02 19:32:37 +08:00
// Move right, until entering a word.
while (cursor_position < (int)content_().size()) {
cursor_position = GlyphNext(content_(), cursor_position);
if (IsWordCharacter(content_(), cursor_position)) {
break;
}
}
// Move right, as long as right is a word character:
2023-05-02 19:32:37 +08:00
while (cursor_position < (int)content_().size()) {
size_t next = GlyphNext(content_(), cursor_position);
if (!IsWordCharacter(content_(), cursor_position)) {
break;
}
cursor_position = next;
}
2023-05-02 19:32:37 +08:00
return true;
}
2023-05-02 19:32:37 +08:00
bool HandleMouse(Event event) {
hovered_ = box_.Contain(event.mouse().x, //
event.mouse().y) &&
CaptureMouse(event);
2022-03-31 08:17:43 +08:00
if (!hovered_) {
2021-07-10 18:29:39 +08:00
return false;
2022-03-31 08:17:43 +08:00
}
if (event.mouse().button != Mouse::Left ||
event.mouse().motion != Mouse::Pressed) {
2021-07-10 18:29:39 +08:00
return false;
}
2021-07-10 18:29:39 +08:00
TakeFocus();
2023-05-02 19:32:37 +08:00
2022-03-31 08:17:43 +08:00
if (content_->empty()) {
2023-05-02 19:32:37 +08:00
option_->cursor_position() = 0;
return true;
2022-03-31 08:17:43 +08:00
}
2023-05-02 19:32:37 +08:00
// Find the line and index of the cursor.
std::vector<std::string> lines = Split(*content_);
int& cursor_position = option_->cursor_position();
int cursor_line = 0;
int cursor_char_index = cursor_position;
for (const auto& line : lines) {
if (cursor_char_index <= (int)line.size()) {
break;
}
2023-05-02 19:32:37 +08:00
cursor_char_index -= line.size() + 1;
cursor_line++;
}
2023-05-02 19:32:37 +08:00
int cursor_column =
string_width(lines[cursor_line].substr(0, cursor_char_index));
int new_cursor_column = cursor_column + event.mouse().x - cursor_box_.x_min;
int new_cursor_line = cursor_line + event.mouse().y - cursor_box_.y_min;
// Fix the new cursor position:
new_cursor_line = std::max(std::min(new_cursor_line, (int)lines.size()), 0);
std::string empty_string;
const std::string& line = new_cursor_line < (int)lines.size()
? lines[new_cursor_line]
: empty_string;
new_cursor_column = util::clamp(new_cursor_column, 0, string_width(line));
if (new_cursor_column == cursor_column && //
new_cursor_line == cursor_line) {
return false;
2022-03-31 08:17:43 +08:00
}
2023-05-02 19:32:37 +08:00
// Convert back the new_cursor_{line,column} toward cursor_position:
cursor_position = 0;
for (int i = 0; i < new_cursor_line; ++i) {
cursor_position += lines[i].size() + 1;
}
while (new_cursor_column > 0) {
new_cursor_column -= GlyphWidth(content_(), cursor_position);
cursor_position = GlyphNext(content_(), cursor_position);
2021-07-10 18:29:39 +08:00
}
2023-05-02 19:32:37 +08:00
option_->on_change();
2018-10-19 04:58:38 +08:00
return true;
}
2021-08-06 04:40:40 +08:00
2021-08-07 02:32:33 +08:00
bool Focusable() const final { return true; }
2021-08-06 04:40:40 +08:00
bool hovered_ = false;
StringRef content_;
2018-10-19 04:58:38 +08:00
2021-09-08 15:36:37 +08:00
Box box_;
2021-07-10 18:29:39 +08:00
Box cursor_box_;
Ref<InputOption> option_;
};
} // namespace
2021-07-10 18:29:39 +08:00
/// @brief An input box for editing text.
/// @param content The editable content.
2021-07-10 20:23:46 +08:00
/// @param option Additional optional parameters.
2021-07-10 18:29:39 +08:00
/// @ingroup component
/// @see InputBase
///
/// ### Example
///
/// ```cpp
/// auto screen = ScreenInteractive::FitComponent();
/// std::string content= "";
/// std::string placeholder = "placeholder";
2021-07-10 18:29:39 +08:00
/// Component input = Input(&content, &placeholder);
/// screen.Loop(input);
/// ```
///
/// ### Output
///
/// ```bash
/// placeholder
/// ```
2023-05-02 19:32:37 +08:00
Component Input(StringRef content, Ref<InputOption> option) {
return Make<InputBase>(std::move(content), std::move(option));
}
2021-07-10 18:29:39 +08:00
Component Input(StringRef content,
2023-05-02 19:32:37 +08:00
StringRef placeholder,
2021-07-10 18:29:39 +08:00
Ref<InputOption> option) {
2023-05-02 19:32:37 +08:00
option->placeholder = placeholder;
return Make<InputBase>(std::move(content), std::move(option));
}
} // namespace ftxui
2023-05-02 19:32:37 +08:00
// Copyright 2022 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.