From c31aecf2edde81d20f4f4bc8c97af053026483a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Cl=C3=A9ment=20Roblot?= <clement.roblot@martobre.fr>
Date: Sat, 11 Nov 2023 23:33:50 +0700
Subject: [PATCH] Checkbox button debounce (#774)

This fixes: https://github.com/ArthurSonzogni/FTXUI/issues/773

Dragging the mouse with the left button pressed now avoids activating multiple
checkboxes.

Add support for detecting mouse press transition. Added:
```cpp
// The previous mouse event.
Mouse Mouse::previous;

// Return whether the mouse transitionned from:
// released to pressed => IsPressed()
// pressed to pressed => IsHeld()
// pressed to released => IsReleased()
bool Mouse::IsPressed(Button button) const;
bool Mouse::IsHeld(Button button) const;
bool Mouse::IsReleased(Button button) const;
```
A couple of components are now activated when the mouse is pressed,
as opposed to released.

Co-authored-by: ArthurSonzogni <sonzogniarthur@gmail.com>
---
 CHANGELOG.md                                  | 20 +++++++++++
 CMakeLists.txt                                |  1 +
 include/ftxui/component/mouse.hpp             |  8 +++++
 .../ftxui/component/screen_interactive.hpp    |  1 +
 src/ftxui/component/button.cpp                |  3 +-
 src/ftxui/component/checkbox.cpp              |  3 +-
 src/ftxui/component/input.cpp                 |  3 +-
 src/ftxui/component/menu.cpp                  |  6 ++--
 src/ftxui/component/mouse.cpp                 | 36 +++++++++++++++++++
 src/ftxui/component/radiobox.cpp              |  3 +-
 src/ftxui/component/resizable_split.cpp       |  3 +-
 src/ftxui/component/screen_interactive.cpp    |  6 ++++
 src/ftxui/component/slider.cpp                |  3 +-
 src/ftxui/component/window.cpp                |  3 +-
 14 files changed, 81 insertions(+), 18 deletions(-)
 create mode 100644 src/ftxui/component/mouse.cpp

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 847e6bca..d3cefa5a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,26 @@ current (development)
 ### Component
 - Feature: Add support for `Input`'s insert mode. Add `InputOption::insert`
   option. Added by @mingsheng13.
+- Feature/Bugfix/Breaking change: `Mouse transition`:
+  This fixes: https://github.com/ArthurSonzogni/FTXUI/issues/773
+  Dragging the mouse with the left button pressed now avoids activating multiple
+  checkboxes.
+
+  Add support for detecting mouse press transition. Added:
+  ```cpp
+  // The previous mouse event.
+  Mouse Mouse::previous;
+
+  // Return whether the mouse transitionned from:
+  // released to pressed => IsPressed()
+  // pressed to pressed => IsHeld()
+  // pressed to released => IsReleased()
+  bool Mouse::IsPressed(Button button) const;
+  bool Mouse::IsHeld(Button button) const;
+  bool Mouse::IsReleased(Button button) const;
+  ```
+  A couple of components are now activated when the mouse is pressed,
+  as opposed to released.
 - Bugfix: `Input` `onchange` was not called on backspace or delete key.
   Fixed by @chrysante in chrysante in PR #776.
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 583ed46a..6dc246bf 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -123,6 +123,7 @@ add_library(component
   src/ftxui/component/maybe.cpp
   src/ftxui/component/menu.cpp
   src/ftxui/component/modal.cpp
+  src/ftxui/component/mouse.cpp
   src/ftxui/component/radiobox.cpp
   src/ftxui/component/radiobox.cpp
   src/ftxui/component/renderer.cpp
diff --git a/include/ftxui/component/mouse.hpp b/include/ftxui/component/mouse.hpp
index 3c61008d..38bf1e15 100644
--- a/include/ftxui/component/mouse.hpp
+++ b/include/ftxui/component/mouse.hpp
@@ -23,6 +23,11 @@ struct Mouse {
     Pressed = 1,
   };
 
+  // Utility function to check the variations of the mouse state.
+  bool IsPressed(Button btn = Left) const;   // Released => Pressed.
+  bool IsHeld(Button btn = Left) const;      // Pressed => Pressed.
+  bool IsReleased(Button btn = Left) const;  // Pressed => Released.
+
   // Button
   Button button = Button::None;
 
@@ -37,6 +42,9 @@ struct Mouse {
   // Coordinates:
   int x = 0;
   int y = 0;
+
+  // Previous mouse event, if any.
+  Mouse* previous = nullptr;
 };
 
 }  // namespace ftxui
diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp
index 10d022c6..fd3a629f 100644
--- a/include/ftxui/component/screen_interactive.hpp
+++ b/include/ftxui/component/screen_interactive.hpp
@@ -112,6 +112,7 @@ class ScreenInteractive : public Screen {
 
   bool frame_valid_ = false;
 
+  Mouse latest_mouse_event_;
   friend class Loop;
 
  public:
diff --git a/src/ftxui/component/button.cpp b/src/ftxui/component/button.cpp
index c983f31f..229c32ee 100644
--- a/src/ftxui/component/button.cpp
+++ b/src/ftxui/component/button.cpp
@@ -124,8 +124,7 @@ class ButtonBase : public ComponentBase, public ButtonOption {
       return false;
     }
 
-    if (event.mouse().button == Mouse::Left &&
-        event.mouse().motion == Mouse::Pressed) {
+    if (event.mouse().IsPressed()) {
       TakeFocus();
       OnClick();
       return true;
diff --git a/src/ftxui/component/checkbox.cpp b/src/ftxui/component/checkbox.cpp
index ebfa46d8..bf1324f0 100644
--- a/src/ftxui/component/checkbox.cpp
+++ b/src/ftxui/component/checkbox.cpp
@@ -69,8 +69,7 @@ class CheckboxBase : public ComponentBase, public CheckboxOption {
       return false;
     }
 
-    if (event.mouse().button == Mouse::Left &&
-        event.mouse().motion == Mouse::Pressed) {
+    if (event.mouse().IsPressed()) {
       *checked = !*checked;
       on_change();
       return true;
diff --git a/src/ftxui/component/input.cpp b/src/ftxui/component/input.cpp
index 7228f131..b41a9b4a 100644
--- a/src/ftxui/component/input.cpp
+++ b/src/ftxui/component/input.cpp
@@ -466,8 +466,7 @@ class InputBase : public ComponentBase, public InputOption {
       return false;
     }
 
-    if (event.mouse().button != Mouse::Left ||
-        event.mouse().motion != Mouse::Pressed) {
+    if (!event.mouse().IsPressed()) {
       return false;
     }
 
diff --git a/src/ftxui/component/menu.cpp b/src/ftxui/component/menu.cpp
index 7f6b92d1..890e500f 100644
--- a/src/ftxui/component/menu.cpp
+++ b/src/ftxui/component/menu.cpp
@@ -318,8 +318,7 @@ class MenuBase : public ComponentBase, public MenuOption {
 
       TakeFocus();
       focused_entry() = i;
-      if (event.mouse().button == Mouse::Left &&
-          event.mouse().motion == Mouse::Released) {
+      if (event.mouse().IsPressed()) {
         if (selected() != i) {
           selected() = i;
           selected_previous_ = selected();
@@ -683,8 +682,7 @@ Component MenuEntry(MenuEntryOption option) {
         return false;
       }
 
-      if (event.mouse().button == Mouse::Left &&
-          event.mouse().motion == Mouse::Released) {
+      if (event.mouse().IsPressed()) {
         TakeFocus();
         return true;
       }
diff --git a/src/ftxui/component/mouse.cpp b/src/ftxui/component/mouse.cpp
new file mode 100644
index 00000000..74e51d2e
--- /dev/null
+++ b/src/ftxui/component/mouse.cpp
@@ -0,0 +1,36 @@
+// Copyright 2023 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 "ftxui/component/mouse.hpp"
+
+namespace ftxui {
+
+namespace {
+bool IsDown(const Mouse* mouse, Mouse::Button btn) {
+  return mouse->button == btn && mouse->motion == Mouse::Pressed;
+}
+}  // namespace
+
+/// Return whether the mouse transitionned from released to pressed.
+/// This is useful to detect a click.
+/// @arg btn The button to check.
+bool Mouse::IsPressed(Button btn) const {
+  return IsDown(this, btn) && (!previous || !IsDown(previous, btn));
+}
+
+/// Return whether the mouse is currently held.
+/// This is useful to detect a drag.
+/// @arg btn The button to check.
+bool Mouse::IsHeld(Button btn) const {
+  return IsDown(this, btn) && previous && IsDown(previous, btn);
+}
+
+/// Return whether the mouse transitionned from pressed to released.
+/// This is useful to detect a click.
+/// @arg btn The button to check.
+bool Mouse::IsReleased(Button btn) const {
+  return !IsDown(this, btn) && (previous && IsDown(previous, btn));
+}
+
+}  // namespace ftxui
diff --git a/src/ftxui/component/radiobox.cpp b/src/ftxui/component/radiobox.cpp
index 004a2428..0d00f403 100644
--- a/src/ftxui/component/radiobox.cpp
+++ b/src/ftxui/component/radiobox.cpp
@@ -123,8 +123,7 @@ class RadioboxBase : public ComponentBase, public RadioboxOption {
 
       TakeFocus();
       focused_entry() = i;
-      if (event.mouse().button == Mouse::Left &&
-          event.mouse().motion == Mouse::Released) {
+      if (event.mouse().IsPressed()) {
         if (selected() != i) {
           selected() = i;
           on_change();
diff --git a/src/ftxui/component/resizable_split.cpp b/src/ftxui/component/resizable_split.cpp
index 9e0d62e3..614fd417 100644
--- a/src/ftxui/component/resizable_split.cpp
+++ b/src/ftxui/component/resizable_split.cpp
@@ -42,8 +42,7 @@ class ResizableSplitBase : public ComponentBase {
       return true;
     }
 
-    if (event.mouse().button == Mouse::Left &&
-        event.mouse().motion == Mouse::Pressed &&
+    if (event.mouse().IsPressed() &&
         separator_box_.Contain(event.mouse().x, event.mouse().y) &&
         !captured_mouse_) {
       captured_mouse_ = CaptureMouse(event);
diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp
index ca908609..33660514 100644
--- a/src/ftxui/component/screen_interactive.cpp
+++ b/src/ftxui/component/screen_interactive.cpp
@@ -689,11 +689,17 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
       if (arg.is_mouse()) {
         arg.mouse().x -= cursor_x_;
         arg.mouse().y -= cursor_y_;
+        arg.mouse().previous = &latest_mouse_event_;
       }
 
       arg.screen_ = this;
       component->OnEvent(arg);
       frame_valid_ = false;
+
+      if (arg.is_mouse()) {
+        latest_mouse_event_ = arg.mouse();
+        latest_mouse_event_.previous = nullptr;
+      }
       return;
     }
 
diff --git a/src/ftxui/component/slider.cpp b/src/ftxui/component/slider.cpp
index d30b87b7..80d29fdb 100644
--- a/src/ftxui/component/slider.cpp
+++ b/src/ftxui/component/slider.cpp
@@ -174,8 +174,7 @@ class SliderBase : public ComponentBase {
       return true;
     }
 
-    if (event.mouse().button != Mouse::Left ||
-        event.mouse().motion != Mouse::Pressed) {
+    if (!event.mouse().IsPressed()) {
       return false;
     }
 
diff --git a/src/ftxui/component/window.cpp b/src/ftxui/component/window.cpp
index daed7e10..cae72b45 100644
--- a/src/ftxui/component/window.cpp
+++ b/src/ftxui/component/window.cpp
@@ -225,8 +225,7 @@ class WindowImpl : public ComponentBase, public WindowOptions {
       return true;
     }
 
-    if (event.mouse().button != Mouse::Left ||
-        event.mouse().motion != Mouse::Pressed) {
+    if (!event.mouse().IsPressed()) {
       return true;
     }
 
-- 
GitLab