mirror of
https://github.com/ArthurSonzogni/FTXUI.git
synced 2024-11-23 03:10:01 +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
11
CHANGELOG.md
11
CHANGELOG.md
@ -5,6 +5,8 @@ unreleased (development)
|
|||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
### Features:
|
### Features:
|
||||||
|
|
||||||
|
#### DOM:
|
||||||
- Support `flexbox` dom elements. This is build symmetrically to the HTML one.
|
- Support `flexbox` dom elements. This is build symmetrically to the HTML one.
|
||||||
All the following attributes are supported: direction, wrap, justify-content,
|
All the following attributes are supported: direction, wrap, justify-content,
|
||||||
align-items, align-content, gap
|
align-items, align-content, gap
|
||||||
@ -16,13 +18,16 @@ unreleased (development)
|
|||||||
- `paragraphAlignJustify`
|
- `paragraphAlignJustify`
|
||||||
- Add the helper elements based on `flexbox`: `hflow()`, `vflow()`.
|
- 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:
|
### Breaking changes:
|
||||||
- The behavior of `paragraph` has been modified. It now returns en Element,
|
- The behavior of `paragraph` has been modified. It now returns en Element,
|
||||||
instead of a list of elements.
|
instead of a list of elements.
|
||||||
|
|
||||||
### Bug
|
|
||||||
- Input shouldn't take focus when hovered by the mouse.
|
|
||||||
|
|
||||||
0.11.1
|
0.11.1
|
||||||
------
|
------
|
||||||
|
|
||||||
|
@ -14,7 +14,20 @@ std::wstring to_wstring(T s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int string_width(const std::string&);
|
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);
|
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
|
} // namespace ftxui
|
||||||
|
|
||||||
|
@ -20,10 +20,20 @@
|
|||||||
|
|
||||||
namespace ftxui {
|
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.
|
// An input box. The user can type text into it.
|
||||||
class WideInputBase : public ComponentBase {
|
class InputBase : public ComponentBase {
|
||||||
public:
|
public:
|
||||||
WideInputBase(WideStringRef content,
|
InputBase(StringRef content,
|
||||||
ConstStringRef placeholder,
|
ConstStringRef placeholder,
|
||||||
Ref<InputOption> option)
|
Ref<InputOption> option)
|
||||||
: content_(content), placeholder_(placeholder), option_(option) {}
|
: content_(content), placeholder_(placeholder), option_(option) {}
|
||||||
@ -38,22 +48,23 @@ class WideInputBase : public ComponentBase {
|
|||||||
|
|
||||||
// Component implementation:
|
// Component implementation:
|
||||||
Element Render() override {
|
Element Render() override {
|
||||||
std::wstring password_content;
|
std::string password_content;
|
||||||
if (option_->password())
|
if (option_->password())
|
||||||
password_content = std::wstring(content_->size(), U'•');
|
password_content = PasswordField(content_->size());
|
||||||
std::wstring& content = option_->password() ? password_content : *content_;
|
std::string& content = option_->password() ? password_content : *content_;
|
||||||
|
|
||||||
cursor_position() =
|
int size = GlyphCount(content);
|
||||||
std::max(0, std::min<int>(content.size(), cursor_position()));
|
|
||||||
auto main_decorator = flex | size(HEIGHT, EQUAL, 1);
|
cursor_position() = std::max(0, std::min<int>(size, cursor_position()));
|
||||||
|
auto main_decorator = flex | ftxui::size(HEIGHT, EQUAL, 1);
|
||||||
bool is_focused = Focused();
|
bool is_focused = Focused();
|
||||||
|
|
||||||
// placeholder.
|
// placeholder.
|
||||||
if (content.size() == 0) {
|
if (size == 0) {
|
||||||
bool hovered = hovered_;
|
bool hovered = hovered_;
|
||||||
Decorator decorator = dim | main_decorator;
|
Decorator decorator = dim | main_decorator;
|
||||||
if (is_focused)
|
if (is_focused)
|
||||||
decorator = decorator | focus | bold;
|
decorator = decorator | focus | inverted;
|
||||||
if (hovered || is_focused)
|
if (hovered || is_focused)
|
||||||
decorator = decorator | inverted;
|
decorator = decorator | inverted;
|
||||||
return text(*placeholder_) | decorator | reflect(box_);
|
return text(*placeholder_) | decorator | reflect(box_);
|
||||||
@ -67,22 +78,22 @@ class WideInputBase : public ComponentBase {
|
|||||||
return text(content) | main_decorator | reflect(box_);
|
return text(content) | main_decorator | reflect(box_);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::wstring part_before_cursor = content.substr(0, cursor_position());
|
int index_before_cursor = GlyphPosition(content, cursor_position());
|
||||||
std::wstring part_at_cursor = cursor_position() < (int)content.size()
|
int index_after_cursor = GlyphPosition(content, 1, index_before_cursor);
|
||||||
? content.substr(cursor_position(), 1)
|
std::string part_before_cursor = content.substr(0, index_before_cursor);
|
||||||
: L" ";
|
std::string part_at_cursor = " ";
|
||||||
std::wstring part_after_cursor = cursor_position() < (int)content.size() - 1
|
if (cursor_position() < size) {
|
||||||
? content.substr(cursor_position() + 1)
|
part_at_cursor = content.substr(index_before_cursor,
|
||||||
: L"";
|
index_after_cursor - index_before_cursor);
|
||||||
|
}
|
||||||
|
std::string part_after_cursor = content.substr(index_after_cursor);
|
||||||
auto focused = (is_focused || hovered_) ? focus : select;
|
auto focused = (is_focused || hovered_) ? focus : select;
|
||||||
// clang-format off
|
return hbox({
|
||||||
return
|
|
||||||
hbox(
|
|
||||||
text(part_before_cursor),
|
text(part_before_cursor),
|
||||||
text(part_at_cursor) | underlined | focused | reflect(cursor_box_),
|
text(part_at_cursor) | focused | inverted | reflect(cursor_box_),
|
||||||
text(part_after_cursor)
|
text(part_after_cursor),
|
||||||
) | flex | inverted | frame | bold |main_decorator | reflect(box_);
|
}) |
|
||||||
// clang-format on
|
flex | frame | bold | main_decorator | reflect(box_);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OnEvent(Event event) override {
|
bool OnEvent(Event event) override {
|
||||||
@ -92,13 +103,15 @@ class WideInputBase : public ComponentBase {
|
|||||||
if (event.is_mouse())
|
if (event.is_mouse())
|
||||||
return OnMouseEvent(event);
|
return OnMouseEvent(event);
|
||||||
|
|
||||||
std::wstring c;
|
std::string c;
|
||||||
|
|
||||||
// Backspace.
|
// Backspace.
|
||||||
if (event == Event::Backspace) {
|
if (event == Event::Backspace) {
|
||||||
if (cursor_position() == 0)
|
if (cursor_position() == 0)
|
||||||
return false;
|
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()--;
|
cursor_position()--;
|
||||||
option_->on_change();
|
option_->on_change();
|
||||||
return true;
|
return true;
|
||||||
@ -108,7 +121,9 @@ class WideInputBase : public ComponentBase {
|
|||||||
if (event == Event::Delete) {
|
if (event == Event::Delete) {
|
||||||
if (cursor_position() == int(content_->size()))
|
if (cursor_position() == int(content_->size()))
|
||||||
return false;
|
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();
|
option_->on_change();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -140,13 +155,14 @@ class WideInputBase : public ComponentBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event == Event::End) {
|
if (event == Event::End) {
|
||||||
cursor_position() = (int)content_->size();
|
cursor_position() = GlyphCount(*content_);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
if (event.is_character()) {
|
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()++;
|
cursor_position()++;
|
||||||
option_->on_change();
|
option_->on_change();
|
||||||
return true;
|
return true;
|
||||||
@ -167,12 +183,27 @@ class WideInputBase : public ComponentBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TakeFocus();
|
TakeFocus();
|
||||||
int new_cursor_position =
|
if (content_->size() == 0)
|
||||||
cursor_position() + event.mouse().x - cursor_box_.x_min;
|
return true;
|
||||||
new_cursor_position =
|
|
||||||
std::max(0, std::min<int>(content_->size(), new_cursor_position));
|
auto mapping = CellToGlyphIndex(*content_);
|
||||||
if (cursor_position() != new_cursor_position) {
|
int original_glyph = cursor_position();
|
||||||
cursor_position() = new_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();
|
option_->on_change();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -181,7 +212,7 @@ class WideInputBase : public ComponentBase {
|
|||||||
bool Focusable() const final { return true; }
|
bool Focusable() const final { return true; }
|
||||||
|
|
||||||
bool hovered_ = false;
|
bool hovered_ = false;
|
||||||
WideStringRef content_;
|
StringRef content_;
|
||||||
ConstStringRef placeholder_;
|
ConstStringRef placeholder_;
|
||||||
|
|
||||||
Box box_;
|
Box box_;
|
||||||
@ -190,39 +221,37 @@ class WideInputBase : public ComponentBase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// An input box. The user can type text into it.
|
// An input box. The user can type text into it.
|
||||||
// For convenience, the std::string version of Input simply wrap a
|
// For convenience, the std::wstring version of Input simply wrap a
|
||||||
// WideInputBase.
|
// InputBase.
|
||||||
// TODO(arthursonzogni): Provide an implementation handling std::string natively
|
class WideInputBase : public InputBase {
|
||||||
// and adds better support for combining characters.
|
|
||||||
class InputBase : public WideInputBase {
|
|
||||||
public:
|
public:
|
||||||
InputBase(StringRef content,
|
WideInputBase(WideStringRef content,
|
||||||
ConstStringRef placeholder,
|
ConstStringRef placeholder,
|
||||||
Ref<InputOption> option)
|
Ref<InputOption> option)
|
||||||
: WideInputBase(&wrapped_content_,
|
: InputBase(&wrapped_content_, std::move(placeholder), std::move(option)),
|
||||||
std::move(placeholder),
|
|
||||||
std::move(option)),
|
|
||||||
content_(std::move(content)),
|
content_(std::move(content)),
|
||||||
wrapped_content_(to_wstring(*content_)) {}
|
wrapped_content_(to_string(*content_)) {}
|
||||||
|
|
||||||
Element Render() override {
|
Element Render() override {
|
||||||
wrapped_content_ = to_wstring(*content_);
|
wrapped_content_ = to_string(*content_);
|
||||||
return WideInputBase::Render();
|
return InputBase::Render();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OnEvent(Event event) override {
|
bool OnEvent(Event event) override {
|
||||||
wrapped_content_ = to_wstring(*content_);
|
wrapped_content_ = to_string(*content_);
|
||||||
if (WideInputBase::OnEvent(event)) {
|
if (InputBase::OnEvent(event)) {
|
||||||
*content_ = to_string(wrapped_content_);
|
*content_ = to_wstring(wrapped_content_);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringRef content_;
|
WideStringRef content_;
|
||||||
std::wstring wrapped_content_;
|
std::string wrapped_content_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
/// @brief An input box for editing text.
|
/// @brief An input box for editing text.
|
||||||
/// @param content The editable content.
|
/// @param content The editable content.
|
||||||
/// @param placeholder The text displayed when content is still empty.
|
/// @param placeholder The text displayed when content is still empty.
|
||||||
|
@ -226,6 +226,151 @@ TEST(InputTest, Backspace) {
|
|||||||
EXPECT_EQ(option.cursor_position(), 0u);
|
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.
|
// Copyright 2021 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
|
||||||
// the LICENSE file.
|
// the LICENSE file.
|
||||||
|
@ -288,6 +288,98 @@ std::vector<std::string> Utf8ToGlyphs(const std::string& input) {
|
|||||||
return out;
|
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
|
#ifdef _MSC_VER
|
||||||
#pragma warning(push)
|
#pragma warning(push)
|
||||||
#pragma warning(disable : 4996) // codecvt_utf8_utf16 is deprecated
|
#pragma warning(disable : 4996) // codecvt_utf8_utf16 is deprecated
|
||||||
|
@ -42,6 +42,85 @@ TEST(StringTest, Utf8ToGlyphs) {
|
|||||||
EXPECT_EQ(Utf8ToGlyphs("a\1a"), T({"a", "a"}));
|
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.
|
// 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
|
||||||
// the LICENSE file.
|
// the LICENSE file.
|
||||||
|
Loading…
Reference in New Issue
Block a user