ArthurSonzogni / FTXUI

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

How to display a multi-line string? #68

Closed b1060t closed 3 years ago

b1060t commented 3 years ago

I'd like to render a multi-line string which is scrollable to display logs. Is there any element I can use to achieve this?

ArthurSonzogni commented 3 years ago

Hello @Biobots!

I am not totally sure of what you want. Let me present the two potential solution:

static dom (non interactive)

You can use:

auto document = hbox(paragraph(long_string))

Try with the example: https://arthursonzogni.com/FTXUI/doc/_2examples_2dom_2paragraph_8cpp-example.html#a3

interactive component (using keyboard)

I don't have anything built-in. Here is an example you can adapt: https://github.com/ArthurSonzogni/chrome-log-beautifier/blob/master/src/ui/log_displayer.cpp#L28

Selected parts:

Element LogDisplayer::RenderLines(const std::vector<std::string>& lines) {
    Elements list;

    for (auto& line : lines) {
      Decorator line_decorator =  nothing;
      if (index++ == selected_) {
        if (Focused())
          line_decorator = focus | inverted;
        else
          line_decorator = focus;
      }
      list.push_back(text(line) | line_decorator)
   }
   return vbox(std::move(list)) | frame | border;
}

bool LogDisplayer::OnEvent(Event event) {
  if (!Focused())
    return false;

  int old_selected = selected_;
  if (event == Event::ArrowUp || event == Event::Character('k'))
    selected_--;
  if (event == Event::ArrowDown || event == Event::Character('j'))
    selected_++;
  if (event == Event::Tab && size)
    selected_ = (selected_ + 1) % size;
  if (event == Event::TabReverse && size)
    selected_ = (selected_ + size - 1) % size;

  selected_ = std::max(0, std::min(size-1, selected_));

  if (selected_ != old_selected) {
    return true;
  }

  return false;
}
b1060t commented 3 years ago

@ArthurSonzogni Thanks a lot! I'm trying the second solution now. However. there's another problem. It seems that in interactive mode, the 'render()' function is only called when there are some keyboard input. Is it possible to render the screen at a fixed speed like 30FPS since the output log should be automatically updated?

My logger's render function is called by logger.RenderLog() which is defined as:

class LogDisplayer : public Component
{
private:
    vector<string> _payload;
public:
    LogDisplayer() = default;
    void getLog(string str)
    {
        istringstream ss(str);
        string tmp;
        while(getline(ss, tmp))
        {
            _payload.push_back(tmp);
        }
    }
    Element RenderLog()
    {
        Elements list;
        for(int i = _payload.size() - 13; i < _payload.size(); i++)
        {
            Element doc = hbox({
                text(buildwstring(_payload[i])),
            }) | flex;
            list.push_back(doc);
        }
        if(list.empty()) list.push_back(text(L"empty"));
        return window(text(L"test"), vbox(list) | flex);
    }
};
ArthurSonzogni commented 3 years ago

You can post custom event using PostEvent and a Event::Custom. Those will refresh the view.

You usually want to send them after you know some data have been added. However, I guess in your case you are not actively waiting, but are checking periodically if there are updates. In this case, I guess you can create a new thread that will periodically send such events.

  std::thread update([&screen]() {
    for (;;) {
      using namespace std::chrono_literals;
      std::this_thread::sleep_for(0.05s);
      screen.PostEvent(Event::Custom);
    }
  });

From the example: https://github.com/ArthurSonzogni/FTXUI/blob/92ec5ab4ca0f71e042844fa8ac88bc086a1426a9/examples/component/homescreen.cpp#L370

b1060t commented 3 years ago

Actually I evoked a new thread to keep query for log needed. By adding PostEvent, it indeed worked as I expected.

system("clear");
auto screen = ScreenInteractive::FixedSize(100, 30);
MainWindow component;

thread t([&](){
    AsyncCommand cmd(std::string("logcat"));
    cmd.execute();
    while(!cmd.isDone())
    {
        string log = cmd.getOutput();
        if(log.length() > 0)
        {
            component.getLog(log);
            screen.PostEvent(Event::Custom);
        }
    }
});
t.detach();

screen.Loop(&component);

However, another problem I've met is that it's a bit unconvenient when the length of a sentence is greater than the maximum width of the screen. Though this can be implemented by the user, I'm still wondering if you are considering adding a built-in textarea which can automatically increase its height when there's not enough space to show one sentence without line break.

ArthurSonzogni commented 3 years ago

The difficulty is: The children pass their Requirement to their parent: https://github.com/ArthurSonzogni/FTXUI/blob/92ec5ab4ca0f71e042844fa8ac88bc086a1426a9/include/ftxui/dom/node.hpp#L27

Then the parent give them a Box where they are allowed to draw themselves. https://github.com/ArthurSonzogni/FTXUI/blob/92ec5ab4ca0f71e042844fa8ac88bc086a1426a9/include/ftxui/dom/node.hpp#L31

For the hflow layout, it is very difficult to provide a Requirement. Because the dependencies are inversed. We need to know the parent's final width to determine the children required height. For now, the hflow layout just require a 1x1 pixel to draw itself and declare themselves to be happy about being expanded if possible: https://github.com/ArthurSonzogni/FTXUI/blob/92ec5ab4ca0f71e042844fa8ac88bc086a1426a9/src/ftxui/dom/hflow.cpp#L13

I regret I don't have something built-in for this use case.

b1060t commented 3 years ago

Thanks! Another question:

This is how I define the movement of the window:

bool OnEvent(Event e)
{
    if(!Focused()) return false;

    if(e == Event::ArrowUp) _yoffset++;
    if(e == Event::ArrowDown) _yoffset--;
    if(e == Event::ArrowRight) _xoffset++;
    if(e == Event::ArrowLeft) _xoffset--;

    return true;
}

Once the component I defined is activated, I can never focus to the other part of the screen. I also tried hjkl to define the movement, but the the focused component still stays fixed using the arrow button.

ArthurSonzogni commented 3 years ago

I guess it depends on which component contains your component.

When the parent receives an event, it:

  1. Check if the active child can process the event. If yes, return.
  2. Process the event itself, which likely make the active child to become the next one.

Your component, as a children seems to accept any event.

See for instance the default implementation Container, which is used in most built-in component: https://github.com/ArthurSonzogni/FTXUI/blob/92ec5ab4ca0f71e042844fa8ac88bc086a1426a9/src/ftxui/component/container.cpp#L32

bool Container::OnEvent(Event event) {
  if (!Focused())
    return false;

  if (ActiveChild() && ActiveChild()->OnEvent(event))
    return true;

  return (this->*event_handler_)(event);
}

I guess in your case:

(ActiveChild() && ActiveChild()->OnEvent(event))

is always true when the ActiveChild() is your component.

Then this->*eventhandler is never called. So the current ActiveChild stays the same.

b1060t commented 3 years ago

Thanks. It seems that my OnEvent function is always returning true once focused. After fixing that, I'm able to enter the other component.