Remove Ref<XxxOption> and add new interfaces. (#686)

1. Stop taking Ref<XxxOption> in Component constructors. Instead, use
   the XxxOption directly. Passing by copy avoid problems developers had
   where one was shared in between multiple component, causing issues.

2. Add variants of most component constructors taking a struct only.

This replaces:
https://github.com/ArthurSonzogni/FTXUI/pull/670

This fixes:
https://github.com/ArthurSonzogni/FTXUI/issues/426
This commit is contained in:
Arthur Sonzogni 2023-06-25 17:22:05 +02:00 committed by GitHub
parent e73e7f0d68
commit 455998d759
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 918 additions and 693 deletions

View File

@ -1,6 +1,8 @@
--- ---
Checks: "*, Checks: "*,
-*-magic-numbers,
-*-narrowing-conversions -*-narrowing-conversions
-*-unnecessary-value-param,
-*-uppercase-literal-suffix, -*-uppercase-literal-suffix,
-abseil-*, -abseil-*,
-altera-*, -altera-*,
@ -9,6 +11,7 @@ Checks: "*,
-cppcoreguidelines-non-private-member-variables-in-classes, -cppcoreguidelines-non-private-member-variables-in-classes,
-fuchsia-*, -fuchsia-*,
-google-*, -google-*,
-hicpp-signed-bitwise,
-llvm*, -llvm*,
-misc-no-recursion, -misc-no-recursion,
-misc-non-private-member-variables-in-classes, -misc-non-private-member-variables-in-classes,

View File

@ -9,10 +9,23 @@ current (development)
- Breaking: GaugeDirection enum is renamed Direction - Breaking: GaugeDirection enum is renamed Direction
- Breaking: Direction enum is renamed WidthOrHeight - Breaking: Direction enum is renamed WidthOrHeight
- Breaking: Remove `ComponentBase` copy constructor/assignment. - Breaking: Remove `ComponentBase` copy constructor/assignment.
- Breaking: MenuOption::entries is renamed MenuOption::entries_option.
- Breaking: Ref<XxxOption> becomes XxxOption in component constructors.
- Feature: `ResizeableSplit` now support arbitrary element as a separator. - Feature: `ResizeableSplit` now support arbitrary element as a separator.
- Feature: `input` is now supporting multiple lines. - Feature: `input` is now supporting multiple lines.
- Feature: `input` style is now customizeable. - Feature: `input` style is now customizeable.
- Bugfix: Support F1-F5 from OS terminal. - Bugfix: Support F1-F5 from OS terminal.
- Feature: Add struct based constructor:
```cpp
Component Button(ButtonOption options);
Component Checkbox(CheckboxOption options);
Component Input(InputOption options);
Component Menu(MenuOption options);
Component MenuEntry(MenuEntryOption options);
Component Radiobox(RadioboxOption options);
Component Slider(SliderOption<T> options);
Component ResizableSplit(ResizableSplitOption options);
```
### Dom ### Dom
- Feature: Add `hyperlink` decorator. For instance: - Feature: Add `hyperlink` decorator. For instance:

View File

@ -69,4 +69,6 @@ gtest_discover_tests(ftxui-tests
DISCOVERY_TIMEOUT 600 DISCOVERY_TIMEOUT 600
) )
set(CMAKE_CTEST_ARGUMENTS "--rerun-failed --output-on-failure") #set(CMAKE_CTEST_ARGUMENTS "--rerun-failed --output-on-failure")
#set_tests_properties(gen_init_queries PROPERTIES FIXTURES_SETUP f_init_queries)
#set_tests_properties(test PROPERTIES FIXTURES_REQUIRED f_init_queries)

View File

