From d38f3d229ae1c44298e1a3054d6ac3b25ba21726 Mon Sep 17 00:00:00 2001 From: ArthurSonzogni Date: Wed, 13 Nov 2024 22:05:04 +0100 Subject: [PATCH] Start the tree-aware selection. --- examples/component/selectable_input.cpp | 103 +++++++++--------- .../ftxui/component/screen_interactive.hpp | 9 +- include/ftxui/dom/node.hpp | 13 ++- include/ftxui/screen/box.hpp | 3 - include/ftxui/screen/screen.hpp | 6 - src/ftxui/component/screen_interactive.cpp | 54 +++------ src/ftxui/dom/hbox.cpp | 24 ++++ src/ftxui/dom/node.cpp | 29 ++++- src/ftxui/dom/text.cpp | 73 +++++++------ src/ftxui/dom/vbox.cpp | 23 ++++ src/ftxui/screen/box.cpp | 20 ---- src/ftxui/screen/screen.cpp | 3 +- 12 files changed, 195 insertions(+), 165 deletions(-) diff --git a/examples/component/selectable_input.cpp b/examples/component/selectable_input.cpp index 91dd577..0546cd9 100644 --- a/examples/component/selectable_input.cpp +++ b/examples/component/selectable_input.cpp @@ -10,62 +10,63 @@ #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() { - 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"); + auto quit = Button("Quit", screen.ExitLoopClosure()); - // 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, [&] { + // The components: + auto renderer = Renderer(quit, [&] { 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; + 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 6fd61c2..29d01e3 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -68,7 +68,9 @@ class ScreenInteractive : public Screen { void ForceHandleCtrlC(bool force); void ForceHandleCtrlZ(bool force); - std::string GetSelection(); + // 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,11 +57,6 @@ 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_; @@ -66,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/box.hpp b/include/ftxui/screen/box.hpp index 4992bb5..3770803 100644 --- a/include/ftxui/screen/box.hpp +++ b/include/ftxui/screen/box.hpp @@ -11,13 +11,10 @@ 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/screen.hpp b/include/ftxui/screen/screen.hpp index 2f9bf19..39cc4ea 100644 --- a/include/ftxui/screen/screen.hpp +++ b/include/ftxui/screen/screen.hpp @@ -63,12 +63,6 @@ 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 ee2591d..1f856f1 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -838,53 +838,36 @@ bool ScreenInteractive::HandleSelection(Event event) { } if (mouse.motion == Mouse::Pressed) { - selection_pending = CaptureMouse(); - if (!selection_pending) { + 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(); + 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; } - if (!selection_pending) { + 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(); + selection_box_.x_max = mouse.x; + selection_box_.y_max = mouse.y; 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; + if (mouse.motion != Mouse::Released) { + return false; } - return false; -} - -std::string ScreenInteractive::GetSelection() { - return selection_text; + selection_box_.x_max = mouse.x; + selection_box_.y_max = mouse.y; + selection_pending_ = nullptr; + return true; } // private @@ -955,10 +938,7 @@ void ScreenInteractive::Draw(Component component) { #endif previous_frame_resized_ = resized; - // Clear selection text. - selection_text = ""; - - 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..b323414 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_satured = + selection.y_min < box_.y_min || selection.x_min < box_.x_min; + const bool xmax_satured = + selection.y_max > box_.y_max || selection.x_max > box_.x_max; + + if (xmin_satured) { + selection.x_min = box_.x_min; + } + if (xmax_satured) { + selection.x_max = box_.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..23191ff 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,15 @@ void Render(Screen& screen, Node* node) { node->Check(&status); } - // Step 3: Draw the element. + // Step 3: Selection + std::vector selected; + node->Selection(selection, &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/text.cpp b/src/ftxui/dom/text.cpp index e1b2c51..791c658 100644 --- a/src/ftxui/dom/text.cpp +++ b/src/ftxui/dom/text.cpp @@ -28,6 +28,29 @@ 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_satured = + selection.y_min < box_.y_min || selection.x_min < box_.x_min; + const bool xmax_satured = + selection.y_max > box_.y_max || selection.x_max > box_.x_max; + + selection_start_ = xmin_satured ? box_.x_min : selection.x_min; + selection_end_ = xmax_satured ? 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; @@ -35,55 +58,33 @@ class Text : public Node { 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; + break; } if (cell == "\n") { continue; } - 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; - } - - } + screen.PixelAt(x, y).character = cell; ++x; } + + if (!has_selection) { + return; + } + + // Invert the selection + for(int x = selection_start_; x <= selection_end_; x++) { + screen.PixelAt(x, y).inverted = true; + } } 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..9a48f7c 100644 --- a/src/ftxui/dom/vbox.cpp +++ b/src/ftxui/dom/vbox.cpp @@ -64,6 +64,29 @@ 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_satured = + selection.x_min < box_.x_min || selection.y_min < box_.y_min; + const bool ymax_satured = + selection.x_max > box_.x_max || selection.y_max > box_.y_max; + if (ymin_satured) { + selection.y_min = box_.y_min; + } + if (ymax_satured) { + selection.y_max = box_.y_max; + } + + for (auto& child : children_) { + child->Selection(selection, selected); + } + } }; } // namespace diff --git a/src/ftxui/screen/box.cpp b/src/ftxui/screen/box.cpp index e82884a..e190305 100644 --- a/src/ftxui/screen/box.cpp +++ b/src/ftxui/screen/box.cpp @@ -39,26 +39,6 @@ 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 b1c0945..be6f1d9 100644 --- a/src/ftxui/screen/screen.cpp +++ b/src/ftxui/screen/screen.cpp @@ -390,8 +390,7 @@ Screen Screen::Create(Dimensions dimension) { } Screen::Screen(int dimx, int dimy) : - Image{dimx, dimy}, - selection_text("") { + 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