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
This commit is contained in:
Arthur Sonzogni 2022-10-18 21:29:27 +02:00 committed by GitHub
parent 26d63bc56f
commit 0acfd8f255
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 273 additions and 79 deletions

View File

@ -20,6 +20,10 @@ current (development)
- multiple directions. - multiple directions.
- multiple colors. - multiple colors.
- various values (value, min, max, increment). - 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 - Feature: `Input` supports CTRL+Left and CTRL+Right
- Improvement: The `Menu` keeps the focus when an entry is selected with the - Improvement: The `Menu` keeps the focus when an entry is selected with the
mouse. mouse.

View File

@ -93,6 +93,7 @@ add_library(component
include/ftxui/component/component_base.hpp include/ftxui/component/component_base.hpp
include/ftxui/component/component_options.hpp include/ftxui/component/component_options.hpp
include/ftxui/component/event.hpp include/ftxui/component/event.hpp
include/ftxui/component/loop.hpp
include/ftxui/component/mouse.hpp include/ftxui/component/mouse.hpp
include/ftxui/component/receiver.hpp include/ftxui/component/receiver.hpp
include/ftxui/component/screen_interactive.hpp include/ftxui/component/screen_interactive.hpp
@ -108,9 +109,10 @@ add_library(component
src/ftxui/component/dropdown.cpp src/ftxui/component/dropdown.cpp
src/ftxui/component/event.cpp src/ftxui/component/event.cpp
src/ftxui/component/input.cpp src/ftxui/component/input.cpp
src/ftxui/component/loop.cpp
src/ftxui/component/maybe.cpp src/ftxui/component/maybe.cpp
src/ftxui/component/modal.cpp
src/ftxui/component/menu.cpp src/ftxui/component/menu.cpp
src/ftxui/component/modal.cpp
src/ftxui/component/radiobox.cpp src/ftxui/component/radiobox.cpp
src/ftxui/component/radiobox.cpp src/ftxui/component/radiobox.cpp
src/ftxui/component/renderer.cpp src/ftxui/component/renderer.cpp

View File

@ -9,6 +9,7 @@ example(checkbox)
example(checkbox_in_frame) example(checkbox_in_frame)
example(collapsible) example(collapsible)
example(composition) example(composition)
example(custom_loop)
example(dropdown) example(dropdown)
example(flexbox_gallery) example(flexbox_gallery)
example(focus) example(focus)

View File

@ -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.

View File

@ -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.

View File

@ -86,11 +86,25 @@ class ReceiverImpl {
return false; 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() { bool HasPending() {
std::unique_lock<std::mutex> lock(mutex_); std::unique_lock<std::mutex> lock(mutex_);
return !queue_.empty(); return !queue_.empty();
} }
bool HasQuitted() {
std::unique_lock<std::mutex> lock(mutex_);
return queue_.empty() && !senders_;
}
private: private:
friend class SenderImpl<T>; friend class SenderImpl<T>;

View File

@ -12,11 +12,12 @@
#include "ftxui/component/animation.hpp" // for TimePoint #include "ftxui/component/animation.hpp" // for TimePoint
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse #include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
#include "ftxui/component/event.hpp" // for Event #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 #include "ftxui/screen/screen.hpp" // for Screen
namespace ftxui { namespace ftxui {
class ComponentBase; class ComponentBase;
class Loop;
struct Event; struct Event;
using Component = std::shared_ptr<ComponentBase>; using Component = std::shared_ptr<ComponentBase>;
@ -33,9 +34,12 @@ class ScreenInteractive : public Screen {
// Return the currently active screen, nullptr if none. // Return the currently active screen, nullptr if none.
static ScreenInteractive* Active(); static ScreenInteractive* Active();
// Start/Stop the main loop.
void Loop(Component); void Loop(Component);
void Exit();
Closure ExitLoopClosure(); Closure ExitLoopClosure();
// Post tasks to be executed by the loop.
void Post(Task task); void Post(Task task);
void PostEvent(Event event); void PostEvent(Event event);
void RequestAnimationFrame(); void RequestAnimationFrame();
@ -51,9 +55,16 @@ class ScreenInteractive : public Screen {
void Install(); void Install();
void Uninstall(); 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 Draw(Component component);
void SigStop(); void SigStop();
ScreenInteractive* suspended_screen_ = nullptr; ScreenInteractive* suspended_screen_ = nullptr;
@ -80,7 +91,7 @@ class ScreenInteractive : public Screen {
std::thread event_listener_; std::thread event_listener_;
std::thread animation_listener_; std::thread animation_listener_;
bool animation_requested_ = false; bool animation_requested_ = false;
animation::TimePoint previous_animation_time; animation::TimePoint previous_animation_time_;
int cursor_x_ = 1; int cursor_x_ = 1;
int cursor_y_ = 1; int cursor_y_ = 1;
@ -88,6 +99,10 @@ class ScreenInteractive : public Screen {
bool mouse_captured = false; bool mouse_captured = false;
bool previous_frame_resized_ = false; bool previous_frame_resized_ = false;
bool frame_valid_ = false;
friend class Loop;
public: public:
class Private { class Private {
public: public:

View File

@ -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.

View File

@ -20,6 +20,7 @@
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface #include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
#include "ftxui/component/component_base.hpp" // for ComponentBase #include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/event.hpp" // for Event #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/receiver.hpp" // for Sender, ReceiverImpl, MakeReceiver, SenderImpl, Receiver
#include "ftxui/component/screen_interactive.hpp" #include "ftxui/component/screen_interactive.hpp"
#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser #include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
@ -350,8 +351,8 @@ void ScreenInteractive::RequestAnimationFrame() {
animation_requested_ = true; animation_requested_ = true;
auto now = animation::Clock::now(); auto now = animation::Clock::now();
const auto time_histeresis = std::chrono::milliseconds(33); const auto time_histeresis = std::chrono::milliseconds(33);
if (now - previous_animation_time >= time_histeresis) { if (now - previous_animation_time_ >= time_histeresis) {
previous_animation_time = now; previous_animation_time_ = now;
} }
} }
@ -365,6 +366,15 @@ CapturedMouse ScreenInteractive::CaptureMouse() {
} }
void ScreenInteractive::Loop(Component component) { // NOLINT 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: // Suspend previously active screen:
if (g_active_screen) { if (g_active_screen) {
std::swap(suspended_screen_, 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: // This screen is now active:
g_active_screen = this; g_active_screen = this;
g_active_screen->Install(); 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->Uninstall();
g_active_screen = nullptr; g_active_screen = nullptr;
@ -531,81 +545,78 @@ void ScreenInteractive::Uninstall() {
} }
// NOLINTNEXTLINE // NOLINTNEXTLINE
void ScreenInteractive::Main(Component component) { void ScreenInteractive::RunOnceBlocking(Component component) {
previous_animation_time = animation::Clock::now(); Task task;
if (task_receiver_->Receive(&task)) {
auto draw = [&] { HandleTask(component, task);
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<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;
return;
}
// Handle callback
if constexpr (std::is_same_v<T, Closure>) {
arg();
return;
}
// Handle Animation
if constexpr (std::is_same_v<T, AnimationTask>) {
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
} }
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<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);
frame_valid_ = false;
return;
}
// Handle callback
if constexpr (std::is_same_v<T, Closure>) {
arg();
return;
}
// Handle Animation
if constexpr (std::is_same_v<T, AnimationTask>) {
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 // NOLINTNEXTLINE
void ScreenInteractive::Draw(Component component) { void ScreenInteractive::Draw(Component component) {
if (frame_valid_)
return;
auto document = component->Render(); auto document = component->Render();
int dimx = 0; int dimx = 0;
int dimy = 0; int dimy = 0;
@ -685,13 +696,22 @@ void ScreenInteractive::Draw(Component component) {
set_cursor_position += "\x1B[" + std::to_string(dy) + "A"; set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
reset_cursor_position += "\x1B[" + std::to_string(dy) + "B"; reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
} }
std::cout << ToString() << set_cursor_position;
Flush();
Clear();
frame_valid_ = true;
} }
Closure ScreenInteractive::ExitLoopClosure() { Closure ScreenInteractive::ExitLoopClosure() {
return [this] { return [this] { Exit(); };
}
void ScreenInteractive::Exit() {
Post([this] {
quit_ = true; quit_ = true;
task_sender_.reset(); task_sender_.reset();
}; });
} }
void ScreenInteractive::SigStop() { void ScreenInteractive::SigStop() {