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..6fd61c2 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -68,6 +68,8 @@ class ScreenInteractive : public Screen { void ForceHandleCtrlC(bool force); void ForceHandleCtrlZ(bool force); + std::string GetSelection(); + private: void ExitNow(); @@ -82,6 +84,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(); 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/box.hpp b/include/ftxui/screen/box.hpp index 3770803..4992bb5 100644 --- a/include/ftxui/screen/box.hpp +++ b/include/ftxui/screen/box.hpp @@ -11,10 +11,13 @@ struct Box { int x_max = 0; int y_min = 0; int y_max = 0; + bool isXInverted = false; // false means the box box from x_min to x_max (in the case of a selection for example) + bool isYInverted = false; // false means the box box from y_min to y_max (in the case of a selection for example) static auto Intersection(Box a, Box b) -> Box; static auto Union(Box a, Box b) -> Box; bool Contain(int x, int y) const; + Box Clean() const; bool IsEmpty() const; bool operator==(const Box& other) const; bool operator!=(const Box& other) const; 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/include/ftxui/screen/screen.hpp b/include/ftxui/screen/screen.hpp index 51b83a0..2f9bf19 100644 --- a/include/ftxui/screen/screen.hpp +++ b/include/ftxui/screen/screen.hpp @@ -10,6 +10,7 @@ #include "ftxui/screen/image.hpp" // for Pixel, Image #include "ftxui/screen/terminal.hpp" // for Dimensions +#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse namespace ftxui { @@ -62,6 +63,12 @@ class Screen : public Image { Cursor cursor() const { return cursor_; } void SetCursor(Cursor cursor) { cursor_ = cursor; } + bool selection_enabled = false; + CapturedMouse selection_pending; + Box mouse_selection_region; + Box selection_region; + std::string selection_text; + // 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. uint8_t RegisterHyperlink(const std::string& link); diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index b5643ed..ee2591d 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -781,7 +781,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 +826,67 @@ 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; + mouse_selection_region.x_min = mouse.x; + mouse_selection_region.y_min = mouse.y; + mouse_selection_region.x_max = mouse.x; + mouse_selection_region.y_max = mouse.y; + + selection_region = mouse_selection_region.Clean(); + return true; + } + + if (!selection_pending) { + return false; + } + + if (mouse.motion == Mouse::Moved) { + mouse_selection_region.x_max = mouse.x; + mouse_selection_region.y_max = mouse.y; + + selection_region = mouse_selection_region.Clean(); + return true; + } + + if (mouse.motion == Mouse::Released) { + mouse_selection_region.x_max = mouse.x; + mouse_selection_region.y_max = mouse.y; + selection_pending = nullptr; + + selection_region = mouse_selection_region.Clean(); + + if (mouse_selection_region.x_min == mouse_selection_region.x_max && + mouse_selection_region.y_min == mouse_selection_region.y_max) { + selection_enabled = false; + return true; + } + + return true; + } + + return false; +} + +std::string ScreenInteractive::GetSelection() { + return selection_text; +} + // private // NOLINTNEXTLINE void ScreenInteractive::Draw(Component component) { @@ -892,6 +955,9 @@ void ScreenInteractive::Draw(Component component) { #endif previous_frame_resized_ = resized; + // Clear selection text. + selection_text = ""; + Render(*this, document); // Set cursor position for user using tools to insert CJK characters. 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 diff --git a/src/ftxui/dom/text.cpp b/src/ftxui/dom/text.cpp index 228e714..e1b2c51 100644 --- a/src/ftxui/dom/text.cpp +++ b/src/ftxui/dom/text.cpp @@ -34,6 +34,17 @@ class Text : public Node { if (y > box_.y_max) { return; } + + // Get the selection start point + int selection_start_x = !screen.selection_region.isXInverted ? screen.selection_region.x_min : screen.selection_region.x_max; + int selection_start_y = !screen.selection_region.isYInverted ? screen.selection_region.y_min : screen.selection_region.y_max; + bool selectedWidget = false; + + if(box_.Contain(selection_start_x, selection_start_y)) + { + selectedWidget = true; + } + for (const auto& cell : Utf8ToGlyphs(text_)) { if (x > box_.x_max) { return; @@ -41,7 +52,32 @@ class Text : public Node { if (cell == "\n") { continue; } - screen.PixelAt(x, y).character = cell; + Pixel ¤tPixel = screen.PixelAt(x, y); + currentPixel.character = cell; + + if((selectedWidget == true) && (currentPixel.selectable == true)) + { + if(screen.selection_region.Contain(x, y)) { + currentPixel.inverted ^= true; + screen.selection_text += currentPixel.character; + } + else if((x >= screen.selection_region.x_min) && (x >= screen.selection_region.x_max) && + (y >= screen.selection_region.y_min) && (y < screen.selection_region.y_max)) + { + // Wrap around selection on the right + currentPixel.inverted ^= true; + screen.selection_text += currentPixel.character; + } + else if((x <= screen.selection_region.x_min) && (x <= screen.selection_region.x_max) && + (y > screen.selection_region.y_min) && (y <= screen.selection_region.y_max)) + { + // Wrap around selection on the left + currentPixel.inverted ^= true; + screen.selection_text += currentPixel.character; + } + + } + ++x; } } diff --git a/src/ftxui/screen/box.cpp b/src/ftxui/screen/box.cpp index e190305..e82884a 100644 --- a/src/ftxui/screen/box.cpp +++ b/src/ftxui/screen/box.cpp @@ -39,6 +39,26 @@ bool Box::Contain(int x, int y) const { y_max >= y; } +/// @return a copy of box with the x_min <= x_max and y_min <= y_max. +/// @ingroup screen +Box Box::Clean() const { + Box newBox = *this; + + if(newBox.x_min > newBox.x_max) + { + std::swap(newBox.x_min, newBox.x_max); + newBox.isXInverted = true; + } + + if(newBox.y_min > newBox.y_max) + { + std::swap(newBox.y_min, newBox.y_max); + newBox.isYInverted = true; + } + + return newBox; +} + /// @return whether the box is empty. /// @ingroup screen bool Box::IsEmpty() const { diff --git a/src/ftxui/screen/screen.cpp b/src/ftxui/screen/screen.cpp index 7bd64e2..b1c0945 100644 --- a/src/ftxui/screen/screen.cpp +++ b/src/ftxui/screen/screen.cpp @@ -389,7 +389,9 @@ Screen Screen::Create(Dimensions dimension) { return {dimension.dimx, dimension.dimy}; } -Screen::Screen(int dimx, int dimy) : Image{dimx, dimy} { +Screen::Screen(int dimx, int dimy) : + Image{dimx, dimy}, + selection_text("") { #if defined(_WIN32) // The placement of this call is a bit weird, however we can assume that // anybody who instantiates a Screen object eventually wants to output