displayLayout and Rendering TUI library | 
          
| git clone git://git.dimitrijedobrota.com/display.git | 
| Log | Files | Refs | README | LICENSE | HACKING | CONTRIBUTING | CODE_OF_CONDUCT | BUILDING | 
| commit | acac25d5c637911f5a48637df44028440e1c2ee1 | 
| parent | ef565de7fa473b6bff7980e0a675cc362e891d05 | 
| author | Dimitrije Dobrota < mail@dimitrijedobrota.com > | 
| date | Sat, 15 Feb 2025 15:53:04 +0100 | 
Leverage stamen to create interactive menus
* Input handling proof of concept
* Window switching proof of concept
* Text rendering and aligning proof of concept
* LayoutPivot experiment
| M | CMakeLists.txt | | | ++ - | 
| M | example/CMakeLists.txt | | | ++ | 
| A | example/navig/CMakeLists.txt | | | ++++++++++++++++++++++++++++ | 
| A | example/navig/menu.conf | | | ++++++++++++++++++++++++++++ | 
| A | example/navig/navig.cpp | | | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | include/display/layout_pivot.hpp | | | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
| A | source/layout_pivot.cpp | | | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | 
7 files changed, 440 insertions(+), 1 deletions(-)
diff --git a/ CMakeLists.txt b/ CMakeLists.txt
          @@ -4,7 +4,7 @@ 
          include(cmake/prelude.cmake)
        
        
          project(
              display
              VERSION 0.1.22
              VERSION 0.1.23
              DESCRIPTION "TUI library"
              HOMEPAGE_URL "https://example.com/"
              LANGUAGES CXX
        
        
          @@ -23,6 +23,7 @@ 
          add_library(
        
        
              source/screen.cpp
              # ---- utility
              source/layout_free.cpp
              source/layout_pivot.cpp
              source/layout_rigid.cpp
              source/window_pivot.cpp
          )
        
        diff --git a/ example/CMakeLists.txt b/ example/CMakeLists.txt
          @@ -22,4 +22,6 @@ 
          endfunction()
        
        
          add_example(example)
          add_subdirectory(navig)
          add_folders(Example)
        
        diff --git a/ example/navig/CMakeLists.txt b/ example/navig/CMakeLists.txt
@@ -0,0 +1,28 @@
find_package(stamen 1.2.1 REQUIRED)
          find_package(poafloc 1.2 CONFIG REQUIRED)
          configure_file(menu.conf menu.conf COPYONLY)
          add_custom_command(
              OUTPUT menu.hpp menu.cpp
              COMMAND stamen -d test_display --cpp -n example menu.conf
              DEPENDS menu.conf
              WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
              COMMENT "Generating menu files"
          )
          function(add_example NAME)
            add_executable("${NAME}" "${NAME}.cpp")
            target_include_directories("${NAME}" PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")
            target_link_libraries("${NAME}" PUBLIC display::display stamen::stamen)
            target_compile_features("${NAME}" PRIVATE cxx_std_20)
            add_custom_target("run_${NAME}" COMMAND "${NAME}" menu.conf VERBATIM)
            add_dependencies("run_${NAME}" "${NAME}")
            add_dependencies(run-examples "run_${NAME}")
          endfunction()
          add_example(navig)
          target_sources(navig PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/menu.cpp")
          add_folders(Navig)
        
        diff --git a/ example/navig/menu.conf b/ example/navig/menu.conf
@@ -0,0 +1,28 @@
+ menu_main  Main Menu
          - submenu_1  Enter Submenu 1
          - submenu_2  Enter Submenu 2
          - finish Quit
          + submenu_1 Submenu 1
          - submenu_3   Enter Submenu 3
          - operation1  Operation 1
          - operation2  Operation 2
          - operation3  Operation 3
          + submenu_2 Submenu 2
          - submenu_3   Enter Submenu 3
          - operation1  Operation 1
          - operation2  Operation 2
          - operation3  Operation 3
          + submenu_3   Submenu 3
          - operation1  Operation 1
          - operation2  Operation 2
          - operation3  Operation 3
        
        diff --git a/ example/navig/navig.cpp b/ example/navig/navig.cpp
@@ -0,0 +1,247 @@
#include <format>
          #include <iostream>
          #include <stack>
          #include <string>
          #include <stamen/stamen.hpp>
          #include "display/display.hpp"
          #include "display/layout_pivot.hpp"
          #include "display/window_pivot.hpp"
          #include "menu.hpp"
          namespace
          {
          bool is_finished = false;  // NOLINT
          class WindowCustom : public display::WindowPivot
          {
          public:
            WindowCustom(display::apos_t apos,
                         display::dim_t adim,
                         display::pos_t pos,
                         display::piv_t piv,
                         const example::menu_t& menu)
                : WindowPivot(apos, adim, pos, calc_dim(menu), piv)
                , m_menu(menu)
            {
            }
            WindowCustom(const WindowCustom&) = delete;
            WindowCustom& operator=(const WindowCustom&) = delete;
            WindowCustom(WindowCustom&&) = default;
            WindowCustom& operator=(WindowCustom&&) = default;
            ~WindowCustom() override
            {
              std::cout << alec::background_v<alec::Color::DEFAULT>;
              std::cout << alec::foreground_v<alec::Color::DEFAULT>;
              const auto [apos, adim] = place();
              const auto [x, y] = apos;
              const auto [w, h] = adim;
              display::sz_t ypos = y;
              const auto cursor = [&]() { return alec::cursor_position(++ypos, x + 1); };
              for (std::size_t i = 0; i < h; i++) {
                std::cout << cursor() << std::string(w, ' ');
              }
            }
            void render() const override
            {
              const auto [apos, adim] = place();
              const auto [x, y] = apos;
              const auto [w, h] = adim;
              display::sz_t ypos = y;
              auto cursor = [&]() { return alec::cursor_position(++ypos, x + 1); };
              auto empty = [&]() { std::cout << cursor() << std::string(w, ' '); };
              auto center = [&](const std::string& value)
              { std::cout << cursor() << std::format("{:^{}}", value, w); };
              auto right = [&](const std::string& value)
              { std::cout << cursor() << std::format("{:>{}} ", value, w - 1); };
              std::cout << alec::background_v<alec::Color::BLUE>;
              empty(), center(m_menu.title), empty();
              for (std::size_t i = 0; i < m_menu.items.size(); i++) {
                if (m_selected == i) {
                  std::cout << alec::foreground_v<alec::Color::GREEN>;
                } else {
                  std::cout << alec::foreground_v<alec::Color::DEFAULT>;
                }
                right(m_menu.items[i].prompt);
              }
              std::cout << alec::foreground_v<alec::Color::DEFAULT>;
              empty();
              std::cout << alec::background_v<alec::Color::DEFAULT>;
              std::cout << std::flush;
            }
            void input(display::event& evnt) override
            {
              if (evnt.type() != display::event::Type::KEY) {
                return;
              }
              if (evnt.key() == 'j') {
                if (m_selected + 1 < m_menu.items.size()) {
                  m_selected++;
                }
                evnt.type() = display::event::Type::NONE;
                render();
                return;
              }
              if (evnt.key() == 'k') {
                if (m_selected > 0) {
                  m_selected--;
                }
                evnt.type() = display::event::Type::NONE;
                render();
                return;
              }
              if (evnt.key() == 'l') {
                m_menu.items[m_selected].callback(m_selected);
                evnt.type() = display::event::Type::NONE;
                return;
              }
              if (evnt.key() == 'h') {
                m_menu.callback(0);
                evnt.type() = display::event::Type::NONE;
                return;
              }
            }
          private:
            static display::dim_t calc_dim(const example::menu_t& menu)
            {
              std::size_t width = menu.title.size();
              for (const auto& item : menu.items) {
                width = std::max(width, item.prompt.size());
              }
              return {static_cast<display::sz_t>(width + 2),
                      static_cast<display::sz_t>(menu.items.size() + 4)};
            }
            example::menu_t m_menu;
            uint8_t m_selected = 0;
          };
          }  // namespace
          namespace example
          {
          int operation1(std::size_t /* unused */)  // NOLINT
          {
            std::cout << alec::cursor_position(1, 1) << "operation 1";
            std::cout << alec::cursor_position(2, 1)
                      << alec::erase_line_v<alec::Motion::WHOLE>
                      << "Some operation is done";
            std::cout << std::flush;
            return 1;
          }
          int operation2(std::size_t /* unused */)  // NOLINT
          {
            std::cout << alec::cursor_position(1, 1) << "operation 2";
            std::cout << alec::cursor_position(2, 1)
                      << alec::erase_line_v<alec::Motion::WHOLE>
                      << "Some other operation is done";
            std::cout << std::flush;
            return 1;
          }
          int operation3(std::size_t /* unused */)  // NOLINT
          {
            std::cout << alec::cursor_position(1, 1) << "operation 3";
            std::cout << alec::cursor_position(2, 1)
                      << alec::erase_line_v<alec::Motion::WHOLE>
                      << "Yet another operation is done";
            std::cout << std::flush;
            return 1;
          }
          int finish(std::size_t /* unused */)  // NOLINT
          {
            std::cout << alec::cursor_position(1, 1)
                      << alec::erase_line_v<alec::Motion::WHOLE>;
            std::cout << "finishing...";
            std::cout << std::flush;
            is_finished = true;
            return 0;
          }
          int menu_t::visit(const menu_t& menu)
          {
            using display::Display, display::LayoutPivot;
            auto& layout = Display::display().screen().get_layout<LayoutPivot>();
            static std::stack<const menu_t*> stk;
            if (!stk.empty() && stk.top()->title == menu.title) {
              stk.pop();
              if (stk.empty()) {
                finish(0);
                return 0;
              }
              layout.set_window<WindowCustom>(*stk.top());
              layout.render();
              return 0;
            }
            stk.push(&menu);
            layout.set_window<WindowCustom>(menu);
            layout.render();
            return 0;
          }
          }  // namespace example
          int main()
          {
            try {
              using namespace display;  // NOLINT
              auto& display = Display::display();
              display.screen().set_layout<LayoutPivot>(piv_t(PvtX::Center, PvtY::Center));
              example::menu_main(0);
              while (!is_finished) {
                auto evnt = display.get_event();
                if (evnt.type() == event::Type::RESIZE) {
                  std::cout << alec::erase_display_v<alec::Motion::WHOLE>;
                  display.render();
                  continue;
                }
                if (evnt.type() == event::Type::KEY) {
                  if (evnt.key() == 'q') {
                    break;
                  }
                  display.screen().input(evnt);
                }
              }
            } catch (std::exception& err) {
              std::cout << err.what() << '\n' << std::flush;
            }
            return 0;
          }
        
        diff --git a/ include/display/layout_pivot.hpp b/ include/display/layout_pivot.hpp
@@ -0,0 +1,58 @@
#pragma once
          #include <memory>
          #include "display/layout.hpp"
          #include "display/types.hpp"
          #include "display/window_pivot.hpp"
          namespace display
          {
          class LayoutPivot : public Layout
          {
          public:
            using ptr_t = std::unique_ptr<WindowPivot>;
            LayoutPivot(apos_t apos, dim_t dim, piv_t piv)  // NOLINT
                : Layout(apos, dim)
                , m_piv(piv)
            {
            }
            template<typename T, class... Args>
            T& set_window(Args&&... args)
            {
              m_window = std::make_unique<T>(
                  apos(), dim(), get_pos(), m_piv, std::forward<Args>(args)...);
              return get_window<T>();
            }
            template<typename T>
            const T& get_window() const
            {
              return *dynamic_cast<T*>(m_window.get());
            }
            template<typename T>
            T& get_window()
            {
              return *dynamic_cast<T*>(m_window.get());
            }
            void reset_window() { m_window.reset(); }
            bool has_window() const { return m_window != nullptr; }
            void resize(apos_t apos, dim_t dim) override;
            void render() const override;
            void input(event& evnt) override;
          private:
            pos_t get_pos() const;
            piv_t m_piv;
            ptr_t m_window;
          };
          }  // namespace display
        
        diff --git a/ source/layout_pivot.cpp b/ source/layout_pivot.cpp
@@ -0,0 +1,75 @@
#include "display/layout_pivot.hpp"
          namespace display
          {
          void LayoutPivot::resize(apos_t apos, dim_t dim)
          {
            Layout::resize(apos, dim);
            if (has_window()) {
              m_window->pos() = get_pos();
              m_window->resize(apos, dim);
            }
          }
          void LayoutPivot::render() const
          {
            if (has_window()) {
              m_window->render();
            }
          }
          void LayoutPivot::input(event& evnt)
          {
            if (has_window()) {
              m_window->input(evnt);
            }
          }
          pos_t LayoutPivot::get_pos() const
          {
            const auto [width, height] = dim();
            const display::sz_t midw = width / 2;
            const display::sz_t midh = height / 2;
            if (m_piv.x == PvtX::Left && m_piv.y == PvtY::Top) {
              return {0, 0};
            }
            if (m_piv.x == PvtX::Center && m_piv.y == PvtY::Top) {
              return {midw, 0};
            }
            if (m_piv.x == PvtX::Right && m_piv.y == PvtY::Top) {
              return {width, 0};
            }
            if (m_piv.x == PvtX::Right && m_piv.y == PvtY::Center) {
              return {width, midh};
            }
            if (m_piv.x == PvtX::Right && m_piv.y == PvtY::Bottom) {
              return {width, height};
            }
            if (m_piv.x == PvtX::Center && m_piv.y == PvtY::Bottom) {
              return {midw, height};
            }
            if (m_piv.x == PvtX::Left && m_piv.y == PvtY::Bottom) {
              return {0, height};
            }
            if (m_piv.x == PvtX::Left && m_piv.y == PvtY::Center) {
              return {0, midh};
            }
            if (m_piv.x == PvtX::Center && m_piv.y == PvtY::Center) {
              return {midw, midh};
            }
            return {0, 0};
          }
          }  // namespace display