From e19550ae69eedd720f47e1d4911d5119b22838e3 Mon Sep 17 00:00:00 2001 From: Arthur Sonzogni Date: Sat, 15 Jul 2023 16:29:48 +0200 Subject: [PATCH] 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. --- CMakeLists.txt | 1 + examples/component/CMakeLists.txt | 1 + examples/component/window.cpp | 86 +++++ examples/dom/CMakeLists.txt | 1 - examples/dom/window.cpp | 34 -- include/ftxui/component/component.hpp | 3 + include/ftxui/component/component_options.hpp | 33 ++ src/ftxui/component/container.cpp | 83 +++++ src/ftxui/component/slider.cpp | 48 +-- src/ftxui/component/slider_test.cpp | 12 +- src/ftxui/component/window.cpp | 306 ++++++++++++++++++ 11 files changed, 549 insertions(+), 59 deletions(-) create mode 100644 examples/component/window.cpp delete mode 100644 examples/dom/window.cpp create mode 100644 src/ftxui/component/window.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ebed83e..dc75085 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/examples/component/CMakeLists.txt b/examples/component/CMakeLists.txt index 6211583..4ac82eb 100644 --- a/examples/component/CMakeLists.txt +++ b/examples/component/CMakeLists.txt @@ -43,4 +43,5 @@ example(tab_horizontal) example(tab_vertical) example(textarea) example(toggle) +example(window) example(with_restored_io) diff --git a/examples/component/window.cpp b/examples/component/window.cpp new file mode 100644 index 0000000..efd9ec0 --- /dev/null +++ b/examples/component/window.cpp @@ -0,0 +1,86 @@ +#include +#include + +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(); +} + +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; +} diff --git a/examples/dom/CMakeLists.txt b/examples/dom/CMakeLists.txt index 911bf7f..59844ad 100644 --- a/examples/dom/CMakeLists.txt +++ b/examples/dom/CMakeLists.txt @@ -35,4 +35,3 @@ example(style_underlined_double) example(table) example(vbox_hbox) example(vflow) -example(window) diff --git a/examples/dom/window.cpp b/examples/dom/window.cpp deleted file mode 100644 index ed0e2b9..0000000 --- a/examples/dom/window.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include // for EXIT_SUCCESS -#include // for operator|=, Element, bgcolor, color, graph, border -#include // for Fixed, Screen -#include // 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 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. diff --git a/include/ftxui/component/component.hpp b/include/ftxui/component/component.hpp index 1842bf4..ccda278 100644 --- a/include/ftxui/component/component.hpp +++ b/include/ftxui/component/component.hpp @@ -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 on_enter, std::function on_leave); ComponentDecorator Hoverable(std::function on_change); +Component Window(WindowOptions option); + } // namespace ftxui #endif /* end of include guard: FTXUI_COMPONENT_HPP */ diff --git a/include/ftxui/component/component_options.hpp b/include/ftxui/component/component_options.hpp index 0967306..58fc71d 100644 --- a/include/ftxui/component/component_options.hpp +++ b/include/ftxui/component/component_options.hpp @@ -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 left = 0; /// < The left side position of the window. + Ref top = 0; /// < The top side position of the window. + Ref width = 20; /// < The width of the window. + Ref height = 10; /// < The height of the window. + + Ref resize_left = true; /// < Can the left side be resized? + Ref resize_right = true; /// < Can the right side be resized? + Ref resize_top = true; /// < Can the top side be resized? + Ref resize_down = true; /// < Can the down side be resized? + + /// An optional function to customize how the window looks like: + std::function render; +}; + } // namespace ftxui #endif /* end of include guard: FTXUI_COMPONENT_COMPONENT_OPTIONS_HPP */ diff --git a/src/ftxui/component/container.cpp b/src/ftxui/component/container.cpp index b2dc602..a4facdb 100644 --- a/src/ftxui/component/container.cpp +++ b/src/ftxui/component/container.cpp @@ -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(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(std::move(children)); +} + } // namespace Container } // namespace ftxui diff --git a/src/ftxui/component/slider.cpp b/src/ftxui/component/slider.cpp index 59199a8..3a38aa8 100644 --- a/src/ftxui/component/slider.cpp +++ b/src/ftxui/component/slider.cpp @@ -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. diff --git a/src/ftxui/component/slider_test.cpp b/src/ftxui/component/slider_test.cpp index af4d7f1..d33225a 100644 --- a/src/ftxui/component/slider_test.cpp +++ b/src/ftxui/component/slider_test.cpp @@ -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))); diff --git a/src/ftxui/component/window.cpp b/src/ftxui/component/window.cpp new file mode 100644 index 0000000..a8022a6 --- /dev/null +++ b/src/ftxui/component/window.cpp @@ -0,0 +1,306 @@ +#define NOMINMAX +#include +#include +#include +#include // 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( // + 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(); + } + 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(width(), title().size() + 2); + height() = std::max(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(std::move(option)); +} + +}; // namespace ftxui