display

Layout 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

Diffstat:
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