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