ArthurSonzogni / FTXUI

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

Customize menu items #194

Closed MattBystrin closed 2 years ago

MattBystrin commented 3 years ago

Hello, and thanks for the lib again) Is there any built-in way to customize (i.e. change color ) particular item in menu ?

First approach on my mind is to add MenuOptions for each entry, but it implies some changes in MenuBase. If it is a necessary feature I can later send code for review, of course if it is not already implemented.

Thanks!

ArthurSonzogni commented 3 years ago

Hello! I will give a more detailed answer tonight.

I was thinking about maybe something like:


auto item_1 = Renderer([] (bool focused) {
  if (focused)
    return text("> item_1") | bgcolor(Color::Red);
  else
    return text("  item_1")
})

auto item_2 = Renderer([] (bool focused) {
  if (focused)
    return text("> item_2") | bgcolor(Color::Blue);
  else
    return text("  item_2")
})

int selected_item = 0;
auto menu = Container::Vertical({
  item_1,
  item_2,
  &selected_item);

It has some drawback, but you can render anything inside the menu.

MattBystrin commented 3 years ago

Thanks for reply ! Today I came up with 2 possible solutions. Thirst one is to pass std::pair<std::string,ftxui::MenuOption> as menu entry, and it's close to solution you provide in some case. Another one is to modify MenuBase class a little bit. First of all declare something like this:

using MenuOptions = std::vector<MenuOption>;

And then pass this instead of MenuOption.

MenuBase(ConstStringListRef entries, int* selected, Ref<MenuOptions> options)

And edit Render a little bit

  Element Render() {
    Elements elements;
    bool is_menu_focused = Focused();
    boxes_.resize(entries_.size());
    for (size_t i = 0,j = 0; i < entries_.size(); ++i) {
      bool is_focused = (focused_entry() == int(i)) && is_menu_focused;
      bool is_selected = (*selected_ == int(i));

      auto style = is_selected ? (is_focused ? options_[i]->style_selected_focused
                                             : options_[i]->style_selected)
                               : (is_focused ? options_[i]->style_focused
                                             : options_[i]->style_normal);
      auto focus_management = !is_selected      ? nothing
                              : is_menu_focused ? focus
                                                : select;
      auto icon = is_selected ? "> " : "  ";
      elements.push_back(text(icon + entries_[i]) | style | focus_management |
                         reflect(boxes_[i]));
      if (j < options_.size())  j++;
    }
    return vbox(std::move(elements));
  }

The advantage of this solution is that it provides flexible customization of each menu entry. As you can see, if you pass less options than entries, the remaining entries would "inherit" style of last option you give. So you can pass only one option and things will work in the old way ( some kind of 'backward compatibility" ).

@ArthurSonzogni, what is your thoughts about that ?

ArthurSonzogni commented 3 years ago

I think I would love something composable.


auto menu = Container::Vertical({
  MenuEntry("text"),
  MenuEntry("text", menu_entry_options_1),
  MenuEntry("text", menu_entry_options_2),
  MenuEntry("text", menu_entry_options_2),
  MenuEntry("text", menu_entry_options_1),
 }, &menu_entry_selected);

So adding the "MenuEntry" component that would behave similarly to what we see inside a menu.


I can also add the alias:

Component Menu(Components entries, int* selected_entry) {
  return Container::Vertical(std::move(entries), selected_entry)
}

So that one could write:

auto menu = Menu({
  MenuEntry("text"),
  MenuEntry("text", menu_entry_options_1),
  MenuEntry("text", menu_entry_options_2),
  MenuEntry("text", menu_entry_options_2),
  MenuEntry("text", menu_entry_options_1),
 }, &menu_entry_selected);
MattBystrin commented 3 years ago

Sorry, missclicked to close the issue.

I think I would love something composable.

auto menu = Container::Vertical({
  MenuEntry("text"),
  MenuEntry("text", menu_entry_options_1),
  MenuEntry("text", menu_entry_options_2),
  MenuEntry("text", menu_entry_options_2),
  MenuEntry("text", menu_entry_options_1),
 }, &menu_entry_selected);

So adding the "MenuEntry" component that would behave similarly to what we see inside a menu.

I can also add the alias:

Component Menu(Components entries, int* selected_entry) {
  return Container::Vertical(std::move(entries), selected_entry)
}

So that one could write:

auto menu = Menu({
  MenuEntry("text"),
  MenuEntry("text", menu_entry_options_1),
  MenuEntry("text", menu_entry_options_2),
  MenuEntry("text", menu_entry_options_2),
  MenuEntry("text", menu_entry_options_1),
 }, &menu_entry_selected);

Well, that is even more intuitive solution, actually i like it so much. Thanks so much for replies ! Waiting for updates !

stefandxm commented 3 years ago

I am using the menu component to show lists of log messages. They're all dynamic and uses a FIFO based "entries" vector. Even though the component might not be designed for this use case, in general dynamic menus are very common in applications.

I believe that the solutions provided by @MattBystrin would work.

Maybe we need different paths or components since I like both suggestions but for dynamic content I cannot see how to make use of the one suggested by @ArthurSonzogni .

I am also not sure if all these simplified solutions will work in the long run, maybe a callback for rendering is needed to be able to compose a more dynamic view (like icons/animations etc).

MattBystrin commented 3 years ago

@stefandxm , recently I made up some text non-selectable text scroller, based on Arthurs'. If you interested in, write me on email.

stefandxm commented 3 years ago

@MattBystrin iam interested in that for the details of the log item but for the log viewer itself i need something like the menu component since i use it to select logfile and log entry.

A video of the UI i made is available at https://www.youtube.com/watch?v=kieYzp5owds and the repo is at https://github.com/stefandxm/nglogger (the ngloggerui folder is the ui in the video). Please dont hang me for the UI code, I didnt have time to read up properly on the toolkit nor to write perfect code. But any pointers on how to use this library better is greatly appreciated.

ArthurSonzogni commented 3 years ago

Hey @MattBystrin and @stefandxm,

I added the MenuEntry component and improve support for scrolling using mouse wheel. Would you have any feedback?

MattBystrin commented 3 years ago

Thank you @ArthurSonzogni ! Gonna test it on the weekend and give a feedback

MattBystrin commented 2 years ago

@ArthurSonzogni sorry for not giving feedback for so long. Now I've just tested your example and view the code. I think it is quiet inconvenient to change the style dynamically, because I have to pass vector by value. In my opinion it would be much comfortable with Menu interface - one vector with all MenuEntires which we can pass by reference.

ArthurSonzogni commented 2 years ago

With FTXUI, you can pass almost everything by value or by pointer.

For instance:

{
  MenuEntryOption style_1;
  MenuEntryOption style_2;

  int selected = 0;
  auto menu = Container::Vertical(
      {
          MenuEntry(" 1. improve", &style_1),
          MenuEntry(" 2. tolerant", &style_2),
          MenuEntry(" 3. career", &style_1),
          MenuEntry(" 4. cast", &style_2),
      },
      &selected);

   // Side effect:
   style_1.style_normal = Decorator(color(Color::Red));
}  
stefandxm commented 2 years ago

Hey @MattBystrin and @stefandxm,

I added the MenuEntry component and improve support for scrolling using mouse wheel. Would you have any feedback?

I will definitely check it out but I am in a tight release right now so I will have to come back with feedback.