diff --git a/CHANGELOG.md b/CHANGELOG.md index b23b5882070bccee6b590563503cecd9fac4c35f..b6fbdc92ec5b5c81df7843ba1379799bf11b12b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ current (development) mouse. - Bugfix: Add implementation of `ButtonOption::Border()`. It was missing. - Bugfix: Provide the correct key for F1-F4 and F11. +- Feature: Add the `Hoverable` component decorators. ### Screen - Feature: add `Box::Union(a,b) -> Box` diff --git a/CMakeLists.txt b/CMakeLists.txt index 4f885630f41c6c371b6ae1cdc80fd018ddfe7ce0..f457e485538ab85fd87138d50ee86e0196a1acda 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -108,6 +108,7 @@ add_library(component src/ftxui/component/container.cpp src/ftxui/component/dropdown.cpp src/ftxui/component/event.cpp + src/ftxui/component/hoverable.cpp src/ftxui/component/input.cpp src/ftxui/component/loop.cpp src/ftxui/component/maybe.cpp diff --git a/cmake/ftxui_test.cmake b/cmake/ftxui_test.cmake index 0382e6a0b56a2c2f3cdc1f30a133908e5aa93a63..4f8225984c107fb88ce464270aafa19ec8b4a45c 100644 --- a/cmake/ftxui_test.cmake +++ b/cmake/ftxui_test.cmake @@ -29,14 +29,15 @@ add_executable(tests src/ftxui/component/component_test.cpp src/ftxui/component/component_test.cpp src/ftxui/component/container_test.cpp + src/ftxui/component/hoverable_test.cpp src/ftxui/component/input_test.cpp src/ftxui/component/menu_test.cpp src/ftxui/component/modal_test.cpp src/ftxui/component/radiobox_test.cpp src/ftxui/component/receiver_test.cpp - src/ftxui/component/slider_test.cpp src/ftxui/component/resizable_split_test.cpp src/ftxui/component/screen_interactive_test.cpp + src/ftxui/component/slider_test.cpp src/ftxui/component/terminal_input_parser_test.cpp src/ftxui/component/toggle_test.cpp src/ftxui/dom/blink_test.cpp diff --git a/include/ftxui/component/component.hpp b/include/ftxui/component/component.hpp index 80f48ed7e16dd725d2070877ef0fdeea5ee6b286..7eb33bcf2d33f62d1b193afc9627d6ba0d23a45c 100644 --- a/include/ftxui/component/component.hpp +++ b/include/ftxui/component/component.hpp @@ -110,6 +110,18 @@ ComponentDecorator Modal(Component modal, const bool* show_modal); Component Collapsible(ConstStringRef label, Component child, Ref<bool> show = false); + +Component Hoverable(Component component, bool* hover); +Component Hoverable(Component component, + std::function<void()> on_enter, + std::function<void()> on_leave); +Component Hoverable(Component component, // + std::function<void(bool)> on_change); +ComponentDecorator Hoverable(bool* hover); +ComponentDecorator Hoverable(std::function<void()> on_enter, + std::function<void()> on_leave); +ComponentDecorator Hoverable(std::function<void(bool)> on_change); + } // namespace ftxui #endif /* end of include guard: FTXUI_COMPONENT_HPP */ diff --git a/src/ftxui/component/hoverable.cpp b/src/ftxui/component/hoverable.cpp new file mode 100644 index 0000000000000000000000000000000000000000..56ce275913ad063590f88714932c22edc51a02ef --- /dev/null +++ b/src/ftxui/component/hoverable.cpp @@ -0,0 +1,171 @@ +#include <memory> // for shared_ptr +#include <utility> // for move + +#include "ftxui/component/component.hpp" // for Make, Button +#include "ftxui/component/component_base.hpp" // for ComponentBase +#include "ftxui/component/event.hpp" // for Event, Event::Return +#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Left, Mouse::Pressed +#include "ftxui/component/screen_interactive.hpp" // for Component +#include "ftxui/dom/elements.hpp" // for operator|, Decorator, Element, operator|=, bgcolor, color, reflect, text, bold, border, inverted, nothing +#include "ftxui/screen/box.hpp" // for Box +#include "ftxui/screen/color.hpp" // for Color +#include "ftxui/util/ref.hpp" // for Ref, ConstStringRef + +namespace ftxui { + +namespace { + +void Post(std::function<void()> f) { + if (auto* screen = ScreenInteractive::Active()) { + screen->Post(std::move(f)); + return; + } + f(); +} + +} // namespace + +/// @brief Wrap a component. Gives the ability to know if it is hovered by the +/// mouse. +/// @param component: The wrapped component. +/// @param hover: The value to reflect whether the component is hovered or not. +/// @ingroup component +/// +/// ### Example +/// +/// ```cpp +/// auto button = Button("exit", screen.ExitLoopClosure()); +/// bool hover = false; +/// auto button_hover = Hoverable(button, &hover); +/// ``` +Component Hoverable(Component component, bool* hover) { + class Impl : public ComponentBase { + public: + Impl(Component component, bool* hover) + : component_(component), hover_(hover) { + Add(component_); + } + + private: + Element Render() override { + return ComponentBase::Render() | reflect(box_); + } + + bool OnEvent(Event event) override { + if (event.is_mouse()) { + *hover_ = box_.Contain(event.mouse().x, event.mouse().y) && + CaptureMouse(event); + } + + return ComponentBase::OnEvent(event); + } + + Component component_; + bool* hover_; + Box box_; + }; + + return Make<Impl>(component, hover); +} + +/// @brief Wrap a component. Gives the ability to know if it is hovered by the +/// mouse. +/// @param component: The wrapped component. +/// @param hover: The value to reflect whether the component is hovered or not. +/// @ingroup component +/// +/// ### Example +/// +/// ```cpp +/// auto button = Button("exit", screen.ExitLoopClosure()); +/// bool hover = false; +/// auto button_hover = Hoverable(button, &hover); +/// ``` +Component Hoverable(Component component, + std::function<void()> on_enter, + std::function<void()> on_leave) { + class Impl : public ComponentBase { + public: + Impl(Component component, + std::function<void()> on_enter, + std::function<void()> on_leave) + : component_(std::move(component)), + on_enter_(std::move(on_enter)), + on_leave_(std::move(on_leave)) { + Add(component_); + } + + private: + Element Render() override { + return ComponentBase::Render() | reflect(box_); + } + + bool OnEvent(Event event) override { + if (event.is_mouse()) { + bool hover = box_.Contain(event.mouse().x, event.mouse().y) && + CaptureMouse(event); + if (hover != hover_) { + Post(hover ? on_enter_ : on_leave_); + } + hover_ = hover; + } + + return ComponentBase::OnEvent(event); + } + + Component component_; + Box box_; + bool hover_ = false; + std::function<void()> on_enter_; + std::function<void()> on_leave_; + }; + + return Make<Impl>(component, on_enter, on_leave); +} + +/// @brief Wrap a component. Gives the ability to know if it is hovered by the +/// mouse. +/// @param hover: The value to reflect whether the component is hovered or not. +/// @ingroup component +/// +/// ### Example +/// +/// ```cpp +/// bool hover = false; +/// auto button = Button("exit", screen.ExitLoopClosure()); +/// button |= Hoverable(&hover); +/// ``` +ComponentDecorator Hoverable(bool* hover) { + return [hover](Component component) { return Hoverable(component, hover); }; +} + +/// @brief Wrap a component. Two callback can be used to know when the mouse +/// enter and leave its area. +ComponentDecorator Hoverable(std::function<void()> on_enter, + std::function<void()> on_leave) { + return [on_enter, on_leave](Component component) { + return Hoverable(component, on_enter, on_leave); + }; +} + +Component Hoverable(Component component, std::function<void(bool)> on_change) { + return Hoverable( + component, // + [on_change] { on_change(true); }, // + [on_change] { on_change(false); } // + ); +} + +/// @brief Wrap a component. Two callback can be used to know when the mouse +/// enter and leave its area. +ComponentDecorator Hoverable(std::function<void(bool)> on_change) { + return [on_change](Component component) { + return Hoverable(component, on_change); + }; +} + +} // namespace ftxui + +// Copyright 2022 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. diff --git a/src/ftxui/component/hoverable_test.cpp b/src/ftxui/component/hoverable_test.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ea04a7771882d3b6f0fb6a70f2cb83efe39d0745 --- /dev/null +++ b/src/ftxui/component/hoverable_test.cpp @@ -0,0 +1,192 @@ +#include <gtest/gtest.h> +#include <memory> // for __shared_ptr_access, shared_ptr, allocator +#include <string> // for string + +#include "ftxui/component/component.hpp" // for Input +#include "ftxui/component/component_base.hpp" // for ComponentBase, Component +#include "ftxui/component/event.hpp" // for Event, Event::ArrowLeft, Event::ArrowRight, Event::Backspace, Event::Delete, Event::End, Event::Home +#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::Button, Mouse::Left, Mouse::Motion, Mouse::Pressed +#include "ftxui/dom/node.hpp" // for Render +#include "ftxui/screen/screen.hpp" // for Screen + +namespace ftxui { + +namespace { +Event HoverEvent(int x, int y) { + Mouse mouse; + mouse.button = Mouse::Left; + mouse.motion = Mouse::Released; + mouse.shift = false; + mouse.meta = false; + mouse.control = false; + mouse.x = x; + mouse.y = y; + return Event::Mouse("jjj", mouse); +} + +Component BasicComponent() { + return Renderer([] { return text("[ ]"); }); +} + +TEST(HoverableTest, BasicBool) { + bool hover_1 = false; + bool hover_2 = false; + auto c1 = Hoverable(BasicComponent(), &hover_1); + auto c2 = Hoverable(BasicComponent(), &hover_2); + auto layout = Container::Horizontal({c1, c2}); + auto screen = Screen(8, 2); + Render(screen, layout->Render()); + EXPECT_FALSE(hover_1); + EXPECT_FALSE(hover_2); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(0, 0))); + EXPECT_TRUE(hover_1); + EXPECT_FALSE(hover_2); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(1, 0))); + EXPECT_TRUE(hover_1); + EXPECT_FALSE(hover_2); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(2, 0))); + EXPECT_TRUE(hover_1); + EXPECT_FALSE(hover_2); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(3, 0))); + EXPECT_FALSE(hover_1); + EXPECT_TRUE(hover_2); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(0, 0))); + EXPECT_TRUE(hover_1); + EXPECT_FALSE(hover_2); +} + +TEST(HoverableTest, BasicCallback) { + int on_enter_1 = 0; + int on_enter_2 = 0; + int on_leave_1 = 0; + int on_leave_2 = 0; + auto c1 = Hoverable( + BasicComponent(), [&] { on_enter_1++; }, [&] { on_leave_1++; }); + auto c2 = Hoverable( + BasicComponent(), [&] { on_enter_2++; }, [&] { on_leave_2++; }); + auto layout = Container::Horizontal({c1, c2}); + auto screen = Screen(8, 2); + Render(screen, layout->Render()); + EXPECT_EQ(on_enter_1, 0); + EXPECT_EQ(on_enter_2, 0); + EXPECT_EQ(on_leave_1, 0); + EXPECT_EQ(on_leave_2, 0); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(0, 0))); + EXPECT_EQ(on_enter_1, 1); + EXPECT_EQ(on_enter_2, 0); + EXPECT_EQ(on_leave_1, 0); + EXPECT_EQ(on_leave_2, 0); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(1, 0))); + EXPECT_EQ(on_enter_1, 1); + EXPECT_EQ(on_enter_2, 0); + EXPECT_EQ(on_leave_1, 0); + EXPECT_EQ(on_leave_2, 0); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(2, 0))); + EXPECT_EQ(on_enter_1, 1); + EXPECT_EQ(on_enter_2, 0); + EXPECT_EQ(on_leave_1, 0); + EXPECT_EQ(on_leave_2, 0); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(3, 0))); + EXPECT_EQ(on_enter_1, 1); + EXPECT_EQ(on_enter_2, 1); + EXPECT_EQ(on_leave_1, 1); + EXPECT_EQ(on_leave_2, 0); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(0, 0))); + EXPECT_EQ(on_enter_1, 2); + EXPECT_EQ(on_enter_2, 1); + EXPECT_EQ(on_leave_1, 1); + EXPECT_EQ(on_leave_2, 1); +} + +TEST(HoverableTest, BasicBoolCallback) { + bool hover_1 = false; + bool hover_2 = false; + auto c1 = Hoverable(BasicComponent(), [&](bool hover) { hover_1 = hover; }); + auto c2 = Hoverable(BasicComponent(), [&](bool hover) { hover_2 = hover; }); + auto layout = Container::Horizontal({c1, c2}); + auto screen = Screen(8, 2); + Render(screen, layout->Render()); + EXPECT_FALSE(hover_1); + EXPECT_FALSE(hover_2); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(0, 0))); + EXPECT_TRUE(hover_1); + EXPECT_FALSE(hover_2); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(1, 0))); + EXPECT_TRUE(hover_1); + EXPECT_FALSE(hover_2); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(2, 0))); + EXPECT_TRUE(hover_1); + EXPECT_FALSE(hover_2); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(3, 0))); + EXPECT_FALSE(hover_1); + EXPECT_TRUE(hover_2); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(0, 0))); + EXPECT_TRUE(hover_1); + EXPECT_FALSE(hover_2); +} + +TEST(HoverableTest, Coverage) { + bool hover_1 = false; + bool hover_2 = false; + int on_enter = 0; + int on_leave = 0; + auto c1 = BasicComponent(); + c1 |= Hoverable(&hover_1); + c1 |= Hoverable([&](bool hover) { hover_2 = hover; }); + c1 |= Hoverable([&] { on_enter++; }, [&] { on_leave++; }); + auto c2 = BasicComponent(); + auto layout = Container::Horizontal({c1, c2}); + + auto screen = Screen(8, 2); + Render(screen, layout->Render()); + EXPECT_FALSE(hover_1); + EXPECT_FALSE(hover_2); + EXPECT_EQ(on_enter, 0); + EXPECT_EQ(on_leave, 0); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(0, 0))); + EXPECT_TRUE(hover_1); + EXPECT_TRUE(hover_2); + EXPECT_EQ(on_enter, 1); + EXPECT_EQ(on_leave, 0); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(1, 0))); + EXPECT_TRUE(hover_1); + EXPECT_TRUE(hover_2); + EXPECT_EQ(on_enter, 1); + EXPECT_EQ(on_leave, 0); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(3, 0))); + EXPECT_FALSE(hover_1); + EXPECT_FALSE(hover_2); + EXPECT_EQ(on_enter, 1); + EXPECT_EQ(on_leave, 1); + + EXPECT_FALSE(layout->OnEvent(HoverEvent(0, 0))); + EXPECT_TRUE(hover_1); + EXPECT_TRUE(hover_2); + EXPECT_EQ(on_enter, 2); + EXPECT_EQ(on_leave, 1); +} + +} // namespace +} // namespace ftxui + +// Copyright 2021 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file.