docs

AppCore in C

Create desktop applications, manage windows and layouts, and dispatch work in C.

On this page

AppCore provides an application runtime, windows, layout containers, panels, and popups for C applications. You include <AppCore/CAPI.h>, which automatically includes the window layout declarations and <Ultralight/CAPI.h>.

Before working in C, read the App Framework guides starting with App Lifecycle and Settings. This page covers what differs in C, while C API Conventions covers shared C rules for memory management, callbacks, and threading.

Differences from C++

AppCore replaces C++ structs and listener interfaces with opaque handles, descriptor structs, and individual callback setters.

C++ Feature C Equivalent What Changes
Settings struct ulCreateSettings() and ulSettingsSet*() Configure an allocated handle and destroy it after calling ulCreateApp().
AppListener and WindowListener ulAppSet*Callback() and ulWindowSet*Callback() Register individual callbacks per event instead of implementing an interface.
LayoutCallback guard objects Slot setters like ulLayoutNodeSetLayoutChangeCallback() Attach callbacks directly to slots without saving a guard object.
Option structs with designated initializers Descriptor structs (ULPanelDesc, ULContainerDesc, ULForegroundPanelDesc) Zero-initialize the descriptor and set struct_size.
Size strings ("240px", "30%", "2fr") ULLayoutSize via ulLayoutSizePx(), ulLayoutSizePct(), and ulLayoutSizeFr() Build sizes with helper functions, where zero-initialization leaves the size unset.
RefPtr node handles ULPanel, ULContainer, and ULLayoutNode handles Returned layout handles are owned and must be destroyed manually.
TimerHandle ULAppTimer Timers repeat on the main thread until released with ulDestroyAppTimer().
Anchor fluent builder methods ULAnchorDesc Set kind and populate only the fields that placement style requires.

Creating the App

To initialize the application, configure a settings handle, create the app runtime, and register your per-frame update callback.

C
#include <AppCore/CAPI.h>

static ULApp app = NULL;

static void OnUpdate(void* user_data) {
  (void)user_data;
  // Per-frame app logic goes here.
}

void CreateApp(void) {
  ULSettings settings = ulCreateSettings();
  ULString developer = ulCreateString("Acme");
  ULString name = ulCreateString("Rocket Planner");
  ulSettingsSetDeveloperName(settings, developer);
  ulSettingsSetAppName(settings, name);
  ulDestroyString(developer);
  ulDestroyString(name);

  app = ulCreateApp(settings, NULL);
  ulDestroySettings(settings);  // The App keeps its own copy.

  ulAppSetUpdateCallback(app, OnUpdate, NULL, NULL);
}

Both ulCreateApp() and the settings setters copy the values you pass, so you can destroy temporary ULString and ULSettings handles immediately after using them. If you need the underlying ULRenderer instance, call ulAppGetRenderer()— the returned handle is borrowed and owned by the app, so you must never destroy it.

Destroy all open windows before calling ulDestroyApp(). For the full teardown sequence and application lifecycle, see the C examples in Writing Your First App.

Window Callbacks

AppCore provides individual callback setters for each window event rather than a listener object. Passing NULL as the callback function clears the registration.

Closing and Destroying Windows

Calling ulWindowClose() triggers the window's close callback, closes any owned popups, and disables hosted panels. The window handle remains valid, so you must still release it with ulDestroyWindow().

🚧 Close Before Destroying

Calling ulDestroyWindow() on an open window closes it immediately without invoking your close callback. If you rely on close logic to save state or confirm teardown, call ulWindowClose() first.

Intercepting Input Events

You can intercept keyboard, mouse, and scroll events before any page receives them by registering input callbacks on the window.

C
#include <AppCore/CAPI.h>

void ShowHelp(void);

// Open the help window on F1 before any page sees the key.
static bool OnKeyEvent(void* user_data, ULWindow window, ULKeyEvent evt) {
  (void)user_data;
  (void)window;
  if (ulKeyEventGetType(evt) == kKeyEventType_RawKeyDown &&
      ulKeyEventGetVirtualKeyCode(evt) == 0x70) {  // F1
    ShowHelp();
    return false;
  }
  return true;
}

void InterceptKeys(ULWindow window) {
  ulWindowSetKeyEventCallback(window, OnKeyEvent, NULL, NULL);
}

Keep these rules in mind when handling input events:

For details on window input routing, see Windows, Monitors, and DPI.

Laying Out Panels

You arrange a window's interface by organizing panels into a tree of row and column containers.

Configuring Descriptors and Sizes

To split a window into rows and columns, configure descriptor structs and add child nodes one call at a time:

C
#include <AppCore/CAPI.h>

static ULWindow main_window = NULL;  // From ulCreateWindow().
static ULPanel sidebar = NULL;
static ULPanel content = NULL;

void BuildLayout(void) {
  ULContainerDesc body_desc = {0};
  body_desc.struct_size = sizeof(ULContainerDesc);
  body_desc.key = "body";
  body_desc.flags = kULLayoutFlags_Resizable;

  ULPanelDesc sidebar_desc = {0};
  sidebar_desc.struct_size = sizeof(ULPanelDesc);
  sidebar_desc.key = "sidebar";
  sidebar_desc.size = ulLayoutSizePx(240);
  sidebar_desc.min_size = ulLayoutSizePx(160);

  ULContainer root = ulWindowGetLayout(main_window);
  ULContainer body = ulContainerAddRow(root, &body_desc, NULL);
  sidebar = ulContainerAddPanel(body, &sidebar_desc, NULL, NULL);
  content = ulContainerAddPanel(body, NULL, NULL, NULL);

  // The tree keeps both containers.
  ulDestroyContainer(body);
  ulDestroyContainer(root);
}

