From 0d54285e19ad858804e796782ddcd3fbfe24cc1b Mon Sep 17 00:00:00 2001
From: "Arthur Sonzogni (slow/sick)" <sonzogniarthur@gmail.com>
Date: Sun, 4 Dec 2022 11:54:49 +0100
Subject: [PATCH] Add the Hoverable wrapper. (#522)

This will make it easier for developers. For instance:
https://github.com/ArthurSonzogni/FTXUI/issues/521
---
 CHANGELOG.md                           |   1 +
 CMakeLists.txt                         |   1 +
 cmake/ftxui_test.cmake                 |   3 +-
 include/ftxui/component/component.hpp  |  12 ++
 src/ftxui/component/hoverable.cpp      | 171 ++++++++++++++++++++++
 src/ftxui/component/hoverable_test.cpp | 192 +++++++++++++++++++++++++
 6 files changed, 379 insertions(+), 1 deletion(-)
 create mode 100644 src/ftxui/component/hoverable.cpp
 create mode 100644 src/ftxui/component/hoverable_test.cpp

diff --git a/CHANGELOG.md b/CHANGELOG.md
index b23b5882..b6fbdc92 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 4f885630..f457e485 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 0382e6a0..4f822598 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 80f48ed7..7eb33bcf 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 00000000..56ce2759
--- /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 00000000..ea04a777
--- /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.
-- 
GitLab