Feature: Windows. (#690)

Into ftxui/component/, add:
```
Container::Stacked(...)
Window(...);
```

Together, they can be used to display draggable/resizable windows.

Bug:https://github.com/ArthurSonzogni/FTXUI/issues/682

* Fix typo.
This commit is contained in:
Arthur Sonzogni 2023-07-15 16:29:48 +02:00 committed by GitHub
parent 79f8293a0d
commit e19550ae69
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 549 additions and 59 deletions

View File

@ -132,6 +132,7 @@ add_library(component
src/ftxui/component/terminal_input_parser.cpp
src/ftxui/component/terminal_input_parser.hpp
src/ftxui/component/util.cpp
src/ftxui/component/window.cpp
)
target_link_libraries(dom

View File

@ -43,4 +43,5 @@ example(tab_horizontal)
example(tab_vertical)
example(textarea)
example(toggle)
example(window)
example(with_restored_io)

View File

@ -0,0 +1,86 @@
#include <ftxui/component/component.hpp>
#include <ftxui/component/screen_interactive.hpp>
using namespace ftxui;
Component DummyWindowContent() {
class Impl : public ComponentBase {
private:
bool checked[3] = {false, false, false};
float slider = 50;
public:
Impl() {
Add(Container::Vertical({
Checkbox("Check me", &checked[0]),
Checkbox("Check me", &checked[1]),
Checkbox("Check me", &checked[2]),
Slider("Slider", &slider, 0.f, 100.f),
}));
}
};
return Make<Impl>();
}
int main() {
int window_1_left = 20;
int window_1_top = 10;
int window_1_width = 40;
int window_1_height = 20;
auto window_1 = Window({
.inner = DummyWindowContent(),
.title = "First window",
.left = &window_1_left,
.top = &window_1_top,
.width = &window_1_width,
.height = &window_1_height,
});
auto window_2 = Window({
.inner = DummyWindowContent(),
.title = "My window",
.left = 40,
.top = 20,
});
auto window_3 = Window({
.inner = DummyWindowContent(),
.title = "My window",
.left = 60,
.top = 30,
});
auto window_4 = Window({
.inner = DummyWindowContent(),
});
auto window_5 = Window({});
auto window_container = Container::Stacked({
window_1,
window_2,
window_3,
window_4,
window_5,
});
auto display_win_1 = Renderer([&] {
return text("window_1: " + //
std::to_string(window_1_width) + "x" +
std::to_string(window_1_height) + " + " +
std::to_string(window_1_left) + "," +
std::to_string(window_1_top));
});
auto layout = Container::Vertical({
display_win_1,
window_container,
});
auto screen = ScreenInteractive::Fullscreen();
screen.Loop(layout);
return EXIT_SUCCESS;
}

View File

@ -35,4 +35,3 @@ example(style_underlined_double)
example(table)
example(vbox_hbox)
example(vflow)
example(window)

View File

@ -1,34 +0,0 @@
#include <stdlib.h> // for EXIT_SUCCESS
#include <ftxui/dom/elements.hpp> // for operator|=, Element, bgcolor, color, graph, border
#include <ftxui/screen/screen.hpp> // for Fixed, Screen
#include <vector> // for vector
#include "ftxui/dom/node.hpp" // for Render
#include "ftxui/screen/color.hpp" // for Color, Color::DarkBlue, Color::Red, ftxui
int main() {
using namespace ftxui;
Element document = graph([](int x, int y) {
std::vector<int> result(x, 0);
for (int i{0}; i < x; ++i) {
result[i] = ((3 * i) / 2) % y;
}
return result;
});
document |= color(Color::Red);
document |= bgcolor(Color::DarkBlue);
document |= border;
const int width = 80;
const int height = 10;
auto screen =
Screen::Create(Dimension::Fixed(width), Dimension::Fixed(height));
Render(screen, document);
screen.Print();
return EXIT_SUCCESS;
}
// 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.

View File

@ -40,6 +40,7 @@ Component Vertical(Components children, int* selector);
Component Horizontal(Components children);
Component Horizontal(Components children, int* selector);
Component Tab(Components children, int* selector);
Component Stacked(Components children);
} // namespace Container
Component Button(ButtonOption options);
@ -131,6 +132,8 @@ ComponentDecorator Hoverable(std::function<void()> on_enter,
std::function<void()> on_leave);
ComponentDecorator Hoverable(std::function<void(bool)> on_change);
Component Window(WindowOptions option);
} // namespace ftxui
#endif /* end of include guard: FTXUI_COMPONENT_HPP */

