Getting started

Requirements

AppCUI is a C++20 library built with CMake. Its dependencies (SDL2, SDL2_ttf, FreeType, libpng, libjpeg-turbo, bzip2, brotli and, on Linux / macOS, ncurses) are built automatically by the vcpkg copy that is included in the repository as a git submodule.

  • a C++20 compiler: Visual Studio 2022 (Windows), GCC or Clang (Linux), Apple Clang / Xcode command line tools (macOS)

  • CMake 3.13 or newer (and, preferably, Ninja)

  • git (the vcpkg submodule)

  • Linux only: the X11 / OpenGL development packages needed to build SDL2. The packages installed by the CI are a known-good set:

    sudo apt-get install -y build-essential cmake ninja-build libsdl2-dev libsdl2-ttf-dev libltdl-dev
    

Building

git clone --recursive https://github.com/gdt050579/AppCUI.git
cd AppCUI
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release

The first configuration builds the dependencies (this takes a while; vcpkg caches them afterwards). When AppCUI is the top level project, the library (libAppCUI) and every example of the Examples folder are written to the bin folder. If the repository was cloned without --recursive, run git submodule update --init --recursive.

Using AppCUI in your project

Add the repository as a sub-folder (for example a git submodule) and link the AppCUI target:

cmake_minimum_required(VERSION 3.13)
# the toolchain must be set before project(): it builds the dependencies of AppCUI
set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/AppCUI/vcpkg/scripts/buildsystems/vcpkg.cmake"
    CACHE STRING "Vcpkg toolchain file")
project(MyApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)

add_subdirectory(AppCUI)
add_executable(MyApp main.cpp)
target_include_directories(MyApp PRIVATE AppCUI/AppCUI/include)
target_link_libraries(MyApp PRIVATE AppCUI)

As vcpkg runs in manifest mode, your project needs a vcpkg.json that lists the same dependencies as AppCUI/vcpkg.json (plus your own). AppCUI is a shared library: ship libAppCUI (and the libraries it depends on) next to your executable.

A first application

Everything is declared in one header, AppCUI.hpp. An application initializes AppCUI, adds one or more windows to the desktop and runs the event loop:

#include "AppCUI.hpp"

using namespace AppCUI;
using namespace AppCUI::Application;
using namespace AppCUI::Controls;

class GreetingWindow : public Window
{
    static constexpr int BUTTON_GREET = 1;
    Reference<TextField> name;

  public:
    GreetingWindow() : Window("Hello", "d:c,w:50,h:8", WindowFlags::None)
    {
        Factory::Label::Create(this, "Name", "x:1,y:1,w:6");
        name = Factory::TextField::Create(this, "", "x:8,y:1,w:39");
        Factory::Button::Create(this, "&Greet", "x:50%,y:4,w:20,a:t", BUTTON_GREET);
        name->SetFocus();
    }
    bool OnEvent(Reference<Control> sender, Event eventType, int controlID) override
    {
        if ((eventType == Event::ButtonClicked) && (controlID == BUTTON_GREET))
        {
            std::string text;
            name->GetText().ToString(text);
            text = "Hello, " + (text.empty() ? std::string("stranger") : text) + "!";
            Dialogs::MessageBox::ShowNotification("Greeting", std::string_view(text));
            return true;
        }
        if (eventType == Event::WindowClose)
        {
            Application::Close(); // closing the window ends the application
            return true;
        }
        return Window::OnEvent(sender, eventType, controlID);
    }
};

int main()
{
    if (!Application::Init())
        return 1;
    Application::AddWindow(std::make_unique<GreetingWindow>());
    Application::Run();
    return 0;
}
  • every control is created with a layout string ("d:c,w:50,h:8" = docked in the center, 50 characters wide and 8 lines high);

  • Factory::<Control>::Create(parent, ...) creates a control, adds it to parent and returns a non-owning Reference to it;

  • events (button clicks, window close, …) reach OnEvent of the control and then of its parents;

  • Application::Run() returns when Application::Close() is called or when the last window is closed.

Instead of deriving classes, a control can also receive its events through handlers (a pointer to an object that implements the handler interface, or a plain function / lambda without captures):

int main()
{
    if (!Application::Init(InitializationFlags::CommandBar))
        return 1;
    auto window = Factory::Window::Create("Handlers", "d:c,w:40,h:8");
    auto label  = Factory::Label::Create(*window, "Press the button", "x:1,y:1,w:36");
    auto button = Factory::Button::Create(*window, "&Press me", "x:50%,y:3,w:16,a:t");

    // a plain function (or a lambda without captures) instead of a new class
    button->Handlers()->OnButtonPressed = [](Reference<Button> btn) {
        btn->SetText("Pressed !");
    };
    label->SetText("Use Tab / Shift+Tab to move the focus");

    Application::AddWindow(std::move(window));
    Application::Run();
    return 0;
}

Both samples are compiled with the examples (docs/snippets). The Examples folder has a program for almost every control (buttons, list views, tree views, grids, property lists, menus, tabs, splitters, images, …).

Next steps