ArthurSonzogni / FTXUI

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

Catching events for nested components #898

Open MarcelFerrari opened 1 month ago

MarcelFerrari commented 1 month ago

Hi everyone,

I am trying to understand how the CatchEvent decorator interacts with components and with the Render() method.

I am writing a program that has multiple tabs containing lots of information and I have split each tab into its own class. Each class has a construct() method which returns a Component object containing all the data representing that tab. This component may contain CatchEvent decorators that are only relevant for that specific tab. However, it seems like events are not forwarded to child components whenever the Render() method is called.

I include a small example to reproduce the issue.

This code works absolutely fine

// FTXUI includes
#include <ftxui/component/captured_mouse.hpp>
#include <ftxui/component/component.hpp>
#include <ftxui/component/component_base.hpp>
#include <ftxui/component/screen_interactive.hpp>
#include <ftxui/dom/elements.hpp>

using namespace ftxui;

class ExampleUI
{
public:
    Component render_example_tab()
    {
        Component rax = Renderer([&] {
        return text("N recorded events: " + std::to_string(n_events));
        });

        rax |= CatchEvent([&](Event event) {
            if (event == Event::ArrowDown) {
                n_events++;
                return true;
            } else if (event == Event::ArrowUp) {
                n_events--;
                return true;
            }
            return false; });

        return std::move(rax);
    }

    private:
        int n_events = 0;
};

int main()
{
    // Init UI class
    ExampleUI example_ui;

    auto screen = ScreenInteractive::Fullscreen();

    Component display = example_ui.render_example_tab();

    screen.Loop(display);
    return 0;
}

This one however fails to register the catch event calls:

// FTXUI includes
#include <ftxui/component/captured_mouse.hpp>
#include <ftxui/component/component.hpp>
#include <ftxui/component/component_base.hpp>
#include <ftxui/component/screen_interactive.hpp>
#include <ftxui/dom/elements.hpp>

using namespace ftxui;

class ExampleUI
{
public:
    Component render_example_tab()
    {
        Component rax = Renderer([&] {
        return text("N recorded events: " + std::to_string(n_events));
        });

        rax |= CatchEvent([&](Event event) {
            if (event == Event::ArrowDown) {
                n_events++;
                return true;
            } else if (event == Event::ArrowUp) {
                n_events--;
                return true;
            }
            return false; });

        return std::move(rax);
    }

    private:
        int n_events = 0;
};

int main()
{
    // Init UI class
    ExampleUI example_ui;

    auto screen = ScreenInteractive::Fullscreen();

    Component display = example_ui.render_example_tab();

    Component frame = Renderer([&]{
        return window(text("Example UI"), display->Render());
    });

    screen.Loop(frame);
    return 0;
}

From what I understand calling the Render() method transforms components into "static" elements before any events are caught and handled.

How can one go about achieving the functionality I want?

Many thanks in advance,

Marcel

ArthurSonzogni commented 1 month ago

Each component defines how it routes events. Typically, most implementations are:


In your case, the problem was using:

    Component frame = Renderer([&]{
        return window(text("Example UI"), display->Render());
    });

instead of:

    Component frame = Renderer(display, [&]{
        return window(text("Example UI"), display->Render());
    });

So, you create a brand new Component, without forwarding events to display. The second form does "decorate" the wrapped component, by overriding only its "Render()" function, but still forwarding everything else.

MarcelFerrari commented 1 month ago

Thank you for the prompt reply! I understand now. I looked up the documentation and found the Component Renderer(Component child, std::function<Element()> render) function. In the case of multiple children, e.g. when rendering multiple components into a vbox, I assume it is possible to wrap all the child components into e.g. a stacked container in order to propagate events, even if the container is not rendered itself, right?