View File

@ -226,6 +226,39 @@ struct SliderOption {
Color color_inactive = Color::GrayDark;
};
// Parameter pack used by `WindowOptions::render`.
struct WindowRenderState {
Element inner; /// < The element wrapped inside this window.
const std::string& title; /// < The title of the window.
bool active = false; /// < Whether the window is the active one.
bool drag = false; /// < Whether the window is being dragged.
bool resize = false; /// < Whether the window is being resized.
bool hover_left = false; /// < Whether the resizeable left side is hovered.
bool hover_right = false; /// < Whether the resizeable right side is hovered.
bool hover_top = false; /// < Whether the resizeable top side is hovered.
bool hover_down = false; /// < Whether the resizeable down side is hovered.
};
// @brief Option for the `Window` component.
// @ingroup component
struct WindowOptions {
Component inner; /// < The component wrapped by this window.
ConstStringRef title = ""; /// < The title displayed by this window.
Ref<int> left = 0; /// < The left side position of the window.
Ref<int> top = 0; /// < The top side position of the window.
Ref<int> width = 20; /// < The width of the window.
Ref<int> height = 10; /// < The height of the window.
Ref<bool> resize_left = true; /// < Can the left side be resized?
Ref<bool> resize_right = true; /// < Can the right side be resized?
Ref<bool> resize_top = true; /// < Can the top side be resized?
Ref<bool> resize_down = true; /// < Can the down side be resized?
/// An optional function to customize how the window looks like:
std::function<Element(const WindowRenderState&)> render;
};
} // namespace ftxui
#endif /* end of include guard: FTXUI_COMPONENT_COMPONENT_OPTIONS_HPP */

View File

@ -235,6 +235,62 @@ class TabContainer : public ContainerBase {
}
};
class StackedContainer : public ContainerBase{
public:
StackedContainer(Components children)
: ContainerBase(std::move(children), nullptr) {}
private:
Element Render() final {
Elements elements;
for (auto& child : children_) {
elements.push_back(child->Render());
}
// Reverse the order of the elements.
std::reverse(elements.begin(), elements.end());
return dbox(std::move(elements));
}
bool Focusable() const final {
for (auto& child : children_) {
if (child->Focusable()) {
return true;
}
}
return false;
}
Component ActiveChild() final {
if (children_.size() == 0)
return nullptr;
return children_[0];
}
void SetActiveChild(ComponentBase* child) final {
if (children_.size() == 0) {
return;
}
// Find `child` and put it at the beginning without change the order of the
// other children.
auto it = std::find_if(children_.begin(), children_.end(),
[child](const Component& c) { return c.get() == child; });
if (it == children_.end()) {
return;
}
std::rotate(children_.begin(), it, it + 1);
}
bool OnEvent(Event event) final {
for (auto& child : children_) {
if (child->OnEvent(event)) {
return true;
}
}
return false;
}
};
namespace Container {
/// @brief A list of components, drawn one by one vertically and navigated
@ -345,6 +401,33 @@ Component Tab(Components children, int* selector) {
return std::make_shared<TabContainer>(std::move(children), selector);
}
/// @brief A list of components to be stacked on top of each other.
/// Events are propagated to the first component, then the second if not
/// handled, etc.
/// The components are drawn in the reverse order they are given.
/// When a component take focus, it is put at the front, without changing the
/// relative order of the other elements.
///
/// This should be used with the `Window` component.
///
/// @param children The list of components.
/// @ingroup component
/// @see Window
///
/// ### Example
///
/// ```cpp
/// auto container = Container::Stacked({
/// children_1,
/// children_2,
/// children_3,
/// children_4,
/// });
/// ```
Component Stacked(Components children) {
return std::make_shared<StackedContainer>(std::move(children));
}
} // namespace Container
} // namespace ftxui

