From 0acfd8f25524fca858a98aceca2119b62a23dddc Mon Sep 17 00:00:00 2001 From: Arthur Sonzogni 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 | 170 ++++++++++-------- 9 files changed, 273 insertions(+), 79 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 1c72579..29a6ae9 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 2a88435..4f88563 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 e891110..4eb971e 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 0000000..50702d0 --- /dev/null +++ b/examples/component/custom_loop.cpp @@ -0,0 +1,55 @@ +#include // for EXIT_SUCCESS +#include // for milliseconds +#include // for Event +#include // for text, separator, Element, operator|, vbox, border +#include // for shared_ptr +#include // for operator+, to_string, allocator +#include // 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 0000000..63200be --- /dev/null +++ b/include/ftxui/component/loop.hpp @@ -0,0 +1,39 @@ +#ifndef FTXUI_COMPONENT_LOOP_HPP +#define FTXUI_COMPONENT_LOOP_HPP + +#include // for shared_ptr + +#include "ftxui/component/component_base.hpp" // for ComponentBase + +namespace ftxui { +class ComponentBase; + +using Component = std::shared_ptr; +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 d257988..1d48764 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 lock(mutex_); + if (queue_.empty()) + return false; + *t = queue_.front(); + queue_.pop(); + return true; + } + bool HasPending() { std::unique_lock lock(mutex_); return !queue_.empty(); } + bool HasQuitted() { + std::unique_lock lock(mutex_); + return queue_.empty() && !senders_; + } + private: friend class SenderImpl; diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index ab34207..9aed718 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; @@ -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 0000000..2646428 --- /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 32d7aab..278c53c 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(); - - auto draw = [&] { - Draw(component); - std::cout << ToString() << set_cursor_position; - Flush(); - Clear(); - }; - - bool attempt_draw = true; - while (!quit_) { - if (attempt_draw && !task_receiver_->HasPending()) { - draw(); - attempt_draw = false; - } - - Task task; - if (!task_receiver_->Receive(&task)) { - break; - } - - // clang-format off - std::visit([&](auto&& arg) { - using T = std::decay_t; - - // Handle Event. - if constexpr (std::is_same_v) { - 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; - return; - } - - // Handle callback - if constexpr (std::is_same_v) { - arg(); - return; - } - - // Handle Animation - if constexpr (std::is_same_v) { - if (!animation_requested_) { - return; - } - - 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); - attempt_draw = true; - return; - } - }, - task); - // clang-format on +void ScreenInteractive::RunOnceBlocking(Component component) { + Task task; + if (task_receiver_->Receive(&task)) { + HandleTask(component, task); } + + RunOnce(component); +} + +void ScreenInteractive::RunOnce(Component component) { + Task task; + while (task_receiver_->ReceiveNonBlocking(&task)) { + HandleTask(component, task); + } + Draw(component); +} + +void ScreenInteractive::HandleTask(Component component, Task& task) { + // clang-format off + std::visit([&](auto&& arg) { + using T = std::decay_t; + + // Handle Event. + if constexpr (std::is_same_v) { + 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); + frame_valid_ = false; + return; + } + + // Handle callback + if constexpr (std::is_same_v) { + arg(); + return; + } + + // Handle Animation + if constexpr (std::is_same_v) { + if (!animation_requested_) { + return; + } + + 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() {