mirror of
https://github.com/ArthurSonzogni/FTXUI.git
synced 2024-11-25 20:27:31 +08:00
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:
parent
ccfe22bc24
commit
f4b47333be
@ -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.
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user