From fe7d67ac6ac79fc09337fbab8448015501a31d82 Mon Sep 17 00:00:00 2001 From: ArthurSonzogni Date: Mon, 23 Dec 2024 17:53:28 +0100 Subject: [PATCH] Fix + support for indpendant styles. --- examples/component/gallery.cpp | 23 ++++- examples/component/homescreen.cpp | 2 +- examples/component/selection.cpp | 89 +++++++++++++++++ .../ftxui/component/screen_interactive.hpp | 25 +++-- include/ftxui/dom/node.hpp | 4 +- include/ftxui/dom/selection.hpp | 27 ++++-- src/ftxui/component/screen_interactive.cpp | 85 +++++++++++----- src/ftxui/dom/node.cpp | 14 +-- src/ftxui/dom/paragraph.cpp | 48 +++++++--- src/ftxui/dom/selection.cpp | 54 ++++++++++- src/ftxui/dom/selection_style.cpp | 89 +++++++++++++++++ src/ftxui/dom/selection_test.cpp | 96 +++++++++++++++---- src/ftxui/dom/text.cpp | 43 ++++----- 13 files changed, 491 insertions(+), 108 deletions(-) create mode 100644 examples/component/selection.cpp create mode 100644 src/ftxui/dom/selection_style.cpp diff --git a/examples/component/gallery.cpp b/examples/component/gallery.cpp index 6add4f8b..9c51120b 100644 --- a/examples/component/gallery.cpp +++ b/examples/component/gallery.cpp @@ -97,7 +97,25 @@ int main() { }); sliders = Wrap("Slider", sliders); - // -- Layout ----------------------------------------------------------------- + // A large text: + auto lorel_ipsum = Renderer([] { + return vbox({ + text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. "), + text("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. "), + + }); + }); + lorel_ipsum = Wrap("Lorel Ipsum", lorel_ipsum); + + // -- Layout + // ----------------------------------------------------------------- auto layout = Container::Vertical({ menu, toggle, @@ -106,6 +124,7 @@ int main() { input, sliders, button, + lorel_ipsum, }); auto component = Renderer(layout, [&] { @@ -123,6 +142,8 @@ int main() { sliders->Render(), separator(), button->Render(), + separator(), + lorel_ipsum->Render(), }) | xflex | size(WIDTH, GREATER_THAN, 40) | border; }); diff --git a/examples/component/homescreen.cpp b/examples/component/homescreen.cpp index d650ba2e..7a5e710c 100644 --- a/examples/component/homescreen.cpp +++ b/examples/component/homescreen.cpp @@ -424,7 +424,7 @@ int main() { auto paragraph_renderer_left = Renderer([&] { std::string str = "Lorem Ipsum is simply dummy text of the printing and typesetting " - "industry. Lorem Ipsum has been the industry's standard dummy text " + "industry.\nLorem Ipsum has been the industry's standard dummy text " "ever since the 1500s, when an unknown printer took a galley of type " "and scrambled it to make a type specimen book."; return vbox({ diff --git a/examples/component/selection.cpp b/examples/component/selection.cpp new file mode 100644 index 00000000..89eaf7c9 --- /dev/null +++ b/examples/component/selection.cpp @@ -0,0 +1,89 @@ +// 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."), + }); +} + +int main() { + auto screen = ScreenInteractive::TerminalOutput(); + + auto quit = + Button("Quit", screen.ExitLoopClosure(), ButtonOption::Animated()); + + int selection_change_counter = 0; + std::string selection_content = ""; + screen.SelectionOnChange([&] { + selection_change_counter++; + selection_content = screen.SelectionAsString(); + }); + + // The components: + auto renderer = Renderer(quit, [&] { + return vbox({ + text("Select changed: " + std::to_string(selection_change_counter) + + " times"), + text("Currently selected: "), + paragraph(selection_content) | frame | border | xflex | + size(HEIGHT, EQUAL, 10), + window(text("Horizontal split"), hbox({ + LoremIpsum(), + separator(), + LoremIpsum(), + separator(), + LoremIpsum(), + })), + window(text("Vertical split"), vbox({ + LoremIpsum(), + separator(), + LoremIpsum(), + separator(), + LoremIpsum(), + })), + window(text("Grid split"), + vbox({ + hbox({ + LoremIpsum(), + separator(), + LoremIpsum() // + | selectionBackgroundColor(Color::Yellow) // + | selectionColor(Color::Black) // + | selectionStyleReset, + separator(), + LoremIpsum() | selectionColor(Color::Blue), + }), + separator(), + hbox({ + LoremIpsum() | selectionColor(Color::Red), + separator(), + LoremIpsum() | selectionStyle([](Pixel& pixel) { + pixel.underlined_double = true; + }), + separator(), + LoremIpsum(), + }), + })), + quit->Render(), + }); + }); + + screen.Loop(renderer); +} diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index 2e5bb3f1..a2b7b650 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -15,8 +15,8 @@ #include "ftxui/component/animation.hpp" // for TimePoint #include "ftxui/component/captured_mouse.hpp" // for CapturedMouse #include "ftxui/component/event.hpp" // for Event -#include "ftxui/dom/selection.hpp" // for SelectionOption #include "ftxui/component/task.hpp" // for Task, Closure +#include "ftxui/dom/selection.hpp" // for SelectionOption #include "ftxui/screen/screen.hpp" // for Screen namespace ftxui { @@ -70,7 +70,8 @@ class ScreenInteractive : public Screen { void ForceHandleCtrlZ(bool force); // Selection API. - std::string GetSelectedContent(Component component); + std::string SelectionAsString(); + void SelectionOnChange(std::function callback); private: void ExitNow(); @@ -86,7 +87,7 @@ class ScreenInteractive : public Screen { void RunOnceBlocking(Component component); void HandleTask(Component component, Task& task); - bool HandleSelection(Event event); + bool HandleSelection(bool handled, Event event); void RefreshSelection(); void Draw(Component component); void ResetCursorPosition(); @@ -137,11 +138,19 @@ class ScreenInteractive : public Screen { // Selection API: CapturedMouse selection_pending_; - int selection_start_x_ = 0; - int selection_start_y_ = 0; - int selection_end_x_ = 0; - int selection_end_y_ = 0; - bool selection_changed = false; + struct SelectionData { + int start_x = -1; + int start_y = -1; + int end_x = -2; + int end_y = -2; + bool empty = true; + bool operator==(const SelectionData& other) const; + bool operator!=(const SelectionData& other) const; + }; + SelectionData selection_data_; + SelectionData selection_data_previous_; + std::unique_ptr selection_; + std::function selection_on_change_; friend class Loop; diff --git a/include/ftxui/dom/node.hpp b/include/ftxui/dom/node.hpp index a59e942c..87edce77 100644 --- a/include/ftxui/dom/node.hpp +++ b/include/ftxui/dom/node.hpp @@ -68,7 +68,9 @@ class Node { void Render(Screen& screen, const Element& element); void Render(Screen& screen, Node* node); void Render(Screen& screen, Node* node, Selection& selection); -std::string GetNodeSelectedContent(Screen& screen, Node* node, Selection& selection); +std::string GetNodeSelectedContent(Screen& screen, + Node* node, + Selection& selection); } // namespace ftxui diff --git a/include/ftxui/dom/selection.hpp b/include/ftxui/dom/selection.hpp index 76cc7e4b..3ec0e482 100644 --- a/include/ftxui/dom/selection.hpp +++ b/include/ftxui/dom/selection.hpp @@ -7,6 +7,7 @@ #include +#include #include "ftxui/screen/box.hpp" // for Box #include "ftxui/screen/pixel.hpp" // for Pixel @@ -15,19 +16,33 @@ namespace ftxui { /// @brief Represent a selection in the terminal. class Selection { public: + Selection(); // Empty selection. 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); + bool IsEmpty() const { return empty_; } + + void AddPart(const std::string& part, int y, int left, int right); + std::string GetParts() { return parts_.str(); } private: - Selection* const parent_ = nullptr; - const int start_x_; - const int start_y_; - const int end_x_; - const int end_y_; - const Box box_; + Selection(int start_x, int start_y, int end_x, int end_y, Selection* parent); + + Selection* const parent_ = this; + const bool empty_ = true; + const int start_x_ = 0; + const int start_y_ = 0; + const int end_x_ = 0; + const int end_y_ = 0; + const Box box_ = {}; + std::stringstream parts_; + + // The position of the last inserted part. + int x_ = 0; + int y_ = 0; }; } // namespace ftxui diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index 49b158e1..642b67b0 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -577,11 +577,15 @@ void ScreenInteractive::ForceHandleCtrlZ(bool force) { } /// @brief Returns the content of the current selection -std::string ScreenInteractive::GetSelectedContent(Component component) { - Selection selection(selection_start_x_, selection_start_y_, // - selection_end_x_, selection_end_y_); +std::string ScreenInteractive::SelectionAsString() { + if (!selection_) { + return ""; + } + return selection_->GetParts(); +} - return GetNodeSelectedContent(*this, component->Render().get(), selection); +void ScreenInteractive::SelectionOnChange(std::function callback) { + selection_on_change_ = std::move(callback); } /// @brief Return the currently active screen, or null if none. @@ -759,6 +763,14 @@ void ScreenInteractive::RunOnce(Component component) { ExecuteSignalHandlers(); } Draw(std::move(component)); + + if (selection_data_previous_ != selection_data_) { + selection_data_previous_ = selection_data_; + if (selection_on_change_) { + selection_on_change_(); + Post(Event::Custom); + } + } } // private @@ -791,7 +803,7 @@ void ScreenInteractive::HandleTask(Component component, Task& task) { bool handled = component->OnEvent(arg); - handled = handled || HandleSelection(arg); + handled = HandleSelection(handled, arg); if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) { RecordSignal(SIGABRT); @@ -835,8 +847,13 @@ void ScreenInteractive::HandleTask(Component component, Task& task) { } // private -bool ScreenInteractive::HandleSelection(Event event) { - selection_changed = false; +bool ScreenInteractive::HandleSelection(bool handled, Event event) { + if (handled) { + selection_pending_ = nullptr; + selection_data_.empty = false; + selection_ = nullptr; + return true; + } if (!event.is_mouse()) { return false; @@ -849,12 +866,11 @@ bool ScreenInteractive::HandleSelection(Event event) { 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; - - selection_changed = true; + selection_data_.start_x = mouse.x; + selection_data_.start_y = mouse.y; + selection_data_.end_x = mouse.x; + selection_data_.end_y = mouse.y; + return false; } if (!selection_pending_) { @@ -862,11 +878,11 @@ bool ScreenInteractive::HandleSelection(Event event) { } if (mouse.motion == Mouse::Moved) { - if((mouse.x != selection_end_x_) || (mouse.y != selection_end_y_)) { - selection_end_x_ = mouse.x; - selection_end_y_ = mouse.y; - - selection_changed = true; + if ((mouse.x != selection_data_.end_x) || + (mouse.y != selection_data_.end_y)) { + selection_data_.end_x = mouse.x; + selection_data_.end_y = mouse.y; + selection_data_.empty = false; } return true; @@ -874,10 +890,9 @@ bool ScreenInteractive::HandleSelection(Event event) { if (mouse.motion == Mouse::Released) { selection_pending_ = nullptr; - selection_end_x_ = mouse.x; - selection_end_y_ = mouse.y; - - selection_changed = true; + selection_data_.end_x = mouse.x; + selection_data_.end_y = mouse.y; + selection_data_.empty = false; return true; } @@ -959,9 +974,12 @@ void ScreenInteractive::Draw(Component component) { #endif previous_frame_resized_ = resized; - Selection selection(selection_start_x_, selection_start_y_, // - selection_end_x_, selection_end_y_); - Render(*this, document.get(), selection); + selection_ = selection_data_.empty + ? std::make_unique() + : std::make_unique( + selection_data_.start_x, selection_data_.start_y, // + selection_data_.end_x, selection_data_.end_y); + Render(*this, document.get(), *selection_); // Set cursor position for user using tools to insert CJK characters. { @@ -1050,4 +1068,21 @@ void ScreenInteractive::Signal(int signal) { #endif } +bool ScreenInteractive::SelectionData::operator==( + const ScreenInteractive::SelectionData& other) const { + if (empty && other.empty) { + return true; + } + if (empty || other.empty) { + return false; + } + return start_x == other.start_x && start_y == other.start_y && + end_x == other.end_x && end_y == other.end_y; +} + +bool ScreenInteractive::SelectionData::operator!=( + const ScreenInteractive::SelectionData& other) const { + return !(*this == other); +} + } // namespace ftxui. diff --git a/src/ftxui/dom/node.cpp b/src/ftxui/dom/node.cpp index 0bf71d84..caaa2b38 100644 --- a/src/ftxui/dom/node.cpp +++ b/src/ftxui/dom/node.cpp @@ -58,7 +58,6 @@ void Node::Check(Status* status) { } std::string Node::GetSelectedContent(Selection& selection) { - std::string content; for (auto& child : children_) { @@ -71,14 +70,14 @@ std::string Node::GetSelectedContent(Selection& selection) { /// @brief Display an element on a ftxui::Screen. /// @ingroup dom void Render(Screen& screen, const Element& element) { - Selection selection(0, 0, -1, -1); + Selection selection; Render(screen, element.get(), selection); } /// @brief Display an element on a ftxui::Screen. /// @ingroup dom void Render(Screen& screen, Node* node) { - Selection selection(0, 0, -1, -1); + Selection selection; Render(screen, node, selection); } @@ -106,7 +105,9 @@ void Render(Screen& screen, Node* node, Selection& selection) { } // Step 3: Selection - node->Select(selection); + if (!selection.IsEmpty()) { + node->Select(selection); + } // Step 4: Draw the element. screen.stencil = box; @@ -116,8 +117,9 @@ void Render(Screen& screen, Node* node, Selection& selection) { screen.ApplyShader(); } -std::string GetNodeSelectedContent(Screen& screen, Node* node, Selection& selection) { - +std::string GetNodeSelectedContent(Screen& screen, + Node* node, + Selection& selection) { Box box; box.x_min = 0; box.y_min = 0; diff --git a/src/ftxui/dom/paragraph.cpp b/src/ftxui/dom/paragraph.cpp index 93f75c2f..482e7e3b 100644 --- a/src/ftxui/dom/paragraph.cpp +++ b/src/ftxui/dom/paragraph.cpp @@ -20,6 +20,18 @@ Elements Split(const std::string& the_text) { } return output; } + +Element Split(const std::string& paragraph, + std::function f) { + Elements output; + std::stringstream ss(paragraph); + std::string line; + while (std::getline(ss, line, '\n')) { + output.push_back(f(line)); + } + return vbox(std::move(output)); +} + } // namespace /// @brief Return an element drawing the paragraph on multiple lines. @@ -34,18 +46,22 @@ Element paragraph(const std::string& the_text) { /// @ingroup dom /// @see flexbox. Element paragraphAlignLeft(const std::string& the_text) { - static const auto config = FlexboxConfig().SetGap(1, 0); - return flexbox(Split(the_text), config); -} + return Split(the_text, [](const std::string& line) { + static const auto config = FlexboxConfig().SetGap(1, 0); + return flexbox(Split(line), config); + }); +}; /// @brief Return an element drawing the paragraph on multiple lines, aligned on /// the right. /// @ingroup dom /// @see flexbox. Element paragraphAlignRight(const std::string& the_text) { - static const auto config = - FlexboxConfig().SetGap(1, 0).Set(FlexboxConfig::JustifyContent::FlexEnd); - return flexbox(Split(the_text), config); + return Split(the_text, [](const std::string& line) { + static const auto config = FlexboxConfig().SetGap(1, 0).Set( + FlexboxConfig::JustifyContent::FlexEnd); + return flexbox(Split(line), config); + }); } /// @brief Return an element drawing the paragraph on multiple lines, aligned on @@ -53,9 +69,11 @@ Element paragraphAlignRight(const std::string& the_text) { /// @ingroup dom /// @see flexbox. Element paragraphAlignCenter(const std::string& the_text) { - static const auto config = - FlexboxConfig().SetGap(1, 0).Set(FlexboxConfig::JustifyContent::Center); - return flexbox(Split(the_text), config); + return Split(the_text, [](const std::string& line) { + static const auto config = + FlexboxConfig().SetGap(1, 0).Set(FlexboxConfig::JustifyContent::Center); + return flexbox(Split(line), config); + }); } /// @brief Return an element drawing the paragraph on multiple lines, aligned @@ -64,11 +82,13 @@ Element paragraphAlignCenter(const std::string& the_text) { /// @ingroup dom /// @see flexbox. Element paragraphAlignJustify(const std::string& the_text) { - static const auto config = FlexboxConfig().SetGap(1, 0).Set( - FlexboxConfig::JustifyContent::SpaceBetween); - Elements words = Split(the_text); - words.push_back(text("") | xflex); - return flexbox(std::move(words), config); + return Split(the_text, [](const std::string& line) { + static const auto config = FlexboxConfig().SetGap(1, 0).Set( + FlexboxConfig::JustifyContent::SpaceBetween); + Elements words = Split(line); + words.push_back(text("") | xflex); + return flexbox(std::move(words), config); + }); } } // namespace ftxui diff --git a/src/ftxui/dom/selection.cpp b/src/ftxui/dom/selection.cpp index 6c51e287..750724b6 100644 --- a/src/ftxui/dom/selection.cpp +++ b/src/ftxui/dom/selection.cpp @@ -21,6 +21,9 @@ class Unselectable : public NodeDecorator { }; } // namespace +/// @brief Create an empty selection. +Selection::Selection() : empty_(true) {} + /// @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. @@ -36,7 +39,26 @@ Selection::Selection(int start_x, int start_y, int end_x, int end_y) std::max(start_x, end_x), std::min(start_y, end_y), std::max(start_y, end_y), - } {} + }, + empty_(false) {} + +Selection::Selection(int start_x, + int start_y, + int end_x, + int end_y, + Selection* parent) + : 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), + }, + parent_(parent), + empty_(false) {} /// @brief Get the box of the selection. /// @return The box of the selection. @@ -77,7 +99,7 @@ Selection Selection::SaturateHorizontal(Box box) { end_y = box.y_min; } } - return Selection(start_x, start_y, end_x, end_y); + return Selection(start_x, start_y, end_x, end_y, parent_); } /// @brief Saturate the selection to be inside the box. @@ -114,7 +136,33 @@ Selection Selection::SaturateVertical(Box box) { end_y = box.y_min; } } - return Selection(start_x, start_y, end_x, end_y); + return Selection(start_x, start_y, end_x, end_y, parent_); +} + +void Selection::AddPart(const std::string& part, int y, int left, int right) { + if (parent_ != this) { + return parent_->AddPart(part, y, left, right); + } + [&] { + if (parts_.str().empty()) { + parts_ << "[" + part + "]"; + return; + } + + if (y_ != y) { + parts_ << '\n' << part; + return; + } + + if (x_ == left + 1) { + parts_ << "|" << part; + return; + } + + parts_ << "-" << part; + }(); + y_ = y; + x_ = right; } } // namespace ftxui diff --git a/src/ftxui/dom/selection_style.cpp b/src/ftxui/dom/selection_style.cpp new file mode 100644 index 00000000..c864a79d --- /dev/null +++ b/src/ftxui/dom/selection_style.cpp @@ -0,0 +1,89 @@ +// 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 // for make_shared +#include // for move + +#include "ftxui/dom/elements.hpp" // for Element, Decorator, bgcolor, color +#include "ftxui/dom/node_decorator.hpp" // for NodeDecorator +#include "ftxui/screen/box.hpp" // for Box +#include "ftxui/screen/color.hpp" // for Color +#include "ftxui/screen/screen.hpp" // for Pixel, Screen + +namespace ftxui { + +namespace { + +class SelectionStyleReset : public NodeDecorator { + public: + SelectionStyleReset(Element child) : NodeDecorator(std::move(child)) {} + + void Render(Screen& screen) final { + auto old_style = screen.GetSelectionStyle(); + screen.SetSelectionStyle([](Pixel& pixel) {}); + NodeDecorator::Render(screen); + screen.SetSelectionStyle(old_style); + } +}; + +class SelectionStyle : public NodeDecorator { + public: + SelectionStyle(Element child, std::function style) + : NodeDecorator(std::move(child)), style_(style) {} + + void Render(Screen& screen) final { + auto old_style = screen.GetSelectionStyle(); + auto new_style = [&, old_style](Pixel& pixel) { + old_style(pixel); + style_(pixel); + }; + screen.SetSelectionStyle(new_style); + NodeDecorator::Render(screen); + screen.SetSelectionStyle(old_style); + } + + std::function style_; +}; + +} // namespace + +/// @brief Reset the selection style of an element. +/// @param child The input element. +/// @return The output element with the selection style reset. +Element selectionStyleReset(Element child) { + return std::make_shared(std::move(child)); +} + +/// @brief Set the background color of an element when selected. +/// Note that the style is applied on top of the existing style. +Decorator selectionBackgroundColor(Color foreground) { + return selectionStyle([foreground](Pixel& pixel) { // + pixel.background_color = foreground; + }); +} + +/// @brief Set the foreground color of an element when selected. +/// Note that the style is applied on top of the existing style. +Decorator selectionForegroundColor(Color foreground) { + return selectionStyle([foreground](Pixel& pixel) { // + pixel.foreground_color = foreground; + }); +} + +/// @brief Set the color of an element when selected. +/// @param foreground The color to be applied. +/// Note that the style is applied on top of the existing style. +Decorator selectionColor(Color foreground) { + return selectionForegroundColor(foreground); +} + +/// @brief Set the style of an element when selected. +/// @param style The style to be applied. +/// Note that the style is applied on top of the existing style. +Decorator selectionStyle(std::function style) { + return [style](Element child) -> Element { + return std::make_shared(std::move(child), style); + }; +} + +} // namespace ftxui diff --git a/src/ftxui/dom/selection_test.cpp b/src/ftxui/dom/selection_test.cpp index 1ec4948b..8cf1beac 100644 --- a/src/ftxui/dom/selection_test.cpp +++ b/src/ftxui/dom/selection_test.cpp @@ -40,56 +40,118 @@ Event MouseReleased(int x, int y) { mouse.y = y; return Event::Mouse("jjj", mouse); } + +Event MouseMove(int x, int y) { + Mouse mouse; + mouse.button = Mouse::Left; + mouse.motion = Mouse::Moved; + mouse.shift = false; + mouse.meta = false; + mouse.control = false; + mouse.x = x; + mouse.y = y; + return Event::Mouse("jjj", mouse); +} + } // namespace TEST(SelectionTest, DefaultSelection) { auto component = Renderer([&] { return text("Lorem ipsum dolor"); }); - auto screen = ScreenInteractive::FixedSize(20, 1); - + EXPECT_EQ(screen.SelectionAsString(), ""); Loop loop(&screen, component); - - loop.RunOnce(); screen.PostEvent(MousePressed(3, 1)); - loop.RunOnce(); screen.PostEvent(MouseReleased(10, 1)); loop.RunOnce(); - EXPECT_STREQ(screen.GetSelectedContent(component).c_str(), "rem ipsu"); + EXPECT_EQ(screen.SelectionAsString(), "rem ipsu"); } -TEST(SelectionTest, CallbackSelection) { +TEST(SelectionTest, SelectionOnChange) { int selectionChangeCounter = 0; - auto component = Renderer([&] { return text("Lorem ipsum dolor"); }); - auto screen = ScreenInteractive::FixedSize(20, 1); + screen.SelectionOnChange([&] { selectionChangeCounter++; }); Loop loop(&screen, component); - loop.RunOnce(); + EXPECT_EQ(selectionChangeCounter, 0); + screen.PostEvent(MousePressed(3, 1)); loop.RunOnce(); - screen.PostEvent(MouseReleased(10, 1)); - loop.RunOnce(); + EXPECT_EQ(selectionChangeCounter, 0); + screen.PostEvent(MouseMove(5, 1)); + loop.RunOnce(); + EXPECT_EQ(selectionChangeCounter, 1); + + screen.PostEvent(MouseMove(7, 1)); + loop.RunOnce(); EXPECT_EQ(selectionChangeCounter, 2); - EXPECT_STREQ(screen.GetSelectedContent(component).c_str(), "rem ipsu"); + screen.PostEvent(MouseReleased(10, 1)); + loop.RunOnce(); + EXPECT_EQ(selectionChangeCounter, 3); + + screen.PostEvent(MouseMove(10, 1)); + loop.RunOnce(); + EXPECT_EQ(selectionChangeCounter, 3); + + EXPECT_EQ(screen.SelectionAsString(), "rem ipsu"); +} + +// Check that submitting multiple mouse events quickly doesn't trigger multiple +// selection change events. +TEST(SelectionTest, SelectionOnChangeSquashedEvents) { + int selectionChangeCounter = 0; + auto component = Renderer([&] { return text("Lorem ipsum dolor"); }); + auto screen = ScreenInteractive::FixedSize(20, 1); + screen.SelectionOnChange([&] { selectionChangeCounter++; }); + + Loop loop(&screen, component); + loop.RunOnce(); + EXPECT_EQ(selectionChangeCounter, 0); + + screen.PostEvent(MousePressed(3, 1)); + screen.PostEvent(MouseMove(5, 1)); + screen.PostEvent(MouseMove(7, 1)); + loop.RunOnce(); + EXPECT_EQ(selectionChangeCounter, 1); + + screen.PostEvent(MouseReleased(10, 1)); + screen.PostEvent(MouseMove(10, 1)); + loop.RunOnce(); + EXPECT_EQ(selectionChangeCounter, 2); + + EXPECT_EQ(screen.SelectionAsString(), "rem ipsu"); } TEST(SelectionTest, StyleSelection) { int selectionChangeCounter = 0; - auto component = Renderer([&] { return text("Lorem ipsum dolor"); }); + auto element = hbox({ + text("Lorem "), + text("ipsum") | selectionColor(Color::Red), + text(" dolor"), + }); auto screen = ScreenInteractive::FixedSize(20, 1); - Selection selection(2, 0, 9, 0); - Render(screen, component->Render().get(), selection); + Render(screen, element.get(), selection); + for (int i = 0; i < 20; i++) { + if (i >= 2 && i <= 9) { + EXPECT_EQ(screen.PixelAt(i, 0).inverted, true); + } else { + EXPECT_EQ(screen.PixelAt(i, 0).inverted, false); + } - EXPECT_EQ(screen.ToString(), "Lo\x1B[21mrem ipsu\x1B[24mm dolor "); + if (i >= 6 && i <= 9) { + EXPECT_EQ(screen.PixelAt(i, 0).foreground_color, Color::Red); + } else { + EXPECT_EQ(screen.PixelAt(i, 0).foreground_color, Color::Default); + } + } } TEST(SelectionTest, VBoxSelection) { diff --git a/src/ftxui/dom/text.cpp b/src/ftxui/dom/text.cpp index 2a16aaa5..c416c976 100644 --- a/src/ftxui/dom/text.cpp +++ b/src/ftxui/dom/text.cpp @@ -3,8 +3,9 @@ // the LICENSE file. #include // for min #include // for make_shared -#include // for string, wstring -#include // for move +#include +#include // for string, wstring +#include // for move #include "ftxui/dom/deprecated.hpp" // for text, vtext #include "ftxui/dom/elements.hpp" // for Element, text, vtext @@ -39,6 +40,19 @@ class Text : public Node { has_selection = true; selection_start_ = selection_saturated.GetBox().x_min; selection_end_ = selection_saturated.GetBox().x_max; + + std::stringstream ss; + int x = box_.x_min; + for (const auto& cell : Utf8ToGlyphs(text_)) { + if (cell == "\n") { + continue; + } + if (selection_start_ <= x && x <= selection_end_) { + ss << cell; + } + x++; + } + selection.AddPart(ss.str(), box_.y_min, selection_start_, selection_end_); } void Render(Screen& screen) override { @@ -69,34 +83,11 @@ class Text : public Node { } } - std::string GetSelectedContent(Selection& selection) { - int x = box_.x_min; - std::string selected_text = ""; - - if (has_selection == false) { - return ""; - } - - for (const auto& cell : Utf8ToGlyphs(text_)) { - if (x > box_.x_max) { - break; - } - - if ((x >= selection_start_) && (x <= selection_end_)) { - selected_text += cell; - } - - ++x; - } - - return selected_text; - } - private: std::string text_; bool has_selection = false; int selection_start_ = 0; - int selection_end_ = 10; + int selection_end_ = -1; std::function selectionTransform; };