View File

@ -135,24 +135,12 @@ class SliderBase : public ComponentBase {
}
bool OnMouseEvent(Event event) {
if (captured_mouse_ && event.mouse().motion == Mouse::Released) {
captured_mouse_ = nullptr;
return true;
}
if (gauge_box_.Contain(event.mouse().x, event.mouse().y) &&
CaptureMouse(event)) {
TakeFocus();
}
if (event.mouse().button == Mouse::Left &&
event.mouse().motion == Mouse::Pressed &&
gauge_box_.Contain(event.mouse().x, event.mouse().y) &&
!captured_mouse_) {
captured_mouse_ = CaptureMouse(event);
}
if (captured_mouse_) {
if (event.mouse().motion == Mouse::Released) {
captured_mouse_ = nullptr;
return true;
}
switch (options_.direction) {
case Direction::Right: {
value_() = min_() + (event.mouse().x - gauge_box_.x_min) *
@ -182,6 +170,23 @@ class SliderBase : public ComponentBase {
value_() = std::max(min_(), std::min(max_(), value_()));
return true;
}
if (event.mouse().button != Mouse::Left ||
event.mouse().motion != Mouse::Pressed) {
return false;
}
if (!gauge_box_.Contain(event.mouse().x, event.mouse().y)) {
return false;
}
captured_mouse_ = CaptureMouse(event);
if (captured_mouse_) {
TakeFocus();
return true;
}
return false;
}
@ -214,7 +219,9 @@ class SliderWithLabel : public ComponentBase {
return false;
}
if (!box_.Contain(event.mouse().x, event.mouse().y)) {
mouse_hover_ = box_.Contain(event.mouse().x, event.mouse().y);
if (!mouse_hover_) {
return false;
}
@ -222,13 +229,13 @@ class SliderWithLabel : public ComponentBase {
return false;
}
TakeFocus();
return true;
}
Element Render() override {
auto focus_management = Focused() ? focus : Active() ? select : nothing;
auto gauge_color = Focused() ? color(Color::White) : color(Color::GrayDark);
auto gauge_color = (Focused() || mouse_hover_) ? color(Color::White)
: color(Color::GrayDark);
return hbox({
text(label_()) | dim | vcenter,
hbox({
@ -242,6 +249,7 @@ class SliderWithLabel : public ComponentBase {
ConstStringRef label_;
Box box_;
bool mouse_hover_ = false;
};
/// @brief An horizontal slider.

View File

@ -53,8 +53,9 @@ TEST(SliderTest, Right) {
});
Screen screen(11, 1);
Render(screen, slider->Render());
EXPECT_EQ(value, 50);
EXPECT_TRUE(slider->OnEvent(MousePressed(3, 0)));
EXPECT_EQ(value, 30);
EXPECT_EQ(value, 50);
EXPECT_TRUE(slider->OnEvent(MousePressed(9, 0)));
EXPECT_EQ(value, 90);
EXPECT_TRUE(slider->OnEvent(MousePressed(9, 2)));
@ -76,8 +77,9 @@ TEST(SliderTest, Left) {
});
Screen screen(11, 1);
Render(screen, slider->Render());
EXPECT_EQ(value, 50);
EXPECT_TRUE(slider->OnEvent(MousePressed(3, 0)));
EXPECT_EQ(value, 70);
EXPECT_EQ(value, 50);
EXPECT_TRUE(slider->OnEvent(MousePressed(9, 0)));
EXPECT_EQ(value, 10);
EXPECT_TRUE(slider->OnEvent(MousePressed(9, 2)));
@ -99,8 +101,9 @@ TEST(SliderTest, Down) {
});
Screen screen(1, 11);
Render(screen, slider->Render());
EXPECT_EQ(value, 50);
EXPECT_TRUE(slider->OnEvent(MousePressed(0, 3)));
EXPECT_EQ(value, 30);
EXPECT_EQ(value, 50);
EXPECT_TRUE(slider->OnEvent(MousePressed(0, 9)));
EXPECT_EQ(value, 90);
EXPECT_TRUE(slider->OnEvent(MousePressed(2, 9)));
@ -122,8 +125,9 @@ TEST(SliderTest, Up) {
});
Screen screen(1, 11);
Render(screen, slider->Render());
EXPECT_EQ(value, 50);
EXPECT_TRUE(slider->OnEvent(MousePressed(0, 3)));
EXPECT_EQ(value, 70);
EXPECT_EQ(value, 50);
EXPECT_TRUE(slider->OnEvent(MousePressed(0, 9)));
EXPECT_EQ(value, 10);
EXPECT_TRUE(slider->OnEvent(MousePressed(2, 9)));

View File

@ -0,0 +1,306 @@
#define NOMINMAX
#include <algorithm>
#include <ftxui/component/component.hpp>
#include <ftxui/component/component_base.hpp>
#include <ftxui/component/screen_interactive.hpp> // for ScreenInteractive
#include "ftxui/dom/node_decorator.hpp" // for NodeDecorator
namespace ftxui {
namespace {
Decorator PositionAndSize(int left, int top, int width, int height) {
return [=](Element element) {
element |= size(WIDTH, EQUAL, width);
element |= size(HEIGHT, EQUAL, height);
auto padding_left = emptyElement() | size(WIDTH, EQUAL, left);
auto padding_top = emptyElement() | size(HEIGHT, EQUAL, top);
return vbox({
padding_top,
hbox({
padding_left,
element,
}),
});
};
}
class ResizeDecorator : public NodeDecorator {
public:
ResizeDecorator(Element child,
bool resize_left,
bool resize_right,
bool resize_top,
bool resize_down,
Color color)
: NodeDecorator(std::move(child)),
color_(color),
resize_left_(resize_left),
resize_right_(resize_right),
resize_top_(resize_top),
resize_down_(resize_down) {}
void Render(Screen& screen) override {
NodeDecorator::Render(screen);
if (resize_left_) {
for (int y = box_.y_min; y <= box_.y_max; ++y) {
auto& cell = screen.PixelAt(box_.x_min, y);
cell.foreground_color = color_;
cell.automerge = false;
}
}
if (resize_right_) {
for (int y = box_.y_min; y <= box_.y_max; ++y) {
auto& cell = screen.PixelAt(box_.x_max, y);
cell.foreground_color = color_;
cell.automerge = false;
}
}
if (resize_top_) {
for (int x = box_.x_min; x <= box_.x_max; ++x) {
auto& cell = screen.PixelAt(x, box_.y_min);
cell.foreground_color = color_;
cell.automerge = false;
}
}
if (resize_down_) {
for (int x = box_.x_min; x <= box_.x_max; ++x) {
auto& cell = screen.PixelAt(x, box_.y_max);
cell.foreground_color = color_;
cell.automerge = false;
}
}
}
Color color_;
const bool resize_left_;
const bool resize_right_;
const bool resize_top_;
const bool resize_down_;
};
Element DefaultRenderState(const WindowRenderState& state) {
Element element = state.inner;
if (state.active) {
element |= dim;
}
element = window(text(state.title), element);
element |= clear_under;
Color color = Color::Red;
element = std::make_shared<ResizeDecorator>( //
element, //
state.hover_left, //
state.hover_right, //
state.hover_top, //
state.hover_down, //
color //
);
return element;
}
class WindowImpl : public ComponentBase, public WindowOptions {
public:
WindowImpl(WindowOptions option) : WindowOptions(std::move(option)) {
if (!inner) {
inner = Make<ComponentBase>();
}
Add(inner);
}
private:
Element Render() final {
auto element = ComponentBase::Render();
bool captureable =
captured_mouse_ || ScreenInteractive::Active()->CaptureMouse();
const WindowRenderState state = {
element,
title(),
Active(),
drag_,
resize_left_ || resize_right_ || resize_down_ || resize_top_,
(resize_left_hover_ || resize_left_) && captureable,
(resize_right_hover_ || resize_right_) && captureable,
(resize_top_hover_ || resize_top_) && captureable,
(resize_down_hover_ || resize_down_) && captureable,
};
element = render ? render(state) : DefaultRenderState(state);
// Position and record the drawn area of the window.
element |= reflect(box_window_);
element |= PositionAndSize(left(), top(), width(), height());
element |= reflect(box_);
return element;
}
bool OnEvent(Event event) final {
if (ComponentBase::OnEvent(event)) {
return true;
}
if (!event.is_mouse()) {
return false;
}
mouse_hover_ = box_window_.Contain(event.mouse().x, event.mouse().y);
resize_down_hover_ = false;
resize_top_hover_ = false;
resize_left_hover_ = false;
resize_right_hover_ = false;
if (mouse_hover_) {
resize_left_hover_ = event.mouse().x == left() + box_.x_min;
resize_right_hover_ =
event.mouse().x == left() + width() - 1 + box_.x_min;
resize_top_hover_ = event.mouse().y == top() + box_.y_min;
resize_down_hover_ = event.mouse().y == top() + height() - 1 + box_.y_min;
// Apply the component options:
resize_top_hover_ &= resize_top();
resize_left_hover_ &= resize_left();
resize_down_hover_ &= resize_down();
resize_right_hover_ &= resize_right();
}
if (captured_mouse_) {
if (event.mouse().motion == Mouse::Released) {
captured_mouse_ = nullptr;
return true;
}
if (resize_left_) {
width() = left() + width() - event.mouse().x + box_.x_min;
left() = event.mouse().x - box_.x_min;
}
if (resize_right_) {
width() = event.mouse().x - resize_start_x - box_.x_min;
}
if (resize_top_) {
height() = top() + height() - event.mouse().y + box_.y_min;
top() = event.mouse().y - box_.y_min;
}
if (resize_down_) {
height() = event.mouse().y - resize_start_y - box_.y_min;
}
if (drag_) {
left() = event.mouse().x - drag_start_x - box_.x_min;
top() = event.mouse().y - drag_start_y - box_.y_min;
}
// Clamp the window size.
width() = std::max<int>(width(), title().size() + 2);
height() = std::max<int>(height(), 2);
return true;
}
resize_left_ = false;
resize_right_ = false;
resize_top_ = false;
resize_down_ = false;
if (!mouse_hover_) {
return false;
}
if (!CaptureMouse(event)) {
return true;
}
if (event.mouse().button != Mouse::Left ||
event.mouse().motion != Mouse::Pressed) {
return true;
}
TakeFocus();
captured_mouse_ = CaptureMouse(event);
if (!captured_mouse_) {
return true;
}
resize_left_ = resize_left_hover_;
resize_right_ = resize_right_hover_;
resize_top_ = resize_top_hover_;
resize_down_ = resize_down_hover_;
resize_start_x = event.mouse().x - width() - box_.x_min;
resize_start_y = event.mouse().y - height() - box_.y_min;
drag_start_x = event.mouse().x - left() - box_.x_min;
drag_start_y = event.mouse().y - top() - box_.y_min;
// Drag only if we are not resizeing a border yet:
drag_ = !resize_right_ && !resize_down_ && !resize_top_ && !resize_left_;
return true;
}
Box box_;
Box box_window_;
CapturedMouse captured_mouse_;
int drag_start_x = 0;
int drag_start_y = 0;
int resize_start_x = 0;
int resize_start_y = 0;
bool mouse_hover_ = false;
bool drag_ = false;
bool resize_top_ = false;
bool resize_left_ = false;
bool resize_down_ = false;
bool resize_right_ = false;
bool resize_top_hover_ = false;
bool resize_left_hover_ = false;
bool resize_down_hover_ = false;
bool resize_right_hover_ = false;
};
} // namespace
/// @brief A draggeable / resizeable window. To use multiple of them, they must
/// be stacked using `Container::Stacked({...})` component;
///
/// @param option A struct holding every parameters.
/// @ingroup component
/// @see Window
///
/// ### Example
///
/// ```cpp
/// auto window_1= Window({
/// .inner = DummyWindowContent(),
/// .title = "First window",
/// });
///
/// auto window_2= Window({
/// .inner = DummyWindowContent(),
/// .title = "Second window",
/// });
///
/// auto container = Container::Stacked({
/// window_1,
/// window_2,
/// });
/// ```
Component Window(WindowOptions option) {
return Make<WindowImpl>(std::move(option));
}
}; // namespace ftxui