From 26e26fd41a4c7c938d4a97c7106746b27cc4520a Mon Sep 17 00:00:00 2001 From: Arthur Sonzogni Date: Thu, 5 Aug 2021 22:40:40 +0200 Subject: [PATCH] Add non focusable components. (#172) --- include/ftxui/component/component_base.hpp | 5 +++ src/ftxui/component/button.cpp | 4 ++ src/ftxui/component/checkbox.cpp | 4 ++ src/ftxui/component/component.cpp | 12 ++++++ src/ftxui/component/container.cpp | 35 ++++++++++++++---- src/ftxui/component/container_test.cpp | 43 ++++++++++++++-------- src/ftxui/component/input.cpp | 5 +++ src/ftxui/component/menu.cpp | 4 ++ src/ftxui/component/radiobox.cpp | 4 ++ src/ftxui/component/slider.cpp | 4 ++ src/ftxui/component/toggle.cpp | 2 + 11 files changed, 99 insertions(+), 23 deletions(-) diff --git a/include/ftxui/component/component_base.hpp b/include/ftxui/component/component_base.hpp index 78e380c..7d8fe71 100644 --- a/include/ftxui/component/component_base.hpp +++ b/include/ftxui/component/component_base.hpp @@ -51,6 +51,11 @@ class ComponentBase { // root component contains this object. virtual Component ActiveChild(); + // Return true when the component contains focusable elements. + // The non focusable Component will be skipped when navigating using the + // keyboard. + virtual bool Focusable() const; + // Whether this is the active child of its parent. bool Active() const; // Whether all the ancestors are active. diff --git a/src/ftxui/component/button.cpp b/src/ftxui/component/button.cpp index aa35e25..889bcad 100644 --- a/src/ftxui/component/button.cpp +++ b/src/ftxui/component/button.cpp @@ -53,6 +53,10 @@ class ButtonBase : public ComponentBase { return false; } + bool Focusable() const final { + return true; + } + private: ConstStringRef label_; std::function on_click_; diff --git a/src/ftxui/component/checkbox.cpp b/src/ftxui/component/checkbox.cpp index cf242d7..de46ccc 100644 --- a/src/ftxui/component/checkbox.cpp +++ b/src/ftxui/component/checkbox.cpp @@ -71,6 +71,10 @@ class CheckboxBase : public ComponentBase { return false; } + bool Focusable() const final { + return true; + } + ConstStringRef label_; bool* const state_; Box box_; diff --git a/src/ftxui/component/component.cpp b/src/ftxui/component/component.cpp index 8c9ba56..9d89259 100644 --- a/src/ftxui/component/component.cpp +++ b/src/ftxui/component/component.cpp @@ -106,6 +106,18 @@ Component ComponentBase::ActiveChild() { return children_.empty() ? nullptr : children_.front(); } +/// @brief Return true when the component contains focusable elements. +/// The non focusable Components will be skipped when navigating using the +/// keyboard. +/// @ingroup component +bool ComponentBase::Focusable() const { + for (const Component& child : children_) { + if (child->Focusable()) + return true; + } + return false; +} + /// @brief Returns if the element if the currently active child of its parent. /// @ingroup component bool ComponentBase::Active() const { diff --git a/src/ftxui/component/container.cpp b/src/ftxui/component/container.cpp index 45e441e..3c2008c 100644 --- a/src/ftxui/component/container.cpp +++ b/src/ftxui/component/container.cpp @@ -63,6 +63,25 @@ class ContainerBase : public ComponentBase { int selected_ = 0; int* selector_ = nullptr; + + void MoveSelector(int dir) { + for (int i = *selector_ + dir; i >= 0 && i < (int)children_.size(); + i += dir) { + if (children_[i]->Focusable()) { + *selector_ = i; + return; + } + } + } + void MoveSelectorWrap(int dir) { + for (size_t offset = 1; offset < children_.size(); ++offset) { + int i = (*selector_ + offset * dir + children_.size()) % children_.size(); + if (children_[i]->Focusable()) { + *selector_ = i; + return; + } + } + } }; class VerticalContainer : public ContainerBase { @@ -81,13 +100,13 @@ class VerticalContainer : public ContainerBase { bool EventHandler(Event event) override { int old_selected = *selector_; if (event == Event::ArrowUp || event == Event::Character('k')) - (*selector_)--; + MoveSelector(-1); if (event == Event::ArrowDown || event == Event::Character('j')) - (*selector_)++; + MoveSelector(+1); if (event == Event::Tab && children_.size()) - *selector_ = (*selector_ + 1) % children_.size(); + MoveSelectorWrap(+1); if (event == Event::TabReverse && children_.size()) - *selector_ = (*selector_ + children_.size() - 1) % children_.size(); + MoveSelectorWrap(-1); *selector_ = std::max(0, std::min(int(children_.size()) - 1, *selector_)); return old_selected != *selector_; @@ -110,13 +129,13 @@ class HorizontalContainer : public ContainerBase { bool EventHandler(Event event) override { int old_selected = *selector_; if (event == Event::ArrowLeft || event == Event::Character('h')) - (*selector_)--; + MoveSelector(-1); if (event == Event::ArrowRight || event == Event::Character('l')) - (*selector_)++; + MoveSelector(+1); if (event == Event::Tab && children_.size()) - *selector_ = (*selector_ + 1) % children_.size(); + MoveSelectorWrap(+1); if (event == Event::TabReverse && children_.size()) - *selector_ = (*selector_ + children_.size() - 1) % children_.size(); + MoveSelectorWrap(-1); *selector_ = std::max(0, std::min(int(children_.size()) - 1, *selector_)); return old_selected != *selector_; diff --git a/src/ftxui/component/container_test.cpp b/src/ftxui/component/container_test.cpp index 04d6f95..edf6044 100644 --- a/src/ftxui/component/container_test.cpp +++ b/src/ftxui/component/container_test.cpp @@ -10,14 +10,24 @@ using namespace ftxui; +Component Focusable() { + return Button(L"", [] {}); +} +Component NonFocusable() { + return Container::Horizontal({}); +} + TEST(ContainerTest, HorizontalEvent) { auto container = Container::Horizontal({}); - auto c0 = Container::Horizontal({}); - auto c1 = Container::Horizontal({}); - auto c2 = Container::Horizontal({}); + auto c0 = Focusable(); + auto c1 = Focusable(); + auto c2 = Focusable(); container->Add(c0); container->Add(c1); + container->Add(NonFocusable()); + container->Add(NonFocusable()); container->Add(c2); + container->Add(NonFocusable()); // With arrow key. EXPECT_EQ(container->ActiveChild(), c0); @@ -85,12 +95,15 @@ TEST(ContainerTest, HorizontalEvent) { TEST(ContainerTest, VerticalEvent) { auto container = Container::Vertical({}); - auto c0 = Container::Horizontal({}); - auto c1 = Container::Horizontal({}); - auto c2 = Container::Horizontal({}); + auto c0 = Focusable(); + auto c1 = Focusable(); + auto c2 = Focusable(); container->Add(c0); container->Add(c1); + container->Add(NonFocusable()); + container->Add(NonFocusable()); container->Add(c2); + container->Add(NonFocusable()); // With arrow key. EXPECT_EQ(container->ActiveChild(), c0); @@ -158,9 +171,9 @@ TEST(ContainerTest, VerticalEvent) { TEST(ContainerTest, SetActiveChild) { auto container = Container::Horizontal({}); - auto c0 = Container::Horizontal({}); - auto c1 = Container::Horizontal({}); - auto c2 = Container::Horizontal({}); + auto c0 = Focusable(); + auto c1 = Focusable(); + auto c2 = Focusable(); container->Add(c0); container->Add(c1); container->Add(c2); @@ -215,12 +228,12 @@ TEST(ContainerTest, TakeFocus) { auto c1 = Container::Vertical({}); auto c2 = Container::Vertical({}); auto c3 = Container::Vertical({}); - auto c11 = Container::Horizontal({}); - auto c12 = Container::Horizontal({}); - auto c13 = Container::Horizontal({}); - auto c21 = Container::Horizontal({}); - auto c22 = Container::Horizontal({}); - auto c23 = Container::Horizontal({}); + auto c11 = Focusable(); + auto c12 = Focusable(); + auto c13 = Focusable(); + auto c21 = Focusable(); + auto c22 = Focusable(); + auto c23 = Focusable(); c->Add(c1); c->Add(c2); diff --git a/src/ftxui/component/input.cpp b/src/ftxui/component/input.cpp index e62b12c..061a106 100644 --- a/src/ftxui/component/input.cpp +++ b/src/ftxui/component/input.cpp @@ -163,6 +163,11 @@ class InputBase : public ComponentBase { } return true; } + + bool Focusable() const final { + return true; + } + StringRef content_; ConstStringRef placeholder_; diff --git a/src/ftxui/component/menu.cpp b/src/ftxui/component/menu.cpp index a824566..8c97bee 100644 --- a/src/ftxui/component/menu.cpp +++ b/src/ftxui/component/menu.cpp @@ -106,6 +106,10 @@ class MenuBase : public ComponentBase { return false; } + bool Focusable() const final { + return entries_->size(); + } + int& focused_entry() { return option_->focused_entry(); } protected: diff --git a/src/ftxui/component/radiobox.cpp b/src/ftxui/component/radiobox.cpp index 5834857..d61fe30 100644 --- a/src/ftxui/component/radiobox.cpp +++ b/src/ftxui/component/radiobox.cpp @@ -119,6 +119,10 @@ class RadioboxBase : public ComponentBase { return false; } + bool Focusable() const final { + return entries_->size(); + } + int& focused_entry() { return option_->focused_entry(); } const std::vector* const entries_; diff --git a/src/ftxui/component/slider.cpp b/src/ftxui/component/slider.cpp index 0f61567..da5f2b7 100644 --- a/src/ftxui/component/slider.cpp +++ b/src/ftxui/component/slider.cpp @@ -84,6 +84,10 @@ class SliderBase : public ComponentBase { return false; } + bool Focusable() const final { + return true; + } + private: StringRef label_; T* value_; diff --git a/src/ftxui/component/toggle.cpp b/src/ftxui/component/toggle.cpp index a82429f..1368c8e 100644 --- a/src/ftxui/component/toggle.cpp +++ b/src/ftxui/component/toggle.cpp @@ -107,6 +107,8 @@ class ToggleBase : public ComponentBase { return false; } + bool Focusable() const final { return entries_->size(); } + int& focused_entry() { return option_->focused_entry(); } const std::vector* const entries_;