From 7de4f8683d530ff5573483f40b17df4b3c6ed736 Mon Sep 17 00:00:00 2001 From: Arthur Sonzogni Date: Tue, 2 May 2023 13:32:37 +0200 Subject: [PATCH] Feature: Add multi-line input. (#630) --- CHANGELOG.md | 6 +- cmake/ftxui_test.cmake | 2 + examples/component/CMakeLists.txt | 2 + examples/component/input_style.cpp | 98 ++ examples/component/textarea.cpp | 35 + include/ftxui/component/component.hpp | 3 +- include/ftxui/component/component_options.hpp | 36 +- include/ftxui/component/receiver.hpp | 2 +- include/ftxui/screen/string.hpp | 36 +- include/ftxui/util/ref.hpp | 1 + src/ftxui/component/button_test.cpp | 8 +- src/ftxui/component/collapsible_test.cpp | 5 +- src/ftxui/component/component_options.cpp | 80 +- src/ftxui/component/component_test.cpp | 3 +- src/ftxui/component/container_test.cpp | 3 +- src/ftxui/component/hoverable_test.cpp | 3 +- src/ftxui/component/input.cpp | 603 ++++++++---- src/ftxui/component/input_test.cpp | 921 +++++++++++------- src/ftxui/component/radiobox_test.cpp | 2 +- src/ftxui/component/receiver_test.cpp | 3 +- src/ftxui/component/resizable_split_test.cpp | 3 +- src/ftxui/component/slider_test.cpp | 8 +- src/ftxui/component/terminal_input_parser.cpp | 12 +- .../component/terminal_input_parser_test.cpp | 4 +- src/ftxui/component/toggle_test.cpp | 10 +- src/ftxui/dom/blink_test.cpp | 4 +- src/ftxui/dom/bold_test.cpp | 4 +- src/ftxui/dom/dim_test.cpp | 4 +- src/ftxui/dom/text.cpp | 3 + src/ftxui/dom/underlined_test.cpp | 4 +- src/ftxui/screen/string.cpp | 124 ++- src/ftxui/screen/string_internal.hpp | 64 ++ src/ftxui/screen/string_test.cpp | 65 +- 33 files changed, 1471 insertions(+), 690 deletions(-) create mode 100644 examples/component/input_style.cpp create mode 100644 examples/component/textarea.cpp create mode 100644 src/ftxui/screen/string_internal.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dd1eb6..2d5cffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ current (development) --------------------- ### Component +- Feature: `input` is now multi-line. +- Feature: `input` style can now be customized. - Feature: Support `ResizableSplit` with customizable separator. - Breaking: MenuDirection enum is renamed Direction - Fix: Remove useless new line when using an alternative screen. @@ -12,9 +14,7 @@ current (development) ### Dom - Feature: Add the dashed style for border and separator. - Feature: Add colored borders. -- Feature: Customize with gradient color effect. Add the following decorators: - - `colorgrad` - - `bgcolorgrad` +- Feature: Add `LinearGradient`! - Improvement: Color::Interpolate() uses gamma correction. ### diff --git a/cmake/ftxui_test.cmake b/cmake/ftxui_test.cmake index 5b62cb3..fbbbbfa 100644 --- a/cmake/ftxui_test.cmake +++ b/cmake/ftxui_test.cmake @@ -71,3 +71,5 @@ include(GoogleTest) gtest_discover_tests(ftxui-tests DISCOVERY_TIMEOUT 600 ) + +set(CMAKE_CTEST_ARGUMENTS "--rerun-failed --output-on-failure") diff --git a/examples/component/CMakeLists.txt b/examples/component/CMakeLists.txt index c080690..6211583 100644 --- a/examples/component/CMakeLists.txt +++ b/examples/component/CMakeLists.txt @@ -17,6 +17,7 @@ example(focus_cursor) example(gallery) example(homescreen) example(input) +example(input_style) example(linear_gradient_gallery) example(maybe) example(menu) @@ -40,5 +41,6 @@ example(slider_direction) example(slider_rgb) example(tab_horizontal) example(tab_vertical) +example(textarea) example(toggle) example(with_restored_io) diff --git a/examples/component/input_style.cpp b/examples/component/input_style.cpp new file mode 100644 index 0000000..f98ae39 --- /dev/null +++ b/examples/component/input_style.cpp @@ -0,0 +1,98 @@ +#include // for LinearGradient +#include // for Color, Color::White, Color::Red, Color::Blue, Color::Black, Color::GrayDark, ftxui +#include // for function +#include // for allocator, string +#include // for move + +#include "ftxui/component/component.hpp" // for Input, Horizontal, Vertical, operator| +#include "ftxui/component/component_base.hpp" // for Component +#include "ftxui/component/component_options.hpp" // for InputState, InputOption +#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive +#include "ftxui/dom/elements.hpp" // for operator|=, Element, bgcolor, operator|, separatorEmpty, color, borderEmpty, separator, text, center, dim, hbox, vbox, border, borderDouble, borderRounded + +int main(int argc, const char* argv[]) { + using namespace ftxui; + + InputOption style_1 = InputOption::Default(); + + InputOption style_2 = InputOption::Spacious(); + + InputOption style_3 = InputOption::Spacious(); + style_3.transform = [](InputState state) { + state.element |= borderEmpty; + + if (state.is_placeholder) { + state.element |= dim; + } + + if (state.focused) { + state.element |= borderDouble; + state.element |= bgcolor(Color::White); + state.element |= color(Color::Black); + } else if (state.hovered) { + state.element |= borderRounded; + state.element |= bgcolor(LinearGradient(90, Color::Blue, Color::Red)); + state.element |= color(Color::White); + } else { + state.element |= border; + state.element |= bgcolor(LinearGradient(0, Color::Blue, Color::Red)); + state.element |= color(Color::White); + } + + return state.element; + }; + + InputOption style_4 = InputOption::Spacious(); + style_4.transform = [](InputState state) { + state.element = hbox({ + text("Theorem") | center | borderEmpty | bgcolor(Color::Red), + separatorEmpty(), + separator() | color(Color::White), + separatorEmpty(), + std::move(state.element), + }); + + state.element |= borderEmpty; + if (state.is_placeholder) { + state.element |= dim; + } + + if (state.focused) { + state.element |= bgcolor(Color::Black); + } else { + state.element |= bgcolor(Color::Blue); + } + + if (state.hovered) { + state.element |= bgcolor(Color::GrayDark); + } + + return vbox({state.element, separatorEmpty()}); + }; + + auto generateUiFromStyle = [&](InputOption style) { + auto first_name = new std::string(); // Leaked + auto middle_name = new std::string(); // Leaked + auto last_name = new std::string(); // Leaked + return Container::Vertical({ + Input(first_name, "first name", style), + Input(middle_name, "middle name", style), + Input(last_name, "last name", style), + }) | + borderEmpty; + }; + + auto ui = Container::Horizontal({ + generateUiFromStyle(style_1), + generateUiFromStyle(style_2), + generateUiFromStyle(style_3), + generateUiFromStyle(style_4), + }); + + auto screen = ScreenInteractive::TerminalOutput(); + screen.Loop(ui); +} + +// 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. diff --git a/examples/component/textarea.cpp b/examples/component/textarea.cpp new file mode 100644 index 0000000..b8753b3 --- /dev/null +++ b/examples/component/textarea.cpp @@ -0,0 +1,35 @@ +#include // for allocator, __shared_ptr_access, shared_ptr +#include // for string + +#include "ftxui/component/captured_mouse.hpp" // for ftxui +#include "ftxui/component/component.hpp" // for Input, Renderer, ResizableSplitLeft +#include "ftxui/component/component_base.hpp" // for ComponentBase, Component +#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive +#include "ftxui/dom/elements.hpp" // for operator|, separator, text, Element, flex, vbox, border + +int main(int argc, const char* argv[]) { + using namespace ftxui; + + std::string content_1; + std::string content_2; + auto textarea_1 = Input(&content_1); + auto textarea_2 = Input(&content_2); + int size = 50; + auto layout = ResizableSplitLeft(textarea_1, textarea_2, &size); + + auto component = Renderer(layout, [&] { + return vbox({ + text("Input:"), + separator(), + layout->Render() | flex, + }) | + border; + }); + + auto screen = ScreenInteractive::Fullscreen(); + screen.Loop(component); +} + +// 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. diff --git a/include/ftxui/component/component.hpp b/include/ftxui/component/component.hpp index b54f89f..a9289ae 100644 --- a/include/ftxui/component/component.hpp +++ b/include/ftxui/component/component.hpp @@ -51,8 +51,9 @@ Component Checkbox(ConstStringRef label, bool* checked, Ref option = CheckboxOption::Simple()); +Component Input(StringRef content, Ref option = {}); Component Input(StringRef content, - ConstStringRef placeholder, + StringRef placeholder, Ref option = {}); Component Menu(ConstStringListRef entries, diff --git a/include/ftxui/component/component_options.hpp b/include/ftxui/component/component_options.hpp index 8c49bd6..172f3c5 100644 --- a/include/ftxui/component/component_options.hpp +++ b/include/ftxui/component/component_options.hpp @@ -5,7 +5,7 @@ #include // for Duration, QuadraticInOut, Function #include // for Direction, Direction::Left, Direction::Right, Direction::Down #include // for Element, separator -#include // for Ref, ConstRef +#include // for Ref, ConstRef, StringRef #include // for function #include // for optional #include // for string @@ -134,20 +134,42 @@ struct CheckboxOption { std::function on_change = [] {}; }; +/// @brief Used to define style for the Input component. +struct InputState { + Element element; + bool hovered; /// < Whether the input is hovered by the mouse. + bool focused; /// < Whether the input is focused by the user. + bool is_placeholder; /// < Whether the input is empty and displaying the + /// < placeholder. +}; + /// @brief Option for the Input component. /// @ingroup component struct InputOption { + // A set of predefined styles: + + /// @brief Create the default input style: + static InputOption Default(); + /// @brief A white on black style with high margins: + static InputOption Spacious(); + /// @brief A style with a border: + static InputOption Arthur(); + + /// The content of the input when it's empty. + StringRef placeholder = ""; + + // Style: + std::function transform; + Ref password = false; /// < Obscure the input content using '*'. + Ref multiline = true; /// < Whether the input can be multiline. + /// Called when the content changes. std::function on_change = [] {}; /// Called when the user presses enter. std::function on_enter = [] {}; - /// Obscure the input content using '*'. - Ref password = false; - - /// When set different from -1, this attributes is used to store the cursor - /// position. - Ref cursor_position = -1; + // The char position of the cursor: + Ref cursor_position = 0; }; /// @brief Option for the Radiobox component. diff --git a/include/ftxui/component/receiver.hpp b/include/ftxui/component/receiver.hpp index 1d48764..212341c 100644 --- a/include/ftxui/component/receiver.hpp +++ b/include/ftxui/component/receiver.hpp @@ -1,7 +1,7 @@ #ifndef FTXUI_COMPONENT_RECEIVER_HPP_ #define FTXUI_COMPONENT_RECEIVER_HPP_ -#include // for copy +#include // for copy, max #include // for atomic, __atomic_base #include // for condition_variable #include diff --git a/include/ftxui/screen/string.hpp b/include/ftxui/screen/string.hpp index 0475c41..9657330 100644 --- a/include/ftxui/screen/string.hpp +++ b/include/ftxui/screen/string.hpp @@ -15,44 +15,10 @@ 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 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); - -// 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, -}; -WordBreakProperty CodepointToWordBreakProperty(uint32_t codepoint); -std::vector 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 // Glyphs takes one cell, full-size Glyphs take two cells. diff --git a/include/ftxui/util/ref.hpp b/include/ftxui/util/ref.hpp index ac20b4f..c650bc4 100644 --- a/include/ftxui/util/ref.hpp +++ b/include/ftxui/util/ref.hpp @@ -48,6 +48,7 @@ class StringRef { StringRef(const wchar_t* ref) : StringRef(to_string(std::wstring(ref))) {} StringRef(const char* ref) : StringRef(std::string(ref)) {} std::string& operator*() { return address_ ? *address_ : owned_; } + std::string& operator()() { return address_ ? *address_ : owned_; } std::string* operator->() { return address_ ? address_ : &owned_; } private: diff --git a/src/ftxui/component/button_test.cpp b/src/ftxui/component/button_test.cpp index 4099b96..c49582c 100644 --- a/src/ftxui/component/button_test.cpp +++ b/src/ftxui/component/button_test.cpp @@ -1,7 +1,6 @@ -#include // for AssertionResult, Message, TestPartResult, EXPECT_EQ, Test, EXPECT_FALSE, EXPECT_TRUE, TestInfo (ptr only), TEST -#include // for operator""s, chrono_literals -#include // for __shared_ptr_access, shared_ptr, allocator -#include // for string +#include // for operator""s, chrono_literals +#include // for __shared_ptr_access, shared_ptr, allocator +#include // for string #include "ftxui/component/animation.hpp" // for Duration, Params #include "ftxui/component/component.hpp" // for Button, Horizontal @@ -12,6 +11,7 @@ #include "ftxui/dom/node.hpp" // for Render #include "ftxui/screen/screen.hpp" // for Screen #include "ftxui/screen/terminal.hpp" // for SetColorSupport, Color, TrueColor +#include "gtest/gtest.h" // for AssertionResult, Message, TestPartResult, EXPECT_EQ, Test, EXPECT_FALSE, EXPECT_TRUE, TEST // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/component/collapsible_test.cpp b/src/ftxui/component/collapsible_test.cpp index 4492fe6..bbea5cc 100644 --- a/src/ftxui/component/collapsible_test.cpp +++ b/src/ftxui/component/collapsible_test.cpp @@ -1,12 +1,13 @@ -#include #include // for __shared_ptr_access, shared_ptr, allocator +#include // for string #include "ftxui/component/component.hpp" // for Collapsible, Renderer #include "ftxui/component/component_base.hpp" // for ComponentBase #include "ftxui/component/event.hpp" // for Event, Event::Return, Event::ArrowDown -#include "ftxui/dom/elements.hpp" // for text, Element +#include "ftxui/dom/elements.hpp" // for Element, text #include "ftxui/dom/node.hpp" // for Render #include "ftxui/screen/screen.hpp" // for Screen +#include "gtest/gtest.h" // for AssertionResult, Message, Test, TestPartResult, EXPECT_EQ, EXPECT_FALSE, EXPECT_TRUE, TEST // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/component/component_options.cpp b/src/ftxui/component/component_options.cpp index 4c955e5..e36ac35 100644 --- a/src/ftxui/component/component_options.cpp +++ b/src/ftxui/component/component_options.cpp @@ -1,11 +1,12 @@ #include "ftxui/component/component_options.hpp" -#include // for Color, Color::Black, Color::White, Color::GrayDark, Color::GrayLight -#include // for shared_ptr -#include // for move +#include // for LinearGradient +#include // for Color, Color::White, Color::Black, Color::GrayDark, Color::Blue, Color::GrayLight, Color::Red +#include // for shared_ptr +#include // for move #include "ftxui/component/animation.hpp" // for Function, Duration -#include "ftxui/dom/elements.hpp" // for operator|=, text, Element, bold, inverted, operator|, dim, hbox, automerge, borderEmpty, borderLight +#include "ftxui/dom/elements.hpp" // for operator|=, Element, text, bgcolor, inverted, bold, dim, operator|, color, borderEmpty, hbox, automerge, border, borderLight namespace ftxui { @@ -257,6 +258,77 @@ RadioboxOption RadioboxOption::Simple() { return option; } +// static +InputOption InputOption::Default() { + InputOption option; + option.transform = [](InputState state) { + state.element |= color(Color::White); + + if (state.is_placeholder) { + state.element |= dim; + } + + if (state.focused) { + state.element |= inverted; + } else if (state.hovered) { + state.element |= bgcolor(Color::GrayDark); + } + + return state.element; + }; + return option; +} + +// static +InputOption InputOption::Spacious() { + InputOption option; + option.transform = [](InputState state) { + state.element |= borderEmpty; + state.element |= color(Color::White); + + if (state.is_placeholder) { + state.element |= dim; + } + + if (state.focused) { + state.element |= bgcolor(Color::Black); + } + + if (state.hovered) { + state.element |= bgcolor(Color::GrayDark); + } + + return state.element; + }; + return option; +} + +// static +InputOption InputOption::Arthur() { + InputOption option; + option.transform = [](InputState state) { + state.element |= borderEmpty; + state.element |= color(Color::White); + + if (state.is_placeholder) { + state.element |= dim; + } + + if (state.focused) { + state.element |= bgcolor(Color::Black); + } else { + state.element |= bgcolor(LinearGradient(0, Color::Blue, Color::Red)); + } + + if (state.hovered) { + state.element |= bgcolor(Color::GrayDark); + } + + return state.element; + }; + return option; +} + } // namespace ftxui // Copyright 2022 Arthur Sonzogni. All rights reserved. diff --git a/src/ftxui/component/component_test.cpp b/src/ftxui/component/component_test.cpp index 5a8b6aa..4247c04 100644 --- a/src/ftxui/component/component_test.cpp +++ b/src/ftxui/component/component_test.cpp @@ -1,8 +1,9 @@ -#include #include // for shared_ptr, __shared_ptr_access, allocator, __shared_ptr_access<>::element_type, make_shared +#include // for string #include "ftxui/component/component.hpp" // for Make #include "ftxui/component/component_base.hpp" // for ComponentBase, Component +#include "gtest/gtest.h" // for Message, TestPartResult, EXPECT_EQ, Test, AssertionResult, TEST, EXPECT_FALSE namespace ftxui { diff --git a/src/ftxui/component/container_test.cpp b/src/ftxui/component/container_test.cpp index 6974c48..9710c67 100644 --- a/src/ftxui/component/container_test.cpp +++ b/src/ftxui/component/container_test.cpp @@ -1,9 +1,10 @@ -#include #include // for __shared_ptr_access, shared_ptr, allocator +#include // for string #include "ftxui/component/component.hpp" // for Horizontal, Vertical, Button, Tab #include "ftxui/component/component_base.hpp" // for ComponentBase, Component #include "ftxui/component/event.hpp" // for Event, Event::Tab, Event::TabReverse, Event::ArrowDown, Event::ArrowLeft, Event::ArrowRight, Event::ArrowUp +#include "gtest/gtest.h" // for AssertionResult, Message, TestPartResult, EXPECT_EQ, EXPECT_FALSE, Test, EXPECT_TRUE, TEST namespace ftxui { diff --git a/src/ftxui/component/hoverable_test.cpp b/src/ftxui/component/hoverable_test.cpp index 039d563..3a023a0 100644 --- a/src/ftxui/component/hoverable_test.cpp +++ b/src/ftxui/component/hoverable_test.cpp @@ -1,6 +1,6 @@ -#include // for AssertionResult, Message, TestPartResult, EXPECT_FALSE, EXPECT_EQ, Test, EXPECT_TRUE, TestInfo (ptr only), TEST #include // for Element, text #include // for shared_ptr, __shared_ptr_access, allocator +#include // for string #include "ftxui/component/component.hpp" // for Hoverable, Horizontal, operator|=, Renderer #include "ftxui/component/component_base.hpp" // for ComponentBase, Component @@ -8,6 +8,7 @@ #include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Left, Mouse::Released #include "ftxui/dom/node.hpp" // for Render #include "ftxui/screen/screen.hpp" // for Screen +#include "gtest/gtest.h" // for AssertionResult, Message, TestPartResult, EXPECT_FALSE, EXPECT_EQ, Test, EXPECT_TRUE, TEST // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/component/input.cpp b/src/ftxui/component/input.cpp index c21bdd4..7275b12 100644 --- a/src/ftxui/component/input.cpp +++ b/src/ftxui/component/input.cpp @@ -1,32 +1,55 @@ +#include // for uint32_t #include // for max, min #include // for size_t #include // for function -#include // for shared_ptr -#include // for string, allocator -#include // for move -#include // for vector +#include // for allocator, shared_ptr, allocator_traits<>::value_type +#include // for basic_istream, stringstream +#include // for string, basic_string, operator==, getline +#include // for move +#include // for vector #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 -#include "ftxui/component/event.hpp" // for Event, Event::ArrowLeft, Event::ArrowLeftCtrl, Event::ArrowRight, Event::ArrowRightCtrl, Event::Backspace, Event::Custom, Event::Delete, Event::End, Event::Home, Event::Return +#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 #include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Left, Mouse::Pressed #include "ftxui/component/screen_interactive.hpp" // for Component -#include "ftxui/dom/elements.hpp" // for operator|, text, Element, reflect, operator|=, flex, inverted, hbox, size, bold, dim, focus, focusCursorBarBlinking, frame, select, Decorator, EQUAL, HEIGHT +#include "ftxui/dom/elements.hpp" // for operator|, reflect, text, Element, xflex, hbox, Elements, frame, operator|=, vbox, focus, focusCursorBarBlinking, select #include "ftxui/screen/box.hpp" // for Box -#include "ftxui/screen/string.hpp" // for GlyphPosition, WordBreakProperty, GlyphCount, Utf8ToWordBreakProperty, CellToGlyphIndex, 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, ConstStringRef +#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 namespace ftxui { 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) { +std::vector Split(const std::string& input) { + std::vector 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: @@ -42,7 +65,7 @@ bool IsWordCharacter(WordBreakProperty property) { case WordBreakProperty::Newline: case WordBreakProperty::Single_Quote: case WordBreakProperty::WSegSpace: - // Unsure: + // Unexpected/Unsure case WordBreakProperty::Extend: case WordBreakProperty::ExtendNumLet: case WordBreakProperty::Format: @@ -50,216 +73,398 @@ bool IsWordCharacter(WordBreakProperty property) { case WordBreakProperty::ZWJ: return false; } - return true; // NOT_REACHED(); + return false; // NOT_REACHED(); } -std::string PasswordField(size_t size) { - std::string out; - out.reserve(2 * size); - while (size--) { - out += "•"; +bool IsWordCharacter(const std::string& input, size_t iter) { + uint32_t ucs = 0; + if (!EatCodePoint(input, iter, &iter, &ucs)) { + return false; } - return out; + + return IsWordCodePoint(ucs); } // An input box. The user can type text into it. class InputBase : public ComponentBase { public: - InputBase(StringRef content, - ConstStringRef placeholder, - Ref option) - : content_(std::move(content)), - placeholder_(std::move(placeholder)), - option_(std::move(option)) {} - - int cursor_position_internal_ = 0; - int& cursor_position() { - int& opt = option_->cursor_position(); - if (opt != -1) { - return opt; - } - return cursor_position_internal_; - } + // NOLINTNEXTLINE + InputBase(StringRef content, Ref option) + : content_(std::move(content)), option_(std::move(option)) {} + private: // Component implementation: Element Render() override { - std::string password_content; - if (option_->password()) { - password_content = PasswordField(content_->size()); - } - const std::string& content = - option_->password() ? password_content : *content_; - - const int size = GlyphCount(content); - - cursor_position() = std::max(0, std::min(size, cursor_position())); - auto main_decorator = flex | ftxui::size(HEIGHT, EQUAL, 1); const bool is_focused = Focused(); + const auto focused = + (is_focused || hovered_) ? focusCursorBarBlinking : select; + + auto transform = option_->transform ? option_->transform + : InputOption::Default().transform; // placeholder. - if (size == 0) { - auto element = text(*placeholder_) | dim | main_decorator | reflect(box_); + if (content_->empty()) { + auto element = text(option_->placeholder()) | xflex | frame; if (is_focused) { element |= focus; } - if (hovered_ || is_focused) { - element |= inverted; - } - return element; + + return transform({ + std::move(element), hovered_, is_focused, + true // placeholder + }) | + reflect(box_); } - // Not focused. - if (!is_focused) { - auto element = text(content) | main_decorator | reflect(box_); - if (hovered_) { - element |= inverted; + Elements elements; + std::vector lines = Split(*content_); + + 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; } - return element; + + cursor_char_index -= line.size() + 1; + cursor_line++; } - const int index_before_cursor = GlyphPosition(content, cursor_position()); - const int index_after_cursor = - GlyphPosition(content, 1, index_before_cursor); - const 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); + if (lines.empty()) { + elements.push_back(text("") | focused); } - const std::string part_after_cursor = content.substr(index_after_cursor); - auto focused = (is_focused || hovered_) ? focusCursorBarBlinking : select; - return hbox({ - text(part_before_cursor), - text(part_at_cursor) | focused | reflect(cursor_box_), - text(part_after_cursor), + + 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; + } + + // 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 }) | - flex | frame | bold | main_decorator | reflect(box_); + xflex | reflect(box_); } - bool OnEvent(Event event) override { - cursor_position() = - std::max(0, std::min((int)content_->size(), cursor_position())); - - if (event.is_mouse()) { - return OnMouseEvent(event); + Element Text(const std::string& input) { + if (!option_->password()) { + return text(input); } - // Backspace. - if (event == Event::Backspace) { - if (cursor_position() == 0) { - return false; - } - const size_t start = GlyphPosition(*content_, cursor_position() - 1); - const size_t end = GlyphPosition(*content_, cursor_position()); - content_->erase(start, end - start); - cursor_position()--; - option_->on_change(); - return true; + std::string out; + out.reserve(10 + input.size() * 3 / 2); + for (size_t i = 0; i < input.size(); ++i) { + out += "•"; } + return text(out); + } - // Delete - if (event == Event::Delete) { - if (cursor_position() == int(content_->size())) { - return false; - } - const size_t start = GlyphPosition(*content_, cursor_position()); - const size_t end = GlyphPosition(*content_, cursor_position() + 1); - content_->erase(start, end - start); - option_->on_change(); - return true; + bool HandleBackspace() { + int& cursor_position = option_->cursor_position(); + if (cursor_position == 0) { + return false; } + const size_t start = GlyphPrevious(content_(), cursor_position); + const size_t end = cursor_position; + content_->erase(start, end - start); + cursor_position = start; + return true; + } - // Enter. - if (event == Event::Return) { - option_->on_enter(); - return true; + bool HandleDelete() { + int& cursor_position = option_->cursor_position(); + if (cursor_position == (int)content_->size()) { + return false; } + const size_t start = cursor_position; + const size_t end = GlyphNext(content_(), cursor_position); + content_->erase(start, end - start); + return true; + } - if (event == Event::Custom) { + bool HandleArrowLeft() { + int& cursor_position = option_->cursor_position(); + if (cursor_position == 0) { return false; } - // Arrow - if (event == Event::ArrowLeft && cursor_position() > 0) { - cursor_position()--; - return true; + cursor_position = GlyphPrevious(content_(), cursor_position); + return true; + } + + bool HandleArrowRight() { + int& cursor_position = option_->cursor_position(); + if (cursor_position == (int)content_->size()) { + return false; } - if (event == Event::ArrowRight && - cursor_position() < static_cast(content_->size())) { - cursor_position()++; - return true; + 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); + } + return width; + } + + // 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); + } + } + + bool HandleArrowUp() { + int& cursor_position = option_->cursor_position(); + if (cursor_position == 0) { + return false; } - // CTRL + Arrow: + 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; + } + + MoveCursorColumn(columns); + return true; + } + + bool HandleArrowDown() { + int& cursor_position = option_->cursor_position(); + if (cursor_position == (int)content_->size()) { + return false; + } + + 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; + } + + 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()); + + if (event.is_character()) { + 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(); + } + if (event == Event::ArrowDown) { + return HandleArrowDown(); + } + if (event == Event::Home) { + return HandleHome(); + } + if (event == Event::End) { + return HandleEnd(); + } if (event == Event::ArrowLeftCtrl) { - HandleLeftCtrl(); - return true; + return HandleLeftCtrl(); } if (event == Event::ArrowRightCtrl) { - HandleRightCtrl(); - return true; + return HandleRightCtrl(); + } + if (event == Event::Return) { + return HandleReturn(); } - if (event == Event::Home) { - cursor_position() = 0; - return true; - } - - if (event == Event::End) { - cursor_position() = GlyphCount(*content_); - return true; - } - - // Content - if (event.is_character()) { - const size_t start = GlyphPosition(*content_, cursor_position()); - content_->insert(start, event.character()); - cursor_position()++; - option_->on_change(); - return true; - } return false; } - 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()--; + bool HandleLeftCtrl() { + int& cursor_position = option_->cursor_position(); + if (cursor_position == 0) { + return false; } + // 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: - while (cursor_position() > 0 && - IsWordCharacter(properties[cursor_position() - 1])) { - cursor_position()--; + while (cursor_position) { + size_t previous = GlyphPrevious(content_(), cursor_position); + if (!IsWordCharacter(content_(), previous)) { + break; + } + cursor_position = previous; } + return true; } - void HandleRightCtrl() { - auto properties = Utf8ToWordBreakProperty(*content_); - const int max = properties.size(); - - // Move right, as long as right is not a word character. - while (cursor_position() < max && - !IsWordCharacter(properties[cursor_position()])) { - cursor_position()++; + bool HandleRightCtrl() { + int& cursor_position = option_->cursor_position(); + if (cursor_position == (int)content_().size()) { + return false; } + // 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: - while (cursor_position() < max && - IsWordCharacter(properties[cursor_position()])) { - cursor_position()++; + while (cursor_position < (int)content_().size()) { + size_t next = GlyphNext(content_(), cursor_position); + if (!IsWordCharacter(content_(), cursor_position)) { + break; + } + cursor_position = next; } + + return true; } - bool OnMouseEvent(Event event) { - hovered_ = - box_.Contain(event.mouse().x, event.mouse().y) && CaptureMouse(event); + bool HandleMouse(Event event) { + hovered_ = box_.Contain(event.mouse().x, // + event.mouse().y) && + CaptureMouse(event); if (!hovered_) { return false; } @@ -270,32 +475,56 @@ class InputBase : public ComponentBase { } TakeFocus(); + if (content_->empty()) { + option_->cursor_position() = 0; return true; } - auto mapping = CellToGlyphIndex(*content_); - int original_glyph = cursor_position(); - original_glyph = util::clamp(original_glyph, 0, int(mapping.size())); - size_t original_cell = 0; - for (size_t i = 0; i < mapping.size(); i++) { - if (mapping[i] == original_glyph) { - original_cell = i; + // Find the line and index of the cursor. + std::vector 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; } + + cursor_char_index -= line.size() + 1; + cursor_line++; } - if (mapping[original_cell] != original_glyph) { - original_cell = mapping.size(); + 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; } - const int target_cell = - int(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 = util::clamp(target_glyph, 0, GlyphCount(*content_)); - if (cursor_position() != target_glyph) { - cursor_position() = target_glyph; - option_->on_change(); + + // 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); + } + + option_->on_change(); return true; } @@ -303,7 +532,6 @@ class InputBase : public ComponentBase { bool hovered_ = false; StringRef content_; - ConstStringRef placeholder_; Box box_; Box cursor_box_; @@ -314,7 +542,6 @@ class InputBase : public ComponentBase { /// @brief An input box for editing text. /// @param content The editable content. -/// @param placeholder The text displayed when content is still empty. /// @param option Additional optional parameters. /// @ingroup component /// @see InputBase @@ -334,15 +561,19 @@ class InputBase : public ComponentBase { /// ```bash /// placeholder /// ``` +Component Input(StringRef content, Ref option) { + return Make(std::move(content), std::move(option)); +} + Component Input(StringRef content, - ConstStringRef placeholder, + StringRef placeholder, Ref option) { - return Make(std::move(content), std::move(placeholder), - std::move(option)); + option->placeholder = placeholder; + return Make(std::move(content), std::move(option)); } } // namespace ftxui -// Copyright 2020 Arthur Sonzogni. All rights reserved. +// 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. diff --git a/src/ftxui/component/input_test.cpp b/src/ftxui/component/input_test.cpp index f187e82..057c3bd 100644 --- a/src/ftxui/component/input_test.cpp +++ b/src/ftxui/component/input_test.cpp @@ -1,27 +1,23 @@ -#include #include // for __shared_ptr_access, shared_ptr, allocator #include // for string #include "ftxui/component/component.hpp" // for Input #include "ftxui/component/component_base.hpp" // for ComponentBase, Component #include "ftxui/component/component_options.hpp" // for InputOption -#include "ftxui/component/event.hpp" // for Event, Event::ArrowLeft, Event::ArrowRight, Event::Backspace, Event::Delete, Event::End, Event::Home +#include "ftxui/component/event.hpp" // for Event, Event::ArrowRightCtrl, Event::ArrowLeftCtrl, Event::ArrowLeft, Event::ArrowRight, Event::ArrowDown, Event::ArrowUp, Event::Delete, Event::Backspace, Event::Return, Event::End, Event::Home #include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Button, Mouse::Left, Mouse::Motion, Mouse::Pressed #include "ftxui/dom/elements.hpp" // for Fit #include "ftxui/dom/node.hpp" // for Render #include "ftxui/screen/screen.hpp" // for Fixed, Screen, Pixel #include "ftxui/util/ref.hpp" // for Ref +#include "gtest/gtest.h" // for AssertionResult, Message, TestPartResult, EXPECT_EQ, EXPECT_TRUE, Test, EXPECT_FALSE, TEST -// NOLINTBEGIN namespace ftxui { TEST(InputTest, Init) { std::string content; - std::string placeholder; auto option = InputOption(); - option.cursor_position = 0; - Component input = Input(&content, &placeholder, &option); - + Component input = Input(&content, &option); EXPECT_EQ(option.cursor_position(), 0); } @@ -29,22 +25,527 @@ TEST(InputTest, Type) { std::string content; std::string placeholder; auto option = InputOption(); - option.cursor_position = 0; - Component input = Input(&content, &placeholder, &option); + Component input = Input(&content, &option); input->OnEvent(Event::Character("a")); EXPECT_EQ(content, "a"); - EXPECT_EQ(option.cursor_position(), 1u); + EXPECT_EQ(option.cursor_position(), 1); input->OnEvent(Event::Character('b')); EXPECT_EQ(content, "ab"); - EXPECT_EQ(option.cursor_position(), 2u); + EXPECT_EQ(option.cursor_position(), 2); + + input->OnEvent(Event::Return); + EXPECT_EQ(content, "ab\n"); + EXPECT_EQ(option.cursor_position(), 3); + + input->OnEvent(Event::Character('c')); + EXPECT_EQ(content, "ab\nc"); + EXPECT_EQ(option.cursor_position(), 4); auto document = input->Render(); - auto screen = Screen::Create(Dimension::Fit(document)); + + auto screen = Screen::Create(Dimension::Fixed(10), Dimension::Fixed(2)); Render(screen, document); EXPECT_EQ(screen.PixelAt(0, 0).character, "a"); EXPECT_EQ(screen.PixelAt(1, 0).character, "b"); + EXPECT_EQ(screen.PixelAt(0, 1).character, "c"); + EXPECT_EQ(screen.PixelAt(1, 1).character, " "); +} + +TEST(InputTest, ArrowLeftRight) { + std::string content = "abc测测a测\na测\n"; + auto option = InputOption(); + auto input = Input(&content, &option); + EXPECT_EQ(option.cursor_position(), 0); + + EXPECT_FALSE(input->OnEvent(Event::ArrowLeft)); + EXPECT_EQ(option.cursor_position(), 0); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 1); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_EQ(option.cursor_position(), 0); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 1); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 2); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 3); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 6); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 9); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 10); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 13); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 14); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 15); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 18); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 19); + + EXPECT_FALSE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 19); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_EQ(option.cursor_position(), 18); +} + +TEST(InputTest, ArrowUpDown) { + std::string content = + "0\n" + "00\n" + "000\n" + "0a0\n" + "00\n" + "0\n" + ""; + auto option = InputOption(); + auto input = Input(&content, &option); + + EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 4); + EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 11); + EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 21); + EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 29); + EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 36); + EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 40); + EXPECT_FALSE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 40); + + EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); + EXPECT_EQ(option.cursor_position(), 36); + EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); + EXPECT_EQ(option.cursor_position(), 29); + EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); + EXPECT_EQ(option.cursor_position(), 21); + EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); + EXPECT_EQ(option.cursor_position(), 11); + EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); + EXPECT_EQ(option.cursor_position(), 4); + EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); + EXPECT_EQ(option.cursor_position(), 0); + EXPECT_FALSE(input->OnEvent(Event::ArrowUp)); + EXPECT_EQ(option.cursor_position(), 0); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_EQ(option.cursor_position(), 3); + EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 7); + EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 14); + EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 24); + EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 32); + EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 39); + EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 40); + EXPECT_FALSE(input->OnEvent(Event::ArrowDown)); + EXPECT_EQ(option.cursor_position(), 40); + + option.cursor_position() = 39; + EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); + EXPECT_EQ(option.cursor_position(), 32); + EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); + EXPECT_EQ(option.cursor_position(), 24); + EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); + EXPECT_EQ(option.cursor_position(), 14); + EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); + EXPECT_EQ(option.cursor_position(), 7); +} + +TEST(InputTest, Insert) { + std::string content; + Component input = Input(&content); + + EXPECT_TRUE(input->OnEvent(Event::Character('a'))); + EXPECT_TRUE(input->OnEvent(Event::Character('b'))); + EXPECT_TRUE(input->OnEvent(Event::Character('c'))); + EXPECT_EQ(content, "abc"); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_TRUE(input->OnEvent(Event::Character('-'))); + EXPECT_EQ(content, "a-bc"); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_TRUE(input->OnEvent(Event::Character('-'))); + EXPECT_EQ(content, "a--bc"); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_TRUE(input->OnEvent(Event::Character('-'))); + EXPECT_EQ(content, "-a--bc"); + + EXPECT_TRUE(input->OnEvent(Event::Character("测"))); + EXPECT_EQ(content, "-测a--bc"); + + EXPECT_TRUE(input->OnEvent(Event::Character("a"))); + EXPECT_EQ(content, "-测aa--bc"); + + EXPECT_TRUE(input->OnEvent(Event::Character("测"))); + EXPECT_EQ(content, "-测a测a--bc"); +} + +TEST(InputTest, Home) { + std::string content; + auto option = InputOption(); + auto input = Input(&content, &option); + + EXPECT_TRUE(input->OnEvent(Event::Character('a'))); + EXPECT_TRUE(input->OnEvent(Event::Character('b'))); + EXPECT_TRUE(input->OnEvent(Event::Character('c'))); + EXPECT_TRUE(input->OnEvent(Event::Return)); + EXPECT_TRUE(input->OnEvent(Event::Character("测"))); + EXPECT_TRUE(input->OnEvent(Event::Character('b'))); + EXPECT_TRUE(input->OnEvent(Event::Character('c'))); + EXPECT_EQ(content, "abc\n测bc"); + EXPECT_EQ(option.cursor_position(), 9u); + + EXPECT_TRUE(input->OnEvent(Event::Home)); + EXPECT_EQ(option.cursor_position(), 0u); + + EXPECT_TRUE(input->OnEvent(Event::Character('-'))); + EXPECT_EQ(option.cursor_position(), 1u); + EXPECT_EQ(content, "-abc\n测bc"); +} + +TEST(InputTest, End) { + std::string content; + std::string placeholder; + auto option = InputOption(); + auto input = Input(&content, &option); + + EXPECT_TRUE(input->OnEvent(Event::Character('a'))); + EXPECT_TRUE(input->OnEvent(Event::Character('b'))); + EXPECT_TRUE(input->OnEvent(Event::Character('c'))); + EXPECT_TRUE(input->OnEvent(Event::Return)); + EXPECT_TRUE(input->OnEvent(Event::Character("测"))); + EXPECT_TRUE(input->OnEvent(Event::Character('b'))); + EXPECT_TRUE(input->OnEvent(Event::Character('c'))); + EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_EQ(content, "abc\n测bc"); + EXPECT_EQ(option.cursor_position(), 2u); + + input->OnEvent(Event::End); + EXPECT_EQ(option.cursor_position(), 9u); +} + +TEST(InputTest, Delete) { + std::string content; + std::string placeholder; + auto option = InputOption(); + auto input = Input(&content, &option); + + EXPECT_TRUE(input->OnEvent(Event::Character('a'))); + EXPECT_TRUE(input->OnEvent(Event::Character('b'))); + EXPECT_TRUE(input->OnEvent(Event::Character('c'))); + EXPECT_TRUE(input->OnEvent(Event::Return)); + EXPECT_TRUE(input->OnEvent(Event::Character("测"))); + EXPECT_TRUE(input->OnEvent(Event::Character('b'))); + EXPECT_TRUE(input->OnEvent(Event::Character('c'))); + + EXPECT_EQ(content, "abc\n测bc"); + EXPECT_EQ(option.cursor_position(), 9u); + + EXPECT_FALSE(input->OnEvent(Event::Delete)); + EXPECT_EQ(content, "abc\n测bc"); + EXPECT_EQ(option.cursor_position(), 9u); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_EQ(content, "abc\n测bc"); + EXPECT_EQ(option.cursor_position(), 8u); + + EXPECT_TRUE(input->OnEvent(Event::Delete)); + EXPECT_EQ(content, "abc\n测b"); + EXPECT_EQ(option.cursor_position(), 8u); + + EXPECT_FALSE(input->OnEvent(Event::Delete)); + EXPECT_EQ(content, "abc\n测b"); + EXPECT_EQ(option.cursor_position(), 8u); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_TRUE(input->OnEvent(Event::Delete)); + EXPECT_EQ(content, "abc\nb"); + EXPECT_EQ(option.cursor_position(), 4u); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_TRUE(input->OnEvent(Event::Delete)); + EXPECT_EQ(content, "abcb"); + EXPECT_EQ(option.cursor_position(), 3u); + + EXPECT_TRUE(input->OnEvent(Event::Delete)); + EXPECT_EQ(content, "abc"); + EXPECT_EQ(option.cursor_position(), 3u); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_TRUE(input->OnEvent(Event::Delete)); + EXPECT_TRUE(input->OnEvent(Event::Delete)); + EXPECT_TRUE(input->OnEvent(Event::Delete)); + EXPECT_EQ(content, ""); + + EXPECT_FALSE(input->OnEvent(Event::Delete)); + EXPECT_EQ(content, ""); +} + +TEST(InputTest, Backspace) { + std::string content; + std::string placeholder; + auto option = InputOption(); + auto input = Input(&content, &option); + + EXPECT_TRUE(input->OnEvent(Event::Character('a'))); + EXPECT_TRUE(input->OnEvent(Event::Character('b'))); + EXPECT_TRUE(input->OnEvent(Event::Character('c'))); + EXPECT_TRUE(input->OnEvent(Event::Return)); + EXPECT_TRUE(input->OnEvent(Event::Character("测"))); + EXPECT_TRUE(input->OnEvent(Event::Character('b'))); + EXPECT_TRUE(input->OnEvent(Event::Character('c'))); + + EXPECT_EQ(content, "abc\n测bc"); + EXPECT_EQ(option.cursor_position(), 9u); + + EXPECT_TRUE(input->OnEvent(Event::Backspace)); + EXPECT_EQ(content, "abc\n测b"); + EXPECT_EQ(option.cursor_position(), 8u); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); + EXPECT_TRUE(input->OnEvent(Event::Backspace)); + EXPECT_EQ(content, "abc\nb"); + EXPECT_EQ(option.cursor_position(), 4u); + + EXPECT_TRUE(input->OnEvent(Event::Backspace)); + EXPECT_EQ(content, "abcb"); + EXPECT_EQ(option.cursor_position(), 3u); + + EXPECT_TRUE(input->OnEvent(Event::Backspace)); + EXPECT_EQ(content, "abb"); + EXPECT_EQ(option.cursor_position(), 2u); + + EXPECT_TRUE(input->OnEvent(Event::Backspace)); + EXPECT_EQ(content, "ab"); + EXPECT_EQ(option.cursor_position(), 1u); + + EXPECT_TRUE(input->OnEvent(Event::Backspace)); + EXPECT_EQ(content, "b"); + EXPECT_EQ(option.cursor_position(), 0u); + + EXPECT_FALSE(input->OnEvent(Event::Backspace)); + EXPECT_EQ(content, "b"); + EXPECT_EQ(option.cursor_position(), 0u); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); + EXPECT_TRUE(input->OnEvent(Event::Backspace)); + EXPECT_EQ(content, ""); + EXPECT_EQ(option.cursor_position(), 0u); + + EXPECT_FALSE(input->OnEvent(Event::Backspace)); + EXPECT_EQ(content, ""); + EXPECT_EQ(option.cursor_position(), 0u); +} + +TEST(InputTest, CtrlArrow) { + std::string content = + "word word 测ord wo测d word\n" + "coucou coucou coucou\n" + "coucou coucou coucou\n"; + std::string placeholder; + auto option = InputOption(); + option.cursor_position = 1000; + auto input = Input(&content, &option); + + // Use CTRL+Left several time + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 67); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 60); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 53); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 46); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 39); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 29); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 24); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 17); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 10); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 5); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 0); + + EXPECT_FALSE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 0); + + 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(), 16); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 23); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 28); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 35); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 45); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 52); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 59); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 66); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 73); +} + +TEST(InputTest, CtrlArrowLeft2) { + std::string content = " word word 测ord wo测d word "; + auto option = InputOption(); + option.cursor_position = 33; + auto input = Input(&content, &option); + + // Use CTRL+Left several time + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 31); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 23); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 15); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 9); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 3); + + EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 0); + + EXPECT_FALSE(input->OnEvent(Event::ArrowLeftCtrl)); + EXPECT_EQ(option.cursor_position(), 0); +} + +TEST(InputTest, CtrlArrowRight) { + std::string content = + "word word 测ord wo测d word\n" + "coucou dfqdsf jmlkjm"; + + auto option = InputOption(); + option.cursor_position = 2; + auto input = Input(&content, &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(), 16); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 23); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 28); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 35); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 42); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 49); + + EXPECT_FALSE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 49); +} + +TEST(InputTest, CtrlArrowRight2) { + std::string content = " word word 测ord wo测d word "; + auto option = InputOption(); + auto input = Input(&content, &option); + + // Use CTRL+Left several time + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 7); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 13); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 21); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 29); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 35); + + EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 38); + + EXPECT_FALSE(input->OnEvent(Event::ArrowRightCtrl)); + EXPECT_EQ(option.cursor_position(), 38); } TEST(InputTest, TypePassword) { @@ -70,243 +571,121 @@ TEST(InputTest, TypePassword) { EXPECT_EQ(screen.PixelAt(1, 0).character, "•"); } -TEST(InputTest, Arrow) { - 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')); - - EXPECT_EQ(option.cursor_position(), 3u); - - input->OnEvent(Event::ArrowLeft); - EXPECT_EQ(option.cursor_position(), 2u); - - input->OnEvent(Event::ArrowLeft); - EXPECT_EQ(option.cursor_position(), 1u); - - input->OnEvent(Event::ArrowLeft); - EXPECT_EQ(option.cursor_position(), 0u); - - input->OnEvent(Event::ArrowLeft); - EXPECT_EQ(option.cursor_position(), 0u); - - input->OnEvent(Event::ArrowRight); - EXPECT_EQ(option.cursor_position(), 1u); - - input->OnEvent(Event::ArrowRight); - EXPECT_EQ(option.cursor_position(), 2u); - - input->OnEvent(Event::ArrowRight); - EXPECT_EQ(option.cursor_position(), 3u); - - input->OnEvent(Event::ArrowRight); - EXPECT_EQ(option.cursor_position(), 3u); -} - -TEST(InputTest, Insert) { - std::string content; - std::string placeholder; - Component input = Input(&content, &placeholder); - - input->OnEvent(Event::Character('a')); - input->OnEvent(Event::Character('b')); - input->OnEvent(Event::Character('c')); - EXPECT_EQ(content, "abc"); - - input->OnEvent(Event::ArrowLeft); - input->OnEvent(Event::ArrowLeft); - input->OnEvent(Event::Character('-')); - EXPECT_EQ(content, "a-bc"); - - input->OnEvent(Event::ArrowLeft); - input->OnEvent(Event::Character('-')); - EXPECT_EQ(content, "a--bc"); - - input->OnEvent(Event::ArrowLeft); - input->OnEvent(Event::ArrowLeft); - input->OnEvent(Event::ArrowLeft); - input->OnEvent(Event::Character('-')); - EXPECT_EQ(content, "-a--bc"); -} - -TEST(InputTest, Home) { - 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')); - EXPECT_EQ(content, "abc"); - - EXPECT_EQ(option.cursor_position(), 3u); - input->OnEvent(Event::Home); - EXPECT_EQ(option.cursor_position(), 0u); - - input->OnEvent(Event::Character('-')); - EXPECT_EQ(content, "-abc"); -} - -TEST(InputTest, End) { - 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::ArrowLeft); - input->OnEvent(Event::ArrowLeft); - - EXPECT_EQ(option.cursor_position(), 1u); - input->OnEvent(Event::End); - EXPECT_EQ(option.cursor_position(), 3u); -} - -TEST(InputTest, Delete) { - 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::ArrowLeft); - - EXPECT_EQ(content, "abc"); - EXPECT_EQ(option.cursor_position(), 2u); - - input->OnEvent(Event::Delete); - EXPECT_EQ(content, "ab"); - EXPECT_EQ(option.cursor_position(), 2u); - - input->OnEvent(Event::Delete); - EXPECT_EQ(content, "ab"); - EXPECT_EQ(option.cursor_position(), 2u); -} - -TEST(InputTest, Backspace) { - 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::ArrowLeft); - - EXPECT_EQ(content, "abc"); - EXPECT_EQ(option.cursor_position(), 2u); - - input->OnEvent(Event::Backspace); - EXPECT_EQ(content, "ac"); - EXPECT_EQ(option.cursor_position(), 1u); - - input->OnEvent(Event::Backspace); - EXPECT_EQ(content, "c"); - EXPECT_EQ(option.cursor_position(), 0u); - - input->OnEvent(Event::Backspace); - EXPECT_EQ(content, "c"); - 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); + auto input = Input(&content, &option); input->OnEvent(Event::Character("a")); input->OnEvent(Event::Character("b")); input->OnEvent(Event::Character("c")); input->OnEvent(Event::Character("d")); + input->OnEvent(Event::Return); + input->OnEvent(Event::Character("a")); + input->OnEvent(Event::Character("b")); + input->OnEvent(Event::Character("c")); + input->OnEvent(Event::Character("d")); + input->OnEvent(Event::Return); - EXPECT_EQ(option.cursor_position(), 4u); + EXPECT_EQ(content, "abcd\nabcd\n"); + EXPECT_EQ(option.cursor_position(), 10u); auto render = [&] { auto document = input->Render(); - auto screen = Screen::Create(Dimension::Fixed(10), Dimension::Fixed(1)); + auto screen = Screen::Create(Dimension::Fixed(10), Dimension::Fixed(3)); Render(screen, document); }; render(); + EXPECT_EQ(option.cursor_position(), 10u); 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)); + mouse.y = 0; + EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); render(); EXPECT_EQ(option.cursor_position(), 0u); mouse.x = 2; - input->OnEvent(Event::Mouse("", mouse)); + mouse.y = 0; + EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); render(); EXPECT_EQ(option.cursor_position(), 2u); mouse.x = 2; - input->OnEvent(Event::Mouse("", mouse)); + mouse.y = 0; + EXPECT_FALSE(input->OnEvent(Event::Mouse("", mouse))); render(); EXPECT_EQ(option.cursor_position(), 2u); mouse.x = 1; - input->OnEvent(Event::Mouse("", mouse)); + mouse.y = 0; + EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); render(); EXPECT_EQ(option.cursor_position(), 1u); mouse.x = 3; - input->OnEvent(Event::Mouse("", mouse)); + mouse.y = 0; + EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); render(); EXPECT_EQ(option.cursor_position(), 3u); mouse.x = 4; - input->OnEvent(Event::Mouse("", mouse)); + mouse.y = 0; + EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); render(); EXPECT_EQ(option.cursor_position(), 4u); mouse.x = 5; - input->OnEvent(Event::Mouse("", mouse)); + mouse.y = 0; + EXPECT_FALSE(input->OnEvent(Event::Mouse("", mouse))); render(); EXPECT_EQ(option.cursor_position(), 4u); + + mouse.x = 5; + mouse.y = 1; + EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); + render(); + EXPECT_EQ(option.cursor_position(), 9u); + + mouse.x = 1; + mouse.y = 1; + EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); + render(); + EXPECT_EQ(option.cursor_position(), 6u); + + mouse.x = 4; + mouse.y = 2; + EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); + render(); + EXPECT_EQ(option.cursor_position(), 10u); } TEST(InputTest, MouseClickComplex) { std::string content; - std::string placeholder; auto option = InputOption(); - option.cursor_position = 0; - auto input = Input(&content, &placeholder, &option); + auto input = Input(&content, &option); + input->OnEvent(Event::Character("测")); + input->OnEvent(Event::Character("试")); + input->OnEvent(Event::Character("a⃒")); + input->OnEvent(Event::Character("ā")); + input->OnEvent(Event::Return); input->OnEvent(Event::Character("测")); input->OnEvent(Event::Character("试")); input->OnEvent(Event::Character("a⃒")); input->OnEvent(Event::Character("ā")); - EXPECT_EQ(option.cursor_position(), 4u); + EXPECT_EQ(option.cursor_position(), 27u); auto render = [&] { auto document = input->Render(); - auto screen = Screen::Create(Dimension::Fixed(10), Dimension::Fixed(1)); + auto screen = Screen::Create(Dimension::Fixed(100), Dimension::Fixed(4)); Render(screen, document); }; render(); @@ -314,183 +693,37 @@ TEST(InputTest, MouseClickComplex) { 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)); + mouse.y = 0; + EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); render(); - EXPECT_EQ(option.cursor_position(), 0u); + EXPECT_EQ(option.cursor_position(), 0); mouse.x = 0; - input->OnEvent(Event::Mouse("", mouse)); + mouse.y = 1; + EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); render(); - EXPECT_EQ(option.cursor_position(), 0u); + EXPECT_EQ(option.cursor_position(), 14); mouse.x = 1; - input->OnEvent(Event::Mouse("", mouse)); + mouse.y = 0; + EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); render(); - EXPECT_EQ(option.cursor_position(), 0u); + EXPECT_EQ(option.cursor_position(), 3); mouse.x = 1; - input->OnEvent(Event::Mouse("", mouse)); + mouse.y = 1; + EXPECT_TRUE(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); -} - -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); + EXPECT_EQ(option.cursor_position(), 17); } } // namespace ftxui -// NOLINTEND -// Copyright 2021 Arthur Sonzogni. All rights reserved. +// Copyright 2023 Arthur Sonzogni. All rights reserved. // Use of this source code is governed by the MIT license that can be found in // the LICENSE file. diff --git a/src/ftxui/component/radiobox_test.cpp b/src/ftxui/component/radiobox_test.cpp index c743a22..eca86ec 100644 --- a/src/ftxui/component/radiobox_test.cpp +++ b/src/ftxui/component/radiobox_test.cpp @@ -1,4 +1,3 @@ -#include // for AssertionResult, Message, TestPartResult, EXPECT_EQ, EXPECT_TRUE, Test, TestInfo (ptr only), EXPECT_FALSE, TEST #include // for yframe #include // for Render #include // for Screen @@ -11,6 +10,7 @@ #include "ftxui/component/component_options.hpp" // for RadioboxOption #include "ftxui/component/event.hpp" // for Event, Event::Return, Event::ArrowDown, Event::End, Event::Home, Event::Tab, Event::TabReverse, Event::PageDown, Event::PageUp, Event::ArrowUp #include "ftxui/util/ref.hpp" // for Ref +#include "gtest/gtest.h" // for AssertionResult, Message, TestPartResult, EXPECT_EQ, EXPECT_TRUE, Test, EXPECT_FALSE, TEST // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/component/receiver_test.cpp b/src/ftxui/component/receiver_test.cpp index a45e9f3..0d83b7e 100644 --- a/src/ftxui/component/receiver_test.cpp +++ b/src/ftxui/component/receiver_test.cpp @@ -1,8 +1,9 @@ -#include +#include // for string #include // for thread #include // for move #include "ftxui/component/receiver.hpp" +#include "gtest/gtest.h" // for AssertionResult, Message, Test, TestPartResult, EXPECT_EQ, EXPECT_TRUE, EXPECT_FALSE, TEST // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/component/resizable_split_test.cpp b/src/ftxui/component/resizable_split_test.cpp index 627d412..851f9c3 100644 --- a/src/ftxui/component/resizable_split_test.cpp +++ b/src/ftxui/component/resizable_split_test.cpp @@ -1,6 +1,6 @@ -#include // for AssertionResult, Message, TestPartResult, Test, EXPECT_EQ, EXPECT_TRUE, TestInfo (ptr only), TEST #include // for Direction, Direction::Down, Direction::Left, Direction::Right, Direction::Up #include // for __shared_ptr_access, shared_ptr, allocator +#include // for string #include "ftxui/component/component.hpp" // for ResizableSplit, Renderer, ResizableSplitBottom, ResizableSplitLeft, ResizableSplitRight, ResizableSplitTop #include "ftxui/component/component_base.hpp" // for ComponentBase, Component @@ -9,6 +9,7 @@ #include "ftxui/dom/elements.hpp" // for Element, separatorDouble, text #include "ftxui/dom/node.hpp" // for Render #include "ftxui/screen/screen.hpp" // for Screen +#include "gtest/gtest.h" // for AssertionResult, Message, TestPartResult, Test, EXPECT_EQ, EXPECT_TRUE, TEST // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/component/slider_test.cpp b/src/ftxui/component/slider_test.cpp index c6159db..af4d7f1 100644 --- a/src/ftxui/component/slider_test.cpp +++ b/src/ftxui/component/slider_test.cpp @@ -1,17 +1,17 @@ -#include // for AssertionResult, Message, TestPartResult, Test, EXPECT_EQ, EXPECT_TRUE, TestInfo (ptr only), EXPECT_FALSE, TEST -#include // for array -#include // for size_t +#include // for array +#include // for size_t #include // for Mouse, Mouse::Left, Mouse::Pressed, Mouse::Released #include // for Direction, Direction::Down, Direction::Left, Direction::Right, Direction::Up #include // for frame #include // for shared_ptr, __shared_ptr_access, allocator -#include // for to_string +#include // for string, to_string #include "ftxui/component/component.hpp" // for Slider, Vertical, operator|= #include "ftxui/component/component_base.hpp" // for ComponentBase #include "ftxui/component/event.hpp" // for Event, Event::ArrowDown #include "ftxui/dom/node.hpp" // for Render #include "ftxui/screen/screen.hpp" // for Screen +#include "gtest/gtest.h" // for AssertionResult, Message, TestPartResult, Test, EXPECT_EQ, EXPECT_TRUE, EXPECT_FALSE, TEST // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/component/terminal_input_parser.cpp b/src/ftxui/component/terminal_input_parser.cpp index 72da0d1..9fad1ba 100644 --- a/src/ftxui/component/terminal_input_parser.cpp +++ b/src/ftxui/component/terminal_input_parser.cpp @@ -37,12 +37,12 @@ const std::map g_uniformize = { // Home ESC [ H ESC O H // End ESC [ F ESC O F // - {"\x1BOA", "\x1B[A"}, // UP - {"\x1BOB", "\x1B[B"}, // DOWN - {"\x1BOC", "\x1B[C"}, // RIGHT - {"\x1BOD", "\x1B[D"}, // LEFT - {"\x1BOH", "\x1B[H"}, // HOME - {"\x1BOF", "\x1B[F"}, // END + {"\x1BOA", "\x1B[A"}, // UP + {"\x1BOB", "\x1B[B"}, // DOWN + {"\x1BOC", "\x1B[C"}, // RIGHT + {"\x1BOD", "\x1B[D"}, // LEFT + {"\x1BOH", "\x1B[H"}, // HOME + {"\x1BOF", "\x1B[F"}, // END }; diff --git a/src/ftxui/component/terminal_input_parser_test.cpp b/src/ftxui/component/terminal_input_parser_test.cpp index 4eec23b..79ca86d 100644 --- a/src/ftxui/component/terminal_input_parser_test.cpp +++ b/src/ftxui/component/terminal_input_parser_test.cpp @@ -1,13 +1,13 @@ -#include // for AssertionResult, Test, Message, TestPartResult, SuiteApiResolver, TestInfo (ptr only), EXPECT_EQ, EXPECT_TRUE, TEST, TestFactoryImpl, EXPECT_FALSE #include // for Mouse, Mouse::Left, Mouse::Middle, Mouse::Pressed, Mouse::Released, Mouse::Right #include // for Task #include // for initializer_list #include // for allocator, unique_ptr #include // for get -#include "ftxui/component/event.hpp" // for Event, Event::Return, Event::ArrowDown, Event::ArrowLeft, Event::ArrowRight, Event::ArrowUp, Event::Backspace, Event::Custom, Event::Delete, Event::End, Event::F10, Event::F11, Event::F12, Event::F5, Event::F6, Event::F7, Event::F8, Event::F9, Event::Home, Event::PageDown, Event::PageUp, Event::Tab, Event::TabReverse, Event::Escape +#include "ftxui/component/event.hpp" // for Event, Event::Return, Event::ArrowDown, Event::ArrowLeft, Event::ArrowRight, Event::ArrowUp, Event::Backspace, Event::End, Event::Home, Event::Custom, Event::Delete, Event::F1, Event::F10, Event::F11, Event::F12, Event::F2, Event::F3, Event::F4, Event::F5, Event::F6, Event::F7, Event::F8, Event::F9, Event::PageDown, Event::PageUp, Event::Tab, Event::TabReverse, Event::Escape #include "ftxui/component/receiver.hpp" // for MakeReceiver, ReceiverImpl #include "ftxui/component/terminal_input_parser.hpp" +#include "gtest/gtest.h" // for AssertionResult, Test, Message, TestPartResult, EXPECT_EQ, EXPECT_TRUE, TEST, EXPECT_FALSE // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/component/toggle_test.cpp b/src/ftxui/component/toggle_test.cpp index 7e61fd1..b3dd5b0 100644 --- a/src/ftxui/component/toggle_test.cpp +++ b/src/ftxui/component/toggle_test.cpp @@ -1,14 +1,14 @@ -#include // for AssertionResult, Message, TestPartResult, EXPECT_EQ, Test, EXPECT_TRUE, TestInfo (ptr only), EXPECT_FALSE, TEST -#include // for function -#include // for __shared_ptr_access, shared_ptr, allocator -#include // for string, basic_string -#include // for vector +#include // for function +#include // for __shared_ptr_access, shared_ptr, allocator +#include // for string, basic_string +#include // for vector #include "ftxui/component/component.hpp" // for Menu, Toggle #include "ftxui/component/component_base.hpp" // for ComponentBase #include "ftxui/component/component_options.hpp" // for MenuOption #include "ftxui/component/event.hpp" // for Event, Event::ArrowLeft, Event::ArrowRight, Event::Return, Event::Tab, Event::TabReverse #include "ftxui/util/ref.hpp" // for Ref +#include "gtest/gtest.h" // for AssertionResult, Message, TestPartResult, EXPECT_EQ, Test, EXPECT_TRUE, EXPECT_FALSE, TEST // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/dom/blink_test.cpp b/src/ftxui/dom/blink_test.cpp index c0713c1..2a96825 100644 --- a/src/ftxui/dom/blink_test.cpp +++ b/src/ftxui/dom/blink_test.cpp @@ -1,9 +1,9 @@ -#include -#include // for allocator +#include // for allocator, string #include "ftxui/dom/elements.hpp" // for operator|, text, blink, Element #include "ftxui/dom/node.hpp" // for Render #include "ftxui/screen/screen.hpp" // for Screen, Pixel +#include "gtest/gtest.h" // for Test, AssertionResult, EXPECT_TRUE, Message, TEST, TestPartResult // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/dom/bold_test.cpp b/src/ftxui/dom/bold_test.cpp index c5ae167..9007bb3 100644 --- a/src/ftxui/dom/bold_test.cpp +++ b/src/ftxui/dom/bold_test.cpp @@ -1,9 +1,9 @@ -#include -#include // for allocator +#include // for allocator, string #include "ftxui/dom/elements.hpp" // for operator|, text, bold, Element #include "ftxui/dom/node.hpp" // for Render #include "ftxui/screen/screen.hpp" // for Screen, Pixel +#include "gtest/gtest.h" // for Test, AssertionResult, EXPECT_TRUE, Message, TEST, TestPartResult // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/dom/dim_test.cpp b/src/ftxui/dom/dim_test.cpp index 2073158..d1d0198 100644 --- a/src/ftxui/dom/dim_test.cpp +++ b/src/ftxui/dom/dim_test.cpp @@ -1,9 +1,9 @@ -#include -#include // for allocator +#include // for allocator, string #include "ftxui/dom/elements.hpp" // for operator|, text, dim, Element #include "ftxui/dom/node.hpp" // for Render #include "ftxui/screen/screen.hpp" // for Screen, Pixel +#include "gtest/gtest.h" // for Test, AssertionResult, EXPECT_TRUE, Message, TEST, TestPartResult // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/dom/text.cpp b/src/ftxui/dom/text.cpp index 10017e9..c598be1 100644 --- a/src/ftxui/dom/text.cpp +++ b/src/ftxui/dom/text.cpp @@ -35,6 +35,9 @@ class Text : public Node { if (x > box_.x_max) { return; } + if (cell == "\n") { + continue; + } screen.PixelAt(x, y).character = cell; ++x; } diff --git a/src/ftxui/dom/underlined_test.cpp b/src/ftxui/dom/underlined_test.cpp index ba30d1a..acb3947 100644 --- a/src/ftxui/dom/underlined_test.cpp +++ b/src/ftxui/dom/underlined_test.cpp @@ -1,9 +1,9 @@ -#include -#include // for allocator +#include // for allocator, string #include "ftxui/dom/elements.hpp" // for operator|, text, underlined, Element #include "ftxui/dom/node.hpp" // for Render #include "ftxui/screen/screen.hpp" // for Screen, Pixel +#include "gtest/gtest.h" // for Test, AssertionResult, EXPECT_TRUE, Message, TEST, TestPartResult // NOLINTBEGIN namespace ftxui { diff --git a/src/ftxui/screen/string.cpp b/src/ftxui/screen/string.cpp index 1fb5c33..37305e7 100644 --- a/src/ftxui/screen/string.cpp +++ b/src/ftxui/screen/string.cpp @@ -7,15 +7,22 @@ #include "ftxui/screen/string.hpp" -#include // for array -#include // for uint32_t, uint8_t, uint16_t, int32_t -#include // for string, basic_string, wstring -#include // for _Swallow_assign, ignore +#include // for size_t +#include // for array +#include // for uint32_t, uint8_t, uint16_t, int32_t +#include // for string, basic_string, wstring +#include // for _Swallow_assign, ignore -#include "ftxui/screen/deprecated.hpp" // for wchar_width, wstring_width +#include "ftxui/screen/deprecated.hpp" // for wchar_width, wstring_width +#include "ftxui/screen/string_internal.hpp" // for WordBreakProperty, EatCodePoint, CodepointToWordBreakProperty, GlyphCount, GlyphIterate, GlyphNext, GlyphPrevious, IsCombining, IsControl, IsFullWidth, Utf8ToWordBreakProperty namespace { +using ftxui::EatCodePoint; +using ftxui::IsCombining; +using ftxui::IsControl; +using ftxui::IsFullWidth; + struct Interval { uint32_t first; uint32_t last; @@ -1411,46 +1418,26 @@ bool Bisearch(uint32_t ucs, const std::array table, C* out) { return false; } -bool IsCombining(uint32_t ucs) { - return ftxui::CodepointToWordBreakProperty(ucs) == WBP::Extend; -} - -bool IsFullWidth(uint32_t ucs) { - if (ucs < 0x0300) // Quick path: // NOLINT - return false; - - return Bisearch(ucs, g_full_width_characters); -} - -bool IsControl(uint32_t ucs) { - if (ucs == 0) { - return true; - } - if (ucs < 32) { // NOLINT - return true; - } - if (ucs >= 0x7f && ucs < 0xa0) { // NOLINT - return true; - } - return false; -} - int codepoint_width(uint32_t ucs) { - if (IsControl(ucs)) { + if (ftxui::IsControl(ucs)) { return -1; } - if (IsCombining(ucs)) { + if (ftxui::IsCombining(ucs)) { return 0; } - if (IsFullWidth(ucs)) { + if (ftxui::IsFullWidth(ucs)) { return 2; } return 1; } +} // namespace + +namespace ftxui { + // From UTF8 encoded string |input|, eat in between 1 and 4 byte representing // one codepoint. Put the codepoint into |ucs|. Start at |start| and update // |end| to represent the beginning of the next byte to eat for consecutive @@ -1563,9 +1550,29 @@ bool EatCodePoint(const std::wstring& input, return true; } -} // namespace +bool IsCombining(uint32_t ucs) { + return ftxui::CodepointToWordBreakProperty(ucs) == WBP::Extend; +} -namespace ftxui { +bool IsFullWidth(uint32_t ucs) { + if (ucs < 0x0300) // Quick path: // NOLINT + return false; + + return Bisearch(ucs, g_full_width_characters); +} + +bool IsControl(uint32_t ucs) { + if (ucs == 0) { + return true; + } + if (ucs < 32) { // NOLINT + return ucs != 10; // 10 => Line feed. + } + if (ucs >= 0x7f && ucs < 0xa0) { // NOLINT + return true; + } + return false; +} WordBreakProperty CodepointToWordBreakProperty(uint32_t codepoint) { WordBreakPropertyInterval interval = {0, 0, WBP::ALetter}; @@ -1660,12 +1667,35 @@ std::vector Utf8ToGlyphs(const std::string& input) { return out; } -int GlyphPosition(const std::string& input, size_t glyph_index, size_t start) { - if (glyph_index <= 0) { - return 0; +size_t GlyphPrevious(const std::string& input, size_t start) { + while (true) { + if (start == 0) { + return 0; + } + start--; + + // Skip the UTF8 continuation bytes. + if ((input[start] & 0b1100'0000) == 0b1000'0000) { + continue; + } + + uint32_t codepoint = 0; + size_t end = 0; + const bool eaten = EatCodePoint(input, start, &end, &codepoint); + + // Ignore invalid, control characters and combining characters. + if (!eaten || IsControl(codepoint) || IsCombining(codepoint)) { + continue; + } + + return start; } - size_t end = 0; +} + +size_t GlyphNext(const std::string& input, size_t start) { + bool glyph_found = false; while (start < input.size()) { + size_t end = 0; uint32_t codepoint = 0; const bool eaten = EatCodePoint(input, start, &end, &codepoint); @@ -1677,17 +1707,31 @@ int GlyphPosition(const std::string& input, size_t glyph_index, size_t start) { // We eat the beginning of the next glyph. If we are eating the one // requested, return its start position immediately. - if (glyph_index == 0) { + if (glyph_found) { return static_cast(start); } // Otherwise, skip this glyph and iterate: - glyph_index--; + glyph_found = true; start = end; } return static_cast(input.size()); } +size_t GlyphIterate(const std::string& input, int glyph_offset, size_t start) { + if (glyph_offset >= 0) { + for (int i = 0; i < glyph_offset; ++i) { + start = GlyphNext(input, start); + } + return start; + } else { + for (int i = 0; i < -glyph_offset; ++i) { + start = GlyphPrevious(input, start); + } + return start; + } +} + std::vector CellToGlyphIndex(const std::string& input) { int x = -1; std::vector out; diff --git a/src/ftxui/screen/string_internal.hpp b/src/ftxui/screen/string_internal.hpp new file mode 100644 index 0000000..eab4aa2 --- /dev/null +++ b/src/ftxui/screen/string_internal.hpp @@ -0,0 +1,64 @@ +#ifndef FTXUI_SCREEN_STRING_INTERNAL_HPP +#define FTXUI_SCREEN_STRING_INTERNAL_HPP + +namespace ftxui { + +bool EatCodePoint(const std::string& input, + size_t start, + size_t* end, + uint32_t* ucs); +bool EatCodePoint(const std::wstring& input, + size_t start, + size_t* end, + uint32_t* ucs); + +bool IsCombining(uint32_t ucs); +bool IsFullWidth(uint32_t ucs); +bool IsControl(uint32_t ucs); + +size_t GlyphPrevious(const std::string& input, size_t start); +size_t GlyphNext(const std::string& input, size_t start); + +// Return the index in the |input| string of the glyph at |glyph_offset|, +// starting at |start| +size_t GlyphIterate(const std::string& input, + int glyph_offset, + size_t start = 0); + +// Returns the number of glyphs in |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, +}; +WordBreakProperty CodepointToWordBreakProperty(uint32_t codepoint); +std::vector Utf8ToWordBreakProperty( + const std::string& input); + +bool IsWordBreakingCharacter(const std::string& input, size_t glyph_index); +} // namespace ftxui + +#endif /* end of include guard: FTXUI_SCREEN_STRING_INTERNAL_HPP */ + +// Copyright 2023 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. diff --git a/src/ftxui/screen/string_test.cpp b/src/ftxui/screen/string_test.cpp index 57a220d..98c8fae 100644 --- a/src/ftxui/screen/string_test.cpp +++ b/src/ftxui/screen/string_test.cpp @@ -1,6 +1,7 @@ #include "ftxui/screen/string.hpp" #include #include // for allocator, string +#include "ftxui/screen/string_internal.hpp" namespace ftxui { @@ -61,41 +62,41 @@ TEST(StringTest, GlyphCount) { EXPECT_EQ(GlyphCount("a\1a"), 2); } -TEST(StringTest, GlyphPosition) { +TEST(StringTest, GlyphIterate) { // 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); + EXPECT_EQ(GlyphIterate("", -1), 0); + EXPECT_EQ(GlyphIterate("", 0), 0); + EXPECT_EQ(GlyphIterate("", 1), 0); + EXPECT_EQ(GlyphIterate("a", 0), 0); + EXPECT_EQ(GlyphIterate("a", 1), 1); + EXPECT_EQ(GlyphIterate("ab", 0), 0); + EXPECT_EQ(GlyphIterate("ab", 1), 1); + EXPECT_EQ(GlyphIterate("ab", 2), 2); + EXPECT_EQ(GlyphIterate("abc", 0), 0); + EXPECT_EQ(GlyphIterate("abc", 1), 1); + EXPECT_EQ(GlyphIterate("abc", 2), 2); + EXPECT_EQ(GlyphIterate("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); + EXPECT_EQ(GlyphIterate("测", 0), 0); + EXPECT_EQ(GlyphIterate("测", 1), 3); + EXPECT_EQ(GlyphIterate("测试", 0), 0); + EXPECT_EQ(GlyphIterate("测试", 1), 3); + EXPECT_EQ(GlyphIterate("测试", 2), 6); + EXPECT_EQ(GlyphIterate("测试", 1, 3), 6); + EXPECT_EQ(GlyphIterate("测试", 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); + EXPECT_EQ(GlyphIterate("ā", 0), 0); + EXPECT_EQ(GlyphIterate("ā", 1), 3); + EXPECT_EQ(GlyphIterate("a⃒a̗ā", 0), 0); + EXPECT_EQ(GlyphIterate("a⃒a̗ā", 1), 4); + EXPECT_EQ(GlyphIterate("a⃒a̗ā", 2), 7); + EXPECT_EQ(GlyphIterate("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); + EXPECT_EQ(GlyphIterate("\1", 0), 0); + EXPECT_EQ(GlyphIterate("\1", 1), 1); + EXPECT_EQ(GlyphIterate("a\1a", 0), 0); + EXPECT_EQ(GlyphIterate("a\1a", 1), 2); + EXPECT_EQ(GlyphIterate("a\1a", 2), 3); } TEST(StringTest, CellToGlyphIndex) { @@ -135,7 +136,7 @@ TEST(StringTest, Utf8ToWordBreakProperty) { EXPECT_EQ(Utf8ToWordBreakProperty(":"), T({P::MidLetter})); EXPECT_EQ(Utf8ToWordBreakProperty("."), T({P::MidNumLet})); EXPECT_EQ(Utf8ToWordBreakProperty("\r"), T({})); // FIXME - EXPECT_EQ(Utf8ToWordBreakProperty("\n"), T({})); // FIXME + EXPECT_EQ(Utf8ToWordBreakProperty("\n"), T({P::LF})); } TEST(StringTest, to_string) {