Featre: Support ctrl+arrow in input. (#494)

CTRL+LEFT: Move the cursor to the beginning of the word.
CTRL+RIGHT: Move the cursor to the beginning of the word.

This was requested by:
https://github.com/ArthurSonzogni/FTXUI/issues/490
This commit is contained in:
Arthur Sonzogni 2022-10-06 21:16:55 +02:00 committed by GitHub
parent ccfe22bc24
commit f4b47333be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1776 additions and 250 deletions

View File

@ -20,6 +20,7 @@ current (development)
- multiple directions. - multiple directions.
- multiple colors. - multiple colors.
- various values (value, min, max, increment). - various values (value, min, max, increment).
- Feature: `Input` supports CTRL+Left and CTRL+Right
- Improvement: The `Menu` keeps the focus when an entry is selected with the - Improvement: The `Menu` keeps the focus when an entry is selected with the
mouse. mouse.
- Bug: Add implementation of `ButtonOption::Border()`. It was missing. - Bug: Add implementation of `ButtonOption::Border()`. It was missing.

View File

@ -38,6 +38,11 @@ struct Event {
static const Event ArrowUp; static const Event ArrowUp;
static const Event ArrowDown; static const Event ArrowDown;
static const Event ArrowLeftCtrl;
static const Event ArrowRightCtrl;
static const Event ArrowUpCtrl;
static const Event ArrowDownCtrl;
// --- Other --- // --- Other ---
static const Event Backspace; static const Event Backspace;
static const Event Delete; static const Event Delete;

View File

@ -26,6 +26,33 @@ int GlyphPosition(const std::string& input,
// Returns the number of glyphs in |input|. // Returns the number of glyphs in |input|.
int GlyphCount(const std::string& input); int GlyphCount(const std::string& input);
// Properties from:
// https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/WordBreakProperty.txt
enum class WordBreakProperty {
ALetter,
CR,
Double_Quote,
Extend,
ExtendNumLet,
Format,
Hebrew_Letter,
Katakana,
LF,
MidLetter,
MidNum,
MidNumLet,
Newline,
Numeric,
Regional_Indicator,
Single_Quote,
WSegSpace,
ZWJ,
};
std::vector<WordBreakProperty> Utf8ToWordBreakProperty(
const std::string& input);
bool IsWordBreakingCharacter(const std::string& input, size_t glyph_index);
// Map every cells drawn by |input| to their corresponding Glyphs. Half-size // Map every cells drawn by |input| to their corresponding Glyphs. Half-size
// Glyphs takes one cell, full-size Glyphs take two cells. // Glyphs takes one cell, full-size Glyphs take two cells.
std::vector<int> CellToGlyphIndex(const std::string& input); std::vector<int> CellToGlyphIndex(const std::string& input);

View File

@ -51,26 +51,30 @@ Event Event::CursorReporting(std::string input, int x, int y) {
} }
// --- Arrow --- // --- Arrow ---
const Event Event::ArrowLeft = Event::Special("\x1B[D"); // NOLINT const Event Event::ArrowLeft = Event::Special("\x1B[D"); // NOLINT
const Event Event::ArrowRight = Event::Special("\x1B[C"); // NOLINT const Event Event::ArrowRight = Event::Special("\x1B[C"); // NOLINT
const Event Event::ArrowUp = Event::Special("\x1B[A"); // NOLINT const Event Event::ArrowUp = Event::Special("\x1B[A"); // NOLINT
const Event Event::ArrowDown = Event::Special("\x1B[B"); // NOLINT const Event Event::ArrowDown = Event::Special("\x1B[B"); // NOLINT
const Event Event::Backspace = Event::Special({127}); // NOLINT const Event Event::ArrowLeftCtrl = Event::Special("\x1B[1;5D"); // NOLINT
const Event Event::Delete = Event::Special("\x1B[3~"); // NOLINT const Event Event::ArrowRightCtrl = Event::Special("\x1B[1;5C"); // NOLINT
const Event Event::Escape = Event::Special("\x1B"); // NOLINT const Event Event::ArrowUpCtrl = Event::Special("\x1B[1;5A"); // NOLINT
const Event Event::Return = Event::Special({10}); // NOLINT const Event Event::ArrowDownCtrl = Event::Special("\x1B[1;5B"); // NOLINT
const Event Event::Tab = Event::Special({9}); // NOLINT const Event Event::Backspace = Event::Special({127}); // NOLINT
const Event Event::TabReverse = Event::Special({27, 91, 90}); // NOLINT const Event Event::Delete = Event::Special("\x1B[3~"); // NOLINT
const Event Event::F1 = Event::Special("\x1B[OP"); // NOLINT const Event Event::Escape = Event::Special("\x1B"); // NOLINT
const Event Event::F2 = Event::Special("\x1B[OQ"); // NOLINT const Event Event::Return = Event::Special({10}); // NOLINT
const Event Event::F3 = Event::Special("\x1B[OR"); // NOLINT const Event Event::Tab = Event::Special({9}); // NOLINT
const Event Event::F4 = Event::Special("\x1B[OS"); // NOLINT const Event Event::TabReverse = Event::Special({27, 91, 90}); // NOLINT
const Event Event::F5 = Event::Special("\x1B[15~"); // NOLINT const Event Event::F1 = Event::Special("\x1B[OP"); // NOLINT
const Event Event::F6 = Event::Special("\x1B[17~"); // NOLINT const Event Event::F2 = Event::Special("\x1B[OQ"); // NOLINT
const Event Event::F7 = Event::Special("\x1B[18~"); // NOLINT const Event Event::F3 = Event::Special("\x1B[OR"); // NOLINT
const Event Event::F8 = Event::Special("\x1B[19~"); // NOLINT const Event Event::F4 = Event::Special("\x1B[OS"); // NOLINT
const Event Event::F9 = Event::Special("\x1B[20~"); // NOLINT const Event Event::F5 = Event::Special("\x1B[15~"); // NOLINT
const Event Event::F10 = Event::Special("\x1B[21~"); // NOLINT const Event Event::F6 = Event::Special("\x1B[17~"); // NOLINT
const Event Event::F7 = Event::Special("\x1B[18~"); // NOLINT
const Event Event::F8 = Event::Special("\x1B[19~"); // NOLINT
const Event Event::F9 = Event::Special("\x1B[20~"); // NOLINT
const Event Event::F10 = Event::Special("\x1B[21~"); // NOLINT
const Event Event::F11 = Event::Special("\x1B[21~"); // Doesn't exist // NOLINT const Event Event::F11 = Event::Special("\x1B[21~"); // Doesn't exist // NOLINT
const Event Event::F12 = Event::Special("\x1B[24~"); // NOLINT const Event Event::F12 = Event::Special("\x1B[24~"); // NOLINT
const Event Event::Home = Event::Special({27, 91, 72}); // NOLINT const Event Event::Home = Event::Special({27, 91, 72}); // NOLINT

View File

@ -22,6 +22,36 @@ namespace ftxui {
namespace { namespace {
// Group together several propertiej so they appear to form a similar group.
// For instance, letters are grouped with number and form a single word.
bool IsWordCharacter(WordBreakProperty property) {
switch (property) {
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:
// Unsure:
case WordBreakProperty::Extend:
case WordBreakProperty::ExtendNumLet:
case WordBreakProperty::Format:
case WordBreakProperty::Regional_Indicator:
case WordBreakProperty::ZWJ:
return false;
};
return true; // NOT_REACHED();
};
std::string PasswordField(size_t size) { std::string PasswordField(size_t size) {
std::string out; std::string out;
out.reserve(2 * size); out.reserve(2 * size);
@ -111,7 +141,6 @@ class InputBase : public ComponentBase {
if (event.is_mouse()) { if (event.is_mouse()) {
return OnMouseEvent(event); return OnMouseEvent(event);
} }
std::string c; std::string c;
// Backspace. // Backspace.
@ -149,6 +178,7 @@ class InputBase : public ComponentBase {
return false; return false;
} }
// Arrow
if (event == Event::ArrowLeft && cursor_position() > 0) { if (event == Event::ArrowLeft && cursor_position() > 0) {
cursor_position()--; cursor_position()--;
return true; return true;
@ -160,6 +190,16 @@ class InputBase : public ComponentBase {
return true; return true;
} }
// CTRL + Arrow:
if (event == Event::ArrowLeftCtrl) {
HandleLeftCtrl();
return true;
}
if (event == Event::ArrowRightCtrl) {
HandleRightCtrl();
return true;
}
if (event == Event::Home) { if (event == Event::Home) {
cursor_position() = 0; cursor_position() = 0;
return true; return true;
@ -182,6 +222,39 @@ class InputBase : public ComponentBase {
} }
private: private:
void HandleLeftCtrl() {
auto properties = Utf8ToWordBreakProperty(*content_);
// Move left, as long as left is not a word character.
while (cursor_position() > 0 &&
!IsWordCharacter(properties[cursor_position() - 1])) {
cursor_position()--;
}
// Move left, as long as left is a word character:
while (cursor_position() > 0 &&
IsWordCharacter(properties[cursor_position() - 1])) {
cursor_position()--;
}
}
void HandleRightCtrl() {
auto properties = Utf8ToWordBreakProperty(*content_);
int max = (int)properties.size();
// Move right, as long as right is not a word character.
while (cursor_position() < max &&
!IsWordCharacter(properties[cursor_position()])) {
cursor_position()++;
}
// Move right, as long as right is a word character:
while (cursor_position() < max &&
IsWordCharacter(properties[cursor_position()])) {
cursor_position()++;
}
}
bool OnMouseEvent(Event event) { bool OnMouseEvent(Event event) {
hovered_ = hovered_ =
box_.Contain(event.mouse().x, event.mouse().y) && CaptureMouse(event); box_.Contain(event.mouse().x, event.mouse().y) && CaptureMouse(event);

View File

@ -369,6 +369,124 @@ TEST(InputTest, MouseClickComplex) {
EXPECT_EQ(option.cursor_position(), 4u); EXPECT_EQ(option.cursor_position(), 4u);
} }
TEST(InputTest, CtrlArrowLeft) {
std::string content = "word word 测ord wo测d word";
// 0 5 10 15 20
std::string placeholder;
auto option = InputOption();
option.cursor_position = 22;
auto input = Input(&content, &placeholder, &option);
// Use CTRL+Left several time
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 20u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 15u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 10u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 5u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 0u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 0u);
}
TEST(InputTest, CtrlArrowLeft2) {
std::string content = " word word 测ord wo测d word ";
// 0 3 6 9 12 15 18 21 24 27 30 33
std::string placeholder;
auto option = InputOption();
option.cursor_position = 33;
auto input = Input(&content, &placeholder, &option);
// Use CTRL+Left several time
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 27u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 21u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 15u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 9u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 3u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 0u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 0u);
}
TEST(InputTest, CtrlArrowRight) {
std::string content = "word word 测ord wo测d word";
// 0 5 10 15 20
std::string placeholder;
auto option = InputOption();
option.cursor_position = 2;
auto input = Input(&content, &placeholder, &option);
// Use CTRL+Left several time
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 4);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 9);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 14u);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 19u);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 24u);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 24u);
}
TEST(InputTest, CtrlArrowRight2) {
std::string content = " word word 测ord wo测d word ";
// 0 3 6 9 12 15 18 21 24 27 30 33
std::string placeholder;
auto option = InputOption();
option.cursor_position = 0;
auto input = Input(&content, &placeholder, &option);
// Use CTRL+Left several time
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 7u);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 13u);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 19u);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 25u);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 31u);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 34u);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 34u);
}
} // namespace ftxui } // namespace ftxui
// Copyright 2021 Arthur Sonzogni. All rights reserved. // Copyright 2021 Arthur Sonzogni. All rights reserved.

