ArthurSonzogni / FTXUI

:computer: C++ Functional Terminal User Interface. :heart:
MIT License
6.49k stars 392 forks source link

Manage container output #762

Open rbrugo opened 9 months ago

rbrugo commented 9 months ago

Hello! I'm having some issue in display container output as I want.

Let's say I want to render some text like this:

#include <ftxui/component/component.hpp>
#include <ftxui/component/screen_interactive.hpp>

int main()
{
    auto const words = std::vector<std::string>{
        "one ", "two ", "three ", "four ", "five ", "six ", "seven ", "eight ", "nine ", "ten"
    };

    auto elements = ftxui::Elements{
        ftxui::text("Some text some text some text"),
        ftxui::Button("+1", [] {})->Render(),
        ftxui::Button("-1", [] {})->Render()
    };
    for (auto word : words) {
        elements.push_back(ftxui::text(word));
    }
    auto list = ftxui::Renderer([elements] {
        return ftxui::flexbox(elements);
    });

    auto screen = ftxui::ScreenInteractive::FixedSize(45, 10);
    screen.Loop(list); // | ftxui::border);  // or window
}

what I see is: the text continues on a newline as soon as it reaches the end of the page.

Some text some text some text┌──┐┌──┐one two
                             │+1││-1│
                             └──┘└──┘
three four five six seven eight nine ten

Now, I want to make the page dynamic: I want to render a number of elements of words based on the number of times the user pressed the button.

#include <ftxui/component/component.hpp>
#include <ftxui/component/screen_interactive.hpp>

int main()
{
    auto const words = std::vector<std::string>{
        "one ", "two ", "three ", "four ", "five ", "six ", "seven ", "eight ", "nine ", "ten"
    };
    auto const sz = static_cast<int>(words.size());

    auto value = 0;

    auto list = ftxui::Container::Horizontal({
        ftxui::Renderer([] { return ftxui::text("Some text some text some text"); }),
        ftxui::Button("+1", [&value, sz] { value = std::min(value + 1, sz); }),
        ftxui::Button("-1", [&value] { value = std::max(value - 1, 0); }),
        ftxui::Renderer([words, &value] {
            auto elems = std::vector<ftxui::Element>{};
            static auto to_text = [](auto txt) { return ftxui::text(txt); };
            std::transform(words.begin(), words.begin() + value, std::back_inserter(elems), to_text);
            return ftxui::hbox(std::move(elems));
        })
    });

    auto screen = ftxui::ScreenInteractive::FixedSize(45, 3);
    screen.Loop(list);
}

But now, if I press the +1 button until I get to three, I get the following output:

Some text some text some t┌──┐┌──┐one twothre
                          │+1││-1│
                          │  ││  │
                          │  ││  │
                          │  ││  │
                          │  ││  │
                          │  ││  │
                          │  ││  │
                          │  ││  │
                          └──┘└──┘

So part of the text disappears to fit in the screen and nothing gets on the new line.

Studying the source code, I noticed Container::Horizontal uses hbox internally, so I rolled a new container using flexbox and tried with it:

`flex_container` implementation, basycally a copy-paste of `ContainerBase` and `HorizontalContainer` ```cpp #include #include class flex_container_ : public ftxui::ComponentBase { public: flex_container_(ftxui::Components children, ftxui::FlexboxConfig config, int * selector) : _config{std::move(config)}, selector_(selector ? selector : &selected_) { for (auto & child : children) { Add(std::move(child)); } } // Component override. bool OnEvent(ftxui::Event event) override { if (event.is_mouse()) { return OnMouseEvent(event); } if (!Focused()) { return false; } if (ActiveChild() && ActiveChild()->OnEvent(event)) { return true; } return EventHandler(event); } auto ActiveChild() -> ftxui::Component override { if (children_.empty()) { return nullptr; } return children_[static_cast(*selector_) % children_.size()]; } void SetActiveChild(ComponentBase* child) override { for (size_t i = 0; i < children_.size(); ++i) { if (children_[i].get() == child) { *selector_ = static_cast(i); return; } } } protected: // Handlers virtual bool EventHandler(ftxui::Event event) { int const old_selected = *selector_; if (event == ftxui::Event::ArrowLeft or event == ftxui::Event::Character('h')) { MoveSelector(-1); } if (event == ftxui::Event::ArrowRight or event == ftxui::Event::Character('l')) { MoveSelector(+1); } if (event == ftxui::Event::PageUp) { for (int i = 0; i < box_.y_max - box_.y_min; ++i) { MoveSelector(-1); } } if (event == ftxui::Event::PageDown) { for (int i = 0; i < box_.y_max - box_.y_min; ++i) { MoveSelector(1); } } if (event == ftxui::Event::Home) { for (size_t i = 0; i < children_.size(); ++i) { MoveSelector(-1); } } if (event == ftxui::Event::End) { for (size_t i = 0; i < children_.size(); ++i) { MoveSelector(1); } } if (event == ftxui::Event::Tab) { MoveSelectorWrap(+1); } if (event == ftxui::Event::TabReverse) { MoveSelectorWrap(-1); } *selector_ = std::max(0, std::min(int(children_.size()) - 1, *selector_)); return old_selected != *selector_; } virtual bool OnMouseEvent(ftxui::Event event) { return ftxui::ComponentBase::OnEvent(std::move(event)); } void MoveSelector(int dir) { for (int i = *selector_ + dir; i >= 0 && i < int(children_.size()); i += dir) { if (children_[i]->Focusable()) { *selector_ = i; return; } } } void MoveSelectorWrap(int dir) { if (children_.empty()) { return; } for (size_t offset = 1; offset < children_.size(); ++offset) { const size_t i = (*selector_ + offset * dir + children_.size()) % children_.size(); if (children_[i]->Focusable()) { *selector_ = int(i); return; } } } auto Render() -> ftxui::Element override { auto elements = ftxui::Elements{}; elements.reserve(children_.size()); for (auto& it : children_) { elements.push_back(it->Render()); } if (elements.empty()) { return ftxui::text("Empty container") | ftxui::reflect(box_); } return ftxui::flexbox(std::move(elements), _config) | ftxui::reflect(box_); } ftxui::FlexboxConfig _config; int selected_ = 0; int * selector_ = nullptr; ftxui::Box box_; }; inline auto flex_container(ftxui::Components children, ftxui::FlexboxConfig config, int * selector) -> ftxui::Component { return std::make_shared(std::move(children), std::move(config), selector); } inline auto flex_container(ftxui::Components children, ftxui::FlexboxConfig config = {}) -> ftxui::Component { return std::make_shared(std::move(children), std::move(config), nullptr); } ```