@ -15,13 +15,12 @@ int main() {
int counter = 0; int counter = 0;
auto on_click = [&] { counter++; }; auto on_click = [&] { counter++; };
auto button_style = ButtonOption::Animated(Color::Default, Color::GrayDark, auto style = ButtonOption::Animated(Color::Default, Color::GrayDark,
Color::Default, Color::White); Color::Default, Color::White);
auto container = Container::Vertical({}); auto container = Container::Vertical({});
for (int i = 0; i < 30; ++i) { for (int i = 0; i < 30; ++i) {
auto button = auto button = Button("Button " + std::to_string(i), on_click, style);
Button("Button " + std::to_string(i), on_click, &button_style);
container->Add(button); container->Add(button);
} }

View File

@ -21,7 +21,7 @@ int main() {
MenuOption option; MenuOption option;
option.on_enter = screen.ExitLoopClosure(); option.on_enter = screen.ExitLoopClosure();
auto menu = Menu(&entries, &selected, &option); auto menu = Menu(&entries, &selected, option);
screen.Loop(menu); screen.Loop(menu);

View File

@ -27,9 +27,9 @@ int main() {
int left_menu_selected = 0; int left_menu_selected = 0;
int right_menu_selected = 0; int right_menu_selected = 0;
Component left_menu_ = Component left_menu_ =
Menu(&left_menu_entries, &left_menu_selected, &menu_option); Menu(&left_menu_entries, &left_menu_selected, menu_option);
Component right_menu_ = Component right_menu_ =
Menu(&right_menu_entries, &right_menu_selected, &menu_option); Menu(&right_menu_entries, &right_menu_selected, menu_option);
Component container = Container::Horizontal({ Component container = Container::Horizontal({
left_menu_, left_menu_,

View File

@ -110,7 +110,7 @@ int main() {
Component VMenu1(std::vector<std::string>* entries, int* selected) { Component VMenu1(std::vector<std::string>* entries, int* selected) {
auto option = MenuOption::Vertical(); auto option = MenuOption::Vertical();
option.entries.transform = [](EntryState state) { option.entries_option.transform = [](EntryState state) {
state.label = (state.active ? "> " : " ") + state.label; state.label = (state.active ? "> " : " ") + state.label;
Element e = text(state.label); Element e = text(state.label);
if (state.focused) if (state.focused)
@ -124,7 +124,7 @@ Component VMenu1(std::vector<std::string>* entries, int* selected) {
Component VMenu2(std::vector<std::string>* entries, int* selected) { Component VMenu2(std::vector<std::string>* entries, int* selected) {
auto option = MenuOption::Vertical(); auto option = MenuOption::Vertical();
option.entries.transform = [](EntryState state) { option.entries_option.transform = [](EntryState state) {
state.label += (state.active ? " <" : " "); state.label += (state.active ? " <" : " ");
Element e = hbox(filler(), text(state.label)); Element e = hbox(filler(), text(state.label));
if (state.focused) if (state.focused)
@ -138,7 +138,7 @@ Component VMenu2(std::vector<std::string>* entries, int* selected) {
Component VMenu3(std::vector<std::string>* entries, int* selected) { Component VMenu3(std::vector<std::string>* entries, int* selected) {
auto option = MenuOption::Vertical(); auto option = MenuOption::Vertical();
option.entries.transform = [](EntryState state) { option.entries_option.transform = [](EntryState state) {
Element e = state.active ? text("[" + state.label + "]") Element e = state.active ? text("[" + state.label + "]")
: text(" " + state.label + " "); : text(" " + state.label + " ");
if (state.focused) if (state.focused)
@ -155,7 +155,7 @@ Component VMenu3(std::vector<std::string>* entries, int* selected) {
Component VMenu4(std::vector<std::string>* entries, int* selected) { Component VMenu4(std::vector<std::string>* entries, int* selected) {
auto option = MenuOption::Vertical(); auto option = MenuOption::Vertical();
option.entries.transform = [](EntryState state) { option.entries_option.transform = [](EntryState state) {
if (state.active && state.focused) { if (state.active && state.focused) {
return text(state.label) | color(Color::Yellow) | bgcolor(Color::Black) | return text(state.label) | color(Color::Yellow) | bgcolor(Color::Black) |
bold; bold;
@ -175,7 +175,7 @@ Component VMenu4(std::vector<std::string>* entries, int* selected) {
Component VMenu5(std::vector<std::string>* entries, int* selected) { Component VMenu5(std::vector<std::string>* entries, int* selected) {
auto option = MenuOption::Vertical(); auto option = MenuOption::Vertical();
option.entries.transform = [](EntryState state) { option.entries_option.transform = [](EntryState state) {
auto element = text(state.label); auto element = text(state.label);
if (state.active && state.focused) { if (state.active && state.focused) {
return element | borderDouble; return element | borderDouble;
@ -201,19 +201,19 @@ Component VMenu6(std::vector<std::string>* entries, int* selected) {
Component VMenu7(std::vector<std::string>* entries, int* selected) { Component VMenu7(std::vector<std::string>* entries, int* selected) {
auto option = MenuOption::Vertical(); auto option = MenuOption::Vertical();
option.entries.animated_colors.foreground.enabled = true; option.entries_option.animated_colors.foreground.enabled = true;
option.entries.animated_colors.background.enabled = true; option.entries_option.animated_colors.background.enabled = true;
option.entries.animated_colors.background.active = Color::Red; option.entries_option.animated_colors.background.active = Color::Red;
option.entries.animated_colors.background.inactive = Color::Black; option.entries_option.animated_colors.background.inactive = Color::Black;
option.entries.animated_colors.foreground.active = Color::White; option.entries_option.animated_colors.foreground.active = Color::White;
option.entries.animated_colors.foreground.inactive = Color::Red; option.entries_option.animated_colors.foreground.inactive = Color::Red;
return Menu(entries, selected, option); return Menu(entries, selected, option);
} }
Component VMenu8(std::vector<std::string>* entries, int* selected) { Component VMenu8(std::vector<std::string>* entries, int* selected) {
auto option = MenuOption::Vertical(); auto option = MenuOption::Vertical();
option.entries.animated_colors.foreground.Set(Color::Red, Color::White, option.entries_option.animated_colors.foreground.Set(
std::chrono::milliseconds(500)); Color::Red, Color::White, std::chrono::milliseconds(500));
return Menu(entries, selected, option); return Menu(entries, selected, option);
} }
@ -240,7 +240,7 @@ Component HMenu5(std::vector<std::string>* entries, int* selected) {
auto option = MenuOption::HorizontalAnimated(); auto option = MenuOption::HorizontalAnimated();
option.underline.SetAnimation(std::chrono::milliseconds(1500), option.underline.SetAnimation(std::chrono::milliseconds(1500),
animation::easing::ElasticOut); animation::easing::ElasticOut);
option.entries.transform = [](EntryState state) { option.entries_option.transform = [](EntryState state) {
Element e = text(state.label) | hcenter | flex; Element e = text(state.label) | hcenter | flex;
if (state.active && state.focused) if (state.active && state.focused)
e = e | bold; e = e | bold;

View File

@ -40,37 +40,42 @@ Component Vertical(Components children, int* selector);
Component Horizontal(Components children); Component Horizontal(Components children);
Component Horizontal(Components children, int* selector); Component Horizontal(Components children, int* selector);
Component Tab(Components children, int* selector); Component Tab(Components children, int* selector);
} // namespace Container } // namespace Container
Component Button(ButtonOption options);
Component Button(ConstStringRef label, Component Button(ConstStringRef label,
std::function<void()> on_click, std::function<void()> on_click,
Ref<ButtonOption> = ButtonOption::Simple()); ButtonOption options = ButtonOption::Simple());
Component Checkbox(CheckboxOption options);
Component Checkbox(ConstStringRef label, Component Checkbox(ConstStringRef label,
bool* checked, bool* checked,
Ref<CheckboxOption> option = CheckboxOption::Simple()); CheckboxOption options = CheckboxOption::Simple());
Component Input(StringRef content, Ref<InputOption> option = {}); Component Input(InputOption options = {});
Component Input(StringRef content, InputOption options = {});
Component Input(StringRef content, Component Input(StringRef content,
StringRef placeholder, StringRef placeholder,
Ref<InputOption> option = {}); InputOption options = {});
Component Menu(MenuOption options);
Component Menu(ConstStringListRef entries, Component Menu(ConstStringListRef entries,
int* selected_, int* selected_,
Ref<MenuOption> = MenuOption::Vertical()); MenuOption options = MenuOption::Vertical());
Component MenuEntry(ConstStringRef label, Ref<MenuEntryOption> = {}); Component MenuEntry(MenuEntryOption options);
Component MenuEntry(ConstStringRef label, MenuEntryOption options = {});
Component Dropdown(ConstStringListRef entries, int* selected);
Component Radiobox(RadioboxOption options);
Component Radiobox(ConstStringListRef entries, Component Radiobox(ConstStringListRef entries,
int* selected_, int* selected_,
Ref<RadioboxOption> option = {}); RadioboxOption options = {});
Component Dropdown(ConstStringListRef entries, int* selected);
Component Toggle(ConstStringListRef entries, int* selected); Component Toggle(ConstStringListRef entries, int* selected);
// General slider constructor: // General slider constructor:
template <typename T> template <typename T>
Component Slider(SliderOption<T> options = {}); Component Slider(SliderOption<T> options);
// Shorthand without the `SliderOption` constructor: // Shorthand without the `SliderOption` constructor:
Component Slider(ConstStringRef label, Component Slider(ConstStringRef label,
@ -89,11 +94,11 @@ Component Slider(ConstStringRef label,
ConstRef<long> max = 100l, ConstRef<long> max = 100l,
ConstRef<long> increment = 5l); ConstRef<long> increment = 5l);
Component ResizableSplit(ResizableSplitOption options);
Component ResizableSplitLeft(Component main, Component back, int* main_size); Component ResizableSplitLeft(Component main, Component back, int* main_size);
Component ResizableSplitRight(Component main, Component back, int* main_size); Component ResizableSplitRight(Component main, Component back, int* main_size);
Component ResizableSplitTop(Component main, Component back, int* main_size); Component ResizableSplitTop(Component main, Component back, int* main_size);
Component ResizableSplitBottom(Component main, Component back, int* main_size); Component ResizableSplitBottom(Component main, Component back, int* main_size);
Component ResizableSplit(ResizableSplitOption options);
Component Renderer(Component child, std::function<Element()>); Component Renderer(Component child, std::function<Element()>);
Component Renderer(std::function<Element()>); Component Renderer(std::function<Element()>);

View File

@ -72,6 +72,7 @@ struct AnimatedColorsOption {
/// @brief Option for the MenuEntry component. /// @brief Option for the MenuEntry component.
/// @ingroup component /// @ingroup component
struct MenuEntryOption { struct MenuEntryOption {
ConstStringRef label = "MenuEntry";
std::function<Element(const EntryState& state)> transform; std::function<Element(const EntryState& state)> transform;
AnimatedColorsOption animated_colors; AnimatedColorsOption animated_colors;
}; };
@ -86,9 +87,12 @@ struct MenuOption {
static MenuOption VerticalAnimated(); static MenuOption VerticalAnimated();
static MenuOption Toggle(); static MenuOption Toggle();
ConstStringListRef entries; ///> The list of entries.
Ref<int> selected = 0; ///> The index of the selected entry.
// Style: // Style:
UnderlineOption underline; UnderlineOption underline;
MenuEntryOption entries; MenuEntryOption entries_option;
Direction direction = Direction::Down; Direction direction = Direction::Down;
std::function<Element()> elements_prefix; std::function<Element()> elements_prefix;
std::function<Element()> elements_infix; std::function<Element()> elements_infix;
@ -115,6 +119,9 @@ struct ButtonOption {
Color background_active, Color background_active,
Color foreground_active); Color foreground_active);
ConstStringRef label = "Button";
std::function<void()> on_click = [] {};
// Style: // Style:
std::function<Element(const EntryState&)> transform; std::function<Element(const EntryState&)> transform;
AnimatedColorsOption animated_colors; AnimatedColorsOption animated_colors;
@ -126,6 +133,10 @@ struct CheckboxOption {
// Standard constructors: // Standard constructors:
static CheckboxOption Simple(); static CheckboxOption Simple();
ConstStringRef label = "Checkbox";
Ref<bool> checked = false;
// Style: // Style:
std::function<Element(const EntryState&)> transform; std::function<Element(const EntryState&)> transform;
@ -153,6 +164,9 @@ struct InputOption {
/// @brief A white on black style with high margins: /// @brief A white on black style with high margins:
static InputOption Spacious(); static InputOption Spacious();
/// The content of the input.
StringRef content = "";
/// The content of the input when it's empty. /// The content of the input when it's empty.
StringRef placeholder = ""; StringRef placeholder = "";
@ -176,6 +190,10 @@ struct RadioboxOption {
// Standard constructors: // Standard constructors:
static RadioboxOption Simple(); static RadioboxOption Simple();
// Content:
ConstStringListRef entries;
Ref<int> selected = 0;
// Style: // Style:
std::function<Element(const EntryState&)> transform; std::function<Element(const EntryState&)> transform;

View File

@ -106,7 +106,7 @@ class Screen {
// Store an hyperlink in the screen. Return the id of the hyperlink. The id is // Store an hyperlink in the screen. Return the id of the hyperlink. The id is
// used to identify the hyperlink when the user click on it. // used to identify the hyperlink when the user click on it.
uint8_t RegisterHyperlink(std::string link); uint8_t RegisterHyperlink(const std::string& link);
const std::string& Hyperlink(uint8_t id) const; const std::string& Hyperlink(uint8_t id) const;
Box stencil; Box stencil;

View File

@ -13,6 +13,12 @@ class ConstRef {
ConstRef() {} ConstRef() {}
ConstRef(T t) : owned_(t) {} ConstRef(T t) : owned_(t) {}
ConstRef(const T* t) : address_(t) {} ConstRef(const T* t) : address_(t) {}
ConstRef(const ConstRef& t) : owned_(t.owned_), address_(t.address_) {}
ConstRef& operator=(const ConstRef& t) {
owned_ = t.owned_;
address_ = t.address_;
return *this;
}
const T& operator*() const { return address_ ? *address_ : owned_; } const T& operator*() const { return address_ ? *address_ : owned_; }
const T& operator()() const { return address_ ? *address_ : owned_; } const T& operator()() const { return address_ ? *address_ : owned_; }
const T* operator->() const { return address_ ? address_ : &owned_; } const T* operator->() const { return address_ ? address_ : &owned_; }
@ -30,6 +36,12 @@ class Ref {
Ref(const T& t) : owned_(t) {} Ref(const T& t) : owned_(t) {}
Ref(T&& t) : owned_(std::forward<T>(t)) {} Ref(T&& t) : owned_(std::forward<T>(t)) {}
Ref(T* t) : owned_(), address_(t) {} Ref(T* t) : owned_(), address_(t) {}
Ref(const Ref& t) : owned_(t.owned_), address_(t.address_) {}
Ref& operator=(const Ref& t) {
owned_ = t.owned_;
address_ = t.address_;
return *this;
}
T& operator*() { return address_ ? *address_ : owned_; } T& operator*() { return address_ ? *address_ : owned_; }
T& operator()() { return address_ ? *address_ : owned_; } T& operator()() { return address_ ? *address_ : owned_; }
T* operator->() { return address_ ? address_ : &owned_; } T* operator->() { return address_ ? address_ : &owned_; }
@ -47,6 +59,12 @@ class StringRef {
StringRef(std::string ref) : owned_(std::move(ref)) {} StringRef(std::string ref) : owned_(std::move(ref)) {}
StringRef(const wchar_t* ref) : StringRef(to_string(std::wstring(ref))) {} StringRef(const wchar_t* ref) : StringRef(to_string(std::wstring(ref))) {}
StringRef(const char* ref) : StringRef(std::string(ref)) {} StringRef(const char* ref) : StringRef(std::string(ref)) {}
StringRef(const StringRef& t) : owned_(t.owned_), address_(t.address_) {}
StringRef& operator=(const StringRef& t) {
owned_ = t.owned_;
address_ = t.address_;
return *this;
}
std::string& operator*() { return address_ ? *address_ : owned_; } std::string& operator*() { return address_ ? *address_ : owned_; }
std::string& operator()() { return address_ ? *address_ : owned_; } std::string& operator()() { return address_ ? *address_ : owned_; }
std::string* operator->() { return address_ ? address_ : &owned_; } std::string* operator->() { return address_ ? address_ : &owned_; }
@ -67,6 +85,18 @@ class ConstStringRef {
ConstStringRef(const wchar_t* ref) : ConstStringRef(std::wstring(ref)) {} ConstStringRef(const wchar_t* ref) : ConstStringRef(std::wstring(ref)) {}
ConstStringRef(const char* ref) ConstStringRef(const char* ref)
: ConstStringRef(to_wstring(std::string(ref))) {} : ConstStringRef(to_wstring(std::string(ref))) {}
ConstStringRef(const ConstStringRef& t)
: owned_(t.owned_), address_(t.address_) {}
ConstStringRef& operator=(const ConstStringRef& t) {
owned_ = t.owned_;
address_ = t.address_;
return *this;
}
ConstStringRef& operator=(ConstStringRef&& t) {
owned_ = std::move(t.owned_);
address_ = t.address_;
return *this;
}
const std::string& operator()() const { const std::string& operator()() const {
return address_ ? *address_ : owned_; return address_ ? *address_ : owned_;
} }
@ -76,19 +106,35 @@ class ConstStringRef {
} }
private: private:
const std::string owned_; std::string owned_;
const std::string* address_ = nullptr; const std::string* address_ = nullptr;
}; };
/// @brief An adapter. Reference a list of strings. /// @brief An adapter. Reference a list of strings.
class ConstStringListRef { class ConstStringListRef {
public: public:
ConstStringListRef() = default;
ConstStringListRef(const std::vector<std::string>* ref) : ref_(ref) {} ConstStringListRef(const std::vector<std::string>* ref) : ref_(ref) {}
ConstStringListRef(const std::vector<std::wstring>* ref) : ref_wide_(ref) {} ConstStringListRef(const std::vector<std::wstring>* ref) : ref_wide_(ref) {}
size_t size() const { return ref_ ? ref_->size() : ref_wide_->size(); } size_t size() const {
if (ref_) {
return ref_->size();
}
if (ref_wide_) {
return ref_wide_->size();
}
return 0;
}
std::string operator[](size_t i) const { std::string operator[](size_t i) const {
return ref_ ? (*ref_)[i] : to_string((*ref_wide_)[i]); if (ref_) {
return (*ref_)[i];
}
if (ref_wide_) {
return to_string((*ref_wide_)[i]);
}
return "";
} }
private: private:

View File

@ -30,7 +30,148 @@ Element DefaultTransform(EntryState params) { // NOLINT
return element; return element;
} }
class ButtonBase : public ComponentBase, public ButtonOption {
public:
explicit ButtonBase(ButtonOption option) : ButtonOption(std::move(option)) {}
// Component implementation:
Element Render() override {
const bool active = Active();
const bool focused = Focused();
const bool focused_or_hover = focused || mouse_hover_;
float target = focused_or_hover ? 1.f : 0.f; // NOLINT
if (target != animator_background_.to()) {
SetAnimationTarget(target);
}
auto focus_management = focused ? focus : active ? select : nothing;
const EntryState state = {
*label,
false,
active,
focused_or_hover,
};
auto element = (transform ? transform : DefaultTransform) //
(state);
return element | AnimatedColorStyle() | focus_management | reflect(box_);
}
Decorator AnimatedColorStyle() {
Decorator style = nothing;
if (animated_colors.background.enabled) {
style = style |
bgcolor(Color::Interpolate(animation_foreground_, //
animated_colors.background.inactive,
animated_colors.background.active));
}
if (animated_colors.foreground.enabled) {
style =
style | color(Color::Interpolate(animation_foreground_, //
animated_colors.foreground.inactive,
animated_colors.foreground.active));
}
return style;
}
void SetAnimationTarget(float target) {
if (animated_colors.foreground.enabled) {
animator_foreground_ = animation::Animator(
&animation_foreground_, target, animated_colors.foreground.duration,
animated_colors.foreground.function);
}
if (animated_colors.background.enabled) {
animator_background_ = animation::Animator(
&animation_background_, target, animated_colors.background.duration,
animated_colors.background.function);
}
}
void OnAnimation(animation::Params& p) override {
animator_background_.OnAnimation(p);
animator_foreground_.OnAnimation(p);
}
void OnClick() {
on_click();
animation_background_ = 0.5F; // NOLINT
animation_foreground_ = 0.5F; // NOLINT
SetAnimationTarget(1.F); // NOLINT
}
bool OnEvent(Event event) override {
if (event.is_mouse()) {
return OnMouseEvent(event);
}
if (event == Event::Return) {
OnClick();
return true;
}
return false;
}
bool OnMouseEvent(Event event) {
mouse_hover_ =
box_.Contain(event.mouse().x, event.mouse().y) && CaptureMouse(event);
if (!mouse_hover_) {
return false;
}
if (event.mouse().button == Mouse::Left &&
event.mouse().motion == Mouse::Pressed) {
TakeFocus();
OnClick();
return true;
}
return false;
}
bool Focusable() const final { return true; }
private:
bool mouse_hover_ = false;
Box box_;
ButtonOption option_;
float animation_background_ = 0;
float animation_foreground_ = 0;
animation::Animator animator_background_ =
animation::Animator(&animation_background_);
animation::Animator animator_foreground_ =
animation::Animator(&animation_foreground_);
};
} // namespace } // namespace
//
/// @brief Draw a button. Execute a function when clicked.
/// @param option Additional optional parameters.
/// @ingroup component
/// @see ButtonBase
///
/// ### Example
///
/// ```cpp
/// auto screen = ScreenInteractive::FitComponent();
/// Component button = Button({
/// .label = "Click to quit",
/// .on_click = screen.ExitLoopClosure(),
/// });
/// screen.Loop(button)
/// ```
///
/// ### Output
///
/// ```bash
/// ┌─────────────┐
/// │Click to quit│
/// └─────────────┘
/// ```
Component Button(ButtonOption option) {
return Make<ButtonBase>(std::move(option));
}
/// @brief Draw a button. Execute a function when clicked. /// @brief Draw a button. Execute a function when clicked.
/// @param label The label of the button. /// @param label The label of the button.
@ -55,135 +196,13 @@ Element DefaultTransform(EntryState params) { // NOLINT
/// │Click to quit│ /// │Click to quit│
/// └─────────────┘ /// └─────────────┘
/// ``` /// ```
// NOLINTNEXTLINE(readability-function-cognitive-complexity) // NOLINTNEXTLINE
Component Button(ConstStringRef label, Component Button(ConstStringRef label,
std::function<void()> on_click, std::function<void()> on_click,
Ref<ButtonOption> option) { ButtonOption option) {
class Impl : public ComponentBase { option.label = label;
public: option.on_click = std::move(on_click);
Impl(ConstStringRef label, return Make<ButtonBase>(std::move(option));
std::function<void()> on_click,
Ref<ButtonOption> option)
: label_(std::move(label)),
on_click_(std::move(on_click)),
option_(std::move(option)) {}
// Component implementation:
Element Render() override {
const bool active = Active();
const bool focused = Focused();
const bool focused_or_hover = focused || mouse_hover_;
float target = focused_or_hover ? 1.f : 0.f; // NOLINT
if (target != animator_background_.to()) {
SetAnimationTarget(target);
}
auto focus_management = focused ? focus : active ? select : nothing;
const EntryState state = {
*label_,
false,
active,
focused_or_hover,
};
auto element =
(option_->transform ? option_->transform : DefaultTransform) //
(state);
return element | AnimatedColorStyle() | focus_management | reflect(box_);
}
Decorator AnimatedColorStyle() {
Decorator style = nothing;
if (option_->animated_colors.background.enabled) {
style = style | bgcolor(Color::Interpolate(
animation_foreground_, //
option_->animated_colors.background.inactive,
option_->animated_colors.background.active));
}
if (option_->animated_colors.foreground.enabled) {
style = style | color(Color::Interpolate(
animation_foreground_, //
option_->animated_colors.foreground.inactive,
option_->animated_colors.foreground.active));
}
return style;
}
void SetAnimationTarget(float target) {
if (option_->animated_colors.foreground.enabled) {
animator_foreground_ =
animation::Animator(&animation_foreground_, target,
option_->animated_colors.foreground.duration,
option_->animated_colors.foreground.function);
}
if (option_->animated_colors.background.enabled) {
animator_background_ =
animation::Animator(&animation_background_, target,
option_->animated_colors.background.duration,
option_->animated_colors.background.function);
}
}
void OnAnimation(animation::Params& p) override {
animator_background_.OnAnimation(p);
animator_foreground_.OnAnimation(p);
}
void OnClick() {
on_click_();
animation_background_ = 0.5F; // NOLINT
animation_foreground_ = 0.5F; // NOLINT
SetAnimationTarget(1.F); // NOLINT
}
bool OnEvent(Event event) override {
if (event.is_mouse()) {
return OnMouseEvent(event);
}
if (event == Event::Return) {
OnClick();
return true;
}
return false;
}
bool OnMouseEvent(Event event) {
mouse_hover_ =
box_.Contain(event.mouse().x, event.mouse().y) && CaptureMouse(event);
if (!mouse_hover_) {
return false;
}
if (event.mouse().button == Mouse::Left &&
event.mouse().motion == Mouse::Pressed) {
TakeFocus();
OnClick();
return true;
}
return false;
}
bool Focusable() const final { return true; }
private:
ConstStringRef label_;
std::function<void()> on_click_;
bool mouse_hover_ = false;
Box box_;
Ref<ButtonOption> option_;
float animation_background_ = 0;
float animation_foreground_ = 0;
animation::Animator animator_background_ =
animation::Animator(&animation_background_);
animation::Animator animator_foreground_ =
animation::Animator(&animation_foreground_);
};
return Make<Impl>(std::move(label), std::move(on_click), std::move(option));
} }
} // namespace ftxui } // namespace ftxui

View File

@ -14,10 +14,10 @@
namespace ftxui { namespace ftxui {
namespace { namespace {
class CheckboxBase : public ComponentBase { class CheckboxBase : public ComponentBase, public CheckboxOption {
public: public:
CheckboxBase(ConstStringRef label, bool* state, Ref<CheckboxOption> option) explicit CheckboxBase(CheckboxOption option)
: label_(std::move(label)), state_(state), option_(std::move(option)) {} : CheckboxOption(std::move(option)) {}
private: private:
// Component implementation. // Component implementation.
@ -25,15 +25,14 @@ class CheckboxBase : public ComponentBase {
const bool is_focused = Focused(); const bool is_focused = Focused();
const bool is_active = Active(); const bool is_active = Active();
auto focus_management = is_focused ? focus : is_active ? select : nothing; auto focus_management = is_focused ? focus : is_active ? select : nothing;
auto state = EntryState{ auto entry_state = EntryState{
*label_, *label,
*state_, *checked,
is_active, is_active,
is_focused || hovered_, is_focused || hovered_,
}; };
auto element = auto element = (transform ? transform : CheckboxOption::Simple().transform)(
(option_->transform ? option_->transform entry_state);
: CheckboxOption::Simple().transform)(state);
return element | focus_management | reflect(box_); return element | focus_management | reflect(box_);
} }
@ -48,8 +47,8 @@ class CheckboxBase : public ComponentBase {
hovered_ = false; hovered_ = false;
if (event == Event::Character(' ') || event == Event::Return) { if (event == Event::Character(' ') || event == Event::Return) {
*state_ = !*state_; *checked = !*checked;
option_->on_change(); on_change();
TakeFocus(); TakeFocus();
return true; return true;
} }
@ -69,8 +68,8 @@ class CheckboxBase : public ComponentBase {
if (event.mouse().button == Mouse::Left && if (event.mouse().button == Mouse::Left &&
event.mouse().motion == Mouse::Pressed) { event.mouse().motion == Mouse::Pressed) {
*state_ = !*state_; *checked = !*checked;
option_->on_change(); on_change();
return true; return true;
} }
@ -79,10 +78,7 @@ class CheckboxBase : public ComponentBase {
bool Focusable() const final { return true; } bool Focusable() const final { return true; }
ConstStringRef label_;
bool* const state_;
bool hovered_ = false; bool hovered_ = false;
Ref<CheckboxOption> option_;
Box box_; Box box_;
}; };
} // namespace } // namespace
@ -109,10 +105,11 @@ class CheckboxBase : public ComponentBase {
/// ```bash /// ```bash
/// ☐ Make a sandwitch /// ☐ Make a sandwitch
/// ``` /// ```
Component Checkbox(ConstStringRef label, // NOLINTNEXTLINE
bool* checked, Component Checkbox(ConstStringRef label, bool* checked, CheckboxOption option) {
Ref<CheckboxOption> option) { option.label = label;
return Make<CheckboxBase>(std::move(label), checked, std::move(option)); option.checked = checked;
return Make<CheckboxBase>(std::move(option));
} }
} // namespace ftxui } // namespace ftxui

View File

@ -27,6 +27,7 @@ namespace ftxui {
/// ▼ Show details /// ▼ Show details
/// <details component> /// <details component>
///  ``` ///  ```
// NOLINTNEXTLINE
Component Collapsible(ConstStringRef label, Component child, Ref<bool> show) { Component Collapsible(ConstStringRef label, Component child, Ref<bool> show) {
class Impl : public ComponentBase { class Impl : public ComponentBase {
public: public:
@ -44,7 +45,7 @@ Component Collapsible(ConstStringRef label, Component child, Ref<bool> show) {
return hbox({prefix, t}); return hbox({prefix, t});
}; };
Add(Container::Vertical({ Add(Container::Vertical({
Checkbox(std::move(label), show_.operator->(), opt), Checkbox(label, show_.operator->(), opt),
Maybe(std::move(child), show_.operator->()), Maybe(std::move(child), show_.operator->()),
})); }));
} }

View File

@ -48,7 +48,7 @@ void UnderlineOption::SetAnimationFunction(
MenuOption MenuOption::Horizontal() { MenuOption MenuOption::Horizontal() {
MenuOption option; MenuOption option;
option.direction = Direction::Right; option.direction = Direction::Right;
option.entries.transform = [](const EntryState& state) { option.entries_option.transform = [](const EntryState& state) {
Element e = text(state.label); Element e = text(state.label);
if (state.focused) { if (state.focused) {
e |= inverted; e |= inverted;
@ -76,7 +76,7 @@ MenuOption MenuOption::HorizontalAnimated() {
// static // static
MenuOption MenuOption::Vertical() { MenuOption MenuOption::Vertical() {
MenuOption option; MenuOption option;
option.entries.transform = [](const EntryState& state) { option.entries_option.transform = [](const EntryState& state) {
Element e = text((state.active ? "> " : " ") + state.label); // NOLINT Element e = text((state.active ? "> " : " ") + state.label); // NOLINT
if (state.focused) { if (state.focused) {
e |= inverted; e |= inverted;
@ -95,7 +95,7 @@ MenuOption MenuOption::Vertical() {
// static // static
MenuOption MenuOption::VerticalAnimated() { MenuOption MenuOption::VerticalAnimated() {
auto option = MenuOption::Vertical(); auto option = MenuOption::Vertical();
option.entries.transform = [](const EntryState& state) { option.entries_option.transform = [](const EntryState& state) {
Element e = text(state.label); Element e = text(state.label);
if (state.focused) { if (state.focused) {
e |= inverted; e |= inverted;
@ -124,9 +124,9 @@ MenuOption MenuOption::Toggle() {
ButtonOption ButtonOption::Ascii() { ButtonOption ButtonOption::Ascii() {
ButtonOption option; ButtonOption option;
option.transform = [](const EntryState& s) { option.transform = [](const EntryState& s) {
const std::string label = s.focused ? "[" + s.label + "]" // const std::string t = s.focused ? "[" + s.label + "]" //
: " " + s.label + " "; : " " + s.label + " ";
return text(label); return text(t);
}; };
return option; return option;
} }

View File

@ -34,17 +34,19 @@ std::vector<std::string> Split(const std::string& input) {
output.push_back(line); output.push_back(line);
} }
if (input.back() == '\n') { if (input.back() == '\n') {
output.push_back(""); output.emplace_back("");
} }
return output; return output;
} }
size_t GlyphWidth(const std::string& input, size_t iter) { size_t GlyphWidth(const std::string& input, size_t iter) {
uint32_t ucs = 0; uint32_t ucs = 0;
if (!EatCodePoint(input, iter, &iter, &ucs)) if (!EatCodePoint(input, iter, &iter, &ucs)) {
return 0; return 0;
if (IsFullWidth(ucs)) }
if (IsFullWidth(ucs)) {
return 2; return 2;
}
return 1; return 1;
} }
@ -86,11 +88,10 @@ bool IsWordCharacter(const std::string& input, size_t iter) {
} }
// An input box. The user can type text into it. // An input box. The user can type text into it.
class InputBase : public ComponentBase { class InputBase : public ComponentBase, public InputOption {
public: public:
// NOLINTNEXTLINE // NOLINTNEXTLINE
InputBase(StringRef content, Ref<InputOption> option) InputBase(InputOption option) : InputOption(std::move(option)) {}
: content_(std::move(content)), option_(std::move(option)) {}
private: private:
// Component implementation: // Component implementation:
@ -99,17 +100,17 @@ class InputBase : public ComponentBase {
const auto focused = const auto focused =
(is_focused || hovered_) ? focusCursorBarBlinking : select; (is_focused || hovered_) ? focusCursorBarBlinking : select;
auto transform = option_->transform ? option_->transform auto transform_func =
: InputOption::Default().transform; transform ? transform : InputOption::Default().transform;
// placeholder. // placeholder.
if (content_->empty()) { if (content->empty()) {
auto element = text(option_->placeholder()) | xflex | frame; auto element = text(placeholder()) | xflex | frame;
if (is_focused) { if (is_focused) {
element |= focus; element |= focus;
} }
return transform({ return transform_func({
std::move(element), hovered_, is_focused, std::move(element), hovered_, is_focused,
true // placeholder true // placeholder
}) | }) |
@ -117,14 +118,13 @@ class InputBase : public ComponentBase {
} }
Elements elements; Elements elements;
const std::vector<std::string> lines = Split(*content_); const std::vector<std::string> lines = Split(*content);
int& cursor_position = option_->cursor_position(); cursor_position() = util::clamp(cursor_position(), 0, (int)content->size());
cursor_position = util::clamp(cursor_position, 0, (int)content_->size());
// Find the line and index of the cursor. // Find the line and index of the cursor.
int cursor_line = 0; int cursor_line = 0;
int cursor_char_index = cursor_position; int cursor_char_index = cursor_position();
for (const auto& line : lines) { for (const auto& line : lines) {
if (cursor_char_index <= (int)line.size()) { if (cursor_char_index <= (int)line.size()) {
break; break;
@ -175,7 +175,7 @@ class InputBase : public ComponentBase {
} }
auto element = vbox(std::move(elements)) | frame; auto element = vbox(std::move(elements)) | frame;
return transform({ return transform_func({
std::move(element), hovered_, is_focused, std::move(element), hovered_, is_focused,
false // placeholder false // placeholder
}) | }) |
@ -183,7 +183,7 @@ class InputBase : public ComponentBase {
} }
Element Text(const std::string& input) { Element Text(const std::string& input) {
if (!option_->password()) { if (!password()) {
return text(input); return text(input);
} }
@ -196,108 +196,101 @@ class InputBase : public ComponentBase {
} }
bool HandleBackspace() { bool HandleBackspace() {
int& cursor_position = option_->cursor_position(); if (cursor_position() == 0) {
if (cursor_position == 0) {
return false; return false;
} }
const size_t start = GlyphPrevious(content_(), cursor_position); const size_t start = GlyphPrevious(content(), cursor_position());
const size_t end = cursor_position; const size_t end = cursor_position();
content_->erase(start, end - start); content->erase(start, end - start);
cursor_position = start; cursor_position() = start;
return true; return true;
} }
bool HandleDelete() { bool HandleDelete() {
int& cursor_position = option_->cursor_position(); if (cursor_position() == (int)content->size()) {
if (cursor_position == (int)content_->size()) {
return false; return false;
} }
const size_t start = cursor_position; const size_t start = cursor_position();
const size_t end = GlyphNext(content_(), cursor_position); const size_t end = GlyphNext(content(), cursor_position());
content_->erase(start, end - start); content->erase(start, end - start);
return true; return true;
} }
bool HandleArrowLeft() { bool HandleArrowLeft() {
int& cursor_position = option_->cursor_position(); if (cursor_position() == 0) {
if (cursor_position == 0) {
return false; return false;
} }
cursor_position = GlyphPrevious(content_(), cursor_position); cursor_position() = GlyphPrevious(content(), cursor_position());
return true; return true;
} }
bool HandleArrowRight() { bool HandleArrowRight() {
int& cursor_position = option_->cursor_position(); if (cursor_position() == (int)content->size()) {
if (cursor_position == (int)content_->size()) {
return false; return false;
} }
cursor_position = GlyphNext(content_(), cursor_position); cursor_position() = GlyphNext(content(), cursor_position());
return true; return true;
} }
size_t CursorColumn() { size_t CursorColumn() {
int& cursor_position = option_->cursor_position(); size_t iter = cursor_position();
size_t iter = cursor_position;
int width = 0; int width = 0;
while (true) { while (true) {
if (iter == 0) { if (iter == 0) {
break; break;
} }
iter = GlyphPrevious(content_(), iter); iter = GlyphPrevious(content(), iter);
if (content_()[iter] == '\n') { if (content()[iter] == '\n') {
break; break;
} }
width += GlyphWidth(content_(), iter); width += GlyphWidth(content(), iter);
} }
return width; return width;
} }
// Move the cursor `columns` on the right, if possible. // Move the cursor `columns` on the right, if possible.
void MoveCursorColumn(int columns) { void MoveCursorColumn(int columns) {
int& cursor_position = option_->cursor_position();
while (columns > 0) { while (columns > 0) {
if (cursor_position == (int)content_().size() || if (cursor_position() == (int)content().size() ||
content_()[cursor_position] == '\n') { content()[cursor_position()] == '\n') {
return; return;
} }
columns -= GlyphWidth(content_(), cursor_position); columns -= GlyphWidth(content(), cursor_position());
cursor_position = GlyphNext(content_(), cursor_position); cursor_position() = GlyphNext(content(), cursor_position());
} }
} }
bool HandleArrowUp() { bool HandleArrowUp() {
int& cursor_position = option_->cursor_position(); if (cursor_position() == 0) {
if (cursor_position == 0) {
return false; return false;
} }
size_t columns = CursorColumn(); const size_t columns = CursorColumn();
// Move cursor at the beginning of 2 lines above. // Move cursor at the beginning of 2 lines above.
while (true) { while (true) {
if (cursor_position == 0) { if (cursor_position() == 0) {
return true; return true;
} }
size_t previous = GlyphPrevious(content_(), cursor_position); const size_t previous = GlyphPrevious(content(), cursor_position());
if (content_()[previous] == '\n') { if (content()[previous] == '\n') {
break; break;
} }
cursor_position = previous; cursor_position() = previous;
} }
cursor_position = GlyphPrevious(content_(), cursor_position); cursor_position() = GlyphPrevious(content(), cursor_position());
while (true) { while (true) {
if (cursor_position == 0) { if (cursor_position() == 0) {
break; break;
} }
size_t previous = GlyphPrevious(content_(), cursor_position); const size_t previous = GlyphPrevious(content(), cursor_position());
if (content_()[previous] == '\n') { if (content()[previous] == '\n') {
break; break;
} }
cursor_position = previous; cursor_position() = previous;
} }
MoveCursorColumn(columns); MoveCursorColumn(columns);
@ -305,61 +298,56 @@ class InputBase : public ComponentBase {
} }
bool HandleArrowDown() { bool HandleArrowDown() {
int& cursor_position = option_->cursor_position(); if (cursor_position() == (int)content->size()) {
if (cursor_position == (int)content_->size()) {
return false; return false;
} }
size_t columns = CursorColumn(); const size_t columns = CursorColumn();
// Move cursor at the beginning of the next line // Move cursor at the beginning of the next line
while (true) { while (true) {
if (content_()[cursor_position] == '\n') { if (content()[cursor_position()] == '\n') {
break; break;
} }
cursor_position = GlyphNext(content_(), cursor_position); cursor_position() = GlyphNext(content(), cursor_position());
if (cursor_position == (int)content_().size()) { if (cursor_position() == (int)content().size()) {
return true; return true;
} }
} }
cursor_position = GlyphNext(content_(), cursor_position); cursor_position() = GlyphNext(content(), cursor_position());
MoveCursorColumn(columns); MoveCursorColumn(columns);
return true; return true;
} }
bool HandleHome() { bool HandleHome() {
int& cursor_position = option_->cursor_position(); cursor_position() = 0;
cursor_position = 0;
return true; return true;
} }
bool HandleEnd() { bool HandleEnd() {
int& cursor_position = option_->cursor_position(); cursor_position() = content->size();
cursor_position = content_->size();
return true; return true;
} }
bool HandleReturn() { bool HandleReturn() {
if (option_->multiline()) { if (multiline()) {
HandleCharacter("\n"); HandleCharacter("\n");
} }
option_->on_enter(); on_enter();
return true; return true;
} }
bool HandleCharacter(const std::string& character) { bool HandleCharacter(const std::string& character) {
int& cursor_position = option_->cursor_position(); content->insert(cursor_position(), character);
content_->insert(cursor_position, character); cursor_position() += character.size();
cursor_position += character.size(); on_change();
option_->on_change();
return true; return true;
} }
bool OnEvent(Event event) override { bool OnEvent(Event event) override {
int& cursor_position = option_->cursor_position(); cursor_position() = util::clamp(cursor_position(), 0, (int)content->size());
cursor_position = util::clamp(cursor_position, 0, (int)content_->size());
if (event == Event::Return) { if (event == Event::Return) {
return HandleReturn(); return HandleReturn();
@ -405,50 +393,48 @@ class InputBase : public ComponentBase {
} }
bool HandleLeftCtrl() { bool HandleLeftCtrl() {
int& cursor_position = option_->cursor_position(); if (cursor_position() == 0) {
if (cursor_position == 0) {
return false; return false;
} }
// Move left, as long as left it not a word. // Move left, as long as left it not a word.
while (cursor_position) { while (cursor_position()) {
size_t previous = GlyphPrevious(content_(), cursor_position); const size_t previous = GlyphPrevious(content(), cursor_position());
if (IsWordCharacter(content_(), previous)) { if (IsWordCharacter(content(), previous)) {
break; break;
} }
cursor_position = previous; cursor_position() = previous;
} }
// Move left, as long as left is a word character: // Move left, as long as left is a word character:
while (cursor_position) { while (cursor_position()) {
size_t previous = GlyphPrevious(content_(), cursor_position); const size_t previous = GlyphPrevious(content(), cursor_position());
if (!IsWordCharacter(content_(), previous)) { if (!IsWordCharacter(content(), previous)) {
break; break;
} }
cursor_position = previous; cursor_position() = previous;
} }
return true; return true;
} }
bool HandleRightCtrl() { bool HandleRightCtrl() {
int& cursor_position = option_->cursor_position(); if (cursor_position() == (int)content().size()) {
if (cursor_position == (int)content_().size()) {
return false; return false;
} }
// Move right, until entering a word. // Move right, until entering a word.
while (cursor_position < (int)content_().size()) { while (cursor_position() < (int)content().size()) {
cursor_position = GlyphNext(content_(), cursor_position); cursor_position() = GlyphNext(content(), cursor_position());
if (IsWordCharacter(content_(), cursor_position)) { if (IsWordCharacter(content(), cursor_position())) {
break; break;
} }
} }
// Move right, as long as right is a word character: // Move right, as long as right is a word character:
while (cursor_position < (int)content_().size()) { while (cursor_position() < (int)content().size()) {
size_t next = GlyphNext(content_(), cursor_position); const size_t next = GlyphNext(content(), cursor_position());
if (!IsWordCharacter(content_(), cursor_position)) { if (!IsWordCharacter(content(), cursor_position())) {
break; break;
} }
cursor_position = next; cursor_position() = next;
} }
return true; return true;
@ -469,16 +455,15 @@ class InputBase : public ComponentBase {
TakeFocus(); TakeFocus();
if (content_->empty()) { if (content->empty()) {
option_->cursor_position() = 0; cursor_position() = 0;
return true; return true;
} }
// Find the line and index of the cursor. // Find the line and index of the cursor.
std::vector<std::string> lines = Split(*content_); std::vector<std::string> lines = Split(*content);
int& cursor_position = option_->cursor_position();
int cursor_line = 0; int cursor_line = 0;
int cursor_char_index = cursor_position; int cursor_char_index = cursor_position();
for (const auto& line : lines) { for (const auto& line : lines) {
if (cursor_char_index <= (int)line.size()) { if (cursor_char_index <= (int)line.size()) {
break; break;
@ -487,7 +472,7 @@ class InputBase : public ComponentBase {
cursor_char_index -= line.size() + 1; cursor_char_index -= line.size() + 1;
cursor_line++; cursor_line++;
} }
int cursor_column = const int cursor_column =
string_width(lines[cursor_line].substr(0, cursor_char_index)); 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_column = cursor_column + event.mouse().x - cursor_box_.x_min;
@ -496,7 +481,7 @@ class InputBase : public ComponentBase {
// Fix the new cursor position: // Fix the new cursor position:
new_cursor_line = std::max(std::min(new_cursor_line, (int)lines.size()), 0); new_cursor_line = std::max(std::min(new_cursor_line, (int)lines.size()), 0);
std::string empty_string; const std::string empty_string;
const std::string& line = new_cursor_line < (int)lines.size() const std::string& line = new_cursor_line < (int)lines.size()
? lines[new_cursor_line] ? lines[new_cursor_line]
: empty_string; : empty_string;
@ -508,31 +493,56 @@ class InputBase : public ComponentBase {
} }
// Convert back the new_cursor_{line,column} toward cursor_position: // Convert back the new_cursor_{line,column} toward cursor_position:
cursor_position = 0; cursor_position() = 0;
for (int i = 0; i < new_cursor_line; ++i) { for (int i = 0; i < new_cursor_line; ++i) {
cursor_position += lines[i].size() + 1; cursor_position() += lines[i].size() + 1;
} }
while (new_cursor_column > 0) { while (new_cursor_column > 0) {
new_cursor_column -= GlyphWidth(content_(), cursor_position); new_cursor_column -= GlyphWidth(content(), cursor_position());
cursor_position = GlyphNext(content_(), cursor_position); cursor_position() = GlyphNext(content(), cursor_position());
} }
option_->on_change(); on_change();
return true; return true;
} }
bool Focusable() const final { return true; } bool Focusable() const final { return true; }
bool hovered_ = false; bool hovered_ = false;
StringRef content_;
Box box_; Box box_;
Box cursor_box_; Box cursor_box_;
Ref<InputOption> option_;
}; };
} // namespace } // namespace
/// @brief An input box for editing text.
/// @param option Additional optional parameters.
/// @ingroup component
/// @see InputBase
///
/// ### Example
///
/// ```cpp
/// auto screen = ScreenInteractive::FitComponent();
/// std::string content= "";
/// std::string placeholder = "placeholder";
/// Component input = Input({
/// .content = &content,
/// .placeholder = &placeholder,
/// })
/// screen.Loop(input);
/// ```
///
/// ### Output
///
/// ```bash
/// placeholder
/// ```
Component Input(InputOption option) {
return Make<InputBase>(std::move(option));
}
/// @brief An input box for editing text. /// @brief An input box for editing text.
/// @param content The editable content. /// @param content The editable content.
/// @param option Additional optional parameters. /// @param option Additional optional parameters.
@ -545,7 +555,10 @@ class InputBase : public ComponentBase {
/// auto screen = ScreenInteractive::FitComponent(); /// auto screen = ScreenInteractive::FitComponent();
/// std::string content= ""; /// std::string content= "";
/// std::string placeholder = "placeholder"; /// std::string placeholder = "placeholder";
/// Component input = Input(&content, &placeholder); /// Component input = Input(content, {
/// .placeholder = &placeholder,
/// .password = true,
/// })
/// screen.Loop(input); /// screen.Loop(input);
/// ``` /// ```
/// ///
@ -554,15 +567,36 @@ class InputBase : public ComponentBase {
/// ```bash /// ```bash
/// placeholder /// placeholder
/// ``` /// ```
Component Input(StringRef content, Ref<InputOption> option) { Component Input(StringRef content, InputOption option) {
return Make<InputBase>(std::move(content), std::move(option)); option.content = content;
return Make<InputBase>(std::move(option));
} }
Component Input(StringRef content, /// @brief An input box for editing text.
StringRef placeholder, /// @param content The editable content.
Ref<InputOption> option) { /// @param option Additional optional parameters.
option->placeholder = placeholder; /// @ingroup component
return Make<InputBase>(std::move(content), std::move(option)); /// @see InputBase
///
/// ### Example
///
/// ```cpp
/// auto screen = ScreenInteractive::FitComponent();
/// std::string content= "";
/// std::string placeholder = "placeholder";
/// Component input = Input(content, placeholder);
/// screen.Loop(input);
/// ```
///
/// ### Output
///
/// ```bash
/// placeholder
/// ```
Component Input(StringRef content, StringRef placeholder, InputOption option) {
option.content = content;
option.placeholder = placeholder;
return Make<InputBase>(std::move(option));
} }
} // namespace ftxui } // namespace ftxui

View File

@ -16,32 +16,36 @@ namespace ftxui {
TEST(InputTest, Init) { TEST(InputTest, Init) {
std::string content; std::string content;
int cursor_position = 0;
auto option = InputOption(); auto option = InputOption();
Component input = Input(&content, &option); Component input = Input(&content, {
EXPECT_EQ(option.cursor_position(), 0); .cursor_position = &cursor_position,
});
EXPECT_EQ(cursor_position, 0);
} }
TEST(InputTest, Type) { TEST(InputTest, Type) {
std::string content; std::string content;
std::string placeholder; int cursor_position = 0;
auto option = InputOption(); Component input = Input(&content, {
Component input = Input(&content, &option); .cursor_position = &cursor_position,
});
input->OnEvent(Event::Character("a")); input->OnEvent(Event::Character("a"));
EXPECT_EQ(content, "a"); EXPECT_EQ(content, "a");
EXPECT_EQ(option.cursor_position(), 1); EXPECT_EQ(cursor_position, 1);
input->OnEvent(Event::Character('b')); input->OnEvent(Event::Character('b'));
EXPECT_EQ(content, "ab"); EXPECT_EQ(content, "ab");
EXPECT_EQ(option.cursor_position(), 2); EXPECT_EQ(cursor_position, 2);
input->OnEvent(Event::Return); input->OnEvent(Event::Return);
EXPECT_EQ(content, "ab\n"); EXPECT_EQ(content, "ab\n");
EXPECT_EQ(option.cursor_position(), 3); EXPECT_EQ(cursor_position, 3);
input->OnEvent(Event::Character('c')); input->OnEvent(Event::Character('c'));
EXPECT_EQ(content, "ab\nc"); EXPECT_EQ(content, "ab\nc");
EXPECT_EQ(option.cursor_position(), 4); EXPECT_EQ(cursor_position, 4);
auto document = input->Render(); auto document = input->Render();
@ -55,57 +59,59 @@ TEST(InputTest, Type) {
TEST(InputTest, ArrowLeftRight) { TEST(InputTest, ArrowLeftRight) {
std::string content = "abc测测a测\na测\n"; std::string content = "abc测测a测\na测\n";
auto option = InputOption(); int cursor_position = 0;
auto input = Input(&content, &option); Component input = Input(&content, {
EXPECT_EQ(option.cursor_position(), 0); .cursor_position = &cursor_position,
});
EXPECT_EQ(cursor_position, 0);
EXPECT_FALSE(input->OnEvent(Event::ArrowLeft)); EXPECT_FALSE(input->OnEvent(Event::ArrowLeft));
EXPECT_EQ(option.cursor_position(), 0); EXPECT_EQ(cursor_position, 0);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 1); EXPECT_EQ(cursor_position, 1);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeft));
EXPECT_EQ(option.cursor_position(), 0); EXPECT_EQ(cursor_position, 0);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 1); EXPECT_EQ(cursor_position, 1);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 2); EXPECT_EQ(cursor_position, 2);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 3); EXPECT_EQ(cursor_position, 3);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 6); EXPECT_EQ(cursor_position, 6);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 9); EXPECT_EQ(cursor_position, 9);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 10); EXPECT_EQ(cursor_position, 10);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 13); EXPECT_EQ(cursor_position, 13);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 14); EXPECT_EQ(cursor_position, 14);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 15); EXPECT_EQ(cursor_position, 15);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 18); EXPECT_EQ(cursor_position, 18);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 19); EXPECT_EQ(cursor_position, 19);
EXPECT_FALSE(input->OnEvent(Event::ArrowRight)); EXPECT_FALSE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 19); EXPECT_EQ(cursor_position, 19);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeft));
EXPECT_EQ(option.cursor_position(), 18); EXPECT_EQ(cursor_position, 18);
} }
TEST(InputTest, ArrowUpDown) { TEST(InputTest, ArrowUpDown) {
@ -117,70 +123,75 @@ TEST(InputTest, ArrowUpDown) {
"\n" "\n"
"\n" "\n"
""; "";
auto option = InputOption(); int cursor_position = 0;
auto input = Input(&content, &option); Component input = Input(&content, {
.cursor_position = &cursor_position,
});
EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); EXPECT_TRUE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 4); EXPECT_EQ(cursor_position, 4);
EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); EXPECT_TRUE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 11); EXPECT_EQ(cursor_position, 11);
EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); EXPECT_TRUE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 21); EXPECT_EQ(cursor_position, 21);
EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); EXPECT_TRUE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 29); EXPECT_EQ(cursor_position, 29);
EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); EXPECT_TRUE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 36); EXPECT_EQ(cursor_position, 36);
EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); EXPECT_TRUE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 40); EXPECT_EQ(cursor_position, 40);
EXPECT_FALSE(input->OnEvent(Event::ArrowDown)); EXPECT_FALSE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 40); EXPECT_EQ(cursor_position, 40);
EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); EXPECT_TRUE(input->OnEvent(Event::ArrowUp));
EXPECT_EQ(option.cursor_position(), 36); EXPECT_EQ(cursor_position, 36);
EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); EXPECT_TRUE(input->OnEvent(Event::ArrowUp));
EXPECT_EQ(option.cursor_position(), 29); EXPECT_EQ(cursor_position, 29);
EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); EXPECT_TRUE(input->OnEvent(Event::ArrowUp));
EXPECT_EQ(option.cursor_position(), 21); EXPECT_EQ(cursor_position, 21);
EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); EXPECT_TRUE(input->OnEvent(Event::ArrowUp));
EXPECT_EQ(option.cursor_position(), 11); EXPECT_EQ(cursor_position, 11);
EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); EXPECT_TRUE(input->OnEvent(Event::ArrowUp));
EXPECT_EQ(option.cursor_position(), 4); EXPECT_EQ(cursor_position, 4);
EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); EXPECT_TRUE(input->OnEvent(Event::ArrowUp));
EXPECT_EQ(option.cursor_position(), 0); EXPECT_EQ(cursor_position, 0);
EXPECT_FALSE(input->OnEvent(Event::ArrowUp)); EXPECT_FALSE(input->OnEvent(Event::ArrowUp));
EXPECT_EQ(option.cursor_position(), 0); EXPECT_EQ(cursor_position, 0);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_EQ(option.cursor_position(), 3); EXPECT_EQ(cursor_position, 3);
EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); EXPECT_TRUE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 7); EXPECT_EQ(cursor_position, 7);
EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); EXPECT_TRUE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 14); EXPECT_EQ(cursor_position, 14);
EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); EXPECT_TRUE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 24); EXPECT_EQ(cursor_position, 24);
EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); EXPECT_TRUE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 32); EXPECT_EQ(cursor_position, 32);
EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); EXPECT_TRUE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 39); EXPECT_EQ(cursor_position, 39);
EXPECT_TRUE(input->OnEvent(Event::ArrowDown)); EXPECT_TRUE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 40); EXPECT_EQ(cursor_position, 40);
EXPECT_FALSE(input->OnEvent(Event::ArrowDown)); EXPECT_FALSE(input->OnEvent(Event::ArrowDown));
EXPECT_EQ(option.cursor_position(), 40); EXPECT_EQ(cursor_position, 40);
option.cursor_position() = 39; cursor_position = 39;
EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); EXPECT_TRUE(input->OnEvent(Event::ArrowUp));
EXPECT_EQ(option.cursor_position(), 32); EXPECT_EQ(cursor_position, 32);
EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); EXPECT_TRUE(input->OnEvent(Event::ArrowUp));
EXPECT_EQ(option.cursor_position(), 24); EXPECT_EQ(cursor_position, 24);
EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); EXPECT_TRUE(input->OnEvent(Event::ArrowUp));
EXPECT_EQ(option.cursor_position(), 14); EXPECT_EQ(cursor_position, 14);
EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); EXPECT_TRUE(input->OnEvent(Event::ArrowUp));
EXPECT_EQ(option.cursor_position(), 7); EXPECT_EQ(cursor_position, 7);
} }
TEST(InputTest, Insert) { TEST(InputTest, Insert) {
std::string content; std::string content;
Component input = Input(&content); int cursor_position = 0;
Component input = Input(&content, {
.cursor_position = &cursor_position,
});
EXPECT_TRUE(input->OnEvent(Event::Character('a'))); EXPECT_TRUE(input->OnEvent(Event::Character('a')));
EXPECT_TRUE(input->OnEvent(Event::Character('b'))); EXPECT_TRUE(input->OnEvent(Event::Character('b')));
@ -213,8 +224,10 @@ TEST(InputTest, Insert) {
TEST(InputTest, Home) { TEST(InputTest, Home) {
std::string content; std::string content;
auto option = InputOption(); int cursor_position = 0;
auto input = Input(&content, &option); Component input = Input(&content, {
.cursor_position = &cursor_position,
});
EXPECT_TRUE(input->OnEvent(Event::Character('a'))); EXPECT_TRUE(input->OnEvent(Event::Character('a')));
EXPECT_TRUE(input->OnEvent(Event::Character('b'))); EXPECT_TRUE(input->OnEvent(Event::Character('b')));
@ -224,21 +237,22 @@ TEST(InputTest, Home) {
EXPECT_TRUE(input->OnEvent(Event::Character('b'))); EXPECT_TRUE(input->OnEvent(Event::Character('b')));
EXPECT_TRUE(input->OnEvent(Event::Character('c'))); EXPECT_TRUE(input->OnEvent(Event::Character('c')));
EXPECT_EQ(content, "abc\n测bc"); EXPECT_EQ(content, "abc\n测bc");
EXPECT_EQ(option.cursor_position(), 9u); EXPECT_EQ(cursor_position, 9u);
EXPECT_TRUE(input->OnEvent(Event::Home)); EXPECT_TRUE(input->OnEvent(Event::Home));
EXPECT_EQ(option.cursor_position(), 0u); EXPECT_EQ(cursor_position, 0u);
EXPECT_TRUE(input->OnEvent(Event::Character('-'))); EXPECT_TRUE(input->OnEvent(Event::Character('-')));
EXPECT_EQ(option.cursor_position(), 1u); EXPECT_EQ(cursor_position, 1u);
EXPECT_EQ(content, "-abc\n测bc"); EXPECT_EQ(content, "-abc\n测bc");
} }
TEST(InputTest, End) { TEST(InputTest, End) {
std::string content; std::string content;
std::string placeholder; int cursor_position = 0;
auto option = InputOption(); Component input = Input(&content, {
auto input = Input(&content, &option); .cursor_position = &cursor_position,
});
EXPECT_TRUE(input->OnEvent(Event::Character('a'))); EXPECT_TRUE(input->OnEvent(Event::Character('a')));
EXPECT_TRUE(input->OnEvent(Event::Character('b'))); EXPECT_TRUE(input->OnEvent(Event::Character('b')));
@ -250,17 +264,18 @@ TEST(InputTest, End) {
EXPECT_TRUE(input->OnEvent(Event::ArrowUp)); EXPECT_TRUE(input->OnEvent(Event::ArrowUp));
EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeft));
EXPECT_EQ(content, "abc\n测bc"); EXPECT_EQ(content, "abc\n测bc");
EXPECT_EQ(option.cursor_position(), 2u); EXPECT_EQ(cursor_position, 2u);
input->OnEvent(Event::End); input->OnEvent(Event::End);
EXPECT_EQ(option.cursor_position(), 9u); EXPECT_EQ(cursor_position, 9u);
} }
TEST(InputTest, Delete) { TEST(InputTest, Delete) {
std::string content; std::string content;
std::string placeholder; int cursor_position = 0;
auto option = InputOption(); auto input = Input(&content, {
auto input = Input(&content, &option); .cursor_position = &cursor_position,
});
EXPECT_TRUE(input->OnEvent(Event::Character('a'))); EXPECT_TRUE(input->OnEvent(Event::Character('a')));
EXPECT_TRUE(input->OnEvent(Event::Character('b'))); EXPECT_TRUE(input->OnEvent(Event::Character('b')));
@ -271,38 +286,38 @@ TEST(InputTest, Delete) {
EXPECT_TRUE(input->OnEvent(Event::Character('c'))); EXPECT_TRUE(input->OnEvent(Event::Character('c')));
EXPECT_EQ(content, "abc\n测bc"); EXPECT_EQ(content, "abc\n测bc");
EXPECT_EQ(option.cursor_position(), 9u); EXPECT_EQ(cursor_position, 9u);
EXPECT_FALSE(input->OnEvent(Event::Delete)); EXPECT_FALSE(input->OnEvent(Event::Delete));
EXPECT_EQ(content, "abc\n测bc"); EXPECT_EQ(content, "abc\n测bc");
EXPECT_EQ(option.cursor_position(), 9u); EXPECT_EQ(cursor_position, 9u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeft));
EXPECT_EQ(content, "abc\n测bc"); EXPECT_EQ(content, "abc\n测bc");
EXPECT_EQ(option.cursor_position(), 8u); EXPECT_EQ(cursor_position, 8u);
EXPECT_TRUE(input->OnEvent(Event::Delete)); EXPECT_TRUE(input->OnEvent(Event::Delete));
EXPECT_EQ(content, "abc\n测b"); EXPECT_EQ(content, "abc\n测b");
EXPECT_EQ(option.cursor_position(), 8u); EXPECT_EQ(cursor_position, 8u);
EXPECT_FALSE(input->OnEvent(Event::Delete)); EXPECT_FALSE(input->OnEvent(Event::Delete));
EXPECT_EQ(content, "abc\n测b"); EXPECT_EQ(content, "abc\n测b");
EXPECT_EQ(option.cursor_position(), 8u); EXPECT_EQ(cursor_position, 8u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); 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_EQ(content, "abc\nb"); EXPECT_EQ(content, "abc\nb");
EXPECT_EQ(option.cursor_position(), 4u); EXPECT_EQ(cursor_position, 4u);
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_EQ(content, "abcb"); EXPECT_EQ(content, "abcb");
EXPECT_EQ(option.cursor_position(), 3u); EXPECT_EQ(cursor_position, 3u);
EXPECT_TRUE(input->OnEvent(Event::Delete)); EXPECT_TRUE(input->OnEvent(Event::Delete));
EXPECT_EQ(content, "abc"); EXPECT_EQ(content, "abc");
EXPECT_EQ(option.cursor_position(), 3u); EXPECT_EQ(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::ArrowLeft));
@ -318,9 +333,10 @@ TEST(InputTest, Delete) {
TEST(InputTest, Backspace) { TEST(InputTest, Backspace) {
std::string content; std::string content;
std::string placeholder; int cursor_position = 0;
auto option = InputOption(); auto input = Input(&content, {
auto input = Input(&content, &option); .cursor_position = &cursor_position,
});
EXPECT_TRUE(input->OnEvent(Event::Character('a'))); EXPECT_TRUE(input->OnEvent(Event::Character('a')));
EXPECT_TRUE(input->OnEvent(Event::Character('b'))); EXPECT_TRUE(input->OnEvent(Event::Character('b')));
@ -331,45 +347,45 @@ TEST(InputTest, Backspace) {
EXPECT_TRUE(input->OnEvent(Event::Character('c'))); EXPECT_TRUE(input->OnEvent(Event::Character('c')));
EXPECT_EQ(content, "abc\n测bc"); EXPECT_EQ(content, "abc\n测bc");
EXPECT_EQ(option.cursor_position(), 9u); EXPECT_EQ(cursor_position, 9u);
EXPECT_TRUE(input->OnEvent(Event::Backspace)); EXPECT_TRUE(input->OnEvent(Event::Backspace));
EXPECT_EQ(content, "abc\n测b"); EXPECT_EQ(content, "abc\n测b");
EXPECT_EQ(option.cursor_position(), 8u); EXPECT_EQ(cursor_position, 8u);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeft)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeft));
EXPECT_TRUE(input->OnEvent(Event::Backspace)); EXPECT_TRUE(input->OnEvent(Event::Backspace));
EXPECT_EQ(content, "abc\nb"); EXPECT_EQ(content, "abc\nb");
EXPECT_EQ(option.cursor_position(), 4u); EXPECT_EQ(cursor_position, 4u);
EXPECT_TRUE(input->OnEvent(Event::Backspace)); EXPECT_TRUE(input->OnEvent(Event::Backspace));
EXPECT_EQ(content, "abcb"); EXPECT_EQ(content, "abcb");
EXPECT_EQ(option.cursor_position(), 3u); EXPECT_EQ(cursor_position, 3u);
EXPECT_TRUE(input->OnEvent(Event::Backspace)); EXPECT_TRUE(input->OnEvent(Event::Backspace));
EXPECT_EQ(content, "abb"); EXPECT_EQ(content, "abb");
EXPECT_EQ(option.cursor_position(), 2u); EXPECT_EQ(cursor_position, 2u);
EXPECT_TRUE(input->OnEvent(Event::Backspace)); EXPECT_TRUE(input->OnEvent(Event::Backspace));
EXPECT_EQ(content, "ab"); EXPECT_EQ(content, "ab");
EXPECT_EQ(option.cursor_position(), 1u); EXPECT_EQ(cursor_position, 1u);
EXPECT_TRUE(input->OnEvent(Event::Backspace)); EXPECT_TRUE(input->OnEvent(Event::Backspace));
EXPECT_EQ(content, "b"); EXPECT_EQ(content, "b");
EXPECT_EQ(option.cursor_position(), 0u); EXPECT_EQ(cursor_position, 0u);
EXPECT_FALSE(input->OnEvent(Event::Backspace)); EXPECT_FALSE(input->OnEvent(Event::Backspace));
EXPECT_EQ(content, "b"); EXPECT_EQ(content, "b");
EXPECT_EQ(option.cursor_position(), 0u); EXPECT_EQ(cursor_position, 0u);
EXPECT_TRUE(input->OnEvent(Event::ArrowRight)); EXPECT_TRUE(input->OnEvent(Event::ArrowRight));
EXPECT_TRUE(input->OnEvent(Event::Backspace)); EXPECT_TRUE(input->OnEvent(Event::Backspace));
EXPECT_EQ(content, ""); EXPECT_EQ(content, "");
EXPECT_EQ(option.cursor_position(), 0u); EXPECT_EQ(cursor_position, 0u);
EXPECT_FALSE(input->OnEvent(Event::Backspace)); EXPECT_FALSE(input->OnEvent(Event::Backspace));
EXPECT_EQ(content, ""); EXPECT_EQ(content, "");
EXPECT_EQ(option.cursor_position(), 0u); EXPECT_EQ(cursor_position, 0u);
} }
TEST(InputTest, CtrlArrow) { TEST(InputTest, CtrlArrow) {
@ -377,192 +393,193 @@ TEST(InputTest, CtrlArrow) {
"word word 测ord wo测d word\n" "word word 测ord wo测d word\n"
"coucou coucou coucou\n" "coucou coucou coucou\n"
"coucou coucou coucou\n"; "coucou coucou coucou\n";
std::string placeholder; int cursor_position = 1000;
auto option = InputOption(); auto input = Input(&content, {
option.cursor_position = 1000; .cursor_position = &cursor_position,
auto input = Input(&content, &option); });
// Use CTRL+Left several time // Use CTRL+Left several time
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 67); EXPECT_EQ(cursor_position, 67);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 60); EXPECT_EQ(cursor_position, 60);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 53); EXPECT_EQ(cursor_position, 53);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 46); EXPECT_EQ(cursor_position, 46);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 39); EXPECT_EQ(cursor_position, 39);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 29); EXPECT_EQ(cursor_position, 29);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 24); EXPECT_EQ(cursor_position, 24);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 17); EXPECT_EQ(cursor_position, 17);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 10); EXPECT_EQ(cursor_position, 10);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 5); EXPECT_EQ(cursor_position, 5);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 0); EXPECT_EQ(cursor_position, 0);
EXPECT_FALSE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_FALSE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 0); EXPECT_EQ(cursor_position, 0);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 4); EXPECT_EQ(cursor_position, 4);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 9); EXPECT_EQ(cursor_position, 9);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 16); EXPECT_EQ(cursor_position, 16);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 23); EXPECT_EQ(cursor_position, 23);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 28); EXPECT_EQ(cursor_position, 28);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 35); EXPECT_EQ(cursor_position, 35);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 45); EXPECT_EQ(cursor_position, 45);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 52); EXPECT_EQ(cursor_position, 52);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 59); EXPECT_EQ(cursor_position, 59);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 66); EXPECT_EQ(cursor_position, 66);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 73); EXPECT_EQ(cursor_position, 73);
} }
TEST(InputTest, CtrlArrowLeft2) { TEST(InputTest, CtrlArrowLeft2) {
std::string content = " word word 测ord wo测d word "; std::string content = " word word 测ord wo测d word ";
auto option = InputOption(); int cursor_position = 33;
option.cursor_position = 33; auto input = Input(&content, {
auto input = Input(&content, &option); .cursor_position = &cursor_position,
});
// Use CTRL+Left several time // Use CTRL+Left several time
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 31); EXPECT_EQ(cursor_position, 31);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 23); EXPECT_EQ(cursor_position, 23);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 15); EXPECT_EQ(cursor_position, 15);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 9); EXPECT_EQ(cursor_position, 9);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 3); EXPECT_EQ(cursor_position, 3);
EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 0); EXPECT_EQ(cursor_position, 0);
EXPECT_FALSE(input->OnEvent(Event::ArrowLeftCtrl)); EXPECT_FALSE(input->OnEvent(Event::ArrowLeftCtrl));
EXPECT_EQ(option.cursor_position(), 0); EXPECT_EQ(cursor_position, 0);
} }
TEST(InputTest, CtrlArrowRight) { TEST(InputTest, CtrlArrowRight) {
std::string content = std::string content =
"word word 测ord wo测d word\n" "word word 测ord wo测d word\n"
"coucou dfqdsf jmlkjm"; "coucou dfqdsf jmlkjm";
int cursor_position = 2;
auto option = InputOption(); auto input = Input(&content, {.cursor_position = &cursor_position});
option.cursor_position = 2;
auto input = Input(&content, &option);
// Use CTRL+Left several time // Use CTRL+Left several time
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 4); EXPECT_EQ(cursor_position, 4);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 9); EXPECT_EQ(cursor_position, 9);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 16); EXPECT_EQ(cursor_position, 16);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 23); EXPECT_EQ(cursor_position, 23);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 28); EXPECT_EQ(cursor_position, 28);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 35); EXPECT_EQ(cursor_position, 35);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 42); EXPECT_EQ(cursor_position, 42);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 49); EXPECT_EQ(cursor_position, 49);
EXPECT_FALSE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_FALSE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 49); EXPECT_EQ(cursor_position, 49);
} }
TEST(InputTest, CtrlArrowRight2) { TEST(InputTest, CtrlArrowRight2) {
std::string content = " word word 测ord wo测d word "; std::string content = " word word 测ord wo测d word ";
auto option = InputOption(); int cursor_position = 0;
auto input = Input(&content, &option); auto input = Input(&content, {.cursor_position = &cursor_position});
// Use CTRL+Left several time // Use CTRL+Left several time
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 7); EXPECT_EQ(cursor_position, 7);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 13); EXPECT_EQ(cursor_position, 13);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 21); EXPECT_EQ(cursor_position, 21);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 29); EXPECT_EQ(cursor_position, 29);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 35); EXPECT_EQ(cursor_position, 35);
EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_TRUE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 38); EXPECT_EQ(cursor_position, 38);
EXPECT_FALSE(input->OnEvent(Event::ArrowRightCtrl)); EXPECT_FALSE(input->OnEvent(Event::ArrowRightCtrl));
EXPECT_EQ(option.cursor_position(), 38); EXPECT_EQ(cursor_position, 38);
} }
TEST(InputTest, TypePassword) { TEST(InputTest, TypePassword) {
std::string content; std::string content;
std::string placeholder; std::string placeholder;
auto option = InputOption(); int cursor_position = 0;
option.cursor_position = 0; Component input = Input(&content, &placeholder,
option.password = true; {
Component input = Input(&content, &placeholder, &option); .password = true,
.cursor_position = &cursor_position,
});
input->OnEvent(Event::Character('a')); input->OnEvent(Event::Character('a'));
EXPECT_EQ(content, "a"); EXPECT_EQ(content, "a");
EXPECT_EQ(option.cursor_position(), 1u); EXPECT_EQ(cursor_position, 1u);
input->OnEvent(Event::Character('b')); input->OnEvent(Event::Character('b'));
EXPECT_EQ(content, "ab"); EXPECT_EQ(content, "ab");
EXPECT_EQ(option.cursor_position(), 2u); EXPECT_EQ(cursor_position, 2u);
auto document = input->Render(); auto document = input->Render();
auto screen = Screen::Create(Dimension::Fit(document)); auto screen = Screen::Create(Dimension::Fit(document));
@ -573,8 +590,8 @@ TEST(InputTest, TypePassword) {
TEST(InputTest, MouseClick) { TEST(InputTest, MouseClick) {
std::string content; std::string content;
auto option = InputOption(); int cursor_position = 0;
auto input = Input(&content, &option); auto input = Input(&content, {.cursor_position = &cursor_position});
input->OnEvent(Event::Character("a")); input->OnEvent(Event::Character("a"));
input->OnEvent(Event::Character("b")); input->OnEvent(Event::Character("b"));
@ -588,7 +605,7 @@ TEST(InputTest, MouseClick) {
input->OnEvent(Event::Return); input->OnEvent(Event::Return);
EXPECT_EQ(content, "abcd\nabcd\n"); EXPECT_EQ(content, "abcd\nabcd\n");
EXPECT_EQ(option.cursor_position(), 10u); EXPECT_EQ(cursor_position, 10u);
auto render = [&] { auto render = [&] {
auto document = input->Render(); auto document = input->Render();
@ -596,7 +613,7 @@ TEST(InputTest, MouseClick) {
Render(screen, document); Render(screen, document);
}; };
render(); render();
EXPECT_EQ(option.cursor_position(), 10u); EXPECT_EQ(cursor_position, 10u);
Mouse mouse; Mouse mouse;
mouse.button = Mouse::Button::Left; mouse.button = Mouse::Button::Left;
@ -609,67 +626,67 @@ TEST(InputTest, MouseClick) {
mouse.y = 0; mouse.y = 0;
EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 0u); EXPECT_EQ(cursor_position, 0u);
mouse.x = 2; mouse.x = 2;
mouse.y = 0; mouse.y = 0;
EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 2u); EXPECT_EQ(cursor_position, 2u);
mouse.x = 2; mouse.x = 2;
mouse.y = 0; mouse.y = 0;
EXPECT_FALSE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_FALSE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 2u); EXPECT_EQ(cursor_position, 2u);
mouse.x = 1; mouse.x = 1;
mouse.y = 0; mouse.y = 0;
EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 1u); EXPECT_EQ(cursor_position, 1u);
mouse.x = 3; mouse.x = 3;
mouse.y = 0; mouse.y = 0;
EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 3u); EXPECT_EQ(cursor_position, 3u);
mouse.x = 4; mouse.x = 4;
mouse.y = 0; mouse.y = 0;
EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 4u); EXPECT_EQ(cursor_position, 4u);
mouse.x = 5; mouse.x = 5;
mouse.y = 0; mouse.y = 0;
EXPECT_FALSE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_FALSE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 4u); EXPECT_EQ(cursor_position, 4u);
mouse.x = 5; mouse.x = 5;
mouse.y = 1; mouse.y = 1;
EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 9u); EXPECT_EQ(cursor_position, 9u);
mouse.x = 1; mouse.x = 1;
mouse.y = 1; mouse.y = 1;
EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 6u); EXPECT_EQ(cursor_position, 6u);
mouse.x = 4; mouse.x = 4;
mouse.y = 2; mouse.y = 2;
EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 10u); EXPECT_EQ(cursor_position, 10u);
} }
TEST(InputTest, MouseClickComplex) { TEST(InputTest, MouseClickComplex) {
std::string content; std::string content;
auto option = InputOption(); int cursor_position = 0;
auto input = Input(&content, &option); auto input = Input(&content, {.cursor_position = &cursor_position});
input->OnEvent(Event::Character("")); input->OnEvent(Event::Character(""));
input->OnEvent(Event::Character("")); input->OnEvent(Event::Character(""));
@ -681,7 +698,7 @@ TEST(InputTest, MouseClickComplex) {
input->OnEvent(Event::Character("a⃒")); input->OnEvent(Event::Character("a⃒"));
input->OnEvent(Event::Character("")); input->OnEvent(Event::Character(""));
EXPECT_EQ(option.cursor_position(), 27u); EXPECT_EQ(cursor_position, 27u);
auto render = [&] { auto render = [&] {
auto document = input->Render(); auto document = input->Render();
@ -701,25 +718,25 @@ TEST(InputTest, MouseClickComplex) {
mouse.y = 0; mouse.y = 0;
EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 0); EXPECT_EQ(cursor_position, 0);
mouse.x = 0; mouse.x = 0;
mouse.y = 1; mouse.y = 1;
EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 14); EXPECT_EQ(cursor_position, 14);
mouse.x = 1; mouse.x = 1;
mouse.y = 0; mouse.y = 0;
EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 3); EXPECT_EQ(cursor_position, 3);
mouse.x = 1; mouse.x = 1;
mouse.y = 1; mouse.y = 1;
EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse))); EXPECT_TRUE(input->OnEvent(Event::Mouse("", mouse)));
render(); render();
EXPECT_EQ(option.cursor_position(), 17); EXPECT_EQ(cursor_position, 17);
} }
TEST(InputTest, OnEnter) { TEST(InputTest, OnEnter) {
@ -727,7 +744,7 @@ TEST(InputTest, OnEnter) {
auto option = InputOption(); auto option = InputOption();
bool on_enter_called = false; bool on_enter_called = false;
option.on_enter = [&] { on_enter_called = true; }; option.on_enter = [&] { on_enter_called = true; };
Component input = Input(&content, &option); Component input = Input(&content, option);
EXPECT_FALSE(on_enter_called); EXPECT_FALSE(on_enter_called);
EXPECT_TRUE(input->OnEvent(Event::Return)); EXPECT_TRUE(input->OnEvent(Event::Return));

View File

@ -65,30 +65,29 @@ bool IsHorizontal(Direction direction) {
/// @brief A list of items. The user can navigate through them. /// @brief A list of items. The user can navigate through them.
/// @ingroup component /// @ingroup component
class MenuBase : public ComponentBase { class MenuBase : public ComponentBase, public MenuOption {
public: public:
MenuBase(ConstStringListRef entries, int* selected, Ref<MenuOption> option) explicit MenuBase(MenuOption option) : MenuOption(std::move(option)) {}
: entries_(entries), selected_(selected), option_(std::move(option)) {}
bool IsHorizontal() { return ftxui::IsHorizontal(option_->direction); } bool IsHorizontal() { return ftxui::IsHorizontal(direction); }
void OnChange() { void OnChange() {
if (option_->on_change) { if (on_change) {
option_->on_change(); on_change();
} }
} }
void OnEnter() { void OnEnter() {
if (option_->on_enter) { if (on_enter) {
option_->on_enter(); on_enter();
} }
} }
void Clamp() { void Clamp() {
if (*selected_ != selected_previous_) { if (selected() != selected_previous_) {
SelectedTakeFocus(); SelectedTakeFocus();
} }
boxes_.resize(size()); boxes_.resize(size());
*selected_ = util::clamp(*selected_, 0, size() - 1); selected() = util::clamp(selected(), 0, size() - 1);
selected_previous_ = util::clamp(selected_previous_, 0, size() - 1); selected_previous_ = util::clamp(selected_previous_, 0, size() - 1);
selected_focus_ = util::clamp(selected_focus_, 0, size() - 1); selected_focus_ = util::clamp(selected_focus_, 0, size() - 1);
focused_entry() = util::clamp(focused_entry(), 0, size() - 1); focused_entry() = util::clamp(focused_entry(), 0, size() - 1);
@ -111,19 +110,19 @@ class MenuBase : public ComponentBase {
Elements elements; Elements elements;
const bool is_menu_focused = Focused(); const bool is_menu_focused = Focused();
if (option_->elements_prefix) { if (elements_prefix) {
elements.push_back(option_->elements_prefix()); elements.push_back(elements_prefix());
} }
elements.reserve(size()); elements.reserve(size());
for (int i = 0; i < size(); ++i) { for (int i = 0; i < size(); ++i) {
if (i != 0 && option_->elements_infix) { if (i != 0 && elements_infix) {
elements.push_back(option_->elements_infix()); elements.push_back(elements_infix());
} }
const bool is_focused = (focused_entry() == i) && is_menu_focused; const bool is_focused = (focused_entry() == i) && is_menu_focused;
const bool is_selected = (*selected_ == i); const bool is_selected = (selected() == i);
const EntryState state = { const EntryState state = {
entries_[i], entries[i],
false, false,
is_selected, is_selected,
is_focused, is_focused,
@ -133,24 +132,24 @@ class MenuBase : public ComponentBase {
is_menu_focused && (selected_focus_ == i) ? focus : nothing; is_menu_focused && (selected_focus_ == i) ? focus : nothing;
const Element element = const Element element =
(option_->entries.transform ? option_->entries.transform (entries_option.transform ? entries_option.transform
: DefaultOptionTransform) // : DefaultOptionTransform) //
(state); (state);
elements.push_back(element | AnimatedColorStyle(i) | reflect(boxes_[i]) | elements.push_back(element | AnimatedColorStyle(i) | reflect(boxes_[i]) |
focus_management); focus_management);
} }
if (option_->elements_postfix) { if (elements_postfix) {
elements.push_back(option_->elements_postfix()); elements.push_back(elements_postfix());
} }
if (IsInverted(option_->direction)) { if (IsInverted(direction)) {
std::reverse(elements.begin(), elements.end()); std::reverse(elements.begin(), elements.end());
} }
const Element bar = const Element bar =
IsHorizontal() ? hbox(std::move(elements)) : vbox(std::move(elements)); IsHorizontal() ? hbox(std::move(elements)) : vbox(std::move(elements));
if (!option_->underline.enabled) { if (!underline.enabled) {
return bar | reflect(box_); return bar | reflect(box_);
} }
@ -158,15 +157,15 @@ class MenuBase : public ComponentBase {
return vbox({ return vbox({
bar | xflex, bar | xflex,
separatorHSelector(first_, second_, // separatorHSelector(first_, second_, //
option_->underline.color_active, underline.color_active,
option_->underline.color_inactive), underline.color_inactive),
}) | }) |
reflect(box_); reflect(box_);
} else { } else {
return hbox({ return hbox({
separatorVSelector(first_, second_, // separatorVSelector(first_, second_, //
option_->underline.color_active, underline.color_active,
option_->underline.color_inactive), underline.color_inactive),
bar | yflex, bar | yflex,
}) | }) |
reflect(box_); reflect(box_);
@ -174,17 +173,17 @@ class MenuBase : public ComponentBase {
} }
void SelectedTakeFocus() { void SelectedTakeFocus() {
selected_previous_ = *selected_; selected_previous_ = selected();
selected_focus_ = *selected_; selected_focus_ = selected();
} }
void OnUp() { void OnUp() {
switch (option_->direction) { switch (direction) {
case Direction::Up: case Direction::Up:
(*selected_)++; selected()++;
break; break;
case Direction::Down: case Direction::Down:
(*selected_)--; selected()--;
break; break;
case Direction::Left: case Direction::Left:
case Direction::Right: case Direction::Right:
@ -193,12 +192,12 @@ class MenuBase : public ComponentBase {
} }
void OnDown() { void OnDown() {
switch (option_->direction) { switch (direction) {
case Direction::Up: case Direction::Up:
(*selected_)--; selected()--;
break; break;
case Direction::Down: case Direction::Down:
(*selected_)++; selected()++;
break; break;
case Direction::Left: case Direction::Left:
case Direction::Right: case Direction::Right:
@ -207,12 +206,12 @@ class MenuBase : public ComponentBase {
} }
void OnLeft() { void OnLeft() {
switch (option_->direction) { switch (direction) {
case Direction::Left: case Direction::Left:
(*selected_)++; selected()++;
break; break;
case Direction::Right: case Direction::Right:
(*selected_)--; selected()--;
break; break;
case Direction::Down: case Direction::Down:
case Direction::Up: case Direction::Up:
@ -221,12 +220,12 @@ class MenuBase : public ComponentBase {
} }
void OnRight() { void OnRight() {
switch (option_->direction) { switch (direction) {
case Direction::Left: case Direction::Left:
(*selected_)--; selected()--;
break; break;
case Direction::Right: case Direction::Right:
(*selected_)++; selected()++;
break; break;
case Direction::Down: case Direction::Down:
case Direction::Up: case Direction::Up:
@ -246,7 +245,7 @@ class MenuBase : public ComponentBase {
} }
if (Focused()) { if (Focused()) {
const int old_selected = *selected_; const int old_selected = selected();
if (event == Event::ArrowUp || event == Event::Character('k')) { if (event == Event::ArrowUp || event == Event::Character('k')) {
OnUp(); OnUp();
} }
@ -260,28 +259,28 @@ class MenuBase : public ComponentBase {
OnRight(); OnRight();
} }
if (event == Event::PageUp) { if (event == Event::PageUp) {
(*selected_) -= box_.y_max - box_.y_min; selected() -= box_.y_max - box_.y_min;
} }
if (event == Event::PageDown) { if (event == Event::PageDown) {
(*selected_) += box_.y_max - box_.y_min; selected() += box_.y_max - box_.y_min;
} }
if (event == Event::Home) { if (event == Event::Home) {
(*selected_) = 0; selected() = 0;
} }
if (event == Event::End) { if (event == Event::End) {
(*selected_) = size() - 1; selected() = size() - 1;
} }
if (event == Event::Tab && size()) { if (event == Event::Tab && size()) {
*selected_ = (*selected_ + 1) % size(); selected() = (selected() + 1) % size();
} }
if (event == Event::TabReverse && size()) { if (event == Event::TabReverse && size()) {
*selected_ = (*selected_ + size() - 1) % size(); selected() = (selected() + size() - 1) % size();
} }
*selected_ = util::clamp(*selected_, 0, size() - 1); selected() = util::clamp(selected(), 0, size() - 1);
if (*selected_ != old_selected) { if (selected() != old_selected) {
focused_entry() = *selected_; focused_entry() = selected();
SelectedTakeFocus(); SelectedTakeFocus();
OnChange(); OnChange();
return true; return true;
@ -318,9 +317,9 @@ class MenuBase : public ComponentBase {
focused_entry() = i; focused_entry() = i;
if (event.mouse().button == Mouse::Left && if (event.mouse().button == Mouse::Left &&
event.mouse().motion == Mouse::Released) { event.mouse().motion == Mouse::Released) {
if (*selected_ != i) { if (selected() != i) {
*selected_ = i; selected() = i;
selected_previous_ = *selected_; selected_previous_ = selected();
OnChange(); OnChange();
} }
return true; return true;
@ -333,18 +332,18 @@ class MenuBase : public ComponentBase {
if (!box_.Contain(event.mouse().x, event.mouse().y)) { if (!box_.Contain(event.mouse().x, event.mouse().y)) {
return false; return false;
} }
const int old_selected = *selected_; const int old_selected = selected();
if (event.mouse().button == Mouse::WheelUp) { if (event.mouse().button == Mouse::WheelUp) {
(*selected_)--; selected()--;
} }
if (event.mouse().button == Mouse::WheelDown) { if (event.mouse().button == Mouse::WheelDown) {
(*selected_)++; selected()++;
} }
*selected_ = util::clamp(*selected_, 0, size() - 1); selected() = util::clamp(selected(), 0, size() - 1);
if (*selected_ != old_selected) { if (selected() != old_selected) {
SelectedTakeFocus(); SelectedTakeFocus();
OnChange(); OnChange();
} }
@ -381,41 +380,41 @@ class MenuBase : public ComponentBase {
const bool is_menu_focused = Focused(); const bool is_menu_focused = Focused();
for (int i = 0; i < size(); ++i) { for (int i = 0; i < size(); ++i) {
const bool is_focused = (focused_entry() == i) && is_menu_focused; const bool is_focused = (focused_entry() == i) && is_menu_focused;
const bool is_selected = (*selected_ == i); const bool is_selected = (selected() == i);
float target = is_selected ? 1.F : is_focused ? 0.5F : 0.F; // NOLINT float target = is_selected ? 1.F : is_focused ? 0.5F : 0.F; // NOLINT
if (animator_background_[i].to() != target) { if (animator_background_[i].to() != target) {
animator_background_[i] = animation::Animator( animator_background_[i] = animation::Animator(
&animation_background_[i], target, &animation_background_[i], target,
option_->entries.animated_colors.background.duration, entries_option.animated_colors.background.duration,
option_->entries.animated_colors.background.function); entries_option.animated_colors.background.function);
animator_foreground_[i] = animation::Animator( animator_foreground_[i] = animation::Animator(
&animation_foreground_[i], target, &animation_foreground_[i], target,
option_->entries.animated_colors.foreground.duration, entries_option.animated_colors.foreground.duration,
option_->entries.animated_colors.foreground.function); entries_option.animated_colors.foreground.function);
} }
} }
} }
Decorator AnimatedColorStyle(int i) { Decorator AnimatedColorStyle(int i) {
Decorator style = nothing; Decorator style = nothing;
if (option_->entries.animated_colors.foreground.enabled) { if (entries_option.animated_colors.foreground.enabled) {
style = style | color(Color::Interpolate( style = style | color(Color::Interpolate(
animation_foreground_[i], animation_foreground_[i],
option_->entries.animated_colors.foreground.inactive, entries_option.animated_colors.foreground.inactive,
option_->entries.animated_colors.foreground.active)); entries_option.animated_colors.foreground.active));
} }
if (option_->entries.animated_colors.background.enabled) { if (entries_option.animated_colors.background.enabled) {
style = style | bgcolor(Color::Interpolate( style = style | bgcolor(Color::Interpolate(
animation_background_[i], animation_background_[i],
option_->entries.animated_colors.background.inactive, entries_option.animated_colors.background.inactive,
option_->entries.animated_colors.background.active)); entries_option.animated_colors.background.active));
} }
return style; return style;
} }
void UpdateUnderlineTarget() { void UpdateUnderlineTarget() {
if (!option_->underline.enabled) { if (!underline.enabled) {
return; return;
} }
@ -426,66 +425,93 @@ class MenuBase : public ComponentBase {
if (FirstTarget() >= animator_first_.to()) { if (FirstTarget() >= animator_first_.to()) {
animator_first_ = animation::Animator( animator_first_ = animation::Animator(
&first_, FirstTarget(), option_->underline.follower_duration, &first_, FirstTarget(), underline.follower_duration,
option_->underline.follower_function, underline.follower_function, underline.follower_delay);
option_->underline.follower_delay);
animator_second_ = animation::Animator( animator_second_ = animation::Animator(
&second_, SecondTarget(), option_->underline.leader_duration, &second_, SecondTarget(), underline.leader_duration,
option_->underline.leader_function, option_->underline.leader_delay); underline.leader_function, underline.leader_delay);
} else { } else {
animator_first_ = animation::Animator( animator_first_ = animation::Animator(
&first_, FirstTarget(), option_->underline.leader_duration, &first_, FirstTarget(), underline.leader_duration,
option_->underline.leader_function, option_->underline.leader_delay); underline.leader_function, underline.leader_delay);
animator_second_ = animation::Animator( animator_second_ = animation::Animator(
&second_, SecondTarget(), option_->underline.follower_duration, &second_, SecondTarget(), underline.follower_duration,
option_->underline.follower_function, underline.follower_function, underline.follower_delay);
option_->underline.follower_delay);
} }
} }
bool Focusable() const final { return entries_.size(); } bool Focusable() const final { return entries.size(); }
int& focused_entry() { return option_->focused_entry(); } int size() const { return int(entries.size()); }
int size() const { return int(entries_.size()); }
float FirstTarget() { float FirstTarget() {
if (boxes_.empty()) { if (boxes_.empty()) {
return 0.F; return 0.F;
} }
const int value = IsHorizontal() ? boxes_[*selected_].x_min - box_.x_min const int value = IsHorizontal() ? boxes_[selected()].x_min - box_.x_min
: boxes_[*selected_].y_min - box_.y_min; : boxes_[selected()].y_min - box_.y_min;
return float(value); return float(value);
} }
float SecondTarget() { float SecondTarget() {
if (boxes_.empty()) { if (boxes_.empty()) {
return 0.F; return 0.F;
} }
const int value = IsHorizontal() ? boxes_[*selected_].x_max - box_.x_min const int value = IsHorizontal() ? boxes_[selected()].x_max - box_.x_min
: boxes_[*selected_].y_max - box_.y_min; : boxes_[selected()].y_max - box_.y_min;
return float(value); return float(value);
} }
protected: protected:
ConstStringListRef entries_; int selected_previous_ = selected();
int* selected_; int selected_focus_ = selected();
int selected_previous_ = *selected_;
int selected_focus_ = *selected_;
Ref<MenuOption> option_;
// Mouse click support:
std::vector<Box> boxes_; std::vector<Box> boxes_;
Box box_; Box box_;
// Animation support:
float first_ = 0.F; float first_ = 0.F;
float second_ = 0.F; float second_ = 0.F;
animation::Animator animator_first_ = animation::Animator(&first_, 0.F); animation::Animator animator_first_ = animation::Animator(&first_, 0.F);
animation::Animator animator_second_ = animation::Animator(&second_, 0.F); animation::Animator animator_second_ = animation::Animator(&second_, 0.F);
std::vector<animation::Animator> animator_background_; std::vector<animation::Animator> animator_background_;
std::vector<animation::Animator> animator_foreground_; std::vector<animation::Animator> animator_foreground_;
std::vector<float> animation_background_; std::vector<float> animation_background_;
std::vector<float> animation_foreground_; std::vector<float> animation_foreground_;
}; };
/// @brief A list of text. The focused element is selected.
/// @param option a structure containing all the paramters.
/// @ingroup component
///
/// ### Example
///
/// ```cpp
/// auto screen = ScreenInteractive::TerminalOutput();
/// std::vector<std::string> entries = {
/// "entry 1",
/// "entry 2",
/// "entry 3",
/// };
/// int selected = 0;
/// auto menu = Menu({
/// .entries = &entries,
/// .selected = &selected,
/// });
/// screen.Loop(menu);
/// ```
///
/// ### Output
///
/// ```bash
/// > entry 1
/// entry 2
/// entry 3
/// ```
Component Menu(MenuOption option) {
return Make<MenuBase>(std::move(option));
}
/// @brief A list of text. The focused element is selected. /// @brief A list of text. The focused element is selected.
/// @param entries The list of entries in the menu. /// @param entries The list of entries in the menu.
/// @param selected The index of the currently selected element. /// @param selected The index of the currently selected element.
@ -513,10 +539,10 @@ class MenuBase : public ComponentBase {
/// entry 2 /// entry 2
/// entry 3 /// entry 3
/// ``` /// ```
Component Menu(ConstStringListRef entries, Component Menu(ConstStringListRef entries, int* selected, MenuOption option) {
int* selected, option.entries = entries;
Ref<MenuOption> option) { option.selected = selected;
return Make<MenuBase>(entries, selected, std::move(option)); return Menu(std::move(option));
} }
/// @brief An horizontal list of elements. The user can navigate through them. /// @brief An horizontal list of elements. The user can navigate through them.
@ -554,11 +580,41 @@ Component Toggle(ConstStringListRef entries, int* selected) {
/// entry 2 /// entry 2
/// entry 3 /// entry 3
/// ``` /// ```
Component MenuEntry(ConstStringRef label, Ref<MenuEntryOption> option) { Component MenuEntry(ConstStringRef label, MenuEntryOption option) {
class Impl : public ComponentBase { option.label = label;
return MenuEntry(std::move(option));
}
/// @brief A specific menu entry. They can be put into a Container::Vertical to
/// form a menu.
/// @param option The parameters.
/// @ingroup component
///
/// ### Example
///
/// ```cpp
/// auto screen = ScreenInteractive::TerminalOutput();
/// int selected = 0;
/// auto menu = Container::Vertical({
/// MenuEntry({.label = "entry 1"}),
/// MenuEntry({.label = "entry 2"}),
/// MenuEntry({.label = "entry 3"}),
/// }, &selected);
/// screen.Loop(menu);
/// ```
///
/// ### Output
///
/// ```bash
/// > entry 1
/// entry 2
/// entry 3
/// ```
Component MenuEntry(MenuEntryOption option) {
class Impl : public ComponentBase, public MenuEntryOption {
public: public:
Impl(ConstStringRef label, Ref<MenuEntryOption> option) explicit Impl(MenuEntryOption option)
: label_(std::move(label)), option_(std::move(option)) {} : MenuEntryOption(std::move(option)) {}
private: private:
Element Render() override { Element Render() override {
@ -566,14 +622,14 @@ Component MenuEntry(ConstStringRef label, Ref<MenuEntryOption> option) {
UpdateAnimationTarget(); UpdateAnimationTarget();
const EntryState state = { const EntryState state = {
*label_, label(),
false, false,
hovered_, hovered_,
focused, focused,
}; };
const Element element = const Element element =
(option_->transform ? option_->transform : DefaultOptionTransform) // (transform ? transform : DefaultOptionTransform) //
(state); (state);
auto focus_management = focused ? select : nothing; auto focus_management = focused ? select : nothing;
@ -586,30 +642,28 @@ Component MenuEntry(ConstStringRef label, Ref<MenuEntryOption> option) {
if (target == animator_background_.to()) { if (target == animator_background_.to()) {
return; return;
} }
animator_background_ = animator_background_ = animation::Animator(
animation::Animator(&animation_background_, target, &animation_background_, target, animated_colors.background.duration,
option_->animated_colors.background.duration, animated_colors.background.function);
option_->animated_colors.background.function); animator_foreground_ = animation::Animator(
animator_foreground_ = &animation_foreground_, target, animated_colors.foreground.duration,
animation::Animator(&animation_foreground_, target, animated_colors.foreground.function);
option_->animated_colors.foreground.duration,
option_->animated_colors.foreground.function);
} }
Decorator AnimatedColorStyle() { Decorator AnimatedColorStyle() {
Decorator style = nothing; Decorator style = nothing;
if (option_->animated_colors.foreground.enabled) { if (animated_colors.foreground.enabled) {
style = style | color(Color::Interpolate( style = style |
animation_foreground_, color(Color::Interpolate(animation_foreground_,
option_->animated_colors.foreground.inactive, animated_colors.foreground.inactive,
option_->animated_colors.foreground.active)); animated_colors.foreground.active));
} }
if (option_->animated_colors.background.enabled) { if (animated_colors.background.enabled) {
style = style | bgcolor(Color::Interpolate( style = style |
animation_background_, bgcolor(Color::Interpolate(animation_background_,
option_->animated_colors.background.inactive, animated_colors.background.inactive,
option_->animated_colors.background.active)); animated_colors.background.active));
} }
return style; return style;
} }
@ -640,8 +694,7 @@ Component MenuEntry(ConstStringRef label, Ref<MenuEntryOption> option) {
animator_foreground_.OnAnimation(params); animator_foreground_.OnAnimation(params);
} }
ConstStringRef label_; MenuEntryOption option_;
Ref<MenuEntryOption> option_;
Box box_; Box box_;
bool hovered_ = false; bool hovered_ = false;
@ -653,7 +706,7 @@ Component MenuEntry(ConstStringRef label, Ref<MenuEntryOption> option) {
animation::Animator(&animation_foreground_, 0.F); animation::Animator(&animation_foreground_, 0.F);
}; };
return Make<Impl>(std::move(label), std::move(option)); return Make<Impl>(std::move(option));
} }
} // namespace ftxui } // namespace ftxui

View File

@ -23,9 +23,11 @@ TEST(MenuTest, RemoveEntries) {
int focused_entry = 0; int focused_entry = 0;
int selected = 0; int selected = 0;
std::vector<std::string> entries = {"1", "2", "3"}; std::vector<std::string> entries = {"1", "2", "3"};
MenuOption option; auto menu = Menu({
option.focused_entry = &focused_entry; .entries = &entries,
auto menu = Menu(&entries, &selected, option); .selected = &selected,
.focused_entry = &focused_entry,
});
EXPECT_EQ(selected, 0); EXPECT_EQ(selected, 0);
EXPECT_EQ(focused_entry, 0); EXPECT_EQ(focused_entry, 0);
@ -52,10 +54,8 @@ TEST(MenuTest, DirectionDown) {
int selected = 0; int selected = 0;
std::vector<std::string> entries = {"1", "2", "3"}; std::vector<std::string> entries = {"1", "2", "3"};
MenuOption option; MenuOption option;
auto menu = Menu(&entries, &selected, &option); auto menu = Menu(&entries, &selected, {.direction = Direction::Down});
selected = 0;
option.direction = Direction::Down;
Screen screen(4, 3); Screen screen(4, 3);
Render(screen, menu->Render()); Render(screen, menu->Render());
EXPECT_EQ(screen.ToString(), EXPECT_EQ(screen.ToString(),
@ -80,9 +80,7 @@ TEST(MenuTest, DirectionDown) {
TEST(MenuTest, DirectionsUp) { TEST(MenuTest, DirectionsUp) {
int selected = 0; int selected = 0;
std::vector<std::string> entries = {"1", "2", "3"}; std::vector<std::string> entries = {"1", "2", "3"};
MenuOption option; auto menu = Menu(&entries, &selected, {.direction = Direction::Up});
auto menu = Menu(&entries, &selected, &option);
option.direction = Direction::Up;
Screen screen(4, 3); Screen screen(4, 3);
Render(screen, menu->Render()); Render(screen, menu->Render());
EXPECT_EQ(screen.ToString(), EXPECT_EQ(screen.ToString(),
@ -106,9 +104,7 @@ TEST(MenuTest, DirectionsUp) {
TEST(MenuTest, DirectionsRight) { TEST(MenuTest, DirectionsRight) {
int selected = 0; int selected = 0;
std::vector<std::string> entries = {"1", "2", "3"}; std::vector<std::string> entries = {"1", "2", "3"};
MenuOption option; auto menu = Menu(&entries, &selected, {.direction = Direction::Right});
auto menu = Menu(&entries, &selected, &option);
option.direction = Direction::Right;
Screen screen(10, 1); Screen screen(10, 1);
Render(screen, menu->Render()); Render(screen, menu->Render());
EXPECT_EQ(screen.ToString(), EXPECT_EQ(screen.ToString(),
@ -132,9 +128,7 @@ TEST(MenuTest, DirectionsRight) {
TEST(MenuTest, DirectionsLeft) { TEST(MenuTest, DirectionsLeft) {
int selected = 0; int selected = 0;
std::vector<std::string> entries = {"1", "2", "3"}; std::vector<std::string> entries = {"1", "2", "3"};
MenuOption option; auto menu = Menu(&entries, &selected, {.direction = Direction::Left});
auto menu = Menu(&entries, &selected, &option);
option.direction = Direction::Left;
Screen screen(10, 1); Screen screen(10, 1);
Render(screen, menu->Render()); Render(screen, menu->Render());
EXPECT_EQ(screen.ToString(), EXPECT_EQ(screen.ToString(),
@ -158,8 +152,7 @@ TEST(MenuTest, DirectionsLeft) {
TEST(MenuTest, AnimationsHorizontal) { TEST(MenuTest, AnimationsHorizontal) {
int selected = 0; int selected = 0;
std::vector<std::string> entries = {"1", "2", "3"}; std::vector<std::string> entries = {"1", "2", "3"};
auto option = MenuOption::HorizontalAnimated(); auto menu = Menu(&entries, &selected, MenuOption::HorizontalAnimated());
auto menu = Menu(&entries, &selected, &option);
{ {
Screen screen(4, 3); Screen screen(4, 3);
Render(screen, menu->Render()); Render(screen, menu->Render());
@ -195,8 +188,7 @@ TEST(MenuTest, AnimationsHorizontal) {
TEST(MenuTest, AnimationsVertical) { TEST(MenuTest, AnimationsVertical) {
int selected = 0; int selected = 0;
std::vector<std::string> entries = {"1", "2", "3"}; std::vector<std::string> entries = {"1", "2", "3"};
auto option = MenuOption::VerticalAnimated(); auto menu = Menu(&entries, &selected, MenuOption::VerticalAnimated());
auto menu = Menu(&entries, &selected, &option);
{ {
Screen screen(10, 3); Screen screen(10, 3);
Render(screen, menu->Render()); Render(screen, menu->Render());

View File

@ -21,12 +21,9 @@ namespace {
/// @brief A list of selectable element. One and only one can be selected at /// @brief A list of selectable element. One and only one can be selected at
/// the same time. /// the same time.
/// @ingroup component /// @ingroup component
class RadioboxBase : public ComponentBase { class RadioboxBase : public ComponentBase, public RadioboxOption {
public: public:
RadioboxBase(ConstStringListRef entries, explicit RadioboxBase(RadioboxOption option) : RadioboxOption(option) {}
int* selected,
Ref<RadioboxOption> option)
: entries_(entries), selected_(selected), option_(std::move(option)) {}
private: private:
Element Render() override { Element Render() override {
@ -41,14 +38,13 @@ class RadioboxBase : public ComponentBase {
: is_menu_focused ? focus : is_menu_focused ? focus
: select; : select;
auto state = EntryState{ auto state = EntryState{
entries_[i], entries[i],
*selected_ == i, selected() == i,
is_selected, is_selected,
is_focused, is_focused,
}; };
auto element = auto element =
(option_->transform ? option_->transform (transform ? transform : RadioboxOption::Simple().transform)(state);
: RadioboxOption::Simple().transform)(state);
elements.push_back(element | focus_management | reflect(boxes_[i])); elements.push_back(element | focus_management | reflect(boxes_[i]));
} }
@ -97,14 +93,14 @@ class RadioboxBase : public ComponentBase {
if (hovered_ != old_hovered) { if (hovered_ != old_hovered) {
focused_entry() = hovered_; focused_entry() = hovered_;
option_->on_change(); on_change();
return true; return true;
} }
} }
if (event == Event::Character(' ') || event == Event::Return) { if (event == Event::Character(' ') || event == Event::Return) {
*selected_ = hovered_; selected() = hovered_;
option_->on_change(); on_change();
return true; return true;
} }
@ -126,9 +122,9 @@ class RadioboxBase : public ComponentBase {
focused_entry() = i; focused_entry() = i;
if (event.mouse().button == Mouse::Left && if (event.mouse().button == Mouse::Left &&
event.mouse().motion == Mouse::Released) { event.mouse().motion == Mouse::Released) {
if (*selected_ != i) { if (selected() != i) {
*selected_ = i; selected() = i;
option_->on_change(); on_change();
} }
return true; return true;
@ -154,7 +150,7 @@ class RadioboxBase : public ComponentBase {
hovered_ = util::clamp(hovered_, 0, size() - 1); hovered_ = util::clamp(hovered_, 0, size() - 1);
if (hovered_ != old_hovered) { if (hovered_ != old_hovered) {
option_->on_change(); on_change();
} }
return true; return true;
@ -162,25 +158,54 @@ class RadioboxBase : public ComponentBase {
void Clamp() { void Clamp() {
boxes_.resize(size()); boxes_.resize(size());
*selected_ = util::clamp(*selected_, 0, size() - 1); selected() = util::clamp(selected(), 0, size() - 1);
focused_entry() = util::clamp(focused_entry(), 0, size() - 1); focused_entry() = util::clamp(focused_entry(), 0, size() - 1);
hovered_ = util::clamp(hovered_, 0, size() - 1); hovered_ = util::clamp(hovered_, 0, size() - 1);
} }
bool Focusable() const final { return entries_.size(); } bool Focusable() const final { return entries.size(); }
int& focused_entry() { return option_->focused_entry(); } int size() const { return int(entries.size()); }
int size() const { return int(entries_.size()); }
ConstStringListRef entries_; int hovered_ = selected();
int* selected_;
int hovered_ = *selected_;
std::vector<Box> boxes_; std::vector<Box> boxes_;
Box box_; Box box_;
Ref<RadioboxOption> option_;
}; };
} // namespace } // namespace
/// @brief A list of element, where only one can be selected.
/// @param option The parameters
/// @ingroup component
/// @see RadioboxBase
///
/// ### Example
///
/// ```cpp
/// auto screen = ScreenInteractive::TerminalOutput();
/// std::vector<std::string> entries = {
/// "entry 1",
/// "entry 2",
/// "entry 3",
/// };
/// int selected = 0;
/// auto menu = Radiobox({
/// .entries = entries,
/// .selected = &selected,
/// });
/// screen.Loop(menu);
/// ```
///
/// ### Output
///
/// ```bash
/// ◉ entry 1
/// ○ entry 2
/// ○ entry 3
/// ```
Component Radiobox(RadioboxOption option) {
return Make<RadioboxBase>(std::move(option));
}
/// @brief A list of element, where only one can be selected. /// @brief A list of element, where only one can be selected.
/// @param entries The list of entries in the list. /// @param entries The list of entries in the list.
/// @param selected The index of the currently selected element. /// @param selected The index of the currently selected element.
@ -211,8 +236,10 @@ class RadioboxBase : public ComponentBase {
/// ``` /// ```
Component Radiobox(ConstStringListRef entries, Component Radiobox(ConstStringListRef entries,
int* selected, int* selected,
Ref<RadioboxOption> option) { RadioboxOption option) {
return Make<RadioboxBase>(entries, selected, std::move(option)); option.entries = entries;
option.selected = selected;
return Make<RadioboxBase>(std::move(option));
} }
} // namespace ftxui } // namespace ftxui

View File

@ -35,24 +35,24 @@ Decorator flexDirection(Direction direction) {
template <class T> template <class T>
class SliderBase : public ComponentBase { class SliderBase : public ComponentBase {
public: public:
explicit SliderBase(Ref<SliderOption<T>> options) explicit SliderBase(SliderOption<T> options)
: value_(options->value), : value_(options.value),
min_(options->min), min_(options.min),
max_(options->max), max_(options.max),
increment_(options->increment), increment_(options.increment),
options_(options) {} options_(options) {}
Element Render() override { Element Render() override {
auto gauge_color = Focused() ? color(options_->color_active) auto gauge_color = Focused() ? color(options_.color_active)
: color(options_->color_inactive); : color(options_.color_inactive);
const float percent = float(value_() - min_()) / float(max_() - min_()); const float percent = float(value_() - min_()) / float(max_() - min_());
return gaugeDirection(percent, options_->direction) | return gaugeDirection(percent, options_.direction) |
flexDirection(options_->direction) | reflect(gauge_box_) | flexDirection(options_.direction) | reflect(gauge_box_) |
gauge_color; gauge_color;
} }
void OnLeft() { void OnLeft() {
switch (options_->direction) { switch (options_.direction) {
case Direction::Right: case Direction::Right:
value_() -= increment_(); value_() -= increment_();
break; break;
@ -66,7 +66,7 @@ class SliderBase : public ComponentBase {
} }
void OnRight() { void OnRight() {
switch (options_->direction) { switch (options_.direction) {
case Direction::Right: case Direction::Right:
value_() += increment_(); value_() += increment_();
break; break;
@ -80,7 +80,7 @@ class SliderBase : public ComponentBase {
} }
void OnUp() { void OnUp() {
switch (options_->direction) { switch (options_.direction) {
case Direction::Up: case Direction::Up:
value_() -= increment_(); value_() -= increment_();
break; break;
@ -94,7 +94,7 @@ class SliderBase : public ComponentBase {
} }
void OnDown() { void OnDown() {
switch (options_->direction) { switch (options_.direction) {
case Direction::Down: case Direction::Down:
value_() -= increment_(); value_() -= increment_();
break; break;
@ -153,7 +153,7 @@ class SliderBase : public ComponentBase {
} }
if (captured_mouse_) { if (captured_mouse_) {
switch (options_->direction) { switch (options_.direction) {
case Direction::Right: { case Direction::Right: {
value_() = min_() + (event.mouse().x - gauge_box_.x_min) * value_() = min_() + (event.mouse().x - gauge_box_.x_min) *
(max_() - min_()) / (max_() - min_()) /
@ -192,15 +192,14 @@ class SliderBase : public ComponentBase {
ConstRef<T> min_; ConstRef<T> min_;
ConstRef<T> max_; ConstRef<T> max_;
ConstRef<T> increment_; ConstRef<T> increment_;
Ref<SliderOption<T>> options_; SliderOption<T> options_;
Box gauge_box_; Box gauge_box_;
CapturedMouse captured_mouse_; CapturedMouse captured_mouse_;
}; };
class SliderWithLabel : public ComponentBase { class SliderWithLabel : public ComponentBase {
public: public:
SliderWithLabel(ConstStringRef label, Component inner) SliderWithLabel(ConstStringRef label, Component inner) : label_(label) {
: label_(std::move(label)) {
Add(std::move(inner)); Add(std::move(inner));
SetActiveChild(ChildAt(0)); SetActiveChild(ChildAt(0));
} }

View File

@ -91,7 +91,7 @@ TEST(ToggleTest, OnChange) {
auto option = MenuOption::Toggle(); auto option = MenuOption::Toggle();
option.on_change = [&] { counter++; }; option.on_change = [&] { counter++; };
auto toggle = Menu(&entries, &selected, &option); auto toggle = Menu(&entries, &selected, option);
EXPECT_FALSE(toggle->OnEvent(Event::ArrowLeft)); // Reached far left. EXPECT_FALSE(toggle->OnEvent(Event::ArrowLeft)); // Reached far left.
EXPECT_EQ(counter, 0); EXPECT_EQ(counter, 0);
@ -120,7 +120,7 @@ TEST(ToggleTest, OnEnter) {
auto option = MenuOption::Toggle(); auto option = MenuOption::Toggle();
option.on_enter = [&] { counter++; }; option.on_enter = [&] { counter++; };
auto toggle = Menu(&entries, &selected, &option); auto toggle = Menu(&entries, &selected, option);
EXPECT_FALSE(toggle->OnEvent(Event::ArrowLeft)); // Reached far left. EXPECT_FALSE(toggle->OnEvent(Event::ArrowLeft)); // Reached far left.
EXPECT_TRUE(toggle->OnEvent(Event::Return)); EXPECT_TRUE(toggle->OnEvent(Event::Return));

View File

@ -845,9 +845,11 @@ class CanvasNodeBase : public Node {
} // namespace } // namespace
/// @brief Produce an element from a Canvas, or a reference to a Canvas. /// @brief Produce an element from a Canvas, or a reference to a Canvas.
// NOLINTNEXTLINE
Element canvas(ConstRef<Canvas> canvas) { Element canvas(ConstRef<Canvas> canvas) {
class Impl : public CanvasNodeBase { class Impl : public CanvasNodeBase {
public: public:
// NOLINTNEXTLINE
explicit Impl(ConstRef<Canvas> canvas) : canvas_(std::move(canvas)) { explicit Impl(ConstRef<Canvas> canvas) : canvas_(std::move(canvas)) {
requirement_.min_x = (canvas_->width() + 1) / 2; requirement_.min_x = (canvas_->width() + 1) / 2;
requirement_.min_y = (canvas_->height() + 3) / 4; requirement_.min_y = (canvas_->height() + 3) / 4;

View File

@ -13,10 +13,10 @@ namespace ftxui {
class Hyperlink : public NodeDecorator { class Hyperlink : public NodeDecorator {
public: public:
Hyperlink(Element child, std::string link) Hyperlink(Element child, std::string link)
: NodeDecorator(std::move(child)), link_(link) {} : NodeDecorator(std::move(child)), link_(std::move(link)) {}
void Render(Screen& screen) override { void Render(Screen& screen) override {
uint8_t hyperlink_id = screen.RegisterHyperlink(link_); const uint8_t hyperlink_id = screen.RegisterHyperlink(link_);
for (int y = box_.y_min; y <= box_.y_max; ++y) { for (int y = box_.y_min; y <= box_.y_max; ++y) {
for (int x = box_.x_min; x <= box_.x_max; ++x) { for (int x = box_.x_min; x <= box_.x_max; ++x) {
screen.PixelAt(x, y).hyperlink = hyperlink_id; screen.PixelAt(x, y).hyperlink = hyperlink_id;
@ -44,7 +44,7 @@ class Hyperlink : public NodeDecorator {
/// hyperlink("https://github.com/ArthurSonzogni/FTXUI", "link"); /// hyperlink("https://github.com/ArthurSonzogni/FTXUI", "link");
/// ``` /// ```
Element hyperlink(std::string link, Element child) { Element hyperlink(std::string link, Element child) {
return std::make_shared<Hyperlink>(std::move(child), link); return std::make_shared<Hyperlink>(std::move(child), std::move(link));
} }
/// @brief Decorate using an hyperlink. /// @brief Decorate using an hyperlink.
@ -61,6 +61,7 @@ Element hyperlink(std::string link, Element child) {
/// Element document = /// Element document =
/// text("red") | hyperlink("https://github.com/Arthursonzogni/FTXUI"); /// text("red") | hyperlink("https://github.com/Arthursonzogni/FTXUI");
/// ``` /// ```
// NOLINTNEXTLINE
Decorator hyperlink(std::string link) { Decorator hyperlink(std::string link) {
return [link](Element child) { return hyperlink(link, std::move(child)); }; return [link](Element child) { return hyperlink(link, std::move(child)); };
} }

View File

@ -1,5 +1,6 @@
#include <cstdint> // for size_t #include <cstdint> // for size_t
#include <iostream> // for operator<<, stringstream, basic_ostream, flush, cout, ostream #include <iostream> // for operator<<, stringstream, basic_ostream, flush, cout, ostream
#include <limits>
#include <map> // for _Rb_tree_const_iterator, map, operator!=, operator== #include <map> // for _Rb_tree_const_iterator, map, operator!=, operator==
#include <memory> // for allocator, allocator_traits<>::value_type #include <memory> // for allocator, allocator_traits<>::value_type
#include <sstream> // IWYU pragma: keep #include <sstream> // IWYU pragma: keep
@ -553,13 +554,13 @@ void Screen::ApplyShader() {
} }
// clang-format on // clang-format on
uint8_t Screen::RegisterHyperlink(std::string link) { uint8_t Screen::RegisterHyperlink(const std::string& link) {
for (size_t i = 0; i < hyperlinks_.size(); ++i) { for (size_t i = 0; i < hyperlinks_.size(); ++i) {
if (hyperlinks_[i] == link) { if (hyperlinks_[i] == link) {
return i; return i;
} }
} }
if (hyperlinks_.size() == 255) { if (hyperlinks_.size() == std::numeric_limits<uint8_t>::max()) {
return 0; return 0;
} }
hyperlinks_.push_back(link); hyperlinks_.push_back(link);

View File

@ -7,22 +7,17 @@
#include "ftxui/screen/string.hpp" #include "ftxui/screen/string.hpp"
#include <stddef.h> // for size_t #include <array> // for array
#include <array> // for array #include <cstddef> // for size_t
#include <cstdint> // for uint32_t, uint8_t, uint16_t, int32_t #include <cstdint> // for uint32_t, uint8_t, uint16_t, int32_t
#include <string> // for string, basic_string, wstring #include <string> // for string, basic_string, wstring
#include <tuple> // for _Swallow_assign, ignore #include <tuple> // 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 #include "ftxui/screen/string_internal.hpp" // for WordBreakProperty, EatCodePoint, CodepointToWordBreakProperty, GlyphCount, GlyphIterate, GlyphNext, GlyphPrevious, IsCombining, IsControl, IsFullWidth, Utf8ToWordBreakProperty
namespace { namespace {
using ftxui::EatCodePoint;
using ftxui::IsCombining;
using ftxui::IsControl;
using ftxui::IsFullWidth;
struct Interval { struct Interval {
uint32_t first; uint32_t first;
uint32_t last; uint32_t last;
@ -1565,8 +1560,9 @@ bool IsControl(uint32_t ucs) {
if (ucs == 0) { if (ucs == 0) {
return true; return true;
} }
if (ucs < 32) { // NOLINT if (ucs < 32) { // NOLINT
return ucs != 10; // 10 => Line feed. const uint32_t LINE_FEED = 10;
return ucs != LINE_FEED;
} }
if (ucs >= 0x7f && ucs < 0xa0) { // NOLINT if (ucs >= 0x7f && ucs < 0xa0) { // NOLINT
return true; return true;