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 --- 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 4963ac5..17dff7c 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 661fee0..5d80fd8 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 0000000..462d7f7 --- /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 // for basic_string, string, allocator +#include // 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 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 1af03a5..d335fab 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 9847603..d9b7099 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 54249f5..73b8a0e 100644 --- a/include/ftxui/component/component_options.hpp +++ b/include/ftxui/component/component_options.hpp @@ -263,6 +263,21 @@ struct WindowOptions { std::function 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 open = false; + // The options for the checkbox: + CheckboxOption checkbox; + // The options for the radiobox: + RadioboxOption radiobox; + // The transformation function: + std::function + 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 f16dcf4..04a7f84 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 a90686e..e2de91c 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(*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(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 open_; + Ref selected_; Component checkbox_; Component radiobox_; }; - return Make(entries, selected); + return Make(option); } } // namespace ftxui diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index dc729ac..1e1125f 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) {