You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
461 lines
12 KiB
461 lines
12 KiB
1 week ago
|
// 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 <cassert>
|
||
|
#include <algorithm> // for copy, max, min
|
||
|
#include <array> // for array
|
||
|
#include <chrono> // for operator-, milliseconds, operator>=, duration, common_type<>::type, time_point
|
||
|
#include <cstdio> // for fileno, stdin
|
||
|
#include <ftxui/component/task.hpp> // for Task, Closure, AnimationTask
|
||
|
#include <ftxui/screen/screen.hpp> // for Pixel, Screen::Cursor, Screen, Screen::Cursor::Hidden
|
||
|
#include <functional> // for function
|
||
|
#include <initializer_list> // for initializer_list
|
||
|
#include <iostream> // for cout, ostream, operator<<, basic_ostream, endl, flush
|
||
|
#include <stack> // for stack
|
||
|
#include <thread> // for thread, sleep_for
|
||
|
#include <tuple> // for _Swallow_assign, ignore
|
||
|
#include <type_traits> // for decay_t
|
||
|
#include <utility> // for move, swap
|
||
|
#include <variant> // for visit, variant
|
||
|
#include <vector> // for vector
|
||
|
|
||
|
#include <ftxui/component/animation.hpp> // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
|
||
|
#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 ReceiverImpl, Sender, MakeReceiver, SenderImpl, Receiver
|
||
|
#include <ftxui/dom/node.hpp> // for Node, Render
|
||
|
#include <ftxui/dom/requirement.hpp> // for Requirement
|
||
|
#include <ftxui/screen/terminal.hpp> // for Dimensions, Size
|
||
|
#include <fmt/core.h>
|
||
|
#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<Closure> 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<Task> 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<Task> 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<Task>();
|
||
|
}
|
||
|
|
||
|
// 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<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_;
|
||
|
}
|
||
|
|
||
|
// ZED: 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, ftxui::AnimationTask>) {
|
||
|
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<std::vector<ftxui::Pixel>>(dimy, std::vector<ftxui::Pixel>(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();
|
||
|
}
|