The Raven manual

Raven is a GUI library in C11 for editors and tools: native windows on Windows, Linux and macOS, drawn with Vulkan, with a layout in the manner of Slate, interfaces written in XML or in code, a text editor for code, docking, glass materials, translations, accessibility, and bindings for seven other languages. This guide explains every part of it with examples. The two other pages are references: every XML tag and attribute, and every function of the C API.

Raven 0.1. The public API may still change before 1.0.

Contents

1. A first program

The whole API is one include. A window, a box, a label and a button:

#include <Raven.h>

static void on_quit(Rid button, void* user_data) {
    (void)button;
    (void)user_data;
    Rquit(0);
}

int main(void) {
    if (!Rinit(R_BACKEND_VULKAN)) return 1;

    Rid window = Rwindow_create("Hello", 420, 220, R_WINDOW_DEFAULT);
    Rid box = Rwidget_create(R_WIDGET_VBOX);
    Rpanel_set_spacing(box, 12);
    Rslot_set_halign(box, R_HALIGN_CENTER);
    Rslot_set_valign(box, R_VALIGN_CENTER);
    Rwindow_set_content(window, box);

    Rwidget_add_child(box, Rlabel_create("Hello from Raven"));
    Rid quit = Rwidget_add_child(box, Rbutton_create("Quit"));
    Rbutton_set_clicked_callback(quit, on_quit, NULL);

    while (Rwait_frame(-1.0)) {}
    Rshutdown();
    return Rget_exit_code();
}

The same window, written in XML and loaded by the program:

<Window Title="Hello" Width="420" Height="220">
    <VBox Spacing="12" Slot.HAlign="Center" Slot.VAlign="Center">
        <Label>Hello from Raven</Label>
        <Button Text="Quit" OnClicked="app.quit"/>
    </VBox>
</Window>
#include <Raven.h>

static void on_quit(Rid sender, const char* action, void* user_data) {
    (void)sender;
    (void)action;
    (void)user_data;
    Rquit(0);
}

int main(void) {
    if (!Rinit(R_BACKEND_VULKAN)) return 1;
    Raction_bind("app.quit", on_quit, NULL);
    if (Rwindow_create_from_xml_file("hello.xml") == R_INVALID_ID) return 1;
    while (Rwait_frame(-1.0)) {}
    Rshutdown();
    return 0;
}

Built with CMake, against an installed Raven:

cmake_minimum_required(VERSION 3.16)
project(hello C)
find_package(raven REQUIRED)            # -DCMAKE_PREFIX_PATH=<the Raven package>
add_executable(hello main.c)
target_link_libraries(hello PRIVATE raven::raven)

Or with the interface and its code in one file, main.xmlc (see XMLC), which the IDE creates for you.

2. Building Raven

Raven needs CMake 3.16 or newer and a C11 compiler. On Linux it also needs the X11 development package. The Vulkan headers are taken from the Vulkan SDK when it is installed and downloaded during the configuration otherwise; no Vulkan library is linked (it is loaded when the program runs). Each build gives the library, ravenc (the XMLC compiler), the IDE, the examples and the tests.

Windows

With Visual Studio 2022 (workload Desktop development with C++), in the Developer PowerShell:

git clone https://github.com/oscar-soirey/Raven.git
cd Raven
cmake -S . -B build
cmake --build build --config Release --parallel
ctest --test-dir build -C Release --output-on-failure
cmake --install build --config Release --prefix install

With MinGW-w64 (MSYS2, shell MINGW64):

pacman -S --needed git mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
cmake --install build --prefix install

A library built with MinGW is for programs built with MinGW, not with Visual C++. Three scripts help on Windows:

generate-cmake.bat            :: build\raven.sln for the newest Visual Studio installed
generate-cmake.bat --open     :: ... and opens it
build.bat                     :: Release build
build.bat debug --test        :: Debug build, then the tests
build.bat --run glass         :: builds, then starts an example
dist.bat                      :: packages to give away: Visual C++ and MinGW, static and shared

Linux

sudo apt install build-essential cmake git libx11-dev ninja-build   # Debian, Ubuntu
sudo dnf install gcc cmake git libX11-devel ninja-build             # Fedora
sudo pacman -S base-devel cmake git libx11 ninja                    # Arch

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
ctest --test-dir build --output-on-failure
cmake --install build --prefix install

At run time the Vulkan driver of the system is used (libvulkan.so.1). ./dist.sh makes the Linux packages.

macOS

xcode-select --install
brew install cmake molten-vk
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
cmake --install build --prefix install

Vulkan runs on macOS through MoltenVK. Add -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" for a universal library.

Options

OptionDefault
RAVEN_BUILD_SHAREDOFFa shared library (raven.dll, libraven.so, libraven.dylib) instead of a static one
RAVEN_BUILD_EXAMPLESON at top levelthe example programs
RAVEN_BUILD_TESTSON at top levelthe tests, the tests of the bindings, raven_benchmark
RAVEN_BUILD_TOOLSON at top levelraven_designer, the IDE
RAVEN_BUILD_WEBONweb pages (raven_web.h), compiled into the library
RAVEN_BACKEND_VULKANONOFF builds without any GPU code (a custom backend, or headless)
RAVEN_PLATFORMautowin32, x11, cocoa or null
RAVEN_WITH_STB_IMAGEONPNG, JPEG, BMP and TGA decoding
RAVEN_FETCH_VULKAN_HEADERSONdownload the Khronos headers when no SDK is found
RAVEN_VULKAN_INCLUDE_DIRthe folder of vulkan/vulkan.h, to skip the search
RAVEN_REBUILD_SHADERSOFFcompile the GLSL again (needs glslangValidator or glslc); the SPIR-V is in the repository
RAVEN_MSVC_STATIC_RUNTIMEOFFVisual C++: /MT instead of /MD
RAVEN_WARNINGS_AS_ERRORSOFF

With CMake 3.21 or newer, presets do the same: cmake --preset linux (or msvc, msvc-shared, mingw, mingw-shared, linux-shared, dev).

Using Raven from a project

find_package(raven REQUIRED)                  # an installed package
target_link_libraries(app PRIVATE raven::raven)

add_subdirectory(Raven)                       # or the sources, without installing
target_link_libraries(app PRIVATE raven::raven)

An installed package holds include/, lib/ (the library and lib/cmake/raven), bin/ (ravenc, raven_designer, and raven.dll in a shared build) and share/raven/bindings. With a shared library on Windows, define RAVEN_USE_DLL (CMake does it through the target).

3. Concepts

Names

FormExamples
functionsR<object>_<action>Rwindow_create, Rwindow_get_size, Rbutton_set_text, Rslot_set_padding
functions of the libraryR<action>Rinit, Rdraw_frame, Rwait_frame, Rquit, Rshutdown
typesR<Name>Rid, REvent, RInitDesc, RPainter; callbacks end with Callback
constantsR_<NAME>R_INVALID_ID, R_WIDGET_VBOX, R_KEY_ESCAPE, R_THEME_GLASS_DARK
build options, macrosRAVEN_<NAME>RAVEN_BUILD_SHARED, RAVEN_VERSION_MAJOR

Booleans are int (R_TRUE, R_FALSE), colors are four floats from 0 to 1, strings are UTF-8 and always copied by the library. A string returned by the library stays valid until the next call that returns one of the same kind: copy it to keep it.

Handles and errors

Every object is a Rid: windows, widgets, fonts, textures, timers, tree items, documents. A handle carries a generation, so using a deleted object is reported instead of crashing, and R_INVALID_ID is returned on failure. Deleting a widget deletes its children; deleting a window deletes its content.

static void on_error(RError code, RSeverity severity, const char* detail) {
    fprintf(stderr, "[%s] %s: %s\n", Rseverity_to_string(severity), Rerror_to_string(code), detail);
}

Rerror_set_callback(on_error);        /* before Rinit, to see its errors too */

const char* detail;
RSeverity severity;
if (Rerror_get_last(&detail, &severity) != R_NO_ERROR) puts(detail);

The loop

Raven has no loop of its own. The application asks for one frame at a time:

while (Rdraw_frame()) {               /* a game or an engine: never waits    */
    update_and_render_my_scene();
}

while (Rwait_frame(-1.0)) {}          /* a tool: sleeps until something happens */

Rdraw_frame processes the events, runs the timers and the posted tasks, lays out, paints and submits the windows that changed; a window in which nothing changed is not drawn again. Rwait_frame first sleeps until there is something to do (an event, a timer, an animation still moving, a task); its argument is the longest sleep in seconds, negative for no limit. Both return R_FALSE once Rquit was called or the last window closed; Rget_exit_code gives the code. Rset_frame_callback runs a function once per frame, also while the system holds the thread in a loop of its own (on Windows, while a window is moved or resized).

RInitDesc desc = { 0 };               /* zero: the default of every field */
desc.backend = R_BACKEND_VULKAN;
desc.app_name = "My tool";
desc.worker_threads = 0;              /* one per spare core; negative: none */
desc.ui_scale = 1.25f;                /* on top of the scale of the monitor */
desc.disable_vsync = R_FALSE;
desc.enable_validation = R_FALSE;     /* the Vulkan validation layer, when installed */
if (!Rinit_ex(&desc)) return 1;

One thread for the interface

The thread that called Rinit is the UI thread: every function is called from it, except those documented as thread-safe (Rtask_post, Rtask_run_async, Rwake_up, Rquit, Rwindow_request_redraw, Rget_time, Rmemory_alloc, Rmemory_realloc, Rmemory_free, Rmemory_get_stats). Another thread changes the interface by posting a task (Threads). A call from the wrong thread is reported as R_ERROR_WRONG_THREAD.

4. Windows

Rid window = Rwindow_create("Editor", 1100, 720, R_WINDOW_DEFAULT);
Rwindow_set_min_size(window, 640, 400);
Rwindow_center(window);
Rwindow_set_content(window, root_widget);      /* one widget: usually a panel */
Flag
R_WINDOW_DEFAULTa normal window
R_WINDOW_NO_RESIZEits size is fixed
R_WINDOW_HIDDENcreated hidden, until Rwindow_show
R_WINDOW_BORDERLESSno frame at all
R_WINDOW_MAXIMIZEDstarts maximized
R_WINDOW_CUSTOM_HEADERno title bar from the system: a header bar of the interface replaces it (Shell)
R_WINDOW_TRANSPARENTcan be seen through where nothing is drawn (Materials)

Flags combine with |. The size is the size of the content, in logical units; Rwindow_get_framebuffer_size gives pixels and Rwindow_get_scale the scale of the monitor. Several windows can be open at once; the loop draws all of them.

static int on_close(Rid window, void* user_data) {
    /* R_FALSE keeps the window open: ask about unsaved changes first */
    return Rmessage_box(window, "Quit", "Discard the changes?", R_MESSAGE_OK_CANCEL) == R_MESSAGE_RESULT_OK;
}

