diff --git a/examples/component/CMakeLists.txt b/examples/component/CMakeLists.txt index 3444d563..dd6430e0 100644 --- a/examples/component/CMakeLists.txt +++ b/examples/component/CMakeLists.txt @@ -39,6 +39,7 @@ example(radiobox) example(radiobox_in_frame) example(renderer) example(resizable_split) +example(resizable_split_clamp) example(scrollbar) example(selection) example(slider) diff --git a/examples/component/resizable_split.cpp b/examples/component/resizable_split.cpp index 05c6385a..f46fd89b 100644 --- a/examples/component/resizable_split.cpp +++ b/examples/component/resizable_split.cpp @@ -3,7 +3,6 @@ // the LICENSE file. #include // for shared_ptr, allocator, __shared_ptr_access -#include "ftxui/component/captured_mouse.hpp" // for ftxui #include "ftxui/component/component.hpp" // for Renderer, ResizableSplitBottom, ResizableSplitLeft, ResizableSplitRight, ResizableSplitTop #include "ftxui/component/component_base.hpp" // for ComponentBase #include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive @@ -14,17 +13,24 @@ using namespace ftxui; int main() { auto screen = ScreenInteractive::Fullscreen(); - auto middle = Renderer([] { return text("middle") | center; }); - auto left = Renderer([] { return text("Left") | center; }); - auto right = Renderer([] { return text("right") | center; }); - auto top = Renderer([] { return text("top") | center; }); - auto bottom = Renderer([] { return text("bottom") | center; }); - + // State: int left_size = 20; int right_size = 20; int top_size = 10; int bottom_size = 10; + // Renderers: + auto RendererInfo = [](const std::string& name, int* size) { + return Renderer([name, size] { + return text(name + ": " + std::to_string(*size)) | center; + }); + }; + auto middle = Renderer([] { return text("Middle") | center; }); + auto left = RendererInfo("Left", &left_size); + auto right = RendererInfo("Right", &right_size); + auto top = RendererInfo("Top", &top_size); + auto bottom = RendererInfo("Bottom", &bottom_size); + auto container = middle; container = ResizableSplitLeft(left, container, &left_size); container = ResizableSplitRight(right, container, &right_size); diff --git a/examples/component/resizable_split_clamp.cpp b/examples/component/resizable_split_clamp.cpp new file mode 100644 index 00000000..8876b0cb --- /dev/null +++ b/examples/component/resizable_split_clamp.cpp @@ -0,0 +1,43 @@ +// Copyright 2025 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 // for shared_ptr, allocator, __shared_ptr_access + +#include "ftxui/component/component.hpp" // for Renderer, ResizableSplitBottom, ResizableSplitLeft, ResizableSplitRight, ResizableSplitTop +#include "ftxui/component/component_base.hpp" // for ComponentBase +#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive +#include "ftxui/dom/elements.hpp" // for Element, operator|, text, center, border + +using namespace ftxui; + +int main() { + auto screen = ScreenInteractive::Fullscreen(); + + // State: + int size = 40; + int size_min = 10; + int size_max = 80; + + // Renderers: + auto split = ResizableSplit({ + .main = Renderer([] { return text("Left") | center; }), + .back = Renderer([] { return text("Right") | center; }), + .direction = Direction::Left, + .main_size = &size, + .min = &size_min, + .max = &size_max, + }); + + auto renderer = Renderer(split, [&] { + return window(text("Drag the separator with the mouse"), + vbox({ + text("Min: " + std::to_string(size_min)), + text("Max: " + std::to_string(size_max)), + text("Size: " + std::to_string(size)), + separator(), + split->Render() | flex, + })); + }); + + screen.Loop(renderer); +} diff --git a/include/ftxui/component/component_options.hpp b/include/ftxui/component/component_options.hpp index 37fe92d1..dc4414a7 100644 --- a/include/ftxui/component/component_options.hpp +++ b/include/ftxui/component/component_options.hpp @@ -11,6 +11,7 @@ #include // for Ref, ConstRef, StringRef #include #include // for function +#include // for numeric_limits #include // for string #include "ftxui/component/component_base.hpp" // for Component @@ -217,6 +218,10 @@ struct ResizableSplitOption { (direction() == Direction::Left || direction() == Direction::Right) ? 20 : 10; std::function separator_func = [] { return ::ftxui::separator(); }; + + // Constraints on main_size: + Ref min = 0; + Ref max = std::numeric_limits::max(); }; // @brief Option for the `Slider` component. diff --git a/src/ftxui/component/resizable_split.cpp b/src/ftxui/component/resizable_split.cpp index b07bdf12..777f928d 100644 --- a/src/ftxui/component/resizable_split.cpp +++ b/src/ftxui/component/resizable_split.cpp @@ -1,10 +1,10 @@ // Copyright 2021 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 // for max #include // for ResizableSplitOption #include // for Direction, Direction::Down, Direction::Left, Direction::Right, Direction::Up #include // for Ref -#include // for max #include // for function #include // for move @@ -19,34 +19,22 @@ namespace ftxui { namespace { -class ResizableSplitBase : public ComponentBase { +class ResizableSplitBase : public ComponentBase, public ResizableSplitOption { public: explicit ResizableSplitBase(ResizableSplitOption options) - : options_(std::move(options)) { - switch (options_->direction()) { + : ResizableSplitOption(std::move(options)) { + switch (direction()) { case Direction::Left: - Add(Container::Horizontal({ - options_->main, - options_->back, - })); + Add(Container::Horizontal({main, back})); break; case Direction::Right: - Add(Container::Horizontal({ - options_->back, - options_->main, - })); + Add(Container::Horizontal({back, main})); break; case Direction::Up: - Add(Container::Vertical({ - options_->main, - options_->back, - })); + Add(Container::Vertical({main, back})); break; case Direction::Down: - Add(Container::Vertical({ - options_->back, - options_->main, - })); + Add(Container::Vertical({back, main})); break; } } @@ -76,27 +64,27 @@ class ResizableSplitBase : public ComponentBase { return ComponentBase::OnEvent(event); } - switch (options_->direction()) { + switch (direction()) { case Direction::Left: - options_->main_size() = std::max(0, event.mouse().x - box_.x_min); - return true; + main_size() = std::max(0, event.mouse().x - box_.x_min); + break; case Direction::Right: - options_->main_size() = std::max(0, box_.x_max - event.mouse().x); - return true; + main_size() = std::max(0, box_.x_max - event.mouse().x); + break; case Direction::Up: - options_->main_size() = std::max(0, event.mouse().y - box_.y_min); - return true; + main_size() = std::max(0, event.mouse().y - box_.y_min); + break; case Direction::Down: - options_->main_size() = std::max(0, box_.y_max - event.mouse().y); - return true; + main_size() = std::max(0, box_.y_max - event.mouse().y); + break; } - // NOTREACHED() - return false; + main_size() = std::clamp(main_size(), min(), max()); + return true; } Element OnRender() final { - switch (options_->direction()) { + switch (direction()) { case Direction::Left: return RenderLeft(); case Direction::Right: @@ -112,46 +100,41 @@ class ResizableSplitBase : public ComponentBase { Element RenderLeft() { return hbox({ - options_->main->Render() | - size(WIDTH, EQUAL, options_->main_size()), - options_->separator_func() | reflect(separator_box_), - options_->back->Render() | xflex, + main->Render() | size(WIDTH, EQUAL, main_size()), + separator_func() | reflect(separator_box_), + back->Render() | xflex, }) | reflect(box_); } Element RenderRight() { return hbox({ - options_->back->Render() | xflex, - options_->separator_func() | reflect(separator_box_), - options_->main->Render() | - size(WIDTH, EQUAL, options_->main_size()), + back->Render() | xflex, + separator_func() | reflect(separator_box_), + main->Render() | size(WIDTH, EQUAL, main_size()), }) | reflect(box_); } Element RenderTop() { return vbox({ - options_->main->Render() | - size(HEIGHT, EQUAL, options_->main_size()), - options_->separator_func() | reflect(separator_box_), - options_->back->Render() | yflex, + main->Render() | size(HEIGHT, EQUAL, main_size()), + separator_func() | reflect(separator_box_), + back->Render() | yflex, }) | reflect(box_); } Element RenderBottom() { return vbox({ - options_->back->Render() | yflex, - options_->separator_func() | reflect(separator_box_), - options_->main->Render() | - size(HEIGHT, EQUAL, options_->main_size()), + back->Render() | yflex, + separator_func() | reflect(separator_box_), + main->Render() | size(HEIGHT, EQUAL, main_size()), }) | reflect(box_); } private: - Ref options_; CapturedMouse captured_mouse_; Box separator_box_; Box box_; diff --git a/src/ftxui/component/resizable_split_test.cpp b/src/ftxui/component/resizable_split_test.cpp index d74906ac..f6004f92 100644 --- a/src/ftxui/component/resizable_split_test.cpp +++ b/src/ftxui/component/resizable_split_test.cpp @@ -233,5 +233,105 @@ TEST(ResizableSplit, NavigationVertical) { EXPECT_FALSE(component_bottom->Active()); } +TEST(ResizableSplit, MinMaxSizeLeft) { + int position = 5; + auto component = ResizableSplit({ + .main = BasicComponent(), + .back = BasicComponent(), + .direction = Direction::Left, + .main_size = &position, + .separator_func = [] { return separatorDouble(); }, + .min = 3, + .max = 8, + }); + auto screen = Screen(20, 20); + Render(screen, component->Render()); + EXPECT_EQ(position, 5); + EXPECT_TRUE(component->OnEvent(MousePressed(5, 1))); + EXPECT_EQ(position, 5); + // Try to resize below min + EXPECT_TRUE(component->OnEvent(MousePressed(2, 1))); + EXPECT_EQ(position, 3); // Clamped to min + // Try to resize above max + EXPECT_TRUE(component->OnEvent(MousePressed(10, 1))); + EXPECT_EQ(position, 8); // Clamped to max + EXPECT_TRUE(component->OnEvent(MouseReleased(10, 1))); + EXPECT_EQ(position, 8); +} + +TEST(ResizableSplit, MinMaxSizeRight) { + int position = 5; + auto component = ResizableSplit({ + .main = BasicComponent(), + .back = BasicComponent(), + .direction = Direction::Right, + .main_size = &position, + .separator_func = [] { return separatorDouble(); }, + .min = 3, + .max = 8, + }); + auto screen = Screen(20, 20); + Render(screen, component->Render()); + EXPECT_EQ(position, 5); + EXPECT_TRUE(component->OnEvent(MousePressed(14, 1))); + EXPECT_EQ(position, 5); + // Try to resize below min + EXPECT_TRUE(component->OnEvent(MousePressed(18, 1))); + EXPECT_EQ(position, 3); // Clamped to min + // Try to resize above max + EXPECT_TRUE(component->OnEvent(MousePressed(10, 1))); + EXPECT_EQ(position, 8); // Clamped to max + EXPECT_TRUE(component->OnEvent(MouseReleased(10, 1))); + EXPECT_EQ(position, 8); +} + +TEST(ResizableSplit, MinMaxSizeTop) { + int position = 5; + auto component = ResizableSplit({ + .main = BasicComponent(), + .back = BasicComponent(), + .direction = Direction::Up, + .main_size = &position, + .separator_func = [] { return separatorDouble(); }, + .min = 2, + .max = 10, + }); + auto screen = Screen(20, 20); + Render(screen, component->Render()); + EXPECT_EQ(position, 5); + EXPECT_TRUE(component->OnEvent(MousePressed(1, 5))); + EXPECT_EQ(position, 5); + // Try to resize below min + EXPECT_TRUE(component->OnEvent(MousePressed(1, 1))); + EXPECT_EQ(position, 2); // Clamped to min + // Try to resize above max + EXPECT_TRUE(component->OnEvent(MousePressed(1, 15))); + EXPECT_EQ(position, 10); // Clamped to max +} + +TEST(ResizableSplit, MinMaxSizeBottom) { + int position = 5; + auto component = ResizableSplit({ + .main = BasicComponent(), + .back = BasicComponent(), + .direction = Direction::Down, + .main_size = &position, + .separator_func = [] { return separatorDouble(); }, + .min = 3, + .max = 12, + }); + auto screen = Screen(20, 20); + Render(screen, component->Render()); + EXPECT_EQ(position, 5); + EXPECT_TRUE(component->OnEvent(MousePressed(1, 14))); + EXPECT_EQ(position, 5); + // Try to resize below min + EXPECT_TRUE(component->OnEvent(MousePressed(1, 18))); + EXPECT_EQ(position, 3); // Clamped to min + // Try to resize above max + EXPECT_TRUE(component->OnEvent(MousePressed(1, 5))); + EXPECT_EQ(position, 12); // Clamped to max +} + } // namespace ftxui // NOLINTEND