Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f4bc8fb
GTK4: continuous drag for Canvas value controls
joshpar Sep 7, 2026
f21316a
GTK4: narrow in-place widget updates during a drag
joshpar Sep 8, 2026
6c3e8e5
GTK4: TextField as a first-class descriptor node (live in-place text …
joshpar Sep 8, 2026
e0c93b7
GTK4: transparent-describe for single-content wrapper modifiers (narr…
joshpar Sep 8, 2026
eb6a081
GTK4: opaque-leaf descriptors so native widgets stop poisoning the na…
joshpar Sep 8, 2026
617afa6
GTK4: transparent-describe for _ConditionalView and Optional views
joshpar Sep 8, 2026
47eeb23
GTK4: narrow-reject diagnostics (SWIFTOPENUI_NARROW_DEBUG)
joshpar Sep 8, 2026
daf145e
GTK4: broaden narrow-path diagnostics (pass count + full outcome)
joshpar Sep 8, 2026
a9d8b70
GTK4: log supported-slot count mismatch (narrow slot-capture bail)
joshpar Sep 8, 2026
f60b6c5
GTK4: Button describes its custom label's children (slot-count balance)
joshpar Sep 8, 2026
9631e87
GTK4: remove narrow-path diagnostics (Patch F validated)
joshpar Sep 8, 2026
810fdb4
GTK4: broaden narrow-path coverage (batch 2 of Patch F)
joshpar Sep 9, 2026
ecbd1da
GTK4: imperative standalone-window API (GTK4Backend.openStandaloneWin…
joshpar Sep 9, 2026
bc246c9
GTK4: fix Linux build breaks in the batch-2 narrow-path coverage
joshpar Sep 9, 2026
004bfc7
GTK4: process-wide pointer tracking (GTK4PointerTracking)
joshpar Sep 9, 2026
29fd1f1
GTK4: CodeEditor view backed by GtkSourceView 5 (syntax highlighting …
joshpar Sep 10, 2026
78d6ff3
GTK4: CodeEditor exposes its selection (for line-level evaluation)
joshpar Sep 10, 2026
62a74bd
GTK4: CodeEditor reuses its widget across rebuilds (GTKOpaqueLeaf)
joshpar Sep 10, 2026
a392026
GTK4: CodeEditor Ctrl+Space completion popover
joshpar Sep 10, 2026
6d3475a
GTK4: filter completions by typed prefix; replace prefix on insert
joshpar Sep 10, 2026
9cf9423
GTK4: native Open/Save file dialogs + CodeEditor imperative setText
joshpar Sep 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,23 @@ targets += [
pkgConfig: "gtk4",
providers: [.apt(["libgtk-4-dev"])]
),
// GtkSourceView 5 — the code-editor widget behind SwiftOpenUI's `CodeEditor`
// (syntax highlighting / gutter). Its own system-library module so the
// gtksourceview include path + link stay off the gtk4-only CGTK module.
.systemLibrary(
name: "CGtkSource",
path: "Sources/Backend/GTK4/CGtkSource",
pkgConfig: "gtksourceview-5",
providers: [.apt(["libgtksourceview-5-dev"])]
),
.target(
name: "CGTKBridge",
dependencies: ["CGTK"],
path: "Sources/Backend/GTK4/CGTKBridge"
),
.target(
name: "BackendGTK4",
dependencies: ["SwiftOpenUI", "CGTK", "CGTKBridge", "SwiftOpenUISymbols"],
dependencies: ["SwiftOpenUI", "CGTK", "CGtkSource", "CGTKBridge", "SwiftOpenUISymbols"],
path: "Sources/Backend/GTK4/Rendering",
linkerSettings: [
// FontConfig is used by the process-local font registration
Expand Down
87 changes: 87 additions & 0 deletions Sources/Backend/GTK4/CGTK/shim.h
Original file line number Diff line number Diff line change
Expand Up @@ -1191,3 +1191,90 @@ gtk_swift_get_active_window(void) {
if (!app || !GTK_IS_APPLICATION(app)) return NULL;
return gtk_application_get_active_window(GTK_APPLICATION(app));
}

/// Return the process default GApplication cast to GtkApplication, or NULL if
/// there is no default application or it is not a GtkApplication. Used to
/// parent an imperatively-opened standalone window (via
/// GTK4Backend.openStandaloneWindow) to the GtkApplication started by
/// GTK4Backend.run(_:), so a window can be opened at runtime from outside the
/// App/Scene tree.
static inline GtkApplication *
gtk_swift_get_default_gtk_application(void) {
GApplication *app = g_application_get_default();
if (!app || !GTK_IS_APPLICATION(app)) return NULL;
return GTK_APPLICATION(app);
}

// --- Native file dialog (GtkFileDialog, GTK 4.10+) ---
//
// Async open/save wrappers for the live-coding scratchpad's Open/Save buttons.
// The chosen path (or NULL on cancel/error) is delivered to a Swift callback
// via an opaque user pointer; the dialog is parented to the active window (see
// gtk_swift_get_active_window) so GTK 4.14 doesn't emit realization criticals.
typedef void (*LyrebirdFileDialogCB)(const char *path, void *user);

typedef struct {
LyrebirdFileDialogCB cb;
void *user;
} LyrebirdFileDialogCtx;

static void
lyrebird_fd_open_done(GObject *src, GAsyncResult *res, gpointer data) {
LyrebirdFileDialogCtx *ctx = (LyrebirdFileDialogCtx *)data;
GFile *file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(src), res, NULL);
if (file) {
char *p = g_file_get_path(file);
ctx->cb(p, ctx->user);
g_free(p);
g_object_unref(file);
} else {
ctx->cb(NULL, ctx->user);
}
g_free(ctx);
}

static void
lyrebird_fd_save_done(GObject *src, GAsyncResult *res, gpointer data) {
LyrebirdFileDialogCtx *ctx = (LyrebirdFileDialogCtx *)data;
GFile *file = gtk_file_dialog_save_finish(GTK_FILE_DIALOG(src), res, NULL);
if (file) {
char *p = g_file_get_path(file);
ctx->cb(p, ctx->user);
g_free(p);
g_object_unref(file);
} else {
ctx->cb(NULL, ctx->user);
}
g_free(ctx);
}

/// Present a native Open dialog. `cb(path, user)` is invoked on the GTK main
/// thread with the chosen path, or NULL if cancelled.
static inline void
gtk_swift_present_open_dialog(const char *title, void *user, LyrebirdFileDialogCB cb) {
GtkFileDialog *dlg = gtk_file_dialog_new();
if (title) gtk_file_dialog_set_title(dlg, title);
LyrebirdFileDialogCtx *ctx = g_new0(LyrebirdFileDialogCtx, 1);
ctx->cb = cb;
ctx->user = user;
gtk_file_dialog_open(dlg, gtk_swift_get_active_window(), NULL,
lyrebird_fd_open_done, ctx);
g_object_unref(dlg);
}

/// Present a native Save dialog, pre-filling `suggested_name` (may be NULL).
/// `cb(path, user)` is invoked on the GTK main thread with the chosen path, or
/// NULL if cancelled.
static inline void
gtk_swift_present_save_dialog(const char *title, const char *suggested_name,
void *user, LyrebirdFileDialogCB cb) {
GtkFileDialog *dlg = gtk_file_dialog_new();
if (title) gtk_file_dialog_set_title(dlg, title);
if (suggested_name) gtk_file_dialog_set_initial_name(dlg, suggested_name);
LyrebirdFileDialogCtx *ctx = g_new0(LyrebirdFileDialogCtx, 1);
ctx->cb = cb;
ctx->user = user;
gtk_file_dialog_save(dlg, gtk_swift_get_active_window(), NULL,
lyrebird_fd_save_done, ctx);
g_object_unref(dlg);
}
5 changes: 5 additions & 0 deletions Sources/Backend/GTK4/CGtkSource/module.modulemap
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module CGtkSource [system] {
header "shim.h"
link "gtksourceview-5"
export *
}
156 changes: 156 additions & 0 deletions Sources/Backend/GTK4/CGtkSource/shim.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// CGtkSource — a thin, OpaquePointer-only shim over GtkSourceView 5, kept in its
// own system-library module (pkgConfig "gtksourceview-5") so the gtksourceview
// include path + link don't have to be forced onto the gtk4-only CGTK module.
//
// Every function takes/returns `void*` (an opaque GtkWidget* / GtkTextBuffer*),
// so the Swift caller treats the handles as `OpaquePointer`/`gpointer` and never
// imports GtkSourceView's C types alongside CGTK's — avoiding cross-module
// GtkWidget/GtkTextBuffer redefinitions. Signal wiring stays on the CGTK side
// (the buffer is just a gpointer to g_signal_connect_data).
#ifndef LYREBIRD_CGTKSOURCE_SHIM_H
#define LYREBIRD_CGTKSOURCE_SHIM_H

#include <gtksourceview/gtksource.h>

// Create a code editor view configured for Swift: syntax highlighting, a
// line-number gutter, a monospace font, 4-space soft tabs, and auto-indent.
// Returns the GtkSourceView as an opaque GtkWidget*.
static inline void *
gtk_swift_source_view_new(void) {
GtkSourceView *view = GTK_SOURCE_VIEW(gtk_source_view_new());
gtk_source_view_set_show_line_numbers(view, TRUE);
gtk_source_view_set_auto_indent(view, TRUE);
gtk_source_view_set_highlight_current_line(view, TRUE);
gtk_source_view_set_tab_width(view, 4);
gtk_source_view_set_insert_spaces_instead_of_tabs(view, TRUE);

// Monospace is a GtkTextView property in GtkSourceView 5 (there is no
// gtk_source_view_set_monospace).
GtkTextView *tv = GTK_TEXT_VIEW(view);
gtk_text_view_set_monospace(tv, TRUE);

GtkSourceBuffer *buf =
GTK_SOURCE_BUFFER(gtk_text_view_get_buffer(tv));
GtkSourceLanguageManager *lm = gtk_source_language_manager_get_default();
GtkSourceLanguage *lang =
gtk_source_language_manager_get_language(lm, "swift");
if (lang) {
gtk_source_buffer_set_language(buf, lang);
gtk_source_buffer_set_highlight_syntax(buf, TRUE);
}
return (void *)view;
}

// The view's GtkSourceBuffer as an opaque GtkTextBuffer* (it is a subclass, so
// the base gtk_text_buffer_* API applies).
static inline void *
gtk_swift_source_view_get_buffer(void *view) {
return (void *)gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
}

static inline void
gtk_swift_source_buffer_set_text(void *buffer, const char *text, int length) {
gtk_text_buffer_set_text((GtkTextBuffer *)buffer, text, length);
}

// Full buffer contents. Caller must g_free the returned string.
static inline char *
gtk_swift_source_buffer_get_text(void *buffer) {
GtkTextIter start, end;
gtk_text_buffer_get_bounds((GtkTextBuffer *)buffer, &start, &end);
return gtk_text_buffer_get_text((GtkTextBuffer *)buffer, &start, &end, FALSE);
}

// The currently selected text, or an empty string when nothing is selected.
// Caller must g_free the returned string.
static inline char *
gtk_swift_source_buffer_get_selected_text(void *buffer) {
GtkTextBuffer *b = (GtkTextBuffer *)buffer;
GtkTextIter start, end;
if (!gtk_text_buffer_get_selection_bounds(b, &start, &end)) {
return g_strdup("");
}
return gtk_text_buffer_get_text(b, &start, &end, FALSE);
}

// Cursor position as 0-based (line, column-in-characters).
static inline void
gtk_swift_source_buffer_get_cursor_line_col(void *buffer, int *line, int *col) {
GtkTextBuffer *b = (GtkTextBuffer *)buffer;
GtkTextMark *insert = gtk_text_buffer_get_insert(b);
GtkTextIter it;
gtk_text_buffer_get_iter_at_mark(b, &it, insert);
*line = gtk_text_iter_get_line(&it);
*col = gtk_text_iter_get_line_offset(&it);
}

// Insert `text` at the cursor (replacing any selection).
static inline void
gtk_swift_source_buffer_insert_at_cursor(void *buffer, const char *text) {
GtkTextBuffer *b = (GtkTextBuffer *)buffer;
gtk_text_buffer_begin_user_action(b);
if (gtk_text_buffer_get_has_selection(b)) {
gtk_text_buffer_delete_selection(b, FALSE, TRUE);
}
gtk_text_buffer_insert_at_cursor(b, text, -1);
gtk_text_buffer_end_user_action(b);
}

// Replace the identifier word immediately before the cursor with `text`, then
// leave the caret after the inserted text. Used when applying a completion so
// the already-typed prefix (`Osc` when picking `OscSin`) is not duplicated. When
// there is no identifier prefix before the caret (e.g. just after a `.`), this
// inserts without deleting anything. Any active selection is replaced first.
static inline void
gtk_swift_source_buffer_replace_prefix_at_cursor(void *buffer, const char *text) {
GtkTextBuffer *b = (GtkTextBuffer *)buffer;
gtk_text_buffer_begin_user_action(b);
if (gtk_text_buffer_get_has_selection(b)) {
gtk_text_buffer_delete_selection(b, FALSE, TRUE);
}
GtkTextMark *insert = gtk_text_buffer_get_insert(b);
GtkTextIter end;
gtk_text_buffer_get_iter_at_mark(b, &end, insert);
GtkTextIter start = end;
// Walk left over identifier characters (letters, digits, underscore).
while (gtk_text_iter_backward_char(&start)) {
gunichar ch = gtk_text_iter_get_char(&start);
if (!(g_unichar_isalnum(ch) || ch == '_')) {
gtk_text_iter_forward_char(&start);
break;
}
}
gtk_text_buffer_delete(b, &start, &end);
gtk_text_buffer_insert_at_cursor(b, text, -1);
gtk_text_buffer_end_user_action(b);
}

// The caret's rectangle in widget coordinates (for anchoring a popover).
static inline void
gtk_swift_source_view_get_cursor_rect(void *view, int *x, int *y, int *w, int *h) {
GtkTextView *tv = GTK_TEXT_VIEW(view);
GtkTextBuffer *b = gtk_text_view_get_buffer(tv);
GtkTextMark *insert = gtk_text_buffer_get_insert(b);
GtkTextIter it;
gtk_text_buffer_get_iter_at_mark(b, &it, insert);
GdkRectangle loc;
gtk_text_view_get_iter_location(tv, &it, &loc);
int bx = 0, by = 0;
gtk_text_view_buffer_to_window_coords(tv, GTK_TEXT_WINDOW_WIDGET,
loc.x, loc.y, &bx, &by);
*x = bx; *y = by; *w = loc.width; *h = loc.height;
}

// Apply a named style scheme (e.g. "Adwaita-dark", "classic") when present.
static inline void
gtk_swift_source_buffer_set_style_scheme(void *buffer, const char *scheme_id) {
GtkSourceStyleSchemeManager *sm =
gtk_source_style_scheme_manager_get_default();
GtkSourceStyleScheme *scheme =
gtk_source_style_scheme_manager_get_scheme(sm, scheme_id);
if (scheme) {
gtk_source_buffer_set_style_scheme((GtkSourceBuffer *)buffer, scheme);
}
}

#endif /* LYREBIRD_CGTKSOURCE_SHIM_H */
Loading