ArthurSonzogni / FTXUI

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

Dynamic manipulation of components in container #750

Open pralad-p opened 10 months ago

pralad-p commented 10 months ago

Hi @ArthurSonzogni!

Problem

Consider the case where I am using a (Vertical/Horizontal) Container as a wrapper for other components within it. Simple enough, right? But what about the use-case when the number of inner components in this Container is not always the same (changes during program execution). Well, then I need to use the Add and Detach to pinpoint the container's children that I want to dynamically add/remove (as you suggested here to maintain interactivity). Well, now consider the case that I have specific event handlers (like checking the focus of an inner component) that inherently rely on the order of components. Surely enough, for simple cases, using the indices should suffice but when the num. of components increase, I need to keep track of all the indices to account for the change in component order/modification of the container.

Brainstorming

I wanted to bounce some ideas off you before working on this. I was thinking of having a DynamicComponent class implemented like this:

class DynamicComponent : public ftxui::ComponentBase {
public:
    DynamicComponent (ftxui::Component component, const std::string& ident)
        : baseComponent(component), ident(ident) {}
    // ...

    std::string ident;
private:
    ftxui::Component baseComponent;
};

or rather compose directly from a Component,

class DynamicComponent {
public:
    DynamicComponent (ftxui::Component component, const std::string& ident)
        : component(component), ident(ident) {}

    ftxui::Component GetComponent() { return component; }
    std::string ident;
private:
    ftxui::Component component;
};

There's also the approach to directly modify ComponentBase's design so each inheriting Component has access to this new identifier (if they choose to use it). Once the property is available from an arbitrary Component, then it is a matter of extending the Container classes to handle validation and lookup/modification of these dynamic components. What do you think?

ArthurSonzogni commented 10 months ago

Hello! Thanks for opening this!

I am not sure to understand the proposed solution.

I would like to keep FTXUI functional. The UI must be a function of the data. The user shouldn't have to synchronize the UI when the data change and vice-versa. Unfortunately, this is not easy with non garbage collected languages like C++.

I am thinking about this:

// A Component representing a for loop over data.
// - The data is a vector of elements of type T.
// - The mapper is a function of T -> Component.
// - The reducer is a function of Components -> Component.
//
// Example 1:
// ```cpp
//  std::vector<std::shared_ptr<std::string>> data = {
//      std::make_shared("A"),
//      std::make_shared("B"),
//      std::make_shared("C"),
//  };
//  
//  auto component = For({
//    &container,
//    Checkbox,
//    Container::Vertical
//  );
// ```
template <typename T>
For(std::vector<std::shared_ptr<T>>* data,
    std::function<Component(T*)> map,
    std::function<Component(std::vector<Component>)> reduce);

The user would provide:

  1. list of data. The data must be wrapped with std::shared_ptr to have an address that isn't invalidated when the list is resized.
  2. A data -> component mapping function. This could be user defined, or directly some builtin function like Checkbox.
  3. A components -> component function to reduce the list of component into one. User can use the builtin Container::Vertical or Container::Horizontal here.

The returned component would track when the list's element are added/remove, use the map function to regenerate the corresponding component. Then the reduce function to merge them.

What do you think?

pralad-p commented 9 months ago

Ah I see, keeping it completely functional. Indeed, I think that's much more robust (and reduces a lot of the boilerplate I have currently for handling each component state). I would be grateful if this could be implemented (or if you could provide pointers, I could help with implementation).