diff --git a/CMakeLists.txt b/CMakeLists.txt index bf4a9da..f478249 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -88,6 +88,7 @@ add_library(dom src/ftxui/dom/paragraph.cpp src/ftxui/dom/reflect.cpp src/ftxui/dom/scroll_indicator.cpp + src/ftxui/dom/selectable.cpp src/ftxui/dom/separator.cpp src/ftxui/dom/size.cpp src/ftxui/dom/spinner.cpp diff --git a/examples/component/CMakeLists.txt b/examples/component/CMakeLists.txt index 4339215..b55bdc6 100644 --- a/examples/component/CMakeLists.txt +++ b/examples/component/CMakeLists.txt @@ -38,6 +38,7 @@ example(radiobox) example(radiobox_in_frame) example(renderer) example(resizable_split) +example(selectable_input) example(scrollbar) example(slider) example(slider_direction) diff --git a/examples/component/selectable_input.cpp b/examples/component/selectable_input.cpp new file mode 100644 index 0000000..91dd577 --- /dev/null +++ b/examples/component/selectable_input.cpp @@ -0,0 +1,72 @@ +// Copyright 2020 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. +#include // for char_traits, operator+, string, basic_string + +#include "ftxui/component/component.hpp" // for Input, Renderer, Vertical +#include "ftxui/component/component_base.hpp" // for ComponentBase +#include "ftxui/component/component_options.hpp" // for InputOption +#include "ftxui/component/screen_interactive.hpp" // for Component, ScreenInteractive +#include "ftxui/dom/elements.hpp" // for text, hbox, separator, Element, operator|, vbox, border +#include "ftxui/util/ref.hpp" // for Ref + +int main() { + using namespace ftxui; + + // The data: + std::string first_name; + std::string last_name; + std::string password; + std::string phoneNumber; + // Region selection; + std::string textToCopy; + + auto screen = ScreenInteractive::TerminalOutput(); + + // The basic input components: + Component input_first_name = Input(&first_name, "first name"); + Component input_last_name = Input(&last_name, "last name"); + + // The password input component: + InputOption password_option; + password_option.password = true; + Component input_password = Input(&password, "password", password_option); + + // The phone number input component: + // We are using `CatchEvent` to filter out non-digit characters. + Component input_phone_number = Input(&phoneNumber, "phone number"); + input_phone_number |= CatchEvent([&](const Event& event) { + return event.is_character() && !std::isdigit(event.character()[0]); + }); + input_phone_number |= CatchEvent([&](const Event& event) { + return event.is_character() && phoneNumber.size() > 10; + }); + + // The component tree: + auto component = Container::Vertical({ + input_first_name, + input_last_name, + input_password, + input_phone_number, + }); + + // Tweak how the component tree is rendered: + auto renderer = Renderer(component, [&] { + return vbox({ + hbox(text(" First name : "), input_first_name->Render()), + hbox(text(" Last name : ") | selectable(), + input_last_name->Render()), + hbox(text(" Password : "), input_password->Render()), + hbox(text(" Phone num : "), input_phone_number->Render()) | + selectable(), + separator(), + text("Hello " + first_name + " " + last_name), + text("Your password is " + password), + text("Your phone number is " + phoneNumber), + text("Selected test is " + screen.GetSelection()), + }) | + border; + }); + + screen.Loop(renderer); +} diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index 6c79913..49f1837 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -26,6 +26,13 @@ struct Event; using Component = std::shared_ptr; class ScreenInteractivePrivate; +typedef struct { + uint16_t startx = 0; + uint16_t endx = 0; + uint16_t starty = 0; + uint16_t endy = 0; +} Region; + class ScreenInteractive : public Screen { public: // Constructors: @@ -68,6 +75,8 @@ class ScreenInteractive : public Screen { void ForceHandleCtrlC(bool force); void ForceHandleCtrlZ(bool force); + std::string GetSelection(); + private: void ExitNow(); @@ -82,6 +91,8 @@ class ScreenInteractive : public Screen { void RunOnceBlocking(Component component); void HandleTask(Component component, Task& task); + bool HandleSelection(Event event); + void RefreshSelection(); void Draw(Component component); void ResetCursorPosition(); @@ -126,6 +137,11 @@ class ScreenInteractive : public Screen { bool force_handle_ctrl_c_ = true; bool force_handle_ctrl_z_ = true; + bool selection_enabled = false; + CapturedMouse selection_pending; + Region selection_region; + std::string selection_text; + // The style of the cursor to restore on exit. int cursor_reset_shape_ = 1; diff --git a/include/ftxui/dom/elements.hpp b/include/ftxui/dom/elements.hpp index b17a711..325caa9 100644 --- a/include/ftxui/dom/elements.hpp +++ b/include/ftxui/dom/elements.hpp @@ -113,6 +113,8 @@ Decorator focusPositionRelative(float x, float y); Element automerge(Element child); Decorator hyperlink(std::string link); Element hyperlink(std::string link, Element child); +Element selectable(Element child); +Decorator selectable(void); // --- Layout is // Horizontal, Vertical or stacked set of elements. diff --git a/include/ftxui/dom/node.hpp b/include/ftxui/dom/node.hpp index f43157a..5a71512 100644 --- a/include/ftxui/dom/node.hpp +++ b/include/ftxui/dom/node.hpp @@ -52,6 +52,12 @@ class Node { }; virtual void Check(Status* status); + // Selection. + // Propagated from Parents to Children. + virtual void Select(Box selected_area) { + // TODO: Implement this. + } + protected: Elements children_; Requirement requirement_; diff --git a/include/ftxui/screen/pixel.hpp b/include/ftxui/screen/pixel.hpp index cbc7cc2..817778b 100644 --- a/include/ftxui/screen/pixel.hpp +++ b/include/ftxui/screen/pixel.hpp @@ -21,6 +21,7 @@ struct Pixel { underlined(false), underlined_double(false), strikethrough(false), + selectable(false), automerge(false) {} // A bit field representing the style: @@ -30,6 +31,7 @@ struct Pixel { bool inverted : 1; bool underlined : 1; bool underlined_double : 1; + bool selectable : 1; bool strikethrough : 1; bool automerge : 1; diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index b5643ed..0112f62 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -351,7 +351,8 @@ ScreenInteractive::ScreenInteractive(int dimx, bool use_alternative_screen) : Screen(dimx, dimy), dimension_(dimension), - use_alternative_screen_(use_alternative_screen) { + use_alternative_screen_(use_alternative_screen), + selection_text("") { task_receiver_ = MakeReceiver(); } @@ -781,7 +782,9 @@ void ScreenInteractive::HandleTask(Component component, Task& task) { arg.screen_ = this; - const bool handled = component->OnEvent(arg); + bool handled = component->OnEvent(arg); + + handled = handled || HandleSelection(arg); if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) { RecordSignal(SIGABRT); @@ -824,6 +827,80 @@ void ScreenInteractive::HandleTask(Component component, Task& task) { // clang-format on } +// private +bool ScreenInteractive::HandleSelection(Event event) { + if (!event.is_mouse()) { + return false; + } + + auto& mouse = event.mouse(); + if (mouse.button != Mouse::Left) { + return false; + } + + if (mouse.motion == Mouse::Pressed) { + selection_pending = CaptureMouse(); + if (!selection_pending) { + return false; + } + selection_enabled = true; + selection_region.startx = mouse.x; + selection_region.starty = mouse.y; + selection_region.endx = mouse.x; + selection_region.endy = mouse.y; + return true; + } + + if (!selection_pending) { + return false; + } + + if (mouse.motion == Mouse::Moved) { + selection_region.endx = mouse.x; + selection_region.endy = mouse.y; + return true; + } + + if (mouse.motion == Mouse::Released) { + selection_region.endx = mouse.x; + selection_region.endy = mouse.y; + selection_pending = nullptr; + + if (selection_region.startx == selection_region.endx && + selection_region.starty == selection_region.endy) { + selection_enabled = false; + return true; + } + + return true; + } + + return false; +} + +void ScreenInteractive::RefreshSelection() { + if (!selection_enabled) { + return; + } + selection_text = ""; + + for (int y = std::min(selection_region.starty, selection_region.endy); + y <= std::max(selection_region.starty, selection_region.endy); ++y) { + for (int x = std::min(selection_region.startx, selection_region.endx); + x <= std::max(selection_region.startx, selection_region.endx) - 1; + ++x) { + if (PixelAt(x, y).selectable == true) { + PixelAt(x, y).inverted ^= true; + selection_text += PixelAt(x, y).character; + } + } + } +} + +std::string ScreenInteractive::GetSelection() { + return selection_text; +} + // private // NOLINTNEXTLINE void ScreenInteractive::Draw(Component component) { @@ -894,6 +971,8 @@ void ScreenInteractive::Draw(Component component) { Render(*this, document); + RefreshSelection(); + // Set cursor position for user using tools to insert CJK characters. { const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx); diff --git a/src/ftxui/dom/selectable.cpp b/src/ftxui/dom/selectable.cpp new file mode 100644 index 0000000..191c5f4 --- /dev/null +++ b/src/ftxui/dom/selectable.cpp @@ -0,0 +1,39 @@ +#include "ftxui/dom/elements.hpp" // for Element, Decorator +#include "ftxui/dom/node_decorator.hpp" // for NodeDecorator +#include "ftxui/component/event.hpp" // for Event + + +namespace ftxui { +namespace { + +class Selectable : public NodeDecorator { + public: + explicit Selectable(Element child) + : NodeDecorator(std::move(child)) {} + + private: + void Render(Screen& screen) override { + + for (int y = box_.y_min; y <= box_.y_max; ++y) { + for (int x = box_.x_min; x <= box_.x_max; ++x) { + screen.PixelAt(x, y).selectable = true; + } + } + + NodeDecorator::Render(screen); + } +}; + +} // namespace + + +Element selectable(Element child) { + return std::make_shared(std::move(child)); +} + +Decorator selectable(void) { + return + [](Element child) { return selectable(std::move(child)); }; +} + +} // namespace ftxui