Add flex_grow and flex_shrink.

Two new elements:
- flex_grow  : Expand the element to occupy free space.
- flex_shrink: Minimize the element leave away missing space.

flex = flex_grow | flex_shrink.

Other changes:
- hbox and vbox are now non flexible by default.
- the vtext element has been added to help writting tests.
- Many new tests.
This commit is contained in:
ArthurSonzogni 2020-06-01 23:40:32 +02:00 committed by Arthur Sonzogni
parent 7f7775ba62
commit 08ee49f3e6
17 changed files with 929 additions and 189 deletions

View File

@ -41,10 +41,10 @@ A simple C++ library for terminal based user interface.
hbox({ hbox({
text(L"left") | border, text(L"left") | border,
text(L"middle") | border | flex, text(L"middle") | border | flex,
text(L"right") | border text(L"right") | border,
}), }),
gauge(0.5) | border gauge(0.5) | border,
}) });
~~~ ~~~
~~~bash ~~~bash

View File

@ -118,7 +118,7 @@ class CompilerComponent : public Component {
L"gcc", L"gcc",
L"clang", L"clang",
L"emcc", L"emcc",
L"game_maker" L"game_maker",
L"Ada compilers", L"Ada compilers",
L"ALGOL 60 compilers", L"ALGOL 60 compilers",
L"ALGOL 68 compilers", L"ALGOL 68 compilers",
@ -212,9 +212,9 @@ class CompilerComponent : public Component {
}), }),
filler(), filler(),
}), }),
hflow(RenderCommandLine()), hflow(RenderCommandLine()) | flex_grow,
}) | }) |
border; flex_grow | border;
} }
Elements RenderCommandLine() { Elements RenderCommandLine() {

View File

@ -49,7 +49,7 @@ class MyComponent : public Component {
}); });
} }
bool OnEvent(Event event) { bool OnEvent(Event event) override {
if (event == Event::Return) { if (event == Event::Return) {
on_enter(); on_enter();
return true; return true;

View File

@ -21,6 +21,7 @@ using GraphFunction = std::function<std::vector<int>(int, int)>;
// --- Widget --- // --- Widget ---
Element text(std::wstring text); Element text(std::wstring text);
Element vtext(std::wstring text);
Element separator(void); Element separator(void);
Element separator(Pixel); Element separator(Pixel);
Element gauge(float ratio); Element gauge(float ratio);
@ -52,9 +53,11 @@ Element hflow(Elements);
// -- Flexibility --- // -- Flexibility ---
// Define how to share the remaining space when not all of it is used inside a // Define how to share the remaining space when not all of it is used inside a
// container. // container.
Element filler(); Element flex(Element); // Expand/Minimize if possible/needed.
Element flex(Element); Element flex_grow(Element); // Expand element if possible.
Element notflex(Element); Element flex_shrink(Element); // Minimize element if needed.
Element notflex(Element); // Reset the flex attribute.
Element filler(); // A blank expandable element.
// -- Size override; // -- Size override;
enum Direction { WIDTH, HEIGHT }; enum Direction { WIDTH, HEIGHT };

View File

@ -15,8 +15,10 @@ struct Requirement {
int min_y = 0; int min_y = 0;
// How much flexibility is given to the component. // How much flexibility is given to the component.
int flex_x = 0; int flex_grow_x = 0;
int flex_y = 0; int flex_grow_y = 0;
int flex_shrink_x = 0;
int flex_shrink_y = 0;
// Focus management to support the frame/focus/select element. // Focus management to support the frame/focus/select element.
enum Selection { enum Selection {

View File

@ -8,19 +8,19 @@
namespace ftxui { namespace ftxui {
Element hcenter(Element child) { Element hcenter(Element child) {
return hbox(filler(), std::move(child), filler()); return hbox(filler(), std::move(child), filler()) | flex_grow;
} }
Element vcenter(Element child) { Element vcenter(Element child) {
return vbox(filler(), std::move(child), filler()); return vbox(filler(), std::move(child), filler()) | flex_grow;
} }
Element center(Element child) { Element center(Element child) {
return hcenter(vcenter(std::move(child))); return hcenter(vcenter(std::move(child))) | flex_grow;
} }
Element align_right(Element child) { Element align_right(Element child) {
return hbox(filler(), std::move(child)); return hbox(filler(), std::move(child)) | flex_grow;
} }
} // namespace ftxui } // namespace ftxui

View File

@ -17,8 +17,10 @@ class DBox : public Node {
void ComputeRequirement() override { void ComputeRequirement() override {
requirement_.min_x = 0; requirement_.min_x = 0;
requirement_.min_y = 0; requirement_.min_y = 0;
requirement_.flex_x = 1; requirement_.flex_grow_x = 0;
requirement_.flex_y = 0; requirement_.flex_grow_y = 0;
requirement_.flex_shrink_x = 0;
requirement_.flex_shrink_y = 0;
for (auto& child : children) { for (auto& child : children) {
child->ComputeRequirement(); child->ComputeRequirement();
requirement_.min_x = requirement_.min_x =

View File

@ -7,10 +7,36 @@
namespace ftxui { namespace ftxui {
using FlexFunction = void (*)(Requirement&);
void function_flex_grow(Requirement& r) {
r.flex_grow_x = 1;
r.flex_grow_y = 1;
}
void function_flex_shrink(Requirement& r) {
r.flex_shrink_x = 1;
r.flex_shrink_y = 1;
}
void function_flex(Requirement& r) {
r.flex_grow_x = 1;
r.flex_grow_y = 1;
r.flex_shrink_x = 1;
r.flex_shrink_y = 1;
}
void function_not_flex(Requirement& r) {
r.flex_grow_x = 0;
r.flex_grow_y = 0;
r.flex_shrink_x = 0;
r.flex_shrink_y = 0;
}
class Flex : public Node { class Flex : public Node {
public: public:
Flex() {} Flex(FlexFunction f) { f_ = f; }
Flex(Element child) : Node(unpack(std::move(child))) {} Flex(FlexFunction f, Element child) : Node(unpack(std::move(child))), f_(f) {}
~Flex() override {} ~Flex() override {}
void ComputeRequirement() override { void ComputeRequirement() override {
requirement_.min_x = 0; requirement_.min_x = 0;
@ -19,8 +45,7 @@ class Flex : public Node {
children[0]->ComputeRequirement(); children[0]->ComputeRequirement();
requirement_ = children[0]->requirement(); requirement_ = children[0]->requirement();
} }
requirement_.flex_x = 1; f_(requirement_);
requirement_.flex_y = 1;
} }
void SetBox(Box box) override { void SetBox(Box box) override {
@ -28,35 +53,28 @@ class Flex : public Node {
return; return;
children[0]->SetBox(box); children[0]->SetBox(box);
} }
};
class NotFlex : public Flex { FlexFunction f_;
public:
NotFlex() {}
NotFlex(Element child) : Flex(std::move(child)) {}
~NotFlex() override {}
void ComputeRequirement() override {
requirement_.min_x = 0;
requirement_.min_y = 0;
if (!children.empty()) {
children[0]->ComputeRequirement();
requirement_ = children[0]->requirement();
}
requirement_.flex_x = 0;
requirement_.flex_y = 0;
}
}; };
Element filler() { Element filler() {
return std::make_shared<Flex>(); return std::make_shared<Flex>(function_flex);
} }
Element flex(Element child) { Element flex(Element child) {
return std::make_shared<Flex>(std::move(child)); return std::make_shared<Flex>(function_flex, std::move(child));
}
Element flex_grow(Element child) {
return std::make_shared<Flex>(function_flex_grow, std::move(child));
}
Element flex_shrink(Element child) {
return std::make_shared<Flex>(function_flex_shrink, std::move(child));
} }
Element notflex(Element child) { Element notflex(Element child) {
return std::make_shared<NotFlex>(std::move(child)); return std::make_shared<Flex>(function_not_flex, std::move(child));
} }
} // namespace ftxui } // namespace ftxui

View File

@ -15,7 +15,11 @@ class Gauge : public Node {
~Gauge() override {} ~Gauge() override {}
void ComputeRequirement() override { void ComputeRequirement() override {
requirement_.flex_x = 1; requirement_.flex_grow_x = 1;
requirement_.flex_grow_y = 0;
requirement_.flex_shrink_x = 1;
requirement_.flex_shrink_y = 0;
requirement_.min_x = 1;
requirement_.min_y = 1; requirement_.min_y = 1;
} }

View File

@ -14,10 +14,12 @@ class Graph : public Node {
~Graph() override {} ~Graph() override {}
void ComputeRequirement() override { void ComputeRequirement() override {
requirement_.flex_x = 1; requirement_.flex_grow_x = 1;
requirement_.flex_y = 1; requirement_.flex_grow_y = 1;
requirement_.min_x = 1; requirement_.flex_shrink_x = 1;
requirement_.min_y = 1; requirement_.flex_shrink_y = 1;
requirement_.min_x = 3;
requirement_.min_y = 3;
} }
void Render(Screen& screen) override { void Render(Screen& screen) override {

View File

@ -17,8 +17,10 @@ class HBox : public Node {
void ComputeRequirement() override { void ComputeRequirement() override {
requirement_.min_x = 0; requirement_.min_x = 0;
requirement_.min_y = 0; requirement_.min_y = 0;
requirement_.flex_x = 1; requirement_.flex_grow_x = 0;
requirement_.flex_y = 0; requirement_.flex_grow_y = 0;
requirement_.flex_shrink_x = 0;
requirement_.flex_shrink_y = 0;
for (auto& child : children) { for (auto& child : children) {
child->ComputeRequirement(); child->ComputeRequirement();
if (requirement_.selection < child->requirement().selection) { if (requirement_.selection < child->requirement().selection) {
@ -36,31 +38,89 @@ class HBox : public Node {
void SetBox(Box box) override { void SetBox(Box box) override {
Node::SetBox(box); Node::SetBox(box);
int flex_sum = 0;
for (auto& child : children)
flex_sum += child->requirement().flex_x;
int space = box.x_max - box.x_min + 1; int space = box.x_max - box.x_min + 1;
int extra_space = space - requirement_.min_x; int extra_space = space - requirement_.min_x;
int remaining_flex = flex_sum; int size = 0;
int remaining_extra_space = extra_space; int flex_grow_sum = 0;
int flex_shrink_sum = 0;
int flex_shrink_size = 0;
for (auto& child : children) {
const Requirement& r = child->requirement();
flex_grow_sum += r.flex_grow_x;
flex_shrink_sum += r.min_x * r.flex_shrink_x;
if (r.flex_shrink_x) {
flex_shrink_size += r.min_x;
}
size += r.min_x;
}
if (extra_space >= 0)
SetBoxGrow(box, extra_space, flex_grow_sum);
else if (flex_shrink_size + extra_space >= 0)
SetBoxShrinkEasy(box, extra_space, flex_shrink_sum);
else
SetBoxShrinkHard(box, extra_space + flex_shrink_size,
size - flex_shrink_size);
}
void SetBoxGrow(Box box, int extra_space, int flex_grow_sum) {
int x = box.x_min; int x = box.x_min;
for (auto& child : children) { for (auto& child : children) {
Box child_box = box; Box child_box = box;
const Requirement& r = child->requirement();
int added_space =
extra_space * r.flex_grow_x / std::max(flex_grow_sum, 1);
extra_space -= added_space;
flex_grow_sum -= r.flex_grow_x;
child_box.x_min = x; child_box.x_min = x;
child_box.x_max = x + r.min_x + added_space - 1;
child_box.x_max = x + child->requirement().min_x - 1; child->SetBox(child_box);
x = child_box.x_max + 1;
}
}
if (child->requirement().flex_x) { void SetBoxShrinkEasy(Box box, int extra_space, int flex_shrink_sum) {
int added_space = remaining_extra_space * child->requirement().flex_x / int x = box.x_min;
remaining_flex; for (auto& child : children) {
remaining_extra_space -= added_space; Box child_box = box;
remaining_flex -= child->requirement().flex_x; const Requirement& r = child->requirement();
child_box.x_max += added_space;
int added_space = extra_space * r.min_x * r.flex_shrink_x /
std::max(flex_shrink_sum, 1);
extra_space -= added_space;
flex_shrink_sum -= r.flex_shrink_x * r.min_x;
child_box.x_min = x;
child_box.x_max = x + r.min_x + added_space - 1;
child->SetBox(child_box);
x = child_box.x_max + 1;
}
}
void SetBoxShrinkHard(Box box, int extra_space, int size) {
int x = box.x_min;
for (auto& child : children) {
Box child_box = box;
const Requirement& r = child->requirement();
if (r.flex_shrink_x) {
child_box.x_min = x;
child_box.x_max = x - 1;
child->SetBox(child_box);
continue;
} }
child_box.x_max = std::min(child_box.x_max, box.x_max);
int added_space = extra_space * r.min_x / std::max(1, size);
extra_space -= added_space;
size -= r.min_x;
child_box.x_min = x;
child_box.x_max = x + r.min_x + added_space - 1;
child->SetBox(child_box); child->SetBox(child_box);
x = child_box.x_max + 1; x = child_box.x_max + 1;

View File

@ -9,81 +9,347 @@
using namespace ftxui; using namespace ftxui;
using namespace ftxui; using namespace ftxui;
TEST(HBoxTest, ScreenSmaller1) { TEST(HBoxTest, NoFlex_NoFlex_NoFlex) {
auto root = hbox(text(L"text_1"), text(L"text_2")); auto root = hbox({
Screen screen(11, 1); text(L"012"),
Render(screen, root); text(L"abc"),
text(L"ABC"),
});
EXPECT_EQ("text_1text_", screen.ToString()); std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"0aA", //
"01aA", //
"01abA", //
"01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(i, 1);
Render(screen, root);
EXPECT_EQ(expectations[i], screen.ToString());
}
} }
TEST(HBoxTest, ScreenSmaller2) { TEST(HBoxTest, FlexGrow_NoFlex_NoFlex) {
auto root = hbox(text(L"text_1"), text(L"text_2")); auto root = hbox({
Screen screen(10, 1); text(L"012") | flex_grow,
Render(screen, root); text(L"abc"),
text(L"ABC"),
});
EXPECT_EQ("text_1text", screen.ToString()); std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"0aA", //
"01aA", //
"01abA", //
"01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012 abcABC", //
"012 abcABC", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(i, 1);
Render(screen, root);
EXPECT_EQ(expectations[i], screen.ToString());
}
} }
TEST(HBoxTest, ScreenFit) { TEST(HBoxTest, NoFlex_FlexGrow_NoFlex) {
auto root = hbox(text(L"text_1"), text(L"text_2")); auto root = hbox({
Screen screen(12, 1); text(L"012"),
Render(screen, root); text(L"abc") | flex_grow,
text(L"ABC"),
});
EXPECT_EQ("text_1text_2", screen.ToString()); std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"0aA", //
"01aA", //
"01abA", //
"01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012abc ABC", //
"012abc ABC", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(i, 1);
Render(screen, root);
EXPECT_EQ(expectations[i], screen.ToString());
}
} }
TEST(HBoxTest, ScreenBigger1) { TEST(HBoxTest, NoFlex_NoFlex_FlexGrow) {
auto root = hbox(text(L"text_1"), text(L"text_2")); auto root = hbox({
Screen screen(13, 1); text(L"012"),
Render(screen, root); text(L"abc"),
text(L"ABC") | flex_grow,
});
EXPECT_EQ("text_1text_2 ", screen.ToString()); std::vector<std::string> expectations = {
} "", //
TEST(HBoxTest, ScreenBigger2) { "0", //
auto root = hbox(text(L"text_1"), text(L"text_2")); "0a", //
Screen screen(14, 1); "0aA", //
Render(screen, root); "01aA", //
"01abA", //
EXPECT_EQ("text_1text_2 ", screen.ToString()); "01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(i, 1);
Render(screen, root);
EXPECT_EQ(expectations[i], screen.ToString());
}
} }
TEST(HBoxTest, ScreenSmaller1Flex) { TEST(HBoxTest, FlexGrow_NoFlex_FlexGrow) {
auto root = hbox(text(L"text_1"), filler(), text(L"text_2")); auto root = hbox({
Screen screen(11, 1); text(L"012") | flex_grow,
Render(screen, root); text(L"abc"),
text(L"ABC") | flex_grow,
});
EXPECT_EQ("text_text_2", screen.ToString()); std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"0aA", //
"01aA", //
"01abA", //
"01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
"012 abcABC ", //
"012 abcABC ", //
"012 abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(i, 1);
Render(screen, root);
EXPECT_EQ(expectations[i], screen.ToString());
}
} }
TEST(HBoxTest, ScreenSmaller2Flex) { TEST(HBoxTest, FlexGrow_FlexGrow_FlexGrow) {
auto root = hbox(text(L"text_1"), filler(), text(L"text_2")); auto root = hbox({
Screen screen(10, 1); text(L"012") | flex_grow,
Render(screen, root); text(L"abc") | flex_grow,
text(L"ABC") | flex_grow,
});
EXPECT_EQ("texttext_2", screen.ToString()); std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"0aA", //
"01aA", //
"01abA", //
"01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
"012abc ABC ", //
"012 abc ABC ", //
"012 abc ABC ", //
"012 abc ABC ", //
"012 abc ABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(i, 1);
Render(screen, root);
EXPECT_EQ(expectations[i], screen.ToString());
}
} }
TEST(HBoxTest, ScreenFitFlex) { // ------
auto root = hbox(text(L"text_1"), filler(), text(L"text_2"));
Screen screen(12, 1);
Render(screen, root);
EXPECT_EQ("text_1text_2", screen.ToString()); TEST(HBoxTest, FlexShrink_NoFlex_NoFlex) {
auto root = hbox({
text(L"012") | flex_shrink,
text(L"abc"),
text(L"ABC"),
});
std::vector<std::string> expectations = {
"", //
"a", //
"aA", //
"abA", //
"abAB", //
"abcAB", //
"abcABC", //
"0abcABC", //
"01abcABC", //
"012abcABC", //
"012abcABC ", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(i, 1);
Render(screen, root);
EXPECT_EQ(expectations[i], screen.ToString());
}
} }
TEST(HBoxTest, ScreenBigger1Flex) { TEST(HBoxTest, NoFlex_FlexShrink_NoFlex) {
auto root = hbox(text(L"text_1"), filler(), text(L"text_2")); auto root = hbox({
Screen screen(13, 1); text(L"012"),
Render(screen, root); text(L"abc") | flex_shrink,
text(L"ABC"),
});
EXPECT_EQ("text_1 text_2", screen.ToString()); std::vector<std::string> expectations = {
"", //
"0", //
"0A", //
"01A", //
"01AB", //
"012AB", //
"012ABC", //
"012aABC", //
"012abABC", //
"012abcABC", //
"012abcABC ", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(i, 1);
Render(screen, root);
EXPECT_EQ(expectations[i], screen.ToString());
}
} }
TEST(HBoxTest, ScreenBigger2Flex) { TEST(HBoxTest, NoFlex_NoFlex_FlexShrink) {
auto root = hbox(text(L"text_1"), filler(), text(L"text_2")); auto root = hbox({
Screen screen(14, 1); text(L"012"),
Render(screen, root); text(L"abc"),
text(L"ABC") | flex_shrink,
});
EXPECT_EQ("text_1 text_2", screen.ToString()); std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"01a", //
"01ab", //
"012ab", //
"012abc", //
"012abcA", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(i, 1);
Render(screen, root);
EXPECT_EQ(expectations[i], screen.ToString());
}
}
TEST(HBoxTest, FlexShrink_NoFlex_FlexShrink) {
auto root = hbox({
text(L"012") | flex_shrink,
text(L"abc"),
text(L"ABC") | flex_shrink,
});
std::vector<std::string> expectations = {
"", //
"a", //
"ab", //
"abc", //
"0abc", //
"0abcA", //
"01abcA", //
"01abcAB", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(i, 1);
Render(screen, root);
EXPECT_EQ(expectations[i], screen.ToString());
}
}
TEST(HBoxTest, FlexShrink_FlexShrink_FlexShrink) {
auto root = hbox({
text(L"012") | flex_shrink,
text(L"abc") | flex_shrink,
text(L"ABC") | flex_shrink,
});
std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"0aA", //
"01aA", //
"01abA", //
"01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
"012abcABC ", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(i, 1);
Render(screen, root);
EXPECT_EQ(expectations[i], screen.ToString());
}
}
TEST(HBoxTest, FlexGrow_NoFlex_FlewShrink) {
auto root = hbox({
text(L"012") | flex_grow,
text(L"abc"),
text(L"ABC") | flex_shrink,
});
std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"01a", //
"01ab", //
"012ab", //
"012abc", //
"012abcA", //
"012abcAB", //
"012abcABC", //
"012 abcABC", //
"012 abcABC", //
"012 abcABC", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(i, 1);
Render(screen, root);
EXPECT_EQ(expectations[i], screen.ToString());
}
} }

View File

@ -15,10 +15,12 @@ class HFlow : public Node {
~HFlow() {} ~HFlow() {}
void ComputeRequirement() override { void ComputeRequirement() override {
requirement_.min_x = 0; requirement_.min_x = 1;
requirement_.min_y = 0; requirement_.min_y = 1;
requirement_.flex_x = 1; requirement_.flex_grow_x = 1;
requirement_.flex_y = 1; requirement_.flex_grow_y = 1;
requirement_.flex_shrink_x = 0;
requirement_.flex_shrink_y = 0;
for (auto& child : children) for (auto& child : children)
child->ComputeRequirement(); child->ComputeRequirement();
} }

View File

@ -37,10 +37,13 @@ class Size : public Node {
break; break;
} }
if (direction_ == WIDTH) if (direction_ == WIDTH) {
requirement_.flex_x = 0; requirement_.flex_grow_x = 0;
else requirement_.flex_shrink_x = 0;
requirement_.flex_y = 0; } else {
requirement_.flex_grow_y = 0;
requirement_.flex_shrink_y = 0;
}
} }
void SetBox(Box box) override { void SetBox(Box box) override {

View File

@ -4,6 +4,7 @@
#include "ftxui/dom/node.hpp" #include "ftxui/dom/node.hpp"
#include "ftxui/screen/string.hpp" #include "ftxui/screen/string.hpp"
#include <algorithm>
namespace ftxui { namespace ftxui {
@ -36,8 +37,43 @@ class Text : public Node {
std::wstring text_; std::wstring text_;
}; };
class VText : public Node {
public:
VText(std::wstring text) : Node(), text_(text) {
for (auto& c : text_)
width_ = std::max(width_, wchar_width(c));
}
~VText() {}
void ComputeRequirement() override {
requirement_.min_x = width_;
requirement_.min_y = text_.size();
}
void Render(Screen& screen) override {
int x = box_.x_min;
int y = box_.y_min;
if (x + width_ - 1 > box_.x_max)
return;
for (wchar_t c : text_) {
if (y > box_.y_max)
return;
screen.at(x, y) = c;
y += 1;
}
}
private:
std::wstring text_;
int width_ = 1;
};
Element text(std::wstring text) { Element text(std::wstring text) {
return std::make_shared<Text>(text); return std::make_shared<Text>(text);
} }
Element vtext(std::wstring text) {
return std::make_shared<VText>(text);
}
} // namespace ftxui } // namespace ftxui

View File

@ -15,11 +15,13 @@ class VBox : public Node {
VBox(Elements children) : Node(std::move(children)) {} VBox(Elements children) : Node(std::move(children)) {}
~VBox() {} ~VBox() {}
void ComputeRequirement() { void ComputeRequirement() override {
requirement_.min_x = 0; requirement_.min_x = 0;
requirement_.min_y = 0; requirement_.min_y = 0;
requirement_.flex_x = 0; requirement_.flex_grow_x = 0;
requirement_.flex_y = 1; requirement_.flex_grow_y = 0;
requirement_.flex_shrink_x = 0;
requirement_.flex_shrink_y = 0;
for (auto& child : children) { for (auto& child : children) {
child->ComputeRequirement(); child->ComputeRequirement();
if (requirement_.selection < child->requirement().selection) { if (requirement_.selection < child->requirement().selection) {
@ -34,34 +36,92 @@ class VBox : public Node {
} }
} }
void SetBox(Box box) { void SetBox(Box box) override {
Node::SetBox(box); Node::SetBox(box);
int flex_sum = 0;
for (auto& child : children)
flex_sum += child->requirement().flex_y;
int space = box.y_max - box.y_min + 1; int space = box.y_max - box.y_min + 1;
int extra_space = space - requirement_.min_y; int extra_space = space - requirement_.min_y;
int remaining_flex = flex_sum; int size = 0;
int remaining_extra_space = extra_space; int flex_grow_sum = 0;
int flex_shrink_sum = 0;
int flex_shrink_size = 0;
for (auto& child : children) {
const Requirement& r = child->requirement();
flex_grow_sum += r.flex_grow_y;
flex_shrink_sum += r.min_y * r.flex_shrink_y;
if (r.flex_shrink_y) {
flex_shrink_size += r.min_y;
}
size += r.min_y;
}
if (extra_space >= 0)
SetBoxGrow(box, extra_space, flex_grow_sum);
else if (flex_shrink_size + extra_space >= 0)
SetBoxShrinkEasy(box, extra_space, flex_shrink_sum);
else
SetBoxShrinkHard(box, extra_space + flex_shrink_size,
size - flex_shrink_size);
}
void SetBoxGrow(Box box, int extra_space, int flex_grow_sum) {
int y = box.y_min; int y = box.y_min;
for (auto& child : children) { for (auto& child : children) {
Box child_box = box; Box child_box = box;
const Requirement& r = child->requirement();
int added_space =
extra_space * r.flex_grow_y / std::max(flex_grow_sum, 1);
extra_space -= added_space;
flex_grow_sum -= r.flex_grow_y;
child_box.y_min = y; child_box.y_min = y;
child_box.y_max = y + r.min_y + added_space - 1;
child_box.y_max = y + child->requirement().min_y - 1; child->SetBox(child_box);
y = child_box.y_max + 1;
}
}
if (child->requirement().flex_y) { void SetBoxShrinkEasy(Box box, int extra_space, int flex_shrink_sum) {
int added_space = remaining_extra_space * child->requirement().flex_y / int y = box.y_min;
remaining_flex; for (auto& child : children) {
remaining_extra_space -= added_space; Box child_box = box;
remaining_flex -= child->requirement().flex_y; const Requirement& r = child->requirement();
child_box.y_max += added_space;
int added_space = extra_space * r.min_y * r.flex_shrink_y /
std::max(flex_shrink_sum, 1);
extra_space -= added_space;
flex_shrink_sum -= r.flex_shrink_y * r.min_y;
child_box.y_min = y;
child_box.y_max = y + r.min_y + added_space - 1;
child->SetBox(child_box);
y = child_box.y_max + 1;
}
}
void SetBoxShrinkHard(Box box, int extra_space, int size) {
int y = box.y_min;
for (auto& child : children) {
Box child_box = box;
const Requirement& r = child->requirement();
if (r.flex_shrink_y) {
child_box.y_min = y;
child_box.y_max = y - 1;
child->SetBox(child_box);
continue;
} }
child_box.y_max = std::min(child_box.y_max, box.y_max);
int added_space = extra_space * r.min_y / std::max(1, size);
extra_space -= added_space;
size -= r.min_y;
child_box.y_min = y;
child_box.y_max = y + r.min_y + added_space - 1;
child->SetBox(child_box); child->SetBox(child_box);
y = child_box.y_max + 1; y = child_box.y_max + 1;

View File

@ -9,70 +9,352 @@
using namespace ftxui; using namespace ftxui;
using namespace ftxui; using namespace ftxui;
TEST(VBoxTest, ScreenSmaller1) { std::string rotate(std::string str) {
auto root = vbox(text(L"text_1"), text(L"text_2")); str.erase(std::remove(str.begin(), str.end(), '\n'), str.end());
Screen screen(6, 1); return str;
Render(screen, root);
EXPECT_EQ("text_1", screen.ToString());
} }
TEST(VBoxTest, ScreenFit) { TEST(VBoxText, NoFlex_NoFlex_NoFlex) {
auto root = vbox(text(L"text_1"), text(L"text_2")); auto root = vbox({
Screen screen(6, 2); vtext(L"012"),
Render(screen, root); vtext(L"abc"),
vtext(L"ABC"),
});
EXPECT_EQ("text_1\ntext_2", screen.ToString()); std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"0aA", //
"01aA", //
"01abA", //
"01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(1, i);
Render(screen, root);
EXPECT_EQ(expectations[i], rotate(screen.ToString()));
}
} }
TEST(VBoxTest, ScreenBigger1) { TEST(VBoxText, FlexGrow_NoFlex_NoFlex) {
auto root = vbox(text(L"text_1"), text(L"text_2")); auto root = vbox({
Screen screen(6, 3); vtext(L"012") | flex_grow,
Render(screen, root); vtext(L"abc"),
vtext(L"ABC"),
});
EXPECT_EQ("text_1\ntext_2\n ", screen.ToString()); std::vector<std::string> expectations = {
} "", //
TEST(VBoxTest, ScreenBigger2) { "0", //
auto root = vbox(text(L"text_1"), text(L"text_2")); "0a", //
Screen screen(6, 4); "0aA", //
Render(screen, root); "01aA", //
"01abA", //
EXPECT_EQ("text_1\ntext_2\n \n ", screen.ToString()); "01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012 abcABC", //
"012 abcABC", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(1, i);
Render(screen, root);
EXPECT_EQ(expectations[i], rotate(screen.ToString()));
}
} }
TEST(VBoxTest, ScreenSmaller1Flex) { TEST(VBoxText, NoFlex_FlexGrow_NoFlex) {
auto root = vbox(text(L"text_1"), filler(), text(L"text_2")); auto root = vbox({
Screen screen(6, 1); vtext(L"012"),
Render(screen, root); vtext(L"abc") | flex_grow,
vtext(L"ABC"),
});
EXPECT_EQ("text_2", screen.ToString()); std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"0aA", //
"01aA", //
"01abA", //
"01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012abc ABC", //
"012abc ABC", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(1, i);
Render(screen, root);
EXPECT_EQ(expectations[i], rotate(screen.ToString()));
}
} }
TEST(VBoxTest, ScreenFitFlex) { TEST(VBoxText, NoFlex_NoFlex_FlexGrow) {
auto root = vbox(text(L"text_1"), filler(), text(L"text_2")); auto root = vbox({
Screen screen(7, 5); vtext(L"012"),
Render(screen, root); vtext(L"abc"),
vtext(L"ABC") | flex_grow,
});
EXPECT_EQ( std::vector<std::string> expectations = {
"text_1 \n" "", //
" \n" "0", //
" \n" "0a", //
" \n" "0aA", //
"text_2 ", "01aA", //
screen.ToString()); "01abA", //
"01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(1, i);
Render(screen, root);
EXPECT_EQ(expectations[i], rotate(screen.ToString()));
}
} }
TEST(VBoxTest, ScreenBigger1Flex) { TEST(VBoxText, FlexGrow_NoFlex_FlexGrow) {
auto root = vbox(text(L"text_1"), filler(), text(L"text_2")); auto root = vbox({
Screen screen(6, 3); vtext(L"012") | flex_grow,
Render(screen, root); vtext(L"abc"),
vtext(L"ABC") | flex_grow,
});
EXPECT_EQ("text_1\n \ntext_2", screen.ToString()); std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"0aA", //
"01aA", //
"01abA", //
"01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
"012 abcABC ", //
"012 abcABC ", //
"012 abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(1, i);
Render(screen, root);
EXPECT_EQ(expectations[i], rotate(screen.ToString()));
}
} }
TEST(VBoxTest, ScreenBigger2Flex) {
auto root = vbox(text(L"text_1"), filler(), text(L"text_2"));
Screen screen(6, 4);
Render(screen, root);
EXPECT_EQ("text_1\n \n \ntext_2", screen.ToString()); TEST(VBoxText, FlexGrow_FlexGrow_FlexGrow) {
auto root = vbox({
vtext(L"012") | flex_grow,
vtext(L"abc") | flex_grow,
vtext(L"ABC") | flex_grow,
});
std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"0aA", //
"01aA", //
"01abA", //
"01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
"012abc ABC ", //
"012 abc ABC ", //
"012 abc ABC ", //
"012 abc ABC ", //
"012 abc ABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(1, i);
Render(screen, root);
EXPECT_EQ(expectations[i], rotate(screen.ToString()));
}
}
// ------
TEST(VBoxText, FlexShrink_NoFlex_NoFlex) {
auto root = vbox({
vtext(L"012") | flex_shrink,
vtext(L"abc"),
vtext(L"ABC"),
});
std::vector<std::string> expectations = {
"", //
"a", //
"aA", //
"abA", //
"abAB", //
"abcAB", //
"abcABC", //
"0abcABC", //
"01abcABC", //
"012abcABC", //
"012abcABC ", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(1, i);
Render(screen, root);
EXPECT_EQ(expectations[i], rotate(screen.ToString()));
}
}
TEST(VBoxText, NoFlex_FlexShrink_NoFlex) {
auto root = vbox({
vtext(L"012"),
vtext(L"abc") | flex_shrink,
vtext(L"ABC"),
});
std::vector<std::string> expectations = {
"", //
"0", //
"0A", //
"01A", //
"01AB", //
"012AB", //
"012ABC", //
"012aABC", //
"012abABC", //
"012abcABC", //
"012abcABC ", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(1, i);
Render(screen, root);
EXPECT_EQ(expectations[i], rotate(screen.ToString()));
}
}
TEST(VBoxText, NoFlex_NoFlex_FlexShrink) {
auto root = vbox({
vtext(L"012"),
vtext(L"abc"),
vtext(L"ABC") | flex_shrink,
});
std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"01a", //
"01ab", //
"012ab", //
"012abc", //
"012abcA", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(1, i);
Render(screen, root);
EXPECT_EQ(expectations[i], rotate(screen.ToString()));
}
}
TEST(VBoxText, FlexShrink_NoFlex_FlexShrink) {
auto root = vbox({
vtext(L"012") | flex_shrink,
vtext(L"abc"),
vtext(L"ABC") | flex_shrink,
});
std::vector<std::string> expectations = {
"", //
"a", //
"ab", //
"abc", //
"0abc", //
"0abcA", //
"01abcA", //
"01abcAB", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(1, i);
Render(screen, root);
EXPECT_EQ(expectations[i], rotate(screen.ToString()));
}
}
TEST(VBoxText, FlexShrink_FlexShrink_FlexShrink) {
auto root = vbox({
vtext(L"012") | flex_shrink,
vtext(L"abc") | flex_shrink,
vtext(L"ABC") | flex_shrink,
});
std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"0aA", //
"01aA", //
"01abA", //
"01abAB", //
"012abAB", //
"012abcAB", //
"012abcABC", //
"012abcABC ", //
"012abcABC ", //
"012abcABC ", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(1, i);
Render(screen, root);
EXPECT_EQ(expectations[i], rotate(screen.ToString()));
}
}
TEST(VBoxText, FlexGrow_NoFlex_FlewShrink) {
auto root = vbox({
vtext(L"012") | flex_grow,
vtext(L"abc"),
vtext(L"ABC") | flex_shrink,
});
std::vector<std::string> expectations = {
"", //
"0", //
"0a", //
"01a", //
"01ab", //
"012ab", //
"012abc", //
"012abcA", //
"012abcAB", //
"012abcABC", //
"012 abcABC", //
"012 abcABC", //
"012 abcABC", //
};
for (int i = 0; i < expectations.size(); ++i) {
Screen screen(1, i);
Render(screen, root);
EXPECT_EQ(expectations[i], rotate(screen.ToString()));
}
} }