diff --git a/CMakeLists.txt b/CMakeLists.txt index f478249..bdb87bc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,11 +56,11 @@ add_library(dom include/ftxui/dom/flexbox_config.hpp include/ftxui/dom/node.hpp include/ftxui/dom/requirement.hpp + include/ftxui/dom/selection.hpp include/ftxui/dom/take_any_args.hpp src/ftxui/dom/automerge.cpp src/ftxui/dom/blink.cpp src/ftxui/dom/bold.cpp - src/ftxui/dom/hyperlink.cpp src/ftxui/dom/border.cpp src/ftxui/dom/box_helper.cpp src/ftxui/dom/box_helper.hpp @@ -81,6 +81,7 @@ add_library(dom src/ftxui/dom/graph.cpp src/ftxui/dom/gridbox.cpp src/ftxui/dom/hbox.cpp + src/ftxui/dom/hyperlink.cpp src/ftxui/dom/inverted.cpp src/ftxui/dom/linear_gradient.cpp src/ftxui/dom/node.cpp @@ -89,6 +90,7 @@ add_library(dom src/ftxui/dom/reflect.cpp src/ftxui/dom/scroll_indicator.cpp src/ftxui/dom/selectable.cpp + src/ftxui/dom/selection.cpp src/ftxui/dom/separator.cpp src/ftxui/dom/size.cpp src/ftxui/dom/spinner.cpp diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index 29d01e3..b1eb7f7 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -138,7 +138,10 @@ class ScreenInteractive : public Screen { // Selection API: bool selection_enabled_ = false; CapturedMouse selection_pending_; - Box selection_box_; + int selection_start_x_ = 0; + int selection_start_y_ = 0; + int selection_end_x_ = 0; + int selection_end_y_ = 0; friend class Loop; diff --git a/include/ftxui/dom/node.hpp b/include/ftxui/dom/node.hpp index 775d9a9..0abc2bb 100644 --- a/include/ftxui/dom/node.hpp +++ b/include/ftxui/dom/node.hpp @@ -9,6 +9,7 @@ #include // for vector #include "ftxui/dom/requirement.hpp" // for Requirement +#include "ftxui/dom/selection.hpp" // for Selection #include "ftxui/screen/box.hpp" // for Box #include "ftxui/screen/screen.hpp" @@ -43,7 +44,7 @@ class Node { // Step 3: (optional) Selection // Propagated from Parents to Children. - virtual void Selection(Box selection, std::vector* selected); + virtual void Select(Selection& selection); // Step 4: Draw this element. virtual void Render(Screen& screen); @@ -66,7 +67,7 @@ class Node { void Render(Screen& screen, const Element& element); void Render(Screen& screen, Node* node); -void Render(Screen& screen, Node* node, Box selection); +void Render(Screen& screen, Node* node, Selection& selection); } // namespace ftxui diff --git a/include/ftxui/dom/selection.hpp b/include/ftxui/dom/selection.hpp new file mode 100644 index 0000000..d795e2b --- /dev/null +++ b/include/ftxui/dom/selection.hpp @@ -0,0 +1,33 @@ +// Copyright 2024 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. + +#ifndef FTXUI_DOM_SELECTION_HPP +#define FTXUI_DOM_SELECTION_HPP + +#include "ftxui/screen/box.hpp" // for Box + +namespace ftxui { + +/// @brief Represent a selection in the terminal. +class Selection { + public: + Selection(int start_x, int start_y, int end_x, int end_y); + const Box& GetBox() const; + + Selection SaturateHorizontal(Box box); + Selection SaturateVertical(Box box); + + + private: + Selection* const parent_ = nullptr; + const int start_x_; + const int start_y_; + const int end_x_; + const int end_y_; + const Box box_; +}; + +} // namespace ftxui + +#endif /* end of include guard: FTXUI_DOM_SELECTION_HPP */ diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index 8cfa7e0..6537959 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -837,28 +837,29 @@ bool ScreenInteractive::HandleSelection(Event event) { 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; + if (mouse.motion == Mouse::Pressed) { + selection_pending_ = CaptureMouse(); + selection_start_x_ = mouse.x; + selection_start_y_ = mouse.y; + selection_end_x_ = mouse.x; + selection_end_y_ = mouse.y; } - else if((mouse.motion == Mouse::Moved) && (selection_pending_)) { - selection_box_.x_max = mouse.x; - selection_box_.y_max = mouse.y; - return true; + + if (!selection_pending_) { + return false; } - else if((mouse.motion == Mouse::Released) && (selection_pending_)) { - selection_box_.x_max = mouse.x; - selection_box_.y_max = mouse.y; - selection_pending_ = nullptr; + + if (mouse.motion == Mouse::Moved) { + selection_end_x_ = mouse.x; + selection_end_y_ = mouse.y; return true; + } + + if (mouse.motion == Mouse::Released) { + selection_pending_ = nullptr; + selection_end_x_ = mouse.x; + selection_end_y_ = mouse.y; + return true; } return false; @@ -932,7 +933,9 @@ void ScreenInteractive::Draw(Component component) { #endif previous_frame_resized_ = resized; - Render(*this, document.get(), selection_box_); + Selection selection(selection_start_x_, selection_start_y_, // + selection_end_x_, selection_end_y_); + Render(*this, document.get(), selection); // 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 00bc508..bc08b0f 100644 --- a/src/ftxui/dom/hbox.cpp +++ b/src/ftxui/dom/hbox.cpp @@ -65,27 +65,16 @@ class HBox : public Node { } } - void Selection(Box selection, std::vector* selected) override { + void Select(Selection& selection) override { // If this Node box_ doesn't intersect with the selection, then no // selection. - if (Box::Intersection(selection, box_).IsEmpty()) { + if (Box::Intersection(selection.GetBox(), 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); - } - + Selection selection_saturated = selection.SaturateHorizontal(box_); for (auto& child : children_) { - child->Selection(selection, selected); + child->Select(selection_saturated); } } }; diff --git a/src/ftxui/dom/node.cpp b/src/ftxui/dom/node.cpp index 6e0a12f..c52da90 100644 --- a/src/ftxui/dom/node.cpp +++ b/src/ftxui/dom/node.cpp @@ -30,15 +30,15 @@ void Node::SetBox(Box box) { /// @brief Compute the selection of an element. /// @ingroup dom -void Node::Selection(Box selection, std::vector* selected) { +void Node::Select(Selection& selection) { // If this Node box_ doesn't intersect with the selection, then no selection. - if (Box::Intersection(selection, box_).IsEmpty()) { + if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) { return; } // By default we defer the selection to the children. for (auto& child : children_) { - child->Selection(selection, selected); + child->Select(selection); } } @@ -60,16 +60,18 @@ 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(), Box{0, 0, -1, -1}); + Selection selection(0, 0, -1, -1); + Render(screen, element.get(), selection); } /// @brief Display an element on a ftxui::Screen. /// @ingroup dom void Render(Screen& screen, Node* node) { - Render(screen, node, Box{0, 0, -1, -1}); + Selection selection(0, 0, -1, -1); + Render(screen, node, selection); } -void Render(Screen& screen, Node* node, Box selection) { +void Render(Screen& screen, Node* node, Selection& selection) { Box box; box.x_min = 0; box.y_min = 0; @@ -93,22 +95,7 @@ void Render(Screen& screen, Node* node, Box selection) { } // 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); + node->Select(selection); // Step 4: Draw the element. screen.stencil = box; diff --git a/src/ftxui/dom/selection.cpp b/src/ftxui/dom/selection.cpp new file mode 100644 index 0000000..f5f5966 --- /dev/null +++ b/src/ftxui/dom/selection.cpp @@ -0,0 +1,106 @@ +// Copyright 2024 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 "ftxui/dom/selection.hpp" // for Selection +#include // for max, min + +namespace ftxui { + +/// @brief Create a selection. +/// @param start_x The x coordinate of the start of the selection. +/// @param start_y The y coordinate of the start of the selection. +/// @param end_x The x coordinate of the end of the selection. +/// @param end_y The y coordinate of the end of the selection. +Selection::Selection(int start_x, int start_y, int end_x, int end_y) + : start_x_(start_x), + start_y_(start_y), + end_x_(end_x), + end_y_(end_y), + box_{ + std::min(start_x, end_x), + std::max(start_x, end_x), + std::min(start_y, end_y), + std::max(start_y, end_y), + } {} + +/// @brief Get the box of the selection. +/// @return The box of the selection. +const Box& Selection::GetBox() const { + return box_; +} + +/// @brief Saturate the selection to be inside the box. +/// This is called by `hbox` to propagate the selection to its children. +/// @param box The box to saturate the selection in. +/// @return The saturated selection. +Selection Selection::SaturateHorizontal(Box box) { + int start_x = start_x_; + int start_y = start_y_; + int end_x = end_x_; + int end_y = end_y_; + + const bool start_outside = !box.Contain(start_x, start_y); + const bool end_outside = !box.Contain(end_x, end_y); + const bool properly_ordered = + start_y < end_y || (start_y == end_y && start_x <= end_x); + if (properly_ordered) { + if (start_outside) { + start_x = box.x_min; + start_y = box.y_min; + } + if (end_outside) { + end_x = box.x_max; + end_y = box.y_max; + } + } else { + if (start_outside) { + start_x = box.x_max; + start_y = box.y_max; + } + if (end_outside) { + end_x = box.x_min; + end_y = box.y_min; + } + } + return Selection(start_x, start_y, end_x, end_y); +} + +/// @brief Saturate the selection to be inside the box. +/// This is called by `vbox` to propagate the selection to its children. +/// @param box The box to saturate the selection in. +/// @return The saturated selection. +Selection Selection::SaturateVertical(Box box) { + int start_x = start_x_; + int start_y = start_y_; + int end_x = end_x_; + int end_y = end_y_; + + const bool start_outside = !box.Contain(start_x, start_y); + const bool end_outside = !box.Contain(end_x, end_y); + const bool properly_ordered = + start_y < end_y || (start_y == end_y && start_x <= end_x); + + if (properly_ordered) { + if (start_outside) { + start_x = box.x_min; + start_y = box.y_min; + } + if (end_outside) { + end_x = box.x_max; + end_y = box.y_max; + } + } else { + if (start_outside) { + start_x = box.x_max; + start_y = box.y_max; + } + if (end_outside) { + end_x = box.x_min; + end_y = box.y_min; + } + } + return Selection(start_x, start_y, end_x, end_y); +} + +} // namespace ftxui diff --git a/src/ftxui/dom/text.cpp b/src/ftxui/dom/text.cpp index f5188d5..ff52e24 100644 --- a/src/ftxui/dom/text.cpp +++ b/src/ftxui/dom/text.cpp @@ -26,29 +26,19 @@ class Text : public Node { void ComputeRequirement() override { requirement_.min_x = string_width(text_); requirement_.min_y = 1; + has_selection = false; } - void Selection(Box selection, std::vector* selected) override { - if (Box::Intersection(selection, box_).IsEmpty()) { + void Select(Selection& selection) override { + if (Box::Intersection(selection.GetBox(), 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; + Selection selection_saturated = selection.SaturateHorizontal(box_); 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); + selection_start_ = selection_saturated.GetBox().x_min; + selection_end_ = selection_saturated.GetBox().x_max; } void Render(Screen& screen) override { diff --git a/src/ftxui/dom/vbox.cpp b/src/ftxui/dom/vbox.cpp index 761a8b3..2713456 100644 --- a/src/ftxui/dom/vbox.cpp +++ b/src/ftxui/dom/vbox.cpp @@ -65,27 +65,17 @@ class VBox : public Node { } } - void Selection(Box selection, std::vector* selected) override { + void Select(Selection& selection) override { // If this Node box_ doesn't intersect with the selection, then no // selection. - if (Box::Intersection(selection, box_).IsEmpty()) { + if (Box::Intersection(selection.GetBox(), 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; + Selection selection_saturated = selection.SaturateVertical(box_); - 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); + child->Select(selection_saturated); } } }; diff --git a/src/ftxui/screen/screen.cpp b/src/ftxui/screen/screen.cpp index be6f1d9..7bd64e2 100644 --- a/src/ftxui/screen/screen.cpp +++ b/src/ftxui/screen/screen.cpp @@ -389,8 +389,7 @@ 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