После релиза 1.1.0 у библиотеки swift-adwaita вышло семь релизов подряд. Пока я продолжаю разрабатывать первое реальное приложение на этой обёртке, всплывают вещи, которые в синтетических тестах не видны — и почти каждая правка отсюда. Главная история этого цикла: swift-adwaita теперь собирается и запускается на macOS, а заодно я наступил на красивые грабли со Swift Concurrency внутри GLib main-loop и аккуратно с них слез.

Swift Adwaita

История одного бага: async, который никогда не выполняется

В 1.2.0 я схлопнул все диалоги (FileDialog, ColorDialog, FontDialog) на async throws и убрал колбэк-варианты — казалось, так чище. Через сутки выяснилось, что внутри запущенного GTK-приложения (g_application_run) код вида Task { @MainActor in await dialog.open(...) } просто никогда не выполняется. Дефолтный исполнитель главного актора в Swift — это DispatchQueue.main, а GLib main-loop его не крутит. Процесс выглядит живым, ошибок нет, кнопка нажимается — но файловый диалог не появляется.

1.2.1 экстренно вернул колбэк-варианты для FileDialog, 1.2.2 закрыл эту дыру окончательно: колбэк-перегрузки добавлены для всех async-API (Clipboard, ColorDialog, FontDialog, UriLauncher, Texture.load). Async-варианты остались — они нужны для тестов и не-GTK контекста, — но из обработчиков сигналов GTK теперь по умолчанию рекомендуется колбэк-форма. Долгосрочное решение (свой SerialExecutor поверх GLib) отложено как отдельная задача.

1.2.0: что появилось в API

  • Асинхронная загрузка изображений. Texture.load(from:) декодирует всё, что умеет GdkPixbuf — PNG, JPEG, GIF, WebP, TIFF, BMP — вне главного актора. Это шире, чем умеет нативный gdk_texture_new_from_filename.
  • Воспроизведение анимированных изображений. AnimatedImagePlayer крутит кадры из GdkPixbufAnimation в виджете Picture с методами start / stop / advanceFrame.
  • Application.onOpen и Application.run(arguments:) — обработка активации по файлам для приложений с флагом G_APPLICATION_HANDLES_OPEN.
  • Runtime-проверки типов виджетов. Widget.gtkType, isInstance(of:), и более строгий tryCast, который теперь действительно сужает тип, а не «успешно» приводит любой виджет к чему угодно.
  • Изолированные deinit на GObjectRef, GVariant и других обёртках — освобождение GObject теперь всегда происходит на главном акторе явно, а не на случайном потоке, который дропнул последнюю ссылку.
  • Минимальный тулчейн поднят до Swift 6.2 — isolated deinit в 6.1 экспериментальный, релизный тулчейн отказывался его включать.

1.2.3–1.2.5: удобства и буфер обмена

Три небольших релиза о том, чтобы реже импортировать CAdwaita ради рутинных вещей:

  • RGBA(hex:) — парсинг CSS-цветов: #RGB, #RGBA, #RRGGBB, #RRGGBBAA.
  • IconTheme — обёртка над gtk_icon_theme_get_for_display с addSearchPath(_:) для локальных иконок приложения.
  • ApplicationFlags как OptionSet: Application(id: "...", flags: [.handlesOpen, .nonUnique]) вместо сырых битовых масок.
  • MainContext.drainPending() и pump(for:) — однострочные замены для while g_main_context_pending { g_main_context_iteration }, которые в каждом тестовом наборе писались заново.
  • Перехват вставки. Widget.onPasteClipboard, синхронные пробы Clipboard.containsImage / containsFiles, асинхронные readTexture / readFiles, и Texture.encodedPNGData() — теперь можно перехватить вставку картинки в редактор и пропустить её через свой импорт, а не позволять GTK воткнуть её как текст.
  • Silencing GTK-CRITICAL спама от GtkScrolledWindow и неверно настроенных GtkDropTarget — опциональный фильтр + правильные сигнатуры сигналов ::enter / ::motion.

1.3.0: macOS как платформа для разработки

Главная новость цикла. swift-adwaita теперь собирается и работает на macOS 13+ на Apple Silicon. Linux остаётся главной целевой платформой, но локально разрабатывать и тестировать можно прямо на маке, не поднимая виртуалку.

  • Установка через Homebrew: brew install libadwaita gtksourceview5 pkgconf adwaita-icon-theme. Без adwaita-icon-theme кнопки в HeaderBar и баннеры рендерятся пустыми — Homebrew не подтягивает её транзитивно.
  • Обязательная переменная окружения: XDG_DATA_DIRS=/opt/homebrew/share, иначе libadwaita не находит свои GSettings-схемы и падает при старте.
  • DemoAppLib — все 78 примеров галереи теперь живут в отдельной библиотеке, которую можно слинковать с внешним приложением. Исполняемый DemoApp стал трёхстрочной обёрткой.
  • Xcode-пример в examples/macos/DemoApp/ — минимальный Xcode 16+ проект, который оборачивает галерею в обычный .app-бандл. Cmd+R и работает.
  • Параллельный набор тестов на XCTest для macOS. swift-testing на Apple-платформах вставляет autorelease-pool переходы между тестами, которые конфликтуют с Cocoa CFRunLoop источниками от gtk_init — на втором тесте всё падает. XCTest этого не делает, и тот же набор там проходит. Linux продолжает гонять swift-testing. Результат: 1181 тест / 0 падений на macOS.
  • Три специфичных для Apple бага, которые Linux/glibc маскировал: Variant.stringValue возвращал nil для валидных строк (висячий указатель на g_variant_type_checked_); хелперы локализации (localized, nlocalized) возвращали мусор без перевода (gettext возвращает входной указатель untouched, а Swift→C bridge уже освободил его); MediaStream.timestamp не компилировался, потому что gint64 — это long на Linux x86_64 и long long на Apple arm64.
  • macOS CI job на macos-26 с Xcode 26.4.1 (Swift 6.3). Только сборка, без прогона тестов: GitHub runner-ы headless, GTK4-Quartz падает без WindowServer-сессии.
  • REUSE 3.3 метаданные лицензий — SPDX-заголовки в каждом файле, reuse lint зелёный.

1.3.1: уборка

Maintenance-релиз без изменений API. Подтянул документацию по перехвату вставки в README, добавил adwaita-icon-theme во все инструкции по установке для macOS (наступили — записали), поднял в Xcode-примере deployment target до macOS 26, чтобы он совпадал с тем, на чём Homebrew собирает GTK4 — иначе линкер ругается на каждый dylib.

Что дальше

Главная незакрытая проблема — это всё ещё интеграция Swift Concurrency с GLib main-loop. Сейчас в GTK-приложении нельзя писать Task { @MainActor in ... } из обработчика клика, и это огорчает. Долгосрочный план — собственный SerialExecutor, который вместо DispatchQueue.main прокидывает работу через g_idle_add_full. Пока что callback-API закрывают все практические сценарии, но писать настоящий исполнитель когда-то всё-таки придётся.

Проект открытый, под MIT-лицензией. Исходники — на GitHub, документация с гайдами — здесь.

Star Fork