mirror of
https://github.com/ArthurSonzogni/FTXUI.git
synced 2024-11-22 18:59:59 +08:00
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:
parent
26d63bc56f
commit
0acfd8f255
@ -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.
|
||||
|
@ -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
|
||||
|
@ -9,6 +9,7 @@ example(checkbox)
|
||||
example(checkbox_in_frame)
|
||||
example(collapsible)
|
||||
example(composition)
|
||||
example(custom_loop)
|
||||
example(dropdown)
|
||||
example(flexbox_gallery)
|
||||
example(focus)
|
||||
|
55
examples/component/custom_loop.cpp
Normal file
55
examples/component/custom_loop.cpp
Normal 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.
|
39
include/ftxui/component/loop.hpp
Normal file
39
include/ftxui/component/loop.hpp
Normal 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.
|
@ -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>;
|
||||
|
||||
|
@ -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:
|
||||
|
44
src/ftxui/component/loop.cpp
Normal file
44
src/ftxui/component/loop.cpp
Normal 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.
|
@ -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<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
|
||||
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<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
|
||||
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() {
|
||||
|
Loading…
Reference in New Issue
Block a user