mirror of
https://github.com/ArthurSonzogni/FTXUI.git
synced 2025-09-16 16:08:08 +08:00
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:
@@ -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
|
||||
|
@@ -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.
|
||||
|
@@ -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)));
|
||||
|
306
src/ftxui/component/window.cpp
Normal file
306
src/ftxui/component/window.cpp
Normal 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
|
Reference in New Issue
Block a user