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..0546cd9 --- /dev/null +++ b/examples/component/selectable_input.cpp @@ -0,0 +1,73 @@ +// 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 + +using namespace ftxui; + +Element LoremIpsum() { + return vbox({ + text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " + "eiusmod tempor incididunt ut labore et dolore magna aliqua."), + text("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris " + "nisi ut aliquip ex ea commodo consequat."), + text("Duis aute irure dolor in reprehenderit in voluptate velit esse " + "cillum dolore eu fugiat nulla pariatur."), + text("Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " + "officia deserunt mollit anim id est laborum."), + }); +} + +int main() { + auto screen = ScreenInteractive::TerminalOutput(); + + auto quit = Button("Quit", screen.ExitLoopClosure()); + + // The components: + auto renderer = Renderer(quit, [&] { + return vbox({ + window(text("Horizontal split"), hbox({ + LoremIpsum(), + separator(), + LoremIpsum(), + separator(), + LoremIpsum(), + })), + window(text("Vertical split"), vbox({ + LoremIpsum(), + separator(), + LoremIpsum(), + separator(), + LoremIpsum(), + })), + window(text("Vertical split"), + vbox({ + window(text("horizontal split"), hbox({ + LoremIpsum(), + separator(), + LoremIpsum(), + separator(), + LoremIpsum(), + })), + separator(), + window(text("horizontal split"), hbox({ + LoremIpsum(), + separator(), + LoremIpsum(), + separator(), + LoremIpsum(), + })), + })), + quit->Render(), + }); + }); + + screen.Loop(renderer); +} diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index 6c79913..29d01e3 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -68,6 +68,10 @@ class ScreenInteractive : public Screen { void ForceHandleCtrlC(bool force); void ForceHandleCtrlZ(bool force); + // Selection API. + //void OnSelectionChange(std::function // for list #include // for shared_ptr #include // for vector @@ -40,7 +41,11 @@ class Node { // Propagated from Parents to Children. virtual void SetBox(Box box); - // Step 3: Draw this element. + // Step 3: (optional) Selection + // Propagated from Parents to Children. + virtual void Selection(Box selection, std::vector* selected); + + // Step 4: Draw this element. virtual void Render(Screen& screen); // Layout may not resolve within a single iteration for some elements. This @@ -52,6 +57,7 @@ class Node { }; virtual void Check(Status* status); + protected: Elements children_; Requirement requirement_; @@ -60,6 +66,7 @@ class Node { void Render(Screen& screen, const Element& element); void Render(Screen& screen, Node* node); +void Render(Screen& screen, Node* node, Box selection); } // namespace ftxui 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..39cc4ea 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 { diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index b5643ed..8cfa7e0 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,44 @@ 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_box_.x_min = mouse.x; + selection_box_.y_min = mouse.y; + selection_box_.x_max = mouse.x; + selection_box_.y_max = mouse.y; + return true; + } + else if((mouse.motion == Mouse::Moved) && (selection_pending_)) { + selection_box_.x_max = mouse.x; + selection_box_.y_max = mouse.y; + return true; + } + else if((mouse.motion == Mouse::Released) && (selection_pending_)) { + selection_box_.x_max = mouse.x; + selection_box_.y_max = mouse.y; + selection_pending_ = nullptr; + return true; + } + + return false; +} + // private // NOLINTNEXTLINE void ScreenInteractive::Draw(Component component) { @@ -892,7 +932,7 @@ void ScreenInteractive::Draw(Component component) { #endif previous_frame_resized_ = resized; - Render(*this, document); + Render(*this, document.get(), selection_box_); // Set cursor position for user using tools to insert CJK characters. { diff --git a/src/ftxui/dom/hbox.cpp b/src/ftxui/dom/hbox.cpp index 2053e84..00bc508 100644 --- a/src/ftxui/dom/hbox.cpp +++ b/src/ftxui/dom/hbox.cpp @@ -64,6 +64,30 @@ class HBox : public Node { x = box.x_max + 1; } } + + void Selection(Box selection, std::vector* selected) override { + // If this Node box_ doesn't intersect with the selection, then no + // selection. + if (Box::Intersection(selection, box_).IsEmpty()) { + return; + } + + const bool xmin_saturated = + selection.y_min < box_.y_min || selection.x_min < box_.x_min; + const bool xmax_saturated = + selection.y_max > box_.y_max || selection.x_max > box_.x_max; + + if (xmin_saturated) { + selection.x_min = std::min(box_.x_min, selection.x_min); + } + if (xmax_saturated) { + selection.x_max = std::max(box_.x_max, selection.x_max); + } + + for (auto& child : children_) { + child->Selection(selection, selected); + } + } }; } // namespace diff --git a/src/ftxui/dom/node.cpp b/src/ftxui/dom/node.cpp index 5220335..6e0a12f 100644 --- a/src/ftxui/dom/node.cpp +++ b/src/ftxui/dom/node.cpp @@ -1,3 +1,4 @@ +#include // 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. @@ -27,6 +28,20 @@ void Node::SetBox(Box box) { box_ = box; } +/// @brief Compute the selection of an element. +/// @ingroup dom +void Node::Selection(Box selection, std::vector* selected) { + // If this Node box_ doesn't intersect with the selection, then no selection. + if (Box::Intersection(selection, box_).IsEmpty()) { + return; + } + + // By default we defer the selection to the children. + for (auto& child : children_) { + child->Selection(selection, selected); + } +} + /// @brief Display an element on a ftxui::Screen. /// @ingroup dom void Node::Render(Screen& screen) { @@ -45,12 +60,16 @@ void Node::Check(Status* status) { /// @brief Display an element on a ftxui::Screen. /// @ingroup dom void Render(Screen& screen, const Element& element) { - Render(screen, element.get()); + Render(screen, element.get(), Box{0, 0, -1, -1}); } /// @brief Display an element on a ftxui::Screen. /// @ingroup dom void Render(Screen& screen, Node* node) { + Render(screen, node, Box{0, 0, -1, -1}); +} + +void Render(Screen& screen, Node* node, Box selection) { Box box; box.x_min = 0; box.y_min = 0; @@ -73,11 +92,29 @@ void Render(Screen& screen, Node* node) { node->Check(&status); } - // Step 3: Draw the element. + // Step 3: Selection + std::vector selected; + + Box selectionCleaned = selection; + if(selection.x_min > selection.x_max) + { + selectionCleaned.x_min = selection.x_max; + selectionCleaned.x_max = selection.x_min; + } + + if(selection.y_min > selection.y_max) + { + selectionCleaned.y_min = selection.y_max; + selectionCleaned.y_max = selection.y_min; + } + + node->Selection(selectionCleaned, &selected); + + // Step 4: Draw the element. screen.stencil = box; node->Render(screen); - // Step 4: Apply shaders + // Step 5: Apply shaders screen.ApplyShader(); } 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..f5188d5 100644 --- a/src/ftxui/dom/text.cpp +++ b/src/ftxui/dom/text.cpp @@ -28,26 +28,60 @@ class Text : public Node { requirement_.min_y = 1; } + void Selection(Box selection, std::vector* selected) override { + if (Box::Intersection(selection, box_).IsEmpty()) { + return; + } + + const bool xmin_saturated = + selection.y_min < box_.y_min || selection.x_min < box_.x_min; + const bool xmax_saturated = + selection.y_max > box_.y_max || selection.x_max > box_.x_max; + + selection_start_ = xmin_saturated ? box_.x_min : selection.x_min; + selection_end_ = xmax_saturated ? box_.x_max : selection.x_max; + + has_selection = true; + + Box out; + out.x_min = selection_start_; + out.x_max = selection_end_; + out.y_min = box_.y_min; + out.y_max = box_.y_max; + selected->push_back(out); + } + void Render(Screen& screen) override { int x = box_.x_min; const int y = box_.y_min; if (y > box_.y_max) { return; } + for (const auto& cell : Utf8ToGlyphs(text_)) { if (x > box_.x_max) { - return; + break; } if (cell == "\n") { continue; } screen.PixelAt(x, y).character = cell; + + if (has_selection) { + if((x >= selection_start_) && (x <= selection_end_)) { + screen.PixelAt(x, y).inverted = true; + } + } + ++x; } } private: std::string text_; + bool has_selection = false; + int selection_start_ = 0; + int selection_end_ = 10; }; class VText : public Node { diff --git a/src/ftxui/dom/vbox.cpp b/src/ftxui/dom/vbox.cpp index 28d885d..761a8b3 100644 --- a/src/ftxui/dom/vbox.cpp +++ b/src/ftxui/dom/vbox.cpp @@ -64,6 +64,30 @@ class VBox : public Node { y = box.y_max + 1; } } + + void Selection(Box selection, std::vector* selected) override { + // If this Node box_ doesn't intersect with the selection, then no + // selection. + if (Box::Intersection(selection, box_).IsEmpty()) { + return; + } + + const bool ymin_saturated = + selection.x_min < box_.x_min || selection.y_min < box_.y_min; + const bool ymax_saturated = + selection.x_max > box_.x_max || selection.y_max > box_.y_max; + + if (ymin_saturated) { + selection.y_min = std::min(box_.y_min, selection.y_min); + } + if (ymax_saturated) { + selection.y_max = std::max(box_.y_max, selection.y_max); + } + + for (auto& child : children_) { + child->Selection(selection, selected); + } + } }; } // namespace diff --git a/src/ftxui/screen/screen.cpp b/src/ftxui/screen/screen.cpp index 7bd64e2..be6f1d9 100644 --- a/src/ftxui/screen/screen.cpp +++ b/src/ftxui/screen/screen.cpp @@ -389,7 +389,8 @@ 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} { #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