Closed Zjl37 closed 2 years ago
Thanks for using FTXUI! Yes, this is a known issue. Suggestions would be welcome!
The layout algorithm is very simple: https://github.com/ArthurSonzogni/FTXUI/blob/master/include/ftxui/dom/node.hpp
// Step 1: Compute layout requirement. Tell parent what dimensions this
// element wants to be.
// Propagated from Children to Parents.
virtual void ComputeRequirement();
Requirement requirement() { return requirement_; }
// Step 2: Assign this element its final dimensions.
// Propagated from Parents to Children.
virtual void SetBox(Box box);
This works well for the flexbox layout: hbox
, vbox
.
Then, I experimented with hflow
. The difficulty here is that computing the requirements. The required height (computed on phase 1), depends on the assigned width (computed in phase 2). This doesn't really fit with the model, because the dependencies are the other way around.
See https://github.com/ArthurSonzogni/FTXUI/blob/master/src/ftxui/dom/hflow.cpp#L18
As a result, hflow
only ask for 1x1 pixel + flexibility. We hope developers will reserve them enough height using size
for the element to be displayed fully.
void ComputeRequirement() override {
requirement_.min_x = 1;
requirement_.min_y = 1;
requirement_.flex_grow_x = 1;
requirement_.flex_grow_y = 1;
requirement_.flex_shrink_x = 0;
requirement_.flex_shrink_y = 0;
for (auto& child : children_)
child->ComputeRequirement();
}
Would you have ideas how to modify the current algorithm to make flow layout to work better?
Maybe we can run:
hflow
layout)hflow
layout:)@ArthurSonzogni Thanks for the explanation!
However, this brought new problem when a flexbox contains both height-depends-on-width element and width-depends-on-height element. I don't know how to handle this.
Can you add label help wanted
and try to invite more people to discussion? I have limited time and feels helpless.
I took ~2 weeks of reflection and I am now ready to build something for supporting this feature. Wish me luck ;-)
Day 1: We are starting to see something! Here is an example with several complicated flow layout within vertical and horizontal flex box:
https://user-images.githubusercontent.com/4759106/141368881-3d570e6e-045e-4239-9fe6-45a6c91ba343.mp4
Exciting to see that!
Actually I also looked into the code and tried to think of a practical and detailed approach. But as I also want to take "width-depends-on-height" elements into account (say, we might have "vflow" in the future), I found it very difficult.
Can you explain what changes you have currently made to the layout system?
Thanks!
The problem with the "with-depends-on-height" algorithm above are:
Here is my WIP patch: https://github.com/ArthurSonzogni/FTXUI/commit/8197591a2d6afef900e41862c701800c9fadf4e1
What I did was defining a new optional signal going through the elements, indicating the current iteration and asking if a new iteration is needed. I used a struct, in order to be extendable in the future, without breaking the API.
// Layout may not resolve within a single iteration for some elements. This
// allows them to request additionnal iterations. This signal must be
// forwarded to children at least once.
struct Status {
int iteration = 0;
bool need_iteration = false;
};
virtual void Check(Status* status);
Default implementation is:
void Node::Check(Status* status) {
for (auto& child : children_)
child->Check(status);
status->need_iteration |= (status->iteration == 0);
}
Which propagate the signal to every element, and return it needs one iteration of the layout algorithm.
The Render algorithm become:
/// @brief Display an element on a ftxui::Screen.
/// @ingroup dom
void Render(Screen& screen, Node* node) {
Box box;
box.x_min = 0;
box.y_min = 0;
box.x_max = screen.dimx() - 1;
box.y_max = screen.dimy() - 1;
Node::Status status;
node->Check(&status);
while (status.need_iteration && status.iteration < 20) {
// Step 1: Find what dimension this elements wants to be.
node->ComputeRequirement();
// Step 2: Assign a dimension to the element.
node->SetBox(box);
// Check if the element needs another iteration of the layout algorithm.
status.need_iteration = false;
status.iteration++;
node->Check(&status);
}
// Step 3: Draw the element.
screen.stencil = box;
node->Render(screen);
// Step 4: Apply shaders
screen.ApplyShader();
}
The, we have to provide an alternative implementation for the hflow
element. At iteration zero, it is configured with dim_x_to_ask = infinity
. In ComputeRequirement
, it returns width=dim_x_to_ask and height computed given its width. In SetBox
, it reduces dim_x_to_ask
if given less than asked and starts a new iteration.
There are still a lot of work for convincing me this is good enough. I will have to work on some tests.
Hmm, after looking though your implementation, I found it quite different to what I thought, and I doubt that multiple iteration be reliable.
I would like to post my solution here for a reference:
EDITED
struct Requirement
to indicate whether the node's "height is depend on width" and its "width is depend on height", let's call it "dimension dependency" for now, call the values "HDW" and "WDH" for short.class Node
-- virtual void ComputeRequirement()
and virtual void SetBox(Box box);
is broken into the following several methods: (the name not sure yet, and the actual way to implement may differ)
virtual void ComputeRequirementBegin()
, to compute "dimension dependency", propagated bottom-up.virtual void ComputeDimension(<** an enum value that can be COMPUTE_X, COMPUTE_Y or COMPUTE_XY **>)
virtual void SetDimension(std::optional<int> x, std::optional<int> y)
virtual void ComputeRequirementEnd()
, to compute selection and selected box.Render
procedure: consequently call the root element's ComputeRequirementBegin()
, ComputeDimension(COMPUTE_XY)
, SetDimension(<both x and y>)
, ComputeRequirementEnd()
.ComputeRequirementBegin
called.ComputeDimension
called for the first time, they ask for a size when no wrap occurs (as if there is infinite space in the flow direction). This size is usually very big, and is useful later.flex-grow
, but only flex-shrink
in the flow direction. This way we can make use of the existing shrink size calculation.SetDimension(x, y)
:
SetDimension(x, std::nullopt_t)
, then call their ComputeDimension(COMPUTE_Y)
, then subtract these required height (and fixed size children's height) from y
, take it as the remaining vertical space. Most probably this remaining vertical space is not enough for other WDH and (HDW | WDH) children(, if any), so flex-shrink happens, and dispatch the remaining vertical space to these children (proportionally, according to the first time requirement). Call SetDimension(x, <disy>)
on them. As for those HDW children, their height is usually what they required.SetDimension(x, std::nullopt_t)
will only be called on HDW elements (will not be called on WDH or (HDW | WDH) elements), and SetDimension(std::nullopt_t, y)
will only be called on WDH elements.Node
derived class will need rewrite.I noticed that, in the second video you posted, the nested hbox's width is always about twice as the remaining space in that row, which I'm aware that previous version can't do (, and my solution can't do either). (I mean the nested hbox and the paragraph left to it are just two elements of a hbox. Normally when both's flex-grow
is set, they should have 1:1 width.) Can you explain this? Is this some side effect?
Thanks @Zjl37 for the feedback!
Indeed, what I prototyped yesterday is still a prototype. Definitively not ready yet.
Maybe I can learn from Chrome's implementation for new ideas.
I took a look at your proposition. I don't understand fully for now. I will try taking more time to get it. I think it would help if you could show on a simple example how this works recursively.
question
I noticed that, in the second video you posted, the nested hbox's width is always about twice as the remaining space in that row, which I'm aware that previous version can't do (, and my solution can't do either). (I mean the nested hbox and the paragraph left to it are just two elements of a hbox. Normally when both's
flex-grow
is set, they should have 1:1 width.) Can you explain this? Is this some side effect?
It is the side effect of the current algoritm implementing vbox
and hbox
. See:
https://github.com/ArthurSonzogni/FTXUI/blob/master/src/ftxui/dom/box_helper.cpp
Note that there are two kind of flexibility. The flexibility to shrink and the flexibility to grow.
When the size we got is lower than what we asked to render the full hbox
's children, and dispatching the negative space into shrinkable
children won't be enough, we need to dispatch the negative space into normal elements. We don't take any flex coefficients into account, because they aren't shrinkable, we just remove from what they asked the negative space proportionally to what they require.
In our case when the children are hflow
elements, they asked at iteration=0 width=infinity
on the left and the groups of elements on the right asked right 2×`infinity.
As a result, we dispatch proportionally the negative space on the two sides. As a results, they conserve the 1/3 2/3 ratios.
// Called when the size allowed is lower than the requested size, and the
// shrinkable element can not absorbe the (negative) extra_space. This assign
// zero to shrinkable elements and distribute the remaining (negative)
// extra_space toward the other non shrinkable elements.
void ComputeShrinkHard(std::vector<Element>* elements,
int extra_space,
int size) {
for (Element& element : *elements) {
if (element.flex_shrink) {
element.size = 0;
continue;
}
int added_space = extra_space * element.min_size / std::max(1, size);
extra_space -= added_space;
size -= element.min_size;
element.size = element.min_size + added_space;
}
}
One interesting video: https://www.youtube.com/watch?v=SyUBuio_ooE&t=1592s
Hi @ArthurSonzogni , thanks for the explanation on size calculation. Now I understand. In fact, I'm not familiar with web, and when I went reading CSS flexbox tutorials, I found some similarity: the required width in first iteration in your prototype is like flex-basis
in CSS, except that in CSS it's not set to infinity for paragraphs, but instead max-content
size(, as suggested in this MDN document).
The size calculation is not a key point here, but we may keep an eye for it when evaluating solutions.
Indeed, what I prototyped yesterday is still a prototype. Definitively not ready yet.
I see.
I would like to support vertical flow and a few other variations. Here is a tentative API to describe this:
Element flow(Elements, FlowConfig)
struct FlowConfig {
enum Direction {
Row,
RowInversed,
Column,
ColumnInversed,
};
enum Wrap {
NoWrap,
Wrap,
WrapInversed,
}
enum Align {
Start,
Center,
End
Stretch,
SpaceBetween,
SpaceAround,
SpaceEvenly,
}
Direction direction = Row;
Wrap wrap = Wrap;
Align align_main_axis = Start;
Align align_cross_axis = Start;
int gap_main_axis = 0;
int gap_cross_axis = 0;
// Constructor pattern. For use like:
// ```
// FlowConfig()
// .SetDirection(FlowConfig::Row)
// .SetWrap(FlowConfig::Wrap);
// ```
FlowConfig& SetDirection(FlowConfig::Direction direction);
FlowConfig& SetWrap(FlowConfig::Wrap wrap);
FlowConfig& SetAlignMainAxis(FlowConfig::Align align);
FlowConfig& SetAlignCrossAxis(FlowConfig::Align align);
FlowConfig& SetGapMainAxis(int gap);
FlowConfig& SetGapCrossAxis(int gap);
}
I just modified and edited my proposition, which looks clearer and more practical. Please have a look again.
Sorry for this long week without any update...
I worked on implementing all the possible flexbox layout. Having a way to test both height-depends-on-width and width-depends-on-height is going to be very helpful building test and selecting the right algorithm.
https://user-images.githubusercontent.com/4759106/142773234-641eb35d-279a-40b3-922c-af09dc4cc026.mp4
https://user-images.githubusercontent.com/4759106/142773245-cbf9fdb8-0da0-40ba-9966-6b47ff7966ff.mp4
https://user-images.githubusercontent.com/4759106/142773252-bad4b52e-7cee-47d7-8993-0b09620bec47.mp4
https://user-images.githubusercontent.com/4759106/142773248-e60adf13-31b5-4bf3-b732-4d8249981138.mp4
I made a component to test the different flexbox options: WIP:
https://user-images.githubusercontent.com/4759106/142777071-f21b2a6b-b845-46be-bee6-ba1ae747f3e0.mp4
Some new videos. I will soon be able to publish it.
https://user-images.githubusercontent.com/4759106/145106956-7f360ad2-d948-40f6-ad61-962a650e17b8.mp4
https://user-images.githubusercontent.com/4759106/145107473-af45759b-99dd-4710-8a3c-66b5cd257221.mp4
@Zjl37, I landed the flexbox change.
This brings the following elements:
flexbox
And the following built from flexbox
:paragraph
paragraphAlignLeft
paragraphAlignRight
paragraphAlignCenter
paragraphAlignJustify
hflow
vflow
This is a breaking change and it will be included inside version 2.0
Could you please let's give it a try and give your opinions?
(Marking this as fixed)
Hi @ArthurSonzogni , it's exciting to see the feature landed!
From the user's view, the change is great and inspiring, everything working as expected, and I'll of course happily use it. However, as I have studied the code, I'm mostly concerned about the core changes to Node
-- the many-iteration approach:
In some cases, the current implementation unnecessarily slows down the size calculation process, which could have took only 2 iterations in theory if we know more information. More specifically, this happens when
The "many iteration" approach hides the real demand of an element, which sounds like a defective design. Its intent is not clear at first sight. I mean when someone wants to implement a new kind of element, they might write not functioning code more easily. And that passing Node::Status
to children's Check
by pointer -- what if they change int iteration
?
If this is for extensibility, I can't think of any other circumstances that needs more than 2 iterations.
In all, this change is good enough, but looks not perfect to me. If there are other thoughts behind this choice, or you can convince me this is better, please tell me. I am actually willing to wait a longer time for a more perfect solution.
I agree this is not ideal.
This can even give some unexpected results when using nested flexbox.
I should aim for better before releasing 2.0. However I am not sure how. I know there is the solution you kindly provided in: https://github.com/ArthurSonzogni/FTXUI/issues/247#issuecomment-967122836 but I can't really convince it works or won't, and I am afraid breaking existing users to much.
Maybe I should ask directly Chromium engineer, or try to learn from other C++ projects like: https://yogalayout.com/
The main algorith is: https://github.com/facebook/yoga/blob/main/yoga/Yoga.cpp
At first glance, it looks a bit complex to understand. This is a ~4300 algorithm. Mine is 14× shorter. Maybe I can learn something useful from this.
and I am afraid breaking existing users to much.
I see.
May I ask what is a version 2.0? We're now in 0.11, are we jumping across major version 1?
May I ask what is a version 2.0? We're now in 0.11, are we jumping across major version 1?
The flexbox change modified the behavior of hbox
and paragraph
. Updating FTXUI will break folks using it. So I have to move from 0.11 to 1.0
(Yes, I said 2.0 wrongly)
I was enjoying the amazing library when I got crazy to find out that...
There is no proper way I can make two paragraphs (with word wrap) adjacent!
I've tried...
this: ```cpp auto si = ScreenInteractive::Fullscreen(); auto ui = Renderer([]() { return vbox({ text("... title goes here ..."), hflow(paragraph(" To celebrate Dragon Boat Festival, an underwater dance performance titled Pray was aired on Henan TV last Saturday night, the first day of the holiday.")), hflow(paragraph(" Featuring the goddess of Luo River – a mysterious beauty best known in the poetry of Cao Zhi during the Three Kingdoms period, the dancer Haohao He, a former synchronized swimmer, recreated the elegance of this ancient Chinese goddess.")), }); }); si.Loop(ui); ``` ... which spares the vertical space evenly for the two paragraphs, and this: ```cpp auto ui = Container::Vertical({ Renderer([]() { return text("... title goes here ..."); }), Renderer([]() { return vbox({ hflow(paragraph(" To celebrate Dragon Boat Festival, an underwater dance performance titled Pray was aired on Henan TV last Saturday night, the first day of the holiday.")), hflow(paragraph(" Featuring the goddess of Luo River – a mysterious beauty best known in the poetry of Cao Zhi during the Three Kingdoms period, the dancer Haohao He, a former synchronized swimmer, recreated the elegance of this ancient Chinese goddess.")), }); }) }); ``` ... where paragraphs collapse and only one line is visible, with a weird `g` at the top left corner: ``` g.. title goes here ... To celebrate Dragon Boat Festival, an underwater dance performance titled Featuring the goddess of Luo River – a mysterious beauty best known in the ```And I notices this: https://github.com/ArthurSonzogni/FTXUI/issues/68#issuecomment-762807087
So FTXUI is missing an important and very basic feature for displaying longer texts, and the solution seems not clear.
I think a discussion about solution is needed here.