static void on_resize(Rid window, int width, int height, void* user_data) {
    printf("%d x %d\n", width, height);
}

Rwindow_set_close_callback(window, on_close, NULL);
Rwindow_set_resize_callback(window, on_resize, NULL);
Rwindow_set_title(window, "Editor - notes.txt");
Rwindow_maximize(window);                /* minimize, restore, show, hide, close */
Rwindow_set_topmost(window, R_TRUE);
Rwindow_set_icon(window, rgba, 32, 32);  /* RGBA pixels */

Windows also give: their native handle (Rwindow_get_native_handle, a RNativeWindow), a capture of what they show (Rwindow_capture, RGBA pixels freed with Rpixels_free), the widget with the keyboard focus or under the pointer, the state of the keys and buttons for polling (Rwindow_is_key_down, Rwindow_is_mouse_button_down, Rwindow_get_modifiers, Rwindow_get_mouse_position), and synthetic input for tests (Rwindow_inject_mouse_move, Rwindow_inject_key, Rwindow_inject_text...). Rwindow_set_continuous_redraw draws every frame (a game view); Rwindow_request_redraw asks for one more.

5. Widgets

An interface is a tree of widgets. A widget is created by type, put in a parent, found again by name, and deleted with its children:

Rid panel  = Rwidget_create(R_WIDGET_VBOX);
Rid label  = Rwidget_add_child(panel, Rlabel_create("Name"));     /* returns the child */
Rid input  = Rwidget_add_child(panel, Rwidget_create(R_WIDGET_TEXT_INPUT));
Rwidget_set_name(input, "name");

Rid found  = Rwidget_find(window, "name");          /* in a window, or under any widget */
Rid parent = Rwidget_get_parent(found);
int count  = Rwidget_get_child_count(panel);
Rwidget_insert_child(panel, Rbutton_create("First"), 0);
Rwidget_remove_child(panel, label);                 /* removed and deleted */
Rwidget_clear_children(panel);
Rwidget_destroy(panel);

The types: panels (R_WIDGET_VBOX, R_WIDGET_HBOX, R_WIDGET_GRID, R_WIDGET_UNIFORM_GRID, R_WIDGET_OVERLAY, R_WIDGET_BORDER, R_WIDGET_SIZE_BOX, R_WIDGET_SPACER, R_WIDGET_SCROLL_BOX, R_WIDGET_SPLITTER, R_WIDGET_WRAP_BOX, R_WIDGET_CANVAS, R_WIDGET_SWITCHER, R_WIDGET_TAB_VIEW, R_WIDGET_EXPANDER, R_WIDGET_BACKGROUND_BLUR), controls (R_WIDGET_LABEL, R_WIDGET_BUTTON, R_WIDGET_CHECKBOX, R_WIDGET_SLIDER, R_WIDGET_PROGRESSBAR, R_WIDGET_IMAGE, R_WIDGET_TEXT_INPUT, R_WIDGET_SPIN_BOX, R_WIDGET_COMBO_BOX, R_WIDGET_SEPARATOR, R_WIDGET_LIST_VIEW, R_WIDGET_TREE_VIEW, R_WIDGET_TILE_VIEW, R_WIDGET_MENU_BAR, R_WIDGET_MENU, R_WIDGET_MENU_ITEM, R_WIDGET_TEXT_EDITOR), the shell (R_WIDGET_HEADER_BAR, R_WIDGET_DOCK_AREA, R_WIDGET_ISLAND, R_WIDGET_FILE_BROWSER), tools (R_WIDGET_COLOR_PICKER, R_WIDGET_TIMELINE, R_WIDGET_NODE_GRAPH, R_WIDGET_BLOCK_EDITOR) and your own (R_WIDGET_VIEWPORT, R_WIDGET_CUSTOM).

What every widget has

Rwidget_set_visibility(w, R_COLLAPSED);       /* R_VISIBLE, R_HIDDEN (keeps its place), R_COLLAPSED (takes none) */
Rwidget_set_enabled(w, R_FALSE);              /* disables its children too */
Rwidget_set_opacity(w, 0.6f);
Rwidget_set_tooltip(w, "Saves the file (Ctrl+S)");
Rwidget_set_cursor(w, R_CURSOR_HAND);
Rwidget_set_size(w, 200, -1);                 /* a fixed width; -1: the content decides */
Rwidget_set_min_size(w, 120, 0);
Rwidget_set_max_size(w, 480, 0);
Rwidget_set_clipping(w, R_TRUE);              /* children are cut at its bounds */
Rwidget_set_hit_testable(w, R_FALSE);         /* the pointer goes through */
Rwidget_set_focusable(w, R_TRUE);
Rwidget_focus(w);
Rwidget_set_user_handle(w, my_object);        /* your pointer, kept for you */
Rwidget_set_attribute(w, "Tooltip", "Set by name, as in XML");

float x, y, width, height;
Rwidget_get_rect(w, &x, &y, &width, &height); /* where it was laid out, in the window */

Rwidget_set_destroy_callback tells when a widget is deleted (to free what belongs to it), Rwidget_hit_test and Rwidget_pick find the widget at a point.

6. Layout

Layout is the two passes of Slate: each widget computes the size it wants, from the leaves up; then each panel gives a rectangle to its children, from the root down. How a child is placed is stored on the child, in its slot, and read by its parent:

Rid row  = Rwidget_add_child(parent, Rwidget_create(R_WIDGET_HBOX));
Rpanel_set_spacing(row, 8);
Rid name = Rwidget_add_child(row, Rwidget_create(R_WIDGET_TEXT_INPUT));
Rslot_set_fill(name, 1.0f);                   /* takes the free width */
Rid ok   = Rwidget_add_child(row, Rbutton_create("OK"));
Rslot_set_valign(ok, R_VALIGN_CENTER);        /* its own height, centered */
Rslot_set_padding(ok, 8, 0, 0, 0);            /* left, top, right, bottom */
SlotRead by
Rslot_set_paddingevery panel
Rslot_set_halign (R_HALIGN_FILL, _LEFT, _CENTER, _RIGHT)VBox, Grid, Overlay, Border, SizeBox, ScrollBox, Switcher, a window
Rslot_set_valign (R_VALIGN_FILL, _TOP, _CENTER, _BOTTOM)HBox, Grid, Overlay, Border, SizeBox, ScrollBox, Switcher, a window
Rslot_set_fill, Rslot_set_auto, Rslot_set_max_sizeVBox, HBox; Splitter (share of each pane)
Rslot_set_grid, Rslot_set_grid_spanGrid, UniformGrid
Rslot_set_anchors, Rslot_set_position, Rslot_set_size, Rslot_set_alignmentCanvas
Rslot_set_titleTabView, DockArea

Sizes are logical units, multiplied by the scale of the monitor. Only the part of the tree that changed is measured and arranged again.

The panels

/* VBox, HBox: in a column, in a row */
Rid column = Rwidget_create(R_WIDGET_VBOX);
Rpanel_set_spacing(column, 6);

/* Grid: columns and rows, "auto" or shares of what is left */
Rid grid = Rwidget_create(R_WIDGET_GRID);
Rgrid_set_column_fill(grid, 1, 1.0f);         /* column 1 takes the free width */
Rid label = Rwidget_add_child(grid, Rlabel_create("Width"));
Rid value = Rwidget_add_child(grid, Rwidget_create(R_WIDGET_SPIN_BOX));
Rslot_set_grid(value, 1, 0);                  /* column 1, row 0 */

/* UniformGrid: cells of the same size, filled row by row */
Rid cells = Rwidget_create(R_WIDGET_UNIFORM_GRID);
Runiform_grid_set_columns(cells, 4);

/* Border: a surface around one child */
Rid card = Rwidget_create(R_WIDGET_BORDER);
Rborder_set_padding(card, 14, 14, 14, 14);
Rborder_set_background(card, 0.12f, 0.12f, 0.14f, 1.0f);
Rborder_set_corner_radius(card, 12, 12, 12, 12);
Rborder_set_outline(card, 1, 1, 1, 1, 0.1f);
Rborder_set_shadow(card, 24, 0, 8);

/* ScrollBox: what does not fit scrolls */
Rid scroll = Rwidget_create(R_WIDGET_SCROLL_BOX);
Rscroll_box_set_scroll_both(scroll, R_TRUE);
Rscroll_box_scroll_to_end(scroll);            /* a log */

/* Splitter: panes separated by handles the user drags */
Rid split = Rwidget_create(R_WIDGET_SPLITTER);
Rsplitter_set_orientation(split, R_HORIZONTAL);
Rsplitter_set_min_pane_size(split, 120);

/* Switcher: one child at a time */
Rid pages = Rwidget_create(R_WIDGET_SWITCHER);
Rswitcher_set_index(pages, 1);

/* TabView: one tab per child */
Rid tabs = Rwidget_create(R_WIDGET_TAB_VIEW);
Rtab_view_add_tab(tabs, "General", general_page);
Rtab_view_add_tab(tabs, "Advanced", advanced_page);
Rtab_view_set_documents(tabs, R_TRUE);        /* closable tabs, like the files of an editor */

/* Expander: a title that folds what is under it */
Rid more = Rwidget_create(R_WIDGET_EXPANDER);
Rexpander_set_title(more, "More options");
Rexpander_set_expanded(more, R_FALSE);

/* Canvas: free placement with anchors (fractions of the canvas) */
Rid canvas = Rwidget_create(R_WIDGET_CANVAS);
Rid help = Rwidget_add_child(canvas, Rbutton_create("Help"));
Rslot_set_anchors(help, 1, 1, 1, 1);          /* the bottom right corner */
Rslot_set_alignment(help, 1, 1);              /* pivot: its own bottom right corner */
Rslot_set_position(help, -16, -16);

R_WIDGET_OVERLAY stacks its children (the last on top), R_WIDGET_WRAP_BOX flows them and wraps to the next line, R_WIDGET_SIZE_BOX forces a size or an aspect ratio (Rsize_box_set_aspect_ratio), R_WIDGET_SPACER is empty space.

Right to left

A language written from right to left mirrors the layouts: boxes, grids, menus, scroll bars and alignments start on the right. By default (R_DIRECTION_AUTO) the windows follow the language in use; Rset_layout_direction(R_DIRECTION_RTL) or R_DIRECTION_LTR forces one direction for every window, and Rwidget_set_direction gives one to a part of a window (R_DIRECTION_INHERIT goes back to its parent; Direction="RightToLeft" in XML).

7. Controls

Label

