diff --git a/.github/template/template_name b/.github/template/template_name index b4857fcb..d82c7112 100644 --- a/.github/template/template_name +++ b/.github/template/template_name @@ -1 +1 @@ -cmake_conan_boilerplate_template +ftxui_template diff --git a/.github/template/template_repository b/.github/template/template_repository index efbdeb18..310718d0 100644 --- a/.github/template/template_repository +++ b/.github/template/template_repository @@ -1 +1 @@ -cpp-best-practices/cmake_conan_boilerplate_template +cpp-best-practices/ftxui_template diff --git a/CMakeLists.txt b/CMakeLists.txt index 324628e7..bb30f498 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,22 @@ set(CMAKE_CXX_STANDARD 20) # when compiling with PCH enabled set(CMAKE_CXX_EXTENSIONS OFF) +include(FetchContent) + +set(FETCHCONTENT_UPDATES_DISCONNECTED TRUE) +FetchContent_Declare(ftxui + GIT_REPOSITORY https://github.com/ArthurSonzogni/ftxui + GIT_TAG v2.0.0 +) + +FetchContent_GetProperties(ftxui) +if(NOT ftxui_POPULATED) + FetchContent_Populate(ftxui) + add_subdirectory(${ftxui_SOURCE_DIR} ${ftxui_BINARY_DIR} EXCLUDE_FROM_ALL) +endif() + + + # Note: by default ENABLE_DEVELOPER_MODE is True # This means that all analysis (sanitizers, static analysis) # is enabled and all warnings are treated as errors diff --git a/README.md b/README.md index 8b9af443..906afc37 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# cmake_conan_boilerplate_template +# ftxui_template -[![ci](https://github.com/cpp-best-practices/cmake_conan_boilerplate_template/actions/workflows/ci.yml/badge.svg)](https://github.com/cpp-best-practices/cmake_conan_boilerplate_template/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/cpp-best-practices/cmake_conan_boilerplate_template/branch/main/graph/badge.svg)](https://codecov.io/gh/cpp-best-practices/cmake_conan_boilerplate_template) -[![Language grade: C++](https://img.shields.io/lgtm/grade/cpp/github/cpp-best-practices/cmake_conan_boilerplate_template)](https://lgtm.com/projects/g/cpp-best-practices/cmake_conan_boilerplate_template/context:cpp) -[![CodeQL](https://github.com/cpp-best-practices/cmake_conan_boilerplate_template/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/cpp-best-practices/cmake_conan_boilerplate_template/actions/workflows/codeql-analysis.yml) +[![ci](https://github.com/cpp-best-practices/ftxui_template/actions/workflows/ci.yml/badge.svg)](https://github.com/cpp-best-practices/ftxui_template/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/cpp-best-practices/ftxui_template/branch/main/graph/badge.svg)](https://codecov.io/gh/cpp-best-practices/ftxui_template) +[![Language grade: C++](https://img.shields.io/lgtm/grade/cpp/github/cpp-best-practices/ftxui_template)](https://lgtm.com/projects/g/cpp-best-practices/ftxui_template/context:cpp) +[![CodeQL](https://github.com/cpp-best-practices/ftxui_template/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/cpp-best-practices/ftxui_template/actions/workflows/codeql-analysis.yml) -## About cmake_conan_boilerplate_template +## About ftxui_template This is a C++ Best Practices GitHub template for getting up and running with C++ quickly. @@ -28,15 +28,15 @@ It requires * conan * a compiler -If you want a more complex example project, check out the [cpp_starter_project](https://github.com/cpp-best-practices/cpp_starter_project). -Ths Boilerplate project will merge new features first, then they will be merged (as appropriate) into cpp_starter_project. +This project gets you started with a simple example of using FTXUI, which happens to also be a game. + ## Getting Started ### Use the Github template First, click the green `Use this template` button near the top of this page. -This will take you to Github's ['Generate Repository'](https://github.com/cpp-best-practices/cmake_conan_boilerplate_template/generate) page. +This will take you to Github's ['Generate Repository'](https://github.com/cpp-best-practices/ftxui_template/generate) page. Fill in a repository name and short description, and click 'Create repository from template'. This will allow you to create a new repository in your Github account, prepopulated with the contents of this project. diff --git a/conanfile.txt b/conanfile.txt index a92cc590..8757fda7 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -4,6 +4,7 @@ catch2/2.13.9 cli11/2.2.0 spdlog/1.10.0 +ftxui/2.0.0 [generators] cmake_find_package_multi diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ab05fe9f..75023d4c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,12 +1,20 @@ find_package(fmt CONFIG REQUIRED) find_package(spdlog CONFIG REQUIRED) find_package(CLI11 CONFIG REQUIRED) +find_package(ftxui CONFIG REQUIRED) # Generic test that uses conan libs add_executable(intro main.cpp) + target_link_libraries( intro - PUBLIC project_options project_warnings - PRIVATE CLI11::CLI11 fmt::fmt spdlog::spdlog) + PRIVATE project_options + project_warnings + docopt::docopt + fmt::fmt + spdlog::spdlog + ftxui::screen + ftxui::dom + ftxui::component) target_include_directories(intro PRIVATE "${CMAKE_BINARY_DIR}/configured_files/include") diff --git a/src/main.cpp b/src/main.cpp index 9793e979..a2545033 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,15 +1,308 @@ +#include #include #include #include +#include + #include +#include // for ftxui +#include // for Slider +#include // for ScreenInteractive #include -// This file will be generated automatically when you run the CMake configuration step. -// It creates a namespace called `myproject`. -// You can modify the source template at `configured_files/config.hpp.in`. +// This file will be generated automatically when you run the CMake +// configuration step. It creates a namespace called `myproject`. You can modify +// the source template at `configured_files/config.hpp.in`. #include +template struct GameBoard +{ + static constexpr std::size_t width = Width; + static constexpr std::size_t height = Height; + + std::array, width> strings; + std::array, width> values{}; + + std::size_t move_count{ 0 }; + + std::string &get_string(std::size_t x, std::size_t y) { return strings.at(x).at(y); } + + + void set(std::size_t x, std::size_t y, bool new_value) + { + get(x, y) = new_value; + + if (new_value) { + get_string(x, y) = " ON"; + } else { + get_string(x, y) = "OFF"; + } + } + + void visit(auto visitor) + { + for (std::size_t x = 0; x < width; ++x) { + for (std::size_t y = 0; y < height; ++y) { visitor(x, y, *this); } + } + } + + [[nodiscard]] bool get(std::size_t x, std::size_t y) const { return values.at(x).at(y); } + + [[nodiscard]] bool &get(std::size_t x, std::size_t y) { return values.at(x).at(y); } + + GameBoard() + { + visit([](const auto x, const auto y, auto &gameboard) { gameboard.set(x, y, true); }); + } + + void update_strings() + { + for (std::size_t x = 0; x < width; ++x) { + for (std::size_t y = 0; y < height; ++y) { set(x, y, get(x, y)); } + } + } + + void toggle(std::size_t x, std::size_t y) { set(x, y, !get(x, y)); } + + void press(std::size_t x, std::size_t y) + { + ++move_count; + toggle(x, y); + if (x > 0) { toggle(x - 1, y); } + if (y > 0) { toggle(x, y - 1); } + if (x < width - 1) { toggle(x + 1, y); } + if (y < height - 1) { toggle(x, y + 1); } + } + + [[nodiscard]] bool solved() const + { + for (std::size_t x = 0; x < width; ++x) { + for (std::size_t y = 0; y < height; ++y) { + if (!get(x, y)) { return false; } + } + } + + return true; + } +}; + + +void consequence_game() +{ + auto screen = ftxui::ScreenInteractive::TerminalOutput(); + + GameBoard<3, 3> gb; + + std::string quit_text; + + const auto update_quit_text = [&quit_text](const auto &game_board) { + quit_text = fmt::format("Quit ({} moves)", game_board.move_count); + if (game_board.solved()) { quit_text += " Solved!"; } + }; + + const auto make_buttons = [&] { + std::vector buttons; + for (std::size_t x = 0; x < gb.width; ++x) { + for (std::size_t y = 0; y < gb.height; ++y) { + buttons.push_back(ftxui::Button(&gb.get_string(x, y), [=, &gb] { + if (!gb.solved()) { gb.press(x, y); } + update_quit_text(gb); + })); + } + } + return buttons; + }; + + auto buttons = make_buttons(); + + auto quit_button = ftxui::Button(&quit_text, screen.ExitLoopClosure()); + + auto make_layout = [&] { + std::vector rows; + + std::size_t idx = 0; + + for (std::size_t x = 0; x < gb.width; ++x) { + std::vector row; + for (std::size_t y = 0; y < gb.height; ++y) { + row.push_back(buttons[idx]->Render()); + ++idx; + } + rows.push_back(ftxui::hbox(std::move(row))); + } + + rows.push_back(ftxui::hbox({ quit_button->Render() })); + + return ftxui::vbox(std::move(rows)); + }; + + + static constexpr int randomization_iterations = 100; + static constexpr int random_seed = 42; + + std::mt19937 gen32{ random_seed };// NOLINT fixed seed + std::uniform_int_distribution x(static_cast(0), gb.width - 1); + std::uniform_int_distribution y(static_cast(0), gb.height - 1); + + for (int i = 0; i < randomization_iterations; ++i) { gb.press(x(gen32), y(gen32)); } + gb.move_count = 0; + update_quit_text(gb); + + auto all_buttons = buttons; + all_buttons.push_back(quit_button); + auto container = ftxui::Container::Horizontal(all_buttons); + + auto renderer = ftxui::Renderer(container, make_layout); + + screen.Loop(renderer); +} + +struct Color +{ + std::uint8_t R{}; + std::uint8_t G{}; + std::uint8_t B{}; +}; + +// A simple way of representing a bitmap on screen using only characters +struct Bitmap : ftxui::Node +{ + Bitmap(std::size_t width, std::size_t height)// NOLINT same typed parameters adjacent to each other + : width_(width), height_(height) + {} + + Color &at(std::size_t x, std::size_t y) { return pixels.at(width_ * y + x); } + + void ComputeRequirement() override + { + requirement_ = ftxui::Requirement{ + .min_x = static_cast(width_), .min_y = static_cast(height_ / 2), .selected_box{ 0, 0, 0, 0 } + }; + } + + void Render(ftxui::Screen &screen) override + { + for (std::size_t x = 0; x < width_; ++x) { + for (std::size_t y = 0; y < height_ / 2; ++y) { + auto &p = screen.PixelAt(box_.x_min + static_cast(x), box_.y_min + static_cast(y)); + p.character = "▄"; + const auto &top_color = at(x, y * 2); + const auto &bottom_color = at(x, y * 2 + 1); + p.background_color = ftxui::Color{ top_color.R, top_color.G, top_color.B }; + p.foreground_color = ftxui::Color{ bottom_color.R, bottom_color.G, bottom_color.B }; + } + } + } + + [[nodiscard]] auto width() const noexcept { return width_; } + + [[nodiscard]] auto height() const noexcept { return height_; } + + [[nodiscard]] auto &data() noexcept { return pixels; } + +private: + std::size_t width_; + std::size_t height_; + + std::vector pixels = std::vector(width_ * height_, Color{}); +}; + +void game_iteration_canvas() +{ + // this should probably have a `bitmap` helper function that does what you expect + // similar to the other parts of FTXUI + auto bm = std::make_shared(50, 50);// NOLINT magic numbers + auto small_bm = std::make_shared(6, 6);// NOLINT magic numbers + + double fps = 0; + + std::size_t max_row = 0; + std::size_t max_col = 0; + + // to do, add total game time clock also, not just current elapsed time + auto game_iteration = [&](const std::chrono::steady_clock::duration elapsed_time) { + // in here we simulate however much game time has elapsed. Update animations, + // run character AI, whatever, update stats, etc + + // this isn't actually timing based for now, it's just updating the display however fast it can + fps = 1.0 + / (static_cast(std::chrono::duration_cast(elapsed_time).count()) + / 1'000'000.0);// NOLINT magic numbers + + for (std::size_t row = 0; row < max_row; ++row) { + for (std::size_t col = 0; col < bm->width(); ++col) { ++(bm->at(col, row).R); } + } + + for (std::size_t row = 0; row < bm->height(); ++row) { + for (std::size_t col = 0; col < max_col; ++col) { ++(bm->at(col, row).G); } + } + + // for the fun of it, let's have a second window doing interesting things + auto &small_bm_pixel = + small_bm->data().at(static_cast(elapsed_time.count()) % small_bm->data().size()); + + switch (elapsed_time.count() % 3) { + case 0: + small_bm_pixel.R += 11;// NOLINT Magic Number + break; + case 1: + small_bm_pixel.G += 11;// NOLINT Magic Number + break; + case 2: + small_bm_pixel.B += 11;// NOLINT Magic Number + break; + } + + + ++max_row; + if (max_row >= bm->height()) { max_row = 0; } + ++max_col; + if (max_col >= bm->width()) { max_col = 0; } + }; + + auto screen = ftxui::ScreenInteractive::TerminalOutput(); + + int counter = 0; + + auto last_time = std::chrono::steady_clock::now(); + + auto make_layout = [&] { + // This code actually processes the draw event + const auto new_time = std::chrono::steady_clock::now(); + + ++counter; + // we will dispatch to the game_iteration function, where the work happens + game_iteration(new_time - last_time); + last_time = new_time; + + // now actually draw the game elements + return ftxui::hbox({ bm | ftxui::border, + ftxui::vbox({ ftxui::text("Frame: " + std::to_string(counter)), + ftxui::text("FPS: " + std::to_string(fps)), + small_bm | ftxui::border }) }); + }; + + auto renderer = ftxui::Renderer(make_layout); + + + std::atomic refresh_ui_continue = true; + + // This thread exists to make sure that the event queue has an event to + // process at approximately a rate of 30 FPS + std::thread refresh_ui([&] { + while (refresh_ui_continue) { + using namespace std::chrono_literals; + std::this_thread::sleep_for(1.0s / 30.0);// NOLINT magic numbers + screen.PostEvent(ftxui::Event::Custom); + } + }); + + screen.Loop(renderer); + + refresh_ui_continue = false; + refresh_ui.join(); +} // NOLINTNEXTLINE(bugprone-exception-escape) int main(int argc, const char **argv) @@ -22,6 +315,16 @@ int main(int argc, const char **argv) bool show_version = false; app.add_flag("--version", show_version, "Show version information"); + bool is_turn_based = false; + auto *turn_based = app.add_flag("turn_based", is_turn_based); + + bool is_loop_based = false; + auto *loop_based = app.add_flag("loop_based", is_loop_based); + + turn_based->excludes(loop_based); + loop_based->excludes(turn_based); + + CLI11_PARSE(app, argc, argv); if (show_version) { @@ -29,14 +332,12 @@ int main(int argc, const char **argv) return EXIT_SUCCESS; } - // Use the default logger (stdout, multi-threaded, colored) - spdlog::info("Hello, {}!", "World"); - - if (message) { - fmt::print("Message: '{}'\n", *message); + if (is_turn_based) { + consequence_game(); } else { - fmt::print("No Message Provided :(\n"); + game_iteration_canvas(); } + } catch (const std::exception &e) { spdlog::error("Unhandled exception in main: {}", e.what()); }