// 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. #include #include // for copy, max, min #include // for array #include // for operator-, milliseconds, operator>=, duration, common_type<>::type, time_point #include // for fileno, stdin #include // for Task, Closure, AnimationTask #include // for Pixel, Screen::Cursor, Screen, Screen::Cursor::Hidden #include // for function #include // for initializer_list #include // for cout, ostream, operator<<, basic_ostream, endl, flush #include // for stack #include // for thread, sleep_for #include // for _Swallow_assign, ignore #include // for decay_t #include // for move, swap #include // for visit, variant #include // for vector #include // for TimePoint, Clock, Duration, Params, RequestAnimationFrame #include // for CapturedMouse, CapturedMouseInterface #include // for ComponentBase #include // for Event #include // for Loop #include // for ReceiverImpl, Sender, MakeReceiver, SenderImpl, Receiver #include // for Node, Render #include // for Requirement #include // for Dimensions, Size #include #include "sfml_screen.hpp" // Quick exit is missing in standard CLang headers #if defined(__clang__) && defined(__APPLE__) #define quick_exit(a) exit(a) #endif /* namespace ftxui { namespace animation { void RequestAnimationFrame() { auto* screen = SFMLScreen::Active(); if (screen) { screen->RequestAnimationFrame(); } } } // namespace animation } */ namespace { SFMLScreen* g_active_screen = nullptr; // NOLINT void Flush() { // Emscripten doesn't implement flush. We interpret zero as flush. std::cout << '\0' << std::flush; } constexpr int timeout_milliseconds = 20; [[maybe_unused]] constexpr int timeout_microseconds = timeout_milliseconds * 1000; std::stack on_exit_functions; // NOLINT void OnExit() { while (!on_exit_functions.empty()) { on_exit_functions.top()(); on_exit_functions.pop(); } } } // namespace /* * bruxisma: std::thread has some special magic built in so that if you pass in a std::reference_wrapper it'll unpack it and treat it as a reference. So you can pass it as a reference with `std::ref` for mutable references, and `st***ref` for constant references * * ZED: This is al Windows specific code that needs to be replaced * with SFML's events system, so the quit here will die. */ void EventListener(Sender out) { using namespace std::chrono_literals; while (true) { // get the sfml window inputs fmt::println("WAITING FOR EVENT"); std::this_thread::sleep_for(1000ms); } } /* * ZED: This can stay but it doesn't need to be a thread, make it a function * that is called in the event loop. */ void AnimationListener(Sender out) { // Animation at around 60fps. const auto time_delta = std::chrono::milliseconds(15); while (true) { out->Send(ftxui::AnimationTask()); std::this_thread::sleep_for(time_delta); } } SFMLScreen::SFMLScreen(int dimx, int dimy, Dimension dimension, bool use_alternative_screen) : Screen(dimx, dimy), dimension_(dimension), use_alternative_screen_(use_alternative_screen) { task_receiver_ = ftxui::MakeReceiver(); } // static SFMLScreen SFMLScreen::FixedSize(int dimx, int dimy) { return { dimx, dimy, Dimension::Fixed, false, }; } // static SFMLScreen SFMLScreen::Fullscreen() { return { 0, 0, Dimension::Fullscreen, true, }; } // static SFMLScreen SFMLScreen::TerminalOutput() { return { 0, 0, Dimension::TerminalOutput, false, }; } // static SFMLScreen SFMLScreen::FitComponent() { return { 0, 0, Dimension::FitComponent, false, }; } /// @ingroup component /// @brief Set whether mouse is tracked and events reported. /// called outside of the main loop. E.g `SFMLScreen::Loop(...)`. /// @param enable Whether to enable mouse event tracking. /// @note This muse be called outside of the main loop. E.g. before calling /// `SFMLScreen::Loop`. /// @note Mouse tracking is enabled by default. /// @note Mouse tracking is only supported on terminals that supports it. /// /// ### Example /// /// ```cpp /// auto screen = SFMLScreen::TerminalOutput(); /// screen.TrackMouse(false); /// screen.Loop(component); /// ``` void SFMLScreen::TrackMouse(bool enable) { track_mouse_ = enable; } /// @brief Add a task to the main loop. /// It will be executed later, after every other scheduled tasks. /// @ingroup component void SFMLScreen::Post(Task task) { // Task/Events sent toward inactive screen or screen waiting to become // inactive are dropped. if (!task_sender_) { return; } task_sender_->Send(std::move(task)); } /// @brief Add an event to the main loop. /// It will be executed later, after every other scheduled events. /// @ingroup component void SFMLScreen::PostEvent(Event event) { Post(event); } /// @brief Add a task to draw the screen one more time, until all the animations /// are done. void SFMLScreen::RequestAnimationFrame() { if (animation_requested_) { return; } animation_requested_ = true; auto now = ftxui::animation::Clock::now(); const auto time_histeresis = std::chrono::milliseconds(33); if (now - previous_animation_time_ >= time_histeresis) { previous_animation_time_ = now; } } /// @brief Return whether the main loop has been quit. /// @ingroup component bool SFMLScreen::HasQuitted() { return task_receiver_->HasQuitted(); } // private void SFMLScreen::PreMain() { // Suspend previously active screen: if (g_active_screen) { std::swap(suspended_screen_, g_active_screen); // Reset cursor position to the top of the screen and clear the screen. suspended_screen_->ResetCursorPosition(); std::cout << suspended_screen_->ResetPosition(/*clear=*/true); suspended_screen_->dimx_ = 0; suspended_screen_->dimy_ = 0; // Reset dimensions to force drawing the screen again next time: suspended_screen_->Uninstall(); } // This screen is now active: g_active_screen = this; g_active_screen->Install(); previous_animation_time_ = ftxui::animation::Clock::now(); } // private void SFMLScreen::PostMain() { // Put cursor position at the end of the drawing. ResetCursorPosition(); g_active_screen = nullptr; // Restore suspended screen. if (suspended_screen_) { // Clear screen, and put the cursor at the beginning of the drawing. std::cout << ResetPosition(/*clear=*/true); dimx_ = 0; dimy_ = 0; Uninstall(); std::swap(g_active_screen, suspended_screen_); g_active_screen->Install(); } else { Uninstall(); std::cout << '\r'; // On final exit, keep the current drawing and reset cursor position one // line after it. if (!use_alternative_screen_) { std::cout << std::endl; } } } /// @brief Decorate a function. It executes the same way, but with the currently /// active screen terminal hooks temporarilly uninstalled during its execution. /// @param fn The function to decorate. Closure SFMLScreen::WithRestoredIO(Closure fn) { // NOLINT return [this, fn] { Uninstall(); fn(); Install(); }; } /// @brief Return the currently active screen, or null if none. // static SFMLScreen* SFMLScreen::Active() { return g_active_screen; } // private void SFMLScreen::Install() { frame_valid_ = false; // After uninstalling the new configuration, flush it to the terminal to // ensure it is fully applied: on_exit_functions.push([] { Flush(); }); on_exit_functions.push([this] { ExitLoopClosure()(); }); // After installing the new configuration, flush it to the terminal to // ensure it is fully applied: Flush(); task_sender_ = task_receiver_->MakeSender(); event_listener_ = std::thread(&EventListener, task_receiver_->MakeSender()); animation_listener_ = std::thread(&AnimationListener, task_receiver_->MakeSender()); } // private void SFMLScreen::Uninstall() { ExitNow(); event_listener_.join(); animation_listener_.join(); OnExit(); } // private // NOLINTNEXTLINE void SFMLScreen::RunOnceBlocking(Component component) { Task task; if (task_receiver_->Receive(&task)) { HandleTask(component, task); } RunOnce(component); } // private void SFMLScreen::RunOnce(Component component) { Task task; while (task_receiver_->ReceiveNonBlocking(&task)) { HandleTask(component, task); } Draw(std::move(component)); } // private void SFMLScreen::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_; } // ZED: 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; const ftxui::animation::TimePoint now = ftxui::animation::Clock::now(); const ftxui::animation::Duration delta = now - previous_animation_time_; previous_animation_time_ = now; ftxui::animation::Params params(delta); component->OnAnimation(params); frame_valid_ = false; return; } }, task); // clang-format on } // private // NOLINTNEXTLINE void SFMLScreen::Draw(Component component) { if (frame_valid_) { return; } auto document = component->Render(); int dimx = 0; int dimy = 0; // ZED: replace this // auto terminal = Terminal::Size(); document->ComputeRequirement(); switch (dimension_) { case Dimension::Fixed: dimx = dimx_; dimy = dimy_; break; case Dimension::TerminalOutput: assert(false && "NOT IMPLEMENTED!"); // dimx = terminal.dimx; // dimy = document->requirement().min_y; break; case Dimension::Fullscreen: assert(false && "NOT IMPLEMENTED!"); // dimx = terminal.dimx; // dimy = terminal.dimy; break; case Dimension::FitComponent: assert(false && "NOT IMPLEMENTED!"); // dimx = std::min(document->requirement().min_x, terminal.dimx); // dimy = std::min(document->requirement().min_y, terminal.dimy); break; } const bool resized = (dimx != dimx_) || (dimy != dimy_); ResetCursorPosition(); std::cout << ResetPosition(/*clear=*/resized); // Resize the screen if needed. if (resized) { dimx_ = dimx; dimy_ = dimy; pixels_ = std::vector>(dimy, std::vector(dimx)); cursor_.x = dimx_ - 1; cursor_.y = dimy_ - 1; } // ZED: I removed a bunch of terminal stuff but probably need to bring back // resizing? // previous_frame_resized_ = resized; Render(*this, document); std::cout << ToString() << set_cursor_position; Flush(); Clear(); frame_valid_ = true; } // private void SFMLScreen::ResetCursorPosition() { std::cout << reset_cursor_position; reset_cursor_position = ""; } /// @brief Return a function to exit the main loop. /// @ingroup component Closure SFMLScreen::ExitLoopClosure() { return [this] { Exit(); }; } /// @brief Exit the main loop. /// @ingroup component void SFMLScreen::Exit() { Post([this] { ExitNow(); }); } // private: void SFMLScreen::ExitNow() { task_sender_.reset(); }