Rid title = Rlabel_create("Settings");
Rlabel_set_text_size(title, 22);
Rlabel_set_wrap(title, R_TRUE);
Rlabel_set_text_align(title, R_TEXT_ALIGN_CENTER);
Rlabel_set_color(title, 0.6f, 0.6f, 0.65f, 1);
Rlabel_set_markdown(title, R_TRUE);           /* the text is Markdown: **bold**, lists, tables, links */

Button, checkbox

static void on_save(Rid button, void* user_data) { save_document(); }
static void on_dark(Rid checkbox, int checked, void* user_data) { Rtheme_apply(checked ? R_THEME_DARK : R_THEME_LIGHT); }

Rid save = Rbutton_create("Save");
Rbutton_set_style(save, R_BUTTON_PRIMARY);           /* R_BUTTON_DEFAULT, R_BUTTON_PRIMARY, R_BUTTON_FLAT */
Rbutton_set_clicked_callback(save, on_save, NULL);

Rid dark = Rcheckbox_create("Dark mode");
Rcheckbox_set_style(dark, R_CHECKBOX_SWITCH);        /* R_CHECKBOX_CHECK, _RADIO, _SWITCH */
Rcheckbox_set_checked(dark, R_TRUE);
Rcheckbox_set_changed_callback(dark, on_dark, NULL);

Radio boxes of the same parent are exclusive. A button can hold one child widget instead of a text (an icon and a label in an HBox).

Slider, spin box, progress bar

static void on_volume(Rid slider, float value, void* user_data) { set_volume(value); }
static void on_size(Rid spin, double value, void* user_data) { set_font_size(value); }

Rid volume = Rwidget_create(R_WIDGET_SLIDER);
Rslider_set_range(volume, 0, 100);
Rslider_set_step(volume, 5);
Rslider_set_value(volume, 40);
Rslider_set_changed_callback(volume, on_volume, NULL);

Rid size = Rwidget_create(R_WIDGET_SPIN_BOX);       /* drag to scrub, click to type, wheel to step */
Rspin_box_set_range(size, 6, 72);
Rspin_box_set_step(size, 0.5);
Rspin_box_set_decimals(size, 1);
Rspin_box_set_value(size, 12);
Rspin_box_set_changed_callback(size, on_size, NULL);

Rid progress = Rwidget_create(R_WIDGET_PROGRESSBAR);
Rprogress_bar_set_value(progress, 0.35f);           /* 0..1 */

Text input

static void on_typed(Rid input, const char* text, void* user_data) { filter_list(text); }      /* every change */
static void on_enter(Rid input, const char* text, void* user_data) { run_search(text); }       /* Enter */

Rid search = Rwidget_create(R_WIDGET_TEXT_INPUT);
Rtext_input_set_placeholder(search, "Search");
Rtext_input_set_changed_callback(search, on_typed, NULL);
Rtext_input_set_committed_callback(search, on_enter, NULL);
Rtext_input_set_max_length(search, 64);
Rtext_input_set_password(password, R_TRUE);
const char* text = Rtext_input_get_text(search);

Combo box, list, tree, tiles

Rid size = Rwidget_create(R_WIDGET_COMBO_BOX);
Rcombo_box_add_item(size, "Small");
Rcombo_box_add_item(size, "Large");
Rcombo_box_set_selected(size, 0);

Rid list = Rwidget_create(R_WIDGET_LIST_VIEW);
Rlist_view_add_item(list, "README.md");
Rlist_view_set_selected(list, 0);

/* a million rows: only those in view are asked for */
static const char* row_text(Rid list, int index, void* user_data) {
    static char text[32];
    snprintf(text, sizeof(text), "Row %d", index);
    return text;
}
Rlist_view_set_virtual_items(list, 1000000, row_text, NULL);

/* a tree: items are handles */
Rid tree = Rwidget_create(R_WIDGET_TREE_VIEW);
Rid src  = Rtree_view_add_item(tree, R_INVALID_ID, "src");       /* R_INVALID_ID: at the top */
Rid file = Rtree_view_add_item(tree, src, "main.c");
Rtree_item_set_expanded(src, R_TRUE);
Rtree_item_set_user_handle(file, my_file);
Rtree_view_set_multi_select(tree, R_TRUE);
Rtree_view_set_activated_callback(tree, on_open_item, NULL);     /* double click, Enter */

A tile view shows a grid of tiles, an icon over a name, described by a callback for the tiles in view (a file browser, a library of assets):

static void tile(Rid view, int index, RTileItem* item, void* user_data) {
    item->text = g_files[index].name;
    item->detail = g_files[index].size_text;      /* a second, dimmer line */
    item->badge = "PNG";                          /* top right corner */
    item->icon = g_files[index].thumbnail;        /* a texture */
}

Rid tiles = Rwidget_create(R_WIDGET_TILE_VIEW);
Rtile_view_set_tile_size(tiles, 112, 112, 64);
Rtile_view_set_items(tiles, g_file_count, tile, NULL);
Rtile_view_set_empty_text(tiles, "This folder is empty.");
Rtile_view_set_activated_callback(tiles, on_open_tile, NULL);
Rtile_view_set_context_callback(tiles, on_tile_menu, NULL);     /* right click */
Rid bar  = Rwidget_create(R_WIDGET_MENU_BAR);
Rid file = Rmenu_add_menu(bar, "File");
Rid open = Rmenu_add_item(file, "Open...");
Rmenu_item_set_shortcut(open, R_KEY_O, R_MOD_CTRL);
Rwidget_set_action(open, "file.open");                     /* or Rmenu_item_set_clicked_callback */
Rmenu_add_separator(file);
Rid wrap = Rmenu_add_item(Rmenu_add_menu(bar, "View"), "Word wrap");
Rmenu_item_set_checkable(wrap, R_TRUE);

/* a context menu: a menu without a parent */
Rid menu = Rwidget_create(R_WIDGET_MENU);
Rwidget_set_action(Rmenu_add_item(menu, "Rename"), "item.rename");
Rwidget_set_action(Rmenu_add_item(menu, "Delete"), "item.delete");
Rwidget_set_context_menu(tree, menu);                      /* opened by a right click on the tree */
Rmenu_open(menu, window, x, y);                            /* or anywhere, from code */

Image, separator

Rid logo = Rwidget_create(R_WIDGET_IMAGE);
Rimage_set_texture(logo, Rtexture_create_from_file("logo.png"));
Rimage_set_stretch(logo, R_STRETCH_FIT);        /* R_STRETCH_FILL, _FIT, _COVER, _NONE */
Rimage_set_corner_radius(logo, 8);
Rimage_set_tint_text(icon, R_TRUE);             /* a white icon takes the color of the text: it follows the theme */
Rwidget_set_size(logo, 64, 64);

Rid line = Rwidget_create(R_WIDGET_SEPARATOR);
Rseparator_set_orientation(line, R_VERTICAL);

8. Events and actions

Callbacks

Each control has typed callbacks for its events (clicked, changed, committed, activated...), set with `R..._set_..._callback(widget, function, user_data)`. The last argument is given back to the function as it is.

Actions

An action is a name for what the main event of a widget does: a click of a button or of a menu item, a change of a checkbox, Enter in a text input, the activation of a row. Interfaces written in XML use them (OnClicked="file.save"); from C, Rwidget_set_action gives one to a widget.

static void on_action(Rid sender, const char* action, void* user_data) {
    if (strcmp(action, "file.save") == 0) save_document();
}

Raction_bind("file.save", on_action, NULL);
Raction_bind("file.open", on_action, NULL);
Raction_set_default_handler(on_action, NULL);   /* the actions bound to nothing */
Raction_fire("file.save", R_INVALID_ID);        /* from code */
Raction_unbind("file.open");

Keyboard shortcuts

Rwindow_register_shortcut(window, R_KEY_S, R_MOD_CTRL, "file.save");
Rwindow_register_shortcut(window, R_KEY_Z, R_MOD_CTRL | R_MOD_SHIFT, "edit.redo");
Rwindow_unregister_shortcut(window, R_KEY_S, R_MOD_CTRL);

The shortcut of a menu item works by itself; in XML, <Shortcut Keys="Ctrl+S" Action="file.save"/>. The modifiers are R_MOD_CTRL, R_MOD_SHIFT, R_MOD_ALT and R_MOD_SUPER. Tab and Shift+Tab move the focus between the controls (Rwindow_set_tab_navigation turns it off).

Raw input

A widget can see the mouse and the keyboard before its own class does; returning R_TRUE consumes the event. Events bubble from the widget under the pointer (or with the focus) up to the window.

static int on_event(Rid widget, const REvent* e, void* user_data) {
    switch (e->type) {
    case R_EVENT_MOUSE_DOWN:
        if (e->button == R_MOUSE_RIGHT) { open_menu(e->x, e->y); return R_TRUE; }
        if (e->clicks == 2) { rename_item(); return R_TRUE; }
        break;
    case R_EVENT_MOUSE_WHEEL:
        zoom_by(e->wheel_y);
        return R_TRUE;
    case R_EVENT_KEY_DOWN:
        if (e->key == R_KEY_DELETE && !(e->modifiers & R_MOD_CTRL)) { delete_selection(); return R_TRUE; }
        break;
    case R_EVENT_TEXT:          /* e->codepoint: a typed character */
    default:
        break;
    }
    return R_FALSE;             /* not mine: the widget and its parents get it */
}

Rwidget_set_event_callback(widget, on_event, NULL);
Rwindow_set_event_hook(window, on_window_event, NULL);   /* the whole window, before any widget */

REvent gives the position in the window (x, y) and in the widget (local_x, local_y), the movement (delta_x, delta_y), the wheel, the button and the number of clicks, the key, the modifiers, whether the key repeats and the typed character.

Timers

static void on_tick(Rid timer, void* user_data) { autosave(); }

Rid autosave_timer = Rtimer_create(60.0, R_TRUE, on_tick, NULL);   /* every minute; R_FALSE: once */
Rtimer_destroy(autosave_timer);

Clipboard and links

Rclipboard_set_text("copied");
const char* pasted = Rclipboard_get_text();
Ropen_url("https://example.org");              /* the browser of the system; a file opens in its application */

Drag and drop

Files and text dragged from other applications are dropped on a window or on a widget:

static int on_drop(Rid target, const RDropEvent* e, void* user_data) {
    if (e->phase == R_DROP_DROPPED) {
        int i;
        for (i = 0; i < e->path_count; i++) open_file(e->paths[i]);
        if (e->text) insert_text(e->text);
    }
    return R_TRUE;              /* "I take it": the target is highlighted while it is over it */
}

Rwindow_set_drop_callback(window, on_drop, NULL);
Rwidget_set_drop_callback(panel, on_drop, NULL);

Inside the application, Rdrag_begin drags a text with a type, while a mouse button is held; the drop callbacks receive drag_type, drag_source and the data in text, and can refuse it by returning R_FALSE. Tree views and tile views start their drags themselves (Rtree_view_set_drag_type, Rtile_view_set_drag_type).