The code is basically the same:

int main()
{
    auto const words = std::vector<std::string>{
        "one ", "two ", "three ", "four ", "five ", "six ", "seven ", "eight ", "nine ", "ten"
    };
    auto const sz = static_cast<int>(words.size());

    auto value = 0;

    auto list = ftxui::Container::Vertical({
        flex_container({
            ftxui::Renderer([] { return ftxui::text("Some text some text some text"); }),
            ftxui::Button("+1", [&value, sz] { value = std::min(value + 1, sz); }),
            ftxui::Button("-1", [&value] { value = std::max(value - 1, 0); }),
            ftxui::Renderer([words, &value] {
                auto elems = std::vector<ftxui::Element>{};
                static auto to_text = [](auto txt) { return ftxui::text(txt); };
                std::transform(words.begin(), words.begin() + value, std::back_inserter(elems), to_text);
                return ftxui::hbox(std::move(elems));
            })
        }),
        ftxui::Renderer([]{ return ftxui::text("_"); })
    });

    auto screen = ftxui::ScreenInteractive::TerminalOutput();
    screen.Loop(list);
}

What happens is that now, when I increment the value, my elements goes to a new line - but the go there as a whole! (I had to add a Vertical because without it the new line is not rendered, but that's not the point; the rest of the code is the same)

At this point I'm not sure about how to get the result I want. I tried to find a way to "unpack" the Renderer output into the flex_container (basically get {some_text ... some_text btn1 btn2 one two ... ten} instead of {{some_text ... some_text} btn1 btn2 {one two ... ten}}), but got no luck, and now I'm out of ideas.

Do you have any suggestion?

ArthurSonzogni commented 8 months ago

I see you are rendering:

flexbox(
  [...],
  hbox([...])
)

The hbox is displayed as a whole inside the flexbox. So when it doesn't fit the current line, the whole hbox is moved to the next line.

It seems to me you would like to unpack the elements from the hbox and happened them to the flexbox.

ArthurSonzogni commented 8 months ago

What about using this:

https://github.com/ArthurSonzogni/FTXUI/assets/4759106/22741ede-e42f-469a-93d5-1f0275ab81ed

#include <memory>  // for shared_ptr, __shared_ptr_access
#include <string>  // for operator+, to_string

#include <ftxui/component/component.hpp>
#include <ftxui/component/screen_interactive.hpp>
#include "ftxui/component/captured_mouse.hpp"  // for ftxui
#include "ftxui/component/component.hpp"  // for Button, Horizontal, Renderer
#include "ftxui/component/component_base.hpp"      // for ComponentBase
#include "ftxui/component/screen_interactive.hpp"  // for ScreenInteractive
#include "ftxui/dom/elements.hpp"  // for separator, gauge, text, Element, operator|, vbox, border
using namespace ftxui;

int main() {
  auto const words = std::vector<std::string>{
      "one", "two",   "three", "four", "five",
      "six", "seven", "eight", "nine", "ten",
  };
  auto const sz = static_cast<int>(words.size());

  auto value = 0;

  auto btn1 = Button("-1", [&value] { value = std::max(value - 1, 0); });
  auto btn2 = Button("+1", [&value, sz] { value = std::min(value + 1, sz); });

  auto layout = Container::Vertical({
      btn1,
      btn2,
  });

  auto renderer = Renderer(layout, [&] {
    Elements elements = {
        text("Some"), text("text"), text("some"),   text("text"),
        text("some"), text("text"), btn1->Render(), btn2->Render(),
    };
    std::transform(words.begin(), words.begin() + value,
                   std::back_inserter(elements),
                   [](auto txt) { return text(txt); });

    FlexboxConfig config;
    config.gap_x = 1;
    return flexbox(std::move(elements), config);
  });

  int resize = 10;
  Component empty = Renderer([] { return text("Empty"); });
  auto final_renderer = ResizableSplitLeft(renderer, empty, &resize);

  auto screen = ftxui::ScreenInteractive::Fullscreen();
  screen.Loop(final_renderer);
}