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() {