/* in the event callback of the source, once the pointer has moved a few units */
Rdrag_begin(source, "asset", "textures/wood.png", "wood.png");

static int on_asset_drop(Rid target, const RDropEvent* e, void* user_data) {
    if (!e->drag_type || strcmp(e->drag_type, "asset") != 0) return R_FALSE;
    if (e->phase == R_DROP_DROPPED) apply_asset(target, e->text);
    return R_TRUE;
}

Dragging out of a Raven window to another application is not implemented.

9. Interfaces in XML

Every widget, attribute and slot has an XML spelling. A document is loaded into widgets, which are then handles like the others. The XML reference lists every tag and attribute; this section is about using it from a program.

Rid window = Rwindow_create_from_xml_file("ui/main.xml");
Rid editor = Rwidget_find(window, "editor");
Rid toolbar = Rwidget_create_from_xml_file("ui/toolbar.xml");     /* a piece, to put anywhere */
Rwidget_insert_child(Rwidget_find(window, "top"), toolbar, 0);

Live reload

A window created from a file follows the file: save main.xml in any editor while the program runs and the window changes, keeping the widgets that are still there with their handles, their callbacks and what was typed. Rxml_set_reload_callback tells after each reload, Rxml_set_live_reload turns it off (or RAVEN_LIVE_RELOAD=0). Theme files, the files of declared tags and the tables of translations are watched the same way.

Tags of your own

A document declares tags with <Widget Name="Card" Title="">, with parameters, expressions, <Children/>, <Repeat>, <ForEach>, <If> and <Include> (Declaring tags). From C, Rxml_register_widgets_from_file("ui/widgets.xml") reads declarations ahead of the documents that use them, and Rxml_register_tag gives a tag to a widget built by code (Tags made by code).

10. One file, any language: XMLC

An interface and the code that goes with it can be one file: the XML, and its code in <script> elements. ravenc turns the whole file into a source of your language: the scripts are copied as they are, and the XML becomes tables built by Rwindow_create_from_compiled (no XML is read when the program runs). OnClicked="{on_save}" calls the function on_save of the scripts, ui.status is the widget named status, and the compiler of the language checks all of it.

<!-- main.xmlc: a whole C program -->
<script>
#include <stdio.h>

static int g_clicks = 0;

static void on_hello(Rid sender, const char* action, void* user_data) {
    char text[64];
    (void)sender; (void)action; (void)user_data;
    snprintf(text, sizeof(text), "%d clicks", ++g_clicks);
    Rlabel_set_text(ui.status, text);          /* the widget with Name="status" */
}

int main(void) {
    if (!Rinit(R_BACKEND_VULKAN)) return 1;
    main_create();                             /* made by ravenc from the name of the file */
    while (Rwait_frame(-1.0)) {}
    Rshutdown();
    return 0;
}
</script>
<Window Title="Hello" Width="400" Height="200">
    <VBox Spacing="12" Slot.Padding="24">
        <Label Name="status">Not clicked yet</Label>
        <Button Text="Click me" Style="Primary" OnClicked="{on_hello}"/>
    </VBox>
</Window>
# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(hello C)
find_package(raven REQUIRED)
add_executable(hello main.xmlc)
target_link_libraries(hello PRIVATE raven::raven)
raven_target_xmlc(hello)                 # ravenc runs on every .xmlc / .xmlc++ of the target
FileLanguageBuilt byA function {f}A widget
.xmlcCCMake: raven_target_xmlcvoid f(Rid, const char*, void*)ui.x
.xmlc++, .xmlcppC++CMake: raven_target_xmlcthe same, void f(Rid, const char*), or a lambdaui.x
.xmlrsRustCargo: raven_build::compile in build.rsfn f(Rid, &str)ui().x
.xmlpyPythonimport raven_xmlpy, then import main; or python -m raven_xmlpy main.xmlpydef f(sender, action)ui.x
.xmlgoGo//go:generate ravenc main.xmlgo -o main.xmlgo.gofunc f(raven.ID, string)ui.x
.xmlcsC#MSBuild: Raven.XmlCs.targetsstatic void F(uint, string)MainUi.x
.xmlzigZigbuild.zig: xmlzig.compilefn f(Rid, ?[*:0]const u8, ?*anyopaque) callconv(.C) voidui.x
.xmlluaLuaJITrequire("raven_xmllua"), then require("main")function f(sender, action) (global)ui.x

The same program in Rust, Python and Lua:

<!-- src/main.xmlrs -->
<script>
use raven::*;
use std::sync::atomic::{AtomicI32, Ordering};

static CLICKS: AtomicI32 = AtomicI32::new(0);

fn on_hello(_sender: Rid, _action: &str) {
    let n = CLICKS.fetch_add(1, Ordering::SeqCst) + 1;
    let text = cstr(&format!("{} clicks", n));
    unsafe { Rlabel_set_text(ui().status, text.as_ptr()) };
}

fn main() {
    unsafe { Rinit(R_BACKEND_VULKAN) };
    main_create();
    unsafe { while Rdraw_frame() != 0 {} Rshutdown() };
}
</script>
<Window Title="Hello" Width="400" Height="200">
    <VBox Spacing="12" Slot.Padding="24">
        <Label Name="status">Not clicked yet</Label>
        <Button Text="Click me" Style="Primary" OnClicked="{on_hello}"/>
    </VBox>
</Window>
// build.rs
fn main() { raven_build::compile("src/main.xmlrs"); }

// src/main.rs: the program is the .xmlrs
include!(concat!(env!("OUT_DIR"), "/main.xmlrs.rs"));
<!-- main.xmlpy: python -m raven_xmlpy main.xmlpy -->
<script>
import raven as r

clicks = 0

def on_hello(sender, action):
    global clicks
    clicks += 1
    r.Rlabel_set_text(ui.status, f"{clicks} clicks")

if __name__ == "__main__":
    r.Rinit(r.R_BACKEND_VULKAN)
    main_create()
    while r.Rwait_frame(-1.0):
        pass
    r.Rshutdown()
</script>
<Window Title="Hello" Width="400" Height="200">
    <VBox Spacing="12" Slot.Padding="24">
        <Label Name="status">Not clicked yet</Label>
        <Button Text="Click me" OnClicked="{on_hello}"/>
    </VBox>
</Window>
<!-- main.xmllua: luajit raven_xmllua.lua main.xmllua -->
<script>
local r = require("raven")
clicks = 0

function on_hello(sender, action)
    clicks = clicks + 1
    r.Rlabel_set_text(ui.status, clicks .. " clicks")
end

r.Rinit(r.R_BACKEND_VULKAN)
main_create()
while r.Rwait_frame(-1.0) ~= 0 do end
r.Rshutdown()
</script>
<Window Title="Hello" Width="400" Height="200">
    <Label Name="status" Slot.Padding="24">Not clicked yet</Label>
</Window>

ravenc main.xmlc -o main.xmlc.c runs the compiler by hand. examples/languages holds the same program in the eight languages, and examples/bevy_editor a whole 3D scene editor whose interface is one .xmlrs.

11. The text editor

R_WIDGET_TEXT_EDITOR edits text of any number of lines: selection, clipboard, undo and redo, line numbers, word wrap, zoom. With a language it colors the text, proposes words while typing, shows which bracket closes which, closes brackets and quotes, indents, and comments lines out (Ctrl+/).

Rid editor = Rtext_editor_create("int main(void) {\n    return 0;\n}\n");
Rtext_editor_set_language(editor, Rlanguage_for_file("main.c"));   /* "C"; NULL for plain text */
Rtext_editor_set_line_numbers(editor, R_TRUE);
Rtext_editor_set_tab_size(editor, 4);
Rtext_editor_set_tab_inserts_spaces(editor, R_TRUE);
Rtext_editor_set_word_wrap(editor, R_FALSE);
Rtext_editor_set_changed_callback(editor, on_edited, NULL);

Rtext_editor_set_cursor(editor, 0, 4);                /* line, column (bytes), from 0 */
Rtext_editor_select(editor, 1, 4, 1, 10);
Rtext_editor_insert_text(editor, "/* typed */");     /* as the user would: undoable */
Rtext_editor_scroll_to_line(editor, 120);
const char* all = Rtext_editor_get_text(editor);
const char* line = Rtext_editor_get_line(editor, 0);

Built-in languages: C, C++, C#, Java, JavaScript, TypeScript, Python, Rust, GLSL, JSON, CSS, XML, HTML (with the JavaScript and CSS inside it), Markdown, and the XMLC files. A language is a description, not code: change one or add yours with Rlanguage_register.

RLanguageDesc gd = { 0 };
gd.name = "GDScript";
gd.extensions = "gd";
gd.keywords = "func var const if elif else for while match return class extends signal";
gd.types = "int float bool String Vector2 Vector3 Node";
gd.constants = "true false null self";
gd.line_comment = "#";
gd.string_quotes = "\"'";
gd.flags = R_LANGUAGE_INDENT_ON_COLON;
Rlanguage_register(&gd);
Rtext_editor_set_language(editor, "GDScript");

Proposals and markers

The editor knows what the text alone says; it is not a language server. What needs to understand a whole program comes from outside, through two doors: your proposals, listed first, and markers that underline a part of the text with a message (the errors of a compiler).

static void on_complete(Rid editor, const char* prefix, void* user_data) {
    Rtext_editor_add_completion(editor, "Rwindow_create", "Rid (const char*, int, int, Ruint)");
    Rtext_editor_add_completion(editor, "Rwidget_find", "Rid (Rid, const char*)");
}

Rtext_editor_set_completion_callback(editor, on_complete, NULL);
Rtext_editor_add_marker(editor, 11, 4, 9, R_MARKER_ERROR, "undeclared identifier 'cout'");
Rtext_editor_add_marker(editor, 20, 0, -1, R_MARKER_WARNING, "unused variable");    /* -1: to the end of the line */
Rtext_editor_clear_markers(editor);

Zoom

Ctrl + wheel zooms the text around the pointer, and Ctrl + +, Ctrl + - and Ctrl + 0 (back to 100 %) around the cursor, keypad included. The zoom is a factor of the font size, from 0.5 to 4. A program reads and sets it, and hears about the user's changes to apply them elsewhere:

static void on_zoom(Rid editor, float zoom, void* user_data) {
    /* zoom every other editor the same, and keep it for the next time */
    Rtext_editor_set_zoom(other_editor, zoom);
    save_setting("zoom", zoom);
}

Rtext_editor_set_zoom(editor, 1.25f);           /* from the program: no callback */
Rtext_editor_set_zoom_callback(editor, on_zoom, NULL);
Rtext_editor_set_zoomable(log_view, R_FALSE);   /* Ctrl + wheel scrolls instead */

