display

Layout and Rendering TUI library
git clone git://git.dimitrijedobrota.com/display.git
Log | Files | Refs | README | HACKING | CONTRIBUTING | CODE_OF_CONDUCT | BUILDING |

commitacac25d5c637911f5a48637df44028440e1c2ee1
parentef565de7fa473b6bff7980e0a675cc362e891d05
authorDimitrije Dobrota <mail@dimitrijedobrota.com>
dateSat, 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

Diffstat:
MCMakeLists.txt|++-
Mexample/CMakeLists.txt|++
Aexample/navig/CMakeLists.txt|++++++++++++++++++++++++++++
Aexample/navig/menu.conf|++++++++++++++++++++++++++++
Aexample/navig/navig.cpp|+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainclude/display/layout_pivot.hpp|++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asource/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