Follow this guide to build your own application on Vortex.

App Development Guide - Vortex

A Vortex app is a consumer of the framework: it vendors Vortex, declares its interface in its own styles, provides one entry function, and adds a thin per-platform shell. This guide walks the whole contract. The bundled demo (examples/demo/) is a complete, minimal reference; it is built exactly the way any external app is.

Project setup

Vendor Vortex as a git submodule and add it to your CMake build:

git submodule add https://codeberg.org/HuntrSoftware/vortex.git ext/vortex
git submodule update --init --recursive
add_subdirectory(ext/vortex)

This provides the vortex_core library plus the platform reconcilers (vortex_desktop on desktop builds). Vortex's own demo and tests are skipped automatically when it is not the top-level project.

The app entry

Every platform shell calls one function at startup, defined by your app:

#include "vortex/vapp.h"
#include "vortex/vshell.h"

void Vortex::build_app(VApp& app) {
    app.set_root_style("App");
    wire_main_window_shell(app);
    // bind content + wire app-specific interaction here
}

build_app() applies the app's root style, reuses the framework shell, and then binds whatever content and interaction the app needs. Everything in it is portable; it runs unchanged on every platform.

The App root style

Your app ships its own style directory, loaded additively on top of the framework's library styles and un-namespaced, so your styles can inherit any library style through a Vortex/-qualified base. The convention is an App root layer that inherits Vortex/App and lists the app's top-level widgets:

App << Vortex/App:
    Icon: "logo_myapp"
    Name: "MyApp"

    Main Window << Vortex/Main Window
    Theme Directories Dialog << Vortex/Theme Directories Dialog

Icon names an icon file and Name is the app's display name. The titlebar's app tab links to both, so these two attributes are all it takes to brand the shell. Keep the root named App: that's the name those links look for.

An app usually wants its own window rather than the stock one. Define a window style that inherits the framework's Main Window under your app's name: the whole shell structure (titlebar, tabs, settings menu) comes with it, and your style overrides or extends what it needs.

App << Vortex/App:
    Icon: "logo_myapp"
    Name: "MyApp"

    MyApp Window << MyApp Window
    Theme Directories Dialog << Vortex/Theme Directories Dialog

MyApp Window << Vortex/Main Window:
    App Pane/Content:
        Greeting << Vortex/Text:
            Text: "Hello, MyApp"

Then pass the window's name to the shell wiring:

app.set_root_style("App");
wire_main_window_shell(app, "App", "MyApp Window");

See the styling guide for inheritance, path overrides, and theming in depth.

The shared shell

wire_main_window_shell() installs the standard window behavior: single-select titlebar tabs mapped to body panes (App Tab → App Pane, Settings Tab → Settings Menu), the theme/appearance states, and the theme-directories dialog open/close.

It returns a WindowTabs controller, pre-populated with the framework tabs. Apps with their own tabs use it two ways:

  • Static tabs: add the tab and its pane in your styles, then pass them as extraTabs: wire_main_window_shell(app, "App", "Main Window", {{"My Tab", "My Pane"}}).
  • Dynamic tabs: register tabs created at runtime with tabs->add(tabPath, panePath, stateName) / tabs->remove(...) / tabs->activate(...). Static and dynamic tabs share one selection: activating either kind deselects the other.

Platform shells

Desktop

A desktop main() fills a DesktopAppConfig and hands off to the framework runner:

#include "run_desktop.h"

int main(int argc, char** argv) {
    Vortex::DesktopAppConfig config;
    config.orgName = "myorg";                        // QSettings identity:
    config.appName = "MyApp";                        // per-app theme persistence
    config.windowTitle = "MyApp";
    config.defaultTheme = "Dark (huntrsoftwarellc)";
    config.frameworkAssetDir = VORTEX_FRAMEWORK_DIR; // the framework's styles/themes/icons
    config.appStyleDirs = {VORTEX_APP_DIR "/styles"};
    return Vortex::run_desktop(argc, argv, config);
}
add_executable(myapp platforms/desktop/main.cpp app/myapp_app.cpp)
target_link_libraries(myapp PRIVATE vortex_core vortex_desktop)
target_compile_definitions(myapp PRIVATE
    VORTEX_FRAMEWORK_DIR="${CMAKE_SOURCE_DIR}/ext/vortex"
    VORTEX_APP_DIR="${CMAKE_CURRENT_SOURCE_DIR}")

run_desktop() owns the QApplication, theme persistence, the frameless window, and the headless screenshot harness; a consumer's main() stays a few lines.

DesktopAppConfig also takes appIconDirs (searched before the framework's icons, so an app can override a shell icon by name or add its own) and appMediaDirs (searched for Image/Video sources).

Web

The web analog is one CMake call, made from an Emscripten build (emcmake cmake ...):

include(ext/vortex/platforms/web/VortexWebApp.cmake)

vortex_web_app(myapp-web
    BUILD_APP  app/myapp_app.cpp
    APP_STYLES ${CMAKE_SOURCE_DIR}/styles
    OUT_DIR    ${CMAKE_SOURCE_DIR}/site
    APP_ICONS  ${CMAKE_SOURCE_DIR}/icons     # optional
    APP_MEDIA  ${CMAKE_SOURCE_DIR}/media)    # optional

This builds vortex.js + vortex.wasm from the framework bridge plus your build_app(), embeds the framework styles/themes and your app styles into the WASM filesystem, and copies the reusable runtime JS (the DOM reconciler) and icons next to your page. Your OUT_DIR holds an index.html that loads vortex.js; serve it over HTTP.

Android

Subclass the framework's host Activity and supply only your identity:

import com.huntrsoftware.vortex.VortexActivity

class MainActivity : VortexActivity() {
    override val prefsName = "myapp_prefs"
    override val defaultTheme = "Dark (huntrsoftwarellc)"
}

The vortex library module owns the reconciler, JNI bridge, theme persistence, and asset plumbing. Apps that need seams backed by app-only libraries (e.g. Play Billing for in-app purchases) override onInstallAppSeams(bridge) to install them.

Platform seams

App code never talks to platform APIs directly. Each platform shell injects handlers into the core; portable app code consumes them through VApp:

Seam App code calls Provided by
HTTP app.http(request, callback) Qt Network / fetch / OkHttp-style transport
File pick app.pick_path(...) native file dialogs / SAF
File upload app.pick_and_upload(...) pick + POST, per platform
Open URL app.open_url(url) external browser / new tab
Timers app.after(ms, callback) Qt timers / async calls / handlers
Config in app.env(key) set by the shell (e.g. base_url)
Persistence app.store(key, value) QSettings / localStorage / SharedPreferences
Purchases app.start_purchase(...), app.query_purchases() app-installed (e.g. Play Billing)

A platform that doesn't provide a seam simply never delivers it; portable code stays portable.

Binding data and interaction

Beyond the shell, apps drive their widgets through VApp's runtime binds: bind_text, bind_click, bind_visible, set_list, the editor and checkbox APIs, and more. See the widgets guide for the full tour.

Appearance
Theme
—