Passing NULL for a descriptor uses default settings for every field. Leaving a child node's size unset gives it a single flex share (1fr).

The C API provides no fluent layout builder— you build the tree node by node. To configure the window's root container, call ulWindowConfigureLayout(). For sizing rules and container properties, see Laying Out Panels.

🚧 Always Set Struct Size

Leaving struct_size set to 0 causes AppCore to ignore the descriptor, log a warning, and fall back to default values. This rule applies to every descriptor struct in the library, including ULBackdropOptions.

Managing Node Handles and Identity

Here is what to know about layout handles:

Accessing a Panel's View

Call ulPanelGetView() to access a panel's hosted View and load web content:

C
#include <AppCore/CAPI.h>

void LoadIntoPanel(ULPanel panel, const char* url_string) {
  ULView view = ulPanelGetView(panel);
  if (!view)
    return;  // The panel was removed or its window closed.

  ULString url = ulCreateString(url_string);
  ulViewLoadURL(view, url);
  ulDestroyString(url);
  ulDestroyView(view);
}

ulPanelGetView() returns an owned ULView handle, or NULL if the panel was removed or its window closed. Destroying this handle releases only your reference— the panel keeps its hosted View alive. For View callbacks, lifetime rules, and passing NULL handles, see Views in C.

Registering Layout Callbacks

In C, layout callbacks attach directly to slots on the target node or window, eliminating the need to store guard objects. Available setters include ulLayoutNodeSetLayoutChangeCallback(), ulLayoutNodeSetUserResizeCallback(), ulPanelSetDismissCallback(), ulWindowSetFocusChangeCallback(), and ulWindowSetEditableStateCallback(). A slot remains active until you clear it with NULL, replace it, remove the node, or close the window.

Node and panel handles passed as callback parameters are borrowed and remain valid only during the call. If you need to keep one past the callback, call ulCreateLayoutNodeRef() or ulCreatePanelRef(). Never call a destroy function on a borrowed handle.

📘 Window Handles in Callbacks

The focus-change and editable-state callbacks pass a borrowed ULWindow handle that never equals your original window pointer. Compare them using ulWindowIsSame() instead of ==, and never destroy the borrowed handle.

Floating Panels and Popups

You can anchor a floating panel beneath a web element by configuring an anchor descriptor on the window's foreground layer.

C
#include <AppCore/CAPI.h>
#include <Ultralight/CAPI/CAPI_DOMElement.h>

static ULWindow main_window = NULL;
static ULPanel toolbar = NULL;
static ULPanel dropdown = NULL;

static void OnDropdownDismissed(void* user_data, ULPanel panel) {
  (void)user_data;
  (void)panel;
  // The panel is already hidden; update application state here.
}

// Open a dropdown under a button on the toolbar's page.
void OpenDropdown(ULDOMElement button) {
  ULDOMRect box = ulDOMElementGetBoundingClientRect(button);

  ULForegroundPanelDesc desc = {0};
  desc.struct_size = sizeof(ULForegroundPanelDesc);
  desc.width = ulLayoutSizePx(240);
  desc.height = ulLayoutSizePx(180);
  desc.placement.kind = kULAnchorKind_Below;
  desc.placement.target = toolbar;
  desc.placement.rect = (ULLayoutRect){box.x, box.y, box.width, box.height};
  desc.placement.fit = kULAnchorFit_Flip;
  desc.dismiss = kULDismiss_Auto;
  desc.focus = kULFocusPolicy_Grab;

  ULForeground foreground = ulWindowGetForeground(main_window);
  dropdown = ulForegroundAddPanel(foreground, &desc, NULL);
  ulDestroyForeground(foreground);

  ulPanelSetDismissCallback(dropdown, OnDropdownDismissed, NULL, NULL);
}

Keep these points in mind when using floating panels and popups:

Posting Tasks and Timers

To dispatch work to the main thread from any thread, pass a callback and payload to ulAppPostTask():

C
#include <AppCore/CAPI.h>
#include <stdlib.h>
#include <string.h>

void ShowSave(const char* text);

static ULApp app = NULL;

static void ShowSaveTask(void* user_data) {
  ShowSave(user_data);
}

// Called on a worker thread once the save file is read.
void OnSaveLoaded(const char* text) {
  char* copy = malloc(strlen(text) + 1);
  strcpy(copy, text);

  // free() runs once the task has run on the main thread.
  ulAppPostTask(app, ShowSaveTask, copy, free);
}

Here is how tasks and timers work:

Features Without C Equivalents

A few C++ convenience methods and DSL builders have no direct equivalents in C.

C++ Feature C Alternative
Window::BuildLayout() and builders Add nodes one call at a time with ulContainerAddRow() or ulContainerAddPanel(), and configure the root using ulWindowConfigureLayout().
After() insertion position Insert before an anchor using insert_before, or pass NULL to append.
LayoutNode::next_sibling(), previous_sibling(), and Container::children() Walk sibling nodes by index using ulLayoutNodeGetParent(), ulLayoutNodeGetIndex(), and ulContainerGetChildAt().
App::instance() Store your ULApp handle in application state.
App::is_profiler_active() and App::profiler_trace_path() Profiler inspection methods are unavailable in C.