mirror of
https://github.com/ArthurSonzogni/FTXUI.git
synced 2024-11-22 18:59:59 +08:00
Bugfix Input use std::string (#279)
Use std::string by default for the implementation of FTXUI's input component. Along the way: - Give a correct implementation for fullwidth characters. - Add tests - Modify the way the cursor is drawn.
This commit is contained in:
parent
602392c43d
commit
52276c8a2b
13
CHANGELOG.md
13
CHANGELOG.md
@ -5,6 +5,8 @@ unreleased (development)
|
||||
------------------------
|
||||
|
||||
### Features:
|
||||
|
||||
#### DOM:
|
||||
- Support `flexbox` dom elements. This is build symmetrically to the HTML one.
|
||||
All the following attributes are supported: direction, wrap, justify-content,
|
||||
align-items, align-content, gap
|
||||
@ -16,12 +18,15 @@ unreleased (development)
|
||||
- `paragraphAlignJustify`
|
||||
- Add the helper elements based on `flexbox`: `hflow()`, `vflow()`.
|
||||
|
||||
### Bug
|
||||
|
||||
#### Component
|
||||
- `Input` shouldn't take focus when hovered by the mouse.
|
||||
- Modifying `Input`'s during on_enter/on_change event is now working correctly.
|
||||
|
||||
### Breaking changes:
|
||||
- The behavior of `paragraph` has been modified. It now returns en Element,
|
||||
instead of a list of elements.
|
||||
|
||||
### Bug
|
||||
- Input shouldn't take focus when hovered by the mouse.
|
||||
instead of a list of elements.
|
||||
|
||||
0.11.1
|
||||
------
|
||||
|
@ -14,7 +14,20 @@ std::wstring to_wstring(T s) {
|
||||
}
|
||||
|
||||
int string_width(const std::string&);
|
||||
// Split the string into a its glyphs. An empty one is inserted ater fullwidth
|
||||
// ones.
|
||||
std::vector<std::string> Utf8ToGlyphs(const std::string& input);
|
||||
// If |input| was an array of glyphs, this returns the number of char to eat
|
||||
// before reaching the glyph at index |glyph_index|.
|
||||
int GlyphPosition(const std::string& input,
|
||||
size_t glyph_index,
|
||||
size_t start = 0);
|
||||
// Returns the number of glyphs in |input|.
|
||||
int GlyphCount(const std::string& input);
|
||||
|
||||
// Map every cells drawn by |input| to their corresponding Glyphs. Half-size
|
||||
// Glyphs takes one cell, full-size Glyphs take two cells.
|
||||
std::vector<int> CellToGlyphIndex(const std::string& input);
|
||||
|
||||
} // namespace ftxui
|
||||
|
||||
|
@ -20,12 +20,22 @@
|
||||
|
||||
namespace ftxui {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string PasswordField(int size) {
|
||||
std::string out;
|
||||
out.reserve(2 * size);
|
||||
while (size--)
|
||||
out += "•";
|
||||
return out;
|
||||
}
|
||||
|
||||
// An input box. The user can type text into it.
|
||||
class WideInputBase : public ComponentBase {
|
||||
class InputBase : public ComponentBase {
|
||||
public:
|
||||
WideInputBase(WideStringRef content,
|
||||
ConstStringRef placeholder,
|
||||
Ref<InputOption> option)
|
||||
InputBase(StringRef content,
|
||||
ConstStringRef placeholder,
|
||||
Ref<InputOption> option)
|
||||
: content_(content), placeholder_(placeholder), option_(option) {}
|
||||
|
||||
int cursor_position_internal_ = 0;
|
||||
@ -38,22 +48,23 @@ class WideInputBase : public ComponentBase {
|
||||
|
||||
// Component implementation:
|
||||
Element Render() override {
|
||||
std::wstring password_content;
|
||||
std::string password_content;
|
||||
if (option_->password())
|
||||
password_content = std::wstring(content_->size(), U'•');
|
||||
std::wstring& content = option_->password() ? password_content : *content_;
|
||||
password_content = PasswordField(content_->size());
|
||||
std::string& content = option_->password() ? password_content : *content_;
|
||||
|
||||
cursor_position() =
|
||||
std::max(0, std::min<int>(content.size(), cursor_position()));
|
||||
auto main_decorator = flex | size(HEIGHT, EQUAL, 1);
|
||||
int size = GlyphCount(content);
|
||||
|
||||
cursor_position() = std::max(0, std::min<int>(size, cursor_position()));
|
||||
auto main_decorator = flex | ftxui::size(HEIGHT, EQUAL, 1);
|
||||
bool is_focused = Focused();
|
||||
|
||||
// placeholder.
|
||||
if (content.size() == 0) {
|
||||
if (size == 0) {
|
||||
bool hovered = hovered_;
|
||||
Decorator decorator = dim | main_decorator;
|
||||
if (is_focused)
|
||||
decorator = decorator | focus | bold;
|
||||
decorator = decorator | focus | inverted;
|
||||
if (hovered || is_focused)
|
||||
decorator = decorator | inverted;
|
||||
return text(*placeholder_) | decorator | reflect(box_);
|
||||
@ -67,22 +78,22 @@ class WideInputBase : public ComponentBase {
|
||||
return text(content) | main_decorator | reflect(box_);
|
||||
}
|
||||
|
||||
std::wstring part_before_cursor = content.substr(0, cursor_position());
|
||||
std::wstring part_at_cursor = cursor_position() < (int)content.size()
|
||||
? content.substr(cursor_position(), 1)
|
||||
: L" ";
|
||||
std::wstring part_after_cursor = cursor_position() < (int)content.size() - 1
|
||||
? content.substr(cursor_position() + 1)
|
||||
: L"";
|
||||
int index_before_cursor = GlyphPosition(content, cursor_position());
|
||||
int index_after_cursor = GlyphPosition(content, 1, index_before_cursor);
|
||||
std::string part_before_cursor = content.substr(0, index_before_cursor);
|
||||
std::string part_at_cursor = " ";
|
||||
if (cursor_position() < size) {
|
||||
part_at_cursor = content.substr(index_before_cursor,
|
||||
index_after_cursor - index_before_cursor);
|
||||
}
|
||||
std::string part_after_cursor = content.substr(index_after_cursor);
|
||||
auto focused = (is_focused || hovered_) ? focus : select;
|
||||
// clang-format off
|
||||
return
|
||||
hbox(
|
||||
text(part_before_cursor),
|
||||
text(part_at_cursor) | underlined | focused | reflect(cursor_box_),
|
||||
text(part_after_cursor)
|
||||
) | flex | inverted | frame | bold |main_decorator | reflect(box_);
|
||||
// clang-format on
|
||||
return hbox({
|
||||
text(part_before_cursor),
|
||||
text(part_at_cursor) | focused | inverted | reflect(cursor_box_),
|
||||
text(part_after_cursor),
|
||||
}) |
|
||||
flex | frame | bold | main_decorator | reflect(box_);
|
||||
}
|
||||
|
||||
bool OnEvent(Event event) override {
|
||||
@ -92,13 +103,15 @@ class WideInputBase : public ComponentBase {
|
||||
if (event.is_mouse())
|
||||
return OnMouseEvent(event);
|
||||
|
||||
std::wstring c;
|
||||
std::string c;
|
||||
|
||||
// Backspace.
|
||||
if (event == Event::Backspace) {
|
||||
if (cursor_position() == 0)
|
||||
return false;
|
||||
content_->erase(cursor_position() - 1, 1);
|
||||
size_t start = GlyphPosition(*content_, cursor_position() - 1);
|
||||
size_t end = GlyphPosition(*content_, cursor_position());
|
||||
content_->erase(start, end - start);
|
||||
cursor_position()--;
|
||||
option_->on_change();
|
||||
return true;
|
||||
@ -108,7 +121,9 @@ class WideInputBase : public ComponentBase {
|
||||
if (event == Event::Delete) {
|
||||
if (cursor_position() == int(content_->size()))
|
||||
return false;
|
||||
content_->erase(cursor_position(), 1);
|
||||
size_t start = GlyphPosition(*content_, cursor_position());
|
||||
size_t end = GlyphPosition(*content_, cursor_position() + 1);
|
||||
content_->erase(start, end - start);
|
||||
option_->on_change();
|
||||
return true;
|
||||
}
|
||||
@ -140,13 +155,14 @@ class WideInputBase : public ComponentBase {
|
||||
}
|
||||
|
||||
if (event == Event::End) {
|
||||
cursor_position() = (int)content_->size();
|
||||
cursor_position() = GlyphCount(*content_);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Content
|
||||
if (event.is_character()) {
|
||||
content_->insert(cursor_position(), 1, to_wstring(event.character())[0]);
|
||||
size_t start = GlyphPosition(*content_, cursor_position());
|
||||
content_->insert(start, event.character());
|
||||
cursor_position()++;
|
||||
option_->on_change();
|
||||
return true;
|
||||
@ -167,12 +183,27 @@ class WideInputBase : public ComponentBase {
|
||||
}
|
||||
|
||||
TakeFocus();
|
||||
int new_cursor_position =
|
||||
cursor_position() + event.mouse().x - cursor_box_.x_min;
|
||||
new_cursor_position =
|
||||
std::max(0, std::min<int>(content_->size(), new_cursor_position));
|
||||
if (cursor_position() != new_cursor_position) {
|
||||
cursor_position() = new_cursor_position;
|
||||
if (content_->size() == 0)
|
||||
return true;
|
||||
|
||||
auto mapping = CellToGlyphIndex(*content_);
|
||||
int original_glyph = cursor_position();
|
||||
original_glyph = std::clamp(original_glyph, 0, int(mapping.size()));
|
||||
int original_cell = 0;
|
||||
for (size_t i = 0; i < mapping.size(); i++) {
|
||||
if (mapping[i] == original_glyph) {
|
||||
original_cell = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mapping[original_cell] != original_glyph)
|
||||
original_cell = mapping.size();
|
||||
int target_cell = original_cell + event.mouse().x - cursor_box_.x_min;
|
||||
int target_glyph = target_cell < (int)mapping.size() ? mapping[target_cell]
|
||||
: (int)mapping.size();
|
||||
target_glyph = std::clamp(target_glyph, 0, GlyphCount(*content_));
|
||||
if (cursor_position() != target_glyph) {
|
||||
cursor_position() = target_glyph;
|
||||
option_->on_change();
|
||||
}
|
||||
return true;
|
||||
@ -181,7 +212,7 @@ class WideInputBase : public ComponentBase {
|
||||
bool Focusable() const final { return true; }
|
||||
|
||||
bool hovered_ = false;
|
||||
WideStringRef content_;
|
||||
StringRef content_;
|
||||
ConstStringRef placeholder_;
|
||||
|
||||
Box box_;
|
||||
@ -190,39 +221,37 @@ class WideInputBase : public ComponentBase {
|
||||
};
|
||||
|
||||
// An input box. The user can type text into it.
|
||||
// For convenience, the std::string version of Input simply wrap a
|
||||
// WideInputBase.
|
||||
// TODO(arthursonzogni): Provide an implementation handling std::string natively
|
||||
// and adds better support for combining characters.
|
||||
class InputBase : public WideInputBase {
|
||||
// For convenience, the std::wstring version of Input simply wrap a
|
||||
// InputBase.
|
||||
class WideInputBase : public InputBase {
|
||||
public:
|
||||
InputBase(StringRef content,
|
||||
ConstStringRef placeholder,
|
||||
Ref<InputOption> option)
|
||||
: WideInputBase(&wrapped_content_,
|
||||
std::move(placeholder),
|
||||
std::move(option)),
|
||||
WideInputBase(WideStringRef content,
|
||||
ConstStringRef placeholder,
|
||||
Ref<InputOption> option)
|
||||
: InputBase(&wrapped_content_, std::move(placeholder), std::move(option)),
|
||||
content_(std::move(content)),
|
||||
wrapped_content_(to_wstring(*content_)) {}
|
||||
wrapped_content_(to_string(*content_)) {}
|
||||
|
||||
Element Render() override {
|
||||
wrapped_content_ = to_wstring(*content_);
|
||||
return WideInputBase::Render();
|
||||
wrapped_content_ = to_string(*content_);
|
||||
return InputBase::Render();
|
||||
}
|
||||
|
||||
bool OnEvent(Event event) override {
|
||||
wrapped_content_ = to_wstring(*content_);
|
||||
if (WideInputBase::OnEvent(event)) {
|
||||
*content_ = to_string(wrapped_content_);
|
||||
wrapped_content_ = to_string(*content_);
|
||||
if (InputBase::OnEvent(event)) {
|
||||
*content_ = to_wstring(wrapped_content_);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
StringRef content_;
|
||||
std::wstring wrapped_content_;
|
||||
WideStringRef content_;
|
||||
std::string wrapped_content_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
/// @brief An input box for editing text.
|
||||
/// @param content The editable content.
|
||||
/// @param placeholder The text displayed when content is still empty.
|
||||
|
@ -226,6 +226,151 @@ TEST(InputTest, Backspace) {
|
||||
EXPECT_EQ(option.cursor_position(), 0u);
|
||||
}
|
||||
|
||||
TEST(InputTest, MouseClick) {
|
||||
std::string content;
|
||||
std::string placeholder;
|
||||
auto option = InputOption();
|
||||
option.cursor_position = 0;
|
||||
auto input = Input(&content, &placeholder, &option);
|
||||
|
||||
input->OnEvent(Event::Character("a"));
|
||||
input->OnEvent(Event::Character("b"));
|
||||
input->OnEvent(Event::Character("c"));
|
||||
input->OnEvent(Event::Character("d"));
|
||||
|
||||
EXPECT_EQ(option.cursor_position(), 4u);
|
||||
|
||||
auto render = [&] {
|
||||
auto document = input->Render();
|
||||
auto screen = Screen::Create(Dimension::Fixed(10), Dimension::Fixed(1));
|
||||
Render(screen, document);
|
||||
};
|
||||
render();
|
||||
|
||||
Mouse mouse;
|
||||
mouse.button = Mouse::Button::Left;
|
||||
mouse.motion = Mouse::Motion::Pressed;
|
||||
mouse.y = 0;
|
||||
mouse.shift = false;
|
||||
mouse.meta = false;
|
||||
mouse.control = false;
|
||||
|
||||
mouse.x = 0;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 0u);
|
||||
|
||||
mouse.x = 2;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 2u);
|
||||
|
||||
mouse.x = 2;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 2u);
|
||||
|
||||
mouse.x = 1;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 1u);
|
||||
|
||||
mouse.x = 3;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 3u);
|
||||
|
||||
mouse.x = 4;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 4u);
|
||||
|
||||
mouse.x = 5;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 4u);
|
||||
}
|
||||
|
||||
TEST(InputTest, MouseClickComplex) {
|
||||
std::string content;
|
||||
std::string placeholder;
|
||||
auto option = InputOption();
|
||||
option.cursor_position = 0;
|
||||
auto input = Input(&content, &placeholder, &option);
|
||||
|
||||
input->OnEvent(Event::Character("测"));
|
||||
input->OnEvent(Event::Character("试"));
|
||||
input->OnEvent(Event::Character("a⃒"));
|
||||
input->OnEvent(Event::Character("ā"));
|
||||
|
||||
EXPECT_EQ(option.cursor_position(), 4u);
|
||||
|
||||
auto render = [&] {
|
||||
auto document = input->Render();
|
||||
auto screen = Screen::Create(Dimension::Fixed(10), Dimension::Fixed(1));
|
||||
Render(screen, document);
|
||||
};
|
||||
render();
|
||||
|
||||
Mouse mouse;
|
||||
mouse.button = Mouse::Button::Left;
|
||||
mouse.motion = Mouse::Motion::Pressed;
|
||||
mouse.y = 0;
|
||||
mouse.shift = false;
|
||||
mouse.meta = false;
|
||||
mouse.control = false;
|
||||
|
||||
mouse.x = 0;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 0u);
|
||||
|
||||
mouse.x = 0;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 0u);
|
||||
|
||||
mouse.x = 1;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 0u);
|
||||
|
||||
mouse.x = 1;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 0u);
|
||||
|
||||
mouse.x = 2;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 1u);
|
||||
|
||||
mouse.x = 2;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 1u);
|
||||
|
||||
mouse.x = 1;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 0u);
|
||||
|
||||
mouse.x = 4;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 2u);
|
||||
|
||||
mouse.x = 5;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 3u);
|
||||
|
||||
mouse.x = 6;
|
||||
input->OnEvent(Event::Mouse("", mouse));
|
||||
render();
|
||||
EXPECT_EQ(option.cursor_position(), 4u);
|
||||
}
|
||||
|
||||
// Copyright 2021 Arthur Sonzogni. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
@ -288,6 +288,98 @@ std::vector<std::string> Utf8ToGlyphs(const std::string& input) {
|
||||
return out;
|
||||
}
|
||||
|
||||
int GlyphPosition(const std::string& input, size_t glyph_to_skip, size_t start) {
|
||||
if (glyph_to_skip <= 0)
|
||||
return 0;
|
||||
size_t end = 0;
|
||||
while (start < input.size()) {
|
||||
uint32_t codepoint;
|
||||
bool eaten = EatCodePoint(input, start, &end, &codepoint);
|
||||
|
||||
// Ignore invalid, control characters and combining characters.
|
||||
if (!eaten || IsControl(codepoint) || IsCombining(codepoint)) {
|
||||
start = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
// We eat the beginning of the next glyph. If we are eating the one
|
||||
// requested, return its start position immediately.
|
||||
if (glyph_to_skip == 0)
|
||||
return start;
|
||||
|
||||
// Otherwise, skip this glyph and iterate:
|
||||
glyph_to_skip--;
|
||||
start = end;
|
||||
}
|
||||
return input.size();
|
||||
}
|
||||
|
||||
std::vector<int> CellToGlyphIndex(const std::string& input) {
|
||||
int x = -1;
|
||||
std::vector<int> out;
|
||||
out.reserve(input.size());
|
||||
size_t start = 0;
|
||||
size_t end = 0;
|
||||
while (start < input.size()) {
|
||||
uint32_t codepoint;
|
||||
bool eaten = EatCodePoint(input, start, &end, &codepoint);
|
||||
start = end;
|
||||
|
||||
// Ignore invalid / control characters.
|
||||
if (!eaten || IsControl(codepoint))
|
||||
continue;
|
||||
|
||||
// Combining characters are put with the previous glyph they are modifying.
|
||||
if (IsCombining(codepoint)) {
|
||||
if (x == -1) {
|
||||
++x;
|
||||
out.push_back(x);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fullwidth characters take two cells. The second is made of the empty
|
||||
// string to reserve the space the first is taking.
|
||||
if (IsFullWidth(codepoint)) {
|
||||
++x;
|
||||
out.push_back(x);
|
||||
out.push_back(x);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Normal characters:
|
||||
++x;
|
||||
out.push_back(x);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
int GlyphCount(const std::string& input) {
|
||||
int size = 0;
|
||||
size_t start = 0;
|
||||
size_t end = 0;
|
||||
while (start < input.size()) {
|
||||
uint32_t codepoint;
|
||||
bool eaten = EatCodePoint(input, start, &end, &codepoint);
|
||||
start = end;
|
||||
|
||||
// Ignore invalid characters:
|
||||
if (!eaten || IsControl(codepoint))
|
||||
continue;
|
||||
|
||||
// Ignore combining characters, except when they don't have a preceding to
|
||||
// combine with.
|
||||
if (IsCombining(codepoint)) {
|
||||
if (size == 0)
|
||||
size++;
|
||||
continue;
|
||||
}
|
||||
|
||||
size++;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4996) // codecvt_utf8_utf16 is deprecated
|
||||
|
@ -42,6 +42,85 @@ TEST(StringTest, Utf8ToGlyphs) {
|
||||
EXPECT_EQ(Utf8ToGlyphs("a\1a"), T({"a", "a"}));
|
||||
}
|
||||
|
||||
TEST(StringTest, GlyphCount) {
|
||||
// Basic:
|
||||
EXPECT_EQ(GlyphCount(""), 0);
|
||||
EXPECT_EQ(GlyphCount("a"), 1);
|
||||
EXPECT_EQ(GlyphCount("ab"), 2);
|
||||
// Fullwidth glyphs:
|
||||
EXPECT_EQ(GlyphCount("测"), 1);
|
||||
EXPECT_EQ(GlyphCount("测试"), 2);
|
||||
// Combining characters:
|
||||
EXPECT_EQ(GlyphCount("ā"), 1);
|
||||
EXPECT_EQ(GlyphCount("a⃒"), 1);
|
||||
EXPECT_EQ(GlyphCount("a̗"), 1);
|
||||
// Control characters:
|
||||
EXPECT_EQ(GlyphCount("\1"), 0);
|
||||
EXPECT_EQ(GlyphCount("a\1a"), 2);
|
||||
}
|
||||
|
||||
|
||||
TEST(StringTest, GlyphPosition) {
|
||||
// Basic:
|
||||
EXPECT_EQ(GlyphPosition("", -1), 0);
|
||||
EXPECT_EQ(GlyphPosition("", 0), 0);
|
||||
EXPECT_EQ(GlyphPosition("", 1), 0);
|
||||
EXPECT_EQ(GlyphPosition("a", 0), 0);
|
||||
EXPECT_EQ(GlyphPosition("a", 1), 1);
|
||||
EXPECT_EQ(GlyphPosition("ab", 0), 0);
|
||||
EXPECT_EQ(GlyphPosition("ab", 1), 1);
|
||||
EXPECT_EQ(GlyphPosition("ab", 2), 2);
|
||||
EXPECT_EQ(GlyphPosition("abc", 0), 0);
|
||||
EXPECT_EQ(GlyphPosition("abc", 1), 1);
|
||||
EXPECT_EQ(GlyphPosition("abc", 2), 2);
|
||||
EXPECT_EQ(GlyphPosition("abc", 3), 3);
|
||||
// Fullwidth glyphs:
|
||||
EXPECT_EQ(GlyphPosition("测", 0), 0);
|
||||
EXPECT_EQ(GlyphPosition("测", 1), 3);
|
||||
EXPECT_EQ(GlyphPosition("测试", 0), 0);
|
||||
EXPECT_EQ(GlyphPosition("测试", 1), 3);
|
||||
EXPECT_EQ(GlyphPosition("测试", 2), 6);
|
||||
EXPECT_EQ(GlyphPosition("测试", 1, 3), 6);
|
||||
EXPECT_EQ(GlyphPosition("测试", 1, 0), 3);
|
||||
// Combining characters:
|
||||
EXPECT_EQ(GlyphPosition("ā", 0), 0);
|
||||
EXPECT_EQ(GlyphPosition("ā", 1), 3);
|
||||
EXPECT_EQ(GlyphPosition("a⃒a̗ā", 0), 0);
|
||||
EXPECT_EQ(GlyphPosition("a⃒a̗ā", 1), 4);
|
||||
EXPECT_EQ(GlyphPosition("a⃒a̗ā", 2), 7);
|
||||
EXPECT_EQ(GlyphPosition("a⃒a̗ā", 3), 10);
|
||||
// Control characters:
|
||||
EXPECT_EQ(GlyphPosition("\1", 0), 0);
|
||||
EXPECT_EQ(GlyphPosition("\1", 1), 1);
|
||||
EXPECT_EQ(GlyphPosition("a\1a", 0), 0);
|
||||
EXPECT_EQ(GlyphPosition("a\1a", 1), 2);
|
||||
EXPECT_EQ(GlyphPosition("a\1a", 2), 3);
|
||||
}
|
||||
|
||||
TEST(StringTest, CellToGlyphIndex) {
|
||||
// Basic:
|
||||
auto basic = CellToGlyphIndex("abc");
|
||||
ASSERT_EQ(basic.size(), 3);
|
||||
EXPECT_EQ(basic[0], 0);
|
||||
EXPECT_EQ(basic[1], 1);
|
||||
EXPECT_EQ(basic[2], 2);
|
||||
|
||||
// Fullwidth glyphs:
|
||||
auto fullwidth = CellToGlyphIndex("测试");
|
||||
ASSERT_EQ(fullwidth.size(), 4);
|
||||
EXPECT_EQ(fullwidth[0], 0);
|
||||
EXPECT_EQ(fullwidth[1], 0);
|
||||
EXPECT_EQ(fullwidth[2], 1);
|
||||
EXPECT_EQ(fullwidth[3], 1);
|
||||
|
||||
// Combining characters:
|
||||
auto combining = CellToGlyphIndex("a⃒a̗ā");
|
||||
ASSERT_EQ(combining.size(), 3);
|
||||
EXPECT_EQ(combining[0], 0);
|
||||
EXPECT_EQ(combining[1], 1);
|
||||
EXPECT_EQ(combining[2], 2);
|
||||
}
|
||||
|
||||
// Copyright 2020 Arthur Sonzogni. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be found in
|
||||
// the LICENSE file.
|
||||
|
Loading…
Reference in New Issue
Block a user