In XML: Zoom="1.25" and Zoomable="false".

Colors, fonts, Markdown

Rtheme_set_token_color(R_TOKEN_KEYWORD, 0.98f, 0.45f, 0.62f, 1);   /* or <Syntax Keyword="#FB7185"/> in a theme */
Rtext_editor_set_font(editor, Rfont_create_from_file("JetBrainsMono.ttf"), 14);
Rtext_editor_set_markdown(notes, R_TRUE);       /* Markdown styled where it is typed */
RToken t = Rtext_editor_get_token(editor, 3, 8);

12. The shell: header bar, docking, island

Header bar

A window created with R_WINDOW_CUSTOM_HEADER (CustomHeader="true") has no title bar from the system; a header bar of the interface draws the title and the window buttons and holds any widgets. Dragging it moves the window, a double click maximizes it, the edges of the window resize it, and on Windows the snapping to the sides of the screen keeps working.

Rid window = Rwindow_create("Tool", 1000, 640, R_WINDOW_CUSTOM_HEADER);
Rid root = Rwidget_create(R_WIDGET_VBOX);
Rwindow_set_content(window, root);

Rid bar = Rwidget_add_child(root, Rheader_bar_create("Tool"));
Rheader_bar_set_buttons(bar, R_HEADER_ALL_BUTTONS);      /* or R_HEADER_CLOSE | R_HEADER_MINIMIZE ... */
Rheader_bar_set_title_align(bar, R_HALIGN_LEFT);
Rid run = Rwidget_add_child(bar, Rbutton_create("Run"));
Rslot_set_halign(run, R_HALIGN_RIGHT);                  /* packed from the right */

Rwidget_set_window_drag(my_toolbar, R_TRUE);            /* any widget can move the window */

Docking

A dock area arranges its children as panels with tabs. The user drags a tab onto another tab bar, a side of a panel or an edge of the area, and the panels fly to their new place; a tab dragged out of the window takes its panel to a window of its own, and comes back when dropped on the area.

Rid dock = Rdock_area_create();
Rdock_area_add_panel(dock, scene,   "Scene",   R_DOCK_TAB,    R_INVALID_ID, 0);      /* the first one fills the area */
Rdock_area_add_panel(dock, outline, "Outline", R_DOCK_LEFT,   scene, 0.22f);
Rdock_area_add_panel(dock, details, "Details", R_DOCK_RIGHT,  scene, 0.28f);
Rdock_area_add_panel(dock, console, "Console", R_DOCK_BOTTOM, scene, 0.30f);
Rdock_area_add_panel(dock, assets,  "Assets",  R_DOCK_TAB,    console, 0);          /* beside Console, in its tabs */
Rwidget_set_name(outline, "outline");          /* named panels are saved in the layout */

const char* saved = Rdock_area_save_layout(dock);   /* a short text: keep it in your settings */
Rdock_area_load_layout(dock, saved);

Rdock_area_set_close_hides(dock, R_TRUE);      /* closing a tab hides its panel... */
Rdock_area_open_panel(dock, outline);          /* ...and View > Outline brings it back where it was */
Rdock_area_float_panel(dock, console, 200, 200);  /* to a window of its own */
Rdock_area_set_tearable(dock, R_FALSE);        /* panels stay in the window */

In XML: Slot.Dock, Slot.DockTo, Slot.DockSize, Slot.Title, Slot.Closable on the children of a <DockArea>. The close callback can refuse to close a panel (unsaved changes), and the changed callback tells when the user moved, resized, showed or closed one.

Island

A pill at the top of a window that unfolds under the pointer, on a click, or from code: the place for what is going on (a build, a download, a message). Its first child is shown folded, its second unfolded; they cross-fade while it changes size. It is dark whatever the theme, as the notch of a phone is.

Rid island = Risland_create();
Rwidget_add_child(island, Rlabel_create("3 messages"));       /* folded */
Rid more = Rwidget_add_child(island, Rwidget_create(R_WIDGET_VBOX));
Rwidget_add_child(more, Rlabel_create("Build finished in 4 s"));
Rwindow_set_island(window, island);

Risland_expand_for(island, 2.0);               /* say something, then fold back */
Risland_set_attached(island, R_TRUE);          /* square upper corners: a notch */
Risland_set_expand_on_hover(island, R_FALSE);

13. Dialogs and files

Message boxes

RMessageResult answer = Rmessage_box(window, "Unsaved changes", "Save before closing?", R_MESSAGE_YES_NO_CANCEL);
if (answer == R_MESSAGE_RESULT_CANCEL) return;
if (answer == R_MESSAGE_RESULT_YES) save_document();

Dialogs of your own

Any widget can be shown as a dialog of a window: a veil falls on the window, the dialog rises in its middle, and the window only listens to it until it closes. Rdialog_run waits and returns the result; Rdialog_open returns at once and calls a function when the dialog closes. The window takes the content and deletes it when the dialog closes: read the fields before.

static char g_name[128];

static void on_ok(Rid button, void* user_data) {
    snprintf(g_name, sizeof(g_name), "%s", Rtext_input_get_text((Rid)(uintptr_t)user_data));
    Rdialog_close(button, 1);                  /* the dialog that contains the button */
}
static void on_cancel(Rid button, void* user_data) { Rdialog_close(button, 0); }

const char* xml =
    "<Border Padding='20' Width='360'><VBox Spacing='10'>"
    "  <Label Text='Name of the layer' TextSize='18'/>"
    "  <TextInput Name='name'/>"
    "  <HBox Spacing='8' Slot.HAlign='Right'><Button Name='cancel' Text='Cancel'/><Button Name='ok' Text='OK' Style='Primary'/></HBox>"
    "</VBox></Border>";
Rid content = Rwidget_create_from_xml(xml, strlen(xml));
Rid name = Rwidget_find(content, "name");
Rbutton_set_clicked_callback(Rwidget_find(content, "ok"), on_ok, (void*)(uintptr_t)name);
Rbutton_set_clicked_callback(Rwidget_find(content, "cancel"), on_cancel, NULL);
Rwidget_focus(name);
if (Rdialog_run(window, content) == 1) rename_layer(g_name);     /* Escape gives 0 */

File dialogs

static const RFileFilter filters[] = { { "Images", "*.png;*.jpg" }, { "All files", "*" } };
RFileDialogDesc desc = { 0 };
desc.parent = window;
desc.title = "Open an image";
desc.filters = filters;
desc.filter_count = 2;
desc.flags = R_FILE_DIALOG_MULTIPLE;

const char* path = Rfile_dialog_open(&desc);           /* NULL when cancelled */
if (path) {
    int i, n = Rfile_dialog_get_result_count();
    for (i = 0; i < n; i++) open_image(Rfile_dialog_get_result(i));
}
desc.file_name = "untitled.png";
desc.default_extension = "png";
const char* target = Rfile_dialog_save(&desc);
const char* folder = Rfile_dialog_select_folder(&desc);

They are the dialogs of the system (Windows: the common dialogs; Linux: zenity or kdialog when installed), and otherwise a dialog of Raven built on the file browser (R_FILE_DIALOG_RAVEN asks for it everywhere). While a dialog waits, the application keeps running: timers, tasks and the frame callback go on and the windows are still drawn.

The file browser widget

Rid files = Rfile_browser_create();
Rfile_browser_set_mode(files, R_FILE_BROWSER_OPEN);
Rfile_browser_set_directory(files, "~/Projects");
Rfile_browser_set_filters(files, filters, 2);
Rfile_browser_set_controls_visible(files, R_FALSE);    /* no name field, no buttons: a side panel */
Rfile_browser_set_accepted_callback(files, on_file_chosen, NULL);

Reading and writing files

size_t size;
char* text = Rfile_read("notes.txt", &size);          /* UTF-8 paths everywhere; NULL when it cannot be read */
if (text) {
    Rfile_write("notes.bak", text, size);
    Rmemory_free(text);                               /* allocated by Raven */
}

/* a file in memory: every function of Raven that reads files finds it (fonts, images, XML) */
Rfile_register_memory("ui/main.xml", embedded_xml, embedded_xml_size, R_FALSE);

14. Tools: color picker, timeline, node graph, blocks

Each holds a small model that the application fills and reads, or that a document describes (XML).

Color picker

static void on_color(Rid picker, float r, float g, float b, float a, void* user_data) {
    Rtheme_set_accent(r, g, b);
}

Rid picker = Rcolor_picker_create();         /* square, hues, opacity, code, numbers, swatches */
Rcolor_picker_set_color(picker, 0.04f, 0.52f, 1.0f, 1.0f);
Rcolor_picker_set_hex(picker, "#0A84FF");
Rcolor_picker_add_swatch(picker, 1.0f, 0.27f, 0.23f, 1.0f);
Rcolor_picker_set_popup(picker, R_TRUE);     /* a swatch that opens it */
Rcolor_picker_set_changed_callback(picker, on_color, NULL);

Timeline

static void on_time(Rid timeline, double seconds, void* user_data) { seek_to(seconds); }
static void on_item(Rid timeline, int item, void* user_data) {
    printf("%s now starts at %.2f s\n", Rtimeline_get_item_label(timeline, item), Rtimeline_get_item_start(timeline, item));
}

Rid timeline = Rtimeline_create();
Rtimeline_set_duration(timeline, 30.0);
Rtimeline_set_frame_rate(timeline, 24.0);       /* snaps to frames */
int camera = Rtimeline_add_track(timeline, "Camera");
Rtimeline_add_clip(timeline, camera, 0.0, 3.5, "Wide shot");
Rtimeline_add_key(timeline, camera, 6.0);
Rtimeline_add_marker(timeline, 4.0, "Intro");
Rtimeline_set_time_callback(timeline, on_time, NULL);       /* scrubbed, or playing */
Rtimeline_set_changed_callback(timeline, on_item, NULL);    /* moved, stretched */
Rtimeline_set_playing(timeline, R_TRUE);

It snaps to frames, to the playhead and to the other items, moves several items at once, stretches clips by their ends, selects with a rectangle and zooms with Ctrl + wheel.

Node graph

static int may_link(Rid graph, int output_pin, int input_pin, void* user_data) {
    return !would_make_a_loop(output_pin, input_pin);        /* R_FALSE refuses the link */
}

Rid graph = Rnode_graph_create();
int noise = Rnode_graph_add_node(graph, "Noise", 40, 60);
int value = Rnode_graph_add_output(graph, noise, "Value", "number");
int blend = Rnode_graph_add_node(graph, "Blend", 320, 90);
int a     = Rnode_graph_add_input(graph, blend, "A", "number");
Rnode_graph_connect(graph, value, a);
Rnode_graph_set_type_color(graph, "number", 0.39f, 0.82f, 1.0f);
Rnode_graph_set_node_content(graph, noise, Rwidget_create(R_WIDGET_SLIDER));   /* any widget inside a node */
Rnode_graph_set_connect_callback(graph, may_link, NULL);
Rnode_graph_frame_all(graph);

