From 0acfd8f25524fca858a98aceca2119b62a23dddc Mon Sep 17 00:00:00 2001
From: Arthur Sonzogni <sonzogniarthur@gmail.com>
Date: Tue, 18 Oct 2022 21:29:27 +0200
Subject: [PATCH] Introduce Loop. (#476)

It can be used to give developers a better control on the loop. Users
can use it not to take full control of the thread, and poll FTXUI from
time to time as part of an external loop.

This resolves: https://github.com/ArthurSonzogni/FTXUI/issues/474
---
 CHANGELOG.md                                  |   4 +
 CMakeLists.txt                                |   4 +-
 examples/component/CMakeLists.txt             |   1 +
 examples/component/custom_loop.cpp            |  55 +++++++
 include/ftxui/component/loop.hpp              |  39 +++++
 include/ftxui/component/receiver.hpp          |  14 ++
 .../ftxui/component/screen_interactive.hpp    |  21 ++-
 src/ftxui/component/loop.cpp                  |  44 ++++++
 src/ftxui/component/screen_interactive.cpp    | 146 ++++++++++--------
 9 files changed, 261 insertions(+), 67 deletions(-)
 create mode 100644 examples/component/custom_loop.cpp
 create mode 100644 include/ftxui/component/loop.hpp
 create mode 100644 src/ftxui/component/loop.cpp

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1c725796..29a6ae95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,10 @@ current (development)
     - multiple directions.
     - multiple colors.
     - various values (value, min, max, increment).
+- Feature: Define `ScreenInteractive::Exit()`.
+- Feature: Add `Loop` to give developers a better control on the main loop. This
+  can be used to integrate FTXUI into another main loop, without taking the full
+  control.
 - Feature: `Input` supports CTRL+Left and CTRL+Right
 - Improvement: The `Menu` keeps the focus when an entry is selected with the
   mouse.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 2a884350..4f885630 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -93,6 +93,7 @@ add_library(component
   include/ftxui/component/component_base.hpp
   include/ftxui/component/component_options.hpp
   include/ftxui/component/event.hpp
+  include/ftxui/component/loop.hpp
   include/ftxui/component/mouse.hpp
   include/ftxui/component/receiver.hpp
   include/ftxui/component/screen_interactive.hpp
@@ -108,9 +109,10 @@ add_library(component
   src/ftxui/component/dropdown.cpp
   src/ftxui/component/event.cpp
   src/ftxui/component/input.cpp
+  src/ftxui/component/loop.cpp
   src/ftxui/component/maybe.cpp
-  src/ftxui/component/modal.cpp
   src/ftxui/component/menu.cpp
+  src/ftxui/component/modal.cpp
   src/ftxui/component/radiobox.cpp
   src/ftxui/component/radiobox.cpp
   src/ftxui/component/renderer.cpp
diff --git a/examples/component/CMakeLists.txt b/examples/component/CMakeLists.txt
index e8911103..4eb971e6 100644
--- a/examples/component/CMakeLists.txt
+++ b/examples/component/CMakeLists.txt
@@ -9,6 +9,7 @@ example(checkbox)
 example(checkbox_in_frame)
 example(collapsible)
 example(composition)
+example(custom_loop)
 example(dropdown)
 example(flexbox_gallery)
 example(focus)
diff --git a/examples/component/custom_loop.cpp b/examples/component/custom_loop.cpp
new file mode 100644
index 00000000..50702d03
--- /dev/null
+++ b/examples/component/custom_loop.cpp
@@ -0,0 +1,55 @@
+#include <stdlib.h>                   // for EXIT_SUCCESS
+#include <chrono>                     // for milliseconds
+#include <ftxui/component/event.hpp>  // for Event
+#include <ftxui/dom/elements.hpp>  // for text, separator, Element, operator|, vbox, border
+#include <memory>                  // for shared_ptr
+#include <string>                  // for operator+, to_string, allocator
+#include <thread>                  // for sleep_for
+
+#include "ftxui/component/captured_mouse.hpp"  // for ftxui
+#include "ftxui/component/component.hpp"  // for CatchEvent, Renderer, operator|=
+#include "ftxui/component/loop.hpp"       // for Loop
+#include "ftxui/component/screen_interactive.hpp"  // for ScreenInteractive
+
+int main(int argc, const char* argv[]) {
+  using namespace ftxui;
+  auto screen = ScreenInteractive::FitComponent();
+
+  // Create a component counting the number of frames drawn and event handled.
+  int custom_loop_count = 0;
+  int frame_count = 0;
+  int event_count = 0;
+  auto component = Renderer([&] {
+    frame_count++;
+    return vbox({
+               text("This demonstrates using a custom ftxui::Loop. It "),
+               text("runs at 100 iterations per seconds. The FTXUI events "),
+               text("are all processed once per iteration and a new frame "),
+               text("is rendered as needed"),
+               separator(),
+               text("ftxui event count: " + std::to_string(event_count)),
+               text("ftxui frame count: " + std::to_string(frame_count)),
+               text("Custom loop count: " + std::to_string(custom_loop_count)),
+           }) |
+           border;
+  });
+
+  component |= CatchEvent([&](Event) -> bool {
+    event_count++;
+    return false;
+  });
+
+  Loop loop(&screen, component);
+
+  while (!loop.HasQuitted()) {
+    custom_loop_count++;
+    loop.RunOnce();
+    std::this_thread::sleep_for(std::chrono::milliseconds(10));
+  }
+
+  return EXIT_SUCCESS;
+}
+
+// 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.
diff --git a/include/ftxui/component/loop.hpp b/include/ftxui/component/loop.hpp
new file mode 100644
index 00000000..63200bed
--- /dev/null
+++ b/include/ftxui/component/loop.hpp
@@ -0,0 +1,39 @@
+#ifndef FTXUI_COMPONENT_LOOP_HPP
+#define FTXUI_COMPONENT_LOOP_HPP
+
+#include <memory>  // for shared_ptr
+
+#include "ftxui/component/component_base.hpp"  // for ComponentBase
+
+namespace ftxui {
+class ComponentBase;
+
+using Component = std::shared_ptr<ComponentBase>;
+class ScreenInteractive;
+
+class Loop {
+ public:
+  Loop(ScreenInteractive* screen, Component component);
+  ~Loop();
+
+  bool HasQuitted();
+  void RunOnce();
+  void RunOnceBlocking();
+  void Run();
+
+ private:
+  // This class is non copyable.
+  Loop(const ScreenInteractive&) = delete;
+  Loop& operator=(const Loop&) = delete;
+
+  ScreenInteractive* screen_;
+  Component component_;
+};
+
+}  // namespace ftxui
+
+#endif  // FTXUI_COMPONENT_LOOP_HPP
+
+// 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/include/ftxui/component/receiver.hpp b/include/ftxui/component/receiver.hpp
index d2579889..1d487644 100644
--- a/include/ftxui/component/receiver.hpp
+++ b/include/ftxui/component/receiver.hpp
@@ -86,11 +86,25 @@ class ReceiverImpl {
     return false;
   }
 
+  bool ReceiveNonBlocking(T* t) {
+    std::unique_lock<std::mutex> lock(mutex_);
+    if (queue_.empty())
+      return false;
+    *t = queue_.front();
+    queue_.pop();
+    return true;
+  }
+
   bool HasPending() {
     std::unique_lock<std::mutex> lock(mutex_);
     return !queue_.empty();
   }
 
+  bool HasQuitted() {
+    std::unique_lock<std::mutex> lock(mutex_);
+    return queue_.empty() && !senders_;
+  }
+
  private:
   friend class SenderImpl<T>;
 
diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp
index ab342074..9aed718f 100644
--- a/include/ftxui/component/screen_interactive.hpp
+++ b/include/ftxui/component/screen_interactive.hpp
@@ -12,11 +12,12 @@
 #include "ftxui/component/animation.hpp"       // for TimePoint
 #include "ftxui/component/captured_mouse.hpp"  // for CapturedMouse
 #include "ftxui/component/event.hpp"           // for Event
-#include "ftxui/component/task.hpp"            // for Closure, Task
+#include "ftxui/component/task.hpp"            // for Task, Closure
 #include "ftxui/screen/screen.hpp"             // for Screen
 
 namespace ftxui {
 class ComponentBase;
+class Loop;
 struct Event;
 
 using Component = std::shared_ptr<ComponentBase>;
@@ -33,9 +34,12 @@ class ScreenInteractive : public Screen {
   // Return the currently active screen, nullptr if none.
   static ScreenInteractive* Active();
 
+  // Start/Stop the main loop.
   void Loop(Component);
+  void Exit();
   Closure ExitLoopClosure();
 
+  // Post tasks to be executed by the loop.
   void Post(Task task);
   void PostEvent(Event event);
   void RequestAnimationFrame();
@@ -51,9 +55,16 @@ class ScreenInteractive : public Screen {
   void Install();
   void Uninstall();
 
-  void Main(Component component);
+  void PreMain();
+  void PostMain();
 
+  bool HasQuitted();
+  void RunOnce(Component component);
+  void RunOnceBlocking(Component component);
+
+  void HandleTask(Component component, Task& task);
   void Draw(Component component);
+
   void SigStop();
 
   ScreenInteractive* suspended_screen_ = nullptr;
@@ -80,7 +91,7 @@ class ScreenInteractive : public Screen {
   std::thread event_listener_;
   std::thread animation_listener_;
   bool animation_requested_ = false;
-  animation::TimePoint previous_animation_time;
+  animation::TimePoint previous_animation_time_;
 
   int cursor_x_ = 1;
   int cursor_y_ = 1;
@@ -88,6 +99,10 @@ class ScreenInteractive : public Screen {
   bool mouse_captured = false;
   bool previous_frame_resized_ = false;
 
+  bool frame_valid_ = false;
+
+  friend class Loop;
+
  public:
   class Private {
    public:
diff --git a/src/ftxui/component/loop.cpp b/src/ftxui/component/loop.cpp
new file mode 100644
index 00000000..26464288
--- /dev/null
+++ b/src/ftxui/component/loop.cpp
@@ -0,0 +1,44 @@
+#include "ftxui/component/loop.hpp"
+#include "ftxui/component/screen_interactive.hpp"
+
+namespace ftxui {
+
+Loop::Loop(ScreenInteractive* screen, Component component)
+    : screen_(screen), component_(component) {
+  screen_->PreMain();
+}
+
+Loop::~Loop() {
+  screen_->PostMain();
+}
+
+bool Loop::HasQuitted() {
+  return screen_->HasQuitted();
+}
+
+/// @brief Execute the loop. Make the `component` to process every pending
+/// tasks/events. A new frame might be drawn if the previous was invalidated.
+/// Return true until the loop hasn't completed.
+void Loop::RunOnce() {
+  screen_->RunOnce(component_);
+}
+
+/// @brief Wait for at least one event to be handled and execute
+/// `Loop::RunOnce()`.
+void Loop::RunOnceBlocking() {
+  screen_->RunOnceBlocking(component_);
+}
+
+/// Execute the loop, blocking the current thread, up until the loop has
+/// quitted.
+void Loop::Run() {
+  while (!HasQuitted()) {
+    RunOnceBlocking();
+  }
+}
+
+}  // 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/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp
index 32d7aabc..278c53cd 100644
--- a/src/ftxui/component/screen_interactive.cpp
+++ b/src/ftxui/component/screen_interactive.cpp
@@ -20,6 +20,7 @@
 #include "ftxui/component/captured_mouse.hpp"  // for CapturedMouse, CapturedMouseInterface
 #include "ftxui/component/component_base.hpp"  // for ComponentBase
 #include "ftxui/component/event.hpp"           // for Event
+#include "ftxui/component/loop.hpp"            // for Loop
 #include "ftxui/component/receiver.hpp"  // for Sender, ReceiverImpl, MakeReceiver, SenderImpl, Receiver
 #include "ftxui/component/screen_interactive.hpp"
 #include "ftxui/component/terminal_input_parser.hpp"  // for TerminalInputParser
@@ -350,8 +351,8 @@ void ScreenInteractive::RequestAnimationFrame() {
   animation_requested_ = true;
   auto now = animation::Clock::now();
   const auto time_histeresis = std::chrono::milliseconds(33);
-  if (now - previous_animation_time >= time_histeresis) {
-    previous_animation_time = now;
+  if (now - previous_animation_time_ >= time_histeresis) {
+    previous_animation_time_ = now;
   }
 }
 
@@ -365,6 +366,15 @@ CapturedMouse ScreenInteractive::CaptureMouse() {
 }
 
 void ScreenInteractive::Loop(Component component) {  // NOLINT
+  class Loop loop(this, component);
+  loop.Run();
+}
+
+bool ScreenInteractive::HasQuitted() {
+  return task_receiver_->HasQuitted();
+}
+
+void ScreenInteractive::PreMain() {
   // Suspend previously active screen:
   if (g_active_screen) {
     std::swap(suspended_screen_, g_active_screen);
@@ -378,7 +388,11 @@ void ScreenInteractive::Loop(Component component) {  // NOLINT
   // This screen is now active:
   g_active_screen = this;
   g_active_screen->Install();
-  g_active_screen->Main(std::move(component));
+
+  previous_animation_time_ = animation::Clock::now();
+}
+
+void ScreenInteractive::PostMain() {
   g_active_screen->Uninstall();
   g_active_screen = nullptr;
 
@@ -531,81 +545,78 @@ void ScreenInteractive::Uninstall() {
 }
 
 // NOLINTNEXTLINE
-void ScreenInteractive::Main(Component component) {
-  previous_animation_time = animation::Clock::now();
+void ScreenInteractive::RunOnceBlocking(Component component) {
+  Task task;
+  if (task_receiver_->Receive(&task)) {
+    HandleTask(component, task);
+  }
 
-  auto draw = [&] {
-    Draw(component);
-    std::cout << ToString() << set_cursor_position;
-    Flush();
-    Clear();
-  };
+  RunOnce(component);
+}
 
-  bool attempt_draw = true;
-  while (!quit_) {
-    if (attempt_draw && !task_receiver_->HasPending()) {
-      draw();
-      attempt_draw = false;
-    }
+void ScreenInteractive::RunOnce(Component component) {
+  Task task;
+  while (task_receiver_->ReceiveNonBlocking(&task)) {
+    HandleTask(component, task);
+  }
+  Draw(component);
+}
 
-    Task task;
-    if (!task_receiver_->Receive(&task)) {
-      break;
-    }
+void ScreenInteractive::HandleTask(Component component, Task& task) {
+  // clang-format off
+  std::visit([&](auto&& arg) {
+    using T = std::decay_t<decltype(arg)>;
 
-    // clang-format off
-    std::visit([&](auto&& arg) {
-      using T = std::decay_t<decltype(arg)>;
-
-      // Handle Event.
-      if constexpr (std::is_same_v<T, Event>) {
-        if (arg.is_cursor_reporting()) {
-          cursor_x_ = arg.cursor_x();
-          cursor_y_ = arg.cursor_y();
-          return;
-        }
-
-        if (arg.is_mouse()) {
-          arg.mouse().x -= cursor_x_;
-          arg.mouse().y -= cursor_y_;
-        }
-
-        arg.screen_ = this;
-        component->OnEvent(arg);
-        attempt_draw = true;
+    // Handle Event.
+    if constexpr (std::is_same_v<T, Event>) {
+      if (arg.is_cursor_reporting()) {
+        cursor_x_ = arg.cursor_x();
+        cursor_y_ = arg.cursor_y();
         return;
       }
 
-      // Handle callback
-      if constexpr (std::is_same_v<T, Closure>) {
-        arg();
-        return;
+      if (arg.is_mouse()) {
+        arg.mouse().x -= cursor_x_;
+        arg.mouse().y -= cursor_y_;
       }
 
-      // Handle Animation
-      if constexpr (std::is_same_v<T, AnimationTask>) {
-        if (!animation_requested_) {
-          return;
-        }
+      arg.screen_ = this;
+      component->OnEvent(arg);
+      frame_valid_ = false;
+      return;
+    }
 
-        animation_requested_ = false;
-        animation::TimePoint now = animation::Clock::now();
-        animation::Duration delta = now - previous_animation_time;
-        previous_animation_time = now;
+    // Handle callback
+    if constexpr (std::is_same_v<T, Closure>) {
+      arg();
+      return;
+    }
 
-        animation::Params params(delta);
-        component->OnAnimation(params);
-        attempt_draw = true;
+    // Handle Animation
+    if constexpr (std::is_same_v<T, AnimationTask>) {
+      if (!animation_requested_) {
         return;
       }
-    },
-    task);
-    // clang-format on
-  }
+
+      animation_requested_ = false;
+      animation::TimePoint now = animation::Clock::now();
+      animation::Duration delta = now - previous_animation_time_;
+      previous_animation_time_ = now;
+
+      animation::Params params(delta);
+      component->OnAnimation(params);
+      frame_valid_ = false;
+      return;
+    }
+  },
+  task);
+  // clang-format on
 }
 
 // NOLINTNEXTLINE
 void ScreenInteractive::Draw(Component component) {
+  if (frame_valid_)
+    return;
   auto document = component->Render();
   int dimx = 0;
   int dimy = 0;
@@ -685,13 +696,22 @@ void ScreenInteractive::Draw(Component component) {
     set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
     reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
   }
+
+  std::cout << ToString() << set_cursor_position;
+  Flush();
+  Clear();
+  frame_valid_ = true;
 }
 
 Closure ScreenInteractive::ExitLoopClosure() {
-  return [this] {
+  return [this] { Exit(); };
+}
+
+void ScreenInteractive::Exit() {
+  Post([this] {
     quit_ = true;
     task_sender_.reset();
-  };
+  });
 }
 
 void ScreenInteractive::SigStop() {
-- 
GitLab