Fix + support for indpendant styles.

This commit is contained in:
ArthurSonzogni 2024-12-23 17:53:28 +01:00
parent 9f9effa683
commit fe7d67ac6a
No known key found for this signature in database
GPG Key ID: 41D98248C074CD6C
13 changed files with 491 additions and 108 deletions

View File

@ -97,7 +97,25 @@ int main() {
}); });
sliders = Wrap("Slider", sliders); 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({ auto layout = Container::Vertical({
menu, menu,
toggle, toggle,
@ -106,6 +124,7 @@ int main() {
input, input,
sliders, sliders,
button, button,
lorel_ipsum,
}); });
auto component = Renderer(layout, [&] { auto component = Renderer(layout, [&] {
@ -123,6 +142,8 @@ int main() {
sliders->Render(), sliders->Render(),
separator(), separator(),
button->Render(), button->Render(),
separator(),
lorel_ipsum->Render(),
}) | }) |
xflex | size(WIDTH, GREATER_THAN, 40) | border; xflex | size(WIDTH, GREATER_THAN, 40) | border;
}); });

View File

@ -424,7 +424,7 @@ int main() {
auto paragraph_renderer_left = Renderer([&] { auto paragraph_renderer_left = Renderer([&] {
std::string str = std::string str =
"Lorem Ipsum is simply dummy text of the printing and typesetting " "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 " "ever since the 1500s, when an unknown printer took a galley of type "
"and scrambled it to make a type specimen book."; "and scrambled it to make a type specimen book.";
return vbox({ return vbox({

View File

@ -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 <string> // 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);
}

View File

@ -15,8 +15,8 @@
#include "ftxui/component/animation.hpp" // for TimePoint #include "ftxui/component/animation.hpp" // for TimePoint
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse #include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
#include "ftxui/component/event.hpp" // for Event #include "ftxui/component/event.hpp" // for Event
#include "ftxui/dom/selection.hpp" // for SelectionOption
#include "ftxui/component/task.hpp" // for Task, Closure #include "ftxui/component/task.hpp" // for Task, Closure
#include "ftxui/dom/selection.hpp" // for SelectionOption
#include "ftxui/screen/screen.hpp" // for Screen #include "ftxui/screen/screen.hpp" // for Screen
namespace ftxui { namespace ftxui {
@ -70,7 +70,8 @@ class ScreenInteractive : public Screen {
void ForceHandleCtrlZ(bool force); void ForceHandleCtrlZ(bool force);
// Selection API. // Selection API.
std::string GetSelectedContent(Component component); std::string SelectionAsString();
void SelectionOnChange(std::function<void()> callback);
private: private:
void ExitNow(); void ExitNow();
@ -86,7 +87,7 @@ class ScreenInteractive : public Screen {
void RunOnceBlocking(Component component); void RunOnceBlocking(Component component);
void HandleTask(Component component, Task& task); void HandleTask(Component component, Task& task);
bool HandleSelection(Event event); bool HandleSelection(bool handled, Event event);
void RefreshSelection(); void RefreshSelection();
void Draw(Component component); void Draw(Component component);
void ResetCursorPosition(); void ResetCursorPosition();
@ -137,11 +138,19 @@ class ScreenInteractive : public Screen {
// Selection API: // Selection API:
CapturedMouse selection_pending_; CapturedMouse selection_pending_;
int selection_start_x_ = 0; struct SelectionData {
int selection_start_y_ = 0; int start_x = -1;
int selection_end_x_ = 0; int start_y = -1;
int selection_end_y_ = 0; int end_x = -2;
bool selection_changed = false; 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> selection_;
std::function<void()> selection_on_change_;
friend class Loop; friend class Loop;

View File

@ -68,7 +68,9 @@ class Node {
void Render(Screen& screen, const Element& element); void Render(Screen& screen, const Element& element);
void Render(Screen& screen, Node* node); void Render(Screen& screen, Node* node);
void Render(Screen& screen, Node* node, Selection& selection); 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 } // namespace ftxui

View File

@ -7,6 +7,7 @@
#include <functional> #include <functional>
#include <sstream>
#include "ftxui/screen/box.hpp" // for Box #include "ftxui/screen/box.hpp" // for Box
#include "ftxui/screen/pixel.hpp" // for Pixel #include "ftxui/screen/pixel.hpp" // for Pixel
@ -15,19 +16,33 @@ namespace ftxui {
/// @brief Represent a selection in the terminal. /// @brief Represent a selection in the terminal.
class Selection { class Selection {
public: public:
Selection(); // Empty selection.
Selection(int start_x, int start_y, int end_x, int end_y); Selection(int start_x, int start_y, int end_x, int end_y);
const Box& GetBox() const; const Box& GetBox() const;
Selection SaturateHorizontal(Box box); Selection SaturateHorizontal(Box box);
Selection SaturateVertical(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: private:
Selection* const parent_ = nullptr; Selection(int start_x, int start_y, int end_x, int end_y, Selection* parent);
const int start_x_;
const int start_y_; Selection* const parent_ = this;
const int end_x_; const bool empty_ = true;
const int end_y_; const int start_x_ = 0;
const Box box_; 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 } // namespace ftxui

View File

@ -577,11 +577,15 @@ void ScreenInteractive::ForceHandleCtrlZ(bool force) {
} }
/// @brief Returns the content of the current selection /// @brief Returns the content of the current selection
std::string ScreenInteractive::GetSelectedContent(Component component) { std::string ScreenInteractive::SelectionAsString() {
Selection selection(selection_start_x_, selection_start_y_, // if (!selection_) {
selection_end_x_, selection_end_y_); return "";
}
return selection_->GetParts();
}
return GetNodeSelectedContent(*this, component->Render().get(), selection); void ScreenInteractive::SelectionOnChange(std::function<void()> callback) {
selection_on_change_ = std::move(callback);
} }
/// @brief Return the currently active screen, or null if none. /// @brief Return the currently active screen, or null if none.
@ -759,6 +763,14 @@ void ScreenInteractive::RunOnce(Component component) {
ExecuteSignalHandlers(); ExecuteSignalHandlers();
} }
Draw(std::move(component)); 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 // private
@ -791,7 +803,7 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
bool handled = component->OnEvent(arg); bool handled = component->OnEvent(arg);
handled = handled || HandleSelection(arg); handled = HandleSelection(handled, arg);
if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) { if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
RecordSignal(SIGABRT); RecordSignal(SIGABRT);
@ -835,8 +847,13 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
} }
// private // private
bool ScreenInteractive::HandleSelection(Event event) { bool ScreenInteractive::HandleSelection(bool handled, Event event) {
selection_changed = false; if (handled) {
selection_pending_ = nullptr;
selection_data_.empty = false;
selection_ = nullptr;
return true;
}
if (!event.is_mouse()) { if (!event.is_mouse()) {
return false; return false;
@ -849,12 +866,11 @@ bool ScreenInteractive::HandleSelection(Event event) {
if (mouse.motion == Mouse::Pressed) { if (mouse.motion == Mouse::Pressed) {
selection_pending_ = CaptureMouse(); selection_pending_ = CaptureMouse();
selection_start_x_ = mouse.x; selection_data_.start_x = mouse.x;
selection_start_y_ = mouse.y; selection_data_.start_y = mouse.y;
selection_end_x_ = mouse.x; selection_data_.end_x = mouse.x;
selection_end_y_ = mouse.y; selection_data_.end_y = mouse.y;
return false;
selection_changed = true;
} }
if (!selection_pending_) { if (!selection_pending_) {
@ -862,11 +878,11 @@ bool ScreenInteractive::HandleSelection(Event event) {
} }
if (mouse.motion == Mouse::Moved) { if (mouse.motion == Mouse::Moved) {
if((mouse.x != selection_end_x_) || (mouse.y != selection_end_y_)) { if ((mouse.x != selection_data_.end_x) ||
selection_end_x_ = mouse.x; (mouse.y != selection_data_.end_y)) {
selection_end_y_ = mouse.y; selection_data_.end_x = mouse.x;
selection_data_.end_y = mouse.y;
selection_changed = true; selection_data_.empty = false;
} }
return true; return true;
@ -874,10 +890,9 @@ bool ScreenInteractive::HandleSelection(Event event) {
if (mouse.motion == Mouse::Released) { if (mouse.motion == Mouse::Released) {
selection_pending_ = nullptr; selection_pending_ = nullptr;
selection_end_x_ = mouse.x; selection_data_.end_x = mouse.x;
selection_end_y_ = mouse.y; selection_data_.end_y = mouse.y;
selection_data_.empty = false;
selection_changed = true;
return true; return true;
} }
@ -959,9 +974,12 @@ void ScreenInteractive::Draw(Component component) {
#endif #endif
previous_frame_resized_ = resized; previous_frame_resized_ = resized;
Selection selection(selection_start_x_, selection_start_y_, // selection_ = selection_data_.empty
selection_end_x_, selection_end_y_); ? std::make_unique<Selection>()
Render(*this, document.get(), selection); : std::make_unique<Selection>(
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. // Set cursor position for user using tools to insert CJK characters.
{ {
@ -1050,4 +1068,21 @@ void ScreenInteractive::Signal(int signal) {
#endif #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. } // namespace ftxui.

View File

@ -58,7 +58,6 @@ void Node::Check(Status* status) {
} }
std::string Node::GetSelectedContent(Selection& selection) { std::string Node::GetSelectedContent(Selection& selection) {
std::string content; std::string content;
for (auto& child : children_) { for (auto& child : children_) {
@ -71,14 +70,14 @@ std::string Node::GetSelectedContent(Selection& selection) {
/// @brief Display an element on a ftxui::Screen. /// @brief Display an element on a ftxui::Screen.
/// @ingroup dom /// @ingroup dom
void Render(Screen& screen, const Element& element) { void Render(Screen& screen, const Element& element) {
Selection selection(0, 0, -1, -1); Selection selection;
Render(screen, element.get(), selection); Render(screen, element.get(), selection);
} }
/// @brief Display an element on a ftxui::Screen. /// @brief Display an element on a ftxui::Screen.
/// @ingroup dom /// @ingroup dom
void Render(Screen& screen, Node* node) { void Render(Screen& screen, Node* node) {
Selection selection(0, 0, -1, -1); Selection selection;
Render(screen, node, selection); Render(screen, node, selection);
} }
@ -106,7 +105,9 @@ void Render(Screen& screen, Node* node, Selection& selection) {
} }
// Step 3: Selection // Step 3: Selection
node->Select(selection); if (!selection.IsEmpty()) {
node->Select(selection);
}
// Step 4: Draw the element. // Step 4: Draw the element.
screen.stencil = box; screen.stencil = box;
@ -116,8 +117,9 @@ void Render(Screen& screen, Node* node, Selection& selection) {
screen.ApplyShader(); screen.ApplyShader();
} }
std::string GetNodeSelectedContent(Screen& screen, Node* node, Selection& selection) { std::string GetNodeSelectedContent(Screen& screen,
Node* node,
Selection& selection) {
Box box; Box box;
box.x_min = 0; box.x_min = 0;
box.y_min = 0; box.y_min = 0;

View File

@ -20,6 +20,18 @@ Elements Split(const std::string& the_text) {
} }
return output; return output;
} }
Element Split(const std::string& paragraph,
std::function<Element(std::string)> 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 } // namespace
/// @brief Return an element drawing the paragraph on multiple lines. /// @brief Return an element drawing the paragraph on multiple lines.
@ -34,18 +46,22 @@ Element paragraph(const std::string& the_text) {
/// @ingroup dom /// @ingroup dom
/// @see flexbox. /// @see flexbox.
Element paragraphAlignLeft(const std::string& the_text) { Element paragraphAlignLeft(const std::string& the_text) {
static const auto config = FlexboxConfig().SetGap(1, 0); return Split(the_text, [](const std::string& line) {
return flexbox(Split(the_text), config); 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 /// @brief Return an element drawing the paragraph on multiple lines, aligned on
/// the right. /// the right.
/// @ingroup dom /// @ingroup dom
/// @see flexbox. /// @see flexbox.
Element paragraphAlignRight(const std::string& the_text) { Element paragraphAlignRight(const std::string& the_text) {
static const auto config = return Split(the_text, [](const std::string& line) {
FlexboxConfig().SetGap(1, 0).Set(FlexboxConfig::JustifyContent::FlexEnd); static const auto config = FlexboxConfig().SetGap(1, 0).Set(
return flexbox(Split(the_text), config); FlexboxConfig::JustifyContent::FlexEnd);
return flexbox(Split(line), config);
});
} }
/// @brief Return an element drawing the paragraph on multiple lines, aligned on /// @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 /// @ingroup dom
/// @see flexbox. /// @see flexbox.
Element paragraphAlignCenter(const std::string& the_text) { Element paragraphAlignCenter(const std::string& the_text) {
static const auto config = return Split(the_text, [](const std::string& line) {
FlexboxConfig().SetGap(1, 0).Set(FlexboxConfig::JustifyContent::Center); static const auto config =
return flexbox(Split(the_text), 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 /// @brief Return an element drawing the paragraph on multiple lines, aligned
@ -64,11 +82,13 @@ Element paragraphAlignCenter(const std::string& the_text) {
/// @ingroup dom /// @ingroup dom
/// @see flexbox. /// @see flexbox.
Element paragraphAlignJustify(const std::string& the_text) { Element paragraphAlignJustify(const std::string& the_text) {
static const auto config = FlexboxConfig().SetGap(1, 0).Set( return Split(the_text, [](const std::string& line) {
FlexboxConfig::JustifyContent::SpaceBetween); static const auto config = FlexboxConfig().SetGap(1, 0).Set(
Elements words = Split(the_text); FlexboxConfig::JustifyContent::SpaceBetween);
words.push_back(text("") | xflex); Elements words = Split(line);
return flexbox(std::move(words), config); words.push_back(text("") | xflex);
return flexbox(std::move(words), config);
});
} }
} // namespace ftxui } // namespace ftxui

View File

@ -21,6 +21,9 @@ class Unselectable : public NodeDecorator {
}; };
} // namespace } // namespace
/// @brief Create an empty selection.
Selection::Selection() : empty_(true) {}
/// @brief Create a selection. /// @brief Create a selection.
/// @param start_x The x coordinate of the start of the 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 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::max(start_x, end_x),
std::min(start_y, end_y), std::min(start_y, end_y),
std::max(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. /// @brief Get the box of the selection.
/// @return 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; 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. /// @brief Saturate the selection to be inside the box.
@ -114,7 +136,33 @@ Selection Selection::SaturateVertical(Box box) {
end_y = box.y_min; 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 } // namespace ftxui

View File

@ -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 <memory> // for make_shared
#include <utility> // 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<void(Pixel&)> 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<void(Pixel&)> 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<SelectionStyleReset>(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<void(Pixel&)> style) {
return [style](Element child) -> Element {
return std::make_shared<SelectionStyle>(std::move(child), style);
};
}
} // namespace ftxui

View File

@ -40,56 +40,118 @@ Event MouseReleased(int x, int y) {
mouse.y = y; mouse.y = y;
return Event::Mouse("jjj", mouse); 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 } // namespace
TEST(SelectionTest, DefaultSelection) { TEST(SelectionTest, DefaultSelection) {
auto component = Renderer([&] { return text("Lorem ipsum dolor"); }); auto component = Renderer([&] { return text("Lorem ipsum dolor"); });
auto screen = ScreenInteractive::FixedSize(20, 1); auto screen = ScreenInteractive::FixedSize(20, 1);
EXPECT_EQ(screen.SelectionAsString(), "");
Loop loop(&screen, component); Loop loop(&screen, component);
loop.RunOnce();
screen.PostEvent(MousePressed(3, 1)); screen.PostEvent(MousePressed(3, 1));
loop.RunOnce();
screen.PostEvent(MouseReleased(10, 1)); screen.PostEvent(MouseReleased(10, 1));
loop.RunOnce(); 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; int selectionChangeCounter = 0;
auto component = Renderer([&] { return text("Lorem ipsum dolor"); }); auto component = Renderer([&] { return text("Lorem ipsum dolor"); });
auto screen = ScreenInteractive::FixedSize(20, 1); auto screen = ScreenInteractive::FixedSize(20, 1);
screen.SelectionOnChange([&] { selectionChangeCounter++; });
Loop loop(&screen, component); Loop loop(&screen, component);
loop.RunOnce(); loop.RunOnce();
EXPECT_EQ(selectionChangeCounter, 0);
screen.PostEvent(MousePressed(3, 1)); screen.PostEvent(MousePressed(3, 1));
loop.RunOnce(); loop.RunOnce();
screen.PostEvent(MouseReleased(10, 1)); EXPECT_EQ(selectionChangeCounter, 0);
loop.RunOnce();
screen.PostEvent(MouseMove(5, 1));
loop.RunOnce();
EXPECT_EQ(selectionChangeCounter, 1);
screen.PostEvent(MouseMove(7, 1));
loop.RunOnce();
EXPECT_EQ(selectionChangeCounter, 2); 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) { TEST(SelectionTest, StyleSelection) {
int selectionChangeCounter = 0; 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); auto screen = ScreenInteractive::FixedSize(20, 1);
Selection selection(2, 0, 9, 0); 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) { TEST(SelectionTest, VBoxSelection) {

View File

@ -3,8 +3,9 @@
// the LICENSE file. // the LICENSE file.
#include <algorithm> // for min #include <algorithm> // for min
#include <memory> // for make_shared #include <memory> // for make_shared
#include <string> // for string, wstring #include <sstream>
#include <utility> // for move #include <string> // for string, wstring
#include <utility> // for move
#include "ftxui/dom/deprecated.hpp" // for text, vtext #include "ftxui/dom/deprecated.hpp" // for text, vtext
#include "ftxui/dom/elements.hpp" // for Element, text, vtext #include "ftxui/dom/elements.hpp" // for Element, text, vtext
@ -39,6 +40,19 @@ class Text : public Node {
has_selection = true; has_selection = true;
selection_start_ = selection_saturated.GetBox().x_min; selection_start_ = selection_saturated.GetBox().x_min;
selection_end_ = selection_saturated.GetBox().x_max; 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 { 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: private:
std::string text_; std::string text_;
bool has_selection = false; bool has_selection = false;
int selection_start_ = 0; int selection_start_ = 0;
int selection_end_ = 10; int selection_end_ = -1;
std::function<void(Pixel& pixel)> selectionTransform; std::function<void(Pixel& pixel)> selectionTransform;
}; };