commit 9c0343d6e71fc3d55a0c22089d0aa2de974dd7b5 Author: Yohan Boujon Date: Tue May 6 23:01:28 2025 +0200 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..581f8da --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +**/build/** \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3ae5498 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.18) +project( + ftxui_template + DESCRIPTION "FTXUI Template Project" + HOMEPAGE_URL "https://www.etheryo.fr/" + LANGUAGES CXX C +) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# Dependencies +include(dependencies.cmake) + +set(TARGET rmlui_test) +set(SRC "${CMAKE_CURRENT_SOURCE_DIR}/src") +add_executable(${TARGET} + ${SRC}/ui.cpp + ${SRC}/logger.cpp + ${SRC}/event.cpp + ${SRC}/main.cpp +) + +# Include folders +target_include_directories(${TARGET} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_include_directories(${TARGET} PRIVATE "${FTXUI_INCLUDE_DIRS}") + +# Libraries +target_link_libraries( + ${TARGET} PRIVATE + component + dom + screen +) + +# Output folder +set_target_properties(${TARGET} + PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" +) diff --git a/dependencies.cmake b/dependencies.cmake new file mode 100644 index 0000000..6fef553 --- /dev/null +++ b/dependencies.cmake @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.22) #FTXUI asks for CMake 3.22 and C++ 17 + +include(FetchContent) + +FetchContent_Declare(ftxui + GIT_REPOSITORY https://github.com/ArthurSonzogni/ftxui + GIT_TAG v6.0.2 +) + +FetchContent_GetProperties(ftxui) +if(NOT ftxui_POPULATED) + FetchContent_Populate(ftxui) + add_subdirectory(${ftxui_SOURCE_DIR} ${ftxui_BINARY_DIR} EXCLUDE_FROM_ALL) +endif() +set(FTXUI_INCLUDE_DIRS + "${ftxui_SOURCE_DIR}/include" +) \ No newline at end of file diff --git a/include/event.h b/include/event.h new file mode 100644 index 0000000..825183f --- /dev/null +++ b/include/event.h @@ -0,0 +1,53 @@ +#ifndef HEADER_EVENT_FTXUI +#define HEADER_EVENT_FTXUI + +#include +#include +#include +#include + +enum class EventType : uint8_t +{ + SWITCH_SCREEN, + SEND_COMMAND, + LAUNCH_INSTANCE, + STOP, +}; + +struct EventPayload +{ + EventType type; + std::any data; +}; + +class EventHandler +{ +public: + EventHandler(); + void set_handler(std::function function); + void loop(); + void stop(); + +protected: + void add_event(const EventPayload &payload); + friend class Event; + +private: + bool _is_running; + std::function _handler; + std::deque _event_buffer; + std::mutex _mutex; +}; + +class Event +{ +public: + Event(EventHandler &eventhandler); + void send_event(const EventPayload &payload); + void send_event(const EventType &type, std::any data = {}); + +private: + EventHandler &_event_handler; +}; + +#endif // HEADER_EVENT_FTXUI \ No newline at end of file diff --git a/include/logger.h b/include/logger.h new file mode 100644 index 0000000..068da37 --- /dev/null +++ b/include/logger.h @@ -0,0 +1,46 @@ +#ifndef HEADER_LOGGER_FTXUI +#define HEADER_LOGGER_FTXUI + +#include +#include +#include + +enum class LoggerType : uint8_t +{ + _UNDEFINED, + PRINT, + COMMAND, + // You can add more here + _COUNT +}; + +struct LoggerData +{ + std::string str; + uint64_t epoch; + LoggerType type; +}; + +class Logger +{ +public: + static Logger &GetInstance() + { + static Logger instance; + return instance; + } + void set_max_size(int size); + void push_back(std::string str, LoggerType type = LoggerType::_UNDEFINED); + const std::vector &get_buffer(void) noexcept; + +private: + Logger(); + ~Logger(); + void update_view(); + + size_t _maxSize; + std::vector _buffer; + std::vector _view; +}; + +#endif // HEADER_LOGGER_FTXUI diff --git a/include/pages/page.h b/include/pages/page.h new file mode 100644 index 0000000..729e977 --- /dev/null +++ b/include/pages/page.h @@ -0,0 +1,24 @@ +#ifndef HEADER_PAGE_FTXUI +#define HEADER_PAGE_FTXUI + +#include +#include "event.h" + +class ScreenPage : public Event +{ +public: + ScreenPage(EventHandler &handler) + : Event(handler), _page() + { + } + + ftxui::Component *getPage() + { + return &_page; + } + +protected: + ftxui::Component _page; +}; + +#endif // HEADER_PAGE_FTXUI \ No newline at end of file diff --git a/include/pages/page1.h b/include/pages/page1.h new file mode 100644 index 0000000..e69de29 diff --git a/include/pages/page2.h b/include/pages/page2.h new file mode 100644 index 0000000..e69de29 diff --git a/include/ui.h b/include/ui.h new file mode 100644 index 0000000..c10b786 --- /dev/null +++ b/include/ui.h @@ -0,0 +1,39 @@ +#ifndef HEADER_UI_FTXUI +#define HEADER_UI_FTXUI + +#include "pages/page.h" + +#include +#include +#include +#include + +class UserInterface +{ +public: + static UserInterface &GetInstance() + { + static UserInterface instance; + return instance; + } + + ftxui::ScreenInteractive *get_screen(); + void add_screen(ScreenPage *p); + void select_screen(size_t index); + void select_screen(const ScreenPage *p); + void start(); + void update(); + +private: + UserInterface(); + ~UserInterface(); + + ftxui::ScreenInteractive _screen; + std::vector _pages; + ftxui::Component _page_container; + ftxui::Component _main_container; + ftxui::Component _main_renderer; + int _page_index; +}; + +#endif // HEADER_RENDER_FTXUI \ No newline at end of file diff --git a/src/event.cpp b/src/event.cpp new file mode 100644 index 0000000..dcdb852 --- /dev/null +++ b/src/event.cpp @@ -0,0 +1,63 @@ +#include "event.h" + +#include +#include +#include + +constexpr int64_t EVENT_LOOP_WAIT_MS = 100; + +// Event + +Event::Event(EventHandler &eventhandler) + : _event_handler(eventhandler) +{ +} + +void Event::send_event(const EventPayload &payload) +{ + _event_handler.add_event(payload); +} + +void Event::send_event(const EventType &type, std::any data) +{ + _event_handler.add_event({type, data}); +} + +// EventHandler + +EventHandler::EventHandler() + : _is_running(false), _event_buffer({}) +{ +} + +void EventHandler::set_handler(std::function function) +{ + _handler = function; +} + +void EventHandler::add_event(const EventPayload &payload) +{ + std::lock_guard lock(_mutex); + _event_buffer.push_back(payload); +} + +void EventHandler::loop() +{ + _is_running = true; + while (_is_running) + { + if (!_event_buffer.empty()) + { + std::lock_guard lock(_mutex); + EventPayload evt = _event_buffer.front(); + _event_buffer.pop_front(); + _handler(*this, evt); + } + std::this_thread::sleep_for(std::chrono::milliseconds(EVENT_LOOP_WAIT_MS)); + } +} + +void EventHandler::stop() +{ + _is_running = false; +} diff --git a/src/logger.cpp b/src/logger.cpp new file mode 100644 index 0000000..58d6b3b --- /dev/null +++ b/src/logger.cpp @@ -0,0 +1,41 @@ +#include "logger.h" +#include "ui.h" + +#include +#include + +constexpr int MAX_SIZE = 99; + +Logger::Logger() + : _maxSize(MAX_SIZE), _buffer({}) +{ +} + +void Logger::set_max_size(int size) +{ + if (size == _maxSize) + return; + _maxSize = size; + update_view(); +} + +void Logger::push_back(std::string str, LoggerType type) +{ + const uint64_t now = static_cast(std::chrono::system_clock::now().time_since_epoch() / std::chrono::milliseconds(1)); + _buffer.push_back({str, now, type}); + update_view(); + UserInterface::GetInstance().update(); +} + +void Logger::update_view() +{ + typename std::vector::iterator itBegin = _buffer.begin(); + if (_buffer.size() >= _maxSize) + itBegin = _buffer.end() - _maxSize; + + _view.clear(); + std::copy(itBegin, _buffer.end(), std::back_inserter(_view)); +} + +// Getters +const std::vector &Logger::get_buffer(void) noexcept { return _view; } diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..a4677a3 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "ui.h" +#include "event.h" + +using namespace ftxui; + +std::thread t_instance; + +static inline void handle(EventHandler &handler, const EventPayload &payload) +{ + UserInterface &ui = UserInterface::GetInstance(); + + if (payload.type == EventType::STOP) + handler.stop(); + else if (payload.type == EventType::SWITCH_SCREEN) + ui.select_screen(std::any_cast(payload.data)); +} + +int main(int argc, char **argv) +{ + // Event/Screen + EventHandler handler; + UserInterface &ui = UserInterface::GetInstance(); + + // Page declaration + // ScreenPageDebugger debugger(handler, screen); + // screen.add_screen(&launcher); + + // Event handler function + handler.set_handler(handle); + + // Main logic + std::thread t_event(&EventHandler::loop, &handler); + ui.start(); + + // Finish + handler.stop(); + t_event.join(); + return 0; +} \ No newline at end of file diff --git a/src/pages/page1.cpp b/src/pages/page1.cpp new file mode 100644 index 0000000..7c21d1b --- /dev/null +++ b/src/pages/page1.cpp @@ -0,0 +1,130 @@ +#include "pages/debugger.h" +#include "logger.h" + +#include "toolBox.h" + +#include "ftxui/component/component.hpp" +#include "ftxui/component/component_base.hpp" +#include "ftxui/component/event.hpp" +#include "ftxui/component/screen_interactive.hpp" +#include "ftxui/dom/elements.hpp" + +using namespace ftxui; + +constexpr uint8_t LOGGER_OFFSET = 7; + +inline ftxui::Element ScreenPageDebugger::transform(ftxui::InputState state) +{ + if (state.is_placeholder) + state.element |= dim; + + if (state.focused) + { + state.element |= color(Color::White); + input_selected = true; + } + else + { + input_selected = false; + } + + return state.element; +} + +inline bool ScreenPageDebugger::catch_event(Event event) +{ + if (event.is_character()) + { + // Checking if it is a command... + } + + const int size = _render.get_screen()->dimy(); + log_buffer.set_max_size(size - LOGGER_OFFSET); + if (event == Event::Return) + { + send_event(EventType::SEND_COMMAND, cmd_str); + } + return (event == Event::Return); +} + +ftxui::Element ScreenPageDebugger::render_input() +{ + Element arrow = text("> "); + if (input_selected) + arrow |= bgcolor(Color::White) | color(Color::Black); + else + arrow |= color(Color::Default) | bgcolor(Color::Default); + + return vbox({separatorEmpty(), + text("Send an event"), + hbox({arrow, + input_event->Render()})}); +} + +ftxui::Element ScreenPageDebugger::render_log() +{ + Elements log_lines; + for (const auto &logger : log_buffer.get_buffer()) + { + if(logger.type == LoggerType::STUB) { + log_lines.push_back(hbox({ + text(get_time_str(logger.epoch)) | dim, + text(logger.str) + })); + } else { + log_lines.push_back(hbox({ + text(logger.str) | italic | dim + })); + } + } + + auto log_content = vbox(std::move(log_lines)) | yframe; + + return window( + text("log") | hcenter | bold, + log_content | vscroll_indicator | frame) | + flex; +} + +ftxui::Element ScreenPageDebugger::render_status() +{ + return window(text("status") | hcenter | bold, text("content") | center | dim, BorderStyle::EMPTY) | flex | size(WIDTH, GREATER_THAN, 30); +} + +ScreenPageDebugger::ScreenPageDebugger(EventHandler &handler, ScreenRender &sr) + : ScreenPage(handler), _render(sr), input_selected(false) +{ + input_option.transform = [&](const InputState state) + { + return this->transform(state); + }; + + input_event = Input(&cmd_str, "Press 'enter' to send the event. Type '/help' for commands.", input_option); + + Component input = Renderer(input_event, [&]() + { return render_input(); }); + + input |= CatchEvent( + [&](const Event &event) + { + return this->catch_event(event); + }); + + Component log = Renderer([&]() + { return render_log(); }); + + Component status = Renderer([&]() + { return render_status(); }); + + _page = Container::Vertical({ + Container::Horizontal({log, + status}) | + yflex, + input, + }); +} + +Logger *ScreenPageDebugger::get_logger() +{ + return &log_buffer; +} diff --git a/src/pages/page2.cpp b/src/pages/page2.cpp new file mode 100644 index 0000000..96e9888 --- /dev/null +++ b/src/pages/page2.cpp @@ -0,0 +1,57 @@ +#include "pages/launcher.h" +#include "loader.h" + +#include "ftxui/component/component.hpp" +#include "ftxui/component/component_base.hpp" +#include "ftxui/component/event.hpp" +#include "ftxui/component/screen_interactive.hpp" +#include "ftxui/dom/elements.hpp" + +using namespace ftxui; + +static const std::vector _launcher_entries = { + "Legacy", + "Debug"}; + +static const std::vector _library_entries = { + "Reply", + "UDP_IP", + "Emit", + "BOUCHON", + "Push"}; + +inline void ScreenPageLauncher::on_launch() +{ + this->send_event(EventType::SWITCH_SCREEN, static_cast(1)); + // const std::string name = get_simple_name(_library_entries[_library_selected]); + this->send_event(EventType::LAUNCH_INSTANCE, _library_entries[_library_selected]); +} + +ScreenPageLauncher::ScreenPageLauncher(EventHandler &handler) + : ScreenPage(handler) +{ + // Will be used later: for now crashes the launcher because it instanciate some values... + // _library_entries = get_libraries(); + _library_selected = 0; + _library = Radiobox(&_library_entries, &_library_selected); + + _launcher_selected = 0; + _launcher = Radiobox(&_launcher_entries, &_launcher_selected); + + Component library_selection = Renderer(_library, [&] + { return vbox({window(text("Select Library"), + _library->Render() | vscroll_indicator | frame) | + center | flex}) | + xflex_grow; }); + Component launcher_selection = Renderer(_launcher, [&] + { return vbox({window(text("Select launcher"), + _launcher->Render() | vscroll_indicator | frame) | + center | flex}); }); + + _launch_button = Container::Vertical({Button("Launch", [&] + { return on_launch(); })}); + Component launch = Renderer(_launch_button, [&] + { return vbox({_launch_button->Render()}) | center | xflex; }); + + _page = Container::Vertical({Container::Horizontal({library_selection, launcher_selection, launch}) | flex}); +} \ No newline at end of file diff --git a/src/ui.cpp b/src/ui.cpp new file mode 100644 index 0000000..fd99aaf --- /dev/null +++ b/src/ui.cpp @@ -0,0 +1,63 @@ +#include "ui.h" +#include +#include "ftxui/component/component.hpp" + +using namespace ftxui; + +UserInterface::UserInterface() + : _screen(ScreenInteractive::Fullscreen()), _pages({}), _page_container(), _main_container(), _main_renderer(), _page_index(0) +{ +} + +UserInterface::~UserInterface() +{ + _screen.Exit(); +} + +ftxui::ScreenInteractive *UserInterface::get_screen() +{ + return &_screen; +} + +void UserInterface::add_screen(ScreenPage *p) +{ + _pages.push_back(p); +} + +void UserInterface::select_screen(size_t index) +{ + _page_index = index; +} + +void UserInterface::select_screen(const ScreenPage *p) +{ + const typename std::vector::iterator it = std::find(_pages.begin(), _pages.end(), p); + const size_t index = it - _pages.begin(); + select_screen(index); +} + +void UserInterface::start() +{ + Components pages_component; + for (const auto &p : _pages) + pages_component.push_back(*(p->getPage())); + _page_container = Container::Tab(pages_component, &_page_index); + + _main_container = Container::Vertical({ + _page_container | flex, + }); + + _main_renderer = Renderer(_main_container, [&] + { return vbox({ + text("FTXUI Template") | inverted | hcenter, + separatorEmpty(), + _main_container->Render() | yflex_grow, + }); }); + + _screen.Loop(_main_renderer); +} + +void UserInterface::update() +{ + _screen.PostEvent(ftxui::Event::Custom); +}