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.
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.
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.
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
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.
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.
| Option | Default | |
|---|---|---|
RAVEN_BUILD_SHARED | OFF | a shared library (raven.dll, libraven.so, libraven.dylib) instead of a static one |
RAVEN_BUILD_EXAMPLES | ON at top level | the example programs |
RAVEN_BUILD_TESTS | ON at top level | the tests, the tests of the bindings, raven_benchmark |
RAVEN_BUILD_TOOLS | ON at top level | raven_designer, the IDE |
RAVEN_BUILD_WEB | ON | web pages (raven_web.h), compiled into the library |
RAVEN_BACKEND_VULKAN | ON | OFF builds without any GPU code (a custom backend, or headless) |
RAVEN_PLATFORM | auto | win32, x11, cocoa or null |
RAVEN_WITH_STB_IMAGE | ON | PNG, JPEG, BMP and TGA decoding |
RAVEN_FETCH_VULKAN_HEADERS | ON | download the Khronos headers when no SDK is found |
RAVEN_VULKAN_INCLUDE_DIR | the folder of vulkan/vulkan.h, to skip the search | |
RAVEN_REBUILD_SHADERS | OFF | compile the GLSL again (needs glslangValidator or glslc); the SPIR-V is in the repository |
RAVEN_MSVC_STATIC_RUNTIME | OFF | Visual C++: /MT instead of /MD |
RAVEN_WARNINGS_AS_ERRORS | OFF |
With CMake 3.21 or newer, presets do the same: cmake --preset linux
(or msvc, msvc-shared, mingw, mingw-shared, linux-shared, dev).
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).
| Form | Examples | |
|---|---|---|
| functions | R<object>_<action> | Rwindow_create, Rwindow_get_size, Rbutton_set_text, Rslot_set_padding |
| functions of the library | R<action> | Rinit, Rdraw_frame, Rwait_frame, Rquit, Rshutdown |
| types | R<Name> | Rid, REvent, RInitDesc, RPainter; callbacks end with Callback |
| constants | R_<NAME> | R_INVALID_ID, R_WIDGET_VBOX, R_KEY_ESCAPE, R_THEME_GLASS_DARK |
| build options, macros | RAVEN_<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.
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);
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;
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.
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_DEFAULT | a normal window |
R_WINDOW_NO_RESIZE | its size is fixed |
R_WINDOW_HIDDEN | created hidden, until Rwindow_show |
R_WINDOW_BORDERLESS | no frame at all |
R_WINDOW_MAXIMIZED | starts maximized |
R_WINDOW_CUSTOM_HEADER | no title bar from the system: a header bar of the interface replaces it (Shell) |
R_WINDOW_TRANSPARENT | can 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.
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).
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.
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 */
| Slot | Read by |
|---|---|
Rslot_set_padding | every 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_size | VBox, HBox; Splitter (share of each pane) |
Rslot_set_grid, Rslot_set_grid_span | Grid, UniformGrid |
Rslot_set_anchors, Rslot_set_position, Rslot_set_size, Rslot_set_alignment | Canvas |
Rslot_set_title | TabView, 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.
/* 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.
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).
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 */
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).
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 */
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);
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 */
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);
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.
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");
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).
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.
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);
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 */
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.
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);
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.
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).
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
| File | Language | Built by | A function {f} | A widget |
|---|---|---|---|---|
.xmlc | C | CMake: raven_target_xmlc | void f(Rid, const char*, void*) | ui.x |
.xmlc++, .xmlcpp | C++ | CMake: raven_target_xmlc | the same, void f(Rid, const char*), or a lambda | ui.x |
.xmlrs | Rust | Cargo: raven_build::compile in build.rs | fn f(Rid, &str) | ui().x |
.xmlpy | Python | import raven_xmlpy, then import main; or python -m raven_xmlpy main.xmlpy | def f(sender, action) | ui.x |
.xmlgo | Go | //go:generate ravenc main.xmlgo -o main.xmlgo.go | func f(raven.ID, string) | ui.x |
.xmlcs | C# | MSBuild: Raven.XmlCs.targets | static void F(uint, string) | MainUi.x |
.xmlzig | Zig | build.zig: xmlzig.compile | fn f(Rid, ?[*:0]const u8, ?*anyopaque) callconv(.C) void | ui.x |
.xmllua | LuaJIT | require("raven_xmllua"), then require("main") | function f(sender, action) (global) | ui.x |
a < b && c is written as it is). As many <script> as wanted.main.xmlc:12, and the IDE opens it there.Rid main_create(void);.@keys, themes, slots....xml files are still read at run time, with live reload: use them while designing, .xml... to ship.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.
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");
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);
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".
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);
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 */
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.
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);
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();
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 */
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.
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);
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);
Each holds a small model that the application fills and reads, or that a document describes (XML).
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);
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.
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.
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 */
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);
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 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.
| Material | |
|---|---|
R_MATERIAL_SOLID | filled with the colors of the theme |
R_MATERIAL_GLASS | clear glass: what is behind shows through, bent at the rim |
R_MATERIAL_FROSTED_GLASS | blurred: the colors behind, without their shapes |
R_MATERIAL_TINTED_GLASS | blurred 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>
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).
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
#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.
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.
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).
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>".
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.
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);
}
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()
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.
Project > New project (Ctrl+Shift+N) asks a name, a language (C, C++, Rust, Python, Go, C#, Zig, Lua), a location, and a kind:
main.xml... (XMLC), the files of the tools of the
language (CMakeLists.txt, Cargo.toml and build.rs, go.mod, .csproj,
build.zig) and strings.csv. It builds as it is.main.c and a CMakeLists.txt, src/main.rs and a
Cargo.toml, main.py, main.go and a go.mod, Program.cs and a
.csproj, src/main.zig and a build.zig, main.lua). Nothing of Raven.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.
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.
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.
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.
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.
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.
| Variable | |
|---|---|
RAVEN_LIVE_RELOAD=0 | files are not watched (live reload) |
RAVEN_ALLOCATOR=system | malloc instead of the heap of Raven |
RAVEN_ACCESSIBILITY=1 | Linux: expose the interface on AT-SPI even when no screen reader is announced |
RAVEN_X11_COMPOSITED=0 / 1 | Linux: tells whether a compositor runs, instead of asking |
RAVEN_WEB_BROWSER | the browser raven_web starts |
RAVEN_WEB_ARGUMENTS | switches added to its command line |
RAVEN_WEB_NO_SANDBOX | starts it with --no-sandbox (containers) |
RAVEN_WEB_TRACE=1, RAVEN_WEB_DEBUG=1 | print the messages of the browser; let its errors through |
RAVEN_LIBRARY | bindings: the path of the shared library |
RAVEN_LIB_DIR | Rust (build.rs) and Zig (build.zig): the folder of the library |
RAVEN_STATIC=1 | Rust: link the static library instead of the shared one |
RAVENC | the ravenc the language tools run |
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