The graph pans (middle button, or Alt + drag) and zooms around the pointer; pins have types and only link to pins they fit; wires hang like cables and swing when their nodes move.

Block editor

Programs made of blocks that snap into each other, in the manner of Scratch. A kind of block has a label of words and slots: %n a number, %s a text, %b a condition, %r any value, %m a choice; {10} after a slot is its default; | separates the sections of a C-shaped block.

Rid blocks = Rblock_editor_create();
Rblock_editor_add_kind(blocks, "start",  "when started",  R_BLOCK_HAT,       "Events");
Rblock_editor_add_kind(blocks, "move",   "move %n{10} steps", R_BLOCK_STATEMENT, "Motion");
Rblock_editor_add_kind(blocks, "repeat", "repeat %n{10}", R_BLOCK_C,         "Control");
Rblock_editor_add_kind(blocks, "if",     "if %b then|else", R_BLOCK_C,       "Control");
Rblock_editor_set_category_color(blocks, "Motion", 0.30f, 0.59f, 1.0f);

int top  = Rblock_editor_add_block(blocks, "start", 40, 40);
int move = Rblock_editor_add_block(blocks, "move", 0, 0);
Rblock_editor_attach(blocks, move, top, R_BLOCK_NEXT, 0);    /* under the hat */
Rblock_editor_set_clicked_callback(blocks, on_run_script, NULL);
const char* saved = Rblock_editor_save(blocks);             /* a text, for Rblock_editor_load */

15. Themes, styles and materials

A theme gives every widget its look: about forty colors (roles, not widgets: accent, panel, text...), twenty metrics (font size, corner radius, height of a control...), a material, a font, the colors of code, and styles for a type of widget, a class, or both. Six themes are built in.

Rtheme_apply(R_THEME_DARK);          /* R_THEME_LIGHT, R_THEME_GLASS_DARK, R_THEME_GLASS_LIGHT,
                                        R_THEME_HIGH_CONTRAST_DARK, R_THEME_HIGH_CONTRAST_LIGHT */
Rtheme_set_accent(0.13f, 0.83f, 0.93f);                          /* one color, and every shade that follows */
Rtheme_set_color(R_COLOR_PANEL, 0.07f, 0.10f, 0.17f, 1.0f);      /* any role */
Rtheme_set_metric(R_METRIC_CORNER_RADIUS, 10);                   /* any metric */
Rtheme_set_font(Rfont_create_from_file("Inter.ttf"));

/* one widget and what it contains */
Rwidget_set_color(delete_button, R_COLOR_ACCENT, 0.9f, 0.28f, 0.3f, 1.0f);
Rwidget_set_metric(card, R_METRIC_CORNER_RADIUS, 18);

Styles and classes

Rtheme_set_style_metric("Button", NULL, R_METRIC_CORNER_RADIUS, 16);             /* every button */
Rtheme_set_style_color(NULL, "danger", R_COLOR_ACCENT, 0.98f, 0.44f, 0.52f, 1);  /* the class "danger" */
Rtheme_set_style_color("Label", "eyebrow", R_COLOR_TEXT, 0.13f, 0.83f, 0.93f, 1);
Rtheme_set_style_material(NULL, "card", R_MATERIAL_TINTED_GLASS);

Rwidget_add_class(field, "invalid");        /* the look changes at once: a simple way to show a state */
Rwidget_remove_class(field, "invalid");
Rwidget_set_class(card, "card danger");     /* Class="card danger" in XML */

A style with a type and a class wins over one with a class, which wins over one with a type; between equals, the last set wins; what was given to the widget itself wins over every style.

Theme files

<Theme Name="Ocean" Base="Dark" Accent="#22D3EE" CornerRadius="10" Font="fonts/Inter.ttf">
    <Colors Window="#0B1220" Panel="#111A2C" Text="#DCE7F5" Separator="#FFFFFF1A"/>
    <Metrics ControlHeight="32" Spacing="8"/>
    <Glass Frost="0.8" Refraction="1.2" Saturation="1.1" Rim="1" Shadow="1"/>
    <Syntax Keyword="#7DD3FC" String="#86EFAC" Comment="#64748B"/>
    <Style Widget="Button" CornerRadius="16"/>
    <Style Class="danger" Accent="#FB7185"/>
    <Style Class="card" Panel="#1B2742" CornerRadius="18" Material="Solid"/>
</Theme>
const char* name = Rtheme_load("themes/ocean.xml");   /* "Ocean": kept under its name, not applied */
Rtheme_use("Ocean");
Rwidget_set_theme(card, "Paper");                     /* a theme for a part of the window: Theme="Paper" */
Rtheme_save("my-theme.xml", "Mine");                  /* the theme in use, with what was changed */

On <Theme>: Name, Base (Dark, Light, GlassDark, GlassLight, or a theme loaded before), Accent, Material, Dark, Font, Wallpaper (an image, Dark, Light, colors to blend, or None), and any color or metric. Inside: <Colors>, <Metrics>, <Glass> (Frost Refraction Saturation Rim Shadow, multipliers of what the glass does), <Syntax> (`Comment String Number Keyword Type Function Tag Attribute Punctuation Directive Heading Strong Emphasis Link`) and <Style Widget="..." Class="...">. Theme files are loaded again when they are saved. The color roles and metrics are listed in the XML reference.

Materials, glass and blur

Material
R_MATERIAL_SOLIDfilled with the colors of the theme
R_MATERIAL_GLASSclear glass: what is behind shows through, bent at the rim
R_MATERIAL_FROSTED_GLASSblurred: the colors behind, without their shapes
R_MATERIAL_TINTED_GLASSblurred and colored by the theme: the easiest to read on
Rtheme_apply(R_THEME_GLASS_DARK);                    /* a theme comes with its material */
Rtheme_set_material(R_MATERIAL_FROSTED_GLASS);       /* every widget that was not given its own */
Rwidget_set_material(sidebar, R_MATERIAL_GLASS);     /* this widget and what it contains */
Rwindow_set_background(window, Rtexture_create_from_file("wallpaper.jpg"));   /* what the glass bends */

Glass needs something behind it: the glass themes put a wallpaper behind every window. A frame without glass is drawn straight to the window, so the solid themes cost nothing more.

A background blur shows what is behind a panel out of focus, without the rim of glass:

Rid blur = Rbackground_blur_create();
Rbackground_blur_set_style(blur, R_BLUR_FROSTED);    /* R_BLUR_PLAIN, _FROSTED, _GLASS, _NATIVE */
Rbackground_blur_set_tint(blur, 0.06f, 0.08f, 0.11f, 0.6f);
Rbackground_blur_set_corner_radius(blur, 24, 24, 24, 24);
Rbackground_blur_set_padding(blur, 22, 22, 22, 22);

A window created with R_WINDOW_TRANSPARENT is not filled: where no widget draws, the desktop shows. Rwindow_set_backdrop asks the system what to put there (R_BACKDROP_TRANSPARENT, R_BACKDROP_BLUR, R_BACKDROP_ACRYLIC, R_BACKDROP_MICA), and R_BLUR_NATIVE lets the system blur the desktop behind a panel. It depends on the window system (Windows 10 and 11; X11 with a compositor): Rwindow_get_backdrop says what was obtained.

<Window Transparent="true" Backdrop="Blur" Borderless="true" Width="480" Height="300">
    <BackgroundBlur Style="Native" CornerRadius="24" Padding="22" Tint="#10141C99">
        <Label Text="Seen through" TextSize="22"/>
    </BackgroundBlur>
</Window>

16. Custom widgets, drawing and animation

A R_WIDGET_CUSTOM is measured and painted by two callbacks, and receives its input through Rwidget_set_event_callback. Coordinates are logical units from its top left corner.

#include <Raven.h>

typedef struct gauge_t {
    float level;          /* 0..1 */
    RSpring shown;        /* the level drawn, gliding to `level` */
} gauge_t;

static void gauge_measure(Rid widget, float* width, float* height, void* user_data) {
    (void)widget; (void)user_data;
    *width = 160;
    *height = 18;
}

static void gauge_paint(Rid widget, RPainter* p, float width, float height, void* user_data) {
    gauge_t* g = (gauge_t*)user_data;
    float level = Rdraw_spring(p, &g->shown, g->level, R_MOTION_BOUNCY);
    float r, gr, b, a;
    (void)widget;
    Rtheme_get_color(R_COLOR_ACCENT, &r, &gr, &b, &a);
    Rdraw_rect(p, 0, 0, width, height, height * 0.5f, 1, 1, 1, 0.08f);
    Rdraw_rect(p, 0, 0, width * level, height, height * 0.5f, r, gr, b, a);
    Rdraw_text(p, R_INVALID_ID, 11, 8, 2, level > 0.9f ? "full" : "", 1, 1, 1, 1);
}

static int gauge_event(Rid widget, const REvent* e, void* user_data) {
    gauge_t* g = (gauge_t*)user_data;
    float x, y, w, h;
    if (e->type != R_EVENT_MOUSE_DOWN) return R_FALSE;
    Rwidget_get_rect(widget, &x, &y, &w, &h);
    g->level = e->local_x / w;
    Rwidget_invalidate(widget);           /* paint again */
    return R_TRUE;
}

static gauge_t g_gauge = { 0.4f, { 0.0f, 0.0f, 0 } };

int main(void) {
    Rid window, gauge;
    if (!Rinit(R_BACKEND_VULKAN)) return 1;
    window = Rwindow_create("Gauge", 320, 120, R_WINDOW_DEFAULT);
    gauge = Rwidget_create(R_WIDGET_CUSTOM);
    Rcustom_set_measure_callback(gauge, gauge_measure, &g_gauge);
    Rcustom_set_paint_callback(gauge, gauge_paint, &g_gauge);
    Rwidget_set_event_callback(gauge, gauge_event, &g_gauge);
    Rslot_set_halign(gauge, R_HALIGN_CENTER);
    Rslot_set_valign(gauge, R_VALIGN_CENTER);
    Rwindow_set_content(window, gauge);
    while (Rwait_frame(-1.0)) {}
    Rshutdown();
    return 0;
}

The painter draws rounded rectangles and their outlines, circles, lines, curves, triangles, images, text and glass (Rdraw_glass), clipped with Rdraw_push_clip_rect. Rxml_register_tag gives such a widget a tag of its own (XML).

Springs

