Version: 1.2.0-build2
Last updated: 2026-07-30
Language: English |
Reference for the seven 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.0-build2.
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) |
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:"geolocation" / "notifications" /
"audio-capture" /"video-capture" / "midi" /
"clipboard" / "unknown"TRUE, the request is denied
(deny-by-default)const char* blink_permission_request_get_type_name(BlinkPermissionRequest* request);
void blink_permission_request_allow(BlinkPermissionRequest* request);
void blink_permission_request_deny(BlinkPermissionRequest* request);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 |