File diff suppressed because it is too large Load Diff

View File

@ -122,6 +122,22 @@ TEST(StringTest, CellToGlyphIndex) {
EXPECT_EQ(combining[2], 2); EXPECT_EQ(combining[2], 2);
} }
TEST(StringTest, Utf8ToWordBreakProperty) {
using T = std::vector<WordBreakProperty>;
using P = WordBreakProperty;
EXPECT_EQ(Utf8ToWordBreakProperty("a"), T({P::ALetter}));
EXPECT_EQ(Utf8ToWordBreakProperty("0"), T({P::Numeric}));
EXPECT_EQ(Utf8ToWordBreakProperty("א"), T({P::Hebrew_Letter}));
EXPECT_EQ(Utf8ToWordBreakProperty(""), T({P::Katakana}));
EXPECT_EQ(Utf8ToWordBreakProperty(" "), T({P::WSegSpace}));
EXPECT_EQ(Utf8ToWordBreakProperty("\""), T({P::Double_Quote}));
EXPECT_EQ(Utf8ToWordBreakProperty("'"), T({P::Single_Quote}));
EXPECT_EQ(Utf8ToWordBreakProperty(":"), T({P::MidLetter}));
EXPECT_EQ(Utf8ToWordBreakProperty("."), T({P::MidNumLet}));
EXPECT_EQ(Utf8ToWordBreakProperty("\r"), T({})); // FIXME
EXPECT_EQ(Utf8ToWordBreakProperty("\n"), T({})); // FIXME
}
} // namespace ftxui } // namespace ftxui
// Copyright 2020 Arthur Sonzogni. All rights reserved. // Copyright 2020 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in // Use of this source code is governed by the MIT license that can be found in