From 3c9fa60d28fb0e4cd1c29d215a7bcea1b4002c15 Mon Sep 17 00:00:00 2001
From: James <56827968+jeptechnology@users.noreply.github.com>
Date: Sat, 6 Apr 2024 16:45:10 +0100
Subject: [PATCH] Feature: Dropdown options with callback (#826)

Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
---
 CHANGELOG.md                                  |   1 +
 examples/component/CMakeLists.txt             |   1 +
 examples/component/dropdown_custom.cpp        | 104 ++++++++++++++++
 examples/component/homescreen.cpp             |  10 +-
 include/ftxui/component/component.hpp         |   2 +
 include/ftxui/component/component_options.hpp |  15 +++
 src/ftxui/component/button.cpp                |   6 +-
 src/ftxui/component/dropdown.cpp              | 111 +++++++++++-------
 src/ftxui/component/screen_interactive.cpp    |   8 +-
 9 files changed, 202 insertions(+), 56 deletions(-)
 create mode 100644 examples/component/dropdown_custom.cpp

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4963ac57..17dff7c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,7 @@ current (development)
 ### Component
 - Feature: Add support for `Input`'s insert mode. Add `InputOption::insert`
   option. Added by @mingsheng13.
+- Feature: Add `DropdownOption` to configure the dropdown. See #826.
 - Bugfix/Breaking change: `Mouse transition`:
   - Detect when the mouse move, as opposed to being pressed.
     The Mouse::Moved motion was added.
diff --git a/examples/component/CMakeLists.txt b/examples/component/CMakeLists.txt
index 661fee06..5d80fd8d 100644
--- a/examples/component/CMakeLists.txt
+++ b/examples/component/CMakeLists.txt
@@ -11,6 +11,7 @@ example(collapsible)
 example(composition)
 example(custom_loop)
 example(dropdown)
+example(dropdown_custom)
 example(flexbox_gallery)
 example(focus)
 example(focus_cursor)
diff --git a/examples/component/dropdown_custom.cpp b/examples/component/dropdown_custom.cpp
new file mode 100644
index 00000000..462d7f7b
--- /dev/null
+++ b/examples/component/dropdown_custom.cpp
@@ -0,0 +1,104 @@
+// Copyright 2020 Arthur Sonzogni. All rights reserved.
+// Use of this source code is governed by the MIT license that can be found in
+// the LICENSE file.
+#include <string>  // for basic_string, string, allocator
+#include <vector>  // for vector
+
+#include "ftxui/component/captured_mouse.hpp"  // for ftxui
+#include "ftxui/component/component.hpp"  // for Dropdown, Horizontal, Vertical
+#include "ftxui/component/screen_interactive.hpp"  // for ScreenInteractive
+
+int main() {
+  using namespace ftxui;
+
+  std::vector<std::string> entries = {
+      "tribute",     "clearance", "ally",        "bend",        "electronics",
+      "module",      "era",       "cultural",    "sniff",       "nationalism",
+      "negotiation", "deliver",   "figure",      "east",        "tribute",
+      "clearance",   "ally",      "bend",        "electronics", "module",
+      "era",         "cultural",  "sniff",       "nationalism", "negotiation",
+      "deliver",     "figure",    "east",        "tribute",     "clearance",
+      "ally",        "bend",      "electronics", "module",      "era",
+      "cultural",    "sniff",     "nationalism", "negotiation", "deliver",
+      "figure",      "east",
+  };
+
+  auto dropdown_1 = Dropdown({
+      .radiobox = {.entries = &entries},
+      .transform =
+          [](bool open, Element checkbox, Element radiobox) {
+            if (open) {
+              return vbox({
+                  checkbox | inverted,
+                  radiobox | vscroll_indicator | frame |
+                      size(HEIGHT, LESS_THAN, 10),
+                  filler(),
+              });
+            }
+            return vbox({
+                checkbox,
+                filler(),
+            });
+          },
+  });
+
+  auto dropdown_2 = Dropdown({
+      .radiobox = {.entries = &entries},
+      .transform =
+          [](bool open, Element checkbox, Element radiobox) {
+            if (open) {
+              return vbox({
+                  checkbox | inverted,
+                  radiobox | vscroll_indicator | frame |
+                      size(HEIGHT, LESS_THAN, 10) | bgcolor(Color::Blue),
+                  filler(),
+              });
+            }
+            return vbox({
+                checkbox | bgcolor(Color::Blue),
+                filler(),
+            });
+          },
+  });
+
+  auto dropdown_3 = Dropdown({
+      .radiobox =
+          {
+              .entries = &entries,
+              .transform =
+                  [](const EntryState& s) {
+                    auto t = text(s.label) | borderEmpty;
+                    if (s.active) {
+                      t |= bold;
+                    }
+                    if (s.focused) {
+                      t |= inverted;
+                    }
+                    return t;
+                  },
+          },
+      .transform =
+          [](bool open, Element checkbox, Element radiobox) {
+            checkbox |= borderEmpty;
+            if (open) {
+              return vbox({
+                  checkbox | inverted,
+                  radiobox | vscroll_indicator | frame |
+                      size(HEIGHT, LESS_THAN, 20) | bgcolor(Color::Red),
+                  filler(),
+              });
+            }
+            return vbox({
+                checkbox | bgcolor(Color::Red),
+                filler(),
+            });
+          },
+  });
+
+  auto screen = ScreenInteractive::FitComponent();
+  screen.Loop(Container::Horizontal({
+      dropdown_1,
+      dropdown_2,
+      dropdown_3,
+  }));
+}
diff --git a/examples/component/homescreen.cpp b/examples/component/homescreen.cpp
index 1af03a5d..d335fabf 100644
--- a/examples/component/homescreen.cpp
+++ b/examples/component/homescreen.cpp
@@ -494,11 +494,11 @@ int main() {
       "Exit", [&] { screen.Exit(); }, ButtonOption::Animated());
 
   auto main_container = Container::Vertical({
-    Container::Horizontal({
-        tab_selection,
-        exit_button,
-    }),
-    tab_content,
+      Container::Horizontal({
+          tab_selection,
+          exit_button,
+      }),
+      tab_content,
   });
 
   auto main_renderer = Renderer(main_container, [&] {
diff --git a/include/ftxui/component/component.hpp b/include/ftxui/component/component.hpp
index 98476030..d9b70998 100644
--- a/include/ftxui/component/component.hpp
+++ b/include/ftxui/component/component.hpp
@@ -75,6 +75,8 @@ Component Radiobox(ConstStringListRef entries,
                    RadioboxOption options = {});
 
 Component Dropdown(ConstStringListRef entries, int* selected);
+Component Dropdown(DropdownOption options);
+
 Component Toggle(ConstStringListRef entries, int* selected);
 
 // General slider constructor:
diff --git a/include/ftxui/component/component_options.hpp b/include/ftxui/component/component_options.hpp
index 54249f5d..73b8a0e0 100644
--- a/include/ftxui/component/component_options.hpp
+++ b/include/ftxui/component/component_options.hpp
@@ -263,6 +263,21 @@ struct WindowOptions {
   std::function<Element(const WindowRenderState&)> render;
 };
 
+/// @brief Option for the Dropdown component.
+/// @ingroup component
+/// A dropdown menu is a checkbox opening/closing a radiobox.
+struct DropdownOption {
+  /// Whether the dropdown is open or closed:
+  Ref<bool> open = false;
+  // The options for the checkbox:
+  CheckboxOption checkbox;
+  // The options for the radiobox:
+  RadioboxOption radiobox;
+  // The transformation function:
+  std::function<Element(bool open, Element checkbox, Element radiobox)>
+      transform;
+};
+
 }  // namespace ftxui
 
 #endif /* end of include guard: FTXUI_COMPONENT_COMPONENT_OPTIONS_HPP */
diff --git a/src/ftxui/component/button.cpp b/src/ftxui/component/button.cpp
index f16dcf44..04a7f84b 100644
--- a/src/ftxui/component/button.cpp
+++ b/src/ftxui/component/button.cpp
@@ -104,7 +104,7 @@ class ButtonBase : public ComponentBase, public ButtonOption {
 
     // TODO(arthursonzogni): Consider posting the task to the main loop, instead
     // of invoking it immediately.
-    on_click(); // May delete this.
+    on_click();  // May delete this.
   }
 
   bool OnEvent(Event event) override {
@@ -113,7 +113,7 @@ class ButtonBase : public ComponentBase, public ButtonOption {
     }
 
     if (event == Event::Return) {
-      OnClick(); // May delete this.
+      OnClick();  // May delete this.
       return true;
     }
     return false;
@@ -130,7 +130,7 @@ class ButtonBase : public ComponentBase, public ButtonOption {
     if (event.mouse().button == Mouse::Left &&
         event.mouse().motion == Mouse::Pressed) {
       TakeFocus();
-      OnClick(); // May delete this.
+      OnClick();  // May delete this.
       return true;
     }
 
diff --git a/src/ftxui/component/dropdown.cpp b/src/ftxui/component/dropdown.cpp
index a90686e5..e2de91c7 100644
--- a/src/ftxui/component/dropdown.cpp
+++ b/src/ftxui/component/dropdown.cpp
@@ -20,79 +20,102 @@ namespace ftxui {
 /// @param entries The list of entries to display.
 /// @param selected The index of the selected entry.
 Component Dropdown(ConstStringListRef entries, int* selected) {
-  class Impl : public ComponentBase {
+  DropdownOption option;
+  option.radiobox.entries = entries;
+  option.radiobox.selected = selected;
+  return Dropdown(option);
+}
+
+/// @brief A dropdown menu.
+/// @ingroup component
+/// @param option The options for the dropdown.
+Component Dropdown(DropdownOption option) {
+  class Impl : public ComponentBase, public DropdownOption {
    public:
-    Impl(ConstStringListRef entries, int* selected)
-        : entries_(entries), selected_(selected) {
-      CheckboxOption option;
-      option.transform = [](const EntryState& s) {
-        auto prefix = text(s.state ? "↓ " : "→ ");  // NOLINT
-        auto t = text(s.label);
-        if (s.active) {
-          t |= bold;
-        }
-        if (s.focused) {
-          t |= inverted;
-        }
-        return hbox({prefix, t});
-      };
-      checkbox_ = Checkbox(&title_, &show_, option);
-      radiobox_ = Radiobox(entries_, selected_);
+    Impl(DropdownOption option) : DropdownOption(std::move(option)) {
+      FillDefault();
+      checkbox_ = Checkbox(checkbox);
+      radiobox_ = Radiobox(radiobox);
 
       Add(Container::Vertical({
           checkbox_,
-          Maybe(radiobox_, &show_),
+          Maybe(radiobox_, checkbox.checked),
       }));
     }
 
     Element Render() override {
-      *selected_ = util::clamp(*selected_, 0, int(entries_.size()) - 1);
-      title_ = entries_[static_cast<size_t>(*selected_)];
-      if (show_) {
-        const int max_height = 12;
-        return vbox({
-                   checkbox_->Render(),
-                   separator(),
-                   radiobox_->Render() | vscroll_indicator | frame |
-                       size(HEIGHT, LESS_THAN, max_height),
-               }) |
-               border;
-      }
+      radiobox.selected =
+          util::clamp(radiobox.selected(), 0, int(radiobox.entries.size()) - 1);
+      checkbox.label =
+          radiobox.entries[static_cast<size_t>(radiobox.selected())];
 
-      return vbox({
-          checkbox_->Render() | border,
-          filler(),
-      });
+      return transform(*open_, checkbox_->Render(), radiobox_->Render());
     }
 
     // Switch focus in between the checkbox and the radiobox when selecting it.
     bool OnEvent(ftxui::Event event) override {
-      const bool show_old = show_;
-      const int selected_old = *selected_;
+      const bool show_old = open_();
+      const int selected_old = selected_();
       const bool handled = ComponentBase::OnEvent(event);
 
-      if (!show_old && show_) {
+      if (!show_old && open_()) {
         radiobox_->TakeFocus();
       }
 
-      if (selected_old != *selected_) {
+      if (selected_old != selected_()) {
         checkbox_->TakeFocus();
-        show_ = false;
+        open_ = false;
       }
 
       return handled;
     }
 
+    void FillDefault() {
+      open_ = std::move(checkbox.checked);
+      selected_ = std::move(radiobox.selected);
+      checkbox.checked = &*open_;
+      radiobox.selected = &*selected_;
+
+      if (!checkbox.transform) {
+        checkbox.transform = [](const EntryState& s) {
+          auto prefix = text(s.state ? "↓ " : "→ ");  // NOLINT
+          auto t = text(s.label);
+          if (s.active) {
+            t |= bold;
+          }
+          if (s.focused) {
+            t |= inverted;
+          }
+          return hbox({prefix, t});
+        };
+      }
+
+      if (!transform) {
+        transform = [](bool open, Element checkbox_element,
+                       Element radiobox_element) {
+          if (open) {
+            const int max_height = 12;
+            return vbox({
+                       checkbox_element,
+                       separator(),
+                       radiobox_element | vscroll_indicator | frame |
+                           size(HEIGHT, LESS_THAN, max_height),
+                   }) |
+                   border;
+          }
+          return vbox({checkbox_element, filler()}) | border;
+        };
+      }
+    }
+
    private:
-    ConstStringListRef entries_;
-    bool show_ = false;
-    int* selected_;
-    std::string title_;
+    Ref<bool> open_;
+    Ref<int> selected_;
     Component checkbox_;
     Component radiobox_;
   };
 
-  return Make<Impl>(entries, selected);
+  return Make<Impl>(option);
 }
 
 }  // namespace ftxui
diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp
index dc729acb..1e1125f7 100644
--- a/src/ftxui/component/screen_interactive.cpp
+++ b/src/ftxui/component/screen_interactive.cpp
@@ -848,13 +848,13 @@ void ScreenInteractive::Draw(Component component) {
     reset_cursor_position.clear();
 
     if (dy != 0) {
-        set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
-        reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
+      set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
+      reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
     }
 
     if (dx != 0) {
-        set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
-        reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
+      set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
+      reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
     }
 
     if (cursor_.shape == Cursor::Hidden) {
-- 
GitLab