diff --git a/gui.cpp b/gui.cpp index ebcbd1d..2a1a313 100644 --- a/gui.cpp +++ b/gui.cpp @@ -33,7 +33,7 @@ GUI::GUI(DinkyECS::World &world, Map& game_map) : $game_map(game_map), $log({{"Welcome to the game!"}}), $status_ui(SCREEN_X, SCREEN_Y, 0, 0), - $map_view(0, 0, GAME_MAP_POS, 0, false), + $map_view(30, 10, GAME_MAP_POS, 0, false), $view_port{0,0}, $world(world), $sounds("./assets"), diff --git a/input_parser.cpp b/input_parser.cpp new file mode 100644 index 0000000..33d0eeb --- /dev/null +++ b/input_parser.cpp @@ -0,0 +1,419 @@ +// 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 "input_parser.hpp" + +#include // for uint32_t +#include // for Mouse, Mouse::Button, Mouse::Motion +#include // for SenderImpl, Sender +#include +#include // for unique_ptr, allocator +#include // for move + +#include "ftxui/component/event.hpp" // for Event +#include "ftxui/component/task.hpp" // for Task + +namespace ftxui { + +// NOLINTNEXTLINE +const std::map g_uniformize = { + // Microsoft's terminal uses a different new line character for the return + // key. This also happens with linux with the `bind` command: + // See https://github.com/ArthurSonzogni/FTXUI/issues/337 + // Here, we uniformize the new line character to `\n`. + {"\r", "\n"}, + + // See: https://github.com/ArthurSonzogni/FTXUI/issues/508 + {std::string({8}), std::string({127})}, + + // See: https://github.com/ArthurSonzogni/FTXUI/issues/626 + // + // Depending on the Cursor Key Mode (DECCKM), the terminal sends different + // escape sequences: + // + // Key Normal Application + // ----- -------- ----------- + // Up ESC [ A ESC O A + // Down ESC [ B ESC O B + // Right ESC [ C ESC O C + // Left ESC [ D ESC O D + // Home ESC [ H ESC O H + // End ESC [ F ESC O F + // + {"\x1BOA", "\x1B[A"}, // UP + {"\x1BOB", "\x1B[B"}, // DOWN + {"\x1BOC", "\x1B[C"}, // RIGHT + {"\x1BOD", "\x1B[D"}, // LEFT + {"\x1BOH", "\x1B[H"}, // HOME + {"\x1BOF", "\x1B[F"}, // END + + // Variations around the FN keys. + // Internally, we are using: + // vt220, xterm-vt200, xterm-xf86-v44, xterm-new, mgt, screen + // See: https://invisible-island.net/xterm/xterm-function-keys.html + + // For linux OS console (CTRL+ALT+FN), who do not belong to any + // real standard. + // See: https://github.com/ArthurSonzogni/FTXUI/issues/685 + {"\x1B[[A", "\x1BOP"}, // F1 + {"\x1B[[B", "\x1BOQ"}, // F2 + {"\x1B[[C", "\x1BOR"}, // F3 + {"\x1B[[D", "\x1BOS"}, // F4 + {"\x1B[[E", "\x1B[15~"}, // F5 + + // xterm-r5, xterm-r6, rxvt + {"\x1B[11~", "\x1BOP"}, // F1 + {"\x1B[12~", "\x1BOQ"}, // F2 + {"\x1B[13~", "\x1BOR"}, // F3 + {"\x1B[14~", "\x1BOS"}, // F4 + + // vt100 + {"\x1BOt", "\x1B[15~"}, // F5 + {"\x1BOu", "\x1B[17~"}, // F6 + {"\x1BOv", "\x1B[18~"}, // F7 + {"\x1BOl", "\x1B[19~"}, // F8 + {"\x1BOw", "\x1B[20~"}, // F9 + {"\x1BOx", "\x1B[21~"}, // F10 + + // scoansi + {"\x1B[M", "\x1BOP"}, // F1 + {"\x1B[N", "\x1BOQ"}, // F2 + {"\x1B[O", "\x1BOR"}, // F3 + {"\x1B[P", "\x1BOS"}, // F4 + {"\x1B[Q", "\x1B[15~"}, // F5 + {"\x1B[R", "\x1B[17~"}, // F6 + {"\x1B[S", "\x1B[18~"}, // F7 + {"\x1B[T", "\x1B[19~"}, // F8 + {"\x1B[U", "\x1B[20~"}, // F9 + {"\x1B[V", "\x1B[21~"}, // F10 + {"\x1B[W", "\x1B[23~"}, // F11 + {"\x1B[X", "\x1B[24~"}, // F12 +}; + +TerminalInputParser::TerminalInputParser(Sender out) + : out_(std::move(out)) {} + +void TerminalInputParser::Timeout(int time) { + timeout_ += time; + const int timeout_threshold = 50; + if (timeout_ < timeout_threshold) { + return; + } + timeout_ = 0; + if (!pending_.empty()) { + Send(SPECIAL); + } +} + +void TerminalInputParser::Add(char c) { + pending_ += c; + timeout_ = 0; + position_ = -1; + Send(Parse()); +} + +unsigned char TerminalInputParser::Current() { + return pending_[position_]; +} + +bool TerminalInputParser::Eat() { + position_++; + return position_ < static_cast(pending_.size()); +} + +void TerminalInputParser::Send(TerminalInputParser::Output output) { + switch (output.type) { + case UNCOMPLETED: + return; + + case DROP: + pending_.clear(); + return; + + case CHARACTER: + out_->Send(Event::Character(std::move(pending_))); + pending_.clear(); + return; + + case SPECIAL: { + auto it = g_uniformize.find(pending_); + if (it != g_uniformize.end()) { + pending_ = it->second; + } + out_->Send(Event::Special(std::move(pending_))); + pending_.clear(); + } + return; + + case MOUSE: + out_->Send(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT + pending_.clear(); + return; + + case CURSOR_REPORTING: + out_->Send(Event::CursorReporting(std::move(pending_), // NOLINT + output.cursor.x, // NOLINT + output.cursor.y)); // NOLINT + pending_.clear(); + return; + } + // NOT_REACHED(). +} + +TerminalInputParser::Output TerminalInputParser::Parse() { + if (!Eat()) { + return UNCOMPLETED; + } + + switch (Current()) { + case 24: // CAN NOLINT + case 26: // SUB NOLINT + return DROP; + + case '\x1B': + return ParseESC(); + default: + break; + } + + if (Current() < 32) { // C0 NOLINT + return SPECIAL; + } + + if (Current() == 127) { // Delete // NOLINT + return SPECIAL; + } + + return ParseUTF8(); +} + +// Code point <-> UTF-8 conversion +// +// ┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓ +// ┃Byte 1 ┃Byte 2 ┃Byte 3 ┃Byte 4 ┃ +// ┡━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩ +// │0xxxxxxx│ │ │ │ +// ├────────┼────────┼────────┼────────┤ +// │110xxxxx│10xxxxxx│ │ │ +// ├────────┼────────┼────────┼────────┤ +// │1110xxxx│10xxxxxx│10xxxxxx│ │ +// ├────────┼────────┼────────┼────────┤ +// │11110xxx│10xxxxxx│10xxxxxx│10xxxxxx│ +// └────────┴────────┴────────┴────────┘ +// +// Then some sequences are illegal if it exist a shorter representation of the +// same codepoint. +TerminalInputParser::Output TerminalInputParser::ParseUTF8() { + auto head = Current(); + unsigned char selector = 0b1000'0000; // NOLINT + + // The non code-point part of the first byte. + unsigned char mask = selector; + + // Find the first zero in the first byte. + unsigned int first_zero = 8; // NOLINT + for (unsigned int i = 0; i < 8; ++i) { // NOLINT + mask |= selector; + if (!(head & selector)) { + first_zero = i; + break; + } + selector >>= 1U; + } + + // Accumulate the value of the first byte. + auto value = uint32_t(head & ~mask); // NOLINT + + // Invalid UTF8, with more than 5 bytes. + const unsigned int max_utf8_bytes = 5; + if (first_zero == 1 || first_zero >= max_utf8_bytes) { + return DROP; + } + + // Multi byte UTF-8. + for (unsigned int i = 2; i <= first_zero; ++i) { + if (!Eat()) { + return UNCOMPLETED; + } + + // Invalid continuation byte. + head = Current(); + if ((head & 0b1100'0000) != 0b1000'0000) { // NOLINT + return DROP; + } + value <<= 6; // NOLINT + value += head & 0b0011'1111; // NOLINT + } + + // Check for overlong UTF8 encoding. + int extra_byte = 0; + if (value <= 0b000'0000'0111'1111) { // NOLINT + extra_byte = 0; // NOLINT + } else if (value <= 0b000'0111'1111'1111) { // NOLINT + extra_byte = 1; // NOLINT + } else if (value <= 0b1111'1111'1111'1111) { // NOLINT + extra_byte = 2; // NOLINT + } else if (value <= 0b1'0000'1111'1111'1111'1111) { // NOLINT + extra_byte = 3; // NOLINT + } else { // NOLINT + return DROP; + } + + if (extra_byte != position_) { + return DROP; + } + + return CHARACTER; +} + +TerminalInputParser::Output TerminalInputParser::ParseESC() { + if (!Eat()) { + return UNCOMPLETED; + } + switch (Current()) { + case 'P': + return ParseDCS(); + case '[': + return ParseCSI(); + case ']': + return ParseOSC(); + default: + if (!Eat()) { + return UNCOMPLETED; + } else { + return SPECIAL; + } + } +} + +TerminalInputParser::Output TerminalInputParser::ParseDCS() { + // Parse until the string terminator ST. + while (true) { + if (!Eat()) { + return UNCOMPLETED; + } + + if (Current() != '\x1B') { + continue; + } + + if (!Eat()) { + return UNCOMPLETED; + } + + if (Current() != '\\') { + continue; + } + + return SPECIAL; + } +} + +TerminalInputParser::Output TerminalInputParser::ParseCSI() { + bool altered = false; + int argument = 0; + std::vector arguments; + while (true) { + if (!Eat()) { + return UNCOMPLETED; + } + + if (Current() == '<') { + altered = true; + continue; + } + + if (Current() >= '0' && Current() <= '9') { + argument *= 10; // NOLINT + argument += Current() - '0'; + continue; + } + + if (Current() == ';') { + arguments.push_back(argument); + argument = 0; + continue; + } + + // CSI is terminated by a character in the range 0x40–0x7E + // (ASCII @A–Z[\]^_`a–z{|}~), + if (Current() >= '@' && Current() <= '~' && + // Note: I don't remember why we exclude '<' + Current() != '<' && + // To handle F1-F4, we exclude '['. + Current() != '[') { + arguments.push_back(argument); + argument = 0; // NOLINT + + switch (Current()) { + case 'M': + return ParseMouse(altered, true, std::move(arguments)); + case 'm': + return ParseMouse(altered, false, std::move(arguments)); + case 'R': + return ParseCursorReporting(std::move(arguments)); + default: + return SPECIAL; + } + } + + // Invalid ESC in CSI. + if (Current() == '\x1B') { + return SPECIAL; + } + } +} + +TerminalInputParser::Output TerminalInputParser::ParseOSC() { + // Parse until the string terminator ST. + while (true) { + if (!Eat()) { + return UNCOMPLETED; + } + if (Current() != '\x1B') { + continue; + } + if (!Eat()) { + return UNCOMPLETED; + } + if (Current() != '\\') { + continue; + } + return SPECIAL; + } +} + +TerminalInputParser::Output TerminalInputParser::ParseMouse( // NOLINT + bool altered, + bool pressed, + std::vector arguments) { + if (arguments.size() != 3) { + return SPECIAL; + } + + (void)altered; + + Output output(MOUSE); + output.mouse.button = Mouse::Button((arguments[0] & 3) + // NOLINT + ((arguments[0] & 64) >> 4)); // NOLINT + output.mouse.motion = Mouse::Motion(pressed); // NOLINT + output.mouse.shift = bool(arguments[0] & 4); // NOLINT + output.mouse.meta = bool(arguments[0] & 8); // NOLINT + output.mouse.x = arguments[1]; // NOLINT + output.mouse.y = arguments[2]; // NOLINT + return output; +} + +// NOLINTNEXTLINE +TerminalInputParser::Output TerminalInputParser::ParseCursorReporting( + std::vector arguments) { + if (arguments.size() != 2) { + return SPECIAL; + } + Output output(CURSOR_REPORTING); + output.cursor.y = arguments[0]; // NOLINT + output.cursor.x = arguments[1]; // NOLINT + return output; +} + +} // namespace ftxui diff --git a/input_parser.hpp b/input_parser.hpp new file mode 100644 index 0000000..5a808c5 --- /dev/null +++ b/input_parser.hpp @@ -0,0 +1,72 @@ +// 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. +#ifndef FTXUI_COMPONENT_TERMINAL_INPUT_PARSER +#define FTXUI_COMPONENT_TERMINAL_INPUT_PARSER + +#include // for unique_ptr +#include // for string +#include // for vector + +#include "ftxui/component/event.hpp" // for Event (ptr only) +#include "ftxui/component/mouse.hpp" // for Mouse +#include "ftxui/component/receiver.hpp" // for Sender +#include "ftxui/component/task.hpp" // for Task + +namespace ftxui { +struct Event; + +// Parse a sequence of |char| accross |time|. Produces |Event|. +class TerminalInputParser { + public: + TerminalInputParser(Sender out); + void Timeout(int time); + void Add(char c); + + private: + unsigned char Current(); + bool Eat(); + + enum Type { + UNCOMPLETED, + DROP, + CHARACTER, + SPECIAL, + MOUSE, + CURSOR_REPORTING, + }; + + struct CursorReporting { + int x; + int y; + }; + + struct Output { + Type type; + union { + Mouse mouse; + CursorReporting cursor; + }; + + Output(Type t) : type(t) {} + }; + + void Send(Output output); + Output Parse(); + Output ParseUTF8(); + Output ParseESC(); + Output ParseDCS(); + Output ParseCSI(); + Output ParseOSC(); + Output ParseMouse(bool altered, bool pressed, std::vector arguments); + Output ParseCursorReporting(std::vector arguments); + + Sender out_; + int position_ = -1; + int timeout_ = 0; + std::string pending_; +}; + +} // namespace ftxui + +#endif /* end of include guard: FTXUI_COMPONENT_TERMINAL_INPUT_PARSER */ diff --git a/meson.build b/meson.build index 43c835e..d01a2a9 100644 --- a/meson.build +++ b/meson.build @@ -49,6 +49,7 @@ roguish = executable('roguish', [ 'render.cpp', 'config.cpp', 'save.cpp', + 'sfml_screen.cpp', 'panel.cpp', ], dependencies: dependencies) diff --git a/panel.cpp b/panel.cpp index 01e53e7..79be353 100644 --- a/panel.cpp +++ b/panel.cpp @@ -2,7 +2,7 @@ void Panel::resize(int width, int height) { $dirty = true; - $screen = Screen(width, height); + // $screen = ScreenInteractive::FixedSize(width, height); } void Panel::set_renderer(std::function< Element()> render) { diff --git a/panel.hpp b/panel.hpp index 3be0c85..e99e9c9 100644 --- a/panel.hpp +++ b/panel.hpp @@ -1,12 +1,12 @@ #pragma once #include // for Render #include -#include #include #include #include #include #include +#include "sfml_screen.hpp" // for SFMLScreen #include #include @@ -20,7 +20,7 @@ struct Panel { std::wstring $screenout; bool $dirty = true; Component $component; - Screen $screen; + SFMLScreen $screen; bool $must_clear = true; std::wstring_convert> $converter; @@ -29,7 +29,7 @@ struct Panel { y(y), width(width), height(height), - $screen(width, height), + $screen(SFMLScreen::FixedSize(width, height)), $must_clear(must_clear) {}; diff --git a/sfml_screen.cpp b/sfml_screen.cpp new file mode 100644 index 0000000..e068ed3 --- /dev/null +++ b/sfml_screen.cpp @@ -0,0 +1,460 @@ +// 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(); +} diff --git a/sfml_screen.hpp b/sfml_screen.hpp new file mode 100644 index 0000000..eab1009 --- /dev/null +++ b/sfml_screen.hpp @@ -0,0 +1,113 @@ +// 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. +#ifndef FTXUI_COMPONENT_SCREEN_INTERACTIVE_HPP +#define FTXUI_COMPONENT_SCREEN_INTERACTIVE_HPP + +#include // for atomic +#include // for Receiver, Sender +#include // for function +#include // for shared_ptr +#include // for string +#include // for thread +#include // for variant + +#include // for TimePoint +#include // for CapturedMouse +#include // for Event +#include // for Task, Closure +#include // for Screen + +using ftxui::Component, ftxui::Task, ftxui::Closure, ftxui::Event, ftxui::Sender, ftxui::Receiver; + +namespace ftxui { + class ComponentBase; + struct Event; + + using Component = std::shared_ptr; + class SFMLScreenPrivate; +} + +class SFMLScreen : public ftxui::Screen { + public: + // Constructors: + static SFMLScreen FixedSize(int dimx, int dimy); + static SFMLScreen Fullscreen(); + static SFMLScreen FitComponent(); + static SFMLScreen TerminalOutput(); + + // Options. Must be called before Loop(). + void TrackMouse(bool enable = true); + + // Return the currently active screen, nullptr if none. + static SFMLScreen* Active(); + + // Start/Stop the main loop. + void Exit(); + Closure ExitLoopClosure(); + + // Post tasks to be executed by the loop. + void Post(Task task); + void PostEvent(Event event); + void RequestAnimationFrame(); + + // Decorate a function. The outputted one will execute similarly to the + // inputted one, but with the currently active screen terminal hooks + // temporarily uninstalled. + ftxui::Closure WithRestoredIO(ftxui::Closure); + + void ExitNow(); + + void Install(); + void Uninstall(); + + 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 ResetCursorPosition(); + + SFMLScreen* suspended_screen_ = nullptr; + enum class Dimension { + FitComponent, + Fixed, + Fullscreen, + TerminalOutput, + }; + Dimension dimension_ = Dimension::Fixed; + bool use_alternative_screen_ = false; + + SFMLScreen(int dimx, + int dimy, + Dimension dimension, + bool use_alternative_screen); + + bool track_mouse_ = true; + + Sender task_sender_; + Receiver task_receiver_; + + std::string set_cursor_position; + std::string reset_cursor_position; + + std::thread event_listener_; + std::thread animation_listener_; + bool animation_requested_ = false; + ftxui::animation::TimePoint previous_animation_time_; + + int cursor_x_ = 1; + int cursor_y_ = 1; + + bool mouse_captured = false; + bool previous_frame_resized_ = false; + + bool frame_valid_ = false; +}; + + +#endif /* end of include guard: FTXUI_COMPONENT_SCREEN_INTERACTIVE_HPP */ diff --git a/status.txt b/status.txt index 94a1d9f..e609b5a 100644 --- a/status.txt +++ b/status.txt @@ -3,8 +3,6 @@ NOTES: TODO: * panels and everything except renderer should use character coodinates -* camera shake broken -* draw_screen doesn't do x axis offset render * Can std::any be defaulted to a noop in the events? * Save file isn't saving gold. * Inventory needs to be better, but need some kinds of "weapons" or other loot to get and not just gold.