Everything moves with springs, as on a phone: buttons squash and bounce back, switches slide, popups pop out of what opened them, lists stretch at their ends. In a custom widget, Rdraw_spring pulls a value to its target and keeps the window drawing while it moves; Rwait_frame sleeps again when everything rests. The motions are R_MOTION_SNAPPY, R_MOTION_SMOOTH, R_MOTION_BOUNCY and R_MOTION_WOBBLY; Rspring_kick gives a spring a push, Rspring_set puts it somewhere without moving.

Transitions

Rwidget_set_transition(card, R_TRANSITION_FLY_UP, 0.1f);   /* each time it appears; Appear="FlyUp" */
Rwidget_play_transition(card, R_TRANSITION_DROP, 0.0f);    /* once, now */

Rset_animation_speed(0.25f);          /* slow motion */
Rset_animations_enabled(R_FALSE);     /* everything immediate: "reduce motion", tests */
Rset_animation_time_step(1.0 / 60);   /* frames rendered one by one: a video, deterministic tests */

Transitions: FADE, POP, ZOOM, FLY_UP, FLY_DOWN, FLY_LEFT, FLY_RIGHT, DROP, SLIDE_LEFT, SLIDE_RIGHT. Growing delays make a group arrive one after the other (AppearDelay in XML).

17. Effects (shaders)

An effect is a fragment shader that Raven runs on a rounded rectangle of a widget, with the corners, the clipping and the opacity already taken care of. Eight come with Raven: aurora, shimmer, neon, ripple, plasma, grain, spotlight and mesh. Any Border can take one as its background:

<Border Effect="aurora" CornerRadius="18" Height="200">
    <Label Text="Tonight" TextSize="24" Slot.Padding="16" Slot.VAlign="Bottom"/>
</Border>
<Border Effect="neon" Background="#111" EffectColor="#0A84FF" EffectColor2="#FF375F" EffectParams="1,2,12" Padding="16"/>
RShaderLook look;
Rshader_look_defaults(Reffect_get(R_EFFECT_PLASMA), &look);
look.color[0] = 0.37f; look.color[1] = 0.36f; look.color[2] = 0.90f; look.color[3] = 1.0f;
look.radius = 16;
Rborder_set_effect(border, Reffect_get(R_EFFECT_PLASMA), &look);

Your own, in GLSL compiled to SPIR-V:

#version 450
#extension GL_GOOGLE_include_directive : require
#include "raven_effect.glsl"

vec4 raven_effect(RavenEffect fx) {
    float band = 0.5 + 0.5 * sin(fx.pixel.x * 0.25 - fx.time * 4.0);
    return mix(fx.color, fx.color2, band);
}
glslangValidator -V -I<raven package>/include/raven/shaders stripes.frag -o stripes.spv
Rid stripes = Rshader_create_from_file("stripes.spv", R_SHADER_ANIMATED);
Rdraw_shader(painter, stripes, 0, 0, width, height, &look);     /* in a paint callback */

The guide to effects describes everything an effect receives (fx.uv, fx.pixel, fx.time, fx.pointer, fx.hover, the backdrop...), its helpers and its limits.

18. Images and fonts

Rid logo  = Rtexture_create_from_file("logo.png");             /* PNG, JPEG, BMP, TGA */
Rid photo = Rtexture_create_from_file_async("photo.jpg");      /* decoded on a worker; widgets update when ready */
Rid pixels = Rtexture_create_from_pixels(rgba, 256, 256);
Rtexture_update(pixels, rgba, 256, 256);                       /* new content */
Rtexture_set_filter(pixels, R_FILTER_NEAREST);                 /* pixel art */
int w, h;
Rtexture_get_size(logo, &w, &h);

Rid inter = Rfont_create_from_file("Inter-Regular.ttf");
Rfont_set_variant(inter, R_FONT_BOLD, Rfont_create_from_file("Inter-Bold.ttf"));   /* real bold for Markdown */
Rfont_set_fallback(inter, Rfont_create_from_file("NotoSansCJK.ttf"));
Rtheme_set_font(inter);
float tw, th;
Rfont_measure_text(inter, 14, "Hello", &tw, &th);

Subsets of Roboto and Roboto Mono (Latin-1 and Latin Extended-A) are embedded (Rfont_get_default, Rfont_get_monospace). A character they do not have is drawn with a font of the system (Segoe UI, Noto, the CJK fonts...), and emoji are drawn in color. Rimage_decode turns a picture file in memory into RGBA pixels, on any thread.

19. Translations

key,en,fr,ar
menu.file,File,Fichier,ملف
greeting,"Hello, {0}!","Bonjour, {0} !",مرحبا {0}
files.one,{n} file,{n} fichier,ملف واحد
files.other,{n} files,{n} fichiers,{n} ملفات
Rlocale_load_csv("strings.csv");
Rlocale_set_language(Rlocale_get_system_language());     /* "fr" */
Rlocale_set_fallback_language("en");

Rlabel_set_text(label, Rtr("menu.file"));
const char* args[] = { "Ada" };
Rlabel_set_text(hello, Rtr_format("greeting", args, 1));
Rlabel_set_text(count, Rtr_plural("files", 3));          /* "3 fichiers" */
Rlabel_set_text(size, Rlocale_format_number(1234.5, 1)); /* "1 234,5" */
Rwidget_set_text_key(button, "Text", "menu.file");       /* follows the language when it changes */

One column per language, any number of tables, plurals with the rules of each language (.zero, .one, .two, .few, .many, .other), and the tables are read again when they are saved. In XML, a text that starts with @ is a key (Text="@menu.file"). A language written from right to left mirrors the layouts (Right to left); Rlocale_is_rtl says whether the language in use is one, and Rlocale_set_changed_callback tells when the language changes. Text with both directions is ordered with the Unicode bidirectional algorithm and Arabic letters take their joined forms.

20. Accessibility

Raccessibility_follow_system();                     /* text size, contrast, motion: what the system says */
Raccessibility_set_text_scale(1.5f);                /* every text and control 50 % larger */
Raccessibility_set_high_contrast(R_TRUE);
Raccessibility_set_focus_always_visible(R_TRUE);
Raccessibility_set_reduce_motion(R_TRUE);
Raccessibility_set_color_filter(R_COLOR_FILTER_DEUTERANOPIA);   /* or R_COLOR_FILTER_SIMULATE_... */

Rwidget_set_accessible_name(close_button, "Close");
Rwidget_set_accessible_description(close_button, "Closes the document");
Rwidget_set_accessible_role(card, R_ROLE_GROUP);
Rwidget_set_accessible_labelled_by(field, label);
Raccessibility_announce("File saved", R_FALSE);     /* R_TRUE: interrupts what is being said */

Every widget is seen by the screen reader of the system with its role, name, value and state, and can be used from it: UI Automation on Windows (Narrator, NVDA, JAWS), NSAccessibility on macOS (VoiceOver), AT-SPI on Linux (Orca). Names come from the texts and from the label in front of a field, so nothing has to be added for most interfaces. In XML: AccessibleName, AccessibleDescription, AccessibleRole, AccessibleHidden.

21. Viewports: content of your own

A viewport shows what the application renders: a 3D scene, a video, an emulator. Three ways:

Rid view = Rwidget_create(R_WIDGET_VIEWPORT);

/* 1. a texture: shown like any widget, clipped, with the interface over it */
Rviewport_set_texture(view, texture);
Rtexture_update(texture, rgba, width, height);            /* one copy from the CPU */
/* zero copy:  Rvulkan_texture_create_from_image_view(image_view, sampler, w, h)   (Vulkan)
               Rvulkan_texture_create_shared(w, h) + GL_EXT_memory_object         (OpenGL) */

/* 2. a draw callback: your Vulkan commands inside the render pass of Raven (render thread) */
Rviewport_set_draw_callback(view, on_draw, scene);

/* 3. a native child window: an HWND or an X11 Window for OpenGL, Direct3D, anything */
RNativeWindow native;
Rviewport_set_native(view, R_TRUE);
Rviewport_get_native_handle(view, &native);

Rviewport_set_resize_callback(view, on_resize, scene);    /* its size in pixels changed */
Rviewport_set_realtime(view, R_TRUE);                     /* drawn at every frame */

To draw the interface with the Vulkan device of an engine instead of one created by Raven, call Rvulkan_set_external_device (and Rvulkan_set_queue_lock_callbacks) before Rinit; Rvulkan_get_required_instance_extensions and Rvulkan_get_required_device_extensions tell what the device needs.

22. Web pages

raven_web.h shows real web pages by driving a browser of the Chromium family that is already installed (Edge, which comes with Windows; Chrome, Chromium, Brave...): the page arrives as pictures in a texture, the input goes back, scripts run both ways. It contains no browser and needs nothing to build. Without a browser, a view says so and stays safe to call.

#include <raven_web.h>

static void on_title(Rid view, const char* result, const char* error, void* user_data) {
    if (result) printf("title: %s\n", result);      /* JSON: "\"Example Domain\"" */
}
static void on_message(Rid view, const char* message, void* user_data) {
    printf("the page says %s\n", message);         /* window.raven.postMessage("saved") */
}

Rweb_register_xml();                                /* <WebView Url="..."/> becomes a tag */
Rid view = Rweb_view_create();
Rweb_view_load_url(view, "https://example.org");
Rweb_view_run_javascript(view, "document.title", on_title, NULL);   /* promises are awaited */
Rweb_view_set_message_callback(view, on_message, NULL);
Rweb_view_set_zoom(view, 1.25f);

Rweb_is_available tells whether a browser was found; Rweb_set_browser_path chooses one. Pages are pictures: they arrive a frame or two after what they show, which suits documents and tools. Rweb_view_set_frames(view, R_WEB_FRAMES_JPEG, 80) makes them lighter for pages that move a lot. The browser runs with its own temporary profile, and with switches that keep it from talking to anything but the sites shown. File choosers, downloads and printing are not available.

23. Threads

The thread of Rinit owns the interface. A render thread owns the GPU, and a job system of worker threads paints large trees in parallel and runs your work.

typedef struct job_t { char path[260]; char* result; } job_t;

static void job_done(void* user_data) {           /* on the UI thread */
    job_t* job = (job_t*)user_data;
    Rtext_editor_set_text(g_editor, job->result);
    Rmemory_free(job->result);
    Rmemory_free(job);
}

static void job_run(void* user_data) {            /* on a worker thread: no widget here */
    job_t* job = (job_t*)user_data;
    job->result = load_and_format(job->path);
    Rtask_post(job_done, job);                    /* back to the UI thread, and wakes Rwait_frame */
}

job_t* job = (job_t*)Rmemory_alloc(sizeof(job_t));
snprintf(job->path, sizeof(job->path), "%s", path);
Rtask_run_async(job_run, job);

/* split a loop over the workers and this thread; returns when all is done */
static void blur_rows(Ruint begin, Ruint end, void* image) { /* rows begin..end-1 */ }
Rparallel_for(height, 16, blur_rows, image);

