The next little game in the series where I make a fancy rogue game.
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.
roguish/sfml_screen.cpp

461 lines
12 KiB

// 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();
}