Version: 1.2.2-build6
Last updated: 2026-07-30
Language: English |
Reference for the eight GObject signals emitted by
BlinkWebView: load
state, URL and title changes, new-window requests, messages from page
JS,
and permission requests.
This page is written from measurements of the actual engine
implementation.
The wiring status of each handler was re-checked against
1.2.2-build6.
Major correction from earlier editions (before 2026-07-30): previous
editions showedtitle-changed/uri-changedcallbacks with a string
argument. Neither signal has any extra argument. Code written against
the old signatures receivesuser_datain the string-argument position —
a recipe for crashes and memory corruption. The signatures on this page
match the implementation.
| Signal | Extra arguments | When it fires | Use |
|---|---|---|---|
load-changed |
BlinkLoadEvent | Load state changes (see the table below) | Progress display, UI updates |
load-failed |
error_code (int), failing_uri (in this order) | Only non-committed failures (see below) | Error handling |
title-changed |
none — read the value with
get_title() |
Title changes | Window title updates |
uri-changed |
none — read the value with
get_uri() |
URL changes | Address bar updates |
new-window-requested |
url | window.open() / target="_blank" |
Notification of new-window requests (no control, see below) |
message-received |
name, data (two arguments) | postMessage from page JS |
JS-to-C bridge (App Integration API) |
permission-request |
BlinkPermissionRequest* | Page requests a permission | Allow/deny geolocation, notifications, … (default deny, synchronous response required) |
render-path-changed |
path_id, result, gate_ms, seq (4 arguments) | Render-path swap completed | Result of switch_render_path() (v1.2.0-build9+) |
The property notifications notify::uri /
notify::title /
notify::is-loading also work (when migrating from
WebKitGTK, reusing
notify::uri / notify::title unchanged is the
easiest path).
load_uri() fires
synchronously (seetitle-changed / uri-changed carry
no arguments. Read the valuesblink_web_view_get_title() /blink_web_view_get_uri().g_strdup()
them if youstatic void on_title_changed(BlinkWebView *view, gpointer user_data) {
GtkWindow *win = GTK_WINDOW(user_data);
const char *title = blink_web_view_get_title(view); /* getter, not an argument */
gtk_window_set_title(win, title ? title : "BlinkGTK");
}
static void on_uri_changed(BlinkWebView *view, gpointer user_data) {
GtkEditable *entry = GTK_EDITABLE(user_data);
const char *uri = blink_web_view_get_uri(view);
gtk_editable_set_text(entry, uri ? uri : "");
}
g_signal_connect(view, "title-changed", G_CALLBACK(on_title_changed), win);
g_signal_connect(view, "uri-changed", G_CALLBACK(on_uri_changed), entry);void user_function(BlinkWebView* web_view,
BlinkLoadEvent load_event,
gpointer user_data);typedef enum {
BLINK_LOAD_STARTED = 0,
BLINK_LOAD_COMMITTED = 1,
BLINK_LOAD_FINISHED = 2,
BLINK_LOAD_REDIRECTED = 3 /* reserved — not emitted by the current runtime */
} BlinkLoadEvent;BLINK_LOAD_REDIRECTED is never emitted
today (reserved for theif (ev == 3) will
break)| Value | Actual firing condition |
|---|---|
STARTED |
Fires synchronously when you call
blink_web_view_load_uri() /
load_html() — and at no other time. Link clicks,
location.href,
go_back()/go_forward()/reload()
and page-initiated navigations do not fire it (the
biggest incompatibility with WebKitGTK) |
COMMITTED |
On navigation commit — with no frame filtering: iframes, pushState and fragment moves (same-document) all fire it. Not usable as "we entered a new page" |
FINISHED |
Main-frame onload completion. Not fired for iframes, for pushState/fragment moves, or when back/forward restores from the back-forward cache |
| Operation | STARTED | COMMITTED | FINISHED | uri-changed |
|---|---|---|---|---|
load_uri() (normal navigation) |
○ | ○ | ○ | ○ (sometimes twice, see below) |
load_html() |
○ | ○ | ○ | — |
Link click / location.href |
— | ○ | ○ | ○ |
pushState / fragment (#foo) |
— | ○ | — | ○ |
go_back() / go_forward() (normal) |
— | ○ | ○ | ○ |
| Same, restored from BFCache | — | ○ | — | ○ |
reload() |
— | ○ | ○ | — (URL unchanged) |
| iframe load / navigation | — | ○ (per iframe) | — | — |
Right before FINISHED, the engine has finished applying injected
stylesheets
(App Integration API) and the
JS message
bridge (window.blinkgtk). Calling
execute_javascript() from a FINISHED
handler can already use the bridge.
FINISHED means onload, though — not that dynamically-built content
has
finished appearing (a paint-complete notification is not yet
provided).
Stopping only on FINISHED can leave the spinner running
forever:
load-failedblink_web_view_is_loading() as a safety netvoid user_function(BlinkWebView* web_view,
int error_code,
const char* failing_uri,
gpointer user_data);The arguments come in (error_code, failing_uri)
order. A callback in the
reverse order reads a pointer as an integer.
The only condition is "a navigation that ended without
committing, with a
net error". Consequences:
| Case | load-failed | What actually happens |
|---|---|---|
| Cancelled by the user or by a newer navigation | ○ (most frequent) | error_code = -3 (ERR_ABORTED). Fires during
perfectly normal use — an unconditional error dialog will spam
the user |
| DNS failure / connection refused (e.g. -105, -102) | — (usually) | Chromium commits an error page, so you get COMMITTED + FINISHED and the built-in error page is shown |
| HTTP 404 / 500 | — | Normal commit (not a net error). COMMITTED + FINISHED |
| Subresource failures (images, CSS, fetch) | — | Not navigations |
| Aborted iframe navigation | ○ | failing_uri is the iframe's URL —
guard by comparing against the main page URL |
| Turned into a download / 204 / 205 | — | No signal at all |
Values are raw negative Chromium net error codes
(-3, -105, …). The
header's BlinkLoadError enum
(BLINK_LOAD_ERROR_NETWORK etc.) is unused
by the current runtime — treat the argument as an int in
C.
| Value | Constant (reference) | Meaning |
|---|---|---|
| -3 | net::ERR_ABORTED |
Aborted (most frequent; occurs in normal use) |
| -7 | net::ERR_TIMED_OUT |
Timeout |
| -105 | net::ERR_NAME_NOT_RESOLVED |
DNS failure (usually commits an error page instead of firing load-failed) |
Full list: Chromium net_error_list.h.
static void on_load_failed(BlinkWebView *view, int error_code,
const char *failing_uri, gpointer user_data) {
if (error_code == -3) /* ERR_ABORTED: byproduct of normal use; ignore */
return;
const char *current = blink_web_view_get_uri(view);
if (current && failing_uri && strcmp(current, failing_uri) != 0)
return; /* failure of something other than the shown page */
g_message("load failed: %d %s", error_code, failing_uri);
}void user_function(BlinkWebView* web_view, gpointer user_data);Read the values inside the handler with
blink_web_view_get_uri() /
blink_web_view_get_title().
| Aspect | Behavior |
|---|---|
| Firing points | ① synchronously inside load_uri(), ② on navigation
commit. Not fired mid-redirect |
| Can fire twice for one navigation | ① records the string you passed, ② compares against the normalized
URL, so differences like "http://example.com" vs
"http://example.com/" produce two emissions. Deduplicate on
your side if you maintain your own history |
get_uri() at point ① |
Returns the not-yet-committed URL. If the navigation then fails, that value was never actually shown |
| Rollback on abort | None. The failed URL lingers in your address bar; re-read
get_uri() from load-failed to restore it |
load_html() |
Does not fire it |
| Aspect | Behavior |
|---|---|
| Firing condition | When the page sets <title> or changes
document.title (main frame only) |
Pages without <title> |
No emission (silence — not an empty string) |
| History back/forward | If the history entry already carries the same title, it may
not fire. For tab UIs, also listen to
notify::title |
| Value shaping | Leading/trailing whitespace is already trimmed |
get_title() return |
Never empty — for untitled pages it returns a prettified URL. Code that detects "no title" via empty string won't work |
void user_function(BlinkWebView* web_view,
const char* url,
gpointer user_data);It tells you that window.open() or
<a target="_blank"> happened; you
cannot block it or open it elsewhere (no return value). The
current engine
is single-window: by the time the signal fires it has already
started
navigating this same WebView to the target URL.
load_uri(url) from the
handler — the engine has alreadyg_idle_add() (fortarget="_blank"-style requests the engine's own navigation
wins over awindow.open(): normally a window
that isclosed === true; with noopener,
null. Connecting aReceives window.blinkgtk.postMessage() from page JS.
Two arguments:
(name, data) — channel name and Base64 data. Full semantics
(encodings,
multi-subscriber behavior, relation to the C-API handler) are documented
in
the App Integration API.
Receives permission requests such as geolocation and notifications.
static gboolean on_permission(BlinkWebView *view,
BlinkPermissionRequest *req,
gpointer user_data) {
const char *kind = blink_permission_request_get_type_name(req);
if (g_strcmp0(kind, "notifications") == 0)
blink_permission_request_allow(req); /* allow notifications only */
else
blink_permission_request_deny(req);
return TRUE; /* handled */
}
g_signal_connect(view, "permission-request", G_CALLBACK(on_permission), NULL);blink_permission_request_allow() orblink_permission_request_deny(), exactly once,
synchronously insideBlinkPermissionRequest
becomes invalid onceTRUEblink_permission_request_get_type_name() returns the
kind: one of the 38"unknown". Do not allow
everything withoutTRUE, the request is denied
(deny-by-default)What a reading application actually uses:
| Name | What it asks for |
|---|---|
"screen-wake-lock" |
Keep the screen on while reading |
"system-wake-lock" |
Keep the device from suspending |
"local-fonts" |
Read the list of fonts installed on the device |
"persistent-storage" |
Keep bookmarks and annotations from being evicted |
"automatic-fullscreen" |
Enter fullscreen without a user gesture |
"pointer-lock" / "keyboard-lock" |
Take over page-turning input |
"storage-access" /
"top-level-storage-access" |
Read another origin's storage |
Network:
| Name | What it asks for |
|---|---|
"local-network-access" |
Connect to a local or loopback address |
"local-network" |
Connect to a local address |
"loopback-network" |
Connect to loopback (127.0.0.1 and so on) |
"background-fetch" |
Start a transfer that outlives the page |
"background-sync" /
"periodic-background-sync" |
Sync later / periodically |
"payment-handler" |
Handle payments |
Input and devices:
| Name | What it asks for |
|---|---|
"geolocation" /
"geolocation-approximate" |
Location / coarse location |
"notifications" |
Show notifications |
"audio-capture" / "video-capture" |
Microphone / camera |
"camera-pan-tilt-zoom" |
Move and zoom the camera |
"display-capture" |
Capture the screen |
"captured-surface-control" |
Control the captured surface |
"speaker-selection" |
Choose the output speaker |
"midi" |
MIDI devices |
"sensors" |
Accelerometer, orientation and similar sensors |
"nfc" |
NFC |
"smart-card" |
Smart cards |
"web-printing" |
Printing |
"clipboard" |
Read and write the clipboard |
"idle-detection" |
Know whether the user stepped away |
"window-management" |
Screen layout and window placement |
"vr" / "ar" /
"hand-tracking" |
WebXR |
"protected-media-identifier" |
Identifier for protected playback |
"web-app-installation" |
Install as an application |
"unknown" is returned only when Chromium gained
a kind that BlinkGTK has
not caught up with yet.
release/scripts/check-permission-type-names.sh
detects that state.
Views created with blink_web_view_new() share
one profile and cache within a
process. Opening the same URL in several views serves all but
the first from
that shared cache, so the server sees a single request.
Use blink_web_view_new_with_profile() to separate
them.
GtkWidget *a = blink_web_view_new_with_profile("tab1");
GtkWidget *b = blink_web_view_new_with_profile("tab2");profiles/<name> inside the
process profile directory- and
_ only; anything else returns
NULLblink_gtk_shutdown().blink_gtk_get_profile_path() reports does
not changeprofiles/<name>/Cache).blink_web_view_new() keep an in-memory
cacheblink_gtk_shutdown(). The next run starts
warm.const char* blink_permission_request_get_type_name(BlinkPermissionRequest* request);
void blink_permission_request_allow(BlinkPermissionRequest* request);
void blink_permission_request_deny(BlinkPermissionRequest* request);Completion notification for a live render-path switch (BlinkShift)
requested
with blink_web_view_switch_render_path(). The switch is
asynchronous; this
signal is the authoritative result (the function's return value only
says
whether the request was accepted).
static void on_render_path_changed(BlinkWebView *view,
const char *path_id, /* "P1" / "P2" */
const char *result, /* see table */
int gate_ms, /* time taken, ms */
int seq, /* this swap's number */
gpointer user_data) {
g_print("render path -> %s (%s, %d ms, seq=%d)\n",
path_id, result, gate_ms, seq);
}
g_signal_connect(view, "render-path-changed",
G_CALLBACK(on_render_path_changed), NULL);
int seq = blink_web_view_switch_render_path(view, "P2");
if (seq == 0)
g_warning("switch request was not accepted");
/* seq is assigned synchronously when the request is accepted, and the
* completion signal carries the same number — so records taken before and
* after a swap can be keyed to the exact swap they describe. Matching by
* time proximity is unreliable when swaps run back to back. */| Value | Meaning | What your app should do |
|---|---|---|
"ok" |
The new path renders stably | Success. get_render_path() now returns the new
path |
"rollback" |
The new path failed the render gate; the previous path was restored | The screen is intact. The switch did not take effect |
"fail" |
The previous path could not be restored either | Treat as an error |
gate_ms measures from the request to the completion of
the resume gateblink_web_view_get_render_path()"ok")Guaranteed by the implementation:
load_uri(): uri-changed →
load-changed(STARTED) (synchronous,notify::is-loadingNot guaranteed:
load_uri() etc. run on the calling thread (there is no
thread check)blink_gtk_shutdown() is
in progress; atget_uri() / get_title()
return NULL, so handlers mustg_strdup()
anything you keepBlinkWebViewEgl widget currently emits
none of theseBlinkWebView only)#include <blink_gtk/blink_gtk.h>
#include <gtk/gtk.h>
#include <string.h>
typedef struct {
GtkWidget *window;
GtkWidget *url_entry;
GtkWidget *spinner;
guint spinner_timeout;
} BrowserUI;
static gboolean stop_spinner_fallback(gpointer data) {
BrowserUI *ui = data;
gtk_widget_set_visible(ui->spinner, FALSE);
ui->spinner_timeout = 0;
return G_SOURCE_REMOVE;
}
static void on_load_changed(BlinkWebView *view, BlinkLoadEvent ev,
gpointer user_data) {
BrowserUI *ui = user_data;
if (ev == BLINK_LOAD_STARTED) {
gtk_widget_set_visible(ui->spinner, TRUE);
/* safety net for the silent cases (downloads, 204) */
if (ui->spinner_timeout)
g_source_remove(ui->spinner_timeout);
ui->spinner_timeout =
g_timeout_add_seconds(30, stop_spinner_fallback, ui);
} else if (ev == BLINK_LOAD_FINISHED) {
gtk_widget_set_visible(ui->spinner, FALSE);
if (ui->spinner_timeout) {
g_source_remove(ui->spinner_timeout);
ui->spinner_timeout = 0;
}
}
}
static void on_load_failed(BlinkWebView *view, int error_code,
const char *failing_uri, gpointer user_data) {
BrowserUI *ui = user_data;
gtk_widget_set_visible(ui->spinner, FALSE);
if (error_code == -3)
return; /* ERR_ABORTED: byproduct of normal use */
g_message("load failed: %d %s", error_code,
failing_uri ? failing_uri : "");
}
static void on_uri_changed(BlinkWebView *view, gpointer user_data) {
BrowserUI *ui = user_data;
const char *uri = blink_web_view_get_uri(view);
gtk_editable_set_text(GTK_EDITABLE(ui->url_entry), uri ? uri : "");
}
static void on_title_changed(BlinkWebView *view, gpointer user_data) {
BrowserUI *ui = user_data;
const char *title = blink_web_view_get_title(view);
char *t = g_strdup_printf("%s - MyBrowser",
(title && *title) ? title : "(untitled)");
gtk_window_set_title(GTK_WINDOW(ui->window), t);
g_free(t);
}
static void setup_signals(BlinkWebView *view, BrowserUI *ui) {
g_signal_connect(view, "load-changed", G_CALLBACK(on_load_changed), ui);
g_signal_connect(view, "load-failed", G_CALLBACK(on_load_failed), ui);
g_signal_connect(view, "uri-changed", G_CALLBACK(on_uri_changed), ui);
g_signal_connect(view, "title-changed", G_CALLBACK(on_title_changed), ui);
}| WebKitGTK | BlinkGTK | Notes |
|---|---|---|
load-changed (STARTED) |
load-changed (STARTED) |
Incompatible: BlinkGTK's STARTED only fires when
you call load_uri()/load_html() — never for
link clicks. Don't key your spinner start on STARTED alone |
load-changed (REDIRECTED) |
— | Never fires in BlinkGTK |
load-changed (COMMITTED / FINISHED) |
Same names | COMMITTED additionally fires for iframes / same-document navigations |
load-failed |
load-failed |
Different arguments (int + uri instead of GError; error_code comes first). Narrower firing conditions (see the table on this page) |
notify::title / notify::uri |
Work as-is | The easiest migration path. The dedicated signals (title-changed / uri-changed) carry no arguments — that's the only trap |
create (new window) |
new-window-requested |
Incompatible: notification only; you cannot block or open in another WebView |
permission-request |
permission-request |
Synchronous response required (no deferred answers as in WebKitGTK) |
| Item | Status |
|---|---|
| Redirect detection | Not possible (BLINK_LOAD_REDIRECTED is reserved) |
| Detecting page-initiated navigation start | Not possible (STARTED fires only on API calls) |
| Controlling new-window requests | Not possible (notification only; always opens in the same view) |
| load-failed for committed failures (DNS etc.) | Does not fire (the built-in error page is shown instead) |
| Paint-complete notification | Not provided (FINISHED means onload) |
| permission-request from language bindings | Not possible (raw-pointer argument) |
go_back etc.): Navigation API| Version | Changes |
|---|---|
| v1.1.0 (2026-07-30) | Complete revision based on measured engine behavior. Corrected the title-changed / uri-changed signatures (no arguments — the old signatures caused crashes). Documented that BLINK_LOAD_REDIRECTED never fires, that STARTED fires only on API calls, the real load-failed conditions (non-committed failures only / -3 most frequent), the per-operation firing table, emission ordering, the silent cases (downloads, 204) with spinner countermeasures, and that new-window-requested is notification-only |
| v1.1.0 (before 2026-07-29) | Older editions |