mirror of
https://github.com/ArthurSonzogni/FTXUI.git
synced 2025-05-05 22:01:13 +08:00
Feature: Selection
Add support for selection content in the dom.
This commit is contained in:
parent
751c8fab26
commit
6fafa2dfed
@ -16,6 +16,9 @@ current (development)
|
|||||||
- Feature: Add support for `Input`'s insert mode. Add `InputOption::insert`
|
- Feature: Add support for `Input`'s insert mode. Add `InputOption::insert`
|
||||||
option. Added by @mingsheng13.
|
option. Added by @mingsheng13.
|
||||||
- Feature: Add `DropdownOption` to configure the dropdown. See #826.
|
- Feature: Add `DropdownOption` to configure the dropdown. See #826.
|
||||||
|
- Feature: Add support for Selection. Thanks @clement-roblot. See #926.
|
||||||
|
- See `ScreenInteractive::GetSelection()`.
|
||||||
|
- See `ScreenInteractive::SelectionChange(...)` listener.
|
||||||
- Bugfix/Breaking change: `Mouse transition`:
|
- Bugfix/Breaking change: `Mouse transition`:
|
||||||
- Detect when the mouse move, as opposed to being pressed.
|
- Detect when the mouse move, as opposed to being pressed.
|
||||||
The Mouse::Moved motion was added.
|
The Mouse::Moved motion was added.
|
||||||
@ -49,6 +52,12 @@ current (development)
|
|||||||
- Feature: Add `extend_beyond_screen` option to `Dimension::Fit(..)`, allowing
|
- Feature: Add `extend_beyond_screen` option to `Dimension::Fit(..)`, allowing
|
||||||
the element to be larger than the screen. Proposed by @LordWhiro. See #572 and
|
the element to be larger than the screen. Proposed by @LordWhiro. See #572 and
|
||||||
#949.
|
#949.
|
||||||
|
- Feature: Add support for Selection. Thanks @clement-roblot. See #926.
|
||||||
|
- See `selectionColor` decorator.
|
||||||
|
- See `selectionBackgroundColor` decorator.
|
||||||
|
- See `selectionForegroundColor` decorator.
|
||||||
|
- See `selectionStyle(style)` decorator.
|
||||||
|
- See `selectionStyleReset` decorator.
|
||||||
|
|
||||||
### Screen
|
### Screen
|
||||||
- Feature: Add `Box::IsEmpty()`.
|
- Feature: Add `Box::IsEmpty()`.
|
||||||
|
@ -56,11 +56,12 @@ add_library(dom
|
|||||||
include/ftxui/dom/flexbox_config.hpp
|
include/ftxui/dom/flexbox_config.hpp
|
||||||
include/ftxui/dom/node.hpp
|
include/ftxui/dom/node.hpp
|
||||||
include/ftxui/dom/requirement.hpp
|
include/ftxui/dom/requirement.hpp
|
||||||
|
include/ftxui/dom/selection.hpp
|
||||||
include/ftxui/dom/take_any_args.hpp
|
include/ftxui/dom/take_any_args.hpp
|
||||||
src/ftxui/dom/automerge.cpp
|
src/ftxui/dom/automerge.cpp
|
||||||
|
src/ftxui/dom/selection_style.cpp
|
||||||
src/ftxui/dom/blink.cpp
|
src/ftxui/dom/blink.cpp
|
||||||
src/ftxui/dom/bold.cpp
|
src/ftxui/dom/bold.cpp
|
||||||
src/ftxui/dom/hyperlink.cpp
|
|
||||||
src/ftxui/dom/border.cpp
|
src/ftxui/dom/border.cpp
|
||||||
src/ftxui/dom/box_helper.cpp
|
src/ftxui/dom/box_helper.cpp
|
||||||
src/ftxui/dom/box_helper.hpp
|
src/ftxui/dom/box_helper.hpp
|
||||||
@ -81,6 +82,7 @@ add_library(dom
|
|||||||
src/ftxui/dom/graph.cpp
|
src/ftxui/dom/graph.cpp
|
||||||
src/ftxui/dom/gridbox.cpp
|
src/ftxui/dom/gridbox.cpp
|
||||||
src/ftxui/dom/hbox.cpp
|
src/ftxui/dom/hbox.cpp
|
||||||
|
src/ftxui/dom/hyperlink.cpp
|
||||||
src/ftxui/dom/inverted.cpp
|
src/ftxui/dom/inverted.cpp
|
||||||
src/ftxui/dom/linear_gradient.cpp
|
src/ftxui/dom/linear_gradient.cpp
|
||||||
src/ftxui/dom/node.cpp
|
src/ftxui/dom/node.cpp
|
||||||
@ -88,6 +90,7 @@ add_library(dom
|
|||||||
src/ftxui/dom/paragraph.cpp
|
src/ftxui/dom/paragraph.cpp
|
||||||
src/ftxui/dom/reflect.cpp
|
src/ftxui/dom/reflect.cpp
|
||||||
src/ftxui/dom/scroll_indicator.cpp
|
src/ftxui/dom/scroll_indicator.cpp
|
||||||
|
src/ftxui/dom/selection.cpp
|
||||||
src/ftxui/dom/separator.cpp
|
src/ftxui/dom/separator.cpp
|
||||||
src/ftxui/dom/size.cpp
|
src/ftxui/dom/size.cpp
|
||||||
src/ftxui/dom/spinner.cpp
|
src/ftxui/dom/spinner.cpp
|
||||||
|
@ -40,6 +40,7 @@ add_executable(ftxui-tests
|
|||||||
src/ftxui/dom/hyperlink_test.cpp
|
src/ftxui/dom/hyperlink_test.cpp
|
||||||
src/ftxui/dom/linear_gradient_test.cpp
|
src/ftxui/dom/linear_gradient_test.cpp
|
||||||
src/ftxui/dom/scroll_indicator_test.cpp
|
src/ftxui/dom/scroll_indicator_test.cpp
|
||||||
|
src/ftxui/dom/selection_test.cpp
|
||||||
src/ftxui/dom/separator_test.cpp
|
src/ftxui/dom/separator_test.cpp
|
||||||
src/ftxui/dom/spinner_test.cpp
|
src/ftxui/dom/spinner_test.cpp
|
||||||
src/ftxui/dom/table_test.cpp
|
src/ftxui/dom/table_test.cpp
|
||||||
|
@ -38,6 +38,7 @@ example(radiobox)
|
|||||||
example(radiobox_in_frame)
|
example(radiobox_in_frame)
|
||||||
example(renderer)
|
example(renderer)
|
||||||
example(resizable_split)
|
example(resizable_split)
|
||||||
|
example(selection)
|
||||||
example(scrollbar)
|
example(scrollbar)
|
||||||
example(slider)
|
example(slider)
|
||||||
example(slider_direction)
|
example(slider_direction)
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
@ -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({
|
||||||
|
87
examples/component/selection.cpp
Normal file
87
examples/component/selection.cpp
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
// 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("FTXUI: A powerful library for building user interfaces."),
|
||||||
|
text("Enjoy a rich set of components and a declarative style."),
|
||||||
|
text("Create beautiful and responsive UIs with minimal effort."),
|
||||||
|
text("Join the community and experience the power of FTXUI."),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
auto screen = ScreenInteractive::TerminalOutput();
|
||||||
|
|
||||||
|
auto quit =
|
||||||
|
Button("Quit", screen.ExitLoopClosure(), ButtonOption::Animated());
|
||||||
|
|
||||||
|
int selection_change_counter = 0;
|
||||||
|
std::string selection_content = "";
|
||||||
|
screen.SelectionChange([&] {
|
||||||
|
selection_change_counter++;
|
||||||
|
selection_content = screen.GetSelection();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The components:
|
||||||
|
auto renderer = Renderer(quit, [&] {
|
||||||
|
return vbox({
|
||||||
|
text("Select changed: " + std::to_string(selection_change_counter) +
|
||||||
|
" times"),
|
||||||
|
text("Currently selected: "),
|
||||||
|
paragraph(selection_content) | vscroll_indicator | frame | border |
|
||||||
|
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 with different style"),
|
||||||
|
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);
|
||||||
|
}
|
@ -16,6 +16,7 @@
|
|||||||
#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/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 {
|
||||||
@ -68,6 +69,10 @@ class ScreenInteractive : public Screen {
|
|||||||
void ForceHandleCtrlC(bool force);
|
void ForceHandleCtrlC(bool force);
|
||||||
void ForceHandleCtrlZ(bool force);
|
void ForceHandleCtrlZ(bool force);
|
||||||
|
|
||||||
|
// Selection API.
|
||||||
|
std::string GetSelection();
|
||||||
|
void SelectionChange(std::function<void()> callback);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void ExitNow();
|
void ExitNow();
|
||||||
|
|
||||||
@ -82,6 +87,8 @@ 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(bool handled, Event event);
|
||||||
|
void RefreshSelection();
|
||||||
void Draw(Component component);
|
void Draw(Component component);
|
||||||
void ResetCursorPosition();
|
void ResetCursorPosition();
|
||||||
|
|
||||||
@ -129,6 +136,22 @@ class ScreenInteractive : public Screen {
|
|||||||
// The style of the cursor to restore on exit.
|
// The style of the cursor to restore on exit.
|
||||||
int cursor_reset_shape_ = 1;
|
int cursor_reset_shape_ = 1;
|
||||||
|
|
||||||
|
// Selection API:
|
||||||
|
CapturedMouse selection_pending_;
|
||||||
|
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> selection_;
|
||||||
|
std::function<void()> selection_on_change_;
|
||||||
|
|
||||||
friend class Loop;
|
friend class Loop;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
@ -113,6 +113,11 @@ Decorator focusPositionRelative(float x, float y);
|
|||||||
Element automerge(Element child);
|
Element automerge(Element child);
|
||||||
Decorator hyperlink(std::string link);
|
Decorator hyperlink(std::string link);
|
||||||
Element hyperlink(std::string link, Element child);
|
Element hyperlink(std::string link, Element child);
|
||||||
|
Element selectionStyleReset(Element);
|
||||||
|
Decorator selectionColor(Color foreground);
|
||||||
|
Decorator selectionBackgroundColor(Color foreground);
|
||||||
|
Decorator selectionForegroundColor(Color foreground);
|
||||||
|
Decorator selectionStyle(std::function<void(Pixel&)> style);
|
||||||
|
|
||||||
// --- Layout is
|
// --- Layout is
|
||||||
// Horizontal, Vertical or stacked set of elements.
|
// Horizontal, Vertical or stacked set of elements.
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
#include <vector> // for vector
|
#include <vector> // for vector
|
||||||
|
|
||||||
#include "ftxui/dom/requirement.hpp" // for Requirement
|
#include "ftxui/dom/requirement.hpp" // for Requirement
|
||||||
|
#include "ftxui/dom/selection.hpp" // for Selection
|
||||||
#include "ftxui/screen/box.hpp" // for Box
|
#include "ftxui/screen/box.hpp" // for Box
|
||||||
#include "ftxui/screen/screen.hpp"
|
#include "ftxui/screen/screen.hpp"
|
||||||
|
|
||||||
@ -40,9 +41,15 @@ class Node {
|
|||||||
// Propagated from Parents to Children.
|
// Propagated from Parents to Children.
|
||||||
virtual void SetBox(Box box);
|
virtual void SetBox(Box box);
|
||||||
|
|
||||||
// Step 3: Draw this element.
|
// Step 3: (optional) Selection
|
||||||
|
// Propagated from Parents to Children.
|
||||||
|
virtual void Select(Selection& selection);
|
||||||
|
|
||||||
|
// Step 4: Draw this element.
|
||||||
virtual void Render(Screen& screen);
|
virtual void Render(Screen& screen);
|
||||||
|
|
||||||
|
virtual std::string GetSelectedContent(Selection& selection);
|
||||||
|
|
||||||
// Layout may not resolve within a single iteration for some elements. This
|
// Layout may not resolve within a single iteration for some elements. This
|
||||||
// allows them to request additionnal iterations. This signal must be
|
// allows them to request additionnal iterations. This signal must be
|
||||||
// forwarded to children at least once.
|
// forwarded to children at least once.
|
||||||
@ -60,6 +67,10 @@ 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);
|
||||||
|
std::string GetNodeSelectedContent(Screen& screen,
|
||||||
|
Node* node,
|
||||||
|
Selection& selection);
|
||||||
|
|
||||||
} // namespace ftxui
|
} // namespace ftxui
|
||||||
|
|
||||||
|
50
include/ftxui/dom/selection.hpp
Normal file
50
include/ftxui/dom/selection.hpp
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
// 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 <functional>
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include "ftxui/screen/box.hpp" // for Box
|
||||||
|
#include "ftxui/screen/pixel.hpp" // for Pixel
|
||||||
|
|
||||||
|
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(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
|
||||||
|
|
||||||
|
#endif /* end of include guard: FTXUI_DOM_SELECTION_HPP */
|
@ -4,12 +4,14 @@
|
|||||||
#ifndef FTXUI_SCREEN_SCREEN_HPP
|
#ifndef FTXUI_SCREEN_SCREEN_HPP
|
||||||
#define FTXUI_SCREEN_SCREEN_HPP
|
#define FTXUI_SCREEN_SCREEN_HPP
|
||||||
|
|
||||||
#include <cstdint> // for uint8_t
|
#include <cstdint> // for uint8_t
|
||||||
#include <string> // for string, basic_string, allocator
|
#include <functional> // for function
|
||||||
#include <vector> // for vector
|
#include <string> // for string, basic_string, allocator
|
||||||
|
#include <vector> // for vector
|
||||||
|
|
||||||
#include "ftxui/screen/image.hpp" // for Pixel, Image
|
#include "ftxui/screen/image.hpp" // for Pixel, Image
|
||||||
#include "ftxui/screen/terminal.hpp" // for Dimensions
|
#include "ftxui/screen/terminal.hpp" // for Dimensions
|
||||||
|
#include "ftxui/util/autoreset.hpp" // for AutoReset
|
||||||
|
|
||||||
namespace ftxui {
|
namespace ftxui {
|
||||||
|
|
||||||
@ -67,9 +69,18 @@ class Screen : public Image {
|
|||||||
uint8_t RegisterHyperlink(const std::string& link);
|
uint8_t RegisterHyperlink(const std::string& link);
|
||||||
const std::string& Hyperlink(uint8_t id) const;
|
const std::string& Hyperlink(uint8_t id) const;
|
||||||
|
|
||||||
|
using SelectionStyle = std::function<void(Pixel&)>;
|
||||||
|
const SelectionStyle& GetSelectionStyle() const;
|
||||||
|
void SetSelectionStyle(SelectionStyle decorator);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
Cursor cursor_;
|
Cursor cursor_;
|
||||||
std::vector<std::string> hyperlinks_ = {""};
|
std::vector<std::string> hyperlinks_ = {""};
|
||||||
|
|
||||||
|
// The current selection style. This is overridden by various dom elements.
|
||||||
|
SelectionStyle selection_style_ = [](Pixel& pixel) {
|
||||||
|
pixel.inverted ^= true;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ftxui
|
} // namespace ftxui
|
||||||
|
@ -163,6 +163,7 @@ class VerticalContainer : public ContainerBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int old_selected = *selector_;
|
||||||
if (event.mouse().button == Mouse::WheelUp) {
|
if (event.mouse().button == Mouse::WheelUp) {
|
||||||
MoveSelector(-1);
|
MoveSelector(-1);
|
||||||
}
|
}
|
||||||
@ -171,7 +172,7 @@ class VerticalContainer : public ContainerBase {
|
|||||||
}
|
}
|
||||||
*selector_ = std::max(0, std::min(int(children_.size()) - 1, *selector_));
|
*selector_ = std::max(0, std::min(int(children_.size()) - 1, *selector_));
|
||||||
|
|
||||||
return true;
|
return old_selected != *selector_;
|
||||||
}
|
}
|
||||||
|
|
||||||
Box box_;
|
Box box_;
|
||||||
|
@ -576,6 +576,18 @@ void ScreenInteractive::ForceHandleCtrlZ(bool force) {
|
|||||||
force_handle_ctrl_z_ = force;
|
force_handle_ctrl_z_ = force;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @brief Returns the content of the current selection
|
||||||
|
std::string ScreenInteractive::GetSelection() {
|
||||||
|
if (!selection_) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return selection_->GetParts();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScreenInteractive::SelectionChange(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.
|
||||||
// static
|
// static
|
||||||
ScreenInteractive* ScreenInteractive::Active() {
|
ScreenInteractive* ScreenInteractive::Active() {
|
||||||
@ -751,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
|
||||||
@ -781,7 +801,9 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
|
|||||||
|
|
||||||
arg.screen_ = this;
|
arg.screen_ = this;
|
||||||
|
|
||||||
const bool handled = component->OnEvent(arg);
|
bool handled = component->OnEvent(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);
|
||||||
@ -824,6 +846,59 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
|
|||||||
// clang-format on
|
// clang-format on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// private
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& mouse = event.mouse();
|
||||||
|
if (mouse.button != Mouse::Left) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse.motion == Mouse::Pressed) {
|
||||||
|
selection_pending_ = CaptureMouse();
|
||||||
|
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_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse.motion == Mouse::Moved) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mouse.motion == Mouse::Released) {
|
||||||
|
selection_pending_ = nullptr;
|
||||||
|
selection_data_.end_x = mouse.x;
|
||||||
|
selection_data_.end_y = mouse.y;
|
||||||
|
selection_data_.empty = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// private
|
// private
|
||||||
// NOLINTNEXTLINE
|
// NOLINTNEXTLINE
|
||||||
void ScreenInteractive::Draw(Component component) {
|
void ScreenInteractive::Draw(Component component) {
|
||||||
@ -899,7 +974,12 @@ void ScreenInteractive::Draw(Component component) {
|
|||||||
#endif
|
#endif
|
||||||
previous_frame_resized_ = resized;
|
previous_frame_resized_ = resized;
|
||||||
|
|
||||||
Render(*this, document);
|
selection_ = selection_data_.empty
|
||||||
|
? std::make_unique<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.
|
||||||
{
|
{
|
||||||
@ -988,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.
|
||||||
|
@ -80,6 +80,7 @@ class Flex : public Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void SetBox(Box box) override {
|
void SetBox(Box box) override {
|
||||||
|
Node::SetBox(box);
|
||||||
if (children_.empty()) {
|
if (children_.empty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,19 @@ class HBox : public Node {
|
|||||||
x = box.x_max + 1;
|
x = box.x_max + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Select(Selection& selection) override {
|
||||||
|
// If this Node box_ doesn't intersect with the selection, then no
|
||||||
|
// selection.
|
||||||
|
if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Selection selection_saturated = selection.SaturateHorizontal(box_);
|
||||||
|
for (auto& child : children_) {
|
||||||
|
child->Select(selection_saturated);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#include <iostream>
|
||||||
// Copyright 2020 Arthur Sonzogni. All rights reserved.
|
// Copyright 2020 Arthur Sonzogni. All rights reserved.
|
||||||
// Use of this source code is governed by the MIT license that can be found in
|
// Use of this source code is governed by the MIT license that can be found in
|
||||||
// the LICENSE file.
|
// the LICENSE file.
|
||||||
@ -27,6 +28,20 @@ void Node::SetBox(Box box) {
|
|||||||
box_ = box;
|
box_ = box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @brief Compute the selection of an element.
|
||||||
|
/// @ingroup dom
|
||||||
|
void Node::Select(Selection& selection) {
|
||||||
|
// If this Node box_ doesn't intersect with the selection, then no selection.
|
||||||
|
if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default we defer the selection to the children.
|
||||||
|
for (auto& child : children_) {
|
||||||
|
child->Select(selection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// @brief Display an element on a ftxui::Screen.
|
/// @brief Display an element on a ftxui::Screen.
|
||||||
/// @ingroup dom
|
/// @ingroup dom
|
||||||
void Node::Render(Screen& screen) {
|
void Node::Render(Screen& screen) {
|
||||||
@ -42,15 +57,31 @@ void Node::Check(Status* status) {
|
|||||||
status->need_iteration |= (status->iteration == 0);
|
status->need_iteration |= (status->iteration == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string Node::GetSelectedContent(Selection& selection) {
|
||||||
|
std::string content;
|
||||||
|
|
||||||
|
for (auto& child : children_) {
|
||||||
|
content += child->GetSelectedContent(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
/// @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) {
|
||||||
Render(screen, element.get());
|
Selection 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;
|
||||||
|
Render(screen, node, selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Render(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;
|
||||||
@ -73,12 +104,49 @@ void Render(Screen& screen, Node* node) {
|
|||||||
node->Check(&status);
|
node->Check(&status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Draw the element.
|
// Step 3: Selection
|
||||||
|
if (!selection.IsEmpty()) {
|
||||||
|
node->Select(selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Draw the element.
|
||||||
screen.stencil = box;
|
screen.stencil = box;
|
||||||
node->Render(screen);
|
node->Render(screen);
|
||||||
|
|
||||||
// Step 4: Apply shaders
|
// Step 5: Apply shaders
|
||||||
screen.ApplyShader();
|
screen.ApplyShader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string GetNodeSelectedContent(Screen& screen,
|
||||||
|
Node* node,
|
||||||
|
Selection& selection) {
|
||||||
|
Box box;
|
||||||
|
box.x_min = 0;
|
||||||
|
box.y_min = 0;
|
||||||
|
box.x_max = screen.dimx() - 1;
|
||||||
|
box.y_max = screen.dimy() - 1;
|
||||||
|
|
||||||
|
Node::Status status;
|
||||||
|
node->Check(&status);
|
||||||
|
const int max_iterations = 20;
|
||||||
|
while (status.need_iteration && status.iteration < max_iterations) {
|
||||||
|
// Step 1: Find what dimension this elements wants to be.
|
||||||
|
node->ComputeRequirement();
|
||||||
|
|
||||||
|
// Step 2: Assign a dimension to the element.
|
||||||
|
node->SetBox(box);
|
||||||
|
|
||||||
|
// Check if the element needs another iteration of the layout algorithm.
|
||||||
|
status.need_iteration = false;
|
||||||
|
status.iteration++;
|
||||||
|
node->Check(&status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: Selection
|
||||||
|
node->Select(selection);
|
||||||
|
|
||||||
|
// Step 4: get the selected content.
|
||||||
|
return node->GetSelectedContent(selection);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ftxui
|
} // namespace ftxui
|
||||||
|
@ -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
|
||||||
|
168
src/ftxui/dom/selection.cpp
Normal file
168
src/ftxui/dom/selection.cpp
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
// 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 <algorithm> // for max, min
|
||||||
|
|
||||||
|
#include "ftxui/dom/elements.hpp" // for Element, inverted
|
||||||
|
#include "ftxui/dom/node_decorator.hpp" // for NodeDecorator
|
||||||
|
|
||||||
|
namespace ftxui {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class Unselectable : public NodeDecorator {
|
||||||
|
public:
|
||||||
|
using NodeDecorator::NodeDecorator;
|
||||||
|
|
||||||
|
void Select(Selection& selection) override {
|
||||||
|
// Overwrite the select method to do nothing.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} // 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.
|
||||||
|
/// @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),
|
||||||
|
},
|
||||||
|
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.
|
||||||
|
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, parent_);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @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, 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
|
89
src/ftxui/dom/selection_style.cpp
Normal file
89
src/ftxui/dom/selection_style.cpp
Normal 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
|
224
src/ftxui/dom/selection_test.cpp
Normal file
224
src/ftxui/dom/selection_test.cpp
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
// Copyright 2022 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 <gtest/gtest.h>
|
||||||
|
#include <csignal> // for raise, SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM
|
||||||
|
|
||||||
|
#include "ftxui/component/component.hpp" // for Input, Renderer, Vertical
|
||||||
|
#include "ftxui/component/event.hpp" // for Event
|
||||||
|
#include "ftxui/component/loop.hpp" // for Loop
|
||||||
|
#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Left, Mouse::Pressed, Mouse::Released
|
||||||
|
#include "ftxui/component/screen_interactive.hpp"
|
||||||
|
#include "ftxui/dom/elements.hpp" // for text
|
||||||
|
#include "ftxui/dom/node.hpp" // for Render
|
||||||
|
#include "ftxui/screen/screen.hpp" // for Screen
|
||||||
|
|
||||||
|
// NOLINTBEGIN
|
||||||
|
namespace ftxui {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Event MousePressed(int x, int y) {
|
||||||
|
Mouse mouse;
|
||||||
|
mouse.button = Mouse::Left;
|
||||||
|
mouse.motion = Mouse::Pressed;
|
||||||
|
mouse.shift = false;
|
||||||
|
mouse.meta = false;
|
||||||
|
mouse.control = false;
|
||||||
|
mouse.x = x;
|
||||||
|
mouse.y = y;
|
||||||
|
return Event::Mouse("", mouse);
|
||||||
|
}
|
||||||
|
|
||||||
|
Event MouseReleased(int x, int y) {
|
||||||
|
Mouse mouse;
|
||||||
|
mouse.button = Mouse::Left;
|
||||||
|
mouse.motion = Mouse::Released;
|
||||||
|
mouse.shift = false;
|
||||||
|
mouse.meta = false;
|
||||||
|
mouse.control = false;
|
||||||
|
mouse.x = x;
|
||||||
|
mouse.y = y;
|
||||||
|
return Event::Mouse("", 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("", mouse);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(SelectionTest, DefaultSelection) {
|
||||||
|
auto component = Renderer([&] { return text("Lorem ipsum dolor"); });
|
||||||
|
auto screen = ScreenInteractive::FixedSize(20, 1);
|
||||||
|
EXPECT_EQ(screen.GetSelection(), "");
|
||||||
|
Loop loop(&screen, component);
|
||||||
|
screen.PostEvent(MousePressed(3, 1));
|
||||||
|
screen.PostEvent(MouseReleased(10, 1));
|
||||||
|
loop.RunOnce();
|
||||||
|
|
||||||
|
EXPECT_EQ(screen.GetSelection(), "rem ipsu");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelectionTest, SelectionChange) {
|
||||||
|
int selectionChangeCounter = 0;
|
||||||
|
auto component = Renderer([&] { return text("Lorem ipsum dolor"); });
|
||||||
|
auto screen = ScreenInteractive::FixedSize(20, 1);
|
||||||
|
screen.SelectionChange([&] { selectionChangeCounter++; });
|
||||||
|
|
||||||
|
Loop loop(&screen, component);
|
||||||
|
loop.RunOnce();
|
||||||
|
EXPECT_EQ(selectionChangeCounter, 0);
|
||||||
|
|
||||||
|
screen.PostEvent(MousePressed(3, 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);
|
||||||
|
|
||||||
|
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.GetSelection(), "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.SelectionChange([&] { 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.GetSelection(), "rem ipsu");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelectionTest, StyleSelection) {
|
||||||
|
int selectionChangeCounter = 0;
|
||||||
|
|
||||||
|
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, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
auto element = vbox({
|
||||||
|
text("Lorem ipsum dolor"),
|
||||||
|
text("Ut enim ad minim"),
|
||||||
|
});
|
||||||
|
|
||||||
|
auto screen = ScreenInteractive::FixedSize(20, 2);
|
||||||
|
|
||||||
|
Selection selection(2, 0, 2, 1);
|
||||||
|
Render(screen, element.get(), selection);
|
||||||
|
|
||||||
|
EXPECT_EQ(selection.GetParts(), "rem ipsum dolor\nUt ");
|
||||||
|
EXPECT_EQ(screen.ToString(),
|
||||||
|
"Lo\x1B[7mrem ipsum dolor\x1B[27m \r\n"
|
||||||
|
"\x1B[7mUt \x1B[27menim ad minim ");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelectionTest, VBoxSaturatedSelection) {
|
||||||
|
auto element = vbox({
|
||||||
|
text("Lorem ipsum dolor"),
|
||||||
|
text("Ut enim ad minim"),
|
||||||
|
text("Duis aute irure"),
|
||||||
|
});
|
||||||
|
|
||||||
|
auto screen = ScreenInteractive::FixedSize(20, 3);
|
||||||
|
Selection selection(2, 0, 2, 2);
|
||||||
|
Render(screen, element.get(), selection);
|
||||||
|
EXPECT_EQ(selection.GetParts(), "rem ipsum dolor\nUt enim ad minim\nDui");
|
||||||
|
|
||||||
|
EXPECT_EQ(screen.ToString(),
|
||||||
|
"Lo\x1B[7mrem ipsum dolor\x1B[27m \r\n"
|
||||||
|
"\x1B[7mUt enim ad minim\x1B[27m \r\n"
|
||||||
|
"\x1B[7mDui\x1B[27ms aute irure ");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelectionTest, HBoxSelection) {
|
||||||
|
auto element = hbox({
|
||||||
|
text("Lorem ipsum dolor"),
|
||||||
|
text("Ut enim ad minim"),
|
||||||
|
});
|
||||||
|
|
||||||
|
auto screen = ScreenInteractive::FixedSize(40, 1);
|
||||||
|
Selection selection(2, 0, 20, 0);
|
||||||
|
Render(screen, element.get(), selection);
|
||||||
|
EXPECT_EQ(selection.GetParts(), "rem ipsum dolorUt e");
|
||||||
|
EXPECT_EQ(screen.ToString(),
|
||||||
|
"Lo\x1B[7mrem ipsum dolorUt e\x1B[27mnim ad minim ");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SelectionTest, HBoxSaturatedSelection) {
|
||||||
|
auto element = hbox({
|
||||||
|
text("Lorem ipsum dolor"),
|
||||||
|
text("Ut enim ad minim"),
|
||||||
|
text("Duis aute irure"),
|
||||||
|
});
|
||||||
|
|
||||||
|
auto screen = ScreenInteractive::FixedSize(60, 1);
|
||||||
|
|
||||||
|
Selection selection(2, 0, 35, 0);
|
||||||
|
Render(screen, element.get(), selection);
|
||||||
|
EXPECT_EQ(selection.GetParts(), "rem ipsum dolorUt enim ad minimDui");
|
||||||
|
EXPECT_EQ(screen.ToString(),
|
||||||
|
"Lo\x1B[7mrem ipsum dolorUt enim ad minimDui\x1B[27ms aute irure "
|
||||||
|
" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ftxui
|
||||||
|
// NOLINTEND
|
@ -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
|
||||||
@ -26,28 +27,68 @@ class Text : public Node {
|
|||||||
void ComputeRequirement() override {
|
void ComputeRequirement() override {
|
||||||
requirement_.min_x = string_width(text_);
|
requirement_.min_x = string_width(text_);
|
||||||
requirement_.min_y = 1;
|
requirement_.min_y = 1;
|
||||||
|
has_selection = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Select(Selection& selection) override {
|
||||||
|
if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Selection selection_saturated = selection.SaturateHorizontal(box_);
|
||||||
|
|
||||||
|
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 {
|
void Render(Screen& screen) override {
|
||||||
int x = box_.x_min;
|
int x = box_.x_min;
|
||||||
const int y = box_.y_min;
|
const int y = box_.y_min;
|
||||||
|
|
||||||
if (y > box_.y_max) {
|
if (y > box_.y_max) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& cell : Utf8ToGlyphs(text_)) {
|
for (const auto& cell : Utf8ToGlyphs(text_)) {
|
||||||
if (x > box_.x_max) {
|
if (x > box_.x_max) {
|
||||||
return;
|
break;
|
||||||
}
|
}
|
||||||
if (cell == "\n") {
|
if (cell == "\n") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
screen.PixelAt(x, y).character = cell;
|
screen.PixelAt(x, y).character = cell;
|
||||||
|
|
||||||
|
if (has_selection) {
|
||||||
|
auto selectionTransform = screen.GetSelectionStyle();
|
||||||
|
if ((x >= selection_start_) && (x <= selection_end_)) {
|
||||||
|
selectionTransform(screen.PixelAt(x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
++x;
|
++x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string text_;
|
std::string text_;
|
||||||
|
bool has_selection = false;
|
||||||
|
int selection_start_ = 0;
|
||||||
|
int selection_end_ = -1;
|
||||||
|
std::function<void(Pixel& pixel)> selectionTransform;
|
||||||
};
|
};
|
||||||
|
|
||||||
class VText : public Node {
|
class VText : public Node {
|
||||||
|
@ -64,6 +64,20 @@ class VBox : public Node {
|
|||||||
y = box.y_max + 1;
|
y = box.y_max + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Select(Selection& selection) override {
|
||||||
|
// If this Node box_ doesn't intersect with the selection, then no
|
||||||
|
// selection.
|
||||||
|
if (Box::Intersection(selection.GetBox(), box_).IsEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Selection selection_saturated = selection.SaturateVertical(box_);
|
||||||
|
|
||||||
|
for (auto& child : children_) {
|
||||||
|
child->Select(selection_saturated);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
@ -544,4 +544,16 @@ const std::string& Screen::Hyperlink(std::uint8_t id) const {
|
|||||||
return hyperlinks_[id];
|
return hyperlinks_[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @brief Return the current selection style.
|
||||||
|
/// @see SetSelectionStyle
|
||||||
|
const Screen::SelectionStyle& Screen::GetSelectionStyle() const {
|
||||||
|
return selection_style_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @brief Set the current selection style.
|
||||||
|
/// @see GetSelectionStyle
|
||||||
|
void Screen::SetSelectionStyle(SelectionStyle decorator) {
|
||||||
|
selection_style_ = decorator;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace ftxui
|
} // namespace ftxui
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// Copyright 2023 Arthur Sonzogni. All rights reserved.
|
// Copyright 2024 Arthur Sonzogni. All rights reserved.
|
||||||
// Use of this source code is governed by the MIT license that can be found in
|
// Use of this source code is governed by the MIT license that can be found in
|
||||||
// the LICENSE file.
|
// the LICENSE file.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user