Viewport draw callbacks run on the render thread. Rwake_up wakes Rwait_frame from any thread; Ris_ui_thread tells where you are.

24. Memory

Raven does not call malloc: it takes memory from the system by regions of 4 MB and hands it out with its own heap (TLSF: constant time, neighbours merged when freed). The bundled font and image decoders use it too.

static void* my_alloc(size_t size, void* user) { return engine_alloc(size, 16); }
static void* my_realloc(void* p, size_t size, void* user) { return engine_realloc(p, size, 16); }
static void my_free(void* p, void* user) { engine_free(p); }

RAllocator mine = { my_alloc, my_realloc, my_free, NULL };
Rset_allocator(&mine);                 /* before Rinit: every block goes through your functions */

RMemoryStats stats;
Rmemory_get_stats(&stats);             /* bytes in use, peak, blocks, taken from the system */
printf("%zu bytes in %zu blocks\n", stats.used_bytes, stats.allocation_count);
Rmemory_check();                       /* walks the heap: R_FALSE when it is corrupted */
Rmemory_trim();                        /* gives back the regions that hold nothing */

Rset_allocator(Rallocator_get_system()), or RAVEN_ALLOCATOR=system, uses malloc instead (what Valgrind and heap profilers follow; the default in a build with AddressSanitizer). After Rshutdown, allocation_count is 0: anything else is a leak of the library.

25. Other languages

bindings/ holds bindings generated from the headers by tools/bindgen, for C++, Python, Rust, Go, C#, Zig and LuaJIT. They cover everything in Raven.h (raven_web.h, raven_vulkan.h and raven_backend.h are C only). Run python3 tools/bindgen/bindgen.py after changing a public header; the test bindings_up_to_date checks it was done.

C++

#include <raven.hpp>

int main() {
    raven::init(R_BACKEND_VULKAN);
    auto window = raven::Window::create("Hello", 400, 200, R_WINDOW_DEFAULT);
    auto button = raven::Button::create("Quit");
    button.set_clicked_callback([](Rid) { raven::quit(0); });     /* any callable, captures included */
    window.set_content(button);
    while (raven::wait_frame(-1.0)) {}
    raven::shutdown();
}

A class per kind of handle, whose member functions are the C functions of that object (Rbutton_set_text(button, "OK") is button.set_text("OK")); a class is the size of a handle and does not own what it names.

Python

import raven as r

r.Rinit(r.R_BACKEND_VULKAN)
window = r.Rwindow_create_from_xml('<Window Title="Hi"><Button Text="Quit" OnClicked="quit"/></Window>')
r.Raction_bind("quit", lambda sender, action, user: r.Rquit(0))
while r.Rwait_frame(-1.0):
    pass
r.Rshutdown()

ctypes, no compilation; the shared library is found through RAVEN_LIBRARY or the usual places.

Rust

use raven::*;

fn main() {
    unsafe { Rinit(R_BACKEND_VULKAN) };
    let _window = window_from_xml(r#"<Window Title="Hi"><Button Text="Quit" OnClicked="quit"/></Window>"#);
    action_bind("quit", |_, _| unsafe { Rquit(0) });
    while unsafe { Rwait_frame(-1.0) } != 0 {}
    unsafe { Rshutdown() };
}

The crate raven-gui (imported as raven): the C API as it is, plus text, cstr, action_bind, window_from_xml and widget_find. Its build.rs links the library from RAVEN_LIB_DIR (RAVEN_STATIC=1 for the static one).

Go

package main

import (
    "runtime"
    "raven/raven"
)

func main() {
    runtime.LockOSThread()             // Raven belongs to the thread that started it
    raven.Init(raven.R_BACKEND_VULKAN)
    raven.WindowCreateFromXml(`<Window Title="Hi"><Button Text="Quit" OnClicked="quit"/></Window>`)
    raven.ActionBind("quit", func(sender raven.ID, action string) { raven.Quit(0) })
    for raven.WaitFrame(-1.0) != 0 {
    }
    raven.Shutdown()
}

Rwindow_set_title is WindowSetTitle; texts are strings; what a C function writes through a pointer is a result. Build with CGO_CFLAGS="-I<raven>/include" and CGO_LDFLAGS="-L<folder of the library>".

C#

using Raven;

R.Rinit(RBackend.R_BACKEND_VULKAN);
uint window = R.Rwindow_create_from_xml("<Window Title='Hi'><Button Text='Quit' OnClicked='quit'/></Window>");
R.Raction_bind("quit", R.Keep<RActionCallback>((sender, action, user) => R.Rquit(0)), IntPtr.Zero);
while (R.Rwait_frame(-1.0) != 0) { }
R.Rshutdown();

P/Invoke. R.Keep keeps a delegate away from the garbage collector while the library may call it; the shared library goes beside the program.

Zig

const r = @import("raven.zig");

pub fn main() void {
    _ = r.Rinit(r.R_BACKEND_VULKAN);
    const xml = "<Window Title='Hi'><Button Text='Quit' OnClicked='quit'/></Window>";
    _ = r.Rwindow_create_from_xml(xml, xml.len);
    r.Raction_bind("quit", quit, null);
    while (r.Rwait_frame(-1.0) != 0) {}
    r.Rshutdown();
}

fn quit(sender: r.Rid, action: ?[*:0]const u8, user_data: ?*anyopaque) callconv(.C) void {
    _ = sender; _ = action; _ = user_data;
    r.Rquit(0);
}

LuaJIT

local r = require("raven")
local ffi = require("ffi")

r.Rinit(r.R_BACKEND_VULKAN)
local xml = [[<Window Title="Hi"><Button Text="Quit" OnClicked="quit"/></Window>]]
r.Rwindow_create_from_xml(xml, #xml)
local quit = ffi.cast("RActionCallback", function() r.Rquit(0) end)   -- keep it referenced
r.Raction_bind("quit", quit, nil)
while r.Rwait_frame(-1.0) ~= 0 do end
r.Rshutdown()

26. The IDE

raven_designer [project folder | file] is the IDE of Raven, written with Raven. It opens on a start page: new project, open a folder, the recent projects.

Projects

Project > New project (Ctrl+Shift+N) asks a name, a language (C, C++, Rust, Python, Go, C#, Zig, Lua), a location, and a kind:

The content drawer (Ctrl+Space, from anywhere) shows the files of the project as tiles: a double click opens a file where it belongs, a right click offers Open and, for an interface, Preview; the search looks through the whole project.

Code, and the island as a preview

Every file opens in a code editor tab: C, C++, Rust, Python, Go, C#, Zig, Lua, CMake, XML and the XMLC files, with completion of the Raven API (read from its headers), of the widgets of the interface (ui.<Name>), of the tags, attributes and @keys. Ctrl + wheel zooms the text of every editor at once, and the zoom is kept for the next time.

When the file in front is an interface (.xml, .xmlc, .xmlpy...), the island at the top of the window is its preview: unfolded, it shows the interface built from what is being typed, rebuilt as it changes, in the language chosen for the preview; a mistake shows its line instead.

The visual editor

It opens only when asked: a right click on an interface in the content drawer and Preview, the button of the island, or View > Preview the file (Ctrl+Shift+P). The file leaves its code editor for it, its unsaved changes with it, and it shows the same document five ways, kept in step:

Close on the preview (or View > Close the preview) sends the file back to a code editor, and the panels of the visual editor go away.

Translations, build and run

The Translations panel shows the CSV tables as a grid: a row per key, a column per language. Ctrl+B builds, F5 builds and runs, Ctrl+F5 stops, with the tools of the language: CMake and the compiler, Cargo, go generate and go build, dotnet, zig build, Python, LuaJIT. What they write goes to Output; the errors and warnings of GCC, Clang, MSVC, rustc, Go, CMake and ravenc go to Build and onto the lines of the editors, brought back from the generated file to the line of the .xml.... Project > Build settings sets the build type, the folder of the Raven package and the generator, kept in .raven-ide.

Driving it from a script

The IDE can run without a window, which is how its tests work:

raven_designer --null --frames 2 --script "f 2 project /tmp/p demo C preview /tmp/p/demo/main.xmlc expectpanel panel.preview 1 do file.save_all"
raven_designer --null --script "nativeproject /tmp/p hello Rust expectfile /tmp/p/hello/src/main.rs Hello,~world!"

The commands are listed at the top of designer_script in tools/designer/main.c.

27. Tests, headless runs and custom backends

RInitDesc desc = { 0 };
desc.backend = R_BACKEND_NULL;        /* draws nothing */
desc.headless = R_TRUE;               /* no window system */
Rinit_ex(&desc);

Rid window = Rwindow_create_from_xml_file("ui/main.xml");
Rset_animations_enabled(R_FALSE);                     /* read the state right after an input */
Rwindow_inject_mouse_move(window, 120, 40);
Rwindow_inject_mouse_button(window, R_MOUSE_LEFT, R_TRUE);
Rwindow_inject_mouse_button(window, R_MOUSE_LEFT, R_FALSE);
Rwindow_inject_key(window, R_KEY_S, R_TRUE, R_MOD_CTRL);
Rwindow_inject_text(window, "hello");
Rdraw_frame();

A rendering backend is a table of functions (RBackendVTable in raven_backend.h) given to Rbackend_register before Rinit with R_BACKEND_CUSTOM: it receives each frame as instances of rounded rectangles and draw commands. src/render/null/null_backend.c is a template. Rwindow_get_frame_stats gives the times of layout, paint and render of the last frame.

28. Environment variables

Variable
RAVEN_LIVE_RELOAD=0files are not watched (live reload)
RAVEN_ALLOCATOR=systemmalloc instead of the heap of Raven
RAVEN_ACCESSIBILITY=1Linux: expose the interface on AT-SPI even when no screen reader is announced
RAVEN_X11_COMPOSITED=0 / 1Linux: tells whether a compositor runs, instead of asking
RAVEN_WEB_BROWSERthe browser raven_web starts
RAVEN_WEB_ARGUMENTSswitches added to its command line
RAVEN_WEB_NO_SANDBOXstarts it with --no-sandbox (containers)
RAVEN_WEB_TRACE=1, RAVEN_WEB_DEBUG=1print the messages of the browser; let its errors through
RAVEN_LIBRARYbindings: the path of the shared library
RAVEN_LIB_DIRRust (build.rs) and Zig (build.zig): the folder of the library
RAVEN_STATIC=1Rust: link the static library instead of the shared one
RAVENCthe ravenc the language tools run

29. What Raven does not do yet

Questions, issues and contributions: github.com/oscar-soirey/Raven. Raven is open source under the Apache License 2.0: use it for anything, commercial included; what you make with it is yours.

Raven 0.1 — Apache License